@@ -10,9 +10,29 @@ module.exports = class SnippetExpansion {
10
10
this . cursor = cursor
11
11
this . snippets = snippets
12
12
this . subscriptions = new CompositeDisposable
13
- this . tabStopMarkers = [ ]
14
13
this . selections = [ this . cursor . selection ]
15
14
15
+ // Holds the `Insertion` instance corresponding to each tab stop marker. We
16
+ // don't use the tab stop's own numbering here; we renumber them
17
+ // consecutively starting at 0 in the order in which they should be
18
+ // visited. So `$1` (if present) will always be at index `0`, and `$0` (if
19
+ // present) will always be the last index.
20
+ this . insertionsByIndex = [ ]
21
+
22
+ // Each insertion has a corresponding marker. We keep them in a map so we
23
+ // can easily reassociate an insertion with its new marker when we destroy
24
+ // its old one.
25
+ this . markersForInsertions = new Map ( )
26
+
27
+ // The index of the active tab stop.
28
+ this . tabStopIndex = null
29
+
30
+ // If, say, tab stop 4's placeholder references tab stop 2, then tab stop
31
+ // 4's insertion goes into this map as a "related" insertion to tab stop 2.
32
+ // We need to keep track of this because tab stop 4's marker will need to
33
+ // be replaced while 2 is the active index.
34
+ this . relatedInsertionsByIndex = new Map ( )
35
+
16
36
const startPosition = this . cursor . selection . getBufferRange ( ) . start
17
37
let { body, tabStopList} = this . snippet
18
38
let tabStops = tabStopList . toArray ( )
@@ -28,8 +48,11 @@ module.exports = class SnippetExpansion {
28
48
this . editor . transact ( ( ) => {
29
49
this . ignoringBufferChanges ( ( ) => {
30
50
this . editor . transact ( ( ) => {
51
+ // Insert the snippet body at the cursor.
31
52
const newRange = this . cursor . selection . insertText ( body , { autoIndent : false } )
32
53
if ( this . snippet . tabStopList . length > 0 ) {
54
+ // Listen for cursor changes so we can decide whether to keep the
55
+ // snippet active or terminate it.
33
56
this . subscriptions . add ( this . cursor . onDidChangePosition ( event => this . cursorMoved ( event ) ) )
34
57
this . subscriptions . add ( this . cursor . onDidDestroy ( ( ) => this . cursorDestroyed ( ) ) )
35
58
this . placeTabStopMarkers ( startPosition , tabStops )
@@ -49,9 +72,12 @@ module.exports = class SnippetExpansion {
49
72
50
73
cursorMoved ( { oldBufferPosition, newBufferPosition, textChanged} ) {
51
74
if ( this . settingTabStop || textChanged ) { return }
52
- const itemWithCursor = this . tabStopMarkers [ this . tabStopIndex ] . find ( item => item . marker . getBufferRange ( ) . containsPoint ( newBufferPosition ) )
75
+ const insertionAtCursor = this . insertionsByIndex [ this . tabStopIndex ] . find ( insertion => {
76
+ let marker = this . markersForInsertions . get ( insertion )
77
+ return marker . getBufferRange ( ) . containsPoint ( newBufferPosition )
78
+ } )
53
79
54
- if ( itemWithCursor && ! itemWithCursor . insertion . isTransformation ( ) ) { return }
80
+ if ( insertionAtCursor && ! insertionAtCursor . isTransformation ( ) ) { return }
55
81
56
82
this . destroy ( )
57
83
}
@@ -80,30 +106,35 @@ module.exports = class SnippetExpansion {
80
106
81
107
applyAllTransformations ( ) {
82
108
this . editor . transact ( ( ) => {
83
- this . tabStopMarkers . forEach ( ( item , index ) =>
84
- this . applyTransformations ( index , true ) )
109
+ this . insertionsByIndex . forEach ( ( insertion , index ) =>
110
+ this . applyTransformations ( index ) )
85
111
} )
86
112
}
87
113
88
- applyTransformations ( tabStop , initial = false ) {
89
- const items = [ ...this . tabStopMarkers [ tabStop ] ]
90
- if ( items . length === 0 ) { return }
114
+ applyTransformations ( tabStopIndex ) {
115
+ const insertions = [ ...this . insertionsByIndex [ tabStopIndex ] ]
116
+ if ( insertions . length === 0 ) { return }
91
117
92
- const primary = items . shift ( )
93
- const primaryRange = primary . marker . getBufferRange ( )
118
+ const primaryInsertion = insertions . shift ( )
119
+ const primaryRange = this . markersForInsertions . get ( primaryInsertion ) . getBufferRange ( )
94
120
const inputText = this . editor . getTextInBufferRange ( primaryRange )
95
121
96
122
this . ignoringBufferChanges ( ( ) => {
97
- for ( const item of items ) {
98
- const { marker, insertion} = item
99
- var range = marker . getBufferRange ( )
100
-
123
+ for ( const [ index , insertion ] of insertions . entries ( ) ) {
101
124
// Don't transform mirrored tab stops. They have their own cursors, so
102
125
// mirroring happens automatically.
103
126
if ( ! insertion . isTransformation ( ) ) { continue }
104
127
128
+ var marker = this . markersForInsertions . get ( insertion )
129
+ var range = marker . getBufferRange ( )
130
+
105
131
var outputText = insertion . transform ( inputText )
106
132
this . editor . transact ( ( ) => this . editor . setTextInBufferRange ( range , outputText ) )
133
+
134
+ // Manually adjust the marker's range rather than rely on its internal
135
+ // heuristics. (We don't have to worry about whether it's been
136
+ // invalidated because setting its buffer range implicitly marks it as
137
+ // valid again.)
107
138
const newRange = new Range (
108
139
range . start ,
109
140
range . start . traverse ( new Point ( 0 , outputText . length ) )
@@ -114,36 +145,115 @@ module.exports = class SnippetExpansion {
114
145
}
115
146
116
147
placeTabStopMarkers ( startPosition , tabStops ) {
117
- for ( const tabStop of tabStops ) {
148
+ // Tab stops within a snippet refer to one another by their external index
149
+ // (1 for $1, 3 for $3, etc.). We respect the order of these tab stops, but
150
+ // we renumber them starting at 0 and using consecutive numbers.
151
+ //
152
+ // Luckily, we don't need to convert between the two numbering systems very
153
+ // often. But we do have to build a map from external index to our internal
154
+ // index. We do this in a separate loop so that the table is complete
155
+ // before we need to consult it in the following loop.
156
+ const indexTable = { }
157
+ for ( let [ index , tabStop ] of tabStops . entries ( ) ) {
158
+ indexTable [ tabStop . index ] = index
159
+ }
160
+
161
+ for ( let [ index , tabStop ] of tabStops . entries ( ) ) {
118
162
const { insertions} = tabStop
119
- const markers = [ ]
120
163
121
164
if ( ! tabStop . isValid ( ) ) { continue }
122
165
123
166
for ( const insertion of insertions ) {
124
167
const { range} = insertion
125
168
const { start, end} = range
169
+ let references = null
170
+ if ( insertion . references ) {
171
+ references = insertion . references . map ( external => indexTable [ external ] )
172
+ }
173
+ // Since this method is called only once at the beginning of a snippet expansion, we know that 0 is about to be the active tab stop.
174
+ const shouldBeInclusive = ( index === 0 ) || ( references && references . includes ( 0 ) )
126
175
const marker = this . getMarkerLayer ( this . editor ) . markBufferRange ( [
127
176
startPosition . traverse ( start ) ,
128
177
startPosition . traverse ( end )
129
- ] )
130
- markers . push ( {
131
- index : markers . length ,
132
- marker,
133
- insertion
134
- } )
178
+ ] , { exclusive : ! shouldBeInclusive } )
179
+ // Now that we've created these markers, we need to store them in a
180
+ // data structure because they'll need to be deleted and re-created
181
+ // when their exclusivity changes.
182
+ this . markersForInsertions . set ( insertion , marker )
183
+
184
+ if ( references ) {
185
+ const relatedInsertions = this . relatedInsertionsByIndex . get ( index ) || [ ]
186
+ relatedInsertions . push ( insertion )
187
+ this . relatedInsertionsByIndex . set ( index , relatedInsertions )
188
+ }
135
189
}
136
-
137
- this . tabStopMarkers . push ( markers )
190
+ this . insertionsByIndex [ index ] = insertions
138
191
}
139
192
140
193
this . setTabStopIndex ( 0 )
141
194
this . applyAllTransformations ( )
142
195
}
143
196
197
+ // When two insertion markers are directly adjacent to one another, and the
198
+ // cursor is placed right at the border between them, the marker that should
199
+ // "claim" the newly typed content will vary based on context.
200
+ //
201
+ // All else being equal, that content should get added to the marker (if any)
202
+ // whose tab stop is active, or else the marker whose tab stop's placeholder
203
+ // references an active tab stop. The `exclusive` setting on a marker
204
+ // controls whether that marker grows to include content added at its edge.
205
+ //
206
+ // So we need to revisit the markers whenever the active tab stop changes,
207
+ // figure out which ones need to be touched, and replace them with markers
208
+ // that have the settings we need.
209
+ adjustTabStopMarkers ( oldIndex , newIndex ) {
210
+ // Take all the insertions whose markers were made inclusive when they
211
+ // became active and restore their original marker settings.
212
+ const insertionsForOldIndex = [
213
+ ...this . insertionsByIndex [ oldIndex ] ,
214
+ ...( this . relatedInsertionsByIndex . get ( oldIndex ) || [ ] )
215
+ ]
216
+
217
+ for ( let insertion of insertionsForOldIndex ) {
218
+ this . replaceMarkerForInsertion ( insertion , { exclusive : true } )
219
+ }
220
+
221
+ // Take all the insertions belonging to the newly active tab stop (and all
222
+ // insertions whose placeholders reference the newly active tab stop) and
223
+ // change their markers to be inclusive.
224
+ const insertionsForNewIndex = [
225
+ ...this . insertionsByIndex [ newIndex ] ,
226
+ ...( this . relatedInsertionsByIndex . get ( newIndex ) || [ ] )
227
+ ]
228
+
229
+ for ( let insertion of insertionsForNewIndex ) {
230
+ this . replaceMarkerForInsertion ( insertion , { exclusive : false } )
231
+ }
232
+ }
233
+
234
+ replaceMarkerForInsertion ( insertion , settings ) {
235
+ const marker = this . markersForInsertions . get ( insertion )
236
+
237
+ // If the marker is invalid or destroyed, return it as-is. Other methods
238
+ // need to know if a marker has been invalidated or destroyed, and we have
239
+ // no need to change the settings on such markers anyway.
240
+ if ( ! marker . isValid ( ) || marker . isDestroyed ( ) ) {
241
+ return marker
242
+ }
243
+
244
+ // Otherwise, create a new marker with an identical range and the specified
245
+ // settings.
246
+ const range = marker . getBufferRange ( )
247
+ const replacement = this . getMarkerLayer ( this . editor ) . markBufferRange ( range , settings )
248
+
249
+ marker . destroy ( )
250
+ this . markersForInsertions . set ( insertion , replacement )
251
+ return replacement
252
+ }
253
+
144
254
goToNextTabStop ( ) {
145
255
const nextIndex = this . tabStopIndex + 1
146
- if ( nextIndex < this . tabStopMarkers . length ) {
256
+ if ( nextIndex < this . insertionsByIndex . length ) {
147
257
if ( this . setTabStopIndex ( nextIndex ) ) {
148
258
return true
149
259
} else {
@@ -167,28 +277,39 @@ module.exports = class SnippetExpansion {
167
277
if ( this . tabStopIndex > 0 ) { this . setTabStopIndex ( this . tabStopIndex - 1 ) }
168
278
}
169
279
170
- setTabStopIndex ( tabStopIndex ) {
171
- this . tabStopIndex = tabStopIndex
280
+ setTabStopIndex ( newIndex ) {
281
+ const oldIndex = this . tabStopIndex
282
+ this . tabStopIndex = newIndex
283
+ // Set a flag before moving any selections so that our change handlers know
284
+ // that the movements were initiated by us.
172
285
this . settingTabStop = true
286
+ // Keep track of whether we placed any selections or cursors.
173
287
let markerSelected = false
174
288
175
- const items = this . tabStopMarkers [ this . tabStopIndex ]
176
- if ( items . length === 0 ) { return false }
289
+ const insertions = this . insertionsByIndex [ this . tabStopIndex ]
290
+ if ( insertions . length === 0 ) { return false }
177
291
178
292
const ranges = [ ]
179
293
this . hasTransforms = false
180
- for ( const item of items ) {
181
- const { marker, insertion} = item
294
+
295
+ // Go through the active tab stop's markers to figure out where to place
296
+ // cursors and/or selections.
297
+ for ( const insertion of insertions ) {
298
+ const marker = this . markersForInsertions . get ( insertion )
182
299
if ( marker . isDestroyed ( ) ) { continue }
183
300
if ( ! marker . isValid ( ) ) { continue }
184
301
if ( insertion . isTransformation ( ) ) {
302
+ // Set a flag for later, but skip transformation insertions because
303
+ // they don't get their own cursors.
185
304
this . hasTransforms = true
186
305
continue
187
306
}
188
307
ranges . push ( marker . getBufferRange ( ) )
189
308
}
190
309
191
310
if ( ranges . length > 0 ) {
311
+ // We have new selections to apply. Reuse existing selections if
312
+ // possible, destroying the unused ones if we already have too many.
192
313
for ( const selection of this . selections . slice ( ranges . length ) ) { selection . destroy ( ) }
193
314
this . selections = this . selections . slice ( 0 , ranges . length )
194
315
for ( let i = 0 ; i < ranges . length ; i ++ ) {
@@ -202,34 +323,48 @@ module.exports = class SnippetExpansion {
202
323
this . selections . push ( newSelection )
203
324
}
204
325
}
326
+ // We placed at least one selection, so this tab stop was successfully
327
+ // set.
205
328
markerSelected = true
206
329
}
207
330
208
331
this . settingTabStop = false
209
332
// If this snippet has at least one transform, we need to observe changes
210
333
// made to the editor so that we can update the transformed tab stops.
211
- if ( this . hasTransforms ) { this . snippets . observeEditor ( this . editor ) }
334
+ if ( this . hasTransforms ) {
335
+ this . snippets . observeEditor ( this . editor )
336
+ } else {
337
+ this . snippets . stopObservingEditor ( this . editor )
338
+ }
339
+
340
+ if ( oldIndex !== null ) {
341
+ this . adjustTabStopMarkers ( oldIndex , newIndex )
342
+ }
212
343
213
344
return markerSelected
214
345
}
215
346
216
347
goToEndOfLastTabStop ( ) {
217
- if ( this . tabStopMarkers . length === 0 ) { return }
218
- const items = this . tabStopMarkers [ this . tabStopMarkers . length - 1 ]
219
- if ( items . length === 0 ) { return }
220
- const { marker : lastMarker } = items [ items . length - 1 ]
348
+ const size = this . insertionsByIndex . length
349
+ if ( size === 0 ) { return }
350
+ const insertions = this . insertionsByIndex [ size - 1 ]
351
+ if ( insertions . length === 0 ) { return }
352
+ const lastMarker = this . markersForInsertions . get ( insertions [ insertions . length - 1 ] )
353
+
221
354
if ( lastMarker . isDestroyed ( ) ) {
222
355
return false
223
356
} else {
224
- this . editor . setCursorBufferPosition ( lastMarker . getEndBufferPosition ( ) )
357
+ this . seditor . setCursorBufferPosition ( lastMarker . getEndBufferPosition ( ) )
225
358
return true
226
359
}
227
360
}
228
361
229
362
destroy ( ) {
230
363
this . subscriptions . dispose ( )
231
364
this . getMarkerLayer ( this . editor ) . clear ( )
232
- this . tabStopMarkers = [ ]
365
+ this . insertionsByIndex = [ ]
366
+ this . relatedInsertionsByIndex = new Map ( )
367
+ this . markersForInsertions = new Map ( ) ;
233
368
this . snippets . stopObservingEditor ( this . editor )
234
369
this . snippets . clearExpansions ( this . editor )
235
370
}
0 commit comments