@@ -45,7 +45,7 @@ public final class IncrementalParseReusedNodeCollector:
4545/// occurred since it was created.
4646public final class IncrementalParseTransition {
4747 fileprivate let previousTree : SourceFileSyntax
48- fileprivate let edits : [ SourceEdit ]
48+ fileprivate let edits : ConcurrentEdits
4949 fileprivate let reusedDelegate : IncrementalParseReusedNodeDelegate ?
5050
5151 /// - Parameters:
@@ -57,19 +57,122 @@ public final class IncrementalParseTransition {
5757 /// 2. should be in increasing source offset order.
5858 /// - reusedNodeDelegate: Optional delegate to accept information about the
5959 /// 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.
6077 public init ( previousTree: SourceFileSyntax ,
61- edits: [ SourceEdit ] ,
78+ edits: ConcurrentEdits ,
6279 reusedNodeDelegate: IncrementalParseReusedNodeDelegate ? = nil ) {
63- assert ( IncrementalParseTransition . isEditArrayValid ( edits) )
6480 self . previousTree = previousTree
6581 self . edits = edits
6682 self . reusedDelegate = reusedNodeDelegate
6783 }
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+ }
6891
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 {
73176 // Not quite sure if we should disallow creating an `IncrementalParseTransition`
74177 // object without edits but there doesn't seem to be much benefit if we do,
75178 // and there are 'lit' tests that want to test incremental re-parsing without edits.
@@ -87,6 +190,11 @@ public final class IncrementalParseTransition {
87190 }
88191 return true
89192 }
193+
194+ /// **Public for testing purposes only**
195+ public static func _isValidConcurrentEditArray( _ edits: [ SourceEdit ] ) -> Bool {
196+ return isValidConcurrentEditArray ( edits)
197+ }
90198}
91199
92200/// Provides a mechanism for the parser to skip regions of an incrementally
@@ -100,7 +208,7 @@ internal struct IncrementalParseLookup {
100208 self . cursor = . init( root: transition. previousTree. data. absoluteRaw)
101209 }
102210
103- fileprivate var edits : [ SourceEdit ] {
211+ fileprivate var edits : ConcurrentEdits {
104212 return transition. edits
105213 }
106214
@@ -160,7 +268,7 @@ internal struct IncrementalParseLookup {
160268
161269 // Fast path check: if parser is past all the edits then any matching node
162270 // 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 {
164272 return true
165273 }
166274
@@ -172,7 +280,7 @@ internal struct IncrementalParseLookup {
172280 if let nextSibling = cursor. nextSibling {
173281 // Fast path check: if next sibling is before all the edits then we can
174282 // 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 {
176284 return true
177285 }
178286 if let nextToken = nextSibling. raw. firstPresentToken {
@@ -182,7 +290,7 @@ internal struct IncrementalParseLookup {
182290 let nodeAffectRange = ByteSourceRange ( offset: node. position. utf8Offset,
183291 length: ( node. raw. totalLength + nextLeafNodeLength) . utf8Length)
184292
185- for edit in edits {
293+ for edit in edits. edits {
186294 // Check if this node or the trivia of the next node has been edited. If
187295 // it has, we cannot reuse it.
188296 if edit. range. offset > nodeAffectRange. endOffset {
@@ -199,7 +307,7 @@ internal struct IncrementalParseLookup {
199307
200308 fileprivate func translateToPreEditOffset( _ postEditOffset: Int ) -> Int ? {
201309 var offset = postEditOffset
202- for edit in edits {
310+ for edit in edits. edits {
203311 if edit. range. offset > offset {
204312 // Remaining edits doesn't affect the position. (Edits are sorted)
205313 break
0 commit comments