3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
+ import { localize } from 'vs/nls' ;
6
7
import { Emitter , Event } from 'vs/base/common/event' ;
7
8
import { KeyCode , KeyMod } from 'vs/base/common/keyCodes' ;
8
9
import { Disposable , DisposableStore } from 'vs/base/common/lifecycle' ;
@@ -15,7 +16,8 @@ import { Position } from 'vs/editor/common/core/position';
15
16
import { Range } from 'vs/editor/common/core/range' ;
16
17
import { Selection , SelectionDirection } from 'vs/editor/common/core/selection' ;
17
18
import { IWordAtPosition , USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper' ;
18
- import { CursorsController } from 'vs/editor/common/cursor/cursor' ;
19
+ import { CommandExecutor , CursorsController } from 'vs/editor/common/cursor/cursor' ;
20
+ import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations' ;
19
21
import { CursorConfiguration , ICursorSimpleModel } from 'vs/editor/common/cursorCommon' ;
20
22
import { CursorChangeReason } from 'vs/editor/common/cursorEvents' ;
21
23
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry' ;
@@ -24,7 +26,6 @@ import { indentOfLine } from 'vs/editor/common/model/textModel';
24
26
import { ITextModelService } from 'vs/editor/common/services/resolverService' ;
25
27
import { ICoordinatesConverter } from 'vs/editor/common/viewModel' ;
26
28
import { ViewModelEventsCollector } from 'vs/editor/common/viewModelEventDispatcher' ;
27
- import { localize } from 'vs/nls' ;
28
29
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility' ;
29
30
import { MenuId , registerAction2 } from 'vs/platform/actions/common/actions' ;
30
31
import { IConfigurationService } from 'vs/platform/configuration/common/configuration' ;
@@ -35,15 +36,11 @@ import { INotebookActionContext, NotebookAction } from 'vs/workbench/contrib/not
35
36
import { getNotebookEditorFromEditorPane , ICellViewModel , INotebookEditor , INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser' ;
36
37
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions' ;
37
38
import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions' ;
38
- import { NOTEBOOK_IS_ACTIVE_EDITOR , NOTEBOOK_CELL_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys' ;
39
+ import { NOTEBOOK_CELL_EDITOR_FOCUSED , NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys' ;
39
40
import { IEditorService } from 'vs/workbench/services/editor/common/editorService' ;
40
41
41
42
const NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID = 'notebook.addFindMatchToSelection' ;
42
43
43
- export const NOTEBOOK_MULTI_SELECTION_CONTEXT = {
44
- IsNotebookMultiSelect : new RawContextKey < boolean > ( 'isNotebookMultiSelect' , false ) ,
45
- } ;
46
-
47
44
enum NotebookMultiCursorState {
48
45
Idle ,
49
46
Selecting ,
@@ -52,11 +49,17 @@ enum NotebookMultiCursorState {
52
49
53
50
interface TrackedMatch {
54
51
cellViewModel : ICellViewModel ;
55
- selections : Selection [ ] ;
52
+ initialSelection : Selection ;
53
+ wordSelections : Selection [ ] ;
56
54
config : IEditorConfiguration ;
57
55
decorationIds : string [ ] ;
58
56
}
59
57
58
+ export const NOTEBOOK_MULTI_SELECTION_CONTEXT = {
59
+ IsNotebookMultiSelect : new RawContextKey < boolean > ( 'isNotebookMultiSelect' , false ) ,
60
+ NotebookMultiSelectState : new RawContextKey < NotebookMultiCursorState > ( 'notebookMultiSelectState' , NotebookMultiCursorState . Idle ) ,
61
+ } ;
62
+
60
63
export class NotebookMultiCursorController extends Disposable implements INotebookEditorContribution {
61
64
62
65
static readonly id : string = 'notebook.multiCursorController' ;
@@ -75,6 +78,8 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
75
78
private cursorsControllers : ResourceMap < CursorsController > = new ResourceMap < CursorsController > ( ) ;
76
79
77
80
private _nbIsMultiSelectSession = NOTEBOOK_MULTI_SELECTION_CONTEXT . IsNotebookMultiSelect . bindTo ( this . contextKeyService ) ;
81
+ private _nbMultiSelectState = NOTEBOOK_MULTI_SELECTION_CONTEXT . NotebookMultiSelectState . bindTo ( this . contextKeyService ) ;
82
+
78
83
79
84
constructor (
80
85
private readonly notebookEditor : INotebookEditor ,
@@ -104,28 +109,23 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
104
109
private updateCursorsControllers ( ) {
105
110
this . cursorsDisposables . clear ( ) ;
106
111
this . trackedMatches . forEach ( async match => {
107
- // skip this for the anchor cell, there is already a controller for it since it's the focused editor
108
- if ( match . cellViewModel . handle === this . anchorCell ?. [ 0 ] . handle ) {
109
- return ;
110
- }
111
-
112
112
const textModelRef = await this . textModelService . createModelReference ( match . cellViewModel . uri ) ;
113
113
const textModel = textModelRef . object . textEditorModel ;
114
114
if ( ! textModel ) {
115
115
return ;
116
116
}
117
117
118
+ const cursorSimpleModel = this . constructCursorSimpleModel ( match . cellViewModel ) ;
119
+ const converter = this . constructCoordinatesConverter ( ) ;
118
120
const editorConfig = match . config ;
119
121
120
- const converter = this . constructCoordinatesConverter ( ) ;
121
- const cursorSimpleModel = this . constructCursorSimpleModel ( match . cellViewModel ) ;
122
122
const controller = this . cursorsDisposables . add ( new CursorsController (
123
123
textModel ,
124
124
cursorSimpleModel ,
125
125
converter ,
126
126
new CursorConfiguration ( textModel . getLanguageId ( ) , textModel . getOptions ( ) , editorConfig , this . languageConfigurationService )
127
127
) ) ;
128
- controller . setSelections ( new ViewModelEventsCollector ( ) , undefined , match . selections , CursorChangeReason . Explicit ) ;
128
+ controller . setSelections ( new ViewModelEventsCollector ( ) , undefined , match . wordSelections , CursorChangeReason . Explicit ) ;
129
129
this . cursorsControllers . set ( match . cellViewModel . uri , controller ) ;
130
130
} ) ;
131
131
}
@@ -200,43 +200,74 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
200
200
201
201
// typing
202
202
this . anchorDisposables . add ( this . anchorCell [ 1 ] . onWillType ( ( input ) => {
203
- this . state = NotebookMultiCursorState . Editing ; // typing will continue to work as normal across ranges, just preps for another cmd+d
204
- this . cursorsControllers . forEach ( cursorController => {
205
- cursorController . type ( new ViewModelEventsCollector ( ) , input , 'keyboard' ) ;
206
-
203
+ const collector = new ViewModelEventsCollector ( ) ;
204
+ this . trackedMatches . forEach ( match => {
205
+ const controller = this . cursorsControllers . get ( match . cellViewModel . uri ) ;
206
+ if ( ! controller ) {
207
+ // should not happen
208
+ return ;
209
+ }
210
+ if ( match . cellViewModel . handle !== this . anchorCell ?. [ 0 ] . handle ) { // don't relay to active cell, already has a controller for typing
211
+ controller . type ( collector , input , 'keyboard' ) ;
212
+ }
207
213
} ) ;
208
214
} ) ) ;
209
215
210
216
this . anchorDisposables . add ( this . anchorCell [ 1 ] . onDidType ( ( ) => {
211
- this . state = NotebookMultiCursorState . Idle ;
217
+ this . state = NotebookMultiCursorState . Editing ; // typing will continue to work as normal across ranges, just preps for another cmd+d
218
+ this . _nbMultiSelectState . set ( NotebookMultiCursorState . Editing ) ;
219
+
220
+ const anchorController = this . cursorsControllers . get ( this . anchorCell ! [ 0 ] . uri ) ;
221
+ if ( ! anchorController ) {
222
+ return ;
223
+ }
224
+ const activeSelections = this . notebookEditor . activeCodeEditor ?. getSelections ( ) ;
225
+ if ( ! activeSelections ) {
226
+ return ;
227
+ }
228
+
229
+ // need to keep anchor cursor controller in sync manually (for delete usage), since we don't relay type event to it
230
+ anchorController . setSelections ( new ViewModelEventsCollector ( ) , 'keyboard' , activeSelections , CursorChangeReason . Explicit ) ;
231
+
232
+ this . trackedMatches . forEach ( match => {
233
+ const controller = this . cursorsControllers . get ( match . cellViewModel . uri ) ;
234
+ if ( ! controller ) {
235
+ return ;
236
+ }
237
+
238
+ // this is used upon exiting the multicursor session to set the selections back to the correct cursor state
239
+ match . initialSelection = controller . getSelection ( ) ;
240
+ // clear tracked selection data as it is invalid once typing begins
241
+ match . wordSelections = [ ] ;
242
+ } ) ;
243
+
212
244
this . updateLazyDecorations ( ) ;
213
245
} ) ) ;
214
246
215
247
// exit mode
216
248
this . anchorDisposables . add ( this . anchorCell [ 1 ] . onDidChangeCursorSelection ( ( e ) => {
217
- if ( e . source === 'mouse' || e . source === 'deleteLeft' || e . source === ' deleteRight') {
249
+ if ( e . source === 'mouse' || e . source === 'deleteRight' ) {
218
250
this . resetToIdleState ( ) ;
219
251
}
220
252
} ) ) ;
221
253
222
254
this . anchorDisposables . add ( this . anchorCell [ 1 ] . onDidBlurEditorWidget ( ( ) => {
223
- if ( this . state === NotebookMultiCursorState . Editing || this . state === NotebookMultiCursorState . Selecting ) {
255
+ if ( this . state === NotebookMultiCursorState . Selecting || this . state === NotebookMultiCursorState . Editing ) {
224
256
this . resetToIdleState ( ) ;
225
257
}
226
258
} ) ) ;
227
259
}
228
260
229
261
public resetToIdleState ( ) {
230
262
this . state = NotebookMultiCursorState . Idle ;
263
+ this . _nbMultiSelectState . set ( NotebookMultiCursorState . Idle ) ;
231
264
this . _nbIsMultiSelectSession . set ( false ) ;
232
265
233
266
this . trackedMatches . forEach ( match => {
234
267
this . clearDecorations ( match ) ;
268
+ match . cellViewModel . setSelections ( [ match . initialSelection ] ) ; // correct cursor placement upon exiting cmd-d session
235
269
} ) ;
236
270
237
- // todo: polish -- store the precise first selection the user makes. this just sets to the end of the word (due to idle->selecting state transition logic)
238
- this . trackedMatches [ 0 ] . cellViewModel . setSelections ( [ this . trackedMatches [ 0 ] . selections [ 0 ] ] ) ;
239
-
240
271
this . anchorDisposables . clear ( ) ;
241
272
this . cursorsDisposables . clear ( ) ;
242
273
this . cursorsControllers . clear ( ) ;
@@ -277,7 +308,8 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
277
308
const editorConfig = this . constructCellEditorOptions ( this . anchorCell [ 0 ] ) ;
278
309
const newMatch : TrackedMatch = {
279
310
cellViewModel : cell ,
280
- selections : [ newSelection ] ,
311
+ initialSelection : inputSelection ,
312
+ wordSelections : [ newSelection ] ,
281
313
config : editorConfig , // cache this in the match so we can create new cursors controllers with the correct language config
282
314
decorationIds : [ ]
283
315
} ;
@@ -286,6 +318,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
286
318
this . initializeMultiSelectDecorations ( newMatch ) ;
287
319
this . _nbIsMultiSelectSession . set ( true ) ;
288
320
this . state = NotebookMultiCursorState . Selecting ;
321
+ this . _nbMultiSelectState . set ( NotebookMultiCursorState . Selecting ) ;
289
322
this . _onDidChangeAnchorCell . fire ( ) ;
290
323
291
324
} else if ( this . state === NotebookMultiCursorState . Selecting ) { // use the word we stored from idle state transition to find next match, track it
@@ -320,6 +353,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
320
353
await this . notebookEditor . revealRangeInViewAsync ( resultCellViewModel , findResult . match . range ) ;
321
354
this . notebookEditor . focusNotebookCell ( resultCellViewModel , 'editor' ) ;
322
355
356
+ const initialSelection = resultCellViewModel . getSelections ( ) [ 0 ] ;
323
357
const newSelection = Selection . fromRange ( findResult . match . range , SelectionDirection . LTR ) ;
324
358
resultCellViewModel . setSelections ( [ newSelection ] ) ;
325
359
@@ -330,7 +364,8 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
330
364
331
365
newMatch = {
332
366
cellViewModel : resultCellViewModel ,
333
- selections : [ newSelection ] ,
367
+ initialSelection : initialSelection ,
368
+ wordSelections : [ newSelection ] ,
334
369
config : this . constructCellEditorOptions ( this . anchorCell [ 0 ] ) ,
335
370
decorationIds : [ ]
336
371
} satisfies TrackedMatch ;
@@ -340,14 +375,38 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
340
375
341
376
} else { // match is in the same cell, find tracked entry, update and set selections
342
377
newMatch = this . trackedMatches . find ( match => match . cellViewModel . handle === findResult . cell . handle ) ! ;
343
- newMatch . selections . push ( Selection . fromRange ( findResult . match . range , SelectionDirection . LTR ) ) ;
344
- resultCellViewModel . setSelections ( newMatch . selections ) ;
378
+ newMatch . wordSelections . push ( Selection . fromRange ( findResult . match . range , SelectionDirection . LTR ) ) ;
379
+ resultCellViewModel . setSelections ( newMatch . wordSelections ) ;
345
380
}
346
381
347
382
this . initializeMultiSelectDecorations ( newMatch ) ;
348
383
}
349
384
}
350
385
386
+ public async deleteLeft ( ) : Promise < void > {
387
+ this . trackedMatches . forEach ( match => {
388
+ const controller = this . cursorsControllers . get ( match . cellViewModel . uri ) ;
389
+ if ( ! controller ) {
390
+ // should not happen
391
+ return ;
392
+ }
393
+
394
+ const [ , commands ] = DeleteOperations . deleteLeft (
395
+ controller . getPrevEditOperationType ( ) ,
396
+ controller . context . cursorConfig ,
397
+ controller . context . model ,
398
+ controller . getSelections ( ) ,
399
+ controller . getAutoClosedCharacters ( ) ,
400
+ ) ;
401
+
402
+ const delSelections = CommandExecutor . executeCommands ( controller . context . model , controller . getSelections ( ) , commands ) ;
403
+ if ( ! delSelections ) {
404
+ return ;
405
+ }
406
+ controller . setSelections ( new ViewModelEventsCollector ( ) , undefined , delSelections , CursorChangeReason . Explicit ) ;
407
+ } ) ;
408
+ }
409
+
351
410
private constructCellEditorOptions ( cell : ICellViewModel ) : EditorConfiguration {
352
411
const cellEditorOptions = new CellEditorOptions ( this . notebookEditor . getBaseCellEditorOptions ( cell . language ) , this . notebookEditor . notebookOptions , this . configurationService ) ;
353
412
const options = cellEditorOptions . getUpdatedValue ( cell . internalMetadata , cell . uri ) ;
@@ -362,7 +421,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
362
421
private initializeMultiSelectDecorations ( match : TrackedMatch ) {
363
422
const decorations : IModelDeltaDecoration [ ] = [ ] ;
364
423
365
- match . selections . forEach ( selection => {
424
+ match . wordSelections . forEach ( selection => {
366
425
decorations . push ( {
367
426
range : selection ,
368
427
options : {
@@ -379,22 +438,20 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
379
438
}
380
439
381
440
private updateLazyDecorations ( ) {
382
- // const visibleRange = this.notebookEditor.visibleRanges;
383
-
384
441
// for every tracked match that is not in the visible range, dispose of their decorations and update them based off the cursorcontroller
385
442
this . trackedMatches . forEach ( match => {
386
443
const cellIndex = this . notebookEditor . getCellIndex ( match . cellViewModel ) ;
387
444
if ( cellIndex === undefined ) {
388
445
return ;
389
446
}
390
447
391
- let selections ;
392
448
const controller = this . cursorsControllers . get ( match . cellViewModel . uri ) ;
393
- if ( ! controller ) { // active cell doesn't get a stored controller from us
394
- selections = this . notebookEditor . activeCodeEditor ?. getSelections ( ) ;
395
- } else {
396
- selections = controller . getSelections ( ) ;
449
+ if ( ! controller ) {
450
+ // should not happen
451
+ return ;
397
452
}
453
+ const selections = controller . getSelections ( ) ;
454
+
398
455
399
456
const newDecorations = selections ?. map ( selection => {
400
457
return {
@@ -511,6 +568,41 @@ class NotebookExitMultiSelectionAction extends NotebookAction {
511
568
}
512
569
}
513
570
571
+ class NotebookDeleteLeftMultiSelectionAction extends NotebookAction {
572
+ constructor ( ) {
573
+ super ( {
574
+ id : 'noteMultiCursor.deleteLeft' ,
575
+ title : localize ( 'deleteLeftMultiSelection' , "Delete Left" ) ,
576
+ keybinding : {
577
+ when : ContextKeyExpr . and (
578
+ ContextKeyExpr . equals ( 'config.notebook.multiSelect.enabled' , true ) ,
579
+ NOTEBOOK_IS_ACTIVE_EDITOR ,
580
+ NOTEBOOK_MULTI_SELECTION_CONTEXT . IsNotebookMultiSelect ,
581
+ ContextKeyExpr . or (
582
+ NOTEBOOK_MULTI_SELECTION_CONTEXT . NotebookMultiSelectState . isEqualTo ( NotebookMultiCursorState . Selecting ) ,
583
+ NOTEBOOK_MULTI_SELECTION_CONTEXT . NotebookMultiSelectState . isEqualTo ( NotebookMultiCursorState . Editing )
584
+ )
585
+ ) ,
586
+ primary : KeyCode . Backspace ,
587
+ weight : KeybindingWeight . WorkbenchContrib
588
+ }
589
+ } ) ;
590
+ }
591
+
592
+ override async runWithContext ( accessor : ServicesAccessor , context : INotebookActionContext ) : Promise < void > {
593
+ const editorService = accessor . get ( IEditorService ) ;
594
+ const editor = getNotebookEditorFromEditorPane ( editorService . activeEditorPane ) ;
595
+
596
+ if ( ! editor ) {
597
+ return ;
598
+ }
599
+
600
+ const controller = editor . getContribution < NotebookMultiCursorController > ( NotebookMultiCursorController . id ) ;
601
+ controller . deleteLeft ( ) ;
602
+ }
603
+ }
604
+
514
605
registerNotebookContribution ( NotebookMultiCursorController . id , NotebookMultiCursorController ) ;
515
606
registerAction2 ( NotebookAddMatchToMultiSelectionAction ) ;
516
607
registerAction2 ( NotebookExitMultiSelectionAction ) ;
608
+ registerAction2 ( NotebookDeleteLeftMultiSelectionAction ) ;
0 commit comments