@@ -14,132 +14,140 @@ import Combine
1414/// `LineFoldCalculator` observes text edits and rebuilds fold regions asynchronously.
1515/// Fold information is emitted via `rangesPublisher`.
1616/// Notify the calculator it should re-calculate
17- class LineFoldCalculator {
17+ actor LineFoldCalculator {
1818 weak var foldProvider : LineFoldProvider ?
19- weak var textView : TextView ?
19+ weak var controller : TextViewController ?
2020
21- var rangesPublisher = CurrentValueSubject < LineFoldStorage , Never > ( . init ( documentLength : 0 ) )
21+ var valueStream : AsyncStream < LineFoldStorage >
2222
23- private let workQueue = DispatchQueue . global ( qos: . default)
23+ private var valueStreamContinuation : AsyncStream < LineFoldStorage > . Continuation
24+ private var textChangedTask : Task < Void , Never > ?
2425
25- var textChangedReceiver = PassthroughSubject < ( NSRange , Int ) , Never > ( )
26- private var textChangedCancellable : AnyCancellable ?
27-
28- init ( foldProvider: LineFoldProvider ? , textView: TextView ) {
26+ init (
27+ foldProvider: LineFoldProvider ? ,
28+ controller: TextViewController ,
29+ textChangedStream: AsyncStream < ( NSRange , Int ) >
30+ ) {
2931 self . foldProvider = foldProvider
30- self . textView = textView
32+ self . controller = controller
33+ ( valueStream, valueStreamContinuation) = AsyncStream< LineFoldStorage> . makeStream( )
34+ Task { await listenToTextChanges ( textChangedStream: textChangedStream) }
35+ }
36+
37+ deinit {
38+ textChangedTask? . cancel ( )
39+ }
3140
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 )
41+ private func listenToTextChanges ( textChangedStream : AsyncStream < ( NSRange , Int ) > ) {
42+ textChangedTask = Task {
43+ for await edit in textChangedStream {
44+ await buildFoldsForDocument ( afterEditIn: edit. 0 , delta: edit. 1 )
3645 }
46+ }
3747 }
3848
3949 /// Build out the folds for the entire document.
4050 ///
4151 /// For each line in the document, find the indentation level using the ``levelProvider``. At each line, if the
4252 /// 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 : [ LineFoldStorage . RawFold ] = [ ]
47- // Depth: Open range
48- var openFolds : [ Int : LineFoldStorage . RawFold ] = [ : ]
49- var currentDepth : Int = 0
50- var iterator = textView. layoutManager. linesInRange ( textView. documentRange)
53+ private func buildFoldsForDocument( afterEditIn: NSRange , delta: Int ) async {
54+ guard let controller = self . controller, let foldProvider = self . foldProvider else { return }
55+ let documentRange = await controller. textView. documentRange
56+ var foldCache : [ LineFoldStorage . RawFold ] = [ ]
57+ // Depth: Open range
58+ var openFolds : [ Int : LineFoldStorage . RawFold ] = [ : ]
59+ var currentDepth : Int = 0
60+ var iterator = await controller. textView. layoutManager. linesInRange ( documentRange)
61+
62+ var lines = await self . getMoreLines (
63+ controller: controller,
64+ iterator: & iterator,
65+ previousDepth: currentDepth,
66+ foldProvider: foldProvider
67+ )
68+ while let lineChunk = lines {
69+ for lineInfo in lineChunk where lineInfo. depth > 0 {
70+ // Start a new fold, going deeper to a new depth.
71+ if lineInfo. depth > currentDepth {
72+ let newFold = LineFoldStorage . RawFold (
73+ depth: lineInfo. depth,
74+ range: lineInfo. rangeIndice..< lineInfo. rangeIndice
75+ )
76+ openFolds [ newFold. depth] = newFold
77+ } else if lineInfo. depth < currentDepth {
78+ // End open folds > received depth
79+ for openFold in openFolds. values. filter ( { $0. depth > lineInfo. depth } ) {
80+ openFolds. removeValue ( forKey: openFold. depth)
81+ foldCache. append (
82+ LineFoldStorage . RawFold (
83+ depth: openFold. depth,
84+ range: openFold. range. lowerBound..< lineInfo. rangeIndice
85+ )
86+ )
87+ }
88+ }
5189
52- var lines = self . getMoreLines (
53- textView: textView,
90+ currentDepth = lineInfo. depth
91+ }
92+ lines = await self . getMoreLines (
93+ controller: controller,
5494 iterator: & iterator,
5595 previousDepth: currentDepth,
5696 foldProvider: foldProvider
5797 )
58- while let lineChunk = lines {
59- for lineInfo in lineChunk where lineInfo. depth > 0 {
60- // Start a new fold, going deeper to a new depth.
61- if lineInfo. depth > currentDepth {
62- let newFold = LineFoldStorage . RawFold (
63- depth: lineInfo. depth,
64- range: lineInfo. rangeIndice..< lineInfo. rangeIndice
65- )
66- openFolds [ newFold. depth] = newFold
67- } else if lineInfo. depth < currentDepth {
68- // End open folds > received depth
69- for openFold in openFolds. values. filter ( { $0. depth > lineInfo. depth } ) {
70- openFolds. removeValue ( forKey: openFold. depth)
71- foldCache. append (
72- LineFoldStorage . RawFold (
73- depth: openFold. depth,
74- range: openFold. range. lowerBound..< lineInfo. rangeIndice
75- )
76- )
77- }
78- }
98+ }
7999
80- currentDepth = lineInfo. depth
81- }
82- lines = self . getMoreLines (
83- textView: textView,
84- iterator: & iterator,
85- previousDepth: currentDepth,
86- foldProvider: foldProvider
100+ // Clean up any hanging folds.
101+ for fold in openFolds. values {
102+ foldCache. append (
103+ LineFoldStorage . RawFold (
104+ depth: fold. depth,
105+ range: fold. range. lowerBound..< documentRange. length
87106 )
88- }
107+ )
108+ }
89109
90- // Clean up any hanging folds.
91- for fold in openFolds. values {
92- foldCache. append (
93- LineFoldStorage . RawFold (
94- depth: fold. depth,
95- range: fold. range. lowerBound..< textView. length
96- )
97- )
110+ let attachments = await controller. textView. layoutManager. attachments
111+ . getAttachmentsOverlapping ( documentRange)
112+ . compactMap { $0. attachment as? LineFoldPlaceholder }
113+ . map {
114+ LineFoldStorage . DepthStartPair ( depth: $0. fold. depth, start: $0. fold. range. lowerBound)
98115 }
99116
100- let storage = LineFoldStorage (
101- documentLength: textView. length,
102- folds: foldCache. sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } ) ,
103- collapsedProvider: {
104- Set (
105- textView. layoutManager. attachments
106- . getAttachmentsOverlapping ( textView. documentRange)
107- . compactMap { $0. attachment as? LineFoldPlaceholder }
108- . map {
109- LineFoldStorage . DepthStartPair ( depth: $0. fold. depth, start: $0. fold. range. lowerBound)
110- }
111- )
112- }
113- )
114- self . rangesPublisher. send ( storage)
115- }
117+ let storage = LineFoldStorage (
118+ documentLength: foldCache. max (
119+ by: { $0. range. upperBound < $1. range. upperBound }
120+ ) ? . range. upperBound ?? documentRange. length,
121+ folds: foldCache. sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } ) ,
122+ collapsedProvider: { Set ( attachments) }
123+ )
124+ valueStreamContinuation. yield ( storage)
116125 }
117126
127+ @MainActor
118128 private func getMoreLines(
119- textView : TextView ,
129+ controller : TextViewController ,
120130 iterator: inout TextLayoutManager . RangeIterator ,
121131 previousDepth: Int ,
122132 foldProvider: LineFoldProvider
123133 ) -> [ LineFoldProviderLineInfo ] ? {
124- DispatchQueue . main. asyncAndWait {
125- var results : [ LineFoldProviderLineInfo ] = [ ]
126- var count = 0
127- var previousDepth : Int = previousDepth
128- while count < 50 , let linePosition = iterator. next ( ) {
129- let foldInfo = foldProvider. foldLevelAtLine (
130- lineNumber: linePosition. index,
131- lineRange: linePosition. range,
132- previousDepth: previousDepth,
133- text: textView. textStorage
134- )
135- results. append ( contentsOf: foldInfo)
136- count += 1
137- previousDepth = foldInfo. max ( by: { $0. depth < $1. depth } ) ? . depth ?? previousDepth
138- }
139- if results. isEmpty && count == 0 {
140- return nil
141- }
142- return results
134+ var results : [ LineFoldProviderLineInfo ] = [ ]
135+ var count = 0
136+ var previousDepth : Int = previousDepth
137+ while count < 50 , let linePosition = iterator. next ( ) {
138+ let foldInfo = foldProvider. foldLevelAtLine (
139+ lineNumber: linePosition. index,
140+ lineRange: linePosition. range,
141+ previousDepth: previousDepth,
142+ controller: controller
143+ )
144+ results. append ( contentsOf: foldInfo)
145+ count += 1
146+ previousDepth = foldInfo. max ( by: { $0. depth < $1. depth } ) ? . depth ?? previousDepth
147+ }
148+ if results. isEmpty && count == 0 {
149+ return nil
143150 }
151+ return results
144152 }
145153}
0 commit comments