diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index ae05184da63..4f937d9f3cf 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -60,6 +60,18 @@ export class ResourceEditStackSnapshot { ) { } } +export class UndoRedoGroup { + private static _ID = 0; + + public readonly id: number; + + constructor() { + this.id = UndoRedoGroup._ID++; + } + + public static None = new UndoRedoGroup(); +} + export interface IUndoRedoService { readonly _serviceBrand: undefined; @@ -79,7 +91,7 @@ export interface IUndoRedoService { * Add a new element to the `undo` stack. * This will destroy the `redo` stack. */ - pushElement(element: IUndoRedoElement): void; + pushElement(element: IUndoRedoElement, group?: UndoRedoGroup): void; /** * Get the last pushed element for a resource. diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index a42d889cc8b..472bd7e27b0 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements, ResourceEditStackSnapshot, UriComparisonKeyComputer, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements, ResourceEditStackSnapshot, UriComparisonKeyComputer, IResourceUndoRedoElement, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -32,15 +32,17 @@ class ResourceStackElement { public readonly strResource: string; public readonly resourceLabels: string[]; public readonly strResources: string[]; + public readonly groupId: number; public isValid: boolean; - constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string) { + constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number) { this.actual = actual; this.label = actual.label; this.resourceLabel = resourceLabel; this.strResource = strResource; this.resourceLabels = [this.resourceLabel]; this.strResources = [this.strResource]; + this.groupId = groupId; this.isValid = true; } @@ -124,14 +126,16 @@ class WorkspaceStackElement { public readonly resourceLabels: string[]; public readonly strResources: string[]; + public readonly groupId: number; public removedResources: RemovedResources | null; public invalidatedResources: RemovedResources | null; - constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[]) { + constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number) { this.actual = actual; this.label = actual.label; this.resourceLabels = resourceLabels; this.strResources = strResources; + this.groupId = groupId; this.removedResources = null; this.invalidatedResources = null; } @@ -349,6 +353,13 @@ class ResourceEditStack { return this._past[this._past.length - 1]; } + public getSecondClosestPastElement(): StackElement | null { + if (this._past.length < 2) { + return null; + } + return this._past[this._past.length - 2]; + } + public getClosestFutureElement(): StackElement | null { if (this._future.length === 0) { return null; @@ -482,11 +493,11 @@ export class UndoRedoService implements IUndoRedoService { console.log(str.join('\n')); } - public pushElement(element: IUndoRedoElement): void { + public pushElement(element: IUndoRedoElement, group: UndoRedoGroup = UndoRedoGroup.None): void { if (element.type === UndoRedoElementType.Resource) { const resourceLabel = getResourceLabel(element.resource); const strResource = this.getUriComparisonKey(element.resource); - this._pushElement(new ResourceStackElement(element, resourceLabel, strResource)); + this._pushElement(new ResourceStackElement(element, resourceLabel, strResource, group.id)); } else { const seen = new Set(); const resourceLabels: string[] = []; @@ -504,9 +515,9 @@ export class UndoRedoService implements IUndoRedoService { } if (resourceLabels.length === 1) { - this._pushElement(new ResourceStackElement(element, resourceLabels[0], strResources[0])); + this._pushElement(new ResourceStackElement(element, resourceLabels[0], strResources[0], group.id)); } else { - this._pushElement(new WorkspaceStackElement(element, resourceLabels, strResources)); + this._pushElement(new WorkspaceStackElement(element, resourceLabels, strResources, group.id)); } } if (DEBUG) { @@ -550,7 +561,7 @@ export class UndoRedoService implements IUndoRedoService { for (const _element of individualArr) { const resourceLabel = getResourceLabel(_element.resource); const strResource = this.getUriComparisonKey(_element.resource); - const element = new ResourceStackElement(_element, resourceLabel, strResource); + const element = new ResourceStackElement(_element, resourceLabel, strResource, 0); individualMap.set(element.strResource, element); } @@ -569,7 +580,7 @@ export class UndoRedoService implements IUndoRedoService { for (const _element of individualArr) { const resourceLabel = getResourceLabel(_element.resource); const strResource = this.getUriComparisonKey(_element.resource); - const element = new ResourceStackElement(_element, resourceLabel, strResource); + const element = new ResourceStackElement(_element, resourceLabel, strResource, 0); individualMap.set(element.strResource, element); } @@ -688,7 +699,7 @@ export class UndoRedoService implements IUndoRedoService { }; } - private _safeInvokeWithLocks(element: StackElement, invoke: () => Promise | void, editStackSnapshot: EditStackSnapshot, cleanup: IDisposable = Disposable.None): Promise | void { + private _safeInvokeWithLocks(element: StackElement, invoke: () => Promise | void, editStackSnapshot: EditStackSnapshot, cleanup: IDisposable, continuation: () => Promise | void): Promise | void { const releaseLocks = this._acquireLocks(editStackSnapshot); let result: Promise | void; @@ -706,6 +717,7 @@ export class UndoRedoService implements IUndoRedoService { () => { releaseLocks(); cleanup.dispose(); + return continuation(); }, (err) => { releaseLocks(); @@ -717,6 +729,7 @@ export class UndoRedoService implements IUndoRedoService { // result is void releaseLocks(); cleanup.dispose(); + return continuation(); } } @@ -864,9 +877,34 @@ export class UndoRedoService implements IUndoRedoService { return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks); } + private _isPartOfUndoGroup(element: WorkspaceStackElement): boolean { + if (!element.groupId) { + return false; + } + // check that there is at least another element with the same groupId ready to be undone + for (const [, editStack] of this._editStacks) { + const pastElement = editStack.getClosestPastElement(); + if (!pastElement) { + continue; + } + if (pastElement === element) { + const secondPastElement = editStack.getSecondClosestPastElement(); + if (secondPastElement && secondPastElement.groupId === element.groupId) { + // there is another element with the same group id in the same stack! + return true; + } + } + if (pastElement.groupId === element.groupId) { + // there is another element with the same group id in another stack! + return true; + } + } + return false; + } + private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise { - if (element.canSplit()) { + if (element.canSplit() && !this._isPartOfUndoGroup(element)) { // this element can be split const result = await this._dialogService.show( @@ -920,7 +958,7 @@ export class UndoRedoService implements IUndoRedoService { for (const editStack of editStackSnapshot.editStacks) { editStack.moveBackward(element); } - return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId)); } private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { @@ -939,10 +977,26 @@ export class UndoRedoService implements IUndoRedoService { } return this._invokeResourcePrepare(element, (cleanup) => { editStack.moveBackward(element); - return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId)); }); } + private _continueUndoInGroup(groupId: number): Promise | void { + if (!groupId) { + return; + } + // find another element with the same groupId ready to be undone + for (const [strResource, editStack] of this._editStacks) { + const pastElement = editStack.getClosestPastElement(); + if (!pastElement) { + continue; + } + if (pastElement.groupId === groupId) { + return this.undo(strResource); + } + } + } + public undo(resource: URI | string): Promise | void { const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource); if (!this._editStacks.has(strResource)) { @@ -1097,7 +1151,7 @@ export class UndoRedoService implements IUndoRedoService { for (const editStack of editStackSnapshot.editStacks) { editStack.moveForward(element); } - return this._safeInvokeWithLocks(element, () => element.actual.redo(), editStackSnapshot, cleanup); + return this._safeInvokeWithLocks(element, () => element.actual.redo(), editStackSnapshot, cleanup, () => this._continueRedoInGroup(element.groupId)); } private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { @@ -1117,10 +1171,26 @@ export class UndoRedoService implements IUndoRedoService { return this._invokeResourcePrepare(element, (cleanup) => { editStack.moveForward(element); - return this._safeInvokeWithLocks(element, () => element.actual.redo(), new EditStackSnapshot([editStack]), cleanup); + return this._safeInvokeWithLocks(element, () => element.actual.redo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueRedoInGroup(element.groupId)); }); } + private _continueRedoInGroup(groupId: number): Promise | void { + if (!groupId) { + return; + } + // find another element with the same groupId ready to be redone + for (const [strResource, editStack] of this._editStacks) { + const futureElement = editStack.getClosestFutureElement(); + if (!futureElement) { + continue; + } + if (futureElement.groupId === groupId) { + return this.redo(strResource); + } + } + } + public redo(resource: URI | string): Promise | void { const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource); if (!this._editStacks.has(strResource)) { diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index 86714f8d471..714f689d01d 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; -import { UndoRedoElementType, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; +import { UndoRedoElementType, IUndoRedoElement, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -208,4 +208,9 @@ suite('UndoRedoService', () => { assert.ok(service.getLastElement(resource2) === element1); }); + + test('UndoRedoGroup.None uses id 0', () => { + assert.equal(UndoRedoGroup.None, 0); + }); + }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts index bbf9947506e..7a369cd5373 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; import { IProgress } from 'vs/platform/progress/common/progress'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -28,6 +29,7 @@ export class ResourceNotebookCellEdit extends ResourceEdit { export class BulkCellEdits { constructor( + undoRedoGroup: UndoRedoGroup, private readonly _progress: IProgress, private readonly _edits: ResourceNotebookCellEdit[], @INotebookService private readonly _notebookService: INotebookService, diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index b1207b2323c..defc9283af9 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -16,6 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { BulkTextEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkTextEdits'; import { BulkFileEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkFileEdits'; import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; class BulkEdit { @@ -60,16 +61,17 @@ class BulkEdit { this._progress.report({ total: this._edits.length }); const progress: IProgress = { report: _ => this._progress.report({ increment: 1 }) }; + const undoRedoGroup = new UndoRedoGroup(); let index = 0; for (let range of ranges) { const group = this._edits.slice(index, index + range); if (group[0] instanceof ResourceFileEdit) { - await this._performFileEdits(group, progress); + await this._performFileEdits(group, undoRedoGroup, progress); } else if (group[0] instanceof ResourceTextEdit) { - await this._performTextEdits(group, progress); + await this._performTextEdits(group, undoRedoGroup, progress); } else if (group[0] instanceof ResourceNotebookCellEdit) { - await this._performCellEdits(group, progress); + await this._performCellEdits(group, undoRedoGroup, progress); } else { console.log('UNKNOWN EDIT'); } @@ -77,21 +79,21 @@ class BulkEdit { } } - private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits); + const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, progress, edits); await model.apply(); } - private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { + private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits); + const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, undoRedoGroup, progress, edits); await model.apply(); } - private async _performCellEdits(edits: ResourceNotebookCellEdit[], progress: IProgress): Promise { + private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, progress: IProgress): Promise { this._logService.debug('_performCellEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkCellEdits, progress, edits); + const model = this._instaService.createInstance(BulkCellEdits, undoRedoGroup, progress, edits); await model.apply(); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index 2a6e2bc7d54..a5f1d7312a0 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -9,7 +9,7 @@ import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/ import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -147,6 +147,7 @@ export class BulkFileEdits { constructor( private readonly _label: string, + private readonly _undoRedoGroup: UndoRedoGroup, private readonly _progress: IProgress, private readonly _edits: ResourceFileEdit[], @IInstantiationService private readonly _instaService: IInstantiationService, @@ -176,6 +177,6 @@ export class BulkFileEdits { } } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations)); + this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations), this._undoRedoGroup); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index 1877843f01a..70103d64a43 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -14,7 +14,7 @@ import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'v import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; import { ResourceMap } from 'vs/base/common/map'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -121,6 +121,7 @@ export class BulkTextEdits { constructor( private readonly _label: string, private readonly _editor: ICodeEditor | undefined, + private readonly _undoRedoGroup: UndoRedoGroup, private readonly _progress: IProgress, edits: ResourceTextEdit[], @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, @@ -229,7 +230,7 @@ export class BulkTextEdits { this._label, tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState())) ); - this._undoRedoService.pushElement(multiModelEditStackElement); + this._undoRedoService.pushElement(multiModelEditStackElement, this._undoRedoGroup); for (const task of tasks) { task.apply(); this._progress.report(undefined);