diff --git a/CHANGELOG.md b/CHANGELOG.md index c70881ec..bd7b84ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Replace CodeEditSourceEditor/CodeEditTextView (252 files, 25K+ LOC third-party) with native NSTextView-based SQL editor (~1,500 LOC) +- Horizontal scrolling now uses Apple's standard NSTextContainer configuration instead of workarounds +- Find/Replace uses NSTextView's built-in Find Bar instead of custom implementation +- Syntax highlighting uses NSLayoutManager temporary attributes (Apple-recommended pattern) +- Line numbers use standard NSRulerView instead of custom CoreText gutter + +### Fixed + +- Horizontal scrolling broken in SQL editor due to upstream variable shadowing bug (#448) +- Home/End keys not working in SQL editor (#448) + ## [0.24.2] - 2026-03-26 ### Fixed diff --git a/LocalPackages/CodeEditLanguages/Package.swift b/LocalPackages/CodeEditLanguages/Package.swift deleted file mode 100644 index e3c7afd0..00000000 --- a/LocalPackages/CodeEditLanguages/Package.swift +++ /dev/null @@ -1,32 +0,0 @@ -// swift-tools-version: 5.9 -import PackageDescription - -let package = Package( - name: "CodeEditLanguages", - platforms: [.macOS(.v13)], - products: [ - .library(name: "CodeEditLanguages", targets: ["CodeEditLanguages"]) - ], - dependencies: [ - .package(url: "https://github.com/ChimeHQ/SwiftTreeSitter.git", from: "0.9.0") - ], - targets: [ - .target( - name: "TreeSitterGrammars", - path: "Sources/TreeSitterGrammars", - publicHeadersPath: "include", - cSettings: [ - .headerSearchPath("vendored-headers") - ] - ), - .target( - name: "CodeEditLanguages", - dependencies: [ - "TreeSitterGrammars", - .product(name: "SwiftTreeSitter", package: "SwiftTreeSitter") - ], - resources: [.copy("Resources")], - linkerSettings: [] - ) - ] -) diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage+Definitions.swift b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage+Definitions.swift deleted file mode 100644 index 5124aea0..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage+Definitions.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// CodeLanguage+Definitions.swift -// -// -// Created by Lukas Pistrol on 15.01.23. -// - -import Foundation - -public extension CodeLanguage { - - /// An array of all language structures. - static let allLanguages: [CodeLanguage] = [ - .bash, - .javascript, - .jsx, - .sql - ] - - /// A language structure for `Bash` - static let bash: CodeLanguage = .init( - id: .bash, - tsName: "bash", - extensions: ["sh", "bash"], - lineCommentString: "#", - rangeCommentStrings: (":'", "'") - ) - - /// A language structure for `HTML` - static let html: CodeLanguage = .init( - id: .html, - tsName: "html", - extensions: ["html", "htm", "shtml"], - lineCommentString: "", - rangeCommentStrings: (""), - highlights: ["injections"] - ) - - /// A language structure for `JavaScript` - static let javascript: CodeLanguage = .init( - id: .javascript, - tsName: "javascript", - extensions: ["js", "cjs", "mjs"], - lineCommentString: "//", - rangeCommentStrings: ("/*", "*/"), - documentationCommentStrings: [.pair(("/**", "*/"))], - highlights: ["injections"], - additionalIdentifiers: ["node", "deno"] - ) - - /// A language structure for `JSDoc` - static let jsdoc: CodeLanguage = .init( - id: .jsdoc, - tsName: "jsdoc", - extensions: [], - lineCommentString: "", - rangeCommentStrings: ("/**", "*/") - ) - - /// A language structure for `JSX` - static let jsx: CodeLanguage = .init( - id: .jsx, - tsName: "javascript", - extensions: ["jsx"], - lineCommentString: "//", - rangeCommentStrings: ("/*", "*/"), - highlights: ["highlights-jsx", "injections"] - ) - - /// A language structure for `SQL` - static let sql: CodeLanguage = .init( - id: .sql, - tsName: "sql", - extensions: ["sql"], - lineCommentString: "--", - rangeCommentStrings: ("/*", "*/") - ) - - /// A language structure for `TSX` - static let tsx: CodeLanguage = .init( - id: .tsx, - tsName: "typescript", - extensions: ["tsx"], - lineCommentString: "//", - rangeCommentStrings: ("/*", "*/"), - parentURL: CodeLanguage.jsx.queryURL - ) - - /// A language structure for `Typescript` - static let typescript: CodeLanguage = .init( - id: .typescript, - tsName: "typescript", - extensions: ["ts", "cts", "mts"], - lineCommentString: "//", - rangeCommentStrings: ("/*", "*/"), - parentURL: CodeLanguage.javascript.queryURL - ) - - /// The default language (plain text) - static let `default`: CodeLanguage = .init( - id: .plainText, - tsName: "PlainText", - extensions: ["txt"], - lineCommentString: "", - rangeCommentStrings: ("", "") - ) -} diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage+DetectLanguage.swift b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage+DetectLanguage.swift deleted file mode 100644 index ffe5df70..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage+DetectLanguage.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// CodeLanguage+DetectLanguage.swift -// CodeEditLanguages -// -// Created by Khan Winter on 6/17/23. -// - -import Foundation -import RegexBuilder - -public extension CodeLanguage { - - /// Gets the corresponding language for the given file URL - /// - /// Uses the `pathExtension` URL component to detect the language - /// - Returns: A language structure - /// - Parameters: - /// - url: The URL to get the language for. - /// - prefixBuffer: The first few lines of the document. - /// - suffixBuffer: The last few lines of the document. - static func detectLanguageFrom(url: URL, prefixBuffer: String? = nil, suffixBuffer: String? = nil) -> CodeLanguage { - if let urlLanguage = detectLanguageUsingURL(url: url) { - return urlLanguage - } else if let prefixBuffer, - let shebangLanguage = detectLanguageUsingShebang(contents: prefixBuffer.lowercased()) { - return shebangLanguage - } else if let prefixBuffer, - let modelineLanguage = detecLanguageUsingModeline( - prefixBuffer: prefixBuffer.lowercased(), - suffixBuffer: suffixBuffer?.lowercased() - ) { - return modelineLanguage - } else { - return .default - } - } - - /// Detects a file's language using the file url. - /// - Parameter url: The URL of the file. - /// - Returns: The detected code language, if any. - private static func detectLanguageUsingURL(url: URL) -> CodeLanguage? { - let fileExtension = url.pathExtension.lowercased() - let fileName = url.pathComponents.last // should not be lowercase since it has to match e.g. `Dockerfile` - // This is to handle special file types without an extension (e.g., Makefile, Dockerfile) - let fileNameOrExtension = fileExtension.isEmpty ? (fileName != nil ? fileName! : "") : fileExtension - if let lang = allLanguages.first(where: { lang in lang.extensions.contains(fileNameOrExtension)}) { - return lang - } else { - return nil - } - } - - /// Detects code langauges from the shebang of a file. - /// Eg: `#!/usr/bin/env/python2.6` will detect the `python` code language. - /// Or, `#! /usr/bin/env perl` will detect the `perl` code language. - /// - Parameter contents: The contents of the first few lines of the file. - /// - Returns: The detected code language, if any. - private static func detectLanguageUsingShebang(contents: String) -> CodeLanguage? { - var contents = String(contents.split(separator: "\n").first ?? "") - guard - contents.starts(with: "#!"), - contents.trimmingCharacters(in: .whitespacesAndNewlines) != "#!", - let result = contents - .split(separator: "/", omittingEmptySubsequences: true) - .last? - .firstMatch(of: Regex { OneOrMore(.word) }) - else { - return nil - } - - var script = result.output.trimmingCharacters(in: .whitespacesAndNewlines) - - if script == "env" { - guard result.endIndex != contents.endIndex else { return nil } - - let argumentRegex = Regex { - ZeroOrMore(.whitespace) - ChoiceOf { - One("-") - One("--") - } - ZeroOrMore(.word) - ZeroOrMore(.whitespace) - } - let parameterRegex = Regex { - OneOrMore(.word) - One("=") - OneOrMore(.word) - } - - contents.trimPrefix(Regex { - OneOrMore("#!") - ZeroOrMore(.whitespace) - OneOrMore(.any, .reluctant) - OneOrMore(.whitespace) - }) - while !contents.isEmpty { - if contents.prefixMatch(of: argumentRegex) != nil { - contents.trimPrefix(argumentRegex) - } else if contents.prefixMatch(of: parameterRegex) != nil { - contents.trimPrefix(parameterRegex) - } else { - break - } - } - guard let newScript = contents.firstMatch(of: Regex { OneOrMore(.word) })?.output else { - return nil - } - script = String(newScript) - } - - return languageFromIdentifier(script) - } - - /// Detects modelines in either the beginning or end of a file. - /// - /// Examples of valid modelines: - /// ``` - /// # vim: set ft=js ts=4 sw=4 et: - /// # vim: ts=4:sw=4:et:ft=js - /// -*- mode: js; indent-tabs-mode: nil; tab-width: 4 -*- - /// code: language=javascript insertSpaces=true tabSize=4 - /// ``` - /// All of the above would resolve to `javascript` - /// - /// - Parameters: - /// - prefixBuffer: The first few lines of a document. - /// - suffixBuffer: The last few lines of a document. - /// - Returns: The detected code language, if any. - private static func detecLanguageUsingModeline(prefixBuffer: String, suffixBuffer: String?) -> CodeLanguage? { - func detectModeline(in string: String) -> CodeLanguage? { - guard !string.isEmpty else { return nil } - - let emacsLineRegex = Regex { - "-*-" - Capture { - #/.*/# - } - "-*-" - } - - let emacsLanguageRegex = Regex { - "mode:" - ZeroOrMore(.whitespace) - Capture { - OneOrMore(.word) - } - } - - let vimLineRegex = Regex { - ChoiceOf { - One("//") - One("/*") - } - OneOrMore(.whitespace) - #/vim:.*/# - Optionally(.newlineSequence) - } - - let vimLanguageRegex = Regex { - "ft=" - Capture { - OneOrMore(.word) - } - } - - if let emacsLine = string.firstMatch(of: emacsLineRegex)?.1, - let emacsLanguage = emacsLine.firstMatch(of: emacsLanguageRegex)?.1 { - return languageFromIdentifier(String(emacsLanguage)) - } else if let vimLine = string.firstMatch(of: vimLineRegex)?.0, - let vimLanguage = vimLine.firstMatch(of: vimLanguageRegex)?.1 { - return languageFromIdentifier(String(vimLanguage)) - } else { - return nil - } - } - - return detectModeline(in: prefixBuffer) ?? detectModeline(in: suffixBuffer ?? "") - } - - /// Finds a language to match a parsed identifier. - /// - Parameter identifier: The identifier to use. - /// - Returns: The found code language, if any. - private static func languageFromIdentifier(_ identifier: String) -> CodeLanguage? { - return allLanguages.first { - $0.tsName == identifier - || $0.extensions.contains(identifier) - || $0.additionalIdentifiers.contains(identifier) - } - } -} diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage.swift b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage.swift deleted file mode 100644 index 89648afd..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/CodeLanguage.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// CodeLanguage.swift -// CodeEditTextView/CodeLanguage -// -// Created by Lukas Pistrol on 25.05.22. -// - -import Foundation -import SwiftTreeSitter -import TreeSitterGrammars - -/// A structure holding metadata for code languages -public struct CodeLanguage { - internal init( - id: TreeSitterLanguage, - tsName: String, - extensions: Set, - lineCommentString: String, - rangeCommentStrings: (String, String), - documentationCommentStrings: Set = [], - parentURL: URL? = nil, - highlights: Set? = nil, - additionalIdentifiers: Set = [] - ) { - self.id = id - self.tsName = tsName - self.extensions = extensions - self.lineCommentString = lineCommentString - self.rangeCommentStrings = rangeCommentStrings - self.documentationCommentStrings = documentationCommentStrings - self.parentQueryURL = parentURL - self.additionalHighlights = highlights - self.additionalIdentifiers = additionalIdentifiers - } - - /// The ID of the language - public let id: TreeSitterLanguage - - /// The display name of the language - public let tsName: String - - /// A set of file extensions for the language - /// - /// In special cases this can also be a file name - /// (e.g `Dockerfile`, `Makefile`) - public let extensions: Set - - /// The leading string of a comment line - public let lineCommentString: String - - /// The leading and trailing string of a multi-line comment - public let rangeCommentStrings: (String, String) - - /// The leading (and trailing, if there is one) string of a documentation comment - public let documentationCommentStrings: Set - - /// The query URL of a language this language inherits from. (e.g.: C for C++) - public let parentQueryURL: URL? - - /// Additional highlight file names (e.g.: JSX for JavaScript) - public let additionalHighlights: Set? - - /// The query URL for the language if available - public var queryURL: URL? { - queryURL() - } - - /// The bundle's resource URL - internal var resourceURL: URL? = Bundle.module.resourceURL - - /// A set of aditional identifiers to use for things like shebang matching. - public let additionalIdentifiers: Set - - /// The tree-sitter language for the language if available - public var language: Language? { - guard let tsLanguage = tsLanguage else { return nil } - return Language(language: tsLanguage) - } - - internal func queryURL(for highlights: String = "highlights") -> URL? { - return resourceURL? - .appendingPathComponent("Resources/tree-sitter-\(tsName)/\(highlights).scm") - } - - /// Gets the TSLanguage from `tree-sitter` — only SQL, Bash, and JavaScript are supported - private var tsLanguage: OpaquePointer? { - switch id { - case .bash: - return tree_sitter_bash() - case .javascript, .jsx: - return tree_sitter_javascript() - case .sql: - return tree_sitter_sql() - default: - return nil - } - } -} - -extension CodeLanguage: Hashable { - public static func == (lhs: CodeLanguage, rhs: CodeLanguage) -> Bool { - return lhs.id == rhs.id - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - -public enum DocumentationComments: Hashable { - public static func == (lhs: DocumentationComments, rhs: DocumentationComments) -> Bool { - switch lhs { - case .single(let lhsString): - switch rhs { - case .single(let rhsString): - return lhsString == rhsString - case .pair: - return false - } - case .pair(let lhsPair): - switch rhs { - case .single: - return false - case .pair(let rhsPair): - return lhsPair.0 == rhsPair.0 && lhsPair.1 == rhsPair.1 - } - } - } - - public func hash(into hasher: inout Hasher) { - switch self { - case .single(let string): - hasher.combine(string) - case .pair(let pair): - hasher.combine(pair.0) - hasher.combine(pair.1) - } - } - - case single(String) - case pair((String, String)) -} diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-bash/highlights.scm b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-bash/highlights.scm deleted file mode 100644 index f33a7c2d..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-bash/highlights.scm +++ /dev/null @@ -1,56 +0,0 @@ -[ - (string) - (raw_string) - (heredoc_body) - (heredoc_start) -] @string - -(command_name) @function - -(variable_name) @property - -[ - "case" - "do" - "done" - "elif" - "else" - "esac" - "export" - "fi" - "for" - "function" - "if" - "in" - "select" - "then" - "unset" - "until" - "while" -] @keyword - -(comment) @comment - -(function_definition name: (word) @function) - -(file_descriptor) @number - -[ - (command_substitution) - (process_substitution) - (expansion) -]@embedded - -[ - "$" - "&&" - ">" - ">>" - "<" - "|" -] @operator - -( - (command (_) @constant) - (#match? @constant "^-") -) diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights-jsx.scm b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights-jsx.scm deleted file mode 100644 index 0bdd886a..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights-jsx.scm +++ /dev/null @@ -1,8 +0,0 @@ -(jsx_opening_element (identifier) @tag (#match? @tag "^[a-z][^.]*$")) -(jsx_closing_element (identifier) @tag (#match? @tag "^[a-z][^.]*$")) -(jsx_self_closing_element (identifier) @tag (#match? @tag "^[a-z][^.]*$")) - -(jsx_attribute (property_identifier) @attribute) -(jsx_opening_element (["<" ">"]) @punctuation.bracket) -(jsx_closing_element ([""]) @punctuation.bracket) -(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket) diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights-params.scm b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights-params.scm deleted file mode 100644 index 95ffc724..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights-params.scm +++ /dev/null @@ -1,12 +0,0 @@ -(formal_parameters - [ - (identifier) @variable.parameter - (array_pattern - (identifier) @variable.parameter) - (object_pattern - [ - (pair_pattern value: (identifier) @variable.parameter) - (shorthand_property_identifier_pattern) @variable.parameter - ]) - ] -) diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights.scm b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights.scm deleted file mode 100644 index 613a49a8..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/highlights.scm +++ /dev/null @@ -1,205 +0,0 @@ -; Special identifiers -;-------------------- - -([ - (identifier) - (shorthand_property_identifier) - (shorthand_property_identifier_pattern) - ] @constant - (#match? @constant "^[A-Z_][A-Z\\d_]+$")) - - -((identifier) @constructor - (#match? @constructor "^[A-Z]")) - -((identifier) @variable.builtin - (#match? @variable.builtin "^(arguments|module|console|window|document)$") - (#is-not? local)) - -((identifier) @function.builtin - (#eq? @function.builtin "require") - (#is-not? local)) - -; Function and method definitions -;-------------------------------- - -(function - name: (identifier) @function) -(function_declaration - name: (identifier) @function) -(method_definition - name: (property_identifier) @function.method) - -(pair - key: (property_identifier) @function.method - value: [(function) (arrow_function)]) - -(assignment_expression - left: (member_expression - property: (property_identifier) @function.method) - right: [(function) (arrow_function)]) - -(variable_declarator - name: (identifier) @function - value: [(function) (arrow_function)]) - -(assignment_expression - left: (identifier) @function - right: [(function) (arrow_function)]) - -; Function and method calls -;-------------------------- - -(call_expression - function: (identifier) @function) - -(call_expression - function: (member_expression - property: (property_identifier) @function.method)) - -; Variables -;---------- - -(identifier) @variable - -; Properties -;----------- - -(property_identifier) @property - -; Literals -;--------- - -(this) @variable.builtin -(super) @variable.builtin - -[ - (true) - (false) - (null) - (undefined) -] @constant.builtin - -(comment) @comment - -[ - (string) - (template_string) -] @string - -(regex) @string.special -(number) @number - -; Tokens -;------- - -(template_substitution - "${" @punctuation.special - "}" @punctuation.special) @embedded - -[ - ";" - (optional_chain) - "." - "," -] @punctuation.delimiter - -[ - "-" - "--" - "-=" - "+" - "++" - "+=" - "*" - "*=" - "**" - "**=" - "/" - "/=" - "%" - "%=" - "<" - "<=" - "<<" - "<<=" - "=" - "==" - "===" - "!" - "!=" - "!==" - "=>" - ">" - ">=" - ">>" - ">>=" - ">>>" - ">>>=" - "~" - "^" - "&" - "|" - "^=" - "&=" - "|=" - "&&" - "||" - "??" - "&&=" - "||=" - "??=" -] @operator - -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket - -[ - "as" - "async" - "await" - "break" - "case" - "catch" - "class" - "const" - "continue" - "debugger" - "default" - "delete" - "do" - "else" - "export" - "extends" - "finally" - "for" - "from" - "function" - "get" - "if" - "import" - "in" - "instanceof" - "let" - "new" - "of" - "return" - "set" - "static" - "switch" - "target" - "throw" - "try" - "typeof" - "var" - "void" - "while" - "with" - "yield" -] @keyword diff --git a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/injections.scm b/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/injections.scm deleted file mode 100644 index 178188c5..00000000 --- a/LocalPackages/CodeEditLanguages/Sources/CodeEditLanguages/Resources/tree-sitter-javascript/injections.scm +++ /dev/null @@ -1,32 +0,0 @@ -; Parse the contents of tagged template literals using -; a language inferred from the tag. - -(call_expression - function: [ - (identifier) @injection.language - (member_expression - property: (property_identifier) @injection.language) - ] - arguments: (template_string) @injection.content) - -; Parse regex syntax within regex literals - -((regex_pattern) @injection.content - (#set! injection.language "regex")) - - ; Parse JSDoc annotations in comments - -((comment) @injection.content - (#set! injection.language "jsdoc")) - -; Parse Ember/Glimmer/Handlebars/HTMLBars/etc. template literals -; e.g.: await render(hbs``) -(call_expression - function: ((identifier) @_name - (#eq? @_name "hbs")) - arguments: ((template_string) @glimmer - (#offset! @glimmer 0 1 0 -1))) - -; Ember Unified