Skip to content

Commit deaca0e

Browse files
Make styled range store generic (CodeEditApp#323)
### Description This makes the `StyledRangeStore` type generic and `Sendable`. This type was originally created for storing relative ranges of highlight values (captures and modifiers). It excels at storing values for subranges of data even for large documents. I'm hoping to make this type a generic type we can use in other places in the package, like code folding, to store document state that needs to be maintained in lock-step with the document's real contents. Detailed changes: - Renamed `StyledRangeStore` to `RangeStore`, as well as all associated types. - Made `RangeStore` a value type (struct) with copy-on-write semantics, allowing for it to be concurrency safe and `Sendable`. > This doesn't have any effect on performance with the existing highlighter code. The highlighter code modifies the storage objects it uses in-place, so there is no copying necessary, only mutating. - Made `RangeStore` store a new, generic, `RangeStoreElement` type. - Updated `StyledRangeContainer` to use the updated `RangeStore`, with a custom element type that stores captures and modifiers. - Updated `StyledRangeContainer` to use a raised version of the `combineLower[Higher]Priority` methods only where they're relevant (instead of the requirement being on `RangeStoreElement`). - Updated relevant tests. ### Related Issues * CodeEditApp#43 ### 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 No behavior changes. This is in prep for CodeEditApp#43, but also lays the groundwork for other features using a type like this.
1 parent f07d9fa commit deaca0e

File tree

13 files changed

+246
-202
lines changed

13 files changed

+246
-202
lines changed

Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import OSLog
1717
///
1818
/// This class manages multiple objects that help perform this task:
1919
/// - ``StyledRangeContainer``
20-
/// - ``StyledRangeStore``
20+
/// - ``RangeStore``
2121
/// - ``VisibleRangeProvider``
2222
/// - ``HighlightProviderState``
2323
///
@@ -34,12 +34,12 @@ import OSLog
3434
/// |
3535
/// | Queries coalesced styles
3636
/// v
37-
/// +-------------------------------+ +-----------------------------+
38-
/// | StyledRangeContainer | ------> | StyledRangeStore[] |
39-
/// | | | | Stores styles for one provider
40-
/// | - manages combined ranges | | - stores raw ranges & |
41-
/// | - layers highlight styles | | captures |
42-
/// | + getAttributesForRange() | +-----------------------------+
37+
/// +-------------------------------+ +-------------------------+
38+
/// | StyledRangeContainer | ------> | RangeStore[] |
39+
/// | | | | Stores styles for one provider
40+
/// | - manages combined ranges | | - stores raw ranges & |
41+
/// | - layers highlight styles | | captures |
42+
/// | + getAttributesForRange() | +-------------------------+
4343
/// +-------------------------------+
4444
/// ^
4545
/// | Sends highlighted runs
@@ -276,7 +276,7 @@ extension Highlighter: StyledRangeContainerDelegate {
276276
guard let range = NSRange(location: offset, length: run.length).intersection(range) else {
277277
continue
278278
}
279-
storage?.setAttributes(attributeProvider.attributesFor(run.capture), range: range)
279+
storage?.setAttributes(attributeProvider.attributesFor(run.value?.capture), range: range)
280280
offset += range.length
281281
}
282282

Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeContainer.swift

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,34 @@ protocol StyledRangeContainerDelegate: AnyObject {
1818
/// See ``runsIn(range:)`` for more details on how conflicting highlights are handled.
1919
@MainActor
2020
class StyledRangeContainer {
21-
var _storage: [ProviderID: StyledRangeStore] = [:]
21+
struct StyleElement: RangeStoreElement, CustomDebugStringConvertible {
22+
var capture: CaptureName?
23+
var modifiers: CaptureModifierSet
24+
25+
var isEmpty: Bool {
26+
capture == nil && modifiers.isEmpty
27+
}
28+
29+
func combineLowerPriority(_ other: StyleElement?) -> StyleElement {
30+
StyleElement(
31+
capture: self.capture ?? other?.capture,
32+
modifiers: modifiers.union(other?.modifiers ?? [])
33+
)
34+
}
35+
36+
func combineHigherPriority(_ other: StyleElement?) -> StyleElement {
37+
StyleElement(
38+
capture: other?.capture ?? self.capture,
39+
modifiers: modifiers.union(other?.modifiers ?? [])
40+
)
41+
}
42+
43+
var debugDescription: String {
44+
"\(capture?.stringValue ?? "(empty)"), \(modifiers)"
45+
}
46+
}
47+
48+
var _storage: [ProviderID: RangeStore<StyleElement>] = [:]
2249
weak var delegate: StyledRangeContainerDelegate?
2350

2451
/// Initialize the container with a list of provider identifiers. Each provider is given an id, they should be
@@ -28,13 +55,13 @@ class StyledRangeContainer {
2855
/// - providers: An array of identifiers given to providers.
2956
init(documentLength: Int, providers: [ProviderID]) {
3057
for provider in providers {
31-
_storage[provider] = StyledRangeStore(documentLength: documentLength)
58+
_storage[provider] = RangeStore<StyleElement>(documentLength: documentLength)
3259
}
3360
}
3461

3562
func addProvider(_ id: ProviderID, documentLength: Int) {
3663
assert(!_storage.keys.contains(id), "Provider already exists")
37-
_storage[id] = StyledRangeStore(documentLength: documentLength)
64+
_storage[id] = RangeStore<StyleElement>(documentLength: documentLength)
3865
}
3966

4067
func removeProvider(_ id: ProviderID) {
@@ -55,10 +82,18 @@ class StyledRangeContainer {
5582
///
5683
/// - Parameter range: The range to query.
5784
/// - Returns: An array of continuous styled runs.
58-
func runsIn(range: NSRange) -> [StyledRangeStoreRun] {
85+
func runsIn(range: NSRange) -> [RangeStoreRun<StyleElement>] {
86+
func combineLowerPriority(_ lhs: inout RangeStoreRun<StyleElement>, _ rhs: RangeStoreRun<StyleElement>) {
87+
lhs.value = lhs.value?.combineLowerPriority(rhs.value) ?? rhs.value
88+
}
89+
90+
func combineHigherPriority(_ lhs: inout RangeStoreRun<StyleElement>, _ rhs: RangeStoreRun<StyleElement>) {
91+
lhs.value = lhs.value?.combineHigherPriority(rhs.value) ?? rhs.value
92+
}
93+
5994
// Ordered by priority, lower = higher priority.
6095
var allRuns = _storage.sorted(by: { $0.key < $1.key }).map { $0.value.runs(in: range.intRange) }
61-
var runs: [StyledRangeStoreRun] = []
96+
var runs: [RangeStoreRun<StyleElement>] = []
6297

6398
var minValue = allRuns.compactMap { $0.last }.enumerated().min(by: { $0.1.length < $1.1.length })
6499

@@ -70,9 +105,9 @@ class StyledRangeContainer {
70105
for idx in (0..<allRuns.count).reversed() where idx != minRunIdx {
71106
guard let last = allRuns[idx].last else { continue }
72107
if idx < minRunIdx {
73-
minRun.combineHigherPriority(last)
108+
combineHigherPriority(&minRun, last)
74109
} else {
75-
minRun.combineLowerPriority(last)
110+
combineLowerPriority(&minRun, last)
76111
}
77112

78113
if last.length == minRun.length {
@@ -93,8 +128,8 @@ class StyledRangeContainer {
93128
}
94129

95130
func storageUpdated(replacedContentIn range: Range<Int>, withCount newLength: Int) {
96-
_storage.values.forEach {
97-
$0.storageUpdated(replacedCharactersIn: range, withCount: newLength)
131+
for key in _storage.keys {
132+
_storage[key]?.storageUpdated(replacedCharactersIn: range, withCount: newLength)
98133
}
99134
}
100135
}
@@ -109,11 +144,11 @@ extension StyledRangeContainer: HighlightProviderStateDelegate {
109144
/// - rangeToHighlight: The range to apply the highlights to.
110145
func applyHighlightResult(provider: ProviderID, highlights: [HighlightRange], rangeToHighlight: NSRange) {
111146
assert(rangeToHighlight != .notFound, "NSNotFound is an invalid highlight range")
112-
guard let storage = _storage[provider] else {
147+
guard var storage = _storage[provider] else {
113148
assertionFailure("No storage found for the given provider: \(provider)")
114149
return
115150
}
116-
var runs: [StyledRangeStoreRun] = []
151+
var runs: [RangeStoreRun<StyleElement>] = []
117152
var lastIndex = rangeToHighlight.lowerBound
118153

119154
for highlight in highlights {
@@ -123,10 +158,9 @@ extension StyledRangeContainer: HighlightProviderStateDelegate {
123158
continue // Skip! Overlapping
124159
}
125160
runs.append(
126-
StyledRangeStoreRun(
161+
RangeStoreRun<StyleElement>(
127162
length: highlight.range.length,
128-
capture: highlight.capture,
129-
modifiers: highlight.modifiers
163+
value: StyleElement(capture: highlight.capture, modifiers: highlight.modifiers)
130164
)
131165
)
132166
lastIndex = highlight.range.max
@@ -137,6 +171,7 @@ extension StyledRangeContainer: HighlightProviderStateDelegate {
137171
}
138172

139173
storage.set(runs: runs, for: rangeToHighlight.intRange)
174+
_storage[provider] = storage
140175
delegate?.styleContainerDidUpdate(in: rangeToHighlight)
141176
}
142177
}

Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+OffsetMetric.swift

Lines changed: 0 additions & 22 deletions
This file was deleted.

Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStoreRun.swift

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
//
2-
// StyledRangeStore+Internals.swift
2+
// RangeStore+Internals.swift
33
// CodeEditSourceEditor
44
//
55
// Created by Khan Winter on 10/25/24
66
//
77

88
import _RopeModule
99

10-
extension StyledRangeStore {
10+
extension RangeStore {
1111
/// Coalesce items before and after the given range.
1212
///
1313
/// 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 {
1616
/// rather than the queried one.
1717
///
1818
/// - Parameter range: The range of the item to coalesce around.
19-
func coalesceNearby(range: Range<Int>) {
19+
mutating func coalesceNearby(range: Range<Int>) {
2020
var index = findIndex(at: range.lastIndex).index
2121
if index < _guts.endIndex && _guts.index(after: index) != _guts.endIndex {
2222
coalesceRunAfter(index: &index)
@@ -30,11 +30,11 @@ extension StyledRangeStore {
3030
}
3131

3232
/// Check if the run and the run after it are equal, and if so remove the next one and concatenate the two.
33-
private func coalesceRunAfter(index: inout Index) {
33+
private mutating func coalesceRunAfter(index: inout Index) {
3434
let thisRun = _guts[index]
3535
let nextRun = _guts[_guts.index(after: index)]
3636

37-
if thisRun.styleCompare(nextRun) {
37+
if thisRun.compareValue(nextRun) {
3838
_guts.update(at: &index, by: { $0.length += nextRun.length })
3939

4040
var nextIndex = index
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
//
2-
// StyledRangeStore+FindIndex.swift
2+
// RangeStore+FindIndex.swift
33
// CodeEditSourceEditor
44
//
55
// Created by Khan Winter on 1/6/25.
66
//
77

8-
extension StyledRangeStore {
8+
extension RangeStore {
99
/// Finds a Rope index, given a string offset.
1010
/// - Parameter offset: The offset to query for.
1111
/// - Returns: The index of the containing element in the rope.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// RangeStore+OffsetMetric.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 10/25/24
6+
//
7+
8+
import _RopeModule
9+
10+
extension RangeStore {
11+
struct OffsetMetric: RopeMetric {
12+
typealias Element = StoredRun
13+
14+
func size(of summary: RangeStore.StoredRun.Summary) -> Int {
15+
summary.length
16+
}
17+
18+
func index(at offset: Int, in element: RangeStore.StoredRun) -> Int {
19+
return offset
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)