@@ -12,7 +12,21 @@ module.exports = class SnippetExpansion {
1212 this . cursor = cursor
1313 this . snippets = snippets
1414 this . subscriptions = new CompositeDisposable
15- this . tabStopMarkers = [ ]
15+ this . insertionsByIndex = [ ]
16+ this . markersForInsertions = new Map ( )
17+
18+ // The index of the active tab stop. We don't use the tab stop's own
19+ // numbering here; we renumber them consecutively starting at 0 in the order
20+ // in which they should be visited. So `$1` will always be index `0` in the
21+ // above list, and `$0` (if present) will always be the last index.
22+ this . tabStopIndex = null
23+
24+ // If, say, tab stop 4's placeholder references tab stop 2, then tab stop
25+ // 4's insertion goes into this map as a "related" insertion to tab stop 2.
26+ // We need to keep track of this because tab stop 4's marker will need to be
27+ // replaced while 2 is the active index.
28+ this . relatedInsertionsByIndex = new Map ( )
29+
1630 this . selections = [ this . cursor . selection ]
1731
1832 const startPosition = this . cursor . selection . getBufferRange ( ) . start
@@ -29,8 +43,11 @@ module.exports = class SnippetExpansion {
2943
3044 const tabStops = this . tabStopList . toArray ( )
3145 this . ignoringBufferChanges ( ( ) => {
46+ // Insert the snippet body at the cursor.
3247 const newRange = this . cursor . selection . insertText ( body , { autoIndent : false } )
3348 if ( this . tabStopList . length > 0 ) {
49+ // Listen for cursor changes so we can decide whether to keep the
50+ // snippet active or terminate it.
3451 this . subscriptions . add ( this . cursor . onDidChangePosition ( event => this . cursorMoved ( event ) ) )
3552 this . subscriptions . add ( this . cursor . onDidDestroy ( ( ) => this . cursorDestroyed ( ) ) )
3653 this . placeTabStopMarkers ( tabStops )
@@ -49,9 +66,16 @@ module.exports = class SnippetExpansion {
4966
5067 cursorMoved ( { oldBufferPosition, newBufferPosition, textChanged} ) {
5168 if ( this . settingTabStop || ( textChanged && ! this . isUndoingOrRedoing ) ) { return }
52- const itemWithCursor = this . tabStopMarkers [ this . tabStopIndex ] . find ( item => item . marker . getBufferRange ( ) . containsPoint ( newBufferPosition ) )
5369
54- if ( itemWithCursor && ! itemWithCursor . insertion . isTransformation ( ) ) { return }
70+ const insertionAtCursor = this . insertionsByIndex [ this . tabStopIndex ] . find ( ( insertion ) => {
71+ let marker = this . markersForInsertions . get ( insertion )
72+ return marker . getBufferRange ( ) . containsPoint ( newBufferPosition )
73+ } )
74+
75+ if ( insertionAtCursor && ! insertionAtCursor . isTransformation ( ) ) {
76+ // The cursor is still inside an insertion. Return so that the snippet doesn't get destroyed.
77+ return
78+ }
5579
5680 // we get here if there is no item for the current index with the cursor
5781 if ( this . isUndoingOrRedoing ) {
@@ -95,31 +119,35 @@ module.exports = class SnippetExpansion {
95119 }
96120
97121 applyAllTransformations ( ) {
98- this . tabStopMarkers . forEach ( ( item , index ) => this . applyTransformations ( index ) )
122+ this . insertionsByIndex . forEach ( ( _ , index ) => this . applyTransformations ( index ) )
99123 }
100124
101125 applyTransformations ( tabStop ) {
102- const items = [ ...this . tabStopMarkers [ tabStop ] ]
103- if ( items . length === 0 ) { return }
126+ const insertions = [ ...this . insertionsByIndex [ tabStop ] ]
127+ if ( insertions . length === 0 ) { return }
104128
105- const primary = items . shift ( )
106- const primaryRange = primary . marker . getBufferRange ( )
129+ const primaryInsertion = insertions . shift ( )
130+ const primaryRange = this . markersForInsertions . get ( primaryInsertion ) . getBufferRange ( )
107131 const inputText = this . editor . getTextInBufferRange ( primaryRange )
108132
109133 this . ignoringBufferChanges ( ( ) => {
110- for ( const item of items ) {
111- const { marker, insertion} = item
112- var range = marker . getBufferRange ( )
113-
134+ for ( const insertion of insertions ) {
114135 // Don't transform mirrored tab stops. They have their own cursors, so
115136 // mirroring happens automatically.
116137 if ( ! insertion . isTransformation ( ) ) { continue }
117138
139+ let marker = this . markersForInsertions . get ( insertion )
140+ let range = marker . getBufferRange ( )
141+
118142 var outputText = insertion . transform ( inputText )
119143
120144 this . editor . setTextInBufferRange ( range , outputText )
121145 // this.editor.buffer.groupLastChanges()
122146
147+ // Manually adjust the marker's range rather than rely on its internal
148+ // heuristics. (We don't have to worry about whether it's been
149+ // invalidated because setting its buffer range implicitly marks it as
150+ // valid again.)
123151 const newRange = new Range (
124152 range . start ,
125153 range . start . traverse ( getEndpointOfText ( outputText ) )
@@ -130,42 +158,125 @@ module.exports = class SnippetExpansion {
130158 }
131159
132160 placeTabStopMarkers ( tabStops ) {
161+ // Tab stops within a snippet refer to one another by their external index
162+ // (1 for $1, 3 for $3, etc.). We respect the order of these tab stops, but
163+ // we renumber them starting at 0 and using consecutive numbers.
164+ //
165+ // Luckily, we don't need to convert between the two numbering systems very
166+ // often. But we do have to build a map from external index to our internal
167+ // index. We do this in a separate loop so that the table is complete before
168+ // we need to consult it in the following loop.
169+ let indexTable = { }
170+ Object . keys ( tabStops ) . forEach ( ( key , index ) => {
171+ let tabStop = tabStops [ key ]
172+ indexTable [ tabStop . index ] = index
173+ } )
133174 const markerLayer = this . getMarkerLayer ( this . editor )
134175
176+ let tabStopIndex = - 1
135177 for ( const tabStop of tabStops ) {
178+ tabStopIndex ++
136179 const { insertions} = tabStop
137- const markers = [ ]
138-
139180 if ( ! tabStop . isValid ( ) ) { continue }
140181
141182 for ( const insertion of insertions ) {
142- const marker = markerLayer . markBufferRange ( insertion . range )
143- markers . push ( {
144- index : markers . length ,
145- marker,
146- insertion
147- } )
183+ const { range : { start, end} } = insertion
184+ let references = null
185+ if ( insertion . references ) {
186+ references = insertion . references . map ( external => indexTable [ external ] )
187+ }
188+ // Since this method is only called once at the beginning of a snippet
189+ // expansion, we know that 0 is about to be the active tab stop.
190+ let shouldBeInclusive = ( tabStopIndex === 0 ) || ( references && references . includes ( 0 ) )
191+
192+ const marker = markerLayer . markBufferRange ( insertion . range , { exclusive : ! shouldBeInclusive } )
193+ this . markersForInsertions . set ( insertion , marker )
194+ if ( references ) {
195+ let relatedInsertions = this . relatedInsertionsByIndex . get ( tabStopIndex ) || [ ]
196+ relatedInsertions . push ( insertion )
197+ this . relatedInsertionsByIndex . set ( tabStopIndex , relatedInsertions )
198+ }
148199 }
149200
150- this . tabStopMarkers . push ( markers )
201+ // Since we have to replace markers in place when we change their
202+ // exclusivity, we'll store them in a map keyed on the insertion itself.
203+ this . insertionsByIndex [ tabStopIndex ] = insertions
151204 }
152205
153206 this . setTabStopIndex ( 0 )
154207 this . applyAllTransformations ( )
155208 }
156209
210+ // When two insertion markers are directly adjacent to one another, and the
211+ // cursor is placed right at the border between them, the marker that should
212+ // "claim" the newly-typed content will vary based on context.
213+ //
214+ // All else being equal, that content should get added to the marker (if any)
215+ // whose tab stop is active (or the marker whose tab stop's placeholder
216+ // references an active tab stop). The `exclusive` setting controls whether a
217+ // marker grows to include content added at its edge.
218+ //
219+ // So we need to revisit the markers whenever the active tab stop changes,
220+ // figure out which ones need to be touched, and replace them with markers
221+ // that have the settings we need.
222+ adjustTabStopMarkers ( oldIndex , newIndex ) {
223+ // Take all the insertions belonging to the newly-active tab stop (and all
224+ // insertions whose placeholders reference the newly-active tab stop) and
225+ // change their markers to be inclusive.
226+ let insertionsForNewIndex = [
227+ ...this . insertionsByIndex [ newIndex ] ,
228+ ...( this . relatedInsertionsByIndex . get ( newIndex ) || [ ] )
229+ ]
230+
231+ for ( let insertion of insertionsForNewIndex ) {
232+ this . replaceMarkerForInsertion ( insertion , { exclusive : false } )
233+ }
234+
235+ // Take all the insertions whose markers were made inclusive when they
236+ // became active and restore their original marker settings.
237+ let insertionsForOldIndex = [
238+ ...this . insertionsByIndex [ oldIndex ] ,
239+ ...( this . relatedInsertionsByIndex . get ( oldIndex ) || [ ] )
240+ ]
241+
242+ for ( let insertion of insertionsForOldIndex ) {
243+ this . replaceMarkerForInsertion ( insertion , { exclusive : true } )
244+ }
245+ }
246+
247+ replaceMarkerForInsertion ( insertion , settings ) {
248+ let marker = this . markersForInsertions . get ( insertion )
249+
250+ // If the marker is invalid or destroyed, return it as-is. Other methods
251+ // need to know if a marker has been invalidated or destroyed, and there's
252+ // no case in which we'd need to change the settings on such a marker
253+ // anyway.
254+ if ( ! marker . isValid ( ) || marker . isDestroyed ( ) ) {
255+ return marker
256+ }
257+
258+ // Otherwise, create a new marker with an identical range and the specified
259+ // settings.
260+ let range = marker . getBufferRange ( )
261+ let replacement = this . getMarkerLayer ( this . editor ) . markBufferRange ( range , settings )
262+
263+ marker . destroy ( )
264+ this . markersForInsertions . set ( insertion , replacement )
265+ return replacement
266+ }
267+
157268 goToNextTabStop ( ) {
158269 const nextIndex = this . tabStopIndex + 1
159270
160271 // if we have an endstop (implicit ends have already been added) it will be the last one
161- if ( nextIndex === this . tabStopMarkers . length - 1 && this . tabStopList . hasEndStop ) {
272+ if ( nextIndex === this . insertionsByIndex . length - 1 && this . tabStopList . hasEndStop ) {
162273 const succeeded = this . setTabStopIndex ( nextIndex )
163274 this . destroy ( )
164275 return { succeeded, isDestroyed : true }
165276 }
166277
167278 // we are not at the end, and the next is not the endstop; just go to next stop
168- if ( nextIndex < this . tabStopMarkers . length ) {
279+ if ( nextIndex < this . insertionsByIndex . length ) {
169280 const succeeded = this . setTabStopIndex ( nextIndex )
170281 if ( succeeded ) { return { succeeded, isDestroyed : false } }
171282 return this . goToNextTabStop ( )
@@ -190,26 +301,38 @@ module.exports = class SnippetExpansion {
190301 }
191302
192303 setTabStopIndex ( tabStopIndex ) {
304+ let oldIndex = this . tabStopIndex
193305 this . tabStopIndex = tabStopIndex
306+
307+ // Set a flag before we move any selections so that our change handlers
308+ // will know that the movements were initiated by us.
194309 this . settingTabStop = true
310+
311+ // Keep track of whether we replaced any selections or cursors.
195312 let markerSelected = false
196313
197- const items = this . tabStopMarkers [ this . tabStopIndex ]
198- if ( items . length === 0 ) { return false }
314+ let insertions = this . insertionsByIndex [ this . tabStopIndex ]
315+ if ( insertions . length === 0 ) { return false }
199316
200317 const ranges = [ ]
201318 let hasTransforms = false
202- for ( const item of items ) {
203- const { marker, insertion} = item
319+ // Go through the active tab stop's markers to figure out where to place
320+ // cursors and/or selections.
321+ for ( const insertion of insertions ) {
322+ const marker = this . markersForInsertions . get ( insertion )
204323 if ( marker . isDestroyed ( ) || ! marker . isValid ( ) ) { continue }
205324 if ( insertion . isTransformation ( ) ) {
325+ // Set a flag for later, but skip transformation insertions because
326+ // they don't get their own cursors.
206327 hasTransforms = true
207328 continue
208329 }
209330 ranges . push ( marker . getBufferRange ( ) )
210331 }
211332
212333 if ( ranges . length > 0 ) {
334+ // We have new selections to apply. Reuse existing selections if
335+ // possible, and destroy the unused ones if we already have too many.
213336 for ( const selection of this . selections . slice ( ranges . length ) ) { selection . destroy ( ) }
214337 this . selections = this . selections . slice ( 0 , ranges . length )
215338 for ( let i = 0 ; i < ranges . length ; i ++ ) {
@@ -223,20 +346,30 @@ module.exports = class SnippetExpansion {
223346 this . selections . push ( newSelection )
224347 }
225348 }
349+ // We placed at least one selection, so this tab stop was successfully
350+ // set. Update our return value.
226351 markerSelected = true
227352 }
228353
229354 this . settingTabStop = false
230355 // If this snippet has at least one transform, we need to observe changes
231356 // made to the editor so that we can update the transformed tab stops.
232- if ( hasTransforms ) { this . snippets . observeEditor ( this . editor ) }
357+ if ( hasTransforms ) {
358+ this . snippets . observeEditor ( this . editor )
359+ } else {
360+ this . snippets . stopObservingEditor ( this . editor )
361+ }
362+
363+ if ( oldIndex !== null ) {
364+ this . adjustTabStopMarkers ( oldIndex , this . tabStopIndex )
365+ }
233366
234367 return markerSelected
235368 }
236369
237370 destroy ( ) {
238371 this . subscriptions . dispose ( )
239- this . tabStopMarkers = [ ]
372+ this . insertionsByIndex = [ ]
240373 }
241374
242375 getMarkerLayer ( ) {
0 commit comments