diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index c0aee2abe1000aeb52e2255c49f2d2012008a3b3..05baaec0e31f986133d34f817271a9d650418919 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -12,12 +12,12 @@ import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorks import { URI } from 'vs/base/common/uri'; import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; -export class EditStackElement implements IResourceUndoRedoElement { +export class SingleModelEditStackElement implements IResourceUndoRedoElement { public readonly type = UndoRedoElementType.Resource; public readonly label: string; private _isOpen: boolean; - public readonly model: ITextModel; + public model: ITextModel; private readonly _beforeVersionId: number; private readonly _beforeEOL: EndOfLineSequence; private readonly _beforeCursorState: Selection[] | null; @@ -43,6 +43,10 @@ export class EditStackElement implements IResourceUndoRedoElement { this._edits = []; } + public setModel(model: ITextModel): void { + this.model = model; + } + public canAppend(model: ITextModel): boolean { return (this._isOpen && this.model === model); } @@ -78,8 +82,8 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { public readonly label: string; private _isOpen: boolean; - private readonly _editStackElementsArr: EditStackElement[]; - private readonly _editStackElementsMap: Map; + private readonly _editStackElementsArr: SingleModelEditStackElement[]; + private readonly _editStackElementsMap: Map; public get resources(): readonly URI[] { return this._editStackElementsArr.map(editStackElement => editStackElement.model.uri); @@ -87,18 +91,25 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { constructor( label: string, - editStackElements: EditStackElement[] + editStackElements: SingleModelEditStackElement[] ) { this.label = label; this._isOpen = true; this._editStackElementsArr = editStackElements.slice(0); - this._editStackElementsMap = new Map(); + this._editStackElementsMap = new Map(); for (const editStackElement of this._editStackElementsArr) { const key = uriGetComparisonKey(editStackElement.model.uri); this._editStackElementsMap.set(key, editStackElement); } } + public setModel(model: ITextModel): void { + const key = uriGetComparisonKey(model.uri); + if (this._editStackElementsMap.has(key)) { + this._editStackElementsMap.get(key)!.setModel(model); + } + } + public canAppend(model: ITextModel): boolean { if (!this._isOpen) { return false; @@ -140,6 +151,8 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { } } +export type EditStackElement = SingleModelEditStackElement | MultiModelEditStackElement; + function getModelEOL(model: ITextModel): EndOfLineSequence { const eol = model.getEOL(); if (eol === '\n') { @@ -149,11 +162,11 @@ function getModelEOL(model: ITextModel): EndOfLineSequence { } } -function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement | MultiModelEditStackElement { +function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement { if (!element) { return false; } - return ((element instanceof EditStackElement) || (element instanceof MultiModelEditStackElement)); + return ((element instanceof SingleModelEditStackElement) || (element instanceof MultiModelEditStackElement)); } export class EditStack { @@ -177,12 +190,12 @@ export class EditStack { this._undoRedoService.removeElements(this._model.uri); } - private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement | MultiModelEditStackElement { + private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement { const lastElement = this._undoRedoService.getLastElement(this._model.uri); if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) { return lastElement; } - const newElement = new EditStackElement(this._model, beforeCursorState); + const newElement = new SingleModelEditStackElement(this._model, beforeCursorState); this._undoRedoService.pushElement(newElement); return newElement; } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 5e32ac79f3cbeab19984aa3cdaec6e8f56c073af..24537f4603887cf63690d9df458beb37a925f145 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -367,7 +367,6 @@ export class TextModel extends Disposable implements model.ITextModel { this._onWillDispose.fire(); this._languageRegistryListener.dispose(); this._tokenization.dispose(); - this._undoRedoService.removeElements(this.uri); this._isDisposed = true; super.dispose(); this._isDisposing = false; @@ -706,7 +705,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._alternativeVersionId = this._versionId; } - private _overwriteAlternativeVersionId(newAlternativeVersionId: number): void { + public _overwriteAlternativeVersionId(newAlternativeVersionId: number): void { this._alternativeVersionId = newAlternativeVersionId; } diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index b46c56bb6e084524d59687bd5c8d290a771e03e5..f7c5b180b267a4e9268a6aedf8e4643db30ac192 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -25,7 +25,11 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo'; +import { StringSHA1 } from 'vs/base/common/hash'; +import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack'; + +export const MAINTAIN_UNDO_REDO_STACK = true; export interface IEditorSemanticHighlightingOptions { enabled?: boolean; @@ -35,6 +39,18 @@ function MODEL_ID(resource: URI): string { return resource.toString(); } +function computeModelSha1(model: ITextModel): string { + // compute the sha1 + const shaComputer = new StringSHA1(); + const snapshot = model.createSnapshot(); + let text: string | null; + while ((text = snapshot.read())) { + shaComputer.update(text); + } + return shaComputer.digest(); +} + + class ModelData implements IDisposable { public readonly model: ITextModel; @@ -98,6 +114,36 @@ interface IRawConfig { const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF; +interface EditStackPastFutureElements { + past: EditStackElement[]; + future: EditStackElement[]; +} + +function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements { + return (isEditStackElements(undoElements.past) && isEditStackElements(undoElements.future)); +} + +function isEditStackElements(elements: IUndoRedoElement[]): elements is EditStackElement[] { + for (const element of elements) { + if (element instanceof SingleModelEditStackElement) { + continue; + } + if (element instanceof MultiModelEditStackElement) { + continue; + } + return false; + } + return true; +} + +class DisposedModelInfo { + constructor( + public readonly uri: URI, + public readonly sha1: string, + public readonly alternativeVersionId: number, + ) { } +} + export class ModelServiceImpl extends Disposable implements IModelService { public _serviceBrand: undefined; @@ -115,14 +161,13 @@ export class ModelServiceImpl extends Disposable implements IModelService { private readonly _onModelModeChanged: Emitter<{ model: ITextModel; oldModeId: string; }> = this._register(new Emitter<{ model: ITextModel; oldModeId: string; }>()); public readonly onModelModeChanged: Event<{ model: ITextModel; oldModeId: string; }> = this._onModelModeChanged.event; - private _modelCreationOptionsByLanguageAndResource: { - [languageAndResource: string]: ITextModelCreationOptions; - }; + private _modelCreationOptionsByLanguageAndResource: { [languageAndResource: string]: ITextModelCreationOptions; }; /** * All the models known in the system. */ private readonly _models: { [modelId: string]: ModelData; }; + private readonly _disposedModels: Map; constructor( @IConfigurationService configurationService: IConfigurationService, @@ -135,8 +180,9 @@ export class ModelServiceImpl extends Disposable implements IModelService { this._configurationService = configurationService; this._resourcePropertiesService = resourcePropertiesService; this._undoRedoService = undoRedoService; - this._models = {}; this._modelCreationOptionsByLanguageAndResource = Object.create(null); + this._models = {}; + this._disposedModels = new Map(); this._configurationServiceSubscription = this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()); this._updateModelOptions(); @@ -288,6 +334,23 @@ export class ModelServiceImpl extends Disposable implements IModelService { // create & save the model const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget); const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService); + if (resource && this._disposedModels.has(MODEL_ID(resource))) { + const disposedModelData = this._disposedModels.get(MODEL_ID(resource))!; + this._disposedModels.delete(MODEL_ID(resource)); + const elements = this._undoRedoService.getElements(resource); + if (computeModelSha1(model) === disposedModelData.sha1 && isEditStackPastFutureElements(elements)) { + for (const element of elements.past) { + element.setModel(model); + } + for (const element of elements.future) { + element.setModel(model); + } + this._undoRedoService.setElementsIsValid(resource, true); + model._overwriteAlternativeVersionId(disposedModelData.alternativeVersionId); + } else { + this._undoRedoService.removeElements(resource); + } + } const modelId = MODEL_ID(model.uri); if (this._models[modelId]) { @@ -408,6 +471,21 @@ export class ModelServiceImpl extends Disposable implements IModelService { if (!modelData) { return; } + const model = modelData.model; + let maintainUndoRedoStack = false; + if (MAINTAIN_UNDO_REDO_STACK) { + const elements = this._undoRedoService.getElements(resource); + maintainUndoRedoStack = ((elements.past.length > 0 || elements.future.length > 0) && isEditStackPastFutureElements(elements)); + } + + if (maintainUndoRedoStack) { + // We only invalidate the elements, but they remain in the undo-redo service. + this._undoRedoService.setElementsIsValid(resource, false); + this._disposedModels.set(MODEL_ID(resource), new DisposedModelInfo(resource, computeModelSha1(model), model.getAlternativeVersionId())); + } else { + this._undoRedoService.removeElements(resource); + } + modelData.model.dispose(); } diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 84ebc8f2e0e3e561d1cb37f88e44e44470c8bc5c..bc272b954ff80a8d55246a1a401e6416f07a079c 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -9,10 +9,11 @@ import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { createTextBuffer } from 'vs/editor/common/model/textModel'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { ModelServiceImpl, MAINTAIN_UNDO_REDO_STACK } from 'vs/editor/common/services/modelServiceImpl'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -307,6 +308,26 @@ suite('ModelService', () => { ]; assertComputeEdits(file1, file2); }); + + if (MAINTAIN_UNDO_REDO_STACK) { + test('maintains undo for same resource and same content', () => { + const resource = URI.parse('file://test.txt'); + + // create a model + const model1 = modelService.createModel('text', null, resource); + // make an edit + model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); + assert.equal(model1.getValue(), 'text1'); + // dispose it + modelService.destroyModel(resource); + + // create a new model with the same content + const model2 = modelService.createModel('text1', null, resource); + // undo + model2.undo(); + assert.equal(model2.getValue(), 'text'); + }); + } }); function assertComputeEdits(lines1: string[], lines2: string[]): void { diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index b8fef5dd2f68eb35521cc5497285c9b946b08b1e..935e3ffcb22d1d8ff69b59096af4c43571e14e27 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -30,6 +30,13 @@ export interface IWorkspaceUndoRedoElement { split(): IResourceUndoRedoElement[]; } +export type IUndoRedoElement = IResourceUndoRedoElement | IWorkspaceUndoRedoElement; + +export interface IPastFutureElements { + past: IUndoRedoElement[]; + future: IUndoRedoElement[]; +} + export interface IUndoRedoService { _serviceBrand: undefined; @@ -37,12 +44,18 @@ export interface IUndoRedoService { * Add a new element to the `undo` stack. * This will destroy the `redo` stack. */ - pushElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void; + pushElement(element: IUndoRedoElement): void; /** * Get the last pushed element. If the last pushed element has been undone, returns null. */ - getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null; + getLastElement(resource: URI): IUndoRedoElement | null; + + getElements(resource: URI): IPastFutureElements; + + hasElements(resource: URI): boolean; + + setElementsIsValid(resource: URI, isValid: boolean): void; /** * Remove elements that target `resource`. diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 8568767aa3d026990aa13816024773a73afcc155..e5fc882055688a07a0efd8102bfd9daf0751b466 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, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -23,6 +23,7 @@ class ResourceStackElement { public readonly strResource: string; public readonly resources: URI[]; public readonly strResources: string[]; + public isValid: boolean; constructor(actual: IResourceUndoRedoElement) { this.actual = actual; @@ -31,6 +32,11 @@ class ResourceStackElement { this.strResource = uriGetComparisonKey(this.resource); this.resources = [this.resource]; this.strResources = [this.strResource]; + this.isValid = true; + } + + public setValid(isValid: boolean): void { + this.isValid = isValid; } } @@ -39,22 +45,57 @@ const enum RemovedResourceReason { NoParallelUniverses = 1 } +class ResourceReasonPair { + constructor( + public readonly resource: URI, + public readonly reason: RemovedResourceReason + ) { } +} + class RemovedResources { - public readonly set: Set = new Set(); - public readonly reason: [URI[], URI[]] = [[], []]; + private readonly elements = new Map(); + + private _getPath(resource: URI): string { + return resource.scheme === Schemas.file ? resource.fsPath : resource.path; + } public createMessage(): string { + const externalRemoval: string[] = []; + const noParallelUniverses: string[] = []; + for (const [, element] of this.elements) { + const dest = ( + element.reason === RemovedResourceReason.ExternalRemoval + ? externalRemoval + : noParallelUniverses + ); + dest.push(this._getPath(element.resource)); + } + let messages: string[] = []; - if (this.reason[RemovedResourceReason.ExternalRemoval].length > 0) { - const paths = this.reason[RemovedResourceReason.ExternalRemoval].map(uri => uri.scheme === Schemas.file ? uri.fsPath : uri.path); - messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", paths.join(', '))); + if (externalRemoval.length > 0) { + messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", externalRemoval.join(', '))); } - if (this.reason[RemovedResourceReason.NoParallelUniverses].length > 0) { - const paths = this.reason[RemovedResourceReason.NoParallelUniverses].map(uri => uri.scheme === Schemas.file ? uri.fsPath : uri.path); - messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", paths.join(', '))); + if (noParallelUniverses.length > 0) { + messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', '))); } return messages.join('\n'); } + + public get size(): number { + return this.elements.size; + } + + public has(strResource: string): boolean { + return this.elements.has(strResource); + } + + public set(strResource: string, value: ResourceReasonPair): void { + this.elements.set(strResource, value); + } + + public delete(strResource: string): boolean { + return this.elements.delete(strResource); + } } class WorkspaceStackElement { @@ -65,6 +106,7 @@ class WorkspaceStackElement { public readonly resources: URI[]; public readonly strResources: string[]; public removedResources: RemovedResources | null; + public invalidatedResources: RemovedResources | null; constructor(actual: IWorkspaceUndoRedoElement) { this.actual = actual; @@ -72,18 +114,37 @@ class WorkspaceStackElement { this.resources = actual.resources.slice(0); this.strResources = this.resources.map(resource => uriGetComparisonKey(resource)); this.removedResources = null; + this.invalidatedResources = null; } public removeResource(resource: URI, strResource: string, reason: RemovedResourceReason): void { if (!this.removedResources) { this.removedResources = new RemovedResources(); } - if (!this.removedResources.set.has(strResource)) { - this.removedResources.set.add(strResource); - this.removedResources.reason[reason].push(resource); + if (!this.removedResources.has(strResource)) { + this.removedResources.set(strResource, new ResourceReasonPair(resource, reason)); + } + } + + public setValid(resource: URI, strResource: string, isValid: boolean): void { + if (isValid) { + if (this.invalidatedResources) { + this.invalidatedResources.delete(strResource); + if (this.invalidatedResources.size === 0) { + this.invalidatedResources = null; + } + } + } else { + if (!this.invalidatedResources) { + this.invalidatedResources = new RemovedResources(); + } + if (!this.invalidatedResources.has(strResource)) { + this.invalidatedResources.set(strResource, new ResourceReasonPair(resource, RemovedResourceReason.ExternalRemoval)); + } } } } + type StackElement = ResourceStackElement | WorkspaceStackElement; class ResourceEditStack { @@ -110,7 +171,7 @@ export class UndoRedoService implements IUndoRedoService { this._editStacks = new Map(); } - public pushElement(_element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void { + public pushElement(_element: IUndoRedoElement): void { const element: StackElement = (_element.type === UndoRedoElementType.Resource ? new ResourceStackElement(_element) : new WorkspaceStackElement(_element)); for (let i = 0, len = element.resources.length; i < len; i++) { const resource = element.resources[i]; @@ -131,11 +192,18 @@ export class UndoRedoService implements IUndoRedoService { } } editStack.future = []; + if (editStack.past.length > 0) { + const lastElement = editStack.past[editStack.past.length - 1]; + if (lastElement.type === UndoRedoElementType.Resource && !lastElement.isValid) { + // clear undo stack + editStack.past = []; + } + } editStack.past.push(element); } } - public getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null { + public getLastElement(resource: URI): IUndoRedoElement | null { const strResource = uriGetComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; @@ -150,7 +218,7 @@ export class UndoRedoService implements IUndoRedoService { return null; } - private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set | null): void { + private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void { const individualArr = toRemove.actual.split(); const individualMap = new Map(); for (const _element of individualArr) { @@ -178,7 +246,7 @@ export class UndoRedoService implements IUndoRedoService { } } - private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set | null): void { + private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void { const individualArr = toRemove.actual.split(); const individualMap = new Map(); for (const _element of individualArr) { @@ -224,6 +292,56 @@ export class UndoRedoService implements IUndoRedoService { } } + public setElementsIsValid(resource: URI, isValid: boolean): void { + const strResource = uriGetComparisonKey(resource); + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + for (const element of editStack.past) { + if (element.type === UndoRedoElementType.Workspace) { + element.setValid(resource, strResource, isValid); + } else { + element.setValid(isValid); + } + } + for (const element of editStack.future) { + if (element.type === UndoRedoElementType.Workspace) { + element.setValid(resource, strResource, isValid); + } else { + element.setValid(isValid); + } + } + } + } + + // resource + + public hasElements(resource: URI): boolean { + const strResource = uriGetComparisonKey(resource); + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + return (editStack.past.length > 0 || editStack.future.length > 0); + } + return false; + } + + public getElements(resource: URI): IPastFutureElements { + const past: IUndoRedoElement[] = []; + const future: IUndoRedoElement[] = []; + + const strResource = uriGetComparisonKey(resource); + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + for (const element of editStack.past) { + past.push(element.actual); + } + for (const element of editStack.future) { + future.push(element.actual); + } + } + + return { past, future }; + } + public canUndo(resource: URI): boolean { const strResource = uriGetComparisonKey(resource); if (this._editStacks.has(strResource)) { @@ -257,11 +375,17 @@ export class UndoRedoService implements IUndoRedoService { private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise | void { if (element.removedResources) { - this._splitPastWorkspaceElement(element, element.removedResources.set); + this._splitPastWorkspaceElement(element, element.removedResources); const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); this._notificationService.info(message); return this.undo(resource); } + if (element.invalidatedResources) { + this._splitPastWorkspaceElement(element, element.invalidatedResources); + const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()); + this._notificationService.info(message); + return this.undo(resource); + } // this must be the last past element in all the impacted resources! let affectedEditStacks: ResourceEditStack[] = []; @@ -313,6 +437,12 @@ export class UndoRedoService implements IUndoRedoService { } private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + if (!element.isValid) { + // invalid element => immediately flush edit stack! + editStack.past = []; + editStack.future = []; + return; + } editStack.past.pop(); editStack.future.push(element); return this._safeInvoke(element, () => element.actual.undo()); @@ -348,11 +478,17 @@ export class UndoRedoService implements IUndoRedoService { private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise | void { if (element.removedResources) { - this._splitFutureWorkspaceElement(element, element.removedResources.set); + this._splitFutureWorkspaceElement(element, element.removedResources); const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); this._notificationService.info(message); return this.redo(resource); } + if (element.invalidatedResources) { + this._splitFutureWorkspaceElement(element, element.invalidatedResources); + const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()); + this._notificationService.info(message); + return this.redo(resource); + } // this must be the last future element in all the impacted resources! let affectedEditStacks: ResourceEditStack[] = []; @@ -383,6 +519,12 @@ export class UndoRedoService implements IUndoRedoService { } private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + if (!element.isValid) { + // invalid element => immediately flush edit stack! + editStack.past = []; + editStack.future = []; + return; + } editStack.future.pop(); editStack.past.push(element); return this._safeInvoke(element, () => element.actual.redo()); diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 250fbf7243659895745a1553463b24366275aeae..23cc7b80b96901f0456f707c0ed69a9cd9d9aa09 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -28,7 +28,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerServ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { EditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; +import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; @@ -234,7 +234,7 @@ class BulkEditModel implements IDisposable { const multiModelEditStackElement = new MultiModelEditStackElement( this._label || localize('workspaceEdit', "Workspace Edit"), - tasks.map(t => new EditStackElement(t.model, t.getBeforeCursorState())) + tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState())) ); this._undoRedoService.pushElement(multiModelEditStackElement);