@@ -12,7 +12,21 @@ module.exports = class SnippetExpansion {
12
12
this . cursor = cursor
13
13
this . snippets = snippets
14
14
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
+
16
30
this . selections = [ this . cursor . selection ]
17
31
18
32
const startPosition = this . cursor . selection . getBufferRange ( ) . start
@@ -29,8 +43,11 @@ module.exports = class SnippetExpansion {
29
43
30
44
const tabStops = this . tabStopList . toArray ( )
31
45
this . ignoringBufferChanges ( ( ) => {
46
+ // Insert the snippet body at the cursor.
32
47
const newRange = this . cursor . selection . insertText ( body , { autoIndent : false } )
33
48
if ( this . tabStopList . length > 0 ) {
49
+ // Listen for cursor changes so we can decide whether to keep the
50
+ // snippet active or terminate it.
34
51
this . subscriptions . add ( this . cursor . onDidChangePosition ( event => this . cursorMoved ( event ) ) )
35
52
this . subscriptions . add ( this . cursor . onDidDestroy ( ( ) => this . cursorDestroyed ( ) ) )
36
53
this . placeTabStopMarkers ( tabStops )
@@ -49,9 +66,16 @@ module.exports = class SnippetExpansion {
49
66
50
67
cursorMoved ( { oldBufferPosition, newBufferPosition, textChanged} ) {
51
68
if ( this . settingTabStop || ( textChanged && ! this . isUndoingOrRedoing ) ) { return }
52
- const itemWithCursor = this . tabStopMarkers [ this . tabStopIndex ] . find ( item => item . marker . getBufferRange ( ) . containsPoint ( newBufferPosition ) )
53
69
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
+ }
55
79
56
80
// we get here if there is no item for the current index with the cursor
57
81
if ( this . isUndoingOrRedoing ) {
@@ -95,31 +119,35 @@ module.exports = class SnippetExpansion {
95
119
}
96
120
97
121
applyAllTransformations ( ) {
98
- this . tabStopMarkers . forEach ( ( item , index ) => this . applyTransformations ( index ) )
122
+ this . insertionsByIndex . forEach ( ( _ , index ) => this . applyTransformations ( index ) )
99
123
}
100
124
101
125
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 }
104
128
105
- const primary = items . shift ( )
106
- const primaryRange = primary . marker . getBufferRange ( )
129
+ const primaryInsertion = insertions . shift ( )
130
+ const primaryRange = this . markersForInsertions . get ( primaryInsertion ) . getBufferRange ( )
107
131
const inputText = this . editor . getTextInBufferRange ( primaryRange )
108
132
109
133
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 ) {
114
135
// Don't transform mirrored tab stops. They have their own cursors, so
115
136
// mirroring happens automatically.
116
137
if ( ! insertion . isTransformation ( ) ) { continue }
117
138
139
+ let marker = this . markersForInsertions . get ( insertion )
140
+ let range = marker . getBufferRange ( )
141
+
118
142
var outputText = insertion . transform ( inputText )
119
143
120
144
this . editor . setTextInBufferRange ( range , outputText )
121
145
// this.editor.buffer.groupLastChanges()
122
146
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.)
123
151
const newRange = new Range (
124
152
range . start ,
125
153
range . start . traverse ( getEndpointOfText ( outputText ) )
@@ -130,42 +158,125 @@ module.exports = class SnippetExpansion {
130
158
}
131
159
132
160
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
+ } )
133
174
const markerLayer = this . getMarkerLayer ( this . editor )
134
175
176
+ let tabStopIndex = - 1
135
177
for ( const tabStop of tabStops ) {
178
+ tabStopIndex ++
136
179
const { insertions} = tabStop
137
- const markers = [ ]
138
-
139
180
if ( ! tabStop . isValid ( ) ) { continue }
140
181
141
182
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
+ }
148
199
}
149
200
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
151
204
}
152
205
153
206
this . setTabStopIndex ( 0 )
154
207
this . applyAllTransformations ( )
155
208
}
156
209
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
+
157
268
goToNextTabStop ( ) {
158
269
const nextIndex = this . tabStopIndex + 1
159
270
160
271
// 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 ) {
162
273
const succeeded = this . setTabStopIndex ( nextIndex )
163
274
this . destroy ( )
164
275
return { succeeded, isDestroyed : true }
165
276
}
166
277
167
278
// 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 ) {
169
280
const succeeded = this . setTabStopIndex ( nextIndex )
170
281
if ( succeeded ) { return { succeeded, isDestroyed : false } }
171
282
return this . goToNextTabStop ( )
@@ -190,26 +301,38 @@ module.exports = class SnippetExpansion {
190
301
}
191
302
192
303
setTabStopIndex ( tabStopIndex ) {
304
+ let oldIndex = this . tabStopIndex
193
305
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.
194
309
this . settingTabStop = true
310
+
311
+ // Keep track of whether we replaced any selections or cursors.
195
312
let markerSelected = false
196
313
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 }
199
316
200
317
const ranges = [ ]
201
318
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 )
204
323
if ( marker . isDestroyed ( ) || ! marker . isValid ( ) ) { continue }
205
324
if ( insertion . isTransformation ( ) ) {
325
+ // Set a flag for later, but skip transformation insertions because
326
+ // they don't get their own cursors.
206
327
hasTransforms = true
207
328
continue
208
329
}
209
330
ranges . push ( marker . getBufferRange ( ) )
210
331
}
211
332
212
333
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.
213
336
for ( const selection of this . selections . slice ( ranges . length ) ) { selection . destroy ( ) }
214
337
this . selections = this . selections . slice ( 0 , ranges . length )
215
338
for ( let i = 0 ; i < ranges . length ; i ++ ) {
@@ -223,20 +346,30 @@ module.exports = class SnippetExpansion {
223
346
this . selections . push ( newSelection )
224
347
}
225
348
}
349
+ // We placed at least one selection, so this tab stop was successfully
350
+ // set. Update our return value.
226
351
markerSelected = true
227
352
}
228
353
229
354
this . settingTabStop = false
230
355
// If this snippet has at least one transform, we need to observe changes
231
356
// 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
+ }
233
366
234
367
return markerSelected
235
368
}
236
369
237
370
destroy ( ) {
238
371
this . subscriptions . dispose ( )
239
- this . tabStopMarkers = [ ]
372
+ this . insertionsByIndex = [ ]
240
373
}
241
374
242
375
getMarkerLayer ( ) {
0 commit comments