Skip to content

Commit 422b8e9

Browse files
authored
Merge pull request microsoft#226545 from microsoft/rebornix/coherent-orangutan
Experimental undo/redo handling for notebook multicursor
2 parents ab81de5 + 85ce229 commit 422b8e9

File tree

1 file changed

+148
-2
lines changed

1 file changed

+148
-2
lines changed

src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event';
88
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
99
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
1010
import { ResourceMap } from 'vs/base/common/map';
11+
import { URI } from 'vs/base/common/uri';
1112
import { EditorConfiguration } from 'vs/editor/browser/config/editorConfiguration';
1213
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1314
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget';
@@ -32,12 +33,15 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
3233
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
3334
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
3435
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
36+
import { IPastFutureElements, IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
3537
import { INotebookActionContext, NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
3638
import { getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
3739
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
3840
import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions';
3941
import { NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
4042
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';
4145

4246
const NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID = 'notebook.addFindMatchToSelection';
4347

@@ -53,6 +57,7 @@ interface TrackedMatch {
5357
wordSelections: Selection[];
5458
config: IEditorConfiguration;
5559
decorationIds: string[];
60+
undoRedoHistory: IPastFutureElements;
5661
}
5762

5863
export const NOTEBOOK_MULTI_SELECTION_CONTEXT = {
@@ -88,6 +93,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
8893
@ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService,
8994
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
9095
@IConfigurationService private readonly configurationService: IConfigurationService,
96+
@IUndoRedoService private readonly undoRedoService: IUndoRedoService,
9197
) {
9298
super();
9399

@@ -258,10 +264,67 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
258264
}));
259265
}
260266

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+
261323
public resetToIdleState() {
262324
this.state = NotebookMultiCursorState.Idle;
263325
this._nbMultiSelectState.set(NotebookMultiCursorState.Idle);
264326
this._nbIsMultiSelectSession.set(false);
327+
this.updateFinalUndoRedo();
265328

266329
this.trackedMatches.forEach(match => {
267330
this.clearDecorations(match);
@@ -304,14 +367,17 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
304367
throw new Error('Active cell is not an instance of CodeEditorWidget');
305368
}
306369

370+
textModel.pushStackElement();
371+
307372
this.trackedMatches = [];
308373
const editorConfig = this.constructCellEditorOptions(this.anchorCell[0]);
309374
const newMatch: TrackedMatch = {
310375
cellViewModel: cell,
311376
initialSelection: inputSelection,
312377
wordSelections: [newSelection],
313378
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)
315381
};
316382
this.trackedMatches.push(newMatch);
317383

@@ -362,12 +428,16 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
362428
throw new Error('Active cell is not an instance of CodeEditorWidget');
363429
}
364430

431+
const textModel = await resultCellViewModel.resolveTextModel();
432+
textModel.pushStackElement();
433+
365434
newMatch = {
366435
cellViewModel: resultCellViewModel,
367436
initialSelection: initialSelection,
368437
wordSelections: [newSelection],
369438
config: this.constructCellEditorOptions(this.anchorCell[0]),
370-
decorationIds: []
439+
decorationIds: [],
440+
undoRedoHistory: this.undoRedoService.getElements(resultCellViewModel.uri)
371441
} satisfies TrackedMatch;
372442
this.trackedMatches.push(newMatch);
373443

@@ -407,6 +477,30 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
407477
});
408478
}
409479

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+
410504
private constructCellEditorOptions(cell: ICellViewModel): EditorConfiguration {
411505
const cellEditorOptions = new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(cell.language), this.notebookEditor.notebookOptions, this.configurationService);
412506
const options = cellEditorOptions.getUpdatedValue(cell.internalMetadata, cell.uri);
@@ -602,7 +696,59 @@ class NotebookDeleteLeftMultiSelectionAction extends NotebookAction {
602696
}
603697
}
604698

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+
605750
registerNotebookContribution(NotebookMultiCursorController.id, NotebookMultiCursorController);
606751
registerAction2(NotebookAddMatchToMultiSelectionAction);
607752
registerAction2(NotebookExitMultiSelectionAction);
608753
registerAction2(NotebookDeleteLeftMultiSelectionAction);
754+
registerWorkbenchContribution2(NotebookMultiCursorUndoRedoContribution.ID, NotebookMultiCursorUndoRedoContribution, WorkbenchPhase.BlockRestore);

0 commit comments

Comments
 (0)