Skip to content

Commit 7db1e66

Browse files
committed
Filter Ambiguous TreeSitter Highlights
1 parent 53605e4 commit 7db1e66

File tree

3 files changed

+38
-10
lines changed

3 files changed

+38
-10
lines changed

Sources/CodeEditSourceEditor/Highlighting/HighlightRange.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import Foundation
99

1010
/// This struct represents a range to highlight, as well as the capture name for syntax coloring.
11-
public struct HighlightRange: Sendable {
11+
public struct HighlightRange: Hashable, Sendable {
1212
public let range: NSRange
1313
public let capture: CaptureName?
1414
public let modifiers: CaptureModifierSet
@@ -19,3 +19,13 @@ public struct HighlightRange: Sendable {
1919
self.modifiers = modifiers
2020
}
2121
}
22+
23+
extension HighlightRange: CustomDebugStringConvertible {
24+
public var debugDescription: String {
25+
if capture == nil && modifiers.isEmpty {
26+
"\(range) (empty)"
27+
} else {
28+
"\(range) (\(capture?.stringValue ?? "No Capture")) \(modifiers.values.map({ $0.stringValue }))"
29+
}
30+
}
31+
}

Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient+Highlight.swift

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,35 @@ extension TreeSitterClient {
8787
cursor: QueryCursor,
8888
includedRange: NSRange
8989
) -> [HighlightRange] {
90+
guard let readCallback else { return [] }
91+
var ranges: [NSRange: Int] = [:]
9092
return cursor
93+
.resolve(with: .init(textProvider: readCallback)) // Resolve our cursor against the query
9194
.flatMap { $0.captures }
92-
.compactMap {
93-
// Sometimes `cursor.setRange` just doesn't work :( so we have to do a redundant check for a valid range
94-
// in the included range
95-
let intersectionRange = $0.range.intersection(includedRange) ?? .zero
96-
// Check that the capture name is one CESE can parse. If not, ignore it completely.
97-
if intersectionRange.length > 0, let captureName = CaptureName.fromString($0.name) {
98-
return HighlightRange(range: intersectionRange, capture: captureName)
95+
.reversed() // SwiftTreeSitter returns captures in the reverse order of what we need to filter with.
96+
.compactMap { capture in
97+
let range = capture.range
98+
let index = capture.index
99+
100+
// Lower indexed captures are favored over higher, this is why we reverse it above
101+
if let existingLevel = ranges[range], existingLevel <= index {
102+
return nil
103+
}
104+
105+
guard let captureName = CaptureName.fromString(capture.name) else {
106+
return nil
99107
}
100-
return nil
108+
109+
// Update the filter level to the current index since it's lower and a 'valid' capture
110+
ranges[range] = index
111+
112+
// Validate range and capture name
113+
let intersectionRange = range.intersection(includedRange) ?? .zero
114+
guard intersectionRange.length > 0 else {
115+
return nil
116+
}
117+
118+
return HighlightRange(range: intersectionRange, capture: captureName)
101119
}
102120
}
103121
}

Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public final class TreeSitterClient: HighlightProviding {
210210
completion: @escaping @MainActor (Result<[HighlightRange], Error>) -> Void
211211
) {
212212
let operation = { [weak self] in
213-
return self?.queryHighlightsForRange(range: range) ?? []
213+
return (self?.queryHighlightsForRange(range: range) ?? []).sorted { $0.range.location < $1.range.location }
214214
}
215215

216216
let longQuery = range.length > Constants.maxSyncQueryLength

0 commit comments

Comments
 (0)