@@ -43,15 +43,30 @@ function selectBlock(): void {
4343 return ;
4444 }
4545
46- const cursorIndex = activeEditor . document . offsetAt ( activeEditor . selection . active ) ;
47- const selection = fileTree . selectBlock ( cursorIndex ) ;
48- if ( selection !== undefined ) {
49- activeEditor . selection = selection . toVscodeSelection ( ) ;
50- activeEditor . revealRange (
51- activeEditor . selection ,
52- vscode . TextEditorRevealType . InCenterIfOutsideViewport
53- ) ;
46+ const bases = activeEditor . selections . length ? activeEditor . selections : [ activeEditor . selection ] ;
47+ const nextSelections = bases
48+ . map ( ( s ) => {
49+ const idx = activeEditor . document . offsetAt ( s . active ) ;
50+ const sel = fileTree . selectBlock ( idx ) ;
51+ return sel ?. toVscodeSelection ( ) ;
52+ } )
53+ . filter ( ( s ) : s is vscode . Selection => ! ! s ) ;
54+
55+ if ( nextSelections . length === 0 ) {
56+ return ;
57+ }
58+
59+ const merged = mergeSelections ( nextSelections ) ;
60+ if ( merged . length === 1 ) {
61+ activeEditor . selection = merged [ 0 ] ;
62+ } else {
63+ activeEditor . selections = merged ;
5464 }
65+
66+ activeEditor . revealRange (
67+ merged [ 0 ] ?? activeEditor . selection ,
68+ vscode . TextEditorRevealType . InCenterIfOutsideViewport
69+ ) ;
5570}
5671
5772function updateSelection ( direction : UpdateSelectionDirection ) : void {
@@ -62,15 +77,34 @@ function updateSelection(direction: UpdateSelectionDirection): void {
6277 return ;
6378 }
6479
65- const selection = fileTree . resolveVscodeSelection ( activeEditor . selection ) ;
66- if ( selection !== undefined ) {
80+ const bases = activeEditor . selections . length ? activeEditor . selections : [ activeEditor . selection ] ;
81+ const updatedSelections : vscode . Selection [ ] = [ ] ;
82+
83+ for ( const base of bases ) {
84+ const selection = fileTree . resolveVscodeSelection ( base ) ;
85+ if ( selection === undefined ) {
86+ continue ;
87+ }
88+
6789 selection . update ( direction , fileTree . blocks ) ;
68- activeEditor . selection = selection . toVscodeSelection ( ) ;
69- activeEditor . revealRange (
70- activeEditor . selection ,
71- vscode . TextEditorRevealType . InCenterIfOutsideViewport
72- ) ;
90+ updatedSelections . push ( selection . toVscodeSelection ( ) ) ;
7391 }
92+
93+ if ( updatedSelections . length === 0 ) {
94+ return ;
95+ }
96+
97+ const merged = mergeSelections ( updatedSelections ) ;
98+ if ( merged . length === 1 ) {
99+ activeEditor . selection = merged [ 0 ] ;
100+ } else {
101+ activeEditor . selections = merged ;
102+ }
103+
104+ activeEditor . revealRange (
105+ merged [ 0 ] ?? activeEditor . selection ,
106+ vscode . TextEditorRevealType . InCenterIfOutsideViewport
107+ ) ;
74108}
75109
76110async function moveSelection ( direction : MoveSelectionDirection ) : Promise < void > {
@@ -80,23 +114,55 @@ async function moveSelection(direction: MoveSelectionDirection): Promise<void> {
80114 return ;
81115 }
82116
83- const selection = fileTree . resolveVscodeSelection ( activeEditor . selection ) ;
84- if ( selection === undefined ) {
85- return ;
86- }
117+ const bases = activeEditor . selections . length ? activeEditor . selections : [ activeEditor . selection ] ;
87118
88- const result = await fileTree . moveSelection ( selection , direction ) ;
89- switch ( result . status ) {
90- case "ok" :
119+ // Single-selection: preserve existing UX
120+ if ( bases . length === 1 ) {
121+ const selection = fileTree . resolveVscodeSelection ( bases [ 0 ] ) ;
122+ if ( selection === undefined ) {
123+ return ;
124+ }
125+
126+ const result = await fileTree . moveSelection ( selection , direction ) ;
127+ if ( result . status === "ok" ) {
91128 activeEditor . selection = result . result ;
92129 activeEditor . revealRange ( result . result , vscode . TextEditorRevealType . InCenterIfOutsideViewport ) ;
93- break ;
94-
95- case "err" :
96- // TODO: add this as a text box above the cursor (can vscode do that?)
130+ } else {
97131 getLogger ( ) . log ( result . result ) ;
132+ }
98133
99- break ;
134+ return ;
135+ }
136+
137+ // Multi-selection: order moves to reduce interference
138+ const order = bases . map ( ( _ , i ) => i ) ;
139+ order . sort ( ( i , j ) => {
140+ const a = bases [ i ] . start ;
141+ const b = bases [ j ] . start ;
142+ const cmp = a . line - b . line || a . character - b . character ;
143+ return direction === "swap-next" ? - cmp : cmp ; // down: bottom->top, up: top->bottom
144+ } ) ;
145+
146+ const results : ( vscode . Selection | undefined ) [ ] = bases . slice ( ) ;
147+ for ( const i of order ) {
148+ const current = results [ i ] ?? bases [ i ] ;
149+ const selection = fileTree . resolveVscodeSelection ( current ) ;
150+ if ( selection === undefined ) {
151+ continue ;
152+ }
153+
154+ const res = await fileTree . moveSelection ( selection , direction ) ;
155+ if ( res . status === "ok" ) {
156+ results [ i ] = res . result ;
157+ } else {
158+ getLogger ( ) . log ( res . result ) ;
159+ }
160+ }
161+
162+ const finalSelections = results . filter ( ( s ) : s is vscode . Selection => ! ! s ) ;
163+ if ( finalSelections . length ) {
164+ activeEditor . selections = finalSelections ;
165+ activeEditor . revealRange ( finalSelections [ 0 ] , vscode . TextEditorRevealType . InCenterIfOutsideViewport ) ;
100166 }
101167}
102168
@@ -108,43 +174,133 @@ function navigate(direction: "up" | "down" | "left" | "right"): void {
108174 return ;
109175 }
110176
111- const selection = fileTree . resolveVscodeSelection ( activeEditor . selection ) ;
177+ const bases = activeEditor . selections . length ? activeEditor . selections : [ activeEditor . selection ] ;
112178 const blocks = fileTree . blocks ;
113- const parent = selection ?. getParent ( blocks ) ;
114- const previous = selection ?. getPrevious ( blocks ) ;
115- const next = selection ?. getNext ( blocks ) ;
116-
117- let newPosition ;
118- switch ( direction ) {
119- case "up" :
120- if ( parent ) {
121- newPosition = parent . toVscodeSelection ( ) . start ;
122- }
123- break ;
124- case "down" :
125- if ( parent ) {
126- newPosition = parent . toVscodeSelection ( ) . end ;
127- }
128- break ;
129- case "left" :
130- if ( previous ) {
131- newPosition = previous . toVscodeSelection ( ) . start ;
132- }
133- break ;
134- case "right" :
135- if ( next ) {
136- newPosition = next . toVscodeSelection ( ) . start ;
137- }
138- break ;
179+ const nextCursors : vscode . Selection [ ] = [ ] ;
180+
181+ for ( const base of bases ) {
182+ const selection = fileTree . resolveVscodeSelection ( base ) ;
183+ if ( selection === undefined ) {
184+ continue ;
185+ }
186+
187+ const parent = selection . getParent ( blocks ) ;
188+ const previous = selection . getPrevious ( blocks ) ;
189+ const next = selection . getNext ( blocks ) ;
190+
191+ let newPosition : vscode . Position | undefined ;
192+ switch ( direction ) {
193+ case "up" :
194+ if ( parent ) {
195+ newPosition = parent . toVscodeSelection ( ) . start ;
196+ }
197+ break ;
198+ case "down" :
199+ if ( parent ) {
200+ newPosition = parent . toVscodeSelection ( ) . end ;
201+ }
202+ break ;
203+ case "left" :
204+ if ( previous ) {
205+ newPosition = previous . toVscodeSelection ( ) . start ;
206+ }
207+ break ;
208+ case "right" :
209+ if ( next ) {
210+ newPosition = next . toVscodeSelection ( ) . start ;
211+ }
212+ break ;
213+ }
214+
215+ if ( newPosition ) {
216+ nextCursors . push ( new vscode . Selection ( newPosition , newPosition ) ) ;
217+ }
218+ }
219+
220+ if ( nextCursors . length === 0 ) {
221+ return ;
222+ }
223+
224+ const deduped = dedupeSelections ( nextCursors ) ;
225+ activeEditor . selections = deduped ;
226+ activeEditor . revealRange ( deduped [ 0 ] , vscode . TextEditorRevealType . InCenterIfOutsideViewport ) ;
227+ }
228+
229+ /**
230+ * Merge overlapping or touching selections (used to keep UX tidy).
231+ */
232+ function mergeSelections ( selections : vscode . Selection [ ] ) : vscode . Selection [ ] {
233+ if ( selections . length <= 1 ) {
234+ return selections ;
235+ }
236+
237+ const ranges = selections . map ( ( s ) => new vscode . Range ( s . start , s . end ) ) ;
238+ ranges . sort ( ( a , b ) => {
239+ if ( a . start . isBefore ( b . start ) ) {
240+ return - 1 ;
241+ }
242+ if ( a . start . isAfter ( b . start ) ) {
243+ return 1 ;
244+ }
245+ if ( a . end . isBefore ( b . end ) ) {
246+ return - 1 ;
247+ }
248+ if ( a . end . isAfter ( b . end ) ) {
249+ return 1 ;
250+ }
251+ return 0 ;
252+ } ) ;
253+
254+ const merged : vscode . Range [ ] = [ ] ;
255+ for ( const r of ranges ) {
256+ const last = merged . at ( - 1 ) ;
257+ if ( last === undefined ) {
258+ merged . push ( r ) ;
259+ } else if ( ! r . start . isAfter ( last . end ) ) {
260+ const end = r . end . isAfter ( last . end ) ? r . end : last . end ;
261+ merged [ merged . length - 1 ] = new vscode . Range ( last . start , end ) ;
262+ } else {
263+ merged . push ( r ) ;
264+ }
139265 }
140266
141- if ( newPosition ) {
142- activeEditor . selection = new vscode . Selection ( newPosition , newPosition ) ;
143- activeEditor . revealRange (
144- activeEditor . selection ,
145- vscode . TextEditorRevealType . InCenterIfOutsideViewport
146- ) ;
267+ return merged . map ( ( r ) => new vscode . Selection ( r . start , r . end ) ) ;
268+ }
269+
270+ /**
271+ * De-duplicate selections while preserving order.
272+ */
273+ function dedupeSelections ( selections : vscode . Selection [ ] ) : vscode . Selection [ ] {
274+ if ( selections . length <= 1 ) {
275+ return selections ;
276+ }
277+
278+ selections . sort ( ( a , b ) => {
279+ if ( a . start . isBefore ( b . start ) ) {
280+ return - 1 ;
281+ }
282+ if ( a . start . isAfter ( b . start ) ) {
283+ return 1 ;
284+ }
285+ if ( a . end . isBefore ( b . end ) ) {
286+ return - 1 ;
287+ }
288+ if ( a . end . isAfter ( b . end ) ) {
289+ return 1 ;
290+ }
291+ return 0 ;
292+ } ) ;
293+
294+ const seen = new Set < string > ( ) ;
295+ const out : vscode . Selection [ ] = [ ] ;
296+ for ( const s of selections ) {
297+ const key = `${ s . start . line } :${ s . start . character } -${ s . end . line } :${ s . end . character } ` ;
298+ if ( ! seen . has ( key ) ) {
299+ seen . add ( key ) ;
300+ out . push ( s ) ;
301+ }
147302 }
303+ return out ;
148304}
149305
150306function updateTargetHighlights ( editor : vscode . TextEditor , vscodeSelection : vscode . Selection ) : void {
0 commit comments