diff --git a/Sources/SwiftyMonaco/EditorMarker.swift b/Sources/SwiftyMonaco/EditorMarker.swift new file mode 100644 index 0000000..e1962e5 --- /dev/null +++ b/Sources/SwiftyMonaco/EditorMarker.swift @@ -0,0 +1,24 @@ +// +// EditorMarker.swift +// SwiftyMonaco +// +// Created by Patric Dubois on 22.03.26. +// + +public struct EditorMarker: Equatable { + public enum Severity: String,Equatable { + case error, warning + + } + public let severity: Severity + public let message: String + public let startLine: Int // 1-based + public let endLine: Int // 1-based + + public init(severity: Severity, message: String, startLine: Int, endLine: Int) { + self.severity = severity + self.message = message + self.startLine = startLine + self.endLine = endLine + } +} diff --git a/Sources/SwiftyMonaco/Highlighting/Errors.swift b/Sources/SwiftyMonaco/Highlighting/Errors.swift new file mode 100644 index 0000000..5eab79c --- /dev/null +++ b/Sources/SwiftyMonaco/Highlighting/Errors.swift @@ -0,0 +1,32 @@ +// +// Errors.swift +// SwiftyMonaco +// +// Created by Patric Dubois on 17.03.26. +// + +import Foundation +enum Errors : LocalizedError { + case noWebView, + unsupportedType, + noURL, + javascriptEvaluationFailed(String), + webViewNotAvailable, + unsupportedJavascriptReturnType + var errorDescription: String? { + switch self { + case .noWebView: + return "No webview found" + case .unsupportedType: + return "Unsupported type (supported types are: Array, Bool, Dictionary, Double, Data(UTF8), Date, String, URL)" + case .noURL: + return "No URL set for HTMLTeststep" + case .webViewNotAvailable: + return "Webview not initialized, please file a bug" + case .unsupportedJavascriptReturnType: + return "Unsupported type (supported types are: Array, Bool, Dictionary, Double, Data(UTF8), Date, String, URL )" + case .javascriptEvaluationFailed(let message): + return "JavaScript evaluation failed: \(message)" + } + } +} diff --git a/Sources/SwiftyMonaco/Highlighting/LanguageSupportList.swift b/Sources/SwiftyMonaco/Highlighting/LanguageSupportList.swift new file mode 100644 index 0000000..3b750b8 --- /dev/null +++ b/Sources/SwiftyMonaco/Highlighting/LanguageSupportList.swift @@ -0,0 +1,73 @@ +// +// MonacoLanguages.swift +// SwiftyMonaco +// +// Created by Patric Dubois on 17.03.26. +// +import Foundation + + +/// LanguageSupportList +/// The Monaco editor comes with a list of predefined languages (support for code completions and syntax highlighting. +/// The SwiftyMonaco package comes with an additional list of predefined languages, which supports custom syntax highlighting +/// Adopters of this package may include further Language support. +/// Each of the three may override the definitions of the other. +/// The flags on this struct define how definitions are composed to hold a unique list of LanguageSupport definitions. +/// Predefined settings: +/// - include all monaco editor definitions as their support is proven. +/// - SwiftyMonaco-predefined-Settings are not taken into account as they have no additional functionality and serve more as an example +/// - Custom LanguageSupport will override Monaco language support which means, when you define a Monarch definition, it will be registered with the Monaco editor and override an existing definition. +public struct LanguageSupportList : Codable { + + + public static let javascript = """ + + var languages = monaco.languages.getLanguages(); + var monacoLanguages = []; + for (var i = 0;i Bool { + lhs.id < rhs.id + } + + public static func == (lhs: LanguageSupport, rhs: LanguageSupport) -> Bool { + return lhs.id == rhs.id + } + public init(id: String, extensions: [String]?, aliases: [String]?, mimeTypes: [String]?) { + self.id = id + self.extensions = extensions + self.aliases = aliases + self.mimeTypes = mimeTypes + + } + public let id : String + public let extensions : [String]? + public let aliases: [String]? + public let mimeTypes: [String]? + + public var encoded : String { + do { + let jsonEncoder = JSONEncoder() + let data = try jsonEncoder.encode(self) + let text = String(data:data, encoding: .utf8) ?? "" + return text + } + catch { + + return "" + } + } + +} diff --git a/Sources/SwiftyMonaco/Highlighting/SyntaxHighlight.swift b/Sources/SwiftyMonaco/Highlighting/SyntaxHighlight.swift index ff7ae83..8d3a2af 100644 --- a/Sources/SwiftyMonaco/Highlighting/SyntaxHighlight.swift +++ b/Sources/SwiftyMonaco/Highlighting/SyntaxHighlight.swift @@ -6,8 +6,9 @@ // import Foundation +import UniformTypeIdentifiers -public struct SyntaxHighlight { +public struct SyntaxHighlight : Codable, Hashable { public init(title: String, configuration: String) { self.title = title self.configuration = configuration @@ -20,6 +21,7 @@ public struct SyntaxHighlight { public var title: String public var configuration: String + } public extension SyntaxHighlight { diff --git a/Sources/SwiftyMonaco/JSONExtensions.swift b/Sources/SwiftyMonaco/JSONExtensions.swift new file mode 100644 index 0000000..472fb23 --- /dev/null +++ b/Sources/SwiftyMonaco/JSONExtensions.swift @@ -0,0 +1,245 @@ +// +// JSONExtensions.swift +// SwiftyMonaco +// +// Created by Patric Dubois on 17.03.26. +// +import Foundation + +extension String { + func escapeSpecialCharacters() -> String { + var newText = self + if self.contains("This is much better"){ + + } + newText = newText.replacingOccurrences(of: "\"", with: "\\\"") + //newText = newText.replacingOccurrences(of: "\\", with: "\\\\") + newText = newText.replacingOccurrences(of: "\n", with: "\\n") + newText = newText.replacingOccurrences(of: "\t", with: "\\t") + newText = newText.replacingOccurrences(of: "\r", with: "\\r") + return newText + } +} + +extension Any? { + public var jsonString : String { + if let text = self as? String { + + return text.escapeSpecialCharacters() + } + else if let dict = self as? [String:Any?] { + return dict.jsonString + } + + else if let array = self as? [Any] { + return array.jsonString + } + else if let bool = self as? Bool { + return String(bool) + } + else if let number = self as? Int { + return String(number) + } + else if let number = self as? Float { + return String(number) + } + else if let number = self as? Double { + return String(number) + } + else if let data = self as? Data { + return String(data : data, encoding: .utf8) ?? "" + } + else { + return "" + } + } + + +// public var arrayStringValue : String { +// if let text = self as? String { +// return text +// } +// else if let dict = self as? [String:Any] { +// return dict.arrayStringValue +// } +// else if let array = self as? [Any] { +// return array.arrayStringValue +// } +// else { +// return "" +// } +// } +} + + +import Foundation + +public typealias JsonAny = Any? +public typealias JsonArray = [JsonAny] +public typealias JsonDictionary = [String: JsonAny] + + +extension JsonAny { + + + func toBool() -> Bool? { + + switch self { + case let bool as Bool: + return bool + case let int as Int: + return int == 0 ? false : true + case let double as Double: + return double == 0 ? false : true + case let string as String: + return string == "true" + + default: + return nil + } + } + + + func toDouble() -> Double? { + switch self { + case let double as Double: + return double + case let int as Int: + return Double(int) + default: + return nil + } + } + + + func toInt() -> Int? { + switch self { + case let double as Double: + return Int(double) + case let int as Int: + return int + default: + return nil + } + } + + + func toString() -> String? { + switch self { + case let string as String: + return string + + default: + return nil + } + } +} + +extension Dictionary where Key == String, Value == Any?{ + public var jsonString : String { + var resultString = "{" + self.forEach { element in + + resultString.append("\"") + resultString.append(element.key) + resultString.append("\"") + resultString.append(":") + if let dict = element.value as? [String: Any?] { + + resultString.append(dict.jsonString) + resultString.append(",") + } + else if let dict = element.value as? [String: Any?] { + + resultString.append(dict.jsonString) + resultString.append(",") + } + else if let dict = element.value as? [Any] { + resultString.append(dict.jsonString) + resultString.append(",") + } + else if let stringValue = element.value as? String { + + resultString = resultString.appending("\"").appending(stringValue.escapeSpecialCharacters()).appending("\"") + resultString.append(",") + } + else if let number = element.value as? Int { + resultString = resultString.appending(String(number)) + resultString.append(",") + } + else if let number = element.value as? Double { + resultString = resultString.appending(String(number)) + resultString.append(",") + } + else if let bool = element.value as? Bool { + resultString = resultString.appending(String(bool)) + resultString.append(",") + } + + + } + // das führendes Komma entfernen + if resultString.count > 1 && self.count > 0 { + let _ = resultString.removeLast() + } + resultString.append("}") + return resultString + } +} +extension Array where Element == Any { + public var jsonString : String { + var resultString = "[" + + self.forEach { element in + if let textElement = element as? String { + resultString.append("\"") + resultString.append(textElement.escapeSpecialCharacters()) + resultString.append("\"") + resultString.append(",") + } + else if let dict = element as? [String: Any?] { + resultString.append(dict.jsonString) + resultString.append(",") + } + else if let dict = element as? [Any] { + resultString.append(dict.jsonString) + resultString.append(",") + } + else if let number = element as? Int { + resultString = resultString.appending(String(number)) + resultString.append(",") + } + else if let number = element as? Double { + resultString = resultString.appending(String(number)) + resultString.append(",") + } + else if let bool = element as? Bool { + resultString = resultString.appending(String(bool)) + resultString.append(",") + } + + + } + if resultString.count > 1 && self.count > 0 { + let _ = resultString.removeLast() + } + resultString.append("]") + return resultString + } + +} +extension Array where Element == JsonArray { + public var stringValue : String { + if let content = self.first { + return (content as [Any]).jsonString + } + else { + return "" + } + + + + } + +} + diff --git a/Sources/SwiftyMonaco/JavascriptStringResult.swift b/Sources/SwiftyMonaco/JavascriptStringResult.swift new file mode 100644 index 0000000..6d37df6 --- /dev/null +++ b/Sources/SwiftyMonaco/JavascriptStringResult.swift @@ -0,0 +1,78 @@ +// +// JavascriptStringResult.swift +// SwiftyMonaco +// +// Created by Patric Dubois on 17.03.26. +// from the Sextant-Project +// + + +// +// JavascriptStringResult.swift +// APIJockey TEST +// +// Created by Patric Dubois on 03.10.25. +// + +import Foundation +public struct JavascriptStringResult { + + + let positiveFormat : String? + let dateFormatting : String? + enum Errors : LocalizedError { + case unsupportedType + var errorDescription: String? { + switch self { + case .unsupportedType: + return "Unsupported type (supported types are: Array, Bool, Dictionary, Double, Data(UTF8), Date, String, URL)" + } + } + } + func javascriptAnyToString(javascriptResult : Any?) async -> Result { + + + if let javascriptResultAsString = javascriptResult as? String { + return .success(javascriptResultAsString) + } + else if let javascriptResultAsDouble = javascriptResult as? Double { + let numberFormatter = NumberFormatter() + numberFormatter.positiveFormat = self.positiveFormat + if let formatting = self.positiveFormat, + !formatting.isEmpty { + numberFormatter.negativeFormat = "-\(formatting)" + + } + return .success(numberFormatter.string(for: javascriptResultAsDouble) ?? String(javascriptResultAsDouble)) + } + else if let javascriptResultAsBool = javascriptResult as? Bool { + return .success(String(javascriptResultAsBool)) + } + else if let javascriptResultAsArray = javascriptResult as? [Any] { + return .success(javascriptResultAsArray.jsonString) + } + else if let javascriptResultAsDictionary = javascriptResult as? [String: Any?] { + return .success(javascriptResultAsDictionary.jsonString) + } + else if let javascriptResultAsData = javascriptResult as? Data, + let text = String(data: javascriptResultAsData, encoding: .utf8){ + return .success(text) + } + else if let javascriptResultAsURL = javascriptResult as? URL { + return .success(javascriptResultAsURL.absoluteString) + } + else if let javascriptResultAsDate = javascriptResult as? Date { + let dateformatter = DateFormatter() + dateformatter.dateFormat = self.dateFormatting ?? "yyyy-MM-dd HH:mm:ss.SSS" + return .success(dateformatter.string(from: javascriptResultAsDate)) + } + else if javascriptResult as? Never != nil{ + return .failure(Self.Errors.unsupportedType) + } + else { + return .failure(Self.Errors.unsupportedType) + } + + } + +} diff --git a/Sources/SwiftyMonaco/MonacoViewController.swift b/Sources/SwiftyMonaco/MonacoViewController.swift index bc94a69..f952b3a 100644 --- a/Sources/SwiftyMonaco/MonacoViewController.swift +++ b/Sources/SwiftyMonaco/MonacoViewController.swift @@ -13,12 +13,21 @@ import UIKit public typealias ViewController = UIViewController #endif import WebKit - +import OSLog public class MonacoViewController: ViewController, WKUIDelegate, WKNavigationDelegate { - + var logger = Logger(subsystem: "MonacViewController", category: "SwiftyMonaco") var delegate: MonacoViewControllerDelegate? - + var supportedLanguages : [LanguageSupport] = [] var webView: WKWebView! + private(set) var isEditorReady = false + private var appliedLanguageId: String? = nil + /// Tracks the last text value known to Monaco, to avoid pushing text back when Monaco itself triggered the change. + var lastTextFromMonaco: String = "" + /// Prevents duplicate in-flight JS calls when updateNSViewController fires multiple times before the async JS completes. + private var pendingTextUpdate: String? = nil + private var customLanguagesRegistered = false + + public override func loadView() { let webConfiguration = WKWebViewConfiguration() @@ -40,6 +49,7 @@ public class MonacoViewController: ViewController, WKUIDelegate, WKNavigationDel super.viewDidLoad() loadMonaco() + } private func loadMonaco() { @@ -47,7 +57,183 @@ public class MonacoViewController: ViewController, WKUIDelegate, WKNavigationDel let myRequest = URLRequest(url: myURL!) webView.load(myRequest) } - + public func updateLanguage() { + guard let languageSupport = self.delegate?.monacoView(getLanguageSupport: self) else { return } + let languageId = languageSupport.id + guard languageId != appliedLanguageId else { return } + guard isEditorReady else { return } + + let javascript = """ + (function(){ + if (typeof editor !== 'undefined' && editor.editor) { + editor.addAction(function(monaco, editorInstance) { + var model = editorInstance.getModel(); + if (model) { + monaco.editor.setModelLanguage(model, '\(languageId)'); + } + }); + return true; + } + return false; + })() + """ + webView.evaluateJavaScript(javascript, in: nil, in: .page) { [weak self] result in + if case .success(let value) = result, value as? Bool == true { + self?.appliedLanguageId = languageId + } + if case .failure(let error) = result { + self?.logger.error("Failed to update language \(error.localizedDescription)") + } + } + } + public func registerCustomLanguages() { + guard isEditorReady, !customLanguagesRegistered else { return } + let specs = self.delegate?.monacoView(getCustomLanguageSpecs: self) ?? [:] + guard !specs.isEmpty else { + customLanguagesRegistered = true + return + } + + + let registrationJS = specs.map { (languageSupport, spec) in + let encodedLanguageSupport = languageSupport.encoded + return """ + (function() { + var existingLangs = monaco.languages.getLanguages(); + var alreadyRegistered = existingLangs.some(function(l) { return l.id === '\(languageSupport.id)'; }); + if (!alreadyRegistered) { + monaco.languages.register(\(encodedLanguageSupport)); + monaco.languages.setMonarchTokensProvider('\(languageSupport.id)', \(spec)); + } + })(); + """ + }.joined(separator: "\n") + ////{ id: '\(languageId)' } + let javascript = """ + (function(){ + if (typeof editor !== 'undefined' && editor.editor) { + editor.addAction(function(monaco, editorInstance) { + \(registrationJS) + }); + return true; + } + return false; + })() + """ + webView.evaluateJavaScript(javascript, in: nil, in: .page) { [weak self] result in + if case .success(let value) = result, value as? Bool == true { + self?.customLanguagesRegistered = true + self?.updateLanguages() + } + if case .failure(let error) = result { + self?.logger.error("registerCustomLanguages failed: \(error.localizedDescription)") + } + } + } + public func updateText() { + + guard isEditorReady else { return } + let newText = self.delegate?.monacoView(readText: self) ?? "" + + guard newText != lastTextFromMonaco else { return } + + guard pendingTextUpdate != newText else { return } + pendingTextUpdate = newText + let b64 = newText.data(using: .utf8)?.base64EncodedString() ?? "" + let javascript = """ + (function(){ + if (typeof editor !== 'undefined' && editor.editor) { + editor.setText(atob('\(b64)')); + return true; + } + return false; + })() + """ + webView.evaluateJavaScript(javascript, in: nil, in: .page) { [weak self, newText] result in + self?.pendingTextUpdate = nil + switch result { + case .success(let value): + if value as? Bool == true { + self?.lastTextFromMonaco = newText + + } else { + + } + case .failure(let error): + self?.logger.error("SwiftyMonaco: updateText JS failed: \(error.localizedDescription)") + } + } + } + public func updateMarkers() { + guard isEditorReady else { return } + let markers = self.delegate?.monacoView(getMarkers: self) ?? [] + + let markersJS = markers.map { marker -> String in + let severityValue = marker.severity == .error ? 8 : 4 + let startLine = max(1, marker.startLine) + let endLine = max(startLine, marker.endLine) + let escapedMessage = marker.message + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "'", with: "\\'") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "") + return "{ severity: \(severityValue), startLineNumber: \(startLine), startColumn: 1, endLineNumber: \(endLine), endColumn: 10000, message: '\(escapedMessage)' }" + }.joined(separator: ",") + + let javascript = """ + (function(){ + if (typeof editor !== 'undefined' && editor.editor) { + editor.addAction(function(monaco, editorInstance) { + var model = editorInstance.getModel(); + if (model) { + monaco.editor.setModelMarkers(model, 'validation', [\(markersJS)]); + } + }); + return true; + } + return false; + })() + """ + webView.evaluateJavaScript(javascript, in: nil, in: .page) { result in + + switch result { + case .success: + return + case .failure(let error): + self.logger.error("Failed to update markers: \(error.localizedDescription)") + } + } + } + public func updateLanguages() { + + Task { + + + + let result = await readWithJavaScript(string: LanguageSupportList.javascript) + switch result { + case .failure(let error): + handleError(javascript: LanguageSupportList.javascript, message: error.localizedDescription) + break + case .success(let result): + guard let data = result.data(using: .utf8) else { + handleError(javascript: LanguageSupportList.javascript, message: "invalid UTF-8 String for response") + break + } + let jsonDecoder = JSONDecoder() + do { + let monacoLanguages = try jsonDecoder.decode([LanguageSupport].self, from: data) + self.supportedLanguages = monacoLanguages + self.delegate?.monacoView(updateLanguageSupport: self.supportedLanguages, controller: self) + } + catch { + handleError(javascript: LanguageSupportList.javascript, message: "Failed to decode JSON: \(error.localizedDescription)") + } + + } + } + + } // MARK: - Dark Mode private func updateTheme() { evaluateJavascript(""" @@ -86,21 +272,43 @@ public class MonacoViewController: ViewController, WKUIDelegate, WKNavigationDel } #endif } + private func handleError(javascript : String, message : String) { + #if os(macOS) + let alert = NSAlert() + alert.messageText = "Error" + alert.informativeText = "Something went wrong while evaluating \(message): \(javascript)" + alert.alertStyle = .critical + alert.addButton(withTitle: "OK") + alert.runModal() + #else + let alert = UIAlertController(title: "Error", message: "Something went wrong while evaluating \(message): \(javascript)", preferredStyle: .alert) + alert.addAction(.init(title: "OK", style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + #endif + } // MARK: - WKWebView public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + + updateLanguages() + // Syntax Highlighting - let syntax = self.delegate?.monacoView(getSyntax: self) - let syntaxJS = syntax != nil ? """ - // Register a new language - monaco.languages.register({ id: 'mySpecialLanguage' }); - - // Register a tokens provider for the language - monaco.languages.setMonarchTokensProvider('mySpecialLanguage', (function() { - \(syntax!.configuration) - })()); - """ : "" - let syntaxJS2 = syntax != nil ? ", language: 'mySpecialLanguage'" : "" + //let syntax = self.delegate?.monacoView(getSyntax: self) + var theme = detectTheme() + + if let _theme = self.delegate?.monacoView(getTheme: self) { + switch _theme { + case .light: + theme = "vs" + case .dark: + theme = "vs-dark" + } + } + var syntax = "" + if let languageSupport = self.delegate?.monacoView(getLanguageSupport: self) { + + syntax = "language: '\(languageSupport.id)'," + } // Minimap let _minimap = self.delegate?.monacoView(getMinimap: self) @@ -122,16 +330,7 @@ public class MonacoViewController: ViewController, WKUIDelegate, WKNavigationDel let _fontSize = self.delegate?.monacoView(getFontSize: self) let fontSize = "fontSize: \(_fontSize ?? 12)" - var theme = detectTheme() - - if let _theme = self.delegate?.monacoView(getTheme: self) { - switch _theme { - case .light: - theme = "vs" - case .dark: - theme = "vs-dark" - } - } + // Code itself @@ -140,16 +339,37 @@ public class MonacoViewController: ViewController, WKUIDelegate, WKNavigationDel let javascript = """ (function() { - \(syntaxJS) + \(syntax) - editor.create({value: atob('\(b64 ?? "")'), automaticLayout: true, theme: "\(theme)"\(syntaxJS2), \(minimap), \(scrollbar), \(smoothCursor), \(cursorBlink), \(fontSize)}); - var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta); + editor.create({value: atob('\(b64 ?? "")'), + automaticLayout: true, + theme: "\(theme)", + \(syntax) + \(minimap), + \(scrollbar), + \(smoothCursor), + \(cursorBlink), + \(fontSize)}); + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width'); + document.getElementsByTagName('head')[0].appendChild(meta); return true; })(); """ - evaluateJavascript(javascript) + webView.evaluateJavaScript(javascript, in: nil, in: .page) { [weak self] result in + switch result { + case .success: + self?.isEditorReady = true + self?.lastTextFromMonaco = text + self?.registerCustomLanguages() + self?.updateLanguage() + case .failure(let error): + self?.logger.error("Monaco editor initialization failed: \(error.localizedDescription)") + } + } } - + private func evaluateJavascript(_ javascript: String) { webView.evaluateJavaScript(javascript, in: nil, in: WKContentWorld.page) { result in @@ -169,33 +389,98 @@ public class MonacoViewController: ViewController, WKUIDelegate, WKNavigationDel #endif break case .success(_): + break } } } + private func readWithJavaScript(string javaScriptString: String) async -> Result{ + + + do { + let value = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + webView.evaluateJavaScript(javaScriptString) { result, error in + if let error = error { + continuation.resume(throwing: error) + return + } else { + continuation.resume(returning: result) + return + } + } + } + let javascriptResult = await JavascriptStringResult(positiveFormat: nil, dateFormatting: nil).javascriptAnyToString(javascriptResult: value) + switch javascriptResult { + case .success(let success): + return .success(success) + case .failure(let failure): + return .failure(.javascriptEvaluationFailed(failure.localizedDescription)) + } + } + catch { + return .failure(.javascriptEvaluationFailed((error as NSError).debugDescription)) + } + } } // MARK: - Handler private extension MonacoViewController { final class UpdateTextScriptHandler: NSObject, WKScriptMessageHandler { + private var debounceTimer: Timer? + private var lastValidText: String = "" // Speichert den letzten gültigen Inhalt private let parent: MonacoViewController - + init(_ parent: MonacoViewController) { self.parent = parent } - + func userContentController( _ userContentController: WKUserContentController, didReceive message: WKScriptMessage - ) { - guard let encodedText = message.body as? String, - let data = Data(base64Encoded: encodedText), - let text = String(data: data, encoding: .utf8) else { - fatalError("Unexpected message body") + ) { + // 1. Base64-String prüfen + guard let base64 = message.body as? String, !base64.isEmpty else { + + return + } + + // 2. Base64-Padding prüfen und korrigieren + let paddedBase64 = base64.padding( + toLength: ((base64.count + 3) / 4) * 4, + withPad: "=", + startingAt: 0 + ) + + // 3. Daten dekodieren + guard let data = Data(base64Encoded: paddedBase64) else { + + return + } + + // 4. UTF-8-String erstellen (mit Fallback) + if let text = String(data: data, encoding: .utf8) { + // 5. Debouncing: Timer zurücksetzen + debounceTimer?.invalidate() + debounceTimer = Timer.scheduledTimer( + withTimeInterval: 0.5, // 500 ms Verzögerung + repeats: false + ) { _ in + DispatchQueue.main.async { + self.lastValidText = text // Speichere den gültigen Inhalt + self.parent.lastTextFromMonaco = text + self.parent.delegate?.monacoView(controller: self.parent, textDidChange: self.lastValidText) + } + } + } else { + + // Optional: Letzten gültigen Inhalt erneut senden + DispatchQueue.main.async { + if !self.lastValidText.isEmpty { + self.parent.delegate?.monacoView(controller: self.parent, textDidChange: self.lastValidText) + } + } } - - parent.delegate?.monacoView(controller: parent, textDidChange: text) } } } @@ -212,4 +497,8 @@ public protocol MonacoViewControllerDelegate { func monacoView(getFontSize controller: MonacoViewController) -> Int func monacoView(getTheme controller: MonacoViewController) -> Theme? func monacoView(controller: MonacoViewController, textDidChange: String) + func monacoView(getLanguageSupport controller: MonacoViewController) -> LanguageSupport? + func monacoView(getCustomLanguageSpecs controller: MonacoViewController) -> [LanguageSupport: String] + func monacoView(getMarkers controller: MonacoViewController) -> [EditorMarker] + mutating func monacoView(updateLanguageSupport : [LanguageSupport],controller: MonacoViewController) } diff --git a/Sources/SwiftyMonaco/SwiftyMonaco.swift b/Sources/SwiftyMonaco/SwiftyMonaco.swift index 15495ed..8a37512 100644 --- a/Sources/SwiftyMonaco/SwiftyMonaco.swift +++ b/Sources/SwiftyMonaco/SwiftyMonaco.swift @@ -13,19 +13,25 @@ typealias ViewControllerRepresentable = NSViewControllerRepresentable typealias ViewControllerRepresentable = UIViewControllerRepresentable #endif + public struct SwiftyMonaco: ViewControllerRepresentable, MonacoViewControllerDelegate { + + + + var id = UUID() + + var languageSupport : Binding<[LanguageSupport]>? var text: Binding - private var syntax: SyntaxHighlight? - private var _minimap: Bool = true - private var _scrollbar: Bool = true - private var _smoothCursor: Bool = false - private var _cursorBlink: CursorBlink = .blink - private var _fontSize: Int = 12 - private var _theme: Theme? = nil - - public init(text: Binding) { + var isDirty: Binding + var config : SwiftyMonacoConfig + + + public init(text: Binding, isDirty : Binding, config: SwiftyMonacoConfig, languageSupport: Binding<[LanguageSupport]>) { self.text = text + self.isDirty = isDirty + self.config = config + self.languageSupport = languageSupport } #if os(macOS) @@ -36,6 +42,11 @@ public struct SwiftyMonaco: ViewControllerRepresentable, MonacoViewControllerDel } public func updateNSViewController(_ nsViewController: MonacoViewController, context: Context) { + nsViewController.delegate = self + nsViewController.registerCustomLanguages() + nsViewController.updateLanguage() + nsViewController.updateText() + nsViewController.updateMarkers() } #endif @@ -57,42 +68,56 @@ public struct SwiftyMonaco: ViewControllerRepresentable, MonacoViewControllerDel public func monacoView(controller: MonacoViewController, textDidChange text: String) { self.text.wrappedValue = text + self.isDirty.wrappedValue = true } public func monacoView(getSyntax controller: MonacoViewController) -> SyntaxHighlight? { - return syntax + return config.syntax } public func monacoView(getMinimap controller: MonacoViewController) -> Bool { - return _minimap + return config.minimap } public func monacoView(getScrollbar controller: MonacoViewController) -> Bool { - return _scrollbar + return config.scrollbar } public func monacoView(getSmoothCursor controller: MonacoViewController) -> Bool { - return _smoothCursor + return config.smoothCursor } public func monacoView(getCursorBlink controller: MonacoViewController) -> CursorBlink { - return _cursorBlink + return config.cursorBlink } public func monacoView(getFontSize controller: MonacoViewController) -> Int { - return _fontSize + return config.fontSize } public func monacoView(getTheme controller: MonacoViewController) -> Theme? { - return _theme + return config.theme } + public func monacoView(getLanguageSupport controller: MonacoViewController) -> LanguageSupport? { + return config.monacoLanguage + } + public func monacoView(getCustomLanguageSpecs controller: MonacoViewController) -> [LanguageSupport: String] { + return config.customLanguageSpecs + } + public func monacoView(getMarkers controller: MonacoViewController) -> [EditorMarker] { + return config.markers + } + public mutating func monacoView(updateLanguageSupport: [LanguageSupport], controller: MonacoViewController) { + self.languageSupport?.wrappedValue = updateLanguageSupport + } + } // MARK: - Modifiers public extension SwiftyMonaco { func syntaxHighlight(_ syntax: SyntaxHighlight) -> Self { var m = self - m.syntax = syntax + m.config.syntax = syntax return m } } @@ -100,7 +125,7 @@ public extension SwiftyMonaco { public extension SwiftyMonaco { func minimap(_ enabled: Bool) -> Self { var m = self - m._minimap = enabled + m.config.minimap = enabled return m } } @@ -108,7 +133,7 @@ public extension SwiftyMonaco { public extension SwiftyMonaco { func scrollbar(_ enabled: Bool) -> Self { var m = self - m._scrollbar = enabled + m.config.scrollbar = enabled return m } } @@ -116,7 +141,7 @@ public extension SwiftyMonaco { public extension SwiftyMonaco { func smoothCursor(_ enabled: Bool) -> Self { var m = self - m._smoothCursor = enabled + m.config.smoothCursor = enabled return m } } @@ -124,7 +149,7 @@ public extension SwiftyMonaco { public extension SwiftyMonaco { func cursorBlink(_ style: CursorBlink) -> Self { var m = self - m._cursorBlink = style + m.config.cursorBlink = style return m } } @@ -132,7 +157,7 @@ public extension SwiftyMonaco { public extension SwiftyMonaco { func fontSize(_ size: Int) -> Self { var m = self - m._fontSize = size + m.config.fontSize = size return m } } @@ -140,7 +165,7 @@ public extension SwiftyMonaco { public extension SwiftyMonaco { func theme(_ theme: Theme) -> Self { var m = self - m._theme = theme + m.config.theme = theme return m } } diff --git a/Sources/SwiftyMonaco/SwiftyMonacoConfig.swift b/Sources/SwiftyMonaco/SwiftyMonacoConfig.swift new file mode 100644 index 0000000..48604b9 --- /dev/null +++ b/Sources/SwiftyMonaco/SwiftyMonacoConfig.swift @@ -0,0 +1,36 @@ +// +// SwiftyMonacoConfig.swift +// SwiftyMonaco +// +// Created by Patric Dubois on 19.03.26. +// + + +public struct SwiftyMonacoConfig { + public var syntax: SyntaxHighlight? + public var minimap: Bool = true + public var scrollbar: Bool = true + public var smoothCursor: Bool = false + public var updatedText : String? + public var cursorBlink: CursorBlink = .blink + public var fontSize: Int = 12 + public var monacoLanguage : LanguageSupport? = nil + public var theme: Theme? = nil + public var customLanguageSpecs: [LanguageSupport: String] = [:] + public var markers: [EditorMarker] = [] + public init(minimap: Bool, scrollbar: Bool, smoothCursor: Bool, cursorBlink: CursorBlink, fontSize: Int, monacoLanguage: LanguageSupport? = nil, theme: Theme? = nil, customLanguageSpecs: [LanguageSupport: String] = [:], markers: [EditorMarker] = []) { + + self.minimap = minimap + self.scrollbar = scrollbar + self.smoothCursor = smoothCursor + self.cursorBlink = cursorBlink + self.fontSize = fontSize + self.monacoLanguage = monacoLanguage + self.theme = theme + self.customLanguageSpecs = customLanguageSpecs + self.markers = markers + } + public init() { + + } +}