mirror of
https://github.com/Microsoft/vscode
synced 2024-10-12 22:37:41 +00:00
Introduce grouping for undo/redo elements (fixes #101789)
This commit is contained in:
parent
b33d560521
commit
4aa873b4d1
|
@ -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.
|
||||
|
|
|
@ -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<string>();
|
||||
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> | void, editStackSnapshot: EditStackSnapshot, cleanup: IDisposable = Disposable.None): Promise<void> | void {
|
||||
private _safeInvokeWithLocks(element: StackElement, invoke: () => Promise<void> | void, editStackSnapshot: EditStackSnapshot, cleanup: IDisposable, continuation: () => Promise<void> | void): Promise<void> | void {
|
||||
const releaseLocks = this._acquireLocks(editStackSnapshot);
|
||||
|
||||
let result: Promise<void> | 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<void> {
|
||||
|
||||
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> | 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> | 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> | 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> | 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> | 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> | void {
|
||||
const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource);
|
||||
if (!this._editStacks.has(strResource)) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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<void>,
|
||||
private readonly _edits: ResourceNotebookCellEdit[],
|
||||
@INotebookService private readonly _notebookService: INotebookService,
|
||||
|
|
|
@ -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<void> = { 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(<ResourceFileEdit[]>group, progress);
|
||||
await this._performFileEdits(<ResourceFileEdit[]>group, undoRedoGroup, progress);
|
||||
} else if (group[0] instanceof ResourceTextEdit) {
|
||||
await this._performTextEdits(<ResourceTextEdit[]>group, progress);
|
||||
await this._performTextEdits(<ResourceTextEdit[]>group, undoRedoGroup, progress);
|
||||
} else if (group[0] instanceof ResourceNotebookCellEdit) {
|
||||
await this._performCellEdits(<ResourceNotebookCellEdit[]>group, progress);
|
||||
await this._performCellEdits(<ResourceNotebookCellEdit[]>group, undoRedoGroup, progress);
|
||||
} else {
|
||||
console.log('UNKNOWN EDIT');
|
||||
}
|
||||
|
@ -77,21 +79,21 @@ class BulkEdit {
|
|||
}
|
||||
}
|
||||
|
||||
private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress<void>) {
|
||||
private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, progress: IProgress<void>) {
|
||||
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<void>): Promise<void> {
|
||||
private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, progress: IProgress<void>): Promise<void> {
|
||||
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<void>): Promise<void> {
|
||||
private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, progress: IProgress<void>): Promise<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void>,
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue