@@ -9,6 +9,99 @@ import AppKit
99import CodeEditTextView
1010import Combine
1111
12+ class LineFoldCalculator {
13+ weak var foldProvider : LineFoldProvider ?
14+ weak var textView : TextView ?
15+
16+ var rangesPublisher = CurrentValueSubject < [ FoldRange ] , Never > ( [ ] )
17+
18+ private let workQueue = DispatchQueue ( label: " app.codeedit.line-folds " )
19+
20+ var textChangedReceiver = PassthroughSubject < Void , Never > ( )
21+ private var textChangedCancellable : AnyCancellable ?
22+
23+ init ( foldProvider: LineFoldProvider ? , textView: TextView ) {
24+ self . foldProvider = foldProvider
25+ self . textView = textView
26+
27+ textChangedCancellable = textChangedReceiver. throttle ( for: 0.1 , scheduler: RunLoop . main, latest: true ) . sink {
28+ self . buildFoldsForDocument ( )
29+ }
30+ }
31+
32+ /// Build out the folds for the entire document.
33+ ///
34+ /// For each line in the document, find the indentation level using the ``levelProvider``. At each line, if the
35+ /// indent increases from the previous line, we start a new fold. If it decreases we end the fold we were in.
36+ private func buildFoldsForDocument( ) {
37+ workQueue. async {
38+ guard let textView = self . textView, let foldProvider = self . foldProvider else { return }
39+ var foldCache : [ FoldRange ] = [ ]
40+ var currentFold : FoldRange ?
41+ var currentDepth : Int = 0
42+ var iterator = textView. layoutManager. linesInRange ( textView. documentRange)
43+
44+ var lines = self . getMoreLines ( textView: textView, iterator: & iterator, foldProvider: foldProvider)
45+ while !lines. isEmpty {
46+ for (lineNumber, foldDepth) in lines {
47+ // Start a new fold
48+ if foldDepth > currentDepth {
49+ let newFold = FoldRange (
50+ lineRange: ( lineNumber - 1 ) ... ( lineNumber - 1 ) ,
51+ range: . zero,
52+ parent: currentFold,
53+ subFolds: [ ]
54+ )
55+
56+ if currentFold == nil {
57+ foldCache. append ( newFold)
58+ } else {
59+ currentFold? . subFolds. append ( newFold)
60+ }
61+ currentFold = newFold
62+ } else if foldDepth < currentDepth {
63+ // End this fold
64+ if let fold = currentFold {
65+ fold. lineRange = fold. lineRange. lowerBound... lineNumber
66+ }
67+ currentFold = currentFold? . parent
68+ }
69+
70+ currentDepth = foldDepth
71+ }
72+ lines = self . getMoreLines ( textView: textView, iterator: & iterator, foldProvider: foldProvider)
73+ }
74+
75+ self . rangesPublisher. send ( foldCache)
76+ }
77+ }
78+
79+ private func getMoreLines(
80+ textView: TextView ,
81+ iterator: inout TextLayoutManager . RangeIterator ,
82+ foldProvider: LineFoldProvider
83+ ) -> [ ( index: Int , foldDepth: Int ) ] {
84+ DispatchQueue . main. asyncAndWait {
85+ var results : [ ( index: Int , foldDepth: Int ) ] = [ ]
86+ var count = 0
87+ while count < 50 , let linePosition = iterator. next ( ) {
88+ guard let substring = textView. textStorage. substring ( from: linePosition. range) as NSString ? ,
89+ let foldDepth = foldProvider. foldLevelAtLine (
90+ linePosition. index,
91+ substring: substring
92+ ) else {
93+ count += 1
94+ continue
95+ }
96+
97+ results. append ( ( linePosition. index, foldDepth) )
98+ count += 1
99+ }
100+ return results
101+ }
102+ }
103+ }
104+
12105/// # Basic Premise
13106///
14107/// We need to update, delete, or add fold ranges in the invalidated lines.
@@ -21,91 +114,35 @@ import Combine
21114class LineFoldingModel : NSObject , NSTextStorageDelegate {
22115 /// An ordered tree of fold ranges in a document. Can be traversed using ``FoldRange/parent``
23116 /// and ``FoldRange/subFolds``.
24- private var foldCache : [ FoldRange ] = [ ]
117+ @Published var foldCache : [ FoldRange ] = [ ]
118+ private var calculator : LineFoldCalculator
119+ private var cancellable : AnyCancellable ?
25120
26- weak var foldProvider : LineFoldProvider ?
27121 weak var textView : TextView ?
28122
29- lazy var foldsUpdatedPublisher = PassthroughSubject < Void , Never > ( )
30-
31123 init ( textView: TextView , foldProvider: LineFoldProvider ? ) {
32124 self . textView = textView
33- self . foldProvider = foldProvider
125+ self . calculator = LineFoldCalculator ( foldProvider: foldProvider , textView : textView )
34126 super. init ( )
35127 textView. addStorageDelegate ( self )
36- buildFoldsForDocument ( )
128+ cancellable = self . calculator. rangesPublisher. receive ( on: RunLoop . main) . assign ( to: \. foldCache, on: self )
129+ calculator. textChangedReceiver. send ( )
37130 }
38131
39132 func getFolds( in lineRange: ClosedRange < Int > ) -> [ FoldRange ] {
40133 foldCache. filter ( { $0. lineRange. overlaps ( lineRange) } )
41134 }
42135
43- /// Build out the ``foldCache`` for the entire document.
44- ///
45- /// For each line in the document, find the indentation level using the ``levelProvider``. At each line, if the
46- /// indent increases from the previous line, we start a new fold. If it decreases we end the fold we were in.
47- func buildFoldsForDocument( ) {
48- guard let textView, let foldProvider else { return }
49- foldCache. removeAll ( keepingCapacity: true )
50-
51- var currentFold : FoldRange ?
52- var currentDepth : Int = 0
53- for linePosition in textView. layoutManager. linesInRange ( textView. documentRange) {
54- guard let foldDepth = foldProvider. foldLevelAtLine (
55- linePosition. index,
56- layoutManager: textView. layoutManager,
57- textStorage: textView. textStorage
58- ) else {
59- continue
60- }
61-
62- // Start a new fold
63- if foldDepth > currentDepth {
64- let newFold = FoldRange (
65- lineRange: ( linePosition. index - 1 ) ... ( linePosition. index - 1 ) ,
66- range: . zero,
67- parent: currentFold,
68- subFolds: [ ]
69- )
70-
71- if currentFold == nil {
72- foldCache. append ( newFold)
73- } else {
74- currentFold? . subFolds. append ( newFold)
75- }
76- currentFold = newFold
77- } else if foldDepth < currentDepth {
78- // End this fold
79- if let fold = currentFold {
80- fold. lineRange = fold. lineRange. lowerBound... linePosition. index
81- }
82- currentFold = currentFold? . parent
83- }
84-
85- currentDepth = foldDepth
86- }
87-
88- foldsUpdatedPublisher. send ( )
89- }
90-
91- func invalidateLine( lineNumber: Int ) {
92- // TODO: Check if we need to rebuild, or even better, incrementally update the tree.
93-
94- // Temporary
95- buildFoldsForDocument ( )
96- }
97-
98136 func textStorage(
99137 _ textStorage: NSTextStorage ,
100138 didProcessEditing editedMask: NSTextStorageEditActions ,
101139 range editedRange: NSRange ,
102140 changeInLength delta: Int
103141 ) {
104- guard editedMask. contains ( . editedCharacters) ,
105- let lineNumber = textView? . layoutManager. textLineForOffset ( editedRange. location) ? . index else {
142+ guard editedMask. contains ( . editedCharacters) else {
106143 return
107144 }
108- invalidateLine ( lineNumber : lineNumber )
145+ calculator . textChangedReceiver . send ( )
109146 }
110147
111148 /// Finds the deepest cached depth of the fold for a line number.
@@ -119,20 +156,20 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate {
119156 /// - Parameter lineNumber: The line number to query, zero-indexed.
120157 /// - Returns: The deepest cached fold and depth of the fold if it was found.
121158 func getCachedFoldAt( lineNumber: Int ) -> ( range: FoldRange , depth: Int ) ? {
122- binarySearchFoldsArray ( lineNumber: lineNumber, folds: foldCache, currentDepth: 0 )
159+ binarySearchFoldsArray ( lineNumber: lineNumber, folds: foldCache, currentDepth: 0 , findDeepest : true )
123160 }
124161}
125162
126163// MARK: - Search Folds
127164
128165private extension LineFoldingModel {
129-
130166 /// A generic function for searching an ordered array of fold ranges.
131167 /// - Returns: The found range and depth it was found at, if it exists.
132168 func binarySearchFoldsArray(
133169 lineNumber: Int ,
134170 folds: borrowing [ FoldRange ] ,
135- currentDepth: Int
171+ currentDepth: Int ,
172+ findDeepest: Bool
136173 ) -> ( range: FoldRange , depth: Int ) ? {
137174 var low = 0
138175 var high = folds. count - 1
@@ -143,11 +180,16 @@ private extension LineFoldingModel {
143180
144181 if fold. lineRange. contains ( lineNumber) {
145182 // Search deeper into subFolds, if any
146- return binarySearchFoldsArray (
147- lineNumber: lineNumber,
148- folds: fold. subFolds,
149- currentDepth: currentDepth + 1
150- ) ?? ( fold, currentDepth)
183+ if findDeepest {
184+ return binarySearchFoldsArray (
185+ lineNumber: lineNumber,
186+ folds: fold. subFolds,
187+ currentDepth: currentDepth + 1 ,
188+ findDeepest: findDeepest
189+ ) ?? ( fold, currentDepth)
190+ } else {
191+ return ( fold, currentDepth)
192+ }
151193 } else if lineNumber < fold. lineRange. lowerBound {
152194 high = mid - 1
153195 } else {
0 commit comments