@@ -10,9 +10,29 @@ module.exports = class SnippetExpansion {
1010 this . cursor = cursor
1111 this . snippets = snippets
1212 this . subscriptions = new CompositeDisposable
13- this . tabStopMarkers = [ ]
1413 this . selections = [ this . cursor . selection ]
1514
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+
1636 const startPosition = this . cursor . selection . getBufferRange ( ) . start
1737 let { body, tabStopList} = this . snippet
1838 let tabStops = tabStopList . toArray ( )
@@ -28,8 +48,11 @@ module.exports = class SnippetExpansion {
2848 this . editor . transact ( ( ) => {
2949 this . ignoringBufferChanges ( ( ) => {
3050 this . editor . transact ( ( ) => {
51+ // Insert the snippet body at the cursor.
3152 const newRange = this . cursor . selection . insertText ( body , { autoIndent : false } )
3253 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.
3356 this . subscriptions . add ( this . cursor . onDidChangePosition ( event => this . cursorMoved ( event ) ) )
3457 this . subscriptions . add ( this . cursor . onDidDestroy ( ( ) => this . cursorDestroyed ( ) ) )
3558 this . placeTabStopMarkers ( startPosition , tabStops )
@@ -49,9 +72,12 @@ module.exports = class SnippetExpansion {
4972
5073 cursorMoved ( { oldBufferPosition, newBufferPosition, textChanged} ) {
5174 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+ } )
5379
54- if ( itemWithCursor && ! itemWithCursor . insertion . isTransformation ( ) ) { return }
80+ if ( insertionAtCursor && ! insertionAtCursor . isTransformation ( ) ) { return }
5581
5682 this . destroy ( )
5783 }
@@ -80,30 +106,35 @@ module.exports = class SnippetExpansion {
80106
81107 applyAllTransformations ( ) {
82108 this . editor . transact ( ( ) => {
83- this . tabStopMarkers . forEach ( ( item , index ) =>
84- this . applyTransformations ( index , true ) )
109+ this . insertionsByIndex . forEach ( ( insertion , index ) =>
110+ this . applyTransformations ( index ) )
85111 } )
86112 }
87113
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 }
91117
92- const primary = items . shift ( )
93- const primaryRange = primary . marker . getBufferRange ( )
118+ const primaryInsertion = insertions . shift ( )
119+ const primaryRange = this . markersForInsertions . get ( primaryInsertion ) . getBufferRange ( )
94120 const inputText = this . editor . getTextInBufferRange ( primaryRange )
95121
96122 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 ( ) ) {
101124 // Don't transform mirrored tab stops. They have their own cursors, so
102125 // mirroring happens automatically.
103126 if ( ! insertion . isTransformation ( ) ) { continue }
104127
128+ var marker = this . markersForInsertions . get ( insertion )
129+ var range = marker . getBufferRange ( )
130+
105131 var outputText = insertion . transform ( inputText )
106132 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.)
107138 const newRange = new Range (
108139 range . start ,
109140 range . start . traverse ( new Point ( 0 , outputText . length ) )
@@ -114,36 +145,115 @@ module.exports = class SnippetExpansion {
114145 }
115146
116147 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 ( ) ) {
118162 const { insertions} = tabStop
119- const markers = [ ]
120163
121164 if ( ! tabStop . isValid ( ) ) { continue }
122165
123166 for ( const insertion of insertions ) {
124167 const { range} = insertion
125168 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 ) )
126175 const marker = this . getMarkerLayer ( this . editor ) . markBufferRange ( [
127176 startPosition . traverse ( start ) ,
128177 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+ }
135189 }
136-
137- this . tabStopMarkers . push ( markers )
190+ this . insertionsByIndex [ index ] = insertions
138191 }
139192
140193 this . setTabStopIndex ( 0 )
141194 this . applyAllTransformations ( )
142195 }
143196
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+
144254 goToNextTabStop ( ) {
145255 const nextIndex = this . tabStopIndex + 1
146- if ( nextIndex < this . tabStopMarkers . length ) {
256+ if ( nextIndex < this . insertionsByIndex . length ) {
147257 if ( this . setTabStopIndex ( nextIndex ) ) {
148258 return true
149259 } else {
@@ -167,28 +277,39 @@ module.exports = class SnippetExpansion {
167277 if ( this . tabStopIndex > 0 ) { this . setTabStopIndex ( this . tabStopIndex - 1 ) }
168278 }
169279
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.
172285 this . settingTabStop = true
286+ // Keep track of whether we placed any selections or cursors.
173287 let markerSelected = false
174288
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 }
177291
178292 const ranges = [ ]
179293 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 )
182299 if ( marker . isDestroyed ( ) ) { continue }
183300 if ( ! marker . isValid ( ) ) { continue }
184301 if ( insertion . isTransformation ( ) ) {
302+ // Set a flag for later, but skip transformation insertions because
303+ // they don't get their own cursors.
185304 this . hasTransforms = true
186305 continue
187306 }
188307 ranges . push ( marker . getBufferRange ( ) )
189308 }
190309
191310 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.
192313 for ( const selection of this . selections . slice ( ranges . length ) ) { selection . destroy ( ) }
193314 this . selections = this . selections . slice ( 0 , ranges . length )
194315 for ( let i = 0 ; i < ranges . length ; i ++ ) {
@@ -202,34 +323,48 @@ module.exports = class SnippetExpansion {
202323 this . selections . push ( newSelection )
203324 }
204325 }
326+ // We placed at least one selection, so this tab stop was successfully
327+ // set.
205328 markerSelected = true
206329 }
207330
208331 this . settingTabStop = false
209332 // If this snippet has at least one transform, we need to observe changes
210333 // 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+ }
212343
213344 return markerSelected
214345 }
215346
216347 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+
221354 if ( lastMarker . isDestroyed ( ) ) {
222355 return false
223356 } else {
224- this . editor . setCursorBufferPosition ( lastMarker . getEndBufferPosition ( ) )
357+ this . seditor . setCursorBufferPosition ( lastMarker . getEndBufferPosition ( ) )
225358 return true
226359 }
227360 }
228361
229362 destroy ( ) {
230363 this . subscriptions . dispose ( )
231364 this . getMarkerLayer ( this . editor ) . clear ( )
232- this . tabStopMarkers = [ ]
365+ this . insertionsByIndex = [ ]
366+ this . relatedInsertionsByIndex = new Map ( )
367+ this . markersForInsertions = new Map ( ) ;
233368 this . snippets . stopObservingEditor ( this . editor )
234369 this . snippets . clearExpansions ( this . editor )
235370 }
0 commit comments