Skip to content

Commit e7b7d85

Browse files
committed
Faster Highlighting, Fix Linter
- Replaces `textView.addAttributes` by modifying the `textContentStorage` directly. This led to a 10x speedup with large files*. - Makes treeSitterClient optional on the Highlighter object for the case where tree sitter can't be initialized, doesn't have language support, or something else. - Fixes SwiftLint errors. * This speed still feels slow and glitchy, and should be revisited in a future PR.
1 parent 16e03ec commit e7b7d85

5 files changed

Lines changed: 29 additions & 21 deletions

File tree

Sources/CodeEditTextView/Extensions/STTextView+/STTextView+VisibleRange.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ extension STTextView {
2424
}
2525

2626
var visibleTextRange: NSRange {
27-
get {
28-
return textRange(for: visibleRect)
29-
}
27+
return textRange(for: visibleRect)
3028
}
3129
}

Sources/CodeEditTextView/Highlighting/Highlighter.swift

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ class Highlighter: NSObject {
3636

3737
/// The range of the entire document
3838
private var entireTextRange: Range<Int> {
39-
get {
40-
return 0..<(textView.textContentStorage.textStorage?.length ?? 0)
41-
}
39+
return 0..<(textView.textContentStorage.textStorage?.length ?? 0)
4240
}
4341

4442
/// The set of visible indexes in tht text view
@@ -56,7 +54,7 @@ class Highlighter: NSObject {
5654
// MARK: - TreeSitter Client
5755

5856
/// Calculates invalidated ranges given an edit.
59-
private var treeSitterClient: TreeSitterClient
57+
private var treeSitterClient: TreeSitterClient?
6058

6159
// MARK: - Init
6260

@@ -66,7 +64,7 @@ class Highlighter: NSObject {
6664
/// - treeSitterClient: The tree-sitter client to handle tree updates and highlight queries.
6765
/// - theme: The theme to use for highlights.
6866
init(textView: STTextView,
69-
treeSitterClient: TreeSitterClient,
67+
treeSitterClient: TreeSitterClient?,
7068
theme: EditorTheme,
7169
attributeProvider: ThemeAttributesProviding) {
7270
self.textView = textView
@@ -76,7 +74,7 @@ class Highlighter: NSObject {
7674

7775
super.init()
7876

79-
treeSitterClient.setText(text: textView.string)
77+
treeSitterClient?.setText(text: textView.string)
8078

8179
guard textView.textContentStorage.textStorage != nil else {
8280
assertionFailure("Text view does not have a textStorage")
@@ -102,16 +100,16 @@ class Highlighter: NSObject {
102100

103101
/// Invalidates all text in the textview. Useful for updating themes.
104102
func invalidate() {
105-
if !treeSitterClient.hasSetText {
106-
treeSitterClient.setText(text: textView.string)
103+
if !(treeSitterClient?.hasSetText ?? true) {
104+
treeSitterClient?.setText(text: textView.string)
107105
}
108106
invalidate(range: entireTextRange)
109107
}
110108

111109
/// Sets the language and causes a re-highlight of the entire text.
112110
/// - Parameter language: The language to update to.
113111
func setLanguage(language: CodeLanguage) throws {
114-
try treeSitterClient.setLanguage(codeLanguage: language, text: textView.string)
112+
try treeSitterClient?.setLanguage(codeLanguage: language, text: textView.string)
115113
invalidate()
116114
}
117115

@@ -163,14 +161,23 @@ private extension Highlighter {
163161
let range = Range(nsRange)!
164162
pendingSet.insert(integersIn: range)
165163

166-
treeSitterClient.queryColorsFor(range: nsRange) { [weak self] highlightRanges in
164+
treeSitterClient?.queryColorsFor(range: nsRange) { [weak self] highlightRanges in
167165
guard let attributeProvider = self?.attributeProvider,
168166
let textView = self?.textView else { return }
169167
self?.pendingSet.remove(integersIn: range)
170168
self?.validSet.formUnion(IndexSet(integersIn: range))
171-
highlightRanges.forEach { highlight in
172-
textView.addAttributes(attributeProvider.attributesFor(highlight.capture), range: highlight.range, updateLayout: false)
169+
if !(self?.visibleSet ?? .init()).contains(integersIn: range) {
170+
return
171+
}
172+
173+
textView.textContentStorage.textStorage?.beginEditing()
174+
for highlight in highlightRanges {
175+
textView.textContentStorage.textStorage?.setAttributes(
176+
attributeProvider.attributesFor(highlight.capture),
177+
range: highlight.range
178+
)
173179
}
180+
textView.textContentStorage.textStorage?.endEditing()
174181
}
175182
}
176183

@@ -191,7 +198,6 @@ private extension Highlighter {
191198

192199
}
193200

194-
195201
// MARK: - Visible Content Updates
196202

197203
private extension Highlighter {
@@ -212,7 +218,8 @@ private extension Highlighter {
212218
// MARK: - NSTextStorageDelegate
213219

214220
extension Highlighter: NSTextStorageDelegate {
215-
/// Processes an edited range in the text. Will query tree-sitter for any updated indices and re-highlight only the ranges that need it.
221+
/// Processes an edited range in the text.
222+
/// Will query tree-sitter for any updated indices and re-highlight only the ranges that need it.
216223
func textStorage(_ textStorage: NSTextStorage,
217224
didProcessEditing editedMask: NSTextStorageEditActions,
218225
range editedRange: NSRange,
@@ -230,7 +237,7 @@ extension Highlighter: NSTextStorageDelegate {
230237
return
231238
}
232239

233-
treeSitterClient.applyEdit(edit,
240+
treeSitterClient?.applyEdit(edit,
234241
text: textStorage.string) { [weak self] invalidatedIndexSet in
235242
let indexSet = invalidatedIndexSet
236243
.union(IndexSet(integersIn: Range(editedRange)!))

Sources/CodeEditTextView/STTextViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
6363

6464
// MARK: VC Lifecycle
6565

66+
// swiftlint:disable function_body_length
6667
public override func loadView() {
6768
let scrollView = STTextView.scrollableTextView()
6869
textView = scrollView.documentView as? STTextView
@@ -129,7 +130,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
129130
return self?.textView.textContentStorage.textStorage?.attributedSubstring(from: range).string
130131
}
131132

132-
let treeSitterClient = try! TreeSitterClient(codeLanguage: language, textProvider: textProvider)
133+
let treeSitterClient = try? TreeSitterClient(codeLanguage: language, textProvider: textProvider)
133134
self.highlighter = Highlighter(textView: textView,
134135
treeSitterClient: treeSitterClient,
135136
theme: theme,

Sources/CodeEditTextView/Theme/EditorTheme.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public struct EditorTheme {
8585
}
8686

8787
extension EditorTheme: Equatable {
88-
public static func ==(lhs: EditorTheme, rhs: EditorTheme) -> Bool {
88+
public static func == (lhs: EditorTheme, rhs: EditorTheme) -> Bool {
8989
return lhs.text == rhs.text &&
9090
lhs.insertionPoint == rhs.insertionPoint &&
9191
lhs.invisibles == rhs.invisibles &&

Sources/CodeEditTextView/TreeSitter/TreeSitterClient.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ extension TreeSitterClient {
138138
/// processed edit.
139139
/// - Parameter edit: The edit to apply.
140140
/// - Returns: (The old state, the new state).
141-
private func calculateNewState(edit: InputEdit, text: String, readBlock: @escaping Parser.ReadBlock) -> (Tree?, Tree?) {
141+
private func calculateNewState(edit: InputEdit,
142+
text: String,
143+
readBlock: @escaping Parser.ReadBlock) -> (Tree?, Tree?) {
142144
guard let oldTree = self.tree else {
143145
self.tree = self.parser.parse(text)
144146
return (nil, self.tree)

0 commit comments

Comments
 (0)