Skip to content

Commit 098e648

Browse files
Multiple Highlighter Support (#273)
### Description Adds support for multiple highlight providers. > [!NOTE] > For reviewers: You may notice that this uses an underscored module `_RopeModule`. This module is safe, as in it has tests and is used in production (it backs AttributedString and BigString Foundation types). It's underscored because the API may change in the future, and the swift-collections devs consider it to be the incorrect collection type to use for most applications. However, this application meets every requirement for using a Rope. ### Related Issues * #40 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots N/A --------- Co-authored-by: Tom Ludwig <[email protected]>
1 parent b26a606 commit 098e648

32 files changed

+1814
-472
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/CodeEditTextView.xcscheme

-101
This file was deleted.

Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
"kind" : "remoteSourceControl",
4242
"location" : "https://github.com/apple/swift-collections.git",
4343
"state" : {
44-
"revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d",
45-
"version" : "1.1.2"
44+
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
45+
"version" : "1.1.4"
4646
}
4747
},
4848
{

Sources/CodeEditSourceEditor/CodeEditSourceEditor/CodeEditSourceEditor.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
5757
editorOverscroll: CGFloat = 0,
5858
cursorPositions: Binding<[CursorPosition]>,
5959
useThemeBackground: Bool = true,
60-
highlightProvider: HighlightProviding? = nil,
60+
highlightProviders: [HighlightProviding] = [TreeSitterClient()],
6161
contentInsets: NSEdgeInsets? = nil,
6262
isEditable: Bool = true,
6363
isSelectable: Bool = true,
@@ -78,7 +78,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
7878
self.wrapLines = wrapLines
7979
self.editorOverscroll = editorOverscroll
8080
self.cursorPositions = cursorPositions
81-
self.highlightProvider = highlightProvider
81+
self.highlightProviders = highlightProviders
8282
self.contentInsets = contentInsets
8383
self.isEditable = isEditable
8484
self.isSelectable = isSelectable
@@ -132,7 +132,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
132132
editorOverscroll: CGFloat = 0,
133133
cursorPositions: Binding<[CursorPosition]>,
134134
useThemeBackground: Bool = true,
135-
highlightProvider: HighlightProviding? = nil,
135+
highlightProviders: [HighlightProviding] = [TreeSitterClient()],
136136
contentInsets: NSEdgeInsets? = nil,
137137
isEditable: Bool = true,
138138
isSelectable: Bool = true,
@@ -153,7 +153,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
153153
self.wrapLines = wrapLines
154154
self.editorOverscroll = editorOverscroll
155155
self.cursorPositions = cursorPositions
156-
self.highlightProvider = highlightProvider
156+
self.highlightProviders = highlightProviders
157157
self.contentInsets = contentInsets
158158
self.isEditable = isEditable
159159
self.isSelectable = isSelectable
@@ -179,7 +179,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
179179
private var editorOverscroll: CGFloat
180180
package var cursorPositions: Binding<[CursorPosition]>
181181
private var useThemeBackground: Bool
182-
private var highlightProvider: HighlightProviding?
182+
private var highlightProviders: [HighlightProviding]
183183
private var contentInsets: NSEdgeInsets?
184184
private var isEditable: Bool
185185
private var isSelectable: Bool
@@ -204,7 +204,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
204204
cursorPositions: cursorPositions.wrappedValue,
205205
editorOverscroll: editorOverscroll,
206206
useThemeBackground: useThemeBackground,
207-
highlightProvider: highlightProvider,
207+
highlightProviders: highlightProviders,
208208
contentInsets: contentInsets,
209209
isEditable: isEditable,
210210
isSelectable: isSelectable,

Sources/CodeEditSourceEditor/Controller/TextViewController+Highlighter.swift

+4-21
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,14 @@ extension TextViewController {
1515
self.highlighter = nil
1616
}
1717

18-
self.highlighter = Highlighter(
18+
let highlighter = Highlighter(
1919
textView: textView,
20-
highlightProvider: highlightProvider,
21-
theme: theme,
20+
providers: highlightProviders,
2221
attributeProvider: self,
2322
language: language
2423
)
25-
textView.addStorageDelegate(highlighter!)
26-
setHighlightProvider(self.highlightProvider)
27-
}
28-
29-
internal func setHighlightProvider(_ highlightProvider: HighlightProviding? = nil) {
30-
var provider: HighlightProviding?
31-
32-
if let highlightProvider = highlightProvider {
33-
provider = highlightProvider
34-
} else {
35-
self.treeSitterClient = TreeSitterClient()
36-
provider = self.treeSitterClient!
37-
}
38-
39-
if let provider = provider {
40-
self.highlightProvider = provider
41-
highlighter?.setHighlightProvider(provider)
42-
}
24+
textView.addStorageDelegate(highlighter)
25+
self.highlighter = highlighter
4326
}
4427
}
4528

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public class TextViewController: NSViewController {
113113
public var useThemeBackground: Bool
114114

115115
/// The provided highlight provider.
116-
public var highlightProvider: HighlightProviding?
116+
public var highlightProviders: [HighlightProviding]
117117

118118
/// Optional insets to offset the text view in the scroll view by.
119119
public var contentInsets: NSEdgeInsets?
@@ -217,7 +217,7 @@ public class TextViewController: NSViewController {
217217
cursorPositions: [CursorPosition],
218218
editorOverscroll: CGFloat,
219219
useThemeBackground: Bool,
220-
highlightProvider: HighlightProviding?,
220+
highlightProviders: [HighlightProviding] = [TreeSitterClient()],
221221
contentInsets: NSEdgeInsets?,
222222
isEditable: Bool,
223223
isSelectable: Bool,
@@ -237,7 +237,7 @@ public class TextViewController: NSViewController {
237237
self.cursorPositions = cursorPositions
238238
self.editorOverscroll = editorOverscroll
239239
self.useThemeBackground = useThemeBackground
240-
self.highlightProvider = highlightProvider
240+
self.highlightProviders = highlightProviders
241241
self.contentInsets = contentInsets
242242
self.isEditable = isEditable
243243
self.isSelectable = isSelectable
@@ -307,7 +307,7 @@ public class TextViewController: NSViewController {
307307
textView.removeStorageDelegate(highlighter)
308308
}
309309
highlighter = nil
310-
highlightProvider = nil
310+
highlightProviders.removeAll()
311311
textCoordinators.values().forEach {
312312
$0.destroy()
313313
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//
2+
// CaptureModifiers.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 10/24/24.
6+
//
7+
8+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokenModifiers
9+
10+
/// A collection of possible syntax capture modifiers. Represented by an integer for memory efficiency, and with the
11+
/// ability to convert to and from strings for ease of use with tools.
12+
///
13+
/// These are useful for helping differentiate between similar types of syntax. Eg two variables may be declared like
14+
/// ```swift
15+
/// var a = 1
16+
/// let b = 1
17+
/// ```
18+
/// ``CaptureName`` will represent both these later in code, but combined ``CaptureModifier`` themes can differentiate
19+
/// between constants (`b` in the example) and regular variables (`a` in the example).
20+
///
21+
/// This is `Int8` raw representable for memory considerations. In large documents there can be *lots* of these created
22+
/// and passed around, so representing them with a single integer is preferable to a string to save memory.
23+
///
24+
public enum CaptureModifier: Int8, CaseIterable, Sendable {
25+
case declaration
26+
case definition
27+
case readonly
28+
case `static`
29+
case deprecated
30+
case abstract
31+
case async
32+
case modification
33+
case documentation
34+
case defaultLibrary
35+
36+
public var stringValue: String {
37+
switch self {
38+
case .declaration:
39+
return "declaration"
40+
case .definition:
41+
return "definition"
42+
case .readonly:
43+
return "readonly"
44+
case .static:
45+
return "static"
46+
case .deprecated:
47+
return "deprecated"
48+
case .abstract:
49+
return "abstract"
50+
case .async:
51+
return "async"
52+
case .modification:
53+
return "modification"
54+
case .documentation:
55+
return "documentation"
56+
case .defaultLibrary:
57+
return "defaultLibrary"
58+
}
59+
}
60+
61+
// swiftlint:disable:next cyclomatic_complexity
62+
public static func fromString(_ string: String) -> CaptureModifier? {
63+
switch string {
64+
case "declaration":
65+
return .declaration
66+
case "definition":
67+
return .definition
68+
case "readonly":
69+
return .readonly
70+
case "static`":
71+
return .static
72+
case "deprecated":
73+
return .deprecated
74+
case "abstract":
75+
return .abstract
76+
case "async":
77+
return .async
78+
case "modification":
79+
return .modification
80+
case "documentation":
81+
return .documentation
82+
case "defaultLibrary":
83+
return .defaultLibrary
84+
default:
85+
return nil
86+
}
87+
}
88+
}
89+
90+
extension CaptureModifier: CustomDebugStringConvertible {
91+
public var debugDescription: String { stringValue }
92+
}
93+
94+
/// A set of capture modifiers, efficiently represented by a single integer.
95+
public struct CaptureModifierSet: OptionSet, Equatable, Hashable, Sendable {
96+
public var rawValue: UInt
97+
98+
public init(rawValue: UInt) {
99+
self.rawValue = rawValue
100+
}
101+
102+
public static let declaration = CaptureModifierSet(rawValue: 1 << CaptureModifier.declaration.rawValue)
103+
public static let definition = CaptureModifierSet(rawValue: 1 << CaptureModifier.definition.rawValue)
104+
public static let readonly = CaptureModifierSet(rawValue: 1 << CaptureModifier.readonly.rawValue)
105+
public static let `static` = CaptureModifierSet(rawValue: 1 << CaptureModifier.static.rawValue)
106+
public static let deprecated = CaptureModifierSet(rawValue: 1 << CaptureModifier.deprecated.rawValue)
107+
public static let abstract = CaptureModifierSet(rawValue: 1 << CaptureModifier.abstract.rawValue)
108+
public static let async = CaptureModifierSet(rawValue: 1 << CaptureModifier.async.rawValue)
109+
public static let modification = CaptureModifierSet(rawValue: 1 << CaptureModifier.modification.rawValue)
110+
public static let documentation = CaptureModifierSet(rawValue: 1 << CaptureModifier.documentation.rawValue)
111+
public static let defaultLibrary = CaptureModifierSet(rawValue: 1 << CaptureModifier.defaultLibrary.rawValue)
112+
113+
/// All values in the set.
114+
public var values: [CaptureModifier] {
115+
var rawValue = self.rawValue
116+
117+
// This set is represented by an integer, where each `1` in the binary number represents a value.
118+
// We can interpret the index of the `1` as the raw value of a ``CaptureModifier`` (the index in 0b0100 would
119+
// be 2). This loops through each `1` in the `rawValue`, finds the represented modifier, and 0's out the `1` so
120+
// we can get the next one using the binary & operator (0b0110 -> 0b0100 -> 0b0000 -> finish).
121+
var values: [Int8] = []
122+
while rawValue > 0 {
123+
values.append(Int8(rawValue.trailingZeroBitCount))
124+
// Clears the bit at the desired index (eg: 0b110 if clearing index 0)
125+
rawValue &= ~UInt(1 << rawValue.trailingZeroBitCount)
126+
}
127+
return values.compactMap({ CaptureModifier(rawValue: $0) })
128+
}
129+
130+
public mutating func insert(_ value: CaptureModifier) {
131+
rawValue &= 1 << value.rawValue
132+
}
133+
}

0 commit comments

Comments
 (0)