@@ -9,6 +9,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
9
9
import { ResourceMap } from 'vs/base/common/map' ;
10
10
import { EditorConfiguration } from 'vs/editor/browser/config/editorConfiguration' ;
11
11
import { ICodeEditor } from 'vs/editor/browser/editorBrowser' ;
12
+ import { RedoCommand , UndoCommand } from 'vs/editor/browser/editorExtensions' ;
12
13
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget' ;
13
14
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration' ;
14
15
import { Position } from 'vs/editor/common/core/position' ;
@@ -31,6 +32,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
31
32
import { ContextKeyExpr , IContextKeyService , RawContextKey } from 'vs/platform/contextkey/common/contextkey' ;
32
33
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation' ;
33
34
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry' ;
35
+ import { IPastFutureElements , IUndoRedoElement , IUndoRedoService , UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo' ;
36
+ import { registerWorkbenchContribution2 , WorkbenchPhase } from 'vs/workbench/common/contributions' ;
34
37
import { INotebookActionContext , NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions' ;
35
38
import { getNotebookEditorFromEditorPane , ICellViewModel , INotebookEditor , INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser' ;
36
39
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions' ;
@@ -55,6 +58,7 @@ interface TrackedMatch {
55
58
selections : Selection [ ] ;
56
59
config : IEditorConfiguration ;
57
60
decorationIds : string [ ] ;
61
+ undoRedoHistory : IPastFutureElements ;
58
62
}
59
63
60
64
export class NotebookMultiCursorController extends Disposable implements INotebookEditorContribution {
@@ -72,7 +76,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
72
76
73
77
private readonly anchorDisposables = this . _register ( new DisposableStore ( ) ) ;
74
78
private readonly cursorsDisposables = this . _register ( new DisposableStore ( ) ) ;
75
- private cursorsControllers : ResourceMap < CursorsController > = new ResourceMap < CursorsController > ( ) ;
79
+ private cursorsControllers : ResourceMap < [ ITextModel , CursorsController ] > = new ResourceMap < [ ITextModel , CursorsController ] > ( ) ;
76
80
77
81
private _nbIsMultiSelectSession = NOTEBOOK_MULTI_SELECTION_CONTEXT . IsNotebookMultiSelect . bindTo ( this . contextKeyService ) ;
78
82
@@ -83,6 +87,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
83
87
@ILanguageConfigurationService private readonly languageConfigurationService : ILanguageConfigurationService ,
84
88
@IAccessibilityService private readonly accessibilityService : IAccessibilityService ,
85
89
@IConfigurationService private readonly configurationService : IConfigurationService ,
90
+ @IUndoRedoService private readonly undoRedoService : IUndoRedoService ,
86
91
) {
87
92
super ( ) ;
88
93
@@ -126,7 +131,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
126
131
new CursorConfiguration ( textModel . getLanguageId ( ) , textModel . getOptions ( ) , editorConfig , this . languageConfigurationService )
127
132
) ) ;
128
133
controller . setSelections ( new ViewModelEventsCollector ( ) , undefined , match . selections , CursorChangeReason . Explicit ) ;
129
- this . cursorsControllers . set ( match . cellViewModel . uri , controller ) ;
134
+ this . cursorsControllers . set ( match . cellViewModel . uri , [ textModel , controller ] ) ;
130
135
} ) ;
131
136
}
132
137
@@ -202,7 +207,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
202
207
this . anchorDisposables . add ( this . anchorCell [ 1 ] . onWillType ( ( input ) => {
203
208
this . state = NotebookMultiCursorState . Editing ; // typing will continue to work as normal across ranges, just preps for another cmd+d
204
209
this . cursorsControllers . forEach ( cursorController => {
205
- cursorController . type ( new ViewModelEventsCollector ( ) , input , 'keyboard' ) ;
210
+ cursorController [ 1 ] . type ( new ViewModelEventsCollector ( ) , input , 'keyboard' ) ;
206
211
207
212
} ) ;
208
213
} ) ) ;
@@ -226,9 +231,73 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
226
231
} ) ) ;
227
232
}
228
233
234
+ private updateFinalUndoRedo ( ) {
235
+ const anchorCellModel = this . anchorCell ?. [ 1 ] . getModel ( ) ;
236
+ if ( ! anchorCellModel ) {
237
+ // should not happen
238
+ return ;
239
+ }
240
+
241
+ const textModels = [ anchorCellModel ] ;
242
+ this . cursorsControllers . forEach ( controller => {
243
+ const model = controller [ 0 ] ;
244
+ textModels . push ( model ) ;
245
+ } ) ;
246
+
247
+ const newElementsMap : ResourceMap < IUndoRedoElement [ ] > = new ResourceMap < IUndoRedoElement [ ] > ( ) ;
248
+
249
+ textModels . forEach ( model => {
250
+ const trackedMatch = this . trackedMatches . find ( match => match . cellViewModel . uri . toString ( ) === model . uri . toString ( ) ) ;
251
+ if ( ! trackedMatch ) {
252
+ return ;
253
+ }
254
+ const undoRedoState = trackedMatch . undoRedoHistory ;
255
+ if ( ! undoRedoState ) {
256
+ return ;
257
+ }
258
+
259
+ const currentPastElements = this . undoRedoService . getElements ( model . uri ) . past . slice ( ) ;
260
+ const oldPastElements = trackedMatch . undoRedoHistory . past . slice ( ) ;
261
+ const newElements = currentPastElements . slice ( oldPastElements . length ) ;
262
+ if ( newElements . length === 0 ) {
263
+ return ;
264
+ }
265
+
266
+ newElementsMap . set ( model . uri , newElements ) ;
267
+
268
+ this . undoRedoService . removeElements ( model . uri ) ;
269
+ oldPastElements . forEach ( element => {
270
+ this . undoRedoService . pushElement ( element ) ;
271
+ } ) ;
272
+ } ) ;
273
+
274
+ this . undoRedoService . pushElement ( {
275
+ type : UndoRedoElementType . Workspace ,
276
+ resources : textModels . map ( model => model . uri ) ,
277
+ label : 'Multi Cursor Edit' ,
278
+ code : 'multiCursorEdit' ,
279
+ confirmBeforeUndo : false ,
280
+ undo : async ( ) => {
281
+ newElementsMap . forEach ( async value => {
282
+ value . reverse ( ) . forEach ( async element => {
283
+ await element . undo ( ) ;
284
+ } ) ;
285
+ } ) ;
286
+ } ,
287
+ redo : async ( ) => {
288
+ newElementsMap . forEach ( async value => {
289
+ value . forEach ( async element => {
290
+ await element . redo ( ) ;
291
+ } ) ;
292
+ } ) ;
293
+ }
294
+ } ) ;
295
+ }
296
+
229
297
public resetToIdleState ( ) {
230
298
this . state = NotebookMultiCursorState . Idle ;
231
299
this . _nbIsMultiSelectSession . set ( false ) ;
300
+ this . updateFinalUndoRedo ( ) ;
232
301
233
302
this . trackedMatches . forEach ( match => {
234
303
this . clearDecorations ( match ) ;
@@ -273,13 +342,16 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
273
342
throw new Error ( 'Active cell is not an instance of CodeEditorWidget' ) ;
274
343
}
275
344
345
+ textModel . pushStackElement ( ) ;
346
+
276
347
this . trackedMatches = [ ] ;
277
348
const editorConfig = this . constructCellEditorOptions ( this . anchorCell [ 0 ] ) ;
278
349
const newMatch : TrackedMatch = {
279
350
cellViewModel : cell ,
280
351
selections : [ newSelection ] ,
281
352
config : editorConfig , // cache this in the match so we can create new cursors controllers with the correct language config
282
- decorationIds : [ ]
353
+ decorationIds : [ ] ,
354
+ undoRedoHistory : this . undoRedoService . getElements ( cell . uri )
283
355
} ;
284
356
this . trackedMatches . push ( newMatch ) ;
285
357
@@ -328,11 +400,15 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
328
400
throw new Error ( 'Active cell is not an instance of CodeEditorWidget' ) ;
329
401
}
330
402
403
+ const textModel = await resultCellViewModel . resolveTextModel ( ) ;
404
+ textModel . pushStackElement ( ) ;
405
+
331
406
newMatch = {
332
407
cellViewModel : resultCellViewModel ,
333
408
selections : [ newSelection ] ,
334
409
config : this . constructCellEditorOptions ( this . anchorCell [ 0 ] ) ,
335
- decorationIds : [ ]
410
+ decorationIds : [ ] ,
411
+ undoRedoHistory : this . undoRedoService . getElements ( resultCellViewModel . uri )
336
412
} satisfies TrackedMatch ;
337
413
this . trackedMatches . push ( newMatch ) ;
338
414
@@ -393,7 +469,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
393
469
if ( ! controller ) { // active cell doesn't get a stored controller from us
394
470
selections = this . notebookEditor . activeCodeEditor ?. getSelections ( ) ;
395
471
} else {
396
- selections = controller . getSelections ( ) ;
472
+ selections = controller [ 1 ] . getSelections ( ) ;
397
473
}
398
474
399
475
const newDecorations = selections ?. map ( selection => {
@@ -420,6 +496,38 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
420
496
) ;
421
497
}
422
498
499
+ async undo ( ) {
500
+ const anchorCellModel = this . anchorCell ?. [ 1 ] . getModel ( ) ;
501
+ if ( ! anchorCellModel ) {
502
+ // should not happen
503
+ return ;
504
+ }
505
+
506
+ const models = [ anchorCellModel ] ;
507
+ this . cursorsControllers . forEach ( controller => {
508
+ const model = controller [ 0 ] ;
509
+ models . push ( model ) ;
510
+ } ) ;
511
+
512
+ await Promise . all ( models . map ( model => model . undo ( ) ) ) ;
513
+ }
514
+
515
+ async redo ( ) {
516
+ const anchorCellModel = this . anchorCell ?. [ 1 ] . getModel ( ) ;
517
+ if ( ! anchorCellModel ) {
518
+ // should not happen
519
+ return ;
520
+ }
521
+
522
+ const models = [ anchorCellModel ] ;
523
+ this . cursorsControllers . forEach ( controller => {
524
+ const model = controller [ 0 ] ;
525
+ models . push ( model ) ;
526
+ } ) ;
527
+
528
+ await Promise . all ( models . map ( model => model . redo ( ) ) ) ;
529
+ }
530
+
423
531
private getWord ( selection : Selection , model : ITextModel ) : IWordAtPosition | null {
424
532
const lineNumber = selection . startLineNumber ;
425
533
const startColumn = selection . startColumn ;
@@ -511,6 +619,54 @@ class NotebookExitMultiSelectionAction extends NotebookAction {
511
619
}
512
620
}
513
621
622
+ class NotebookMultiCursorUndoRedoContribution extends Disposable {
623
+
624
+ static readonly ID = 'workbench.contrib.notebook.multiCursorUndoRedo' ;
625
+
626
+ constructor ( @IEditorService private readonly _editorService : IEditorService ) {
627
+ super ( ) ;
628
+
629
+ const PRIORITY = 10005 ;
630
+ this . _register ( UndoCommand . addImplementation ( PRIORITY , 'notebook-multicursor-undo-redo' , ( ) => {
631
+ const editor = getNotebookEditorFromEditorPane ( this . _editorService . activeEditorPane ) ;
632
+ if ( ! editor ) {
633
+ return false ;
634
+ }
635
+
636
+ if ( ! editor . hasModel ( ) ) {
637
+ return false ;
638
+ }
639
+
640
+ const controller = editor . getContribution < NotebookMultiCursorController > ( NotebookMultiCursorController . id ) ;
641
+
642
+ return controller . undo ( ) ;
643
+ } , ContextKeyExpr . and (
644
+ ContextKeyExpr . equals ( 'config.notebook.multiSelect.enabled' , true ) ,
645
+ NOTEBOOK_IS_ACTIVE_EDITOR ,
646
+ NOTEBOOK_MULTI_SELECTION_CONTEXT . IsNotebookMultiSelect ,
647
+ ) ) ) ;
648
+
649
+ this . _register ( RedoCommand . addImplementation ( PRIORITY , 'notebook-multicursor-undo-redo' , ( ) => {
650
+ const editor = getNotebookEditorFromEditorPane ( this . _editorService . activeEditorPane ) ;
651
+ if ( ! editor ) {
652
+ return false ;
653
+ }
654
+
655
+ if ( ! editor . hasModel ( ) ) {
656
+ return false ;
657
+ }
658
+
659
+ const controller = editor . getContribution < NotebookMultiCursorController > ( NotebookMultiCursorController . id ) ;
660
+ return controller . redo ( ) ;
661
+ } , ContextKeyExpr . and (
662
+ ContextKeyExpr . equals ( 'config.notebook.multiSelect.enabled' , true ) ,
663
+ NOTEBOOK_IS_ACTIVE_EDITOR ,
664
+ NOTEBOOK_MULTI_SELECTION_CONTEXT . IsNotebookMultiSelect ,
665
+ ) ) ) ;
666
+ }
667
+ }
668
+
514
669
registerNotebookContribution ( NotebookMultiCursorController . id , NotebookMultiCursorController ) ;
515
670
registerAction2 ( NotebookAddMatchToMultiSelectionAction ) ;
516
671
registerAction2 ( NotebookExitMultiSelectionAction ) ;
672
+ registerWorkbenchContribution2 ( NotebookMultiCursorUndoRedoContribution . ID , NotebookMultiCursorUndoRedoContribution , WorkbenchPhase . BlockRestore ) ;
0 commit comments