Implement undo/redo at cell level (#17744)

Implemented undo and redo for adding, deleting and moving cells.
This commit is contained in:
Barbara Valdez
2021-12-02 13:41:42 -08:00
committed by GitHub
parent 9b87973205
commit 8b09ba8844
9 changed files with 253 additions and 42 deletions

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
import { ICellModel, MoveDirection } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { localize } from 'vs/nls';
export class MoveCellEdit implements IResourceUndoRedoElement {
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
label: string = localize('moveCellEdit', "Move Cell");
resource = this.model.notebookUri;
constructor(private model: NotebookModel, private cell: ICellModel, private moveDirection: MoveDirection) {
}
undo(): void {
const direction = this.moveDirection === MoveDirection.Down ? MoveDirection.Up : MoveDirection.Down;
this.model.moveCell(this.cell, direction, false);
}
redo(): void {
this.model.moveCell(this.cell, this.moveDirection, false);
}
}
export class SplitCellEdit implements IResourceUndoRedoElement {
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
label: string = localize('splitCellEdit', "Split Cell");
resource = this.model.notebookUri;
constructor(private model: NotebookModel, private firstCell: ICellModel, private secondCell: ICellModel, private newLinesRemoved: string[]) {
}
undo(): void {
this.model.mergeCells(this.firstCell, this.secondCell, this.newLinesRemoved);
}
redo(): void {
// no-op currently, will add support on next release
}
}
export class DeleteCellEdit implements IResourceUndoRedoElement {
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
label: string = localize('deleteCellEdit', "Delete Cell");
resource = this.model.notebookUri;
constructor(private model: NotebookModel, private cell: ICellModel, private index: number) {
}
undo(): void {
this.model.insertCell(this.cell, this.index, false);
}
redo(): void {
this.model.deleteCell(this.cell, false);
}
}
export class AddCellEdit implements IResourceUndoRedoElement {
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
label: string = localize('addCellEdit', "Add Cell");
resource = this.model.notebookUri;
constructor(private model: NotebookModel, private cell: ICellModel, private index: number) {
}
undo(): void {
this.model.deleteCell(this.cell, false);
}
redo(): void {
this.model.insertCell(this.cell, this.index, false);
}
}

View File

@@ -35,7 +35,8 @@ import { isUUID } from 'vs/base/common/uuid';
import { TextModel } from 'vs/editor/common/model/textModel';
import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { AddCellEdit, DeleteCellEdit, MoveCellEdit, SplitCellEdit } from 'sql/workbench/services/notebook/browser/models/cellEdit';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
/*
* Used to control whether a message in a dialog/wizard is displayed as an error,
* warning, or informational message. Default is error.
@@ -123,7 +124,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
@IAdsTelemetryService private readonly adstelemetryService: IAdsTelemetryService,
@IConnectionManagementService private connectionManagementService: IConnectionManagementService,
@IConfigurationService private configurationService: IConfigurationService,
@ICapabilitiesService private _capabilitiesService?: ICapabilitiesService
@IUndoRedoService private undoService: IUndoRedoService,
@ICapabilitiesService private _capabilitiesService?: ICapabilitiesService,
) {
super();
if (!_notebookOptions || !_notebookOptions.notebookUri || !_notebookOptions.executeManagers) {
@@ -538,12 +540,11 @@ export class NotebookModel extends Disposable implements INotebookModel {
if (this.inErrorState) {
return undefined;
}
let cell = this.createCell(cellType);
return this.insertCell(cell, index);
}
public splitCell(cellType: CellType, notebookService: INotebookService, index?: number): ICellModel | undefined {
public splitCell(cellType: CellType, notebookService: INotebookService, index?: number, addToUndoStack: boolean = true): ICellModel | undefined {
if (this.inErrorState) {
return undefined;
}
@@ -564,6 +565,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
let newCell = undefined, tailCell = undefined, partialSource = undefined;
let newCellIndex = index;
let tailCellIndex = index;
let newLinesRemoved: string[] = [];
// Save UI state
let showMarkdown = this.cells[index].showMarkdown;
@@ -619,7 +621,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
newCell = this.createCell(cellType);
newCell.source = newSource;
newCellIndex++;
this.insertCell(newCell, newCellIndex);
this.insertCell(newCell, newCellIndex, false);
}
else { //update the existing cell
this.cells[index].source = newSource;
@@ -636,19 +638,24 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
//Remove the trailing empty line after the cursor
if (tailSource[0] === '\r\n' || tailSource[0] === '\n') {
tailSource.splice(0, 1);
newLinesRemoved = tailSource.splice(0, 1);
}
tailCell.source = tailSource;
tailCellIndex = newCellIndex + 1;
this.insertCell(tailCell, tailCellIndex);
this.insertCell(tailCell, tailCellIndex, false);
}
let activeCell = newCell ? newCell : (headContent.length ? tailCell : this.cells[index]);
let activeCellIndex = newCell ? newCellIndex : (headContent.length ? tailCellIndex : index);
if (addToUndoStack) {
let headCell = newCell ? newCell : this.cells[index];
this.undoService.pushElement(new SplitCellEdit(this, headCell, tailCell, newLinesRemoved));
}
//make new cell Active
this.updateActiveCell(activeCell);
activeCell.isEditMode = true;
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.CellsModified,
cells: [activeCell],
@@ -664,7 +671,23 @@ export class NotebookModel extends Disposable implements INotebookModel {
return undefined;
}
public insertCell(cell: ICellModel, index?: number): ICellModel | undefined {
public mergeCells(cell: ICellModel, secondCell: ICellModel, newLinesRemoved: string[] | undefined): void {
let index = this._cells.findIndex(cell => cell.equals(cell));
if (index > -1) {
cell.source = newLinesRemoved.length > 0 ? [...cell.source, ...newLinesRemoved, ...secondCell.source] : [...cell.source, ...secondCell.source];
cell.isEditMode = true;
// Set newly created cell as active cell
this.updateActiveCell(cell);
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.CellsModified,
cells: [cell],
cellIndex: index
});
this.deleteCell(secondCell, false);
}
}
public insertCell(cell: ICellModel, index?: number, addToUndoStack: boolean = true): ICellModel | undefined {
if (this.inErrorState) {
return undefined;
}
@@ -674,9 +697,14 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._cells.push(cell);
index = undefined;
}
// Set newly created cell as active cell
this.updateActiveCell(cell);
cell.isEditMode = true;
if (addToUndoStack) {
// Only make cell active when inserting the cell. If we update the active cell when undoing/redoing, the user would have to deselect the cell first
// and to undo multiple times.
this.updateActiveCell(cell);
this.undoService.pushElement(new AddCellEdit(this, cell, index));
}
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.CellsModified,
cells: [cell],
@@ -712,7 +740,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
}
moveCell(cell: ICellModel, direction: MoveDirection): void {
moveCell(cell: ICellModel, direction: MoveDirection, addToUndoStack: boolean = true): void {
if (this.inErrorState) {
return;
}
@@ -735,10 +763,13 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._cells.splice(index - 1, 0, cell);
}
if (addToUndoStack) {
this.undoService.pushElement(new MoveCellEdit(this, cell, direction));
// If we update the active cell when undoing/redoing, the user would have to deselect the cell first and to undo multiple times.
this.updateActiveCell(cell);
}
index = this.findCellIndex(cell);
// Set newly created cell as active cell
this.updateActiveCell(cell);
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.CellsModified,
cells: [cell],
@@ -784,12 +815,16 @@ export class NotebookModel extends Disposable implements INotebookModel {
return this._notebookOptions.factory.createCell(singleCell, { notebook: this, isTrusted: true });
}
deleteCell(cellModel: ICellModel): void {
deleteCell(cellModel: ICellModel, addToUndoStack: boolean = true): void {
if (this.inErrorState || !this._cells) {
return;
}
let index = this._cells.findIndex(cell => cell.equals(cellModel));
if (index > -1) {
if (addToUndoStack) {
this.undoService.pushElement(new DeleteCellEdit(this, cellModel, index));
}
this._cells.splice(index, 1);
if (this._activeCell === cellModel) {
this.updateActiveCell();
@@ -816,6 +851,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
// TODO: should we validate and complete required missing parameters?
let contents: nb.ICellContents = edit.cell as nb.ICellContents;
newCells.push(this._notebookOptions.factory.createCell(contents, { notebook: this, isTrusted: this._trustedMode }));
this.undoService.pushElement(new AddCellEdit(this, newCells[0], edit.range.start));
}
this._cells.splice(edit.range.start, edit.range.end - edit.range.start, ...newCells);
if (newCells.length > 0) {
@@ -1490,4 +1526,16 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._contentChangedEmitter.fire(changeInfo);
}
public undo(): void {
if (this.undoService.canUndo(this.notebookUri)) {
this.undoService.undo(this.notebookUri);
}
}
public redo(): void {
if (this.undoService.canRedo(this.notebookUri)) {
this.undoService.redo(this.notebookUri);
}
}
}