未验证 提交 691c4b15 编写于 作者: A Alex Dima

Cross-file undo/redo

上级 4ae83d0a
......@@ -8,17 +8,15 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { Selection } from 'vs/editor/common/core/selection';
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel, IValidEditOperations } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { IUndoRedoService, IUndoRedoElement, IUndoRedoContext } from 'vs/platform/undoRedo/common/undoRedo';
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
import { Severity } from 'vs/platform/notification/common/notification';
export class EditStackElement implements IUndoRedoElement {
export class EditStackElement implements IResourceUndoRedoElement {
public readonly type = UndoRedoElementType.Resource;
public readonly label: string;
private _isOpen: boolean;
private _isValid: boolean;
public readonly model: ITextModel;
private readonly _beforeVersionId: number;
private readonly _beforeEOL: EndOfLineSequence;
......@@ -28,14 +26,13 @@ export class EditStackElement implements IUndoRedoElement {
private _afterCursorState: Selection[] | null;
private _edits: IValidEditOperations[];
public get resources(): readonly URI[] {
return [this.model.uri];
public get resource(): URI {
return this.model.uri;
}
constructor(model: ITextModel, beforeCursorState: Selection[] | null) {
this.label = nls.localize('edit', "Typing");
this._isOpen = true;
this._isValid = true;
this.model = model;
this._beforeVersionId = this.model.getAlternativeVersionId();
this._beforeEOL = getModelEOL(this.model);
......@@ -46,10 +43,6 @@ export class EditStackElement implements IUndoRedoElement {
this._edits = [];
}
public isValid(): boolean {
return this._isValid;
}
public canAppend(model: ITextModel): boolean {
return (this._isOpen && this.model === model);
}
......@@ -67,44 +60,26 @@ export class EditStackElement implements IUndoRedoElement {
this._isOpen = false;
}
public canUndo(): boolean {
if (!this._isValid) {
return false;
}
return (this._afterVersionId === this.model.getAlternativeVersionId());
}
public undo(ctx: IUndoRedoContext): void {
public undo(): void {
this._isOpen = false;
this._edits.reverse();
this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState);
}
public canRedo(): boolean {
if (!this._isValid) {
return false;
}
return (this._beforeVersionId === this.model.getAlternativeVersionId());
}
public redo(ctx: IUndoRedoContext): void {
public redo(): void {
this._edits.reverse();
this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState);
}
public invalidate(resource: URI): void {
this._isValid = false;
}
}
export class MultiModelEditStackElement implements IUndoRedoElement {
export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
public readonly type = UndoRedoElementType.Workspace;
public readonly label: string;
private _isOpen: boolean;
private readonly _editStackElementsArr: EditStackElement[];
private readonly _editStackElementsMap: Map<string, EditStackElement>;
private _isValid: boolean;
public get resources(): readonly URI[] {
return this._editStackElementsArr.map(editStackElement => editStackElement.model.uri);
......@@ -112,8 +87,7 @@ export class MultiModelEditStackElement implements IUndoRedoElement {
constructor(
label: string,
editStackElements: EditStackElement[],
@IDialogService private readonly _dialogService: IDialogService
editStackElements: EditStackElement[]
) {
this.label = label;
this._isOpen = true;
......@@ -123,7 +97,6 @@ export class MultiModelEditStackElement implements IUndoRedoElement {
const key = uriGetComparisonKey(editStackElement.model.uri);
this._editStackElementsMap.set(key, editStackElement);
}
this._isValid = true;
}
public canAppend(model: ITextModel): boolean {
......@@ -148,66 +121,22 @@ export class MultiModelEditStackElement implements IUndoRedoElement {
this._isOpen = false;
}
private _canUndo(): boolean {
if (!this._isValid) {
return false;
}
for (const editStackElement of this._editStackElementsArr) {
if (!editStackElement.canUndo()) {
return false;
}
}
return true;
}
undo(ctx: IUndoRedoContext): void {
public undo(): void {
this._isOpen = false;
if (this._canUndo()) {
for (const editStackElement of this._editStackElementsArr) {
editStackElement.undo(ctx);
}
} else {
// cannot apply!
const validStackElements = this._editStackElementsArr.filter(stackElement => stackElement.isValid());
ctx.replaceCurrentElement(validStackElements);
this._dialogService.show(Severity.Info, nls.localize('workspace', "Could not apply the edit in all the impacted files."), []);
}
}
private _canRedo(): boolean {
if (!this._isValid) {
return false;
}
for (const editStackElement of this._editStackElementsArr) {
if (!editStackElement.canRedo()) {
return false;
}
editStackElement.undo();
}
return true;
}
redo(ctx: IUndoRedoContext): void {
if (this._canRedo()) {
for (const editStackElement of this._editStackElementsArr) {
editStackElement.redo(ctx);
}
} else {
// cannot apply!
const validStackElements = this._editStackElementsArr.filter(stackElement => stackElement.isValid());
ctx.replaceCurrentElement(validStackElements);
this._dialogService.show(Severity.Info, nls.localize('workspace', "Could not apply the edit in all the impacted files."), []);
public redo(): void {
for (const editStackElement of this._editStackElementsArr) {
editStackElement.redo();
}
}
invalidate(resource: URI): void {
const key = uriGetComparisonKey(resource);
if (!this._editStackElementsMap.has(key)) {
return;
}
this._isValid = false;
const stackElement = this._editStackElementsMap.get(key)!;
stackElement.invalidate(resource);
public split(): IResourceUndoRedoElement[] {
return this._editStackElementsArr;
}
}
......@@ -220,7 +149,7 @@ function getModelEOL(model: ITextModel): EndOfLineSequence {
}
}
function isKnownStackElement(element: IUndoRedoElement | null): element is EditStackElement | MultiModelEditStackElement {
function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement | MultiModelEditStackElement {
if (!element) {
return false;
}
......
......@@ -38,6 +38,8 @@ import { Constants } from 'vs/base/common/uint';
import { EditorTheme } from 'vs/editor/common/view/viewContext';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { NoOpNotification, INotificationService } from 'vs/platform/notification/common/notification';
function createTextBufferBuilder() {
return new PieceTreeTextBufferBuilder();
......@@ -190,7 +192,24 @@ export class TextModel extends Disposable implements model.ITextModel {
};
public static createFromString(text: string, options: model.ITextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier: LanguageIdentifier | null = null, uri: URI | null = null): TextModel {
return new TextModel(text, options, languageIdentifier, uri, new UndoRedoService());
const dialogService = new class implements IDialogService {
_serviceBrand: undefined;
confirm() { return Promise.resolve({ confirmed: false }); }
show() { return Promise.resolve({ choice: 0 }); }
about() { return Promise.resolve(); }
};
const noop = new NoOpNotification();
const notificationService = new class implements INotificationService {
_serviceBrand: undefined;
info() { return noop; }
warn() { return noop; }
error() { return noop; }
notify() { return noop; }
prompt() { return noop; }
status() { return Disposable.None; }
setFilter() { }
};
return new TextModel(text, options, languageIdentifier, uri, new UndoRedoService(dialogService, notificationService));
}
public static resolveOptions(textBuffer: model.ITextBuffer, options: model.ITextModelCreationOptions): model.TextModelResolvedOptions {
......
......@@ -16,6 +16,8 @@ import { FindModelBoundToEditorModel } from 'vs/editor/contrib/find/findModel';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
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';
suite('FindModel', () => {
......@@ -45,7 +47,7 @@ suite('FindModel', () => {
const factory = ptBuilder.finish();
withTestCodeEditor([],
{
model: new TextModel(factory, TextModel.DEFAULT_CREATION_OPTIONS, null, null, new UndoRedoService())
model: new TextModel(factory, TextModel.DEFAULT_CREATION_OPTIONS, null, null, new UndoRedoService(new TestDialogService(), new TestNotificationService()))
},
(editor, cursor) => callback(editor as unknown as IActiveCodeEditor, cursor)
);
......
......@@ -20,6 +20,8 @@ import { TestTextResourcePropertiesService } from 'vs/editor/test/common/service
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { NullLogService } from 'vs/platform/log/common/log';
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';
class MockJSMode extends MockMode {
......@@ -48,7 +50,7 @@ suite('SmartSelect', () => {
setup(() => {
const configurationService = new TestConfigurationService();
modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService());
modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(new TestDialogService(), new TestNotificationService()));
mode = new MockJSMode();
});
......
......@@ -152,7 +152,7 @@ export module StaticServices {
export const logService = define(ILogService, () => new NullLogService());
export const undoRedoService = define(IUndoRedoService, () => new UndoRedoService());
export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o)));
export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o)));
......
......@@ -19,6 +19,8 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { NullLogService } from 'vs/platform/log/common/log';
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';
const GENERATE_TESTS = false;
......@@ -30,7 +32,7 @@ suite('ModelService', () => {
configService.setUserConfiguration('files', { 'eol': '\n' });
configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot'));
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService());
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(new TestDialogService(), new TestNotificationService()));
});
teardown(() => {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Severity from 'vs/base/common/severity';
import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IShowResult } from 'vs/platform/dialogs/common/dialogs';
export class TestDialogService implements IDialogService {
_serviceBrand: undefined;
confirm(_confirmation: IConfirmation): Promise<IConfirmationResult> { return Promise.resolve({ confirmed: false }); }
show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise<IShowResult> { return Promise.resolve({ choice: 0 }); }
about(): Promise<void> { return Promise.resolve(); }
}
......@@ -157,7 +157,7 @@ export class InstantiationService implements IInstantiationService {
graph.lookupOrInsertNode(item);
// a weak but working heuristic for cycle checks
if (cycleCount++ > 150) {
if (cycleCount++ > 200) {
throw new CyclicDependencyError(graph);
}
......
......@@ -8,42 +8,26 @@ import { URI } from 'vs/base/common/uri';
export const IUndoRedoService = createDecorator<IUndoRedoService>('undoRedoService');
export interface IUndoRedoContext {
replaceCurrentElement(others: IUndoRedoElement[]): void;
export const enum UndoRedoElementType {
Resource,
Workspace
}
export interface IUndoRedoElement {
/**
* None, one or multiple resources that this undo/redo element impacts.
*/
readonly resources: readonly URI[];
/**
* The label of the undo/redo element.
*/
export interface IResourceUndoRedoElement {
readonly type: UndoRedoElementType.Resource;
readonly resource: URI;
readonly label: string;
undo(): Promise<void> | void;
redo(): Promise<void> | void;
}
/**
* Undo.
* Will always be called before `redo`.
* Can be called multiple times.
* e.g. `undo` -> `redo` -> `undo` -> `redo`
*/
undo(ctx: IUndoRedoContext): void;
/**
* Redo.
* Will always be called after `undo`.
* Can be called multiple times.
* e.g. `undo` -> `redo` -> `undo` -> `redo`
*/
redo(ctx: IUndoRedoContext): void;
/**
* Invalidate the edits concerning `resource`.
* i.e. the undo/redo stack for that particular resource has been destroyed.
*/
invalidate(resource: URI): void;
export interface IWorkspaceUndoRedoElement {
readonly type: UndoRedoElementType.Workspace;
readonly resources: readonly URI[];
readonly label: string;
undo(): Promise<void> | void;
redo(): Promise<void> | void;
split(): IResourceUndoRedoElement[];
}
export interface IUndoRedoService {
......@@ -53,12 +37,12 @@ export interface IUndoRedoService {
* Add a new element to the `undo` stack.
* This will destroy the `redo` stack.
*/
pushElement(element: IUndoRedoElement): void;
pushElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void;
/**
* Get the last pushed element. If the last pushed element has been undone, returns null.
*/
getLastElement(resource: URI): IUndoRedoElement | null;
getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null;
/**
* Remove elements that target `resource`.
......@@ -66,8 +50,8 @@ export interface IUndoRedoService {
removeElements(resource: URI): void;
canUndo(resource: URI): boolean;
undo(resource: URI): void;
undo(resource: URI): Promise<void> | void;
redo(resource: URI): void;
canRedo(resource: URI): boolean;
redo(resource: URI): Promise<void> | void;
}
......@@ -3,38 +3,52 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUndoRedoService, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
import * as nls from 'vs/nls';
import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType } 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';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
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';
class ResourceStackElement {
public readonly type = UndoRedoElementType.Resource;
public readonly actual: IResourceUndoRedoElement;
public readonly label: string;
public readonly resource: URI;
public readonly strResource: string;
public readonly resources: URI[];
public readonly strResources: string[];
class StackElement {
public readonly actual: IUndoRedoElement;
constructor(actual: IResourceUndoRedoElement) {
this.actual = actual;
this.label = actual.label;
this.resource = actual.resource;
this.strResource = uriGetComparisonKey(this.resource);
this.resources = [this.resource];
this.strResources = [this.strResource];
}
}
class WorkspaceStackElement {
public readonly type = UndoRedoElementType.Workspace;
public readonly actual: IWorkspaceUndoRedoElement;
public readonly label: string;
public resources: URI[];
public strResources: string[];
public readonly resources: URI[];
public readonly strResources: string[];
constructor(actual: IUndoRedoElement) {
constructor(actual: IWorkspaceUndoRedoElement) {
this.actual = actual;
this.label = actual.label;
this.resources = actual.resources.slice(0);
this.strResources = this.resources.map(resource => uriGetComparisonKey(resource));
}
public invalidate(resource: URI): void {
const strResource = uriGetComparisonKey(resource);
for (let i = 0, len = this.strResources.length; i < len; i++) {
if (this.strResources[i] === strResource) {
this.resources.splice(i, 1);
this.strResources.splice(i, 1);
break;
}
}
this.actual.invalidate(resource);
}
}
type StackElement = ResourceStackElement | WorkspaceStackElement;
class ResourceEditStack {
public resource: URI;
......@@ -53,12 +67,15 @@ export class UndoRedoService implements IUndoRedoService {
private readonly _editStacks: Map<string, ResourceEditStack>;
constructor() {
constructor(
@IDialogService private readonly _dialogService: IDialogService,
@INotificationService private readonly _notificationService: INotificationService,
) {
this._editStacks = new Map<string, ResourceEditStack>();
}
public pushElement(_element: IUndoRedoElement): void {
const element = new StackElement(_element);
public pushElement(_element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): 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];
const strResource = element.strResources[i];
......@@ -73,14 +90,16 @@ export class UndoRedoService implements IUndoRedoService {
// remove the future
for (const futureElement of editStack.future) {
futureElement.invalidate(resource);
if (futureElement.type === UndoRedoElementType.Workspace) {
this._splitFutureWorkspaceElement(futureElement, strResource);
}
}
editStack.future = [];
editStack.past.push(element);
}
}
public getLastElement(resource: URI): IUndoRedoElement | null {
public getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
......@@ -95,15 +114,75 @@ export class UndoRedoService implements IUndoRedoService {
return null;
}
private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreStrResource: string | null): void {
const individualArr = toRemove.actual.split();
const individualMap = new Map<string, ResourceStackElement>();
for (const _element of individualArr) {
const element = new ResourceStackElement(_element);
individualMap.set(element.strResource, element);
}
for (const strResource of toRemove.strResources) {
if (strResource === ignoreStrResource) {
continue;
}
const editStack = this._editStacks.get(strResource)!;
for (let j = editStack.past.length - 1; j >= 0; j--) {
if (editStack.past[j] === toRemove) {
if (individualMap.has(strResource)) {
// gets replaced
editStack.past[j] = individualMap.get(strResource)!;
} else {
// gets deleted
editStack.past.splice(j, 1);
}
break;
}
}
}
}
private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreStrResource: string | null): void {
const individualArr = toRemove.actual.split();
const individualMap = new Map<string, ResourceStackElement>();
for (const _element of individualArr) {
const element = new ResourceStackElement(_element);
individualMap.set(element.strResource, element);
}
for (const strResource of toRemove.strResources) {
if (strResource === ignoreStrResource) {
continue;
}
const editStack = this._editStacks.get(strResource)!;
for (let j = editStack.future.length - 1; j >= 0; j--) {
if (editStack.future[j] === toRemove) {
if (individualMap.has(strResource)) {
// gets replaced
editStack.future[j] = individualMap.get(strResource)!;
} else {
// gets deleted
editStack.future.splice(j, 1);
}
break;
}
}
}
}
public removeElements(resource: URI): void {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (const pastElement of editStack.past) {
pastElement.invalidate(resource);
if (pastElement.type === UndoRedoElementType.Workspace) {
this._splitPastWorkspaceElement(pastElement, strResource);
}
}
for (const futureElement of editStack.future) {
futureElement.invalidate(resource);
if (futureElement.type === UndoRedoElementType.Workspace) {
this._splitFutureWorkspaceElement(futureElement, strResource);
}
}
this._editStacks.delete(strResource);
}
......@@ -118,71 +197,93 @@ export class UndoRedoService implements IUndoRedoService {
return false;
}
public undo(resource: URI): void {
const strResource = uriGetComparisonKey(resource);
if (!this._editStacks.has(strResource)) {
return;
private _onError(err: Error, element: StackElement): void {
onUnexpectedError(err);
// An error occured while undoing or redoing => drop the undo/redo stack for all affected resources
for (const resource of element.resources) {
this.removeElements(resource);
}
this._notificationService.error(err);
}
const editStack = this._editStacks.get(strResource)!;
if (editStack.past.length === 0) {
return;
private _safeInvoke(element: StackElement, invoke: () => Promise<void> | void): Promise<void> | void {
let result: Promise<void> | void;
try {
result = invoke();
} catch (err) {
return this._onError(err, element);
}
const element = editStack.past[editStack.past.length - 1];
if (result) {
return result.then(undefined, (err) => this._onError(err, element));
}
}
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null;
try {
element.actual.undo({
replaceCurrentElement: (others: IUndoRedoElement[]): void => {
replaceCurrentElement = others;
}
});
} catch (e) {
onUnexpectedError(e);
editStack.past.pop();
editStack.future.push(element);
return;
private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise<void> | void {
// this must be the last past element in all the impacted resources!
let affectedEditStacks: ResourceEditStack[] = [];
for (const strResource of element.strResources) {
affectedEditStacks.push(this._editStacks.get(strResource)!);
}
if (replaceCurrentElement === null) {
// regular case
editStack.past.pop();
editStack.future.push(element);
let cannotUndoDueToResources: URI[] = [];
for (const editStack of affectedEditStacks) {
if (editStack.past.length === 0 || editStack.past[editStack.past.length - 1] !== element) {
cannotUndoDueToResources.push(editStack.resource);
}
}
if (cannotUndoDueToResources.length > 0) {
this._splitPastWorkspaceElement(element, null);
const paths = cannotUndoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path);
const message = nls.localize('undoFurtherChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, paths.join(', '));
this._notificationService.info(message);
return;
}
const replaceCurrentElementMap = new Map<string, StackElement>();
let foundReplacementForThisResource = false;
for (const _replace of replaceCurrentElement) {
const replace = new StackElement(_replace);
for (const strReplaceResource of replace.strResources) {
replaceCurrentElementMap.set(strReplaceResource, replace);
if (strReplaceResource === strResource) {
foundReplacementForThisResource = true;
return this._dialogService.show(
Severity.Info,
nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label),
[
nls.localize('ok', "Yes, change {0} files.", affectedEditStacks.length),
nls.localize('nok', "No, change only this file.")
]
).then((result) => {
if (result.choice === 0) {
for (const editStack of affectedEditStacks) {
editStack.past.pop();
editStack.future.push(element);
}
return this._safeInvoke(element, () => element.actual.undo());
} else {
this._splitPastWorkspaceElement(element, null);
return this.undo(resource);
}
});
}
private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
editStack.past.pop();
editStack.future.push(element);
return this._safeInvoke(element, () => element.actual.undo());
}
public undo(resource: URI): Promise<void> | void {
const strResource = uriGetComparisonKey(resource);
if (!this._editStacks.has(strResource)) {
return;
}
for (let i = 0, len = element.strResources.length; i < len; i++) {
const strResource = element.strResources[i];
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (let j = editStack.past.length - 1; j >= 0; j--) {
if (editStack.past[j] === element) {
if (replaceCurrentElementMap.has(strResource)) {
editStack.past[j] = replaceCurrentElementMap.get(strResource)!;
} else {
editStack.past.splice(j, 1);
}
break;
}
}
}
const editStack = this._editStacks.get(strResource)!;
if (editStack.past.length === 0) {
return;
}
if (foundReplacementForThisResource) {
this.undo(resource);
const element = editStack.past[editStack.past.length - 1];
if (element.type === UndoRedoElementType.Workspace) {
return this._workspaceUndo(resource, element);
} else {
return this._resourceUndo(editStack, element);
}
}
......@@ -195,71 +296,57 @@ export class UndoRedoService implements IUndoRedoService {
return false;
}
public redo(resource: URI): void {
const strResource = uriGetComparisonKey(resource);
if (!this._editStacks.has(strResource)) {
return;
private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise<void> | void {
// this must be the last future element in all the impacted resources!
let affectedEditStacks: ResourceEditStack[] = [];
for (const strResource of element.strResources) {
affectedEditStacks.push(this._editStacks.get(strResource)!);
}
const editStack = this._editStacks.get(strResource)!;
if (editStack.future.length === 0) {
return;
let cannotRedoDueToResources: URI[] = [];
for (const editStack of affectedEditStacks) {
if (editStack.future.length === 0 || editStack.future[editStack.future.length - 1] !== element) {
cannotRedoDueToResources.push(editStack.resource);
}
}
const element = editStack.future[editStack.future.length - 1];
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null;
try {
element.actual.redo({
replaceCurrentElement: (others: IUndoRedoElement[]): void => {
replaceCurrentElement = others;
}
});
} catch (e) {
onUnexpectedError(e);
editStack.future.pop();
editStack.past.push(element);
if (cannotRedoDueToResources.length > 0) {
this._splitFutureWorkspaceElement(element, null);
const paths = cannotRedoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path);
const message = nls.localize('redoFurtherChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, paths.join(', '));
this._notificationService.info(message);
return;
}
if (replaceCurrentElement === null) {
// regular case
for (const editStack of affectedEditStacks) {
editStack.future.pop();
editStack.past.push(element);
return;
}
return this._safeInvoke(element, () => element.actual.redo());
}
let foundReplacementForThisResource = false;
const replaceCurrentElementMap = new Map<string, StackElement>();
for (const _replace of replaceCurrentElement) {
const replace = new StackElement(_replace);
for (const strReplaceResource of replace.strResources) {
replaceCurrentElementMap.set(strReplaceResource, replace);
if (strReplaceResource === strResource) {
foundReplacementForThisResource = true;
}
}
private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
editStack.future.pop();
editStack.past.push(element);
return this._safeInvoke(element, () => element.actual.redo());
}
public redo(resource: URI): Promise<void> | void {
const strResource = uriGetComparisonKey(resource);
if (!this._editStacks.has(strResource)) {
return;
}
for (let i = 0, len = element.strResources.length; i < len; i++) {
const strResource = element.strResources[i];
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (let j = editStack.future.length - 1; j >= 0; j--) {
if (editStack.future[j] === element) {
if (replaceCurrentElementMap.has(strResource)) {
editStack.future[j] = replaceCurrentElementMap.get(strResource)!;
} else {
editStack.future.splice(j, 1);
}
break;
}
}
}
const editStack = this._editStacks.get(strResource)!;
if (editStack.future.length === 0) {
return;
}
if (foundReplacementForThisResource) {
this.redo(resource);
const element = editStack.future[editStack.future.length - 1];
if (element.type === UndoRedoElementType.Workspace) {
return this._workspaceRedo(resource, element);
} else {
return this._resourceRedo(editStack, element);
}
}
}
......
......@@ -29,9 +29,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
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 { IDialogService } from 'vs/platform/dialogs/common/dialogs';
const USE_MULTI_MODEL_EDIT_STACK_ELEMENT = false;
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
......@@ -138,8 +135,7 @@ class BulkEditModel implements IDisposable {
edits: WorkspaceTextEdit[],
@IEditorWorkerService private readonly _editorWorker: IEditorWorkerService,
@ITextModelService private readonly _textModelResolverService: ITextModelService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
@IDialogService private readonly _dialogService: IDialogService
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService
) {
edits.forEach(this._addEdit, this);
}
......@@ -224,7 +220,7 @@ class BulkEditModel implements IDisposable {
apply(): void {
const tasks = this._tasks!;
if (tasks.length === 1 || !USE_MULTI_MODEL_EDIT_STACK_ELEMENT) {
if (tasks.length === 1) {
// This edit touches a single model => keep things simple
for (const task of tasks) {
task.model.pushStackElement();
......@@ -237,8 +233,7 @@ class BulkEditModel implements IDisposable {
const multiModelEditStackElement = new MultiModelEditStackElement(
localize('workspaceEdit', "Workspace Edit"),
tasks.map(t => new EditStackElement(t.model, t.getBeforeCursorState())),
this._dialogService
tasks.map(t => new EditStackElement(t.model, t.getBeforeCursorState()))
);
this._undoRedoService.pushElement(multiModelEditStackElement);
......
......@@ -23,6 +23,8 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { NullLogService } from 'vs/platform/log/common/log';
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';
suite('MainThreadDocumentsAndEditors', () => {
......@@ -45,7 +47,10 @@ suite('MainThreadDocumentsAndEditors', () => {
deltas.length = 0;
const configService = new TestConfigurationService();
configService.setUserConfiguration('editor', { 'detectIndentation': false });
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService());
const dialogService = new TestDialogService();
const notificationService = new TestNotificationService();
const undoRedoService = new UndoRedoService(dialogService, notificationService);
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService);
codeEditorService = new TestCodeEditorService();
textFileService = new class extends mock<ITextFileService>() {
isDirty() { return false; }
......
......@@ -42,6 +42,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { ILabelService } from 'vs/platform/label/common/label';
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { INotificationService } from 'vs/platform/notification/common/notification';
suite('MainThreadEditors', () => {
......@@ -64,7 +69,10 @@ suite('MainThreadEditors', () => {
const configService = new TestConfigurationService();
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService());
const dialogService = new TestDialogService();
const notificationService = new TestNotificationService();
const undoRedoService = new UndoRedoService(dialogService, notificationService);
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService);
const services = new ServiceCollection();
......@@ -74,6 +82,9 @@ suite('MainThreadEditors', () => {
services.set(IWorkspaceContextService, new TestContextService());
services.set(IWorkbenchEnvironmentService, TestEnvironmentService);
services.set(IConfigurationService, configService);
services.set(IDialogService, dialogService);
services.set(INotificationService, notificationService);
services.set(IUndoRedoService, undoRedoService);
services.set(IModelService, modelService);
services.set(ICodeEditorService, new TestCodeEditorService());
services.set(IFileService, new TestFileService());
......
......@@ -20,6 +20,10 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe
import { TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { INotificationService } from 'vs/platform/notification/common/notification';
class MyEditorModel extends EditorModel { }
class MyTextEditorModel extends BaseTextEditorModel {
......@@ -72,9 +76,14 @@ suite('Workbench editor model', () => {
});
function stubModelService(instantiationService: TestInstantiationService): IModelService {
const dialogService = new TestDialogService();
const notificationService = new TestNotificationService();
const undoRedoService = new UndoRedoService(dialogService, notificationService);
instantiationService.stub(IConfigurationService, new TestConfigurationService());
instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService)));
instantiationService.stub(IUndoRedoService, new UndoRedoService());
instantiationService.stub(IDialogService, dialogService);
instantiationService.stub(INotificationService, notificationService);
instantiationService.stub(IUndoRedoService, undoRedoService);
return instantiationService.createInstance(ModelServiceImpl);
}
});
......@@ -13,7 +13,6 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil
import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult } from 'vs/workbench/common/editor';
import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor';
import { Event, Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchLayoutService, Parts, Position as PartPosition } from 'vs/workbench/services/layout/browser/layoutService';
......@@ -45,7 +44,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, IModelDecorationOptions, ITextModel, ITextSnapshot } from 'vs/editor/common/model';
import { Range } from 'vs/editor/common/core/range';
import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IShowResult, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
import { IDialogService, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions';
......@@ -99,6 +98,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { CancellationToken } from 'vs/base/common/cancellation';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService;
export import TestContextService = CommonWorkbenchTestServices.TestContextService;
......@@ -322,14 +322,7 @@ export class TestHistoryService implements IHistoryService {
openLastEditLocation(): void { }
}
export class TestDialogService implements IDialogService {
_serviceBrand: undefined;
confirm(_confirmation: IConfirmation): Promise<IConfirmationResult> { return Promise.resolve({ confirmed: false }); }
show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise<IShowResult> { return Promise.resolve({ choice: 0 }); }
about(): Promise<void> { return Promise.resolve(); }
}
export class TestFileDialogService implements IFileDialogService {
......
......@@ -34,6 +34,11 @@ import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/pla
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { NullLogService } from 'vs/platform/log/common/log';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { INotificationService } from 'vs/platform/notification/common/notification';
namespace Timer {
export interface ITimerEvent {
......@@ -73,11 +78,17 @@ suite.skip('QuickOpen performance (integration)', () => {
const telemetryService = new TestTelemetryService();
const configurationService = new TestConfigurationService();
const textResourcePropertiesService = new TestTextResourcePropertiesService(configurationService);
const dialogService = new TestDialogService();
const notificationService = new TestNotificationService();
const undoRedoService = new UndoRedoService(dialogService, notificationService);
const instantiationService = new InstantiationService(new ServiceCollection(
[ITelemetryService, telemetryService],
[IConfigurationService, configurationService],
[ITextResourcePropertiesService, textResourcePropertiesService],
[IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), new NullLogService(), new UndoRedoService())],
[IDialogService, dialogService],
[INotificationService, notificationService],
[IUndoRedoService, undoRedoService],
[IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), new NullLogService(), undoRedoService)],
[IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))],
[IEditorService, new TestEditorService()],
[IEditorGroupsService, new TestEditorGroupsService()],
......
......@@ -37,6 +37,11 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe
import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { INotificationService } from 'vs/platform/notification/common/notification';
// declare var __dirname: string;
......@@ -63,11 +68,17 @@ suite.skip('TextSearch performance (integration)', () => {
const configurationService = new TestConfigurationService();
const textResourcePropertiesService = new TestTextResourcePropertiesService(configurationService);
const logService = new NullLogService();
const dialogService = new TestDialogService();
const notificationService = new TestNotificationService();
const undoRedoService = new UndoRedoService(dialogService, notificationService);
const instantiationService = new InstantiationService(new ServiceCollection(
[ITelemetryService, telemetryService],
[IConfigurationService, configurationService],
[ITextResourcePropertiesService, textResourcePropertiesService],
[IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, new UndoRedoService())],
[IDialogService, dialogService],
[INotificationService, notificationService],
[IUndoRedoService, undoRedoService],
[IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService)],
[IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))],
[IEditorService, new TestEditorService()],
[IEditorGroupsService, new TestEditorGroupsService()],
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册