From 696a7f1c7e7c772719fac33e623dd6245a0b5f85 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:14:49 -0600 Subject: [PATCH] Fix Ambiguous Highlights (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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 --- .../Highlighting/HighlightRange.swift | 12 ++++++- .../TreeSitterClient+Highlight.swift | 34 ++++++++++++++----- .../TreeSitter/TreeSitterClient.swift | 2 +- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Highlighting/HighlightRange.swift b/Sources/CodeEditSourceEditor/Highlighting/HighlightRange.swift index ee730954f..85b416b82 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/HighlightRange.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/HighlightRange.swift @@ -8,7 +8,7 @@ import Foundation /// This struct represents a range to highlight, as well as the capture name for syntax coloring. -public struct HighlightRange: Sendable { +public struct HighlightRange: Hashable, Sendable { public let range: NSRange public let capture: CaptureName? public let modifiers: CaptureModifierSet @@ -19,3 +19,13 @@ public struct HighlightRange: Sendable { self.modifiers = modifiers } } + +extension HighlightRange: CustomDebugStringConvertible { + public var debugDescription: String { + if capture == nil && modifiers.isEmpty { + "\(range) (empty)" + } else { + "\(range) (\(capture?.stringValue ?? "No Capture")) \(modifiers.values.map({ $0.stringValue }))" + } + } +} diff --git a/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient+Highlight.swift b/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient+Highlight.swift index d1a5b65c4..ff8f23f48 100644 --- a/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient+Highlight.swift +++ b/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient+Highlight.swift @@ -87,17 +87,35 @@ extension TreeSitterClient { cursor: QueryCursor, includedRange: NSRange ) -> [HighlightRange] { + guard let readCallback else { return [] } + var ranges: [NSRange: Int] = [:] return cursor + .resolve(with: .init(textProvider: readCallback)) // Resolve our cursor against the query .flatMap { $0.captures } - .compactMap { - // Sometimes `cursor.setRange` just doesn't work :( so we have to do a redundant check for a valid range - // in the included range - let intersectionRange = $0.range.intersection(includedRange) ?? .zero - // Check that the capture name is one CESE can parse. If not, ignore it completely. - if intersectionRange.length > 0, let captureName = CaptureName.fromString($0.name) { - return HighlightRange(range: intersectionRange, capture: captureName) + .reversed() // SwiftTreeSitter returns captures in the reverse order of what we need to filter with. + .compactMap { capture in + let range = capture.range + let index = capture.index + + // Lower indexed captures are favored over higher, this is why we reverse it above + if let existingLevel = ranges[range], existingLevel <= index { + return nil + } + + guard let captureName = CaptureName.fromString(capture.name) else { + return nil } - return nil + + // Update the filter level to the current index since it's lower and a 'valid' capture + ranges[range] = index + + // Validate range and capture name + let intersectionRange = range.intersection(includedRange) ?? .zero + guard intersectionRange.length > 0 else { + return nil + } + + return HighlightRange(range: intersectionRange, capture: captureName) } } } diff --git a/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient.swift b/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient.swift index ecc03b22f..44a0a3473 100644 --- a/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient.swift +++ b/Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient.swift @@ -210,7 +210,7 @@ public final class TreeSitterClient: HighlightProviding { completion: @escaping @MainActor (Result<[HighlightRange], Error>) -> Void ) { let operation = { [weak self] in - return self?.queryHighlightsForRange(range: range) ?? [] + return (self?.queryHighlightsForRange(range: range) ?? []).sorted { $0.range.location < $1.range.location } } let longQuery = range.length > Constants.maxSyncQueryLength