未验证 提交 162874e9 编写于 作者: A Alex Dima

Load text models from disk when doing a cross file undo or redo (fixes #92272)

上级 f4c74bd0
......@@ -12,6 +12,7 @@ import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorks
import { URI } from 'vs/base/common/uri';
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange';
import * as buffer from 'vs/base/common/buffer';
import { IDisposable } from 'vs/base/common/lifecycle';
function uriGetComparisonKey(resource: URI): string {
return resource.toString();
......@@ -138,6 +139,10 @@ class SingleModelEditStackData {
}
}
export interface IUndoRedoDelegate {
prepareUndoRedo(element: MultiModelEditStackElement): Promise<IDisposable> | IDisposable | void;
}
export class SingleModelEditStackElement implements IResourceUndoRedoElement {
public model: ITextModel | URI;
......@@ -224,6 +229,8 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
private readonly _editStackElementsArr: SingleModelEditStackElement[];
private readonly _editStackElementsMap: Map<string, SingleModelEditStackElement>;
private _delegate: IUndoRedoDelegate | null;
public get resources(): readonly URI[] {
return this._editStackElementsArr.map(editStackElement => editStackElement.resource);
}
......@@ -240,6 +247,27 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
const key = uriGetComparisonKey(editStackElement.resource);
this._editStackElementsMap.set(key, editStackElement);
}
this._delegate = null;
}
public setDelegate(delegate: IUndoRedoDelegate): void {
this._delegate = delegate;
}
public prepareUndoRedo(): Promise<IDisposable> | IDisposable | void {
if (this._delegate) {
return this._delegate.prepareUndoRedo(this);
}
}
public getMissingModels(): URI[] {
const result: URI[] = [];
for (const editStackElement of this._editStackElementsArr) {
if (URI.isUri(editStackElement.model)) {
result.push(editStackElement.model);
}
}
return result;
}
public setModel(model: ITextModel | URI): void {
......
......@@ -113,12 +113,12 @@ interface IRawConfig {
const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF;
interface EditStackPastFutureElements {
export interface EditStackPastFutureElements {
past: EditStackElement[];
future: EditStackElement[];
}
function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements {
export function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements {
return (isEditStackElements(undoElements.past) && isEditStackElements(undoElements.future));
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IModelService } from 'vs/editor/common/services/modelService';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
import { isEditStackPastFutureElements } from 'vs/editor/common/services/modelServiceImpl';
import { IUndoRedoDelegate, MultiModelEditStackElement } from 'vs/editor/common/model/editStack';
export class ModelUndoRedoParticipant extends Disposable implements IUndoRedoDelegate {
constructor(
@IModelService private readonly _modelService: IModelService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
) {
super();
this._register(this._modelService.onModelRemoved((model) => {
// a model will get disposed, so let's check if the undo redo stack is maintained
const elements = this._undoRedoService.getElements(model.uri);
if (elements.past.length === 0 && elements.future.length === 0) {
return;
}
if (!isEditStackPastFutureElements(elements)) {
return;
}
for (const element of elements.past) {
if (element.type === UndoRedoElementType.Workspace) {
element.setDelegate(this);
}
}
for (const element of elements.future) {
if (element.type === UndoRedoElementType.Workspace) {
element.setDelegate(this);
}
}
}));
}
public prepareUndoRedo(element: MultiModelEditStackElement): IDisposable | Promise<IDisposable> {
// Load all the needed text models
const missingModels = element.getMissingModels();
if (missingModels.length === 0) {
// All models are available!
return Disposable.None;
}
const disposablesPromises = missingModels.map(async (uri) => {
try {
const reference = await this._textModelService.createModelReference(uri);
return <IDisposable>reference;
} catch (err) {
// This model could not be loaded, maybe it was deleted in the meantime?
return Disposable.None;
}
});
return Promise.all(disposablesPromises).then(disposables => {
return {
dispose: () => dispose(disposables)
};
});
}
}
......@@ -108,12 +108,11 @@ function withTypedEditor<T>(widget: IEditor, codeEditorCallback: (editor: ICodeE
export class SimpleEditorModelResolverService implements ITextModelService {
public _serviceBrand: undefined;
private readonly modelService: IModelService | undefined;
private editor?: IEditor;
constructor(modelService: IModelService | undefined) {
this.modelService = modelService;
}
constructor(
@IModelService private readonly modelService: IModelService
) { }
public setEditor(editor: IEditor): void {
this.editor = editor;
......@@ -146,7 +145,7 @@ export class SimpleEditorModelResolverService implements ITextModelService {
}
private findModel(editor: ICodeEditor, resource: URI): ITextModel | null {
let model = this.modelService ? this.modelService.getModel(resource) : editor.getModel();
let model = this.modelService.getModel(resource);
if (model && model.uri.toString() !== resource.toString()) {
return null;
}
......
......@@ -5,6 +5,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { IDisposable } from 'vs/base/common/lifecycle';
export const IUndoRedoService = createDecorator<IUndoRedoService>('undoRedoService');
......@@ -28,6 +29,13 @@ export interface IWorkspaceUndoRedoElement {
undo(): Promise<void> | void;
redo(): Promise<void> | void;
split(): IResourceUndoRedoElement[];
/**
* If implemented, will be invoked before calling `undo()` or `redo()`.
* This is a good place to prepare everything such that the calls to `undo()` or `redo()` are synchronous.
* If a disposable is returned, it will be invoked to clean things up.
*/
prepareUndoRedo?(): Promise<IDisposable> | IDisposable | void;
}
export type IUndoRedoElement = IResourceUndoRedoElement | IWorkspaceUndoRedoElement;
......
......@@ -12,6 +12,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { Schemas } from 'vs/base/common/network';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
function uriGetComparisonKey(resource: URI): string {
return resource.toString();
......@@ -76,7 +77,7 @@ class RemovedResources {
let messages: string[] = [];
if (externalRemoval.length > 0) {
messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", externalRemoval.join(', ')));
messages.push(nls.localize('externalRemoval', "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', ')));
}
if (noParallelUniverses.length > 0) {
messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', ')));
......@@ -386,7 +387,7 @@ export class UndoRedoService implements IUndoRedoService {
};
}
private _safeInvokeWithLocks(element: StackElement, invoke: () => Promise<void> | void, affectedEditStacks: ResourceEditStack[]): Promise<void> | void {
private _safeInvokeWithLocks(element: StackElement, invoke: () => Promise<void> | void, affectedEditStacks: ResourceEditStack[], cleanup: IDisposable = Disposable.None): Promise<void> | void {
const releaseLocks = this._acquireLocks(affectedEditStacks);
let result: Promise<void> | void;
......@@ -394,6 +395,7 @@ export class UndoRedoService implements IUndoRedoService {
result = invoke();
} catch (err) {
releaseLocks();
cleanup.dispose();
return this._onError(err, element);
}
......@@ -402,18 +404,32 @@ export class UndoRedoService implements IUndoRedoService {
return result.then(
() => {
releaseLocks();
cleanup.dispose();
},
(err) => {
releaseLocks();
cleanup.dispose();
return this._onError(err, element);
}
);
} else {
// result is void
releaseLocks();
cleanup.dispose();
}
}
private async _invokePrepare(element: WorkspaceStackElement): Promise<IDisposable> {
if (typeof element.actual.prepareUndoRedo === 'undefined') {
return Disposable.None;
}
const result = element.actual.prepareUndoRedo();
if (typeof result === 'undefined') {
return Disposable.None;
}
return result;
}
private _getAffectedEditStacks(element: WorkspaceStackElement): ResourceEditStack[] {
const affectedEditStacks: ResourceEditStack[] = [];
for (const strResource of element.strResources) {
......@@ -422,14 +438,14 @@ export class UndoRedoService implements IUndoRedoService {
return affectedEditStacks;
}
private _checkWorkspaceUndo(resource: URI, element: WorkspaceStackElement, affectedEditStacks: ResourceEditStack[]): WorkspaceVerificationError | null {
private _checkWorkspaceUndo(resource: URI, element: WorkspaceStackElement, affectedEditStacks: ResourceEditStack[], checkInvalidatedResources: boolean): WorkspaceVerificationError | null {
if (element.removedResources) {
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 new WorkspaceVerificationError(this.undo(resource));
}
if (element.invalidatedResources) {
if (checkInvalidatedResources && 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);
......@@ -470,7 +486,7 @@ export class UndoRedoService implements IUndoRedoService {
private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise<void> | void {
const affectedEditStacks = this._getAffectedEditStacks(element);
const verificationError = this._checkWorkspaceUndo(resource, element, affectedEditStacks);
const verificationError = this._checkWorkspaceUndo(resource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false);
if (verificationError) {
return verificationError.returnValue;
}
......@@ -492,29 +508,44 @@ export class UndoRedoService implements IUndoRedoService {
);
if (result.choice === 2) {
// cancel
// choice: cancel
return;
}
if (result.choice === 1) {
// undo this file
// choice: undo this file
this._splitPastWorkspaceElement(element, null);
return this.undo(resource);
}
// undo in all files
// choice: undo in all files
// At this point, it is possible that the element has been made invalid in the meantime (due to the confirmation await)
const verificationError = this._checkWorkspaceUndo(resource, element, affectedEditStacks);
if (verificationError) {
return verificationError.returnValue;
const verificationError1 = this._checkWorkspaceUndo(resource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false);
if (verificationError1) {
return verificationError1.returnValue;
}
// prepare
let cleanup: IDisposable;
try {
cleanup = await this._invokePrepare(element);
} catch (err) {
return this._onError(err, element);
}
// At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await)
const verificationError2 = this._checkWorkspaceUndo(resource, element, affectedEditStacks, /*now also check that there are no more invalidated resources*/true);
if (verificationError2) {
cleanup.dispose();
return verificationError2.returnValue;
}
for (const editStack of affectedEditStacks) {
editStack.past.pop();
editStack.future.push(element);
}
return this._safeInvokeWithLocks(element, () => element.actual.undo(), affectedEditStacks);
return this._safeInvokeWithLocks(element, () => element.actual.undo(), affectedEditStacks, cleanup);
}
private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
......@@ -562,14 +593,14 @@ export class UndoRedoService implements IUndoRedoService {
return false;
}
private _checkWorkspaceRedo(resource: URI, element: WorkspaceStackElement, affectedEditStacks: ResourceEditStack[]): WorkspaceVerificationError | null {
private _checkWorkspaceRedo(resource: URI, element: WorkspaceStackElement, affectedEditStacks: ResourceEditStack[], checkInvalidatedResources: boolean): WorkspaceVerificationError | null {
if (element.removedResources) {
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 new WorkspaceVerificationError(this.redo(resource));
}
if (element.invalidatedResources) {
if (checkInvalidatedResources && 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);
......@@ -610,8 +641,26 @@ export class UndoRedoService implements IUndoRedoService {
private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise<void> | void {
const affectedEditStacks = this._getAffectedEditStacks(element);
const verificationError = this._checkWorkspaceRedo(resource, element, affectedEditStacks);
const verificationError = this._checkWorkspaceRedo(resource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false);
if (verificationError) {
return verificationError.returnValue;
}
return this._executeWorkspaceRedo(resource, element, affectedEditStacks);
}
private async _executeWorkspaceRedo(resource: URI, element: WorkspaceStackElement, affectedEditStacks: ResourceEditStack[]): Promise<void> {
// prepare
let cleanup: IDisposable;
try {
cleanup = await this._invokePrepare(element);
} catch (err) {
return this._onError(err, element);
}
// At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await)
const verificationError = this._checkWorkspaceRedo(resource, element, affectedEditStacks, /*now also check that there are no more invalidated resources*/true);
if (verificationError) {
cleanup.dispose();
return verificationError.returnValue;
}
......@@ -619,7 +668,7 @@ export class UndoRedoService implements IUndoRedoService {
editStack.future.pop();
editStack.past.push(element);
}
return this._safeInvokeWithLocks(element, () => element.actual.redo(), affectedEditStacks);
return this._safeInvokeWithLocks(element, () => element.actual.redo(), affectedEditStacks, cleanup);
}
private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
......
......@@ -6,7 +6,7 @@
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextModel } from 'vs/editor/common/model';
import { IDisposable, toDisposable, IReference, ReferenceCollection } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable, IReference, ReferenceCollection, Disposable } from 'vs/base/common/lifecycle';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
import { ITextFileService, TextFileLoadReason } from 'vs/workbench/services/textfile/common/textfiles';
......@@ -16,6 +16,8 @@ import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textF
import { IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { ModelUndoRedoParticipant } from 'vs/editor/common/services/modelUndoRedoParticipant';
class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorModel>> {
......@@ -159,7 +161,7 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
}
}
export class TextModelResolverService implements ITextModelService {
export class TextModelResolverService extends Disposable implements ITextModelService {
_serviceBrand: undefined;
......@@ -167,8 +169,12 @@ export class TextModelResolverService implements ITextModelService {
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService
@IFileService private readonly fileService: IFileService,
@IUndoRedoService private readonly undoRedoService: IUndoRedoService,
@IModelService private readonly modelService: IModelService
) {
super();
this._register(new ModelUndoRedoParticipant(this.modelService, this, this.undoRedoService));
}
async createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册