Skip to content

Commit 6214f2d

Browse files
committed
I've Since Realized This Is Incorrect
1 parent 62c1731 commit 6214f2d

File tree

5 files changed

+111
-204
lines changed

5 files changed

+111
-204
lines changed

Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift

Lines changed: 0 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -155,168 +155,6 @@ extension Highlighter: NSTextStorageDelegate {
155155
changeInLength delta: Int
156156
) {
157157
guard editedMask.contains(.editedCharacters) else { return }
158-
159-
<<<<<<< Updated upstream
160-
queryHighlights(for: rangesToQuery)
161-
}
162-
163-
/// Highlights the given ranges
164-
/// - Parameter ranges: The ranges to request highlights for.
165-
func queryHighlights(for rangesToHighlight: [NSRange]) {
166-
guard let textView else { return }
167-
168-
DispatchQueue.dispatchMainIfNot {
169-
for range in rangesToHighlight {
170-
self.highlightProvider?.queryHighlightsFor(textView: textView, range: range) { [weak self] highlights in
171-
assert(Thread.isMainThread, "Highlighted ranges called on non-main thread.")
172-
self?.applyHighlightResult(highlights, rangeToHighlight: range)
173-
}
174-
}
175-
}
176-
}
177-
178-
/// Applies a highlight query result to the text view.
179-
/// - Parameters:
180-
/// - results: The result of a highlight query.
181-
/// - rangeToHighlight: The range to apply the highlight to.
182-
private func applyHighlightResult(_ results: Result<[HighlightRange], Error>, rangeToHighlight: NSRange) {
183-
pendingSet.remove(integersIn: rangeToHighlight)
184-
185-
switch results {
186-
case let .failure(error):
187-
if case HighlightProvidingError.operationCancelled = error {
188-
invalidate(range: rangeToHighlight)
189-
} else {
190-
Self.logger.error("Failed to query highlight range: \(error)")
191-
}
192-
case let .success(results):
193-
guard let attributeProvider = self.attributeProvider,
194-
visibleSet.intersects(integersIn: rangeToHighlight) else {
195-
return
196-
}
197-
validSet.formUnion(IndexSet(integersIn: rangeToHighlight))
198-
199-
// Loop through each highlight and modify the textStorage accordingly.
200-
textView?.textStorage.beginEditing()
201-
202-
// Create a set of indexes that were not highlighted.
203-
var ignoredIndexes = IndexSet(integersIn: rangeToHighlight)
204-
205-
// Apply all highlights that need color
206-
for highlight in results
207-
where textView?.documentRange.upperBound ?? 0 > highlight.range.upperBound {
208-
textView?.textStorage.setAttributes(
209-
attributeProvider.attributesFor(highlight.capture),
210-
range: highlight.range
211-
)
212-
213-
// Remove highlighted indexes from the "ignored" indexes.
214-
ignoredIndexes.remove(integersIn: highlight.range)
215-
}
216-
217-
// For any indices left over, we need to apply normal attributes to them
218-
// This fixes the case where characters are changed to have a non-text color, and then are skipped when
219-
// they need to be changed back.
220-
for ignoredRange in ignoredIndexes.rangeView
221-
where textView?.documentRange.upperBound ?? 0 > ignoredRange.upperBound {
222-
textView?.textStorage.setAttributes(attributeProvider.attributesFor(nil), range: NSRange(ignoredRange))
223-
}
224-
225-
textView?.textStorage.endEditing()
226-
textView?.layoutManager.invalidateLayoutForRange(rangeToHighlight)
227-
}
228-
}
229-
230-
/// Gets the next `NSRange` to highlight based on the invalid set, visible set, and pending set.
231-
/// - Returns: An `NSRange` to highlight if it could be fetched.
232-
func getNextRange() -> NSRange? {
233-
let set: IndexSet = IndexSet(integersIn: textView?.documentRange ?? .zero) // All text
234-
.subtracting(validSet) // Subtract valid = Invalid set
235-
.intersection(visibleSet) // Only visible indexes
236-
.subtracting(pendingSet) // Don't include pending indexes
237-
238-
guard let range = set.rangeView.first else {
239-
return nil
240-
}
241-
242-
// Chunk the ranges in sets of rangeChunkLimit characters.
243-
return NSRange(
244-
location: range.lowerBound,
245-
length: min(rangeChunkLimit, range.upperBound - range.lowerBound)
246-
)
247-
}
248-
}
249-
250-
// MARK: - Visible Content Updates
251-
252-
private extension Highlighter {
253-
private func updateVisibleSet(textView: TextView) {
254-
if let newVisibleRange = textView.visibleTextRange {
255-
visibleSet = IndexSet(integersIn: newVisibleRange)
256-
}
257-
}
258-
259-
/// Updates the view to highlight newly visible text when the textview is scrolled or bounds change.
260-
@objc func visibleTextChanged(_ notification: Notification) {
261-
let textView: TextView
262-
if let clipView = notification.object as? NSClipView,
263-
let documentView = clipView.enclosingScrollView?.documentView as? TextView {
264-
textView = documentView
265-
} else if let scrollView = notification.object as? NSScrollView,
266-
let documentView = scrollView.documentView as? TextView {
267-
textView = documentView
268-
} else {
269-
return
270-
}
271-
272-
updateVisibleSet(textView: textView)
273-
274-
// Any indices that are both *not* valid and in the visible text range should be invalidated
275-
let newlyInvalidSet = visibleSet.subtracting(validSet)
276-
277-
for range in newlyInvalidSet.rangeView.map({ NSRange($0) }) {
278-
invalidate(range: range)
279-
}
280-
}
281-
}
282-
283-
// MARK: - Editing
284-
285-
extension Highlighter {
286-
func storageDidEdit(editedRange: NSRange, delta: Int) {
287-
guard let textView else { return }
288-
289-
let range = NSRange(location: editedRange.location, length: editedRange.length - delta)
290-
if delta > 0 {
291-
visibleSet.insert(range: editedRange)
292-
}
293-
294-
updateVisibleSet(textView: textView)
295-
296-
highlightProvider?.applyEdit(textView: textView, range: range, delta: delta) { [weak self] result in
297-
switch result {
298-
case let .success(invalidIndexSet):
299-
let indexSet = invalidIndexSet.union(IndexSet(integersIn: editedRange))
300-
301-
for range in indexSet.rangeView {
302-
self?.invalidate(range: NSRange(range))
303-
}
304-
case let .failure(error):
305-
if case HighlightProvidingError.operationCancelled = error {
306-
self?.invalidate(range: range)
307-
return
308-
} else {
309-
Self.logger.error("Failed to apply edit. Query returned with error: \(error)")
310-
}
311-
}
312-
}
313-
}
314-
315-
func storageWillEdit(editedRange: NSRange) {
316-
guard let textView else { return }
317-
highlightProvider?.willApplyEdit(textView: textView, range: editedRange)
318-
=======
319158
// self.storageWillEdit(editedRange: editedRange)
320-
>>>>>>> Stashed changes
321159
}
322160
}

Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/RangeStore+Node.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77

88
extension RangeStore {
99
final class Node {
10-
let order: Int
11-
var keys: [KeyValue]
12-
var children: [Node]
13-
var maxContainingEndpoint: UInt32
10+
private let order: Int
11+
private var keys: [KeyValue]
12+
private var children: [Node]
13+
private var maxContainingEndpoint: UInt32
1414

15-
var isLeaf: Bool { children.isEmpty }
15+
private var isLeaf: Bool { children.isEmpty }
1616

1717
init(order: Int) {
1818
self.order = order
@@ -24,15 +24,15 @@ extension RangeStore {
2424
self.children.reserveCapacity(order)
2525
}
2626

27-
func max() -> KeyValue? {
27+
private func max() -> KeyValue? {
2828
var node = self
2929
while !node.isLeaf {
3030
node = node.children[node.children.count - 1]
3131
}
3232
return node.keys.last
3333
}
3434

35-
func min() -> KeyValue? {
35+
private func min() -> KeyValue? {
3636
var node = self
3737
while !node.isLeaf {
3838
node = node.children[0]

Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/RangeStore.swift

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,30 @@ fileprivate extension Range<UInt32> {
1616
var generic: Range<Int> {
1717
return Int(startIndex)..<Int(endIndex)
1818
}
19-
19+
20+
/// Determines if the given range is entirely contained by this range.
21+
/// - Parameter other: The range to compare.
22+
/// - Returns: If the range's indices are equal to or inside this range's indices, returns true.
2023
func strictContains(_ other: Range<UInt32>) -> Bool {
2124
other.startIndex >= startIndex && other.endIndex <= endIndex
2225
}
23-
24-
func subtract(_ other: Range<UInt32>) -> Range<UInt32> {
25-
assert(!strictContains(other), "Subtract cannot act on a range that is larger than the given range")
26-
if startIndex < other.startIndex {
27-
return startIndex..<other.startIndex
28-
} else {
29-
return other.endIndex..<endIndex
30-
}
31-
}
3226
}
3327

28+
/// A `RangeStore` is a generic, B-tree-backed data structure that stores key-value pairs where the keys are ranges.
29+
///
30+
/// This class allows efficient insertion, deletion, and querying of ranges, offering the flexibility to clear entire ranges
31+
/// of values or remove single values. The underlying B-tree gives logarithmic time complexity for most operations.
32+
///
33+
/// - Note: The internal keys are stored as `Range<UInt32>` to optimize memory usage, and are converted from `Range<Int>`
34+
/// when interacting with public
35+
///
36+
/// ```swift
37+
/// let store = RangeStore<String>()
38+
/// store.insert(value: "A", range: 1..<5)
39+
/// store.delete(overlapping: 3..<4) // Clears part of a range.
40+
/// let results = store.ranges(overlapping: 1..<6)
41+
/// ```
42+
3443
package final class RangeStore<Element> {
3544
/// Using UInt32 as we can halve the memory use of keys in the tree for the small cost of converting them
3645
/// in public calls.
@@ -44,35 +53,84 @@ package final class RangeStore<Element> {
4453
private let order: Int
4554
private var root: Node
4655

56+
/// Initialize the store.
57+
/// - Parameter order: The order of the internal B-tree. Defaults to `4`.
4758
init(order: Int = 4) {
4859
self.order = order
4960
self.root = Node(order: self.order)
5061
}
5162

63+
/// Insert a key-value pair into the store.
64+
/// - Parameters:
65+
/// - value: The value to insert.
66+
/// - range: The range to insert the value at.
5267
func insert(value: Element, range: Range<Int>) {
5368
let key = Key.from(range)
5469
root.insert(value: value, range: key)
5570
}
5671

72+
/// Delete a range from the store.
73+
/// The range must match exactly with a range in the store, or it will not be deleted.
74+
/// See ``delete(overlapping:)`` for deleting unknown ranges.
75+
/// - Parameter range: The range to remove.
76+
/// - Returns: Whether or not a value was removed from the store.
5777
@discardableResult
5878
func delete(range: Range<Int>) -> Bool {
5979
let key = Key.from(range)
6080
return root.delete(range: key)
6181
}
6282

63-
// func deleteRanges(overlapping range: Range<Int>) {
64-
// let key = Key.from(range)
65-
// let keyPairs = ranges(overlapping: range)
66-
// for pair in keyPairs {
67-
// root.delete(range: pair.key)
68-
// if !key.strictContains(pair.key) {
69-
// root.insert(value: pair.value, range: key.)
70-
// }
71-
// }
72-
// }
83+
/// Clears a range and all associated values.
84+
///
85+
/// This is different from `delete`, which deletes a single already-known range from the store. This method removes
86+
/// a range entirely, trimming ranges to effectively clear a range of values.
87+
///
88+
/// ```
89+
/// 1 2 3 4 5 6 # Indices
90+
/// |-----| |-----| # Stored Ranges
91+
///
92+
/// - Call `delete` 3..<5
93+
///
94+
/// 1 2 3 4 5 6 # Indices
95+
/// |--| |--| # Stored Ranges
96+
/// ```
97+
///
98+
/// - Complexity: `O(n)` worst case, `O(m log n)` for small ranges where `m` is the number of results returned.
99+
/// - Parameter range: The range to clear.
100+
func delete(overlapping range: Range<Int>) {
101+
let key = Key.from(range)
102+
let keySet = IndexSet(integersIn: key.range)
73103

74-
func ranges(overlapping range: Range<Int>) -> [KeyValue] {
104+
let keyPairs = root.findRanges(overlapping: key)
105+
for pair in keyPairs {
106+
root.delete(range: pair.key)
107+
108+
// Re-Insert any ranges that overlapped with the key but weren't encapsulated.
109+
if !key.strictContains(pair.key) {
110+
let remainingSet = IndexSet(integersIn: pair.key.range).subtracting(keySet)
111+
for range in remainingSet.rangeView {
112+
let newKey = Key.from(range)
113+
root.insert(value: pair.value, range: newKey)
114+
}
115+
}
116+
}
117+
}
118+
119+
/// Search for all ranges overlapping the given range.
120+
/// ```
121+
/// 1 2 3 4 5 6 # Indices
122+
/// |-----| |-----| # Stored Ranges
123+
///
124+
/// - Call `ranges(overlapping:)` 1..<5
125+
/// - Returns: [1..<4, 4..<7]
126+
/// ```
127+
/// - Complexity: `O(n)` worst case, `O(m log n)` for small ranges where `m` is the number of results returned.
128+
/// - Parameter range: The range to search.
129+
/// - Returns: All key-value pairs that overlap the given range.
130+
func ranges(overlapping range: Range<Int>) -> [(key: Range<Int>, value: Element)] {
75131
let key = Key.from(range)
76-
return root.findRanges(overlapping: key)
132+
return root.findRanges(overlapping: key).map { keyValue in
133+
(keyValue.key.generic, keyValue.value)
134+
}
77135
}
78136
}

Tests/CodeEditSourceEditorTests/Highlighting/RangeStoreBenchmarks.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import XCTest
44
class RangeStoreBenchmarkTests: XCTestCase {
55
var rng = RandomNumberGeneratorWithSeed(seed: 942)
66

7+
// to keep these stable
78
struct RandomNumberGeneratorWithSeed: RandomNumberGenerator {
89
init(seed: Int) { srand48(seed) }
910
func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) } // swiftlint:disable:this legacy_random

0 commit comments

Comments
 (0)