@@ -45,7 +45,7 @@ public final class IncrementalParseReusedNodeCollector:
45
45
/// occurred since it was created.
46
46
public final class IncrementalParseTransition {
47
47
fileprivate let previousTree : SourceFileSyntax
48
- fileprivate let edits : [ SourceEdit ]
48
+ fileprivate let edits : ConcurrentEdits
49
49
fileprivate let reusedDelegate : IncrementalParseReusedNodeDelegate ?
50
50
51
51
/// - Parameters:
@@ -57,19 +57,122 @@ public final class IncrementalParseTransition {
57
57
/// 2. should be in increasing source offset order.
58
58
/// - reusedNodeDelegate: Optional delegate to accept information about the
59
59
/// reused regions and nodes.
60
+ @available ( * , deprecated, message: " Use the initializer taking 'ConcurrentEdits' instead " )
61
+ public convenience init ( previousTree: SourceFileSyntax ,
62
+ edits: [ SourceEdit ] ,
63
+ reusedNodeDelegate: IncrementalParseReusedNodeDelegate ? = nil ) {
64
+ self . init (
65
+ previousTree: previousTree,
66
+ edits: ConcurrentEdits ( concurrent: edits) ,
67
+ reusedNodeDelegate: reusedNodeDelegate
68
+ )
69
+ }
70
+
71
+ /// - Parameters:
72
+ /// - previousTree: The previous tree to do lookups on.
73
+ /// - edits: The edits that have occurred since the last parse that resulted
74
+ /// in the new source that is about to be parsed.
75
+ /// - reusedNodeDelegate: Optional delegate to accept information about the
76
+ /// reused regions and nodes.
60
77
public init ( previousTree: SourceFileSyntax ,
61
- edits: [ SourceEdit ] ,
78
+ edits: ConcurrentEdits ,
62
79
reusedNodeDelegate: IncrementalParseReusedNodeDelegate ? = nil ) {
63
- assert ( IncrementalParseTransition . isEditArrayValid ( edits) )
64
80
self . previousTree = previousTree
65
81
self . edits = edits
66
82
self . reusedDelegate = reusedNodeDelegate
67
83
}
84
+ }
85
+
86
+ fileprivate extension Sequence where Element: Comparable {
87
+ var isSorted : Bool {
88
+ return zip ( self , self . dropFirst ( ) ) . allSatisfy ( { $0. 0 < $0. 1 } )
89
+ }
90
+ }
68
91
69
- /// Checks the requirements for the edits array to:
70
- /// 1. not be overlapping.
71
- /// 2. should be in increasing source offset order.
72
- public static func isEditArrayValid( _ edits: [ SourceEdit ] ) -> Bool {
92
+ /// Edits that are applied **simultaneously**. That is, the offsets of all edits
93
+ /// refer to the original string and are not shifted by previous edits. For
94
+ /// example applying
95
+ /// - insert 'x' at offset 0
96
+ /// - insert 'y' at offset 1
97
+ /// - insert 'z' at offset 2
98
+ /// to '012345' results in 'x0y1z2345'.
99
+ ///
100
+ /// The raw `edits` of this struct are guaranteed to
101
+ /// 1. not be overlapping.
102
+ /// 2. be in increasing source offset order.
103
+ public struct ConcurrentEdits {
104
+ /// The raw concurrent edits. Are guaranteed to satisfy the requirements
105
+ /// stated above.
106
+ public let edits : [ SourceEdit ]
107
+
108
+ /// Initialize this struct from edits that are already in a concurrent form
109
+ /// and are guaranteed to satisfy the requirements posed above.
110
+ public init ( concurrent: [ SourceEdit ] ) {
111
+ precondition ( Self . isValidConcurrentEditArray ( concurrent) )
112
+ self . edits = concurrent
113
+ }
114
+
115
+ /// Create concurrent from a set of sequential edits. Sequential edits are
116
+ /// applied one after the other. Applying the first edit results in an
117
+ /// intermediate string to which the second edit is applied etc. For example
118
+ /// applying
119
+ /// - insert 'x' at offset 0
120
+ /// - insert 'y' at offset 1
121
+ /// - insert 'z' at offset 2
122
+ /// to '012345' results in 'xyz012345'.
123
+
124
+ public init ( fromSequential sequentialEdits: [ SourceEdit ] ) {
125
+ self . init ( concurrent: Self . translateSequentialEditsToConcurrentEdits ( sequentialEdits) )
126
+ }
127
+
128
+ /// Construct a concurrent edits struct from a single edit. For a single edit,
129
+ /// there is no differentiation between being it being applied concurrently
130
+ /// or sequentially.
131
+ public init ( _ single: SourceEdit ) {
132
+ self . init ( concurrent: [ single] )
133
+ }
134
+
135
+ private static func translateSequentialEditsToConcurrentEdits(
136
+ _ edits: [ SourceEdit ]
137
+ ) -> [ SourceEdit ] {
138
+ var concurrentEdits : [ SourceEdit ] = [ ]
139
+ for editToAdd in edits {
140
+ var editToAdd = editToAdd
141
+ var editIndiciesMergedWithNewEdit : [ Int ] = [ ]
142
+ for (index, existingEdit) in concurrentEdits. enumerated ( ) {
143
+ if existingEdit. replacementRange. intersectsOrTouches ( editToAdd. range) {
144
+ let intersectionLength =
145
+ existingEdit. replacementRange. intersected ( editToAdd. range) . length
146
+ editToAdd = SourceEdit (
147
+ offset: Swift . min ( existingEdit. offset, editToAdd. offset) ,
148
+ length: existingEdit. length + editToAdd. length - intersectionLength,
149
+ replacementLength: existingEdit. replacementLength +
150
+ editToAdd. replacementLength - intersectionLength
151
+ )
152
+ editIndiciesMergedWithNewEdit. append ( index)
153
+ } else if existingEdit. offset < editToAdd. endOffset {
154
+ editToAdd = SourceEdit (
155
+ offset: editToAdd. offset - existingEdit. replacementLength +
156
+ existingEdit. length,
157
+ length: editToAdd. length,
158
+ replacementLength: editToAdd. replacementLength
159
+ )
160
+ }
161
+ }
162
+ assert ( editIndiciesMergedWithNewEdit. isSorted)
163
+ for indexToRemove in editIndiciesMergedWithNewEdit. reversed ( ) {
164
+ concurrentEdits. remove ( at: indexToRemove)
165
+ }
166
+ let insertPos = concurrentEdits. firstIndex ( where: { edit in
167
+ editToAdd. endOffset <= edit. offset
168
+ } ) ?? concurrentEdits. count
169
+ concurrentEdits. insert ( editToAdd, at: insertPos)
170
+ assert ( ConcurrentEdits . isValidConcurrentEditArray ( concurrentEdits) )
171
+ }
172
+ return concurrentEdits
173
+ }
174
+
175
+ private static func isValidConcurrentEditArray( _ edits: [ SourceEdit ] ) -> Bool {
73
176
// Not quite sure if we should disallow creating an `IncrementalParseTransition`
74
177
// object without edits but there doesn't seem to be much benefit if we do,
75
178
// and there are 'lit' tests that want to test incremental re-parsing without edits.
@@ -87,6 +190,11 @@ public final class IncrementalParseTransition {
87
190
}
88
191
return true
89
192
}
193
+
194
+ /// **Public for testing purposes only**
195
+ public static func _isValidConcurrentEditArray( _ edits: [ SourceEdit ] ) -> Bool {
196
+ return isValidConcurrentEditArray ( edits)
197
+ }
90
198
}
91
199
92
200
/// Provides a mechanism for the parser to skip regions of an incrementally
@@ -100,7 +208,7 @@ internal struct IncrementalParseLookup {
100
208
self . cursor = . init( root: transition. previousTree. data. absoluteRaw)
101
209
}
102
210
103
- fileprivate var edits : [ SourceEdit ] {
211
+ fileprivate var edits : ConcurrentEdits {
104
212
return transition. edits
105
213
}
106
214
@@ -160,7 +268,7 @@ internal struct IncrementalParseLookup {
160
268
161
269
// Fast path check: if parser is past all the edits then any matching node
162
270
// can be re-used.
163
- if !edits. isEmpty && edits. last!. range. endOffset < node. position. utf8Offset {
271
+ if !edits. edits . isEmpty && edits . edits. last!. range. endOffset < node. position. utf8Offset {
164
272
return true
165
273
}
166
274
@@ -172,7 +280,7 @@ internal struct IncrementalParseLookup {
172
280
if let nextSibling = cursor. nextSibling {
173
281
// Fast path check: if next sibling is before all the edits then we can
174
282
// re-use the node.
175
- if !edits. isEmpty && edits. first!. range. offset > nextSibling. endPosition. utf8Offset {
283
+ if !edits. edits . isEmpty && edits . edits. first!. range. offset > nextSibling. endPosition. utf8Offset {
176
284
return true
177
285
}
178
286
if let nextToken = nextSibling. raw. firstPresentToken {
@@ -182,7 +290,7 @@ internal struct IncrementalParseLookup {
182
290
let nodeAffectRange = ByteSourceRange ( offset: node. position. utf8Offset,
183
291
length: ( node. raw. totalLength + nextLeafNodeLength) . utf8Length)
184
292
185
- for edit in edits {
293
+ for edit in edits. edits {
186
294
// Check if this node or the trivia of the next node has been edited. If
187
295
// it has, we cannot reuse it.
188
296
if edit. range. offset > nodeAffectRange. endOffset {
@@ -199,7 +307,7 @@ internal struct IncrementalParseLookup {
199
307
200
308
fileprivate func translateToPreEditOffset( _ postEditOffset: Int ) -> Int ? {
201
309
var offset = postEditOffset
202
- for edit in edits {
310
+ for edit in edits. edits {
203
311
if edit. range. offset > offset {
204
312
// Remaining edits doesn't affect the position. (Edits are sorted)
205
313
break
0 commit comments