Skip to content

Commit 4ae83d0

Browse files
committed
First cut at supporting undo across files
1 parent 985eea1 commit 4ae83d0

3 files changed

Lines changed: 172 additions & 41 deletions

File tree

src/vs/editor/common/model/editStack.ts

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import { TextModel } from 'vs/editor/common/model/textModel';
1111
import { IUndoRedoService, IUndoRedoElement, IUndoRedoContext } from 'vs/platform/undoRedo/common/undoRedo';
1212
import { URI } from 'vs/base/common/uri';
1313
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
14+
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
15+
import { Severity } from 'vs/platform/notification/common/notification';
1416

1517
export class EditStackElement implements IUndoRedoElement {
1618

1719
public readonly label: string;
1820
private _isOpen: boolean;
21+
private _isValid: boolean;
1922
public readonly model: ITextModel;
2023
private readonly _beforeVersionId: number;
2124
private readonly _beforeEOL: EndOfLineSequence;
@@ -32,6 +35,7 @@ export class EditStackElement implements IUndoRedoElement {
3235
constructor(model: ITextModel, beforeCursorState: Selection[] | null) {
3336
this.label = nls.localize('edit', "Typing");
3437
this._isOpen = true;
38+
this._isValid = true;
3539
this.model = model;
3640
this._beforeVersionId = this.model.getAlternativeVersionId();
3741
this._beforeEOL = getModelEOL(this.model);
@@ -42,6 +46,10 @@ export class EditStackElement implements IUndoRedoElement {
4246
this._edits = [];
4347
}
4448

49+
public isValid(): boolean {
50+
return this._isValid;
51+
}
52+
4553
public canAppend(model: ITextModel): boolean {
4654
return (this._isOpen && this.model === model);
4755
}
@@ -59,19 +67,33 @@ export class EditStackElement implements IUndoRedoElement {
5967
this._isOpen = false;
6068
}
6169

62-
undo(ctx: IUndoRedoContext): void {
70+
public canUndo(): boolean {
71+
if (!this._isValid) {
72+
return false;
73+
}
74+
return (this._afterVersionId === this.model.getAlternativeVersionId());
75+
}
76+
77+
public undo(ctx: IUndoRedoContext): void {
6378
this._isOpen = false;
6479
this._edits.reverse();
6580
this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState);
6681
}
6782

68-
redo(ctx: IUndoRedoContext): void {
83+
public canRedo(): boolean {
84+
if (!this._isValid) {
85+
return false;
86+
}
87+
return (this._beforeVersionId === this.model.getAlternativeVersionId());
88+
}
89+
90+
public redo(ctx: IUndoRedoContext): void {
6991
this._edits.reverse();
7092
this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState);
7193
}
7294

73-
invalidate(resource: URI): void {
74-
// nothing to do
95+
public invalidate(resource: URI): void {
96+
this._isValid = false;
7597
}
7698
}
7799

@@ -82,6 +104,7 @@ export class MultiModelEditStackElement implements IUndoRedoElement {
82104

83105
private readonly _editStackElementsArr: EditStackElement[];
84106
private readonly _editStackElementsMap: Map<string, EditStackElement>;
107+
private _isValid: boolean;
85108

86109
public get resources(): readonly URI[] {
87110
return this._editStackElementsArr.map(editStackElement => editStackElement.model.uri);
@@ -90,52 +113,101 @@ export class MultiModelEditStackElement implements IUndoRedoElement {
90113
constructor(
91114
label: string,
92115
editStackElements: EditStackElement[],
93-
@IDialogService dialogService: IDialogService
116+
@IDialogService private readonly _dialogService: IDialogService
94117
) {
95118
this.label = label;
96119
this._isOpen = true;
97120
this._editStackElementsArr = editStackElements.slice(0);
98121
this._editStackElementsMap = new Map<string, EditStackElement>();
99122
for (const editStackElement of this._editStackElementsArr) {
100-
this._editStackElementsMap.set(editStackElement.model.id, editStackElement);
123+
const key = uriGetComparisonKey(editStackElement.model.uri);
124+
this._editStackElementsMap.set(key, editStackElement);
101125
}
126+
this._isValid = true;
102127
}
103128

104129
public canAppend(model: ITextModel): boolean {
105130
if (!this._isOpen) {
106131
return false;
107132
}
108-
if (this._editStackElementsMap.has(model.id)) {
109-
const editStackElement = this._editStackElementsMap.get(model.id)!;
133+
const key = uriGetComparisonKey(model.uri);
134+
if (this._editStackElementsMap.has(key)) {
135+
const editStackElement = this._editStackElementsMap.get(key)!;
110136
return editStackElement.canAppend(model);
111137
}
112138
return false;
113139
}
114140

115141
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
116-
const editStackElement = this._editStackElementsMap.get(model.id)!;
142+
const key = uriGetComparisonKey(model.uri);
143+
const editStackElement = this._editStackElementsMap.get(key)!;
117144
editStackElement.append(model, operations, afterEOL, afterVersionId, afterCursorState);
118145
}
119146

120147
public close(): void {
121148
this._isOpen = false;
122149
}
123150

151+
private _canUndo(): boolean {
152+
if (!this._isValid) {
153+
return false;
154+
}
155+
for (const editStackElement of this._editStackElementsArr) {
156+
if (!editStackElement.canUndo()) {
157+
return false;
158+
}
159+
}
160+
return true;
161+
}
162+
124163
undo(ctx: IUndoRedoContext): void {
125164
this._isOpen = false;
165+
166+
if (this._canUndo()) {
167+
for (const editStackElement of this._editStackElementsArr) {
168+
editStackElement.undo(ctx);
169+
}
170+
} else {
171+
// cannot apply!
172+
const validStackElements = this._editStackElementsArr.filter(stackElement => stackElement.isValid());
173+
ctx.replaceCurrentElement(validStackElements);
174+
this._dialogService.show(Severity.Info, nls.localize('workspace', "Could not apply the edit in all the impacted files."), []);
175+
}
176+
}
177+
178+
private _canRedo(): boolean {
179+
if (!this._isValid) {
180+
return false;
181+
}
126182
for (const editStackElement of this._editStackElementsArr) {
127-
editStackElement.undo(ctx);
183+
if (!editStackElement.canRedo()) {
184+
return false;
185+
}
128186
}
187+
return true;
129188
}
130189

131190
redo(ctx: IUndoRedoContext): void {
132-
for (const editStackElement of this._editStackElementsArr) {
133-
editStackElement.redo(ctx);
191+
if (this._canRedo()) {
192+
for (const editStackElement of this._editStackElementsArr) {
193+
editStackElement.redo(ctx);
194+
}
195+
} else {
196+
// cannot apply!
197+
const validStackElements = this._editStackElementsArr.filter(stackElement => stackElement.isValid());
198+
ctx.replaceCurrentElement(validStackElements);
199+
this._dialogService.show(Severity.Info, nls.localize('workspace', "Could not apply the edit in all the impacted files."), []);
134200
}
135201
}
136202

137203
invalidate(resource: URI): void {
138-
console.log(`MULTI INVALIDATE: ${resource}`);
204+
const key = uriGetComparisonKey(resource);
205+
if (!this._editStackElementsMap.has(key)) {
206+
return;
207+
}
208+
this._isValid = false;
209+
const stackElement = this._editStackElementsMap.get(key)!;
210+
stackElement.invalidate(resource);
139211
}
140212
}
141213

@@ -148,6 +220,13 @@ function getModelEOL(model: ITextModel): EndOfLineSequence {
148220
}
149221
}
150222

223+
function isKnownStackElement(element: IUndoRedoElement | null): element is EditStackElement | MultiModelEditStackElement {
224+
if (!element) {
225+
return false;
226+
}
227+
return ((element instanceof EditStackElement) || (element instanceof MultiModelEditStackElement));
228+
}
229+
151230
export class EditStack {
152231

153232
private readonly _model: TextModel;
@@ -160,7 +239,7 @@ export class EditStack {
160239

161240
public pushStackElement(): void {
162241
const lastElement = this._undoRedoService.getLastElement(this._model.uri);
163-
if (lastElement && lastElement instanceof EditStackElement) {
242+
if (isKnownStackElement(lastElement)) {
164243
lastElement.close();
165244
}
166245
}
@@ -169,9 +248,9 @@ export class EditStack {
169248
this._undoRedoService.removeElements(this._model.uri);
170249
}
171250

172-
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement {
251+
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement | MultiModelEditStackElement {
173252
const lastElement = this._undoRedoService.getLastElement(this._model.uri);
174-
if (lastElement && lastElement instanceof EditStackElement && lastElement.canAppend(this._model)) {
253+
if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) {
175254
return lastElement;
176255
}
177256
const newElement = new EditStackElement(this._model, beforeCursorState);

src/vs/platform/undoRedo/common/undoRedoService.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,27 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
1212
class StackElement {
1313
public readonly actual: IUndoRedoElement;
1414
public readonly label: string;
15-
public readonly resources: readonly URI[];
16-
public readonly strResources: string[];
15+
16+
public resources: URI[];
17+
public strResources: string[];
1718

1819
constructor(actual: IUndoRedoElement) {
1920
this.actual = actual;
2021
this.label = actual.label;
21-
this.resources = actual.resources;
22+
this.resources = actual.resources.slice(0);
2223
this.strResources = this.resources.map(resource => uriGetComparisonKey(resource));
2324
}
2425

2526
public invalidate(resource: URI): void {
26-
if (this.resources.length > 1) {
27-
this.actual.invalidate(resource);
27+
const strResource = uriGetComparisonKey(resource);
28+
for (let i = 0, len = this.strResources.length; i < len; i++) {
29+
if (this.strResources[i] === strResource) {
30+
this.resources.splice(i, 1);
31+
this.strResources.splice(i, 1);
32+
break;
33+
}
2834
}
35+
this.actual.invalidate(resource);
2936
}
3037
}
3138

@@ -146,10 +153,14 @@ export class UndoRedoService implements IUndoRedoService {
146153
}
147154

148155
const replaceCurrentElementMap = new Map<string, StackElement>();
156+
let foundReplacementForThisResource = false;
149157
for (const _replace of replaceCurrentElement) {
150158
const replace = new StackElement(_replace);
151-
for (const strResource of replace.strResources) {
152-
replaceCurrentElementMap.set(strResource, replace);
159+
for (const strReplaceResource of replace.strResources) {
160+
replaceCurrentElementMap.set(strReplaceResource, replace);
161+
if (strReplaceResource === strResource) {
162+
foundReplacementForThisResource = true;
163+
}
153164
}
154165
}
155166

@@ -169,6 +180,10 @@ export class UndoRedoService implements IUndoRedoService {
169180
}
170181
}
171182
}
183+
184+
if (foundReplacementForThisResource) {
185+
this.undo(resource);
186+
}
172187
}
173188

174189
public canRedo(resource: URI): boolean {
@@ -214,11 +229,15 @@ export class UndoRedoService implements IUndoRedoService {
214229
return;
215230
}
216231

232+
let foundReplacementForThisResource = false;
217233
const replaceCurrentElementMap = new Map<string, StackElement>();
218234
for (const _replace of replaceCurrentElement) {
219235
const replace = new StackElement(_replace);
220-
for (const strResource of replace.strResources) {
221-
replaceCurrentElementMap.set(strResource, replace);
236+
for (const strReplaceResource of replace.strResources) {
237+
replaceCurrentElementMap.set(strReplaceResource, replace);
238+
if (strReplaceResource === strResource) {
239+
foundReplacementForThisResource = true;
240+
}
222241
}
223242
}
224243

@@ -238,6 +257,10 @@ export class UndoRedoService implements IUndoRedoService {
238257
}
239258
}
240259
}
260+
261+
if (foundReplacementForThisResource) {
262+
this.redo(resource);
263+
}
241264
}
242265
}
243266

0 commit comments

Comments
 (0)