diff --git a/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift b/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift index 884415bf0..678e2761f 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift @@ -17,7 +17,7 @@ import OSLog /// /// This class manages multiple objects that help perform this task: /// - ``StyledRangeContainer`` -/// - ``StyledRangeStore`` +/// - ``RangeStore`` /// - ``VisibleRangeProvider`` /// - ``HighlightProviderState`` /// @@ -34,12 +34,12 @@ import OSLog /// | /// | Queries coalesced styles /// v -/// +-------------------------------+ +-----------------------------+ -/// | StyledRangeContainer | ------> | StyledRangeStore[] | -/// | | | | Stores styles for one provider -/// | - manages combined ranges | | - stores raw ranges & | -/// | - layers highlight styles | | captures | -/// | + getAttributesForRange() | +-----------------------------+ +/// +-------------------------------+ +-------------------------+ +/// | StyledRangeContainer | ------> | RangeStore[] | +/// | | | | Stores styles for one provider +/// | - manages combined ranges | | - stores raw ranges & | +/// | - layers highlight styles | | captures | +/// | + getAttributesForRange() | +-------------------------+ /// +-------------------------------+ /// ^ /// | Sends highlighted runs @@ -276,7 +276,7 @@ extension Highlighter: StyledRangeContainerDelegate { guard let range = NSRange(location: offset, length: run.length).intersection(range) else { continue } - storage?.setAttributes(attributeProvider.attributesFor(run.capture), range: range) + storage?.setAttributes(attributeProvider.attributesFor(run.value?.capture), range: range) offset += range.length } diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift index 6ac69bcc7..d61bfb009 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift +++ b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift @@ -18,7 +18,34 @@ protocol StyledRangeContainerDelegate: AnyObject { /// See ``runsIn(range:)`` for more details on how conflicting highlights are handled. @MainActor class StyledRangeContainer { - var _storage: [ProviderID: StyledRangeStore] = [:] + struct StyleElement: RangeStoreElement, CustomDebugStringConvertible { + var capture: CaptureName? + var modifiers: CaptureModifierSet + + var isEmpty: Bool { + capture == nil && modifiers.isEmpty + } + + func combineLowerPriority(_ other: StyleElement?) -> StyleElement { + StyleElement( + capture: self.capture ?? other?.capture, + modifiers: modifiers.union(other?.modifiers ?? []) + ) + } + + func combineHigherPriority(_ other: StyleElement?) -> StyleElement { + StyleElement( + capture: other?.capture ?? self.capture, + modifiers: modifiers.union(other?.modifiers ?? []) + ) + } + + var debugDescription: String { + "\(capture?.stringValue ?? "(empty)"), \(modifiers)" + } + } + + var _storage: [ProviderID: RangeStore] = [:] weak var delegate: StyledRangeContainerDelegate? /// Initialize the container with a list of provider identifiers. Each provider is given an id, they should be @@ -28,13 +55,13 @@ class StyledRangeContainer { /// - providers: An array of identifiers given to providers. init(documentLength: Int, providers: [ProviderID]) { for provider in providers { - _storage[provider] = StyledRangeStore(documentLength: documentLength) + _storage[provider] = RangeStore(documentLength: documentLength) } } func addProvider(_ id: ProviderID, documentLength: Int) { assert(!_storage.keys.contains(id), "Provider already exists") - _storage[id] = StyledRangeStore(documentLength: documentLength) + _storage[id] = RangeStore(documentLength: documentLength) } func removeProvider(_ id: ProviderID) { @@ -55,10 +82,18 @@ class StyledRangeContainer { /// /// - Parameter range: The range to query. /// - Returns: An array of continuous styled runs. - func runsIn(range: NSRange) -> [StyledRangeStoreRun] { + 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: [StyledRangeStoreRun] = [] + var runs: [RangeStoreRun] = [] var minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length }) @@ -70,9 +105,9 @@ class StyledRangeContainer { for idx in (0.., withCount newLength: Int) { - _storage.values.forEach { - $0.storageUpdated(replacedCharactersIn: range, withCount: newLength) + for key in _storage.keys { + _storage[key]?.storageUpdated(replacedCharactersIn: range, withCount: newLength) } } } @@ -109,11 +144,11 @@ 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 let storage = _storage[provider] else { + guard var storage = _storage[provider] else { assertionFailure("No storage found for the given provider: \(provider)") return } - var runs: [StyledRangeStoreRun] = [] + var runs: [RangeStoreRun] = [] var lastIndex = rangeToHighlight.lowerBound for highlight in highlights { @@ -123,10 +158,9 @@ extension StyledRangeContainer: HighlightProviderStateDelegate { continue // Skip! Overlapping } runs.append( - StyledRangeStoreRun( + RangeStoreRun( length: highlight.range.length, - capture: highlight.capture, - modifiers: highlight.modifiers + value: StyleElement(capture: highlight.capture, modifiers: highlight.modifiers) ) ) lastIndex = highlight.range.max @@ -137,6 +171,7 @@ extension StyledRangeContainer: HighlightProviderStateDelegate { } storage.set(runs: runs, for: rangeToHighlight.intRange) + _storage[provider] = storage delegate?.styleContainerDidUpdate(in: rangeToHighlight) } } diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+OffsetMetric.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+OffsetMetric.swift deleted file mode 100644 index a05b68f68..000000000 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+OffsetMetric.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// StyledRangeStore+OffsetMetric.swift -// CodeEditSourceEditor -// -// Created by Khan Winter on 10/25/24 -// - -import _RopeModule - -extension StyledRangeStore { - struct OffsetMetric: RopeMetric { - typealias Element = StyledRun - - func size(of summary: StyledRangeStore.StyledRun.Summary) -> Int { - summary.length - } - - func index(at offset: Int, in element: StyledRangeStore.StyledRun) -> Int { - return offset - } - } -} diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStoreRun.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStoreRun.swift deleted file mode 100644 index 06335edba..000000000 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStoreRun.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// StyledRangeStoreRun.swift -// CodeEditSourceEditor -// -// Created by Khan Winter on 11/4/24. -// - -/// Consumer-facing value type for the stored values in this container. -struct StyledRangeStoreRun: Equatable, Hashable { - var length: Int - var capture: CaptureName? - var modifiers: CaptureModifierSet - - static func empty(length: Int) -> Self { - StyledRangeStoreRun(length: length, capture: nil, modifiers: []) - } - - var isEmpty: Bool { - capture == nil && modifiers.isEmpty - } - - mutating package func combineLowerPriority(_ other: borrowing StyledRangeStoreRun) { - if self.capture == nil { - self.capture = other.capture - } - self.modifiers.formUnion(other.modifiers) - } - - mutating package func combineHigherPriority(_ other: borrowing StyledRangeStoreRun) { - self.capture = other.capture ?? self.capture - self.modifiers.formUnion(other.modifiers) - } - - mutating package func subtractLength(_ other: borrowing StyledRangeStoreRun) { - self.length -= other.length - } -} - -extension StyledRangeStoreRun: CustomDebugStringConvertible { - var debugDescription: String { - if isEmpty { - "\(length) (empty)" - } else { - "\(length) (\(capture.debugDescription), \(modifiers.values.debugDescription))" - } - } -} diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+Coalesce.swift b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+Coalesce.swift similarity index 86% rename from Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+Coalesce.swift rename to Sources/CodeEditSourceEditor/RangeStore/RangeStore+Coalesce.swift index 542661b5a..d6fb93259 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+Coalesce.swift +++ b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+Coalesce.swift @@ -1,5 +1,5 @@ // -// StyledRangeStore+Internals.swift +// RangeStore+Internals.swift // CodeEditSourceEditor // // Created by Khan Winter on 10/25/24 @@ -7,7 +7,7 @@ import _RopeModule -extension StyledRangeStore { +extension RangeStore { /// Coalesce items before and after the given range. /// /// Compares the next run with the run at the given range. If they're the same, removes the next run and grows the @@ -16,7 +16,7 @@ extension StyledRangeStore { /// rather than the queried one. /// /// - Parameter range: The range of the item to coalesce around. - func coalesceNearby(range: Range) { + mutating func coalesceNearby(range: Range) { var index = findIndex(at: range.lastIndex).index if index < _guts.endIndex && _guts.index(after: index) != _guts.endIndex { coalesceRunAfter(index: &index) @@ -30,11 +30,11 @@ extension StyledRangeStore { } /// Check if the run and the run after it are equal, and if so remove the next one and concatenate the two. - private func coalesceRunAfter(index: inout Index) { + private mutating func coalesceRunAfter(index: inout Index) { let thisRun = _guts[index] let nextRun = _guts[_guts.index(after: index)] - if thisRun.styleCompare(nextRun) { + if thisRun.compareValue(nextRun) { _guts.update(at: &index, by: { $0.length += nextRun.length }) var nextIndex = index diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+FindIndex.swift b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+FindIndex.swift similarity index 85% rename from Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+FindIndex.swift rename to Sources/CodeEditSourceEditor/RangeStore/RangeStore+FindIndex.swift index a07076b58..363768d2e 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+FindIndex.swift +++ b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+FindIndex.swift @@ -1,11 +1,11 @@ // -// StyledRangeStore+FindIndex.swift +// RangeStore+FindIndex.swift // CodeEditSourceEditor // // Created by Khan Winter on 1/6/25. // -extension StyledRangeStore { +extension RangeStore { /// Finds a Rope index, given a string offset. /// - Parameter offset: The offset to query for. /// - Returns: The index of the containing element in the rope. diff --git a/Sources/CodeEditSourceEditor/RangeStore/RangeStore+OffsetMetric.swift b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+OffsetMetric.swift new file mode 100644 index 000000000..efbfb4a12 --- /dev/null +++ b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+OffsetMetric.swift @@ -0,0 +1,22 @@ +// +// RangeStore+OffsetMetric.swift +// CodeEditSourceEditor +// +// Created by Khan Winter on 10/25/24 +// + +import _RopeModule + +extension RangeStore { + struct OffsetMetric: RopeMetric { + typealias Element = StoredRun + + func size(of summary: RangeStore.StoredRun.Summary) -> Int { + summary.length + } + + func index(at offset: Int, in element: RangeStore.StoredRun) -> Int { + return offset + } + } +} diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+StyledRun.swift b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+StoredRun.swift similarity index 60% rename from Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+StyledRun.swift rename to Sources/CodeEditSourceEditor/RangeStore/RangeStore+StoredRun.swift index 3fe15a150..b7a7afdf1 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+StyledRun.swift +++ b/Sources/CodeEditSourceEditor/RangeStore/RangeStore+StoredRun.swift @@ -1,31 +1,38 @@ // -// StyledRangeStore+StyledRun.swift +// RangeStore+StoredRun.swift // CodeEditSourceEditor // // Created by Khan Winter on 10/25/24 import _RopeModule -extension StyledRangeStore { - struct StyledRun { +extension RangeStore { + struct StoredRun { var length: Int - let capture: CaptureName? - let modifiers: CaptureModifierSet + let value: Element? static func empty(length: Int) -> Self { - StyledRun(length: length, capture: nil, modifiers: []) + StoredRun(length: length, value: nil) } /// Compare two styled ranges by their stored styles. /// - Parameter other: The range to compare to. /// - Returns: The result of the comparison. - func styleCompare(_ other: Self) -> Bool { - capture == other.capture && modifiers == other.modifiers + func compareValue(_ other: Self) -> Bool { + return if let lhs = value, let rhs = other.value { + lhs == rhs + } else if let lhs = value { + lhs.isEmpty + } else if let rhs = other.value { + rhs.isEmpty + } else { + true + } } } } -extension StyledRangeStore.StyledRun: RopeElement { +extension RangeStore.StoredRun: RopeElement { typealias Index = Int var summary: Summary { Summary(length: length) } @@ -50,34 +57,34 @@ extension StyledRangeStore.StyledRun: RopeElement { mutating func split(at index: Self.Index) -> Self { assert(index >= 0 && index <= length) - let tail = Self(length: length - index, capture: capture, modifiers: modifiers) + let tail = Self(length: length - index, value: value) length = index return tail } } -extension StyledRangeStore.StyledRun { +extension RangeStore.StoredRun { struct Summary { var length: Int } } -extension StyledRangeStore.StyledRun.Summary: RopeSummary { +extension RangeStore.StoredRun.Summary: RopeSummary { // FIXME: This is entirely arbitrary. Benchmark this. @inline(__always) static var maxNodeSize: Int { 10 } @inline(__always) - static var zero: StyledRangeStore.StyledRun.Summary { Self(length: 0) } + static var zero: RangeStore.StoredRun.Summary { Self(length: 0) } @inline(__always) var isZero: Bool { length == 0 } - mutating func add(_ other: StyledRangeStore.StyledRun.Summary) { + mutating func add(_ other: RangeStore.StoredRun.Summary) { length += other.length } - mutating func subtract(_ other: StyledRangeStore.StyledRun.Summary) { + mutating func subtract(_ other: RangeStore.StoredRun.Summary) { length -= other.length } } diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore.swift b/Sources/CodeEditSourceEditor/RangeStore/RangeStore.swift similarity index 71% rename from Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore.swift rename to Sources/CodeEditSourceEditor/RangeStore/RangeStore.swift index 68d4056da..5e22fc5f8 100644 --- a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore.swift +++ b/Sources/CodeEditSourceEditor/RangeStore/RangeStore.swift @@ -1,5 +1,5 @@ // -// StyledRangeStore.swift +// RangeStore.swift // CodeEditSourceEditor // // Created by Khan Winter on 10/24/24 @@ -7,15 +7,18 @@ import _RopeModule -/// StyledRangeStore is a container type that allows for setting and querying captures and modifiers for syntax -/// highlighting. The container reflects a text document in that its length needs to be kept up-to-date. +/// RangeStore is a container type that allows for setting and querying values for relative ranges in text. The +/// container reflects a text document in that its length needs to be kept up-to-date. It can efficiently remove and +/// replace subranges even for large documents. Provides helper methods for keeping some state in-sync with a text +/// document's content. /// /// Internally this class uses a `Rope` from the swift-collections package, allowing for efficient updates and /// retrievals. -final class StyledRangeStore { - typealias Run = StyledRangeStoreRun - typealias Index = Rope.Index - var _guts = Rope() +struct RangeStore: Sendable { + typealias Run = RangeStoreRun + typealias RopeType = Rope + typealias Index = RopeType.Index + var _guts = RopeType() var length: Int { _guts.count(in: OffsetMetric()) @@ -26,7 +29,7 @@ final class StyledRangeStore { private var cache: (range: Range, runs: [Run])? init(documentLength: Int) { - self._guts = Rope([StyledRun(length: documentLength, capture: nil, modifiers: [])]) + self._guts = RopeType([StoredRun(length: documentLength, value: nil)]) } // MARK: - Core @@ -48,7 +51,7 @@ final class StyledRangeStore { while index < _guts.endIndex { let run = _guts[index] - runs.append(Run(length: run.length - (offset ?? 0), capture: run.capture, modifiers: run.modifiers)) + runs.append(Run(length: run.length - (offset ?? 0), value: run.value)) index = _guts.index(after: index) offset = nil @@ -57,22 +60,21 @@ final class StyledRangeStore { return runs } - /// Sets a capture and modifiers for a range. + /// Sets a value for a range. /// - Parameters: - /// - capture: The capture to set. - /// - modifiers: The modifiers to set. + /// - value: The value to set for the given range. /// - range: The range to write to. - func set(capture: CaptureName, modifiers: CaptureModifierSet, for range: Range) { + mutating func set(value: Element, for range: Range) { assert(range.lowerBound >= 0, "Negative lowerBound") assert(range.upperBound <= _guts.count(in: OffsetMetric()), "upperBound outside valid range") - set(runs: [Run(length: range.length, capture: capture, modifiers: modifiers)], for: range) + set(runs: [Run(length: range.length, value: value)], for: range) } /// Replaces a range in the document with an array of runs. /// - Parameters: /// - runs: The runs to insert. /// - range: The range to replace. - func set(runs: [Run], for range: Range) { + mutating func set(runs: [Run], for range: Range) { let gutsRange = 0..<_guts.count(in: OffsetMetric()) if range.clamped(to: gutsRange) != range { let upperBound = range.clamped(to: gutsRange).upperBound @@ -83,7 +85,7 @@ final class StyledRangeStore { _guts.replaceSubrange( range, in: OffsetMetric(), - with: runs.map { StyledRun(length: $0.length, capture: $0.capture, modifiers: $0.modifiers) } + with: runs.map { StoredRun(length: $0.length, value: $0.value) } ) coalesceNearby(range: range) @@ -93,9 +95,9 @@ final class StyledRangeStore { // MARK: - Storage Sync -extension StyledRangeStore { +extension RangeStore { /// Handles keeping the internal storage in sync with the document. - func storageUpdated(replacedCharactersIn range: Range, withCount newLength: Int) { + mutating func storageUpdated(replacedCharactersIn range: Range, withCount newLength: Int) { assert(range.lowerBound >= 0, "Negative lowerBound") assert(range.upperBound <= _guts.count(in: OffsetMetric()), "upperBound outside valid range") diff --git a/Sources/CodeEditSourceEditor/RangeStore/RangeStoreElement.swift b/Sources/CodeEditSourceEditor/RangeStore/RangeStoreElement.swift new file mode 100644 index 000000000..9db2d325e --- /dev/null +++ b/Sources/CodeEditSourceEditor/RangeStore/RangeStoreElement.swift @@ -0,0 +1,10 @@ +// +// RangeStoreElement.swift +// CodeEditSourceEditor +// +// Created by Khan Winter on 5/28/25. +// + +protocol RangeStoreElement: Equatable, Hashable { + var isEmpty: Bool { get } +} diff --git a/Sources/CodeEditSourceEditor/RangeStore/RangeStoreRun.swift b/Sources/CodeEditSourceEditor/RangeStore/RangeStoreRun.swift new file mode 100644 index 000000000..116764eec --- /dev/null +++ b/Sources/CodeEditSourceEditor/RangeStore/RangeStoreRun.swift @@ -0,0 +1,34 @@ +// +// RangeStoreRun.swift +// CodeEditSourceEditor +// +// Created by Khan Winter on 11/4/24. +// + +/// Consumer-facing value type for the stored values in this container. +struct RangeStoreRun: Equatable, Hashable { + var length: Int + var value: Element? + + static func empty(length: Int) -> Self { + RangeStoreRun(length: length, value: nil) + } + + var isEmpty: Bool { + value?.isEmpty ?? true + } + + mutating func subtractLength(_ other: borrowing RangeStoreRun) { + self.length -= other.length + } +} + +extension RangeStoreRun: CustomDebugStringConvertible { + var debugDescription: String { + if let value = value as? CustomDebugStringConvertible { + "\(length) (\(value.debugDescription))" + } else { + "\(length) (empty)" + } + } +} diff --git a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift index 1ea05fc20..c366ad46c 100644 --- a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift +++ b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeContainerTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import CodeEditSourceEditor final class StyledRangeContainerTests: XCTestCase { - typealias Run = StyledRangeStoreRun + typealias Run = RangeStoreRun @MainActor func test_init() { @@ -27,16 +27,16 @@ 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].capture) - XCTAssertEqual(store._storage[providers[0]]!.runs(in: 0..<100)[1].capture, .comment) - XCTAssertNil(store._storage[providers[0]]!.runs(in: 0..<100)[2].capture) + 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.runsIn(range: NSRange(location: 0, length: 100)), [ - Run(length: 40, capture: nil, modifiers: []), - Run(length: 10, capture: .comment, modifiers: []), - Run(length: 50, capture: nil, modifiers: []) + Run(length: 40, value: nil), + Run(length: 10, value: .init(capture: .comment, modifiers: [])), + Run(length: 50, value: nil) ] ) } @@ -63,10 +63,10 @@ final class StyledRangeContainerTests: XCTestCase { XCTAssertEqual( store.runsIn(range: NSRange(location: 0, length: 100)), [ - Run(length: 40, capture: nil, modifiers: []), - Run(length: 5, capture: .comment, modifiers: []), - Run(length: 5, capture: .comment, modifiers: [.declaration]), - Run(length: 50, capture: nil, modifiers: []) + Run(length: 40, value: nil), + Run(length: 5, value: .init(capture: .comment, modifiers: [])), + Run(length: 5, value: .init(capture: .comment, modifiers: [.declaration])), + Run(length: 50, value: nil) ] ) } @@ -107,16 +107,16 @@ final class StyledRangeContainerTests: XCTestCase { XCTAssertEqual(runs.reduce(0, { $0 + $1.length}), 200) - XCTAssertEqual(runs[0], Run(length: 30, capture: nil, modifiers: [])) - XCTAssertEqual(runs[1], Run(length: 5, capture: .comment, modifiers: [])) - XCTAssertEqual(runs[2], Run(length: 5, capture: .comment, modifiers: [.declaration])) - XCTAssertEqual(runs[3], Run(length: 5, capture: .comment, modifiers: [.abstract, .declaration])) - XCTAssertEqual(runs[4], Run(length: 5, capture: .comment, modifiers: [])) - XCTAssertEqual(runs[5], Run(length: 30, capture: nil, modifiers: [])) - XCTAssertEqual(runs[6], Run(length: 10, capture: .string, modifiers: [])) - XCTAssertEqual(runs[7], Run(length: 10, capture: .string, modifiers: [.static])) - XCTAssertEqual(runs[8], Run(length: 5, capture: .string, modifiers: [.static, .modification])) - XCTAssertEqual(runs[9], Run(length: 5, capture: .string, modifiers: [.modification])) - XCTAssertEqual(runs[10], Run(length: 90, capture: nil, modifiers: [])) + XCTAssertEqual(runs[0], Run(length: 30, value: nil)) + XCTAssertEqual(runs[1], Run(length: 5, value: .init(capture: .comment, modifiers: []))) + XCTAssertEqual(runs[2], Run(length: 5, value: .init(capture: .comment, modifiers: [.declaration]))) + XCTAssertEqual(runs[3], Run(length: 5, value: .init(capture: .comment, modifiers: [.abstract, .declaration]))) + XCTAssertEqual(runs[4], Run(length: 5, value: .init(capture: .comment, modifiers: []))) + XCTAssertEqual(runs[5], Run(length: 30, value: nil)) + XCTAssertEqual(runs[6], Run(length: 10, value: .init(capture: .string, modifiers: []))) + XCTAssertEqual(runs[7], Run(length: 10, value: .init(capture: .string, modifiers: [.static]))) + XCTAssertEqual(runs[8], Run(length: 5, value: .init(capture: .string, modifiers: [.static, .modification]))) + XCTAssertEqual(runs[9], Run(length: 5, value: .init(capture: .string, modifiers: [.modification]))) + XCTAssertEqual(runs[10], Run(length: 90, value: nil)) } } diff --git a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift b/Tests/CodeEditSourceEditorTests/RangeStoreTests.swift similarity index 68% rename from Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift rename to Tests/CodeEditSourceEditorTests/RangeStoreTests.swift index 5b43d5d2e..6fa78447e 100644 --- a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift +++ b/Tests/CodeEditSourceEditorTests/RangeStoreTests.swift @@ -1,12 +1,14 @@ import XCTest @testable import CodeEditSourceEditor -extension StyledRangeStore { +extension RangeStore { var length: Int { _guts.summary.length } var count: Int { _guts.count } } -final class StyledRangeStoreTests: XCTestCase { +final class RangeStoreTests: XCTestCase { + typealias Store = RangeStore + override var continueAfterFailure: Bool { get { false } set { } @@ -15,7 +17,7 @@ final class StyledRangeStoreTests: XCTestCase { func test_initWithLength() { for _ in 0..<100 { let length = Int.random(in: 0..<1000) - let store = StyledRangeStore(documentLength: length) + var store = Store(documentLength: length) XCTAssertEqual(store.length, length) } } @@ -23,26 +25,27 @@ final class StyledRangeStoreTests: XCTestCase { // MARK: - Storage func test_storageRemoveCharacters() { - let store = StyledRangeStore(documentLength: 100) + 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") } func test_storageRemoveFromEnd() { - let store = StyledRangeStore(documentLength: 100) + 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") } func test_storageRemoveSingleCharacterFromEnd() { - let store = StyledRangeStore(documentLength: 10) + var store = Store(documentLength: 10) store.set( // Test that we can delete a character associated with a single syntax run too runs: [ .empty(length: 8), - .init(length: 1, modifiers: [.abstract]), - .init(length: 1, modifiers: [.declaration])], + .init(length: 1, value: .init(modifiers: [.abstract])), + .init(length: 1, value: .init(modifiers: [.declaration])) + ], for: 0..<10 ) store.storageUpdated(replacedCharactersIn: 9..<10, withCount: 0) @@ -51,70 +54,70 @@ final class StyledRangeStoreTests: XCTestCase { } func test_storageRemoveFromBeginning() { - let store = StyledRangeStore(documentLength: 100) + 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") } func test_storageRemoveAll() { - let store = StyledRangeStore(documentLength: 100) + 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") } func test_storageInsert() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 45..<45, withCount: 10) XCTAssertEqual(store.length, 110) XCTAssertEqual(store.count, 1, "Failed to coalesce") } func test_storageInsertAtEnd() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 100..<100, withCount: 10) XCTAssertEqual(store.length, 110) XCTAssertEqual(store.count, 1, "Failed to coalesce") } func test_storageInsertAtBeginning() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<0, withCount: 10) XCTAssertEqual(store.length, 110) XCTAssertEqual(store.count, 1, "Failed to coalesce") } func test_storageInsertFromEmpty() { - let store = StyledRangeStore(documentLength: 0) + var store = Store(documentLength: 0) store.storageUpdated(replacedCharactersIn: 0..<0, withCount: 10) XCTAssertEqual(store.length, 10) XCTAssertEqual(store.count, 1, "Failed to coalesce") } func test_storageEdit() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 45..<50, withCount: 10) XCTAssertEqual(store.length, 105) XCTAssertEqual(store.count, 1, "Failed to coalesce") } func test_storageEditAtEnd() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 95..<100, withCount: 10) XCTAssertEqual(store.length, 105) XCTAssertEqual(store.count, 1, "Failed to coalesce") } func test_storageEditAtBeginning() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<5, withCount: 10) XCTAssertEqual(store.length, 105) XCTAssertEqual(store.count, 1, "Failed to coalesce") } func test_storageEditAll() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) store.storageUpdated(replacedCharactersIn: 0..<100, withCount: 10) XCTAssertEqual(store.length, 10) XCTAssertEqual(store.count, 1, "Failed to coalesce") @@ -123,8 +126,8 @@ final class StyledRangeStoreTests: XCTestCase { // MARK: - Styles func test_setOneRun() { - let store = StyledRangeStore(documentLength: 100) - store.set(capture: .comment, modifiers: [.static], for: 45..<50) + var store = Store(documentLength: 100) + store.set(value: .init(capture: .comment, modifiers: [.static]), for: 45..<50) XCTAssertEqual(store.length, 100) XCTAssertEqual(store.count, 3) @@ -134,18 +137,18 @@ final class StyledRangeStoreTests: XCTestCase { XCTAssertEqual(runs[1].length, 5) XCTAssertEqual(runs[2].length, 50) - XCTAssertNil(runs[0].capture) - XCTAssertEqual(runs[1].capture, .comment) - XCTAssertNil(runs[2].capture) + XCTAssertNil(runs[0].value?.capture) + XCTAssertEqual(runs[1].value?.capture, .comment) + XCTAssertNil(runs[2].value?.capture) - XCTAssertEqual(runs[0].modifiers, []) - XCTAssertEqual(runs[1].modifiers, [.static]) - XCTAssertEqual(runs[2].modifiers, []) + XCTAssertEqual(runs[0].value?.modifiers, nil) + XCTAssertEqual(runs[1].value?.modifiers, [.static]) + XCTAssertEqual(runs[2].value?.modifiers, nil) } func test_queryOverlappingRun() { - let store = StyledRangeStore(documentLength: 100) - store.set(capture: .comment, modifiers: [.static], for: 45..<50) + var store = Store(documentLength: 100) + store.set(value: .init(capture: .comment, modifiers: [.static]), for: 45..<50) XCTAssertEqual(store.length, 100) XCTAssertEqual(store.count, 3) @@ -154,21 +157,21 @@ final class StyledRangeStoreTests: XCTestCase { XCTAssertEqual(runs[0].length, 3) XCTAssertEqual(runs[1].length, 50) - XCTAssertEqual(runs[0].capture, .comment) - XCTAssertNil(runs[1].capture) + XCTAssertEqual(runs[0].value?.capture, .comment) + XCTAssertNil(runs[1].value?.capture) - XCTAssertEqual(runs[0].modifiers, [.static]) - XCTAssertEqual(runs[1].modifiers, []) + XCTAssertEqual(runs[0].value?.modifiers, [.static]) + XCTAssertEqual(runs[1].value?.modifiers, nil) } func test_setMultipleRuns() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) - store.set(capture: .comment, modifiers: [.static], for: 5..<15) - store.set(capture: .keyword, modifiers: [], for: 20..<30) - store.set(capture: .string, modifiers: [.static], for: 35..<40) - store.set(capture: .function, modifiers: [], for: 45..<50) - store.set(capture: .variable, modifiers: [], for: 60..<70) + store.set(value: .init(capture: .comment, modifiers: [.static]), for: 5..<15) + store.set(value: .init(capture: .keyword, modifiers: []), for: 20..<30) + store.set(value: .init(capture: .string, modifiers: [.static]), for: 35..<40) + store.set(value: .init(capture: .function, modifiers: []), for: 45..<50) + store.set(value: .init(capture: .variable, modifiers: []), for: 60..<70) XCTAssertEqual(store.length, 100) @@ -182,13 +185,13 @@ final class StyledRangeStoreTests: XCTestCase { runs.enumerated().forEach { XCTAssertEqual($0.element.length, lengths[$0.offset]) - XCTAssertEqual($0.element.capture, captures[$0.offset]) - XCTAssertEqual($0.element.modifiers, modifiers[$0.offset]) + XCTAssertEqual($0.element.value?.capture, captures[$0.offset]) + XCTAssertEqual($0.element.value?.modifiers ?? [], modifiers[$0.offset]) } } func test_setMultipleRunsAndStorageUpdate() { - let store = StyledRangeStore(documentLength: 100) + var store = Store(documentLength: 100) var lengths = [5, 10, 5, 10, 5, 5, 5, 5, 10, 10, 30] var captures: [CaptureName?] = [nil, .comment, nil, .keyword, nil, .string, nil, .function, nil, .variable, nil] @@ -196,7 +199,7 @@ final class StyledRangeStoreTests: XCTestCase { store.set( runs: zip(zip(lengths, captures), modifiers).map { - StyledRangeStore.Run(length: $0.0, capture: $0.1, modifiers: $1) + Store.Run(length: $0.0, value: .init(capture: $0.1, modifiers: $1)) }, for: 0..<100 ) @@ -214,14 +217,14 @@ final class StyledRangeStoreTests: XCTestCase { "Run \($0.offset) has incorrect length: \($0.element.length). Expected \(lengths[$0.offset])" ) XCTAssertEqual( - $0.element.capture, + $0.element.value?.capture, captures[$0.offset], // swiftlint:disable:next line_length - "Run \($0.offset) has incorrect capture: \(String(describing: $0.element.capture)). Expected \(String(describing: captures[$0.offset]))" + "Run \($0.offset) has incorrect capture: \(String(describing: $0.element.value?.capture)). Expected \(String(describing: captures[$0.offset]))" ) XCTAssertEqual( - $0.element.modifiers, - modifiers[$0.offset], - "Run \($0.offset) has incorrect modifiers: \($0.element.modifiers). Expected \(modifiers[$0.offset])" + $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])" ) } @@ -236,8 +239,8 @@ final class StyledRangeStoreTests: XCTestCase { runs.enumerated().forEach { XCTAssertEqual($0.element.length, lengths[$0.offset]) - XCTAssertEqual($0.element.capture, captures[$0.offset]) - XCTAssertEqual($0.element.modifiers, modifiers[$0.offset]) + XCTAssertEqual($0.element.value?.capture, captures[$0.offset]) + XCTAssertEqual($0.element.value?.modifiers ?? [], modifiers[$0.offset]) } } }