From fe24a29bb84e5d451c484a46bb5c0e89fdeacc7d Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:58:05 -0500 Subject: [PATCH 1/6] Move Style Container --- .../{StyledRangeContainer => }/StyledRangeContainer.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/CodeEditSourceEditor/Highlighting/{StyledRangeContainer => }/StyledRangeContainer.swift (100%) diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer.swift similarity index 100% rename from Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift rename to Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer.swift From 34e13d64a7e0e4669d1555a84bffaacedadeb94d Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 21 Jul 2025 10:19:24 -0500 Subject: [PATCH 2/6] Fix RangeStore `runsIn` Bugs, Add Tests --- .../StyledRangeContainer+runsIn.swift | 111 ++++++++ .../StyledRangeContainer.swift | 57 +--- .../RangeStore/RangeStore.swift | 23 +- .../RangeStoreTests.swift | 245 +++++++++++------- 4 files changed, 276 insertions(+), 160 deletions(-) create mode 100644 Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift rename Sources/CodeEditSourceEditor/Highlighting/{ => StyledRangeContainer}/StyledRangeContainer.swift (68%) diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift new file mode 100644 index 000000000..f4ae823ab --- /dev/null +++ b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift @@ -0,0 +1,111 @@ +// +// StyledRangeContainer+runsIn.swift +// CodeEditSourceEditor +// +// Created by Khan Winter on 7/18/25. +// + +import Foundation + +extension StyledRangeContainer { + /// Coalesces all styled runs into a single continuous array of styled runs. + /// + /// When there is an overlapping, conflicting style (eg: provider 2 gives `.comment` to the range `0..<2`, and + /// provider 1 gives `.string` to `1..<2`), the provider with a lower identifier will be prioritized. In the example + /// case, the final value would be `0..<1=.comment` and `1..<2=.string`. + /// + /// - Parameter range: The range to query. + /// - Returns: An array of continuous styled runs. + func runsIn(range: NSRange) -> [RangeStoreRun] { + func combineLowerPriority(_ lhs: inout RangeStoreRun, _ rhs: RangeStoreRun) { + lhs.value = lhs.value?.combineLowerPriority(rhs.value) ?? rhs.value + } + + func combineHigherPriority(_ lhs: inout RangeStoreRun, _ rhs: RangeStoreRun) { + lhs.value = lhs.value?.combineHigherPriority(rhs.value) ?? rhs.value + } + + // Ordered by priority, lower = higher priority. + var allRuns = _storage.sorted(by: { $0.key < $1.key }).map { $0.value.runs(in: range.intRange) } + + #if DEBUG + // ASSERTION: Verify input contract - runs should be in positional order + for (priority, runs) in allRuns.enumerated() { + var expectedStart = range.location + for (runIndex, run) in runs.enumerated() { + assert(run.length > 0, "Run \(runIndex) in priority \(priority) has non-positive length: \(run.length)") + } + // Note: Can't easily verify positional order without knowing absolute positions + // This would require the RangeStore to provide position info + } + + // ASSERTION: Verify total length consistency + let originalTotalLengths = allRuns.map { runs in runs.reduce(0) { $0 + $1.length } } + for (priority, totalLength) in originalTotalLengths.enumerated() { + assert( + totalLength == range.length, + "Priority \(priority) total length (\(totalLength)) doesn't match expected length (\(range.length))" + ) + } + #endif + + var runs: [RangeStoreRun] = [] + var minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length }) + var counter = 0 + + while let value = minValue { + // Get minimum length off the end of each array + let minRunIdx = value.offset + var minRun = value.element + + assert(minRun.length > 0, "Minimum run has non-positive length: \(minRun.length)") + + for idx in (0..= minRun.length, + "Run at priority \(idx) length (\(last.length)) is less than minimum length (\(minRun.length))" + ) + if idx < minRunIdx { + combineHigherPriority(&minRun, last) + } else { + combineLowerPriority(&minRun, last) + } + + if last.length == minRun.length { + allRuns[idx].removeLast() + } else { + // safe due to guard a few lines above. + allRuns[idx][allRuns[idx].count - 1].subtractLength(minRun) + } + } + + if !allRuns[minRunIdx].isEmpty { + allRuns[minRunIdx].removeLast() + } + + runs.append(minRun) + minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length }) + + debugRunState(allRuns, step: counter) + counter += 1 + } + + assert(runs.allSatisfy({ $0.length > 0 }), "Empty or negative lengths are not allowed") + + return runs.reversed() + } + +#if DEBUG + private func debugRunState(_ allRuns: [[RangeStoreRun]], step: Int) { + print("=== Debug Step \(step) ===") + for (priority, runs) in allRuns.enumerated() { + let lengths = runs.map { $0.length } + let totalLength = lengths.reduce(0, +) + print("Priority \(priority): lengths=\(lengths), total=\(totalLength)") + } + print() + } +#endif +} diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift similarity index 68% rename from Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer.swift rename to Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift index f0677416b..1f7560b0f 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift @@ -99,61 +99,6 @@ class StyledRangeContainer { _storage.removeValue(forKey: id) } - /// Coalesces all styled runs into a single continuous array of styled runs. - /// - /// When there is an overlapping, conflicting style (eg: provider 2 gives `.comment` to the range `0..<2`, and - /// provider 1 gives `.string` to `1..<2`), the provider with a lower identifier will be prioritized. In the example - /// case, the final value would be `0..<1=.comment` and `1..<2=.string`. - /// - /// - Parameter range: The range to query. - /// - Returns: An array of continuous styled runs. - func runsIn(range: NSRange) -> [RangeStoreRun] { - func combineLowerPriority(_ lhs: inout RangeStoreRun, _ rhs: RangeStoreRun) { - lhs.value = lhs.value?.combineLowerPriority(rhs.value) ?? rhs.value - } - - func combineHigherPriority(_ lhs: inout RangeStoreRun, _ rhs: RangeStoreRun) { - lhs.value = lhs.value?.combineHigherPriority(rhs.value) ?? rhs.value - } - - // Ordered by priority, lower = higher priority. - var allRuns = _storage.sorted(by: { $0.key < $1.key }).map { $0.value.runs(in: range.intRange) } - var runs: [RangeStoreRun] = [] - - var minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length }) - - while let value = minValue { - // Get minimum length off the end of each array - let minRunIdx = value.offset - var minRun = value.element - - for idx in (0..: Sendable { /// - Parameter range: The range to query. /// - Returns: A continuous array of runs representing the queried range. func runs(in range: Range) -> [Run] { + let length = _guts.count(in: OffsetMetric()) assert(range.lowerBound >= 0, "Negative lowerBound") - assert(range.upperBound <= _guts.count(in: OffsetMetric()), "upperBound outside valid range") + assert(range.upperBound <= length, "upperBound outside valid range") if let cache, cache.range == range { return cache.runs } var runs = [Run]() - var index = findIndex(at: range.lowerBound).index - var offset: Int? = range.lowerBound - _guts.offset(of: index, in: OffsetMetric()) + var offset: Int = range.lowerBound - _guts.offset(of: index, in: OffsetMetric()) + var remainingLength = range.upperBound - range.lowerBound - while index < _guts.endIndex, _guts.offset(of: index, in: OffsetMetric()) < range.upperBound { + while index < _guts.endIndex, + _guts.offset(of: index, in: OffsetMetric()) < range.upperBound, + remainingLength > 0 { let run = _guts[index] - runs.append(Run(length: run.length - (offset ?? 0), value: run.value)) + let runLength = min(run.length - offset, remainingLength) + runs.append(Run(length: runLength, value: run.value)) + remainingLength -= runLength + if remainingLength <= 0 { + break // Avoid even checking the storage for the next index + } index = _guts.index(after: index) - offset = nil + offset = 0 } return runs @@ -83,6 +91,9 @@ struct RangeStore: Sendable { storageUpdated(replacedCharactersIn: upperBound.. - override var continueAfterFailure: Bool { - get { false } - set { } - } - - func test_initWithLength() { + @Test + func initWithLength() { for _ in 0..<100 { let length = Int.random(in: 0..<1000) var store = Store(documentLength: length) - XCTAssertEqual(store.length, length) + #expect(store.length == length) } } // MARK: - Storage - func test_storageRemoveCharacters() { + @Test + func storageRemoveCharacters() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 10..<12, withCount: 0) - XCTAssertEqual(store.length, 98, "Failed to remove correct range") - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 98, "Failed to remove correct range") + #expect(store.count == 1, "Failed to coalesce") } - func test_storageRemoveFromEnd() { + @Test + func storageRemoveFromEnd() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 95..<100, withCount: 0) - XCTAssertEqual(store.length, 95, "Failed to remove correct range") - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 95, "Failed to remove correct range") + #expect(store.count == 1, "Failed to coalesce") } - func test_storageRemoveSingleCharacterFromEnd() { + @Test + func storageRemoveSingleCharacterFromEnd() { var store = Store(documentLength: 10) store.set( // Test that we can delete a character associated with a single syntax run too runs: [ @@ -49,122 +49,135 @@ final class RangeStoreTests: XCTestCase { for: 0..<10 ) store.storageUpdated(replacedCharactersIn: 9..<10, withCount: 0) - XCTAssertEqual(store.length, 9, "Failed to remove correct range") - XCTAssertEqual(store.count, 2) + #expect(store.length == 9, "Failed to remove correct range") + #expect(store.count == 2) } - func test_storageRemoveFromBeginning() { + @Test + func storageRemoveFromBeginning() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<15, withCount: 0) - XCTAssertEqual(store.length, 85, "Failed to remove correct range") - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 85, "Failed to remove correct range") + #expect(store.count == 1, "Failed to coalesce") } - func test_storageRemoveAll() { + @Test + func storageRemoveAll() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<100, withCount: 0) - XCTAssertEqual(store.length, 0, "Failed to remove correct range") - XCTAssertEqual(store.count, 0, "Failed to remove all runs") + #expect(store.length == 0, "Failed to remove correct range") + #expect(store.count == 0, "Failed to remove all runs") } - func test_storageInsert() { + @Test + func storageInsert() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 45..<45, withCount: 10) - XCTAssertEqual(store.length, 110) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 110) + #expect(store.count == 1, "Failed to coalesce") } - func test_storageInsertAtEnd() { + @Test + func storageInsertAtEnd() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 100..<100, withCount: 10) - XCTAssertEqual(store.length, 110) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 110) + #expect(store.count == 1, "Failed to coalesce") } - func test_storageInsertAtBeginning() { + @Test + func storageInsertAtBeginning() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<0, withCount: 10) - XCTAssertEqual(store.length, 110) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 110) + #expect(store.count == 1, "Failed to coalesce") } - func test_storageInsertFromEmpty() { + @Test + func storageInsertFromEmpty() { var store = Store(documentLength: 0) store.storageUpdated(replacedCharactersIn: 0..<0, withCount: 10) - XCTAssertEqual(store.length, 10) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 10) + #expect(store.count == 1, "Failed to coalesce") } - func test_storageEdit() { + @Test + func storageEdit() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 45..<50, withCount: 10) - XCTAssertEqual(store.length, 105) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 105) + #expect(store.count == 1, "Failed to coalesce") } - func test_storageEditAtEnd() { + @Test + func storageEditAtEnd() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 95..<100, withCount: 10) - XCTAssertEqual(store.length, 105) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 105) + #expect(store.count == 1, "Failed to coalesce") } - func test_storageEditAtBeginning() { + @Test + func storageEditAtBeginning() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<5, withCount: 10) - XCTAssertEqual(store.length, 105) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 105) + #expect(store.count == 1, "Failed to coalesce") } - func test_storageEditAll() { + @Test + func storageEditAll() { var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<100, withCount: 10) - XCTAssertEqual(store.length, 10) - XCTAssertEqual(store.count, 1, "Failed to coalesce") + #expect(store.length == 10) + #expect(store.count == 1, "Failed to coalesce") } // MARK: - Styles - func test_setOneRun() { + @Test + func setOneRun() { var store = Store(documentLength: 100) store.set(value: .init(capture: .comment, modifiers: [.static]), for: 45..<50) - XCTAssertEqual(store.length, 100) - XCTAssertEqual(store.count, 3) + #expect(store.length == 100) + #expect(store.count == 3) let runs = store.runs(in: 0..<100) - XCTAssertEqual(runs.count, 3) - XCTAssertEqual(runs[0].length, 45) - XCTAssertEqual(runs[1].length, 5) - XCTAssertEqual(runs[2].length, 50) - - XCTAssertNil(runs[0].value?.capture) - XCTAssertEqual(runs[1].value?.capture, .comment) - XCTAssertNil(runs[2].value?.capture) - - XCTAssertEqual(runs[0].value?.modifiers, nil) - XCTAssertEqual(runs[1].value?.modifiers, [.static]) - XCTAssertEqual(runs[2].value?.modifiers, nil) + #expect(runs.count == 3) + #expect(runs[0].length == 45) + #expect(runs[1].length == 5) + #expect(runs[2].length == 50) + + #expect(runs[0].value?.capture == nil) + #expect(runs[1].value?.capture == .comment) + #expect(runs[2].value?.capture == nil) + + #expect(runs[0].value?.modifiers == nil) + #expect(runs[1].value?.modifiers == [.static]) + #expect(runs[2].value?.modifiers == nil) } - func test_queryOverlappingRun() { + @Test + func queryOverlappingRun() { var store = Store(documentLength: 100) store.set(value: .init(capture: .comment, modifiers: [.static]), for: 45..<50) - XCTAssertEqual(store.length, 100) - XCTAssertEqual(store.count, 3) + #expect(store.length == 100) + #expect(store.count == 3) let runs = store.runs(in: 47..<100) - XCTAssertEqual(runs.count, 2) - XCTAssertEqual(runs[0].length, 3) - XCTAssertEqual(runs[1].length, 50) + #expect(runs.count == 2) + #expect(runs[0].length == 3) + #expect(runs[1].length == 50) - XCTAssertEqual(runs[0].value?.capture, .comment) - XCTAssertNil(runs[1].value?.capture) + #expect(runs[0].value?.capture == .comment) + #expect(runs[1].value?.capture == nil) - XCTAssertEqual(runs[0].value?.modifiers, [.static]) - XCTAssertEqual(runs[1].value?.modifiers, nil) + #expect(runs[0].value?.modifiers == [.static]) + #expect(runs[1].value?.modifiers == nil) } - func test_setMultipleRuns() { + @Test + func setMultipleRuns() { var store = Store(documentLength: 100) store.set(value: .init(capture: .comment, modifiers: [.static]), for: 5..<15) @@ -173,24 +186,25 @@ final class RangeStoreTests: XCTestCase { store.set(value: .init(capture: .function, modifiers: []), for: 45..<50) store.set(value: .init(capture: .variable, modifiers: []), for: 60..<70) - XCTAssertEqual(store.length, 100) + #expect(store.length == 100) let runs = store.runs(in: 0..<100) - XCTAssertEqual(runs.count, 11) - XCTAssertEqual(runs.reduce(0, { $0 + $1.length }), 100) + #expect(runs.count == 11) + #expect(runs.reduce(0, { $0 + $1.length }) == 100) let lengths = [5, 10, 5, 10, 5, 5, 5, 5, 10, 10, 30] let captures: [CaptureName?] = [nil, .comment, nil, .keyword, nil, .string, nil, .function, nil, .variable, nil] let modifiers: [CaptureModifierSet] = [[], [.static], [], [], [], [.static], [], [], [], [], []] runs.enumerated().forEach { - XCTAssertEqual($0.element.length, lengths[$0.offset]) - XCTAssertEqual($0.element.value?.capture, captures[$0.offset]) - XCTAssertEqual($0.element.value?.modifiers ?? [], modifiers[$0.offset]) + #expect($0.element.length == lengths[$0.offset]) + #expect($0.element.value?.capture == captures[$0.offset]) + #expect($0.element.value?.modifiers ?? [] == modifiers[$0.offset]) } } - func test_setMultipleRunsAndStorageUpdate() { + @Test + func setMultipleRunsAndStorageUpdate() { var store = Store(documentLength: 100) var lengths = [5, 10, 5, 10, 5, 5, 5, 5, 10, 10, 30] @@ -204,43 +218,78 @@ final class RangeStoreTests: XCTestCase { for: 0..<100 ) - XCTAssertEqual(store.length, 100) + #expect(store.length == 100) var runs = store.runs(in: 0..<100) - XCTAssertEqual(runs.count, 11) - XCTAssertEqual(runs.reduce(0, { $0 + $1.length }), 100) + #expect(runs.count == 11) + #expect(runs.reduce(0, { $0 + $1.length }) == 100) runs.enumerated().forEach { - XCTAssertEqual( - $0.element.length, - lengths[$0.offset], + #expect( + $0.element.length == lengths[$0.offset], "Run \($0.offset) has incorrect length: \($0.element.length). Expected \(lengths[$0.offset])" ) - XCTAssertEqual( - $0.element.value?.capture, - captures[$0.offset], // swiftlint:disable:next line_length + #expect( + $0.element.value?.capture == captures[$0.offset], // swiftlint:disable:next line_length "Run \($0.offset) has incorrect capture: \(String(describing: $0.element.value?.capture)). Expected \(String(describing: captures[$0.offset]))" ) - XCTAssertEqual( - $0.element.value?.modifiers, - modifiers[$0.offset], // swiftlint:disable:next line_length + #expect( + $0.element.value?.modifiers == modifiers[$0.offset], // swiftlint:disable:next line_length "Run \($0.offset) has incorrect modifiers: \(String(describing: $0.element.value?.modifiers)). Expected \(modifiers[$0.offset])" ) } store.storageUpdated(replacedCharactersIn: 30..<45, withCount: 10) runs = store.runs(in: 0..<95) - XCTAssertEqual(runs.count, 9) - XCTAssertEqual(runs.reduce(0, { $0 + $1.length }), 95) + #expect(runs.count == 9) + #expect(runs.reduce(0, { $0 + $1.length }) == 95) lengths = [5, 10, 5, 10, 10, 5, 10, 10, 30] captures = [nil, .comment, nil, .keyword, nil, .function, nil, .variable, nil] modifiers = [[], [.static], [], [], [], [], [], [], []] runs.enumerated().forEach { - XCTAssertEqual($0.element.length, lengths[$0.offset]) - XCTAssertEqual($0.element.value?.capture, captures[$0.offset]) - XCTAssertEqual($0.element.value?.modifiers ?? [], modifiers[$0.offset]) + #expect($0.element.length == lengths[$0.offset]) + #expect($0.element.value?.capture == captures[$0.offset]) + #expect($0.element.value?.modifiers ?? [] == modifiers[$0.offset]) + } + } + + // MARK: - Query + + // A few known bad cases + @Test(arguments: [3..<8, 65..<100, 0..<5, 5..<12]) + func runsInAlwaysBoundedByRange(_ range: Range) { + var store = Store(documentLength: 100) + let lengths = [5, 10, 5, 10, 5, 5, 5, 5, 10, 10, 30] + let captures: [CaptureName?] = [nil, .comment, nil, .keyword, nil, .string, nil, .function, nil, .variable, nil] + let modifiers: [CaptureModifierSet] = [[], [.static], [], [], [], [.static], [], [], [], [], []] + + store.set( + runs: zip(zip(lengths, captures), modifiers).map { + Store.Run(length: $0.0, value: .init(capture: $0.1, modifiers: $1)) + }, + for: 0..<100 + ) + + #expect( + store.runs(in: range).reduce(0, { $0 + $1.length }) == (range.upperBound - range.lowerBound), + "Runs returned by storage did not equal requested range" + ) + #expect(store.runs(in: range).allSatisfy({ $0.length > 0 })) + } + + // Randomized version of the previous test + @Test + func runsAlwaysBoundedByRangeRandom() { + func range() -> Range { + let start = Int.random(in: 0..<100) + let end = Int.random(in: start..<100) + return start.. Date: Mon, 21 Jul 2025 10:25:16 -0500 Subject: [PATCH 3/6] Remove Debug Assertions --- .../StyledRangeContainer+runsIn.swift | 43 +------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift index f4ae823ab..b840e165d 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift @@ -28,27 +28,6 @@ extension StyledRangeContainer { // Ordered by priority, lower = higher priority. var allRuns = _storage.sorted(by: { $0.key < $1.key }).map { $0.value.runs(in: range.intRange) } - #if DEBUG - // ASSERTION: Verify input contract - runs should be in positional order - for (priority, runs) in allRuns.enumerated() { - var expectedStart = range.location - for (runIndex, run) in runs.enumerated() { - assert(run.length > 0, "Run \(runIndex) in priority \(priority) has non-positive length: \(run.length)") - } - // Note: Can't easily verify positional order without knowing absolute positions - // This would require the RangeStore to provide position info - } - - // ASSERTION: Verify total length consistency - let originalTotalLengths = allRuns.map { runs in runs.reduce(0) { $0 + $1.length } } - for (priority, totalLength) in originalTotalLengths.enumerated() { - assert( - totalLength == range.length, - "Priority \(priority) total length (\(totalLength)) doesn't match expected length (\(range.length))" - ) - } - #endif - var runs: [RangeStoreRun] = [] var minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length }) var counter = 0 @@ -58,15 +37,9 @@ extension StyledRangeContainer { let minRunIdx = value.offset var minRun = value.element - assert(minRun.length > 0, "Minimum run has non-positive length: \(minRun.length)") - for idx in (0..= minRun.length, - "Run at priority \(idx) length (\(last.length)) is less than minimum length (\(minRun.length))" - ) if idx < minRunIdx { combineHigherPriority(&minRun, last) } else { @@ -85,27 +58,13 @@ extension StyledRangeContainer { allRuns[minRunIdx].removeLast() } + assert(minRun.length > 0, "Empty or negative runs are not allowed.") runs.append(minRun) minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length }) - debugRunState(allRuns, step: counter) counter += 1 } - assert(runs.allSatisfy({ $0.length > 0 }), "Empty or negative lengths are not allowed") - return runs.reversed() } - -#if DEBUG - private func debugRunState(_ allRuns: [[RangeStoreRun]], step: Int) { - print("=== Debug Step \(step) ===") - for (priority, runs) in allRuns.enumerated() { - let lengths = runs.map { $0.length } - let totalLength = lengths.reduce(0, +) - print("Priority \(priority): lengths=\(lengths), total=\(totalLength)") - } - print() - } -#endif } From d206e1cc014752918c3261bd8a256873fee43ab1 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 21 Jul 2025 10:37:20 -0500 Subject: [PATCH 4/6] Correctly Prioritize Rearranged Providers --- .../Highlighting/Highlighter.swift | 11 +++++----- .../StyledRangeContainer+runsIn.swift | 4 +++- .../StyledRangeContainer.swift | 20 +++++++++++-------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift b/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift index 50feee21f..21c33e343 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift @@ -163,7 +163,7 @@ class Highlighter: NSObject { let difference = newIds.difference(from: existingIds).inferringMoves() var highlightProviders = self.highlightProviders // Make a mutable copy - var moveMap: [Int: HighlightProviderState] = [:] + var moveMap: [Int: (Int, HighlightProviderState)] = [:] for change in difference { switch change { @@ -174,7 +174,8 @@ class Highlighter: NSObject { guard let movedProvider = moveMap[offset] else { continue } - highlightProviders.insert(movedProvider, at: offset) + highlightProviders.insert(movedProvider.1, at: offset) + styleContainer.setPriority(providerId: movedProvider.0, priority: offset) continue } // Set up a new provider and insert it with a unique ID @@ -188,12 +189,12 @@ class Highlighter: NSObject { language: language ) highlightProviders.insert(state, at: offset) - styleContainer.addProvider(providerIdCounter, documentLength: textView.length) + styleContainer.addProvider(providerIdCounter, priority: offset, documentLength: textView.length) state.invalidate() // Invalidate this new one case let .remove(offset, _, associatedOffset): - guard associatedOffset == nil else { + if let associatedOffset { // Moved, add it to the move map - moveMap[associatedOffset!] = highlightProviders.remove(at: offset) + moveMap[associatedOffset] = (offset, highlightProviders.remove(at: offset)) continue } // Removed entirely diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift index b840e165d..69e6a93ca 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer+runsIn.swift @@ -26,7 +26,9 @@ extension StyledRangeContainer { } // Ordered by priority, lower = higher priority. - var allRuns = _storage.sorted(by: { $0.key < $1.key }).map { $0.value.runs(in: range.intRange) } + var allRuns = _storage.values + .sorted(by: { $0.priority < $1.priority }) + .map { $0.store.runs(in: range.intRange) } var runs: [RangeStoreRun] = [] var minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length }) diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift index 1f7560b0f..da1966b31 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift @@ -70,7 +70,7 @@ class StyledRangeContainer { } } - var _storage: [ProviderID: RangeStore] = [:] + var _storage: [ProviderID: (store: RangeStore, priority: Int)] = [:] weak var delegate: StyledRangeContainerDelegate? /// Initialize the container with a list of provider identifiers. Each provider is given an id, they should be @@ -80,17 +80,21 @@ class StyledRangeContainer { /// - providers: An array of identifiers given to providers. init(documentLength: Int, providers: [ProviderID]) { for provider in providers { - _storage[provider] = RangeStore(documentLength: documentLength) + _storage[provider] = (store: RangeStore(documentLength: documentLength), priority: provider) } } - func addProvider(_ id: ProviderID, documentLength: Int) { + func addProvider(_ id: ProviderID, priority: Int, documentLength: Int) { assert(!_storage.keys.contains(id), "Provider already exists") - _storage[id] = RangeStore(documentLength: documentLength) + _storage[id] = (store: RangeStore(documentLength: documentLength), priority: priority) + } + + func setPriority(providerId: ProviderID, priority: Int) { + _storage[providerId]?.priority = priority } func removeProvider(_ id: ProviderID) { - guard let provider = _storage[id] else { return } + guard let provider = _storage[id]?.store else { return } applyHighlightResult( provider: id, highlights: [], @@ -101,7 +105,7 @@ class StyledRangeContainer { func storageUpdated(editedRange: NSRange, changeInLength delta: Int) { for key in _storage.keys { - _storage[key]?.storageUpdated(editedRange: editedRange, changeInLength: delta) + _storage[key]?.store.storageUpdated(editedRange: editedRange, changeInLength: delta) } } } @@ -116,7 +120,7 @@ extension StyledRangeContainer: HighlightProviderStateDelegate { /// - rangeToHighlight: The range to apply the highlights to. func applyHighlightResult(provider: ProviderID, highlights: [HighlightRange], rangeToHighlight: NSRange) { assert(rangeToHighlight != .notFound, "NSNotFound is an invalid highlight range") - guard var storage = _storage[provider] else { + guard var storage = _storage[provider]?.store else { assertionFailure("No storage found for the given provider: \(provider)") return } @@ -143,7 +147,7 @@ extension StyledRangeContainer: HighlightProviderStateDelegate { } storage.set(runs: runs, for: rangeToHighlight.intRange) - _storage[provider] = storage + _storage[provider]?.store = storage delegate?.styleContainerDidUpdate(in: rangeToHighlight) } } From b4d627d71301f7378cca7a2f9a053606d06ca86f Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:41:13 -0500 Subject: [PATCH 5/6] Update Tests --- .../Highlighting/StyledRangeContainerTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift index c366ad46c..8f98e63d0 100644 --- a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift +++ b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift @@ -11,7 +11,7 @@ final class StyledRangeContainerTests: XCTestCase { // Have to do string conversion due to missing Comparable conformance pre-macOS 14 XCTAssertEqual(store._storage.keys.sorted(), providers) - XCTAssert(store._storage.values.allSatisfy({ $0.length == 100 }), "One or more providers have incorrect length") + XCTAssert(store._storage.values.allSatisfy({ $0.store.length == 100 }), "One or more providers have incorrect length") } @MainActor @@ -26,10 +26,10 @@ final class StyledRangeContainerTests: XCTestCase { ) XCTAssertNotNil(store._storage[providers[0]]) - XCTAssertEqual(store._storage[providers[0]]!.count, 3) - XCTAssertNil(store._storage[providers[0]]!.runs(in: 0..<100)[0].value?.capture) - XCTAssertEqual(store._storage[providers[0]]!.runs(in: 0..<100)[1].value?.capture, .comment) - XCTAssertNil(store._storage[providers[0]]!.runs(in: 0..<100)[2].value?.capture) + XCTAssertEqual(store._storage[providers[0]]!.store.count, 3) + XCTAssertNil(store._storage[providers[0]]!.store.runs(in: 0..<100)[0].value?.capture) + XCTAssertEqual(store._storage[providers[0]]!.store.runs(in: 0..<100)[1].value?.capture, .comment) + XCTAssertNil(store._storage[providers[0]]!.store.runs(in: 0..<100)[2].value?.capture) XCTAssertEqual( store.runsIn(range: NSRange(location: 0, length: 100)), From 561bf6af43ea93289736c55bc21bb1ec2c255014 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:42:22 -0500 Subject: [PATCH 6/6] fix:lint --- .../Highlighting/StyledRangeContainerTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift index 8f98e63d0..1c4663f29 100644 --- a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift +++ b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift @@ -11,7 +11,10 @@ final class StyledRangeContainerTests: XCTestCase { // Have to do string conversion due to missing Comparable conformance pre-macOS 14 XCTAssertEqual(store._storage.keys.sorted(), providers) - XCTAssert(store._storage.values.allSatisfy({ $0.store.length == 100 }), "One or more providers have incorrect length") + XCTAssert( + store._storage.values.allSatisfy({ $0.store.length == 100 }), + "One or more providers have incorrect length" + ) } @MainActor