Skip to content

Commit 2f1fdad

Browse files
committed
Use Lock For Cache, Skip Depth Changes
1 parent 2c1af46 commit 2f1fdad

12 files changed

Lines changed: 247 additions & 207 deletions

Sources/CodeEditSourceEditor/Extensions/DispatchQueue+dispatchMainIfNot.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ extension DispatchQueue {
2727
/// executed if not already on the main thread.
2828
/// - Parameter item: The work item to execute.
2929
/// - Returns: The value of the work item.
30-
static func syncMainIfNot<T>(_ item: () -> T) -> T {
30+
static func waitMainIfNot<T>(_ item: () -> T) -> T {
3131
if Thread.isMainThread {
3232
return item()
3333
} else {
34-
return DispatchQueue.main.sync(execute: item)
34+
return DispatchQueue.main.asyncAndWait(execute: item)
3535
}
3636
}
3737
}

Sources/CodeEditSourceEditor/Extensions/TextView+/TextView+createReadBlock.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extension TextView {
3030
let range = NSRange(location..<end)
3131
return self?.textStorage.substring(from: range)?.data(using: String.nativeUTF16Encoding)
3232
}
33-
return DispatchQueue.syncMainIfNot(workItem)
33+
return DispatchQueue.waitMainIfNot(workItem)
3434
}
3535
}
3636
/// Creates a block for safely reading data for a text provider.
@@ -45,7 +45,7 @@ extension TextView {
4545
let workItem: () -> String? = {
4646
self?.textStorage.substring(from: range)
4747
}
48-
return DispatchQueue.syncMainIfNot(workItem)
48+
return DispatchQueue.waitMainIfNot(workItem)
4949
}
5050
}
5151
}

Sources/CodeEditSourceEditor/Gutter/LineFolding/LineFoldingModel.swift

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

Sources/CodeEditSourceEditor/Gutter/LineFolding/DefaultProviders/IndentationLineFoldProvider.swift renamed to Sources/CodeEditSourceEditor/LineFolding/FoldProviders/IndentationLineFoldProvider.swift

File renamed without changes.

Sources/CodeEditSourceEditor/Gutter/LineFolding/LineFoldProvider.swift renamed to Sources/CodeEditSourceEditor/LineFolding/FoldProviders/LineFoldProvider.swift

File renamed without changes.

Sources/CodeEditSourceEditor/Gutter/LineFolding/FoldRange.swift renamed to Sources/CodeEditSourceEditor/LineFolding/Model/FoldRange.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ import Foundation
1111
class FoldRange {
1212
var lineRange: ClosedRange<Int>
1313
var range: NSRange
14+
var depth: Int
1415
/// Ordered array of ranges that are nested in this fold.
1516
var subFolds: [FoldRange]
1617

1718
weak var parent: FoldRange?
1819

19-
init(lineRange: ClosedRange<Int>, range: NSRange, parent: FoldRange?, subFolds: [FoldRange]) {
20+
init(lineRange: ClosedRange<Int>, range: NSRange, depth: Int, parent: FoldRange?, subFolds: [FoldRange]) {
2021
self.lineRange = lineRange
2122
self.range = range
23+
self.depth = depth
2224
self.subFolds = subFolds
2325
self.parent = parent
2426
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//
2+
// LineFoldCalculator.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 5/9/25.
6+
//
7+
8+
import AppKit
9+
import CodeEditTextView
10+
import Combine
11+
12+
/// A utility that calculates foldable line ranges in a text document based on indentation depth.
13+
///
14+
/// `LineFoldCalculator` observes text edits and rebuilds fold regions asynchronously.
15+
/// Fold information is emitted via `rangesPublisher`.
16+
/// Notify the calculator it should re-calculate
17+
class LineFoldCalculator {
18+
weak var foldProvider: LineFoldProvider?
19+
weak var textView: TextView?
20+
21+
var rangesPublisher = CurrentValueSubject<[FoldRange], Never>([])
22+
23+
private let workQueue = DispatchQueue.global(qos: .default)
24+
25+
var textChangedReceiver = PassthroughSubject<(NSRange, Int), Never>()
26+
private var textChangedCancellable: AnyCancellable?
27+
28+
init(foldProvider: LineFoldProvider?, textView: TextView) {
29+
self.foldProvider = foldProvider
30+
self.textView = textView
31+
32+
textChangedCancellable = textChangedReceiver
33+
.throttle(for: 0.1, scheduler: RunLoop.main, latest: true)
34+
.sink { edit in
35+
self.buildFoldsForDocument(afterEditIn: edit.0, delta: edit.1)
36+
}
37+
}
38+
39+
/// Build out the folds for the entire document.
40+
///
41+
/// For each line in the document, find the indentation level using the ``levelProvider``. At each line, if the
42+
/// indent increases from the previous line, we start a new fold. If it decreases we end the fold we were in.
43+
private func buildFoldsForDocument(afterEditIn: NSRange, delta: Int) {
44+
workQueue.async {
45+
guard let textView = self.textView, let foldProvider = self.foldProvider else { return }
46+
var foldCache: [FoldRange] = []
47+
var currentFold: FoldRange?
48+
var currentDepth: Int = 0
49+
var iterator = textView.layoutManager.linesInRange(textView.documentRange)
50+
51+
var lines = self.getMoreLines(textView: textView, iterator: &iterator, foldProvider: foldProvider)
52+
while let lineChunk = lines {
53+
for (lineNumber, foldDepth) in lineChunk {
54+
// Start a new fold, going deeper to a new depth.
55+
if foldDepth > currentDepth {
56+
let newFold = FoldRange(
57+
lineRange: (lineNumber - 1)...(lineNumber - 1),
58+
range: .zero,
59+
depth: foldDepth,
60+
parent: currentFold,
61+
subFolds: []
62+
)
63+
64+
if currentFold == nil {
65+
foldCache.append(newFold)
66+
} else {
67+
currentFold?.subFolds.append(newFold)
68+
}
69+
currentFold = newFold
70+
} else if foldDepth < currentDepth {
71+
// End this fold, go shallower "popping" folds deeper than the new depth
72+
while let fold = currentFold, fold.depth > foldDepth {
73+
// close this fold at the current line
74+
fold.lineRange = fold.lineRange.lowerBound...lineNumber
75+
// move up
76+
currentFold = fold.parent
77+
}
78+
}
79+
80+
currentDepth = foldDepth
81+
}
82+
lines = self.getMoreLines(textView: textView, iterator: &iterator, foldProvider: foldProvider)
83+
}
84+
85+
// Clean up any hanging folds.
86+
while let fold = currentFold {
87+
fold.lineRange = fold.lineRange.lowerBound...textView.layoutManager.lineCount
88+
currentFold = fold.parent
89+
}
90+
91+
self.rangesPublisher.send(foldCache)
92+
}
93+
}
94+
95+
private func getMoreLines(
96+
textView: TextView,
97+
iterator: inout TextLayoutManager.RangeIterator,
98+
foldProvider: LineFoldProvider
99+
) -> [(index: Int, foldDepth: Int)]? {
100+
DispatchQueue.main.asyncAndWait {
101+
var results: [(index: Int, foldDepth: Int)] = []
102+
var count = 0
103+
while count < 50, let linePosition = iterator.next() {
104+
guard let substring = textView.textStorage.substring(from: linePosition.range) as NSString?,
105+
let foldDepth = foldProvider.foldLevelAtLine(
106+
linePosition.index,
107+
substring: substring
108+
) else {
109+
count += 1
110+
continue
111+
}
112+
113+
results.append((linePosition.index, foldDepth))
114+
count += 1
115+
}
116+
if results.isEmpty && count == 0 {
117+
return nil
118+
}
119+
return results
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)