// SyntaxHighlightingTextView.swift // CxIDE — NSViewRepresentable code editor with real-time syntax highlighting. import SwiftUI import AppKit // MARK: - Syntax Highlighting Text View struct SyntaxHighlightingTextView: NSViewRepresentable { @Binding var text: String let language: EditorTab.Language let theme: IDETheme let fontSize: CGFloat var onTextChange: ((String) -> Void)? func makeCoordinator() -> Coordinator { Coordinator(self) } func makeNSView(context: Context) -> NSScrollView { let scrollView = NSScrollView() scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = true scrollView.drawsBackground = false scrollView.borderType = .noBorder let textView = NSTextView() textView.isEditable = true textView.isSelectable = true textView.isRichText = false textView.allowsUndo = true textView.usesFindPanel = true textView.isAutomaticQuoteSubstitutionEnabled = false textView.isAutomaticDashSubstitutionEnabled = false textView.isAutomaticTextReplacementEnabled = false textView.isAutomaticSpellingCorrectionEnabled = false textView.isContinuousSpellCheckingEnabled = false textView.smartInsertDeleteEnabled = false textView.font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) textView.textColor = NSColor(theme.editorForeground) textView.backgroundColor = NSColor(theme.editorBackground) textView.insertionPointColor = .white textView.selectedTextAttributes = [ .backgroundColor: NSColor(theme.selectionColor) ] textView.textContainerInset = NSSize(width: 4, height: 4) textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) textView.minSize = NSSize(width: 0, height: 0) textView.isVerticallyResizable = true textView.isHorizontallyResizable = false textView.textContainer?.containerSize = NSSize( width: scrollView.contentSize.width, height: CGFloat.greatestFiniteMagnitude ) textView.textContainer?.widthTracksTextView = true textView.delegate = context.coordinator context.coordinator.textView = textView scrollView.documentView = textView // Initial content textView.string = text context.coordinator.applyHighlighting() return scrollView } func updateNSView(_ scrollView: NSScrollView, context: Context) { guard let textView = scrollView.documentView as? NSTextView else { return } let coordinator = context.coordinator // Update font if changed let newFont = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) if textView.font != newFont { textView.font = newFont coordinator.applyHighlighting() } // Update background color textView.backgroundColor = NSColor(theme.editorBackground) textView.insertionPointColor = .white // Update theme reference coordinator.parent = self // Only update text if it changed externally (not from typing) if textView.string != text && !coordinator.isUpdating { coordinator.isUpdating = true let selectedRanges = textView.selectedRanges textView.string = text coordinator.applyHighlighting() textView.selectedRanges = selectedRanges coordinator.isUpdating = false } } // MARK: - Coordinator class Coordinator: NSObject, NSTextViewDelegate { var parent: SyntaxHighlightingTextView weak var textView: NSTextView? var isUpdating = false private var highlightWorkItem: DispatchWorkItem? init(_ parent: SyntaxHighlightingTextView) { self.parent = parent } func textDidChange(_ notification: Notification) { guard let textView = notification.object as? NSTextView, !isUpdating else { return } isUpdating = true parent.text = textView.string parent.onTextChange?(textView.string) isUpdating = false // Debounce highlighting for performance highlightWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self] in DispatchQueue.main.async { self?.applyHighlighting() } } highlightWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: workItem) } // MARK: - Syntax Highlighting func applyHighlighting() { guard let textView = textView, let textStorage = textView.textStorage else { return } let text = textView.string guard !text.isEmpty else { return } let fullRange = NSRange(location: 0, length: (text as NSString).length) let theme = parent.theme let font = NSFont.monospacedSystemFont(ofSize: parent.fontSize, weight: .regular) textStorage.beginEditing() // Reset to default textStorage.setAttributes([ .foregroundColor: NSColor(theme.editorForeground), .font: font ], range: fullRange) let highlighter = SyntaxTokenizer(language: parent.language, theme: theme, font: font) highlighter.tokenize(text: text, in: textStorage) textStorage.endEditing() } } } // MARK: - Syntax Tokenizer struct SyntaxTokenizer { let language: EditorTab.Language let theme: IDETheme let font: NSFont func tokenize(text: String, in storage: NSTextStorage) { let nsText = text as NSString // 1. Comments (must come first — overrides keywords inside comments) highlightPattern(storage, text: nsText, pattern: "//[^\n]*", color: theme.commentColor) highlightPattern(storage, text: nsText, pattern: "/\\*[\\s\\S]*?\\*/", color: theme.commentColor) // 2. Strings highlightPattern(storage, text: nsText, pattern: "\"\"\"[\\s\\S]*?\"\"\"", color: theme.stringColor) highlightPattern(storage, text: nsText, pattern: "\"(?:[^\"\\\\]|\\\\.)*\"", color: theme.stringColor) // 3. Numbers highlightPattern(storage, text: nsText, pattern: "\\b\\d+(\\.\\d+)?\\b", color: theme.numberColor) highlightPattern(storage, text: nsText, pattern: "\\b0x[0-9a-fA-F]+\\b", color: theme.numberColor) // 4. Language-specific keywords let keywords = keywordsForLanguage() if !keywords.isEmpty { let pattern = "\\b(" + keywords.joined(separator: "|") + ")\\b" highlightPattern(storage, text: nsText, pattern: pattern, color: theme.keywordColor) } // 5. Types let types = typesForLanguage() if !types.isEmpty { let pattern = "\\b(" + types.joined(separator: "|") + ")\\b" highlightPattern(storage, text: nsText, pattern: pattern, color: theme.typeColor) } // 6. Preprocessor / annotations switch language { case .swift: highlightPattern(storage, text: nsText, pattern: "@\\w+", color: theme.preprocessorColor) highlightPattern(storage, text: nsText, pattern: "#\\w+", color: theme.preprocessorColor) case .c, .cpp, .header, .objc: highlightPattern(storage, text: nsText, pattern: "#\\w+[^\n]*", color: theme.preprocessorColor) default: break } // 7. Function calls: word followed by ( highlightPattern(storage, text: nsText, pattern: "\\b([a-zA-Z_]\\w*)\\s*(?=\\()", color: theme.functionColor) // 8. Type declarations (capitalized identifiers after class/struct/enum/protocol) highlightPattern(storage, text: nsText, pattern: "(?<=\\b(?:class|struct|enum|protocol|extension|typealias)\\s)\\w+", color: theme.typeColor) } private func highlightPattern(_ storage: NSTextStorage, text: NSString, pattern: String, color: Color) { guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return } let fullRange = NSRange(location: 0, length: text.length) regex.enumerateMatches(in: text as String, options: [], range: fullRange) { match, _, _ in guard let matchRange = match?.range else { return } storage.addAttribute(.foregroundColor, value: NSColor(color), range: matchRange) } } private func keywordsForLanguage() -> [String] { switch language { case .swift: return [ "import", "func", "var", "let", "class", "struct", "enum", "protocol", "extension", "typealias", "init", "deinit", "subscript", "operator", "if", "else", "guard", "switch", "case", "default", "for", "while", "repeat", "do", "catch", "throw", "throws", "rethrows", "try", "return", "break", "continue", "fallthrough", "where", "in", "as", "is", "nil", "true", "false", "self", "Self", "super", "async", "await", "actor", "nonisolated", "isolated", "sending", "public", "private", "internal", "fileprivate", "open", "static", "final", "override", "mutating", "nonmutating", "lazy", "weak", "unowned", "inout", "some", "any", "associatedtype", "convenience", "required", "dynamic", "willSet", "didSet", "get", "set", "defer", "indirect", "precedencegroup", "infix", "prefix", "postfix", "@MainActor", "@Published", "@State", "@Binding", "@ObservedObject", "@StateObject", "@Environment", "@EnvironmentObject", "@ViewBuilder", "@escaping", "@autoclosure", "@discardableResult", "@available", "@objc", "@unchecked", "Sendable", "MainActor" ] case .c, .header: return [ "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "int", "long", "register", "return", "short", "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "inline", "restrict", "_Bool", "_Complex", "_Imaginary", "NULL", "true", "false" ] case .cpp: return [ "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "int", "long", "register", "return", "short", "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "inline", "restrict", "class", "namespace", "template", "typename", "using", "virtual", "public", "private", "protected", "friend", "operator", "new", "delete", "this", "throw", "try", "catch", "const_cast", "dynamic_cast", "reinterpret_cast", "static_cast", "explicit", "mutable", "override", "final", "noexcept", "constexpr", "nullptr", "true", "false", "bool", "wchar_t", "thread_local", "decltype", "alignas", "alignof", "static_assert", "concept", "requires", "co_await", "co_return", "co_yield", "consteval", "constinit" ] case .objc: return [ "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "int", "long", "register", "return", "short", "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "inline", "@interface", "@implementation", "@end", "@protocol", "@property", "@synthesize", "@dynamic", "@optional", "@required", "@class", "@selector", "@encode", "@try", "@catch", "@finally", "@throw", "@autoreleasepool", "@synchronized", "self", "super", "nil", "NULL", "YES", "NO", "true", "false", "id", "instancetype", "SEL", "IMP", "Class", "BOOL", "strong", "weak", "assign", "copy", "retain", "nonatomic", "atomic", "readonly", "readwrite", "nullable", "nonnull" ] case .json: return ["true", "false", "null"] case .markdown, .plaintext: return [] } } private func typesForLanguage() -> [String] { switch language { case .swift: return [ "Int", "Int8", "Int16", "Int32", "Int64", "UInt", "UInt8", "UInt16", "UInt32", "UInt64", "Float", "Double", "Float16", "Float80", "Bool", "String", "Character", "Void", "Never", "Optional", "Result", "Array", "Dictionary", "Set", "AnyObject", "AnyHashable", "AnySequence", "AnyCollection", "Data", "Date", "URL", "UUID", "Error", "Codable", "Encodable", "Decodable", "Hashable", "Equatable", "Comparable", "Identifiable", "CustomStringConvertible", "Sequence", "Collection", "IteratorProtocol", "RangeReplaceableCollection", "View", "ObservableObject", "ObservedObject", "Published", "Color", "Image", "Text", "Button", "VStack", "HStack", "ZStack", "List", "ForEach", "NavigationView", "NavigationLink", "CGFloat", "CGPoint", "CGSize", "CGRect", "NSSize", "DispatchQueue", "Task", "AsyncSequence", "AsyncStream" ] case .cpp: return [ "string", "vector", "map", "unordered_map", "set", "unordered_set", "pair", "tuple", "array", "list", "deque", "queue", "stack", "shared_ptr", "unique_ptr", "weak_ptr", "optional", "variant", "size_t", "ptrdiff_t", "int8_t", "int16_t", "int32_t", "int64_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t", "iostream", "istream", "ostream", "ifstream", "ofstream", "stringstream", "istringstream", "ostringstream" ] default: return [] } } }