Scaffold IUndoRedoService
This commit is contained in:
parent
69e0508b63
commit
9bc92b48d9
2 changed files with 314 additions and 0 deletions
73
src/vs/platform/undoRedo/common/undoRedo.ts
Normal file
73
src/vs/platform/undoRedo/common/undoRedo.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export const IUndoRedoService = createDecorator<IUndoRedoService>('undoRedoService');
|
||||
|
||||
export interface IUndoRedoContext {
|
||||
replaceCurrentElement(others: IUndoRedoElement[]): void;
|
||||
}
|
||||
|
||||
export interface IUndoRedoElement {
|
||||
/**
|
||||
* None, one or multiple resources that this undo/redo element impacts.
|
||||
*/
|
||||
readonly resources: URI[];
|
||||
|
||||
/**
|
||||
* The label of the undo/redo element.
|
||||
*/
|
||||
readonly label: string;
|
||||
|
||||
/**
|
||||
* 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): boolean;
|
||||
}
|
||||
|
||||
export interface IUndoRedoService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Add a new element to the `undo` stack.
|
||||
* This will destroy the `redo` stack.
|
||||
*/
|
||||
pushElement(element: IUndoRedoElement): void;
|
||||
|
||||
/**
|
||||
* Get the last pushed element. If the last pushed element has been undone, returns null.
|
||||
*/
|
||||
getLastElement(resource: URI): IUndoRedoElement | null;
|
||||
|
||||
/**
|
||||
* Remove elements that target `resource`.
|
||||
*/
|
||||
removeElements(resource: URI): void;
|
||||
|
||||
canUndo(resource: URI): boolean;
|
||||
undo(resource: URI): void;
|
||||
|
||||
redo(resource: URI): void;
|
||||
canRedo(resource: URI): boolean;
|
||||
}
|
241
src/vs/platform/undoRedo/common/undoRedoService.ts
Normal file
241
src/vs/platform/undoRedo/common/undoRedoService.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { URI } from 'vs/base/common/uri';
|
||||
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
class StackElement {
|
||||
public readonly actual: IUndoRedoElement;
|
||||
public readonly label: string;
|
||||
public readonly resources: URI[];
|
||||
public readonly strResources: string[];
|
||||
|
||||
constructor(actual: IUndoRedoElement) {
|
||||
this.actual = actual;
|
||||
this.label = actual.label;
|
||||
this.resources = actual.resources;
|
||||
this.strResources = this.resources.map(resource => uriGetComparisonKey(resource));
|
||||
}
|
||||
|
||||
public invalidate(resource: URI): void {
|
||||
if (this.resources.length > 1) {
|
||||
this.actual.invalidate(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceEditStack {
|
||||
public resource: URI;
|
||||
public past: StackElement[];
|
||||
public future: StackElement[];
|
||||
|
||||
constructor(resource: URI) {
|
||||
this.resource = resource;
|
||||
this.past = [];
|
||||
this.future = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class UndoRedoService implements IUndoRedoService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _editStacks: Map<string, ResourceEditStack>;
|
||||
|
||||
constructor() {
|
||||
this._editStacks = new Map<string, ResourceEditStack>();
|
||||
}
|
||||
|
||||
public pushElement(_element: IUndoRedoElement): void {
|
||||
const element = new StackElement(_element);
|
||||
for (let i = 0, len = element.resources.length; i < len; i++) {
|
||||
const resource = element.resources[i];
|
||||
const strResource = element.strResources[i];
|
||||
|
||||
let editStack: ResourceEditStack;
|
||||
if (this._editStacks.has(strResource)) {
|
||||
editStack = this._editStacks.get(strResource)!;
|
||||
} else {
|
||||
editStack = new ResourceEditStack(resource);
|
||||
this._editStacks.set(strResource, editStack);
|
||||
}
|
||||
|
||||
// remove the future
|
||||
for (const futureElement of editStack.future) {
|
||||
futureElement.invalidate(resource);
|
||||
}
|
||||
editStack.future = [];
|
||||
editStack.past.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
public getLastElement(resource: URI): IUndoRedoElement | null {
|
||||
const strResource = uriGetComparisonKey(resource);
|
||||
if (this._editStacks.has(strResource)) {
|
||||
const editStack = this._editStacks.get(strResource)!;
|
||||
if (editStack.future.length > 0) {
|
||||
return null;
|
||||
}
|
||||
if (editStack.past.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return editStack.past[editStack.past.length - 1].actual;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
for (const futureElement of editStack.future) {
|
||||
futureElement.invalidate(resource);
|
||||
}
|
||||
this._editStacks.delete(strResource);
|
||||
}
|
||||
}
|
||||
|
||||
public canUndo(resource: URI): boolean {
|
||||
const strResource = uriGetComparisonKey(resource);
|
||||
if (this._editStacks.has(strResource)) {
|
||||
const editStack = this._editStacks.get(strResource)!;
|
||||
return (editStack.past.length > 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public undo(resource: URI): void {
|
||||
const strResource = uriGetComparisonKey(resource);
|
||||
if (!this._editStacks.has(strResource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editStack = this._editStacks.get(strResource)!;
|
||||
if (editStack.past.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = editStack.past[editStack.past.length - 1];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (replaceCurrentElement === null) {
|
||||
// regular case
|
||||
editStack.past.pop();
|
||||
editStack.future.push(element);
|
||||
return;
|
||||
}
|
||||
|
||||
const replaceCurrentElementMap = new Map<string, StackElement>();
|
||||
for (const _replace of replaceCurrentElement) {
|
||||
const replace = new StackElement(_replace);
|
||||
for (const strResource of replace.strResources) {
|
||||
replaceCurrentElementMap.set(strResource, replace);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public canRedo(resource: URI): boolean {
|
||||
const strResource = uriGetComparisonKey(resource);
|
||||
if (this._editStacks.has(strResource)) {
|
||||
const editStack = this._editStacks.get(strResource)!;
|
||||
return (editStack.future.length > 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
redo(resource: URI): void {
|
||||
const strResource = uriGetComparisonKey(resource);
|
||||
if (!this._editStacks.has(strResource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editStack = this._editStacks.get(strResource)!;
|
||||
if (editStack.future.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
if (replaceCurrentElement === null) {
|
||||
// regular case
|
||||
editStack.future.pop();
|
||||
editStack.past.push(element);
|
||||
return;
|
||||
}
|
||||
|
||||
const replaceCurrentElementMap = new Map<string, StackElement>();
|
||||
for (const _replace of replaceCurrentElement) {
|
||||
const replace = new StackElement(_replace);
|
||||
for (const strResource of replace.strResources) {
|
||||
replaceCurrentElementMap.set(strResource, replace);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue