@@ -8,6 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event';
8
8
import { KeyCode , KeyMod } from 'vs/base/common/keyCodes' ;
9
9
import { Disposable , DisposableStore } from 'vs/base/common/lifecycle' ;
10
10
import { ResourceMap } from 'vs/base/common/map' ;
11
+ import { URI } from 'vs/base/common/uri' ;
11
12
import { EditorConfiguration } from 'vs/editor/browser/config/editorConfiguration' ;
12
13
import { ICodeEditor } from 'vs/editor/browser/editorBrowser' ;
13
14
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget' ;
@@ -32,12 +33,15 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
32
33
import { ContextKeyExpr , IContextKeyService , RawContextKey } from 'vs/platform/contextkey/common/contextkey' ;
33
34
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation' ;
34
35
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry' ;
36
+ import { IPastFutureElements , IUndoRedoElement , IUndoRedoService , UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo' ;
35
37
import { INotebookActionContext , NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions' ;
36
38
import { getNotebookEditorFromEditorPane , ICellViewModel , INotebookEditor , INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser' ;
37
39
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions' ;
38
40
import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions' ;
39
41
import { NOTEBOOK_CELL_EDITOR_FOCUSED , NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys' ;
40
42
import { IEditorService } from 'vs/workbench/services/editor/common/editorService' ;
43
+ import { RedoCommand , UndoCommand } from 'vs/editor/browser/editorExtensions' ;
44
+ import { registerWorkbenchContribution2 , WorkbenchPhase } from 'vs/workbench/common/contributions' ;
41
45
42
46
const NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID = 'notebook.addFindMatchToSelection' ;
43
47
@@ -53,6 +57,7 @@ interface TrackedMatch {
53
57
wordSelections : Selection [ ] ;
54
58
config : IEditorConfiguration ;
55
59
decorationIds : string [ ] ;
60
+ undoRedoHistory : IPastFutureElements ;
56
61
}
57
62
58
63
export const NOTEBOOK_MULTI_SELECTION_CONTEXT = {
@@ -88,6 +93,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
88
93
@ILanguageConfigurationService private readonly languageConfigurationService : ILanguageConfigurationService ,
89
94
@IAccessibilityService private readonly accessibilityService : IAccessibilityService ,
90
95
@IConfigurationService private readonly configurationService : IConfigurationService ,
96
+ @IUndoRedoService private readonly undoRedoService : IUndoRedoService ,
91
97
) {
92
98
super ( ) ;
93
99
@@ -258,10 +264,67 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
258
264
} ) ) ;
259
265
}
260
266
267
+ private updateFinalUndoRedo ( ) {
268
+ const anchorCellModel = this . anchorCell ?. [ 1 ] . getModel ( ) ;
269
+ if ( ! anchorCellModel ) {
270
+ // should not happen
271
+ return ;
272
+ }
273
+
274
+ const newElementsMap : ResourceMap < IUndoRedoElement [ ] > = new ResourceMap < IUndoRedoElement [ ] > ( ) ;
275
+ const resources : URI [ ] = [ ] ;
276
+
277
+ this . trackedMatches . forEach ( trackedMatch => {
278
+ const undoRedoState = trackedMatch . undoRedoHistory ;
279
+ if ( ! undoRedoState ) {
280
+ return ;
281
+ }
282
+
283
+ resources . push ( trackedMatch . cellViewModel . uri ) ;
284
+
285
+ const currentPastElements = this . undoRedoService . getElements ( trackedMatch . cellViewModel . uri ) . past . slice ( ) ;
286
+ const oldPastElements = trackedMatch . undoRedoHistory . past . slice ( ) ;
287
+ const newElements = currentPastElements . slice ( oldPastElements . length ) ;
288
+ if ( newElements . length === 0 ) {
289
+ return ;
290
+ }
291
+
292
+ newElementsMap . set ( trackedMatch . cellViewModel . uri , newElements ) ;
293
+
294
+ this . undoRedoService . removeElements ( trackedMatch . cellViewModel . uri ) ;
295
+ oldPastElements . forEach ( element => {
296
+ this . undoRedoService . pushElement ( element ) ;
297
+ } ) ;
298
+ } ) ;
299
+
300
+ this . undoRedoService . pushElement ( {
301
+ type : UndoRedoElementType . Workspace ,
302
+ resources : resources ,
303
+ label : 'Multi Cursor Edit' ,
304
+ code : 'multiCursorEdit' ,
305
+ confirmBeforeUndo : false ,
306
+ undo : async ( ) => {
307
+ newElementsMap . forEach ( async value => {
308
+ value . reverse ( ) . forEach ( async element => {
309
+ await element . undo ( ) ;
310
+ } ) ;
311
+ } ) ;
312
+ } ,
313
+ redo : async ( ) => {
314
+ newElementsMap . forEach ( async value => {
315
+ value . forEach ( async element => {
316
+ await element . redo ( ) ;
317
+ } ) ;
318
+ } ) ;
319
+ }
320
+ } ) ;
321
+ }
322
+
261
323
public resetToIdleState ( ) {
262
324
this . state = NotebookMultiCursorState . Idle ;
263
325
this . _nbMultiSelectState . set ( NotebookMultiCursorState . Idle ) ;
264
326
this . _nbIsMultiSelectSession . set ( false ) ;
327
+ this . updateFinalUndoRedo ( ) ;
265
328
266
329
this . trackedMatches . forEach ( match => {
267
330
this . clearDecorations ( match ) ;
@@ -304,14 +367,17 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
304
367
throw new Error ( 'Active cell is not an instance of CodeEditorWidget' ) ;
305
368
}
306
369
370
+ textModel . pushStackElement ( ) ;
371
+
307
372
this . trackedMatches = [ ] ;
308
373
const editorConfig = this . constructCellEditorOptions ( this . anchorCell [ 0 ] ) ;
309
374
const newMatch : TrackedMatch = {
310
375
cellViewModel : cell ,
311
376
initialSelection : inputSelection ,
312
377
wordSelections : [ newSelection ] ,
313
378
config : editorConfig , // cache this in the match so we can create new cursors controllers with the correct language config
314
- decorationIds : [ ]
379
+ decorationIds : [ ] ,
380
+ undoRedoHistory : this . undoRedoService . getElements ( cell . uri )
315
381
} ;
316
382
this . trackedMatches . push ( newMatch ) ;
317
383
@@ -362,12 +428,16 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
362
428
throw new Error ( 'Active cell is not an instance of CodeEditorWidget' ) ;
363
429
}
364
430
431
+ const textModel = await resultCellViewModel . resolveTextModel ( ) ;
432
+ textModel . pushStackElement ( ) ;
433
+
365
434
newMatch = {
366
435
cellViewModel : resultCellViewModel ,
367
436
initialSelection : initialSelection ,
368
437
wordSelections : [ newSelection ] ,
369
438
config : this . constructCellEditorOptions ( this . anchorCell [ 0 ] ) ,
370
- decorationIds : [ ]
439
+ decorationIds : [ ] ,
440
+ undoRedoHistory : this . undoRedoService . getElements ( resultCellViewModel . uri )
371
441
} satisfies TrackedMatch ;
372
442
this . trackedMatches . push ( newMatch ) ;
373
443
@@ -407,6 +477,30 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
407
477
} ) ;
408
478
}
409
479
480
+ async undo ( ) {
481
+ const models : ITextModel [ ] = [ ] ;
482
+ for ( const match of this . trackedMatches ) {
483
+ const model = await match . cellViewModel . resolveTextModel ( ) ;
484
+ if ( model ) {
485
+ models . push ( model ) ;
486
+ }
487
+ }
488
+
489
+ await Promise . all ( models . map ( model => model . undo ( ) ) ) ;
490
+ }
491
+
492
+ async redo ( ) {
493
+ const models : ITextModel [ ] = [ ] ;
494
+ for ( const match of this . trackedMatches ) {
495
+ const model = await match . cellViewModel . resolveTextModel ( ) ;
496
+ if ( model ) {
497
+ models . push ( model ) ;
498
+ }
499
+ }
500
+
501
+ await Promise . all ( models . map ( model => model . redo ( ) ) ) ;
502
+ }
503
+
410
504
private constructCellEditorOptions ( cell : ICellViewModel ) : EditorConfiguration {
411
505
const cellEditorOptions = new CellEditorOptions ( this . notebookEditor . getBaseCellEditorOptions ( cell . language ) , this . notebookEditor . notebookOptions , this . configurationService ) ;
412
506
const options = cellEditorOptions . getUpdatedValue ( cell . internalMetadata , cell . uri ) ;
@@ -602,7 +696,59 @@ class NotebookDeleteLeftMultiSelectionAction extends NotebookAction {
602
696
}
603
697
}
604
698
699
+ class NotebookMultiCursorUndoRedoContribution extends Disposable {
700
+
701
+ static readonly ID = 'workbench.contrib.notebook.multiCursorUndoRedo' ;
702
+
703
+ constructor ( @IEditorService private readonly _editorService : IEditorService , @IConfigurationService private readonly configurationService : IConfigurationService ) {
704
+ super ( ) ;
705
+
706
+ if ( ! this . configurationService . getValue < boolean > ( 'notebook.multiSelect.enabled' ) ) {
707
+ return ;
708
+ }
709
+
710
+ const PRIORITY = 10005 ;
711
+ this . _register ( UndoCommand . addImplementation ( PRIORITY , 'notebook-multicursor-undo-redo' , ( ) => {
712
+ const editor = getNotebookEditorFromEditorPane ( this . _editorService . activeEditorPane ) ;
713
+ if ( ! editor ) {
714
+ return false ;
715
+ }
716
+
717
+ if ( ! editor . hasModel ( ) ) {
718
+ return false ;
719
+ }
720
+
721
+ const controller = editor . getContribution < NotebookMultiCursorController > ( NotebookMultiCursorController . id ) ;
722
+
723
+ return controller . undo ( ) ;
724
+ } , ContextKeyExpr . and (
725
+ ContextKeyExpr . equals ( 'config.notebook.multiSelect.enabled' , true ) ,
726
+ NOTEBOOK_IS_ACTIVE_EDITOR ,
727
+ NOTEBOOK_MULTI_SELECTION_CONTEXT . IsNotebookMultiSelect ,
728
+ ) ) ) ;
729
+
730
+ this . _register ( RedoCommand . addImplementation ( PRIORITY , 'notebook-multicursor-undo-redo' , ( ) => {
731
+ const editor = getNotebookEditorFromEditorPane ( this . _editorService . activeEditorPane ) ;
732
+ if ( ! editor ) {
733
+ return false ;
734
+ }
735
+
736
+ if ( ! editor . hasModel ( ) ) {
737
+ return false ;
738
+ }
739
+
740
+ const controller = editor . getContribution < NotebookMultiCursorController > ( NotebookMultiCursorController . id ) ;
741
+ return controller . redo ( ) ;
742
+ } , ContextKeyExpr . and (
743
+ ContextKeyExpr . equals ( 'config.notebook.multiSelect.enabled' , true ) ,
744
+ NOTEBOOK_IS_ACTIVE_EDITOR ,
745
+ NOTEBOOK_MULTI_SELECTION_CONTEXT . IsNotebookMultiSelect ,
746
+ ) ) ) ;
747
+ }
748
+ }
749
+
605
750
registerNotebookContribution ( NotebookMultiCursorController . id , NotebookMultiCursorController ) ;
606
751
registerAction2 ( NotebookAddMatchToMultiSelectionAction ) ;
607
752
registerAction2 ( NotebookExitMultiSelectionAction ) ;
608
753
registerAction2 ( NotebookDeleteLeftMultiSelectionAction ) ;
754
+ registerWorkbenchContribution2 ( NotebookMultiCursorUndoRedoContribution . ID , NotebookMultiCursorUndoRedoContribution , WorkbenchPhase . BlockRestore ) ;
0 commit comments