Skip to content

Commit 9bc92b4

Browse files
committed
Scaffold IUndoRedoService
1 parent 69e0508 commit 9bc92b4

2 files changed

Lines changed: 314 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
7+
import { URI } from 'vs/base/common/uri';
8+
9+
export const IUndoRedoService = createDecorator<IUndoRedoService>('undoRedoService');
10+
11+
export interface IUndoRedoContext {
12+
replaceCurrentElement(others: IUndoRedoElement[]): void;
13+
}
14+
15+
export interface IUndoRedoElement {
16+
/**
17+
* None, one or multiple resources that this undo/redo element impacts.
18+
*/
19+
readonly resources: URI[];
20+
21+
/**
22+
* The label of the undo/redo element.
23+
*/
24+
readonly label: string;
25+
26+
/**
27+
* Undo.
28+
* Will always be called before `redo`.
29+
* Can be called multiple times.
30+
* e.g. `undo` -> `redo` -> `undo` -> `redo`
31+
*/
32+
undo(ctx: IUndoRedoContext): void;
33+
34+
/**
35+
* Redo.
36+
* Will always be called after `undo`.
37+
* Can be called multiple times.
38+
* e.g. `undo` -> `redo` -> `undo` -> `redo`
39+
*/
40+
redo(ctx: IUndoRedoContext): void;
41+
42+
/**
43+
* Invalidate the edits concerning `resource`.
44+
* i.e. the undo/redo stack for that particular resource has been destroyed.
45+
*/
46+
invalidate(resource: URI): boolean;
47+
}
48+
49+
export interface IUndoRedoService {
50+
_serviceBrand: undefined;
51+
52+
/**
53+
* Add a new element to the `undo` stack.
54+
* This will destroy the `redo` stack.
55+
*/
56+
pushElement(element: IUndoRedoElement): void;
57+
58+
/**
59+
* Get the last pushed element. If the last pushed element has been undone, returns null.
60+
*/
61+
getLastElement(resource: URI): IUndoRedoElement | null;
62+
63+
/**
64+
* Remove elements that target `resource`.
65+
*/
66+
removeElements(resource: URI): void;
67+
68+
canUndo(resource: URI): boolean;
69+
undo(resource: URI): void;
70+
71+
redo(resource: URI): void;
72+
canRedo(resource: URI): boolean;
73+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IUndoRedoService, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
7+
import { URI } from 'vs/base/common/uri';
8+
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
9+
import { onUnexpectedError } from 'vs/base/common/errors';
10+
11+
class StackElement {
12+
public readonly actual: IUndoRedoElement;
13+
public readonly label: string;
14+
public readonly resources: URI[];
15+
public readonly strResources: string[];
16+
17+
constructor(actual: IUndoRedoElement) {
18+
this.actual = actual;
19+
this.label = actual.label;
20+
this.resources = actual.resources;
21+
this.strResources = this.resources.map(resource => uriGetComparisonKey(resource));
22+
}
23+
24+
public invalidate(resource: URI): void {
25+
if (this.resources.length > 1) {
26+
this.actual.invalidate(resource);
27+
}
28+
}
29+
}
30+
31+
class ResourceEditStack {
32+
public resource: URI;
33+
public past: StackElement[];
34+
public future: StackElement[];
35+
36+
constructor(resource: URI) {
37+
this.resource = resource;
38+
this.past = [];
39+
this.future = [];
40+
}
41+
}
42+
43+
export class UndoRedoService implements IUndoRedoService {
44+
_serviceBrand: undefined;
45+
46+
private readonly _editStacks: Map<string, ResourceEditStack>;
47+
48+
constructor() {
49+
this._editStacks = new Map<string, ResourceEditStack>();
50+
}
51+
52+
public pushElement(_element: IUndoRedoElement): void {
53+
const element = new StackElement(_element);
54+
for (let i = 0, len = element.resources.length; i < len; i++) {
55+
const resource = element.resources[i];
56+
const strResource = element.strResources[i];
57+
58+
let editStack: ResourceEditStack;
59+
if (this._editStacks.has(strResource)) {
60+
editStack = this._editStacks.get(strResource)!;
61+
} else {
62+
editStack = new ResourceEditStack(resource);
63+
this._editStacks.set(strResource, editStack);
64+
}
65+
66+
// remove the future
67+
for (const futureElement of editStack.future) {
68+
futureElement.invalidate(resource);
69+
}
70+
editStack.future = [];
71+
editStack.past.push(element);
72+
}
73+
}
74+
75+
public getLastElement(resource: URI): IUndoRedoElement | null {
76+
const strResource = uriGetComparisonKey(resource);
77+
if (this._editStacks.has(strResource)) {
78+
const editStack = this._editStacks.get(strResource)!;
79+
if (editStack.future.length > 0) {
80+
return null;
81+
}
82+
if (editStack.past.length === 0) {
83+
return null;
84+
}
85+
return editStack.past[editStack.past.length - 1].actual;
86+
}
87+
return null;
88+
}
89+
90+
public removeElements(resource: URI): void {
91+
const strResource = uriGetComparisonKey(resource);
92+
if (this._editStacks.has(strResource)) {
93+
const editStack = this._editStacks.get(strResource)!;
94+
for (const pastElement of editStack.past) {
95+
pastElement.invalidate(resource);
96+
}
97+
for (const futureElement of editStack.future) {
98+
futureElement.invalidate(resource);
99+
}
100+
this._editStacks.delete(strResource);
101+
}
102+
}
103+
104+
public canUndo(resource: URI): boolean {
105+
const strResource = uriGetComparisonKey(resource);
106+
if (this._editStacks.has(strResource)) {
107+
const editStack = this._editStacks.get(strResource)!;
108+
return (editStack.past.length > 0);
109+
}
110+
return false;
111+
}
112+
113+
public undo(resource: URI): void {
114+
const strResource = uriGetComparisonKey(resource);
115+
if (!this._editStacks.has(strResource)) {
116+
return;
117+
}
118+
119+
const editStack = this._editStacks.get(strResource)!;
120+
if (editStack.past.length === 0) {
121+
return;
122+
}
123+
124+
const element = editStack.past[editStack.past.length - 1];
125+
126+
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null;
127+
try {
128+
element.actual.undo({
129+
replaceCurrentElement: (others: IUndoRedoElement[]): void => {
130+
replaceCurrentElement = others;
131+
}
132+
});
133+
} catch (e) {
134+
onUnexpectedError(e);
135+
editStack.past.pop();
136+
editStack.future.push(element);
137+
return;
138+
}
139+
140+
if (replaceCurrentElement === null) {
141+
// regular case
142+
editStack.past.pop();
143+
editStack.future.push(element);
144+
return;
145+
}
146+
147+
const replaceCurrentElementMap = new Map<string, StackElement>();
148+
for (const _replace of replaceCurrentElement) {
149+
const replace = new StackElement(_replace);
150+
for (const strResource of replace.strResources) {
151+
replaceCurrentElementMap.set(strResource, replace);
152+
}
153+
}
154+
155+
for (let i = 0, len = element.strResources.length; i < len; i++) {
156+
const strResource = element.strResources[i];
157+
if (this._editStacks.has(strResource)) {
158+
const editStack = this._editStacks.get(strResource)!;
159+
for (let j = editStack.past.length - 1; j >= 0; j--) {
160+
if (editStack.past[j] === element) {
161+
if (replaceCurrentElementMap.has(strResource)) {
162+
editStack.past[j] = replaceCurrentElementMap.get(strResource)!;
163+
} else {
164+
editStack.past.splice(j, 1);
165+
}
166+
break;
167+
}
168+
}
169+
}
170+
}
171+
}
172+
173+
public canRedo(resource: URI): boolean {
174+
const strResource = uriGetComparisonKey(resource);
175+
if (this._editStacks.has(strResource)) {
176+
const editStack = this._editStacks.get(strResource)!;
177+
return (editStack.future.length > 0);
178+
}
179+
return false;
180+
}
181+
182+
redo(resource: URI): void {
183+
const strResource = uriGetComparisonKey(resource);
184+
if (!this._editStacks.has(strResource)) {
185+
return;
186+
}
187+
188+
const editStack = this._editStacks.get(strResource)!;
189+
if (editStack.future.length === 0) {
190+
return;
191+
}
192+
193+
const element = editStack.future[editStack.future.length - 1];
194+
195+
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null;
196+
try {
197+
element.actual.redo({
198+
replaceCurrentElement: (others: IUndoRedoElement[]): void => {
199+
replaceCurrentElement = others;
200+
}
201+
});
202+
} catch (e) {
203+
onUnexpectedError(e);
204+
editStack.future.pop();
205+
editStack.past.push(element);
206+
return;
207+
}
208+
209+
if (replaceCurrentElement === null) {
210+
// regular case
211+
editStack.future.pop();
212+
editStack.past.push(element);
213+
return;
214+
}
215+
216+
const replaceCurrentElementMap = new Map<string, StackElement>();
217+
for (const _replace of replaceCurrentElement) {
218+
const replace = new StackElement(_replace);
219+
for (const strResource of replace.strResources) {
220+
replaceCurrentElementMap.set(strResource, replace);
221+
}
222+
}
223+
224+
for (let i = 0, len = element.strResources.length; i < len; i++) {
225+
const strResource = element.strResources[i];
226+
if (this._editStacks.has(strResource)) {
227+
const editStack = this._editStacks.get(strResource)!;
228+
for (let j = editStack.future.length - 1; j >= 0; j--) {
229+
if (editStack.future[j] === element) {
230+
if (replaceCurrentElementMap.has(strResource)) {
231+
editStack.future[j] = replaceCurrentElementMap.get(strResource)!;
232+
} else {
233+
editStack.future.splice(j, 1);
234+
}
235+
break;
236+
}
237+
}
238+
}
239+
}
240+
}
241+
}

0 commit comments

Comments
 (0)