Skip to content

Commit 696a7f1

Browse files
Fix Ambiguous Highlights (#275)
### Description Fixes some bad syntax highlighting caused by overlapping captures returned from tree-sitter. Previously the last value returned took precedence, but with the new highlighting system that's not the case. This filters highlights on duplicate ranges, and prioritizes the best capture for any range so this is no longer dependent on the highlighting system's semantics. ### Related Issues - closes #276 ### 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 Disambiguated highlights: ![Screenshot 2024-11-17 at 2 50 28 PM](https://github.com/user-attachments/assets/e1bde8b3-81b5-481f-80d2-428798374c2b) Ambiguous highlights: ![Screenshot 2024-11-17 at 2 52 04 PM](https://github.com/user-attachments/assets/90a89ed4-afb6-4d60-a7d7-39ba0a560ee6) --------- Co-authored-by: Tom Ludwig <[email protected]>
1 parent f8ecd2e commit 696a7f1

File tree

3 files changed

+38
-10
lines changed

3 files changed

+38
-10
lines changed

Sources/CodeEditSourceEditor/Highlighting/HighlightRange.swift

+11-1
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

+26-8
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

+1-1
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)