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

1399 LOC · 769 covered · 630 uncovered · 253 ranges · 1366 concepts · 31 introducers · 666 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- undoRedoService.ts ×83
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 { onUnexpectedError } from '../../../base/common/errors.js';
7 > import { Disposable, IDisposable, isDisposable } from '../../../base/common/lifecycle.js';
8 > import { Schemas } from '../../../base/common/network.js';
9 > import Severity from '../../../base/common/severity.js';
10 > import { URI } from '../../../base/common/uri.js';
11 > import * as nls from '../../../nls.js';
12 > import { IDialogService } from '../../dialogs/common/dialogs.js';
13 > import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
14 > import { INotificationService } from '../../notification/common/notification.js';
15 > import { IPastFutureElements, IResourceUndoRedoElement, IUndoRedoElement, IUndoRedoService, IWorkspaceUndoRedoElement, ResourceEditStackSnapshot, UndoRedoElementType, UndoRedoGroup, UndoRedoSource, UriComparisonKeyComputer } from './undoRedo.js';
16 >
17 > const DEBUG = false;
18 >
19 > function getResourceLabel(resource: URI): string { undoRedoService.ts ×9
20 > return resource.scheme === Schemas.file ? resource.fsPath : resource.path;
21 > }
23 > let stackElementCounter = 0;
24 >
25 > class ResourceStackElement {
26 > public readonly id = (++stackElementCounter);
27 > public readonly type = UndoRedoElementType.Resource;
28 > public readonly actual: IUndoRedoElement;
29 > public readonly label: string;
30 > public readonly confirmBeforeUndo: boolean;
31 >
32 > public readonly resourceLabel: string;
33 > public readonly strResource: string;
34 > public readonly resourceLabels: string[];
35 > public readonly strResources: string[];
36 > public readonly groupId: number;
37 > public readonly groupOrder: number;
38 > public readonly sourceId: number;
39 > public readonly sourceOrder: number;
40 > public isValid: boolean;
41 >
42 > constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) {
43 > this.actual = actual; undoRedoService.ts ×2
44 > this.label = actual.label;
45 > this.confirmBeforeUndo = actual.confirmBeforeUndo || false;
46 > this.resourceLabel = resourceLabel;
47 > this.strResource = strResource;
48 > this.resourceLabels = [this.resourceLabel];
49 > this.strResources = [this.strResource];
50 > this.groupId = groupId;
51 > this.groupOrder = groupOrder;
52 > this.sourceId = sourceId;
53 > this.sourceOrder = sourceOrder;
54 > this.isValid = true;
55 > }
57 > public setValid(isValid: boolean): void {
58 > this.isValid = isValid; modelService.ts ×14
59 > }
61 > public toString(): string {
62 return `[id:${this.id}] [group:${this.groupId}] [${this.isValid ? ' VALID' : 'INVALID'}] ${this.actual.constructor.name} - ${this.actual}`;
63 }
65 >
66 > const enum RemovedResourceReason {
67 > ExternalRemoval = 0,
68 > NoParallelUniverses = 1
69 > }
70 >
71 > class ResourceReasonPair {
72 > constructor(
73 public readonly resourceLabel: string,
74 public readonly reason: RemovedResourceReason
75 ) { }
77 >
78 class RemovedResources {
79 private readonly elements = new Map<string, ResourceReasonPair>();
81 > public createMessage(): string {
82 const externalRemoval: string[] = [];
83 const noParallelUniverses: string[] = [];
84 for (const [, element] of this.elements) {
85 const dest = (
86 element.reason === RemovedResourceReason.ExternalRemoval
87 ? externalRemoval
88 : noParallelUniverses
89 );
90 dest.push(element.resourceLabel);
91 }
92
93 const messages: string[] = [];
94 if (externalRemoval.length > 0) {
95 messages.push(
96 nls.localize(
97 { key: 'externalRemoval', comment: ['{0} is a list of filenames'] },
98 "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', ')
99 )
100 );
101 }
102 if (noParallelUniverses.length > 0) {
103 messages.push(
104 nls.localize(
105 { key: 'noParallelUniverses', comment: ['{0} is a list of filenames'] },
106 "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', ')
107 ));
108 }
109 return messages.join('\n');
110 }
112 > public get size(): number {
113 return this.elements.size;
114 }
116 > public has(strResource: string): boolean {
117 return this.elements.has(strResource);
118 }
120 > public set(strResource: string, value: ResourceReasonPair): void {
121 this.elements.set(strResource, value);
122 }
124 > public delete(strResource: string): boolean {
125 return this.elements.delete(strResource);
126 }
128 >
129 > class WorkspaceStackElement {
130 > public readonly id = (++stackElementCounter);
131 > public readonly type = UndoRedoElementType.Workspace;
132 > public readonly actual: IWorkspaceUndoRedoElement;
133 > public readonly label: string;
134 > public readonly confirmBeforeUndo: boolean;
135 >
136 > public readonly resourceLabels: string[];
137 > public readonly strResources: string[];
138 > public readonly groupId: number;
139 > public readonly groupOrder: number;
140 > public readonly sourceId: number;
141 > public readonly sourceOrder: number;
142 > public removedResources: RemovedResources | null;
143 > public invalidatedResources: RemovedResources | null;
144 >
145 > constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) {
146 > this.actual = actual; undoRedoService.ts ×43
147 > this.label = actual.label;
148 > this.confirmBeforeUndo = actual.confirmBeforeUndo || false;
149 > this.resourceLabels = resourceLabels;
150 > this.strResources = strResources;
151 > this.groupId = groupId;
152 > this.groupOrder = groupOrder;
153 > this.sourceId = sourceId;
154 > this.sourceOrder = sourceOrder;
155 > this.removedResources = null;
156 > this.invalidatedResources = null;
157 > }
159 > public canSplit(): this is WorkspaceStackElement & { actual: { split(): IResourceUndoRedoElement[] } } {
160 > return (typeof this.actual.split === 'function'); undoRedoService.ts ×43
161 > }
163 > public removeResource(resourceLabel: string, strResource: string, reason: RemovedResourceReason): void {
164 if (!this.removedResources) {
165 this.removedResources = new RemovedResources();
166 }
167 if (!this.removedResources.has(strResource)) {
168 this.removedResources.set(strResource, new ResourceReasonPair(resourceLabel, reason));
169 }
170 }
172 > public setValid(resourceLabel: string, strResource: string, isValid: boolean): void {
173 if (isValid) {
174 if (this.invalidatedResources) {
175 this.invalidatedResources.delete(strResource);
176 if (this.invalidatedResources.size === 0) {
177 this.invalidatedResources = null;
178 }
179 }
180 } else {
181 if (!this.invalidatedResources) {
182 this.invalidatedResources = new RemovedResources();
183 }
184 if (!this.invalidatedResources.has(strResource)) {
185 this.invalidatedResources.set(strResource, new ResourceReasonPair(resourceLabel, RemovedResourceReason.ExternalRemoval));
186 }
187 }
188 }
190 > public toString(): string {
191 return `[id:${this.id}] [group:${this.groupId}] [${this.invalidatedResources ? 'INVALID' : ' VALID'}] ${this.actual.constructor.name} - ${this.actual}`;
192 }
194 >
195 > type StackElement = ResourceStackElement | WorkspaceStackElement;
196 >
197 > class ResourceEditStack {
198 > public readonly resourceLabel: string;
199 > private readonly strResource: string;
200 > private _past: StackElement[];
201 > private _future: StackElement[];
202 > public locked: boolean;
203 > public versionId: number;
204 >
205 > constructor(resourceLabel: string, strResource: string) {
206 > this.resourceLabel = resourceLabel;
207 > this.strResource = strResource;
208 > this._past = [];
209 > this._future = [];
210 > this.locked = false;
211 > this.versionId = 1;
212 > }
213 >
214 > public dispose(): void {
215 > for (const element of this._past) { undoRedoService.ts ×4
216 > if (element.type === UndoRedoElementType.Workspace) { undoRedoService.ts ×3
217 element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval);
218 }
220 > for (const element of this._future) { undoRedoService.ts ×4
221 if (element.type === UndoRedoElementType.Workspace) {
222 element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval);
223 }
224 }
225 > this.versionId++; undoRedoService.ts ×4
226 > }
228 > public toString(): string {
229 const result: string[] = [];
230 result.push(`* ${this.strResource}:`);
231 for (let i = 0; i < this._past.length; i++) {
232 result.push(` * [UNDO] ${this._past[i]}`);
233 }
234 for (let i = this._future.length - 1; i >= 0; i--) {
235 result.push(` * [REDO] ${this._future[i]}`);
236 }
237 return result.join('\n');
238 }
240 > public flushAllElements(): void {
241 this._past = [];
242 this._future = [];
243 this.versionId++;
244 }
246 > public setElementsIsValid(isValid: boolean): void {
247 for (const element of this._past) {
248 if (element.type === UndoRedoElementType.Workspace) {
249 element.setValid(this.resourceLabel, this.strResource, isValid);
250 } else {
251 element.setValid(isValid);
252 }
253 }
254 for (const element of this._future) {
255 if (element.type === UndoRedoElementType.Workspace) {
256 element.setValid(this.resourceLabel, this.strResource, isValid);
257 } else {
258 element.setValid(isValid);
259 }
260 }
261 }
263 > private _setElementValidFlag(element: StackElement, isValid: boolean): void {
264 > if (element.type === UndoRedoElementType.Workspace) { modelService.ts ×14
265 element.setValid(this.resourceLabel, this.strResource, isValid);
266 > } else { modelService.ts ×14
267 > element.setValid(isValid);
268 > }
269 > }
271 > public setElementsValidFlag(isValid: boolean, filter: (element: IUndoRedoElement) => boolean): void {
272 > for (const element of this._past) { modelService.ts ×14
273 > if (filter(element.actual)) {
274 > this._setElementValidFlag(element, isValid);
275 > }
276 > }
277 > for (const element of this._future) {
278 > if (filter(element.actual)) { undoRedoService.ts ×2
279 > this._setElementValidFlag(element, isValid);
280 > }
281 > }
284 > public pushElement(element: StackElement): void {
285 > // remove the future undoRedoService.ts ×9
286 > for (const futureElement of this._future) {
287 > if (futureElement.type === UndoRedoElementType.Workspace) { undoRedoService.ts ×9
288 futureElement.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.NoParallelUniverses);
289 }
291 > this._future = []; undoRedoService.ts ×9
292 > this._past.push(element);
293 > this.versionId++;
294 > }
296 > public createSnapshot(resource: URI): ResourceEditStackSnapshot {
297 > const elements: number[] = []; undoRedoService.ts ×3
298 >
299 > for (let i = 0, len = this._past.length; i < len; i++) {
300 > elements.push(this._past[i].id);
301 > }
302 > for (let i = this._future.length - 1; i >= 0; i--) {
303 elements.push(this._future[i].id);
304 }
306 > return new ResourceEditStackSnapshot(resource, elements);
307 > }
309 > public restoreSnapshot(snapshot: ResourceEditStackSnapshot): void {
310 > const snapshotLength = snapshot.elements.length; undoRedoService.ts ×6
311 > let isOK = true;
312 > let snapshotIndex = 0;
313 > let removePastAfter = -1;
314 > for (let i = 0, len = this._past.length; i < len; i++, snapshotIndex++) {
315 > const element = this._past[i];
316 > if (isOK && (snapshotIndex >= snapshotLength || element.id !== snapshot.elements[snapshotIndex])) {
317 > isOK = false;
318 > removePastAfter = i;
319 > }
320 > if (!isOK && element.type === UndoRedoElementType.Workspace) {
321 element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval);
322 }
324 > let removeFutureBefore = -1;
325 > for (let i = this._future.length - 1; i >= 0; i--, snapshotIndex++) {
326 const element = this._future[i];
327 if (isOK && (snapshotIndex >= snapshotLength || element.id !== snapshot.elements[snapshotIndex])) {
328 isOK = false;
329 removeFutureBefore = i;
330 }
331 if (!isOK && element.type === UndoRedoElementType.Workspace) {
332 element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval);
333 }
334 }
335 > if (removePastAfter !== -1) { undoRedoService.ts ×6
336 > this._past = this._past.slice(0, removePastAfter);
337 > }
338 > if (removeFutureBefore !== -1) {
339 this._future = this._future.slice(removeFutureBefore + 1);
340 }
341 > this.versionId++; undoRedoService.ts ×6
342 > }
344 > public getElements(): IPastFutureElements {
345 > const past: IUndoRedoElement[] = []; undoRedoService.ts ×3
346 > const future: IUndoRedoElement[] = [];
347 >
348 > for (const element of this._past) {
349 > past.push(element.actual);
350 > }
351 > for (const element of this._future) {
352 > future.push(element.actual); undoRedoService.ts ×2
353 > }
355 > return { past, future };
356 > }
358 > public getClosestPastElement(): StackElement | null {
359 > if (this._past.length === 0) { undoRedoService.ts ×18
360 return null;
361 }
362 > return this._past[this._past.length - 1]; undoRedoService.ts ×18
363 > }
365 > public getSecondClosestPastElement(): StackElement | null {
366 if (this._past.length < 2) {
367 return null;
368 }
369 return this._past[this._past.length - 2];
370 }
372 > public getClosestFutureElement(): StackElement | null {
373 > if (this._future.length === 0) { undoRedoService.ts ×24
374 return null;
375 }
376 > return this._future[this._future.length - 1]; undoRedoService.ts ×24
377 > }
379 > public hasPastElements(): boolean {
380 > return (this._past.length > 0); undoRedoService.ts ×1
381 > }
383 > public hasFutureElements(): boolean {
384 > return (this._future.length > 0); undoRedoService.ts ×1
385 > }
387 > public splitPastWorkspaceElement(toRemove: WorkspaceStackElement, individualMap: Map<string, ResourceStackElement>): void {
388 for (let j = this._past.length - 1; j >= 0; j--) {
389 if (this._past[j] === toRemove) {
390 if (individualMap.has(this.strResource)) {
391 // gets replaced
392 this._past[j] = individualMap.get(this.strResource)!;
393 } else {
394 // gets deleted
395 this._past.splice(j, 1);
396 }
397 break;
398 }
399 }
400 this.versionId++;
401 }
403 > public splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, individualMap: Map<string, ResourceStackElement>): void {
404 for (let j = this._future.length - 1; j >= 0; j--) {
405 if (this._future[j] === toRemove) {
406 if (individualMap.has(this.strResource)) {
407 // gets replaced
408 this._future[j] = individualMap.get(this.strResource)!;
409 } else {
410 // gets deleted
411 this._future.splice(j, 1);
412 }
413 break;
414 }
415 }
416 this.versionId++;
417 }
419 > public moveBackward(element: StackElement): void {
420 > this._past.pop(); undoRedoService.ts ×18
421 > this._future.push(element);
422 > this.versionId++;
423 > }
425 > public moveForward(element: StackElement): void {
426 > this._future.pop(); undoRedoService.ts ×24
427 > this._past.push(element);
428 > this.versionId++;
429 > }
431 >
432 > class EditStackSnapshot {
433 >
434 > public readonly editStacks: ResourceEditStack[];
435 > private readonly _versionIds: number[];
436 >
437 > constructor(editStacks: ResourceEditStack[]) {
438 > this.editStacks = editStacks; undoRedoService.ts ×18
439 > this._versionIds = [];
440 > for (let i = 0, len = this.editStacks.length; i < len; i++) {
441 > this._versionIds[i] = this.editStacks[i].versionId;
442 > }
443 > }
445 > public isValid(): boolean {
446 > for (let i = 0, len = this.editStacks.length; i < len; i++) { undoRedoService.ts ×43
447 > if (this._versionIds[i] !== this.editStacks[i].versionId) {
448 return false;
449 }
451 > return true;
452 > }
454 >
455 > const missingEditStack = new ResourceEditStack('', '');
456 > missingEditStack.locked = true;
457 >
458 > export class UndoRedoService implements IUndoRedoService {
459 > declare readonly _serviceBrand: undefined;
460 >
461 > private readonly _editStacks: Map<string, ResourceEditStack>;
462 > private readonly _uriComparisonKeyComputers: [string, UriComparisonKeyComputer][];
463 >
464 > constructor(
465 > @IDialogService private readonly _dialogService: IDialogService, undoRedoService.ts ×1
466 > @INotificationService private readonly _notificationService: INotificationService,
467 > ) {
468 > this._editStacks = new Map<string, ResourceEditStack>();
469 > this._uriComparisonKeyComputers = [];
470 > }
472 > public registerUriComparisonKeyComputer(scheme: string, uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable {
473 this._uriComparisonKeyComputers.push([scheme, uriComparisonKeyComputer]);
474 return {
475 dispose: () => {
476 for (let i = 0, len = this._uriComparisonKeyComputers.length; i < len; i++) {
477 if (this._uriComparisonKeyComputers[i][1] === uriComparisonKeyComputer) {
478 this._uriComparisonKeyComputers.splice(i, 1);
479 return;
480 }
481 }
482 }
483 };
484 }
486 > public getUriComparisonKey(resource: URI): string {
487 > for (const uriComparisonKeyComputer of this._uriComparisonKeyComputers) { undoRedoService.ts ×2
488 if (uriComparisonKeyComputer[0] === resource.scheme) {
489 return uriComparisonKeyComputer[1].getComparisonKey(resource);
490 }
491 }
492 > return resource.toString(); undoRedoService.ts ×2
493 > }
495 > private _print(label: string): void {
496 console.log(`------------------------------------`);
497 console.log(`AFTER ${label}: `);
498 const str: string[] = [];
499 for (const element of this._editStacks) {
500 str.push(element[1].toString());
501 }
502 console.log(str.join('\n'));
503 }
505 > public pushElement(element: IUndoRedoElement, group: UndoRedoGroup = UndoRedoGroup.None, source: UndoRedoSource = UndoRedoSource.None): void {
506 > if (element.type === UndoRedoElementType.Resource) { undoRedoService.ts ×9
507 > const resourceLabel = getResourceLabel(element.resource); undoRedoService.ts ×2
508 > const strResource = this.getUriComparisonKey(element.resource);
509 > this._pushElement(new ResourceStackElement(element, resourceLabel, strResource, group.id, group.nextOrder(), source.id, source.nextOrder()));
510 > } else { undoRedoService.ts ×9
511 > const seen = new Set<string>(); undoRedoService.ts ×43
512 > const resourceLabels: string[] = [];
513 > const strResources: string[] = [];
514 > for (const resource of element.resources) {
515 > const resourceLabel = getResourceLabel(resource);
516 > const strResource = this.getUriComparisonKey(resource);
517 >
518 > if (seen.has(strResource)) {
519 continue;
520 }
521 > seen.add(strResource); undoRedoService.ts ×43
522 > resourceLabels.push(resourceLabel);
523 > strResources.push(strResource);
524 > }
525 >
526 > if (resourceLabels.length === 1) {
527 this._pushElement(new ResourceStackElement(element, resourceLabels[0], strResources[0], group.id, group.nextOrder(), source.id, source.nextOrder()));
528 > } else { undoRedoService.ts ×43
529 > this._pushElement(new WorkspaceStackElement(element, resourceLabels, strResources, group.id, group.nextOrder(), source.id, source.nextOrder()));
530 > }
531 > }
532 > if (DEBUG) { undoRedoService.ts ×9
533 this._print('pushElement');
534 }
537 > private _pushElement(element: StackElement): void {
538 > for (let i = 0, len = element.strResources.length; i < len; i++) { undoRedoService.ts ×9
539 > const resourceLabel = element.resourceLabels[i];
540 > const strResource = element.strResources[i];
541 >
542 > let editStack: ResourceEditStack;
543 > if (this._editStacks.has(strResource)) {
544 > editStack = this._editStacks.get(strResource)!; undoRedoService.ts ×1
545 > } else { undoRedoService.ts ×9
546 > editStack = new ResourceEditStack(resourceLabel, strResource);
547 > this._editStacks.set(strResource, editStack);
548 > }
549 >
550 > editStack.pushElement(element);
551 > }
552 > }
554 > public getLastElement(resource: URI): IUndoRedoElement | null {
555 > const strResource = this.getUriComparisonKey(resource); undoRedoService.ts ×2
556 > if (this._editStacks.has(strResource)) {
557 > const editStack = this._editStacks.get(strResource)!; undoRedoService.ts ×2
558 > if (editStack.hasFutureElements()) {
559 > return null; undoRedoService.ts ×24
560 > }
561 > const closestPastElement = editStack.getClosestPastElement(); undoRedoService.ts ×2
562 > return closestPastElement ? closestPastElement.actual : null;
563 > }
564 > return null; undoRedoService.ts ×1
567 > private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement & { actual: { split(): IResourceUndoRedoElement[] } }, ignoreResources: RemovedResources | null): void {
568 const individualArr = toRemove.actual.split();
569 const individualMap = new Map<string, ResourceStackElement>();
570 for (const _element of individualArr) {
571 const resourceLabel = getResourceLabel(_element.resource);
572 const strResource = this.getUriComparisonKey(_element.resource);
573 const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0, 0, 0);
574 individualMap.set(element.strResource, element);
575 }
576
577 for (const strResource of toRemove.strResources) {
578 if (ignoreResources && ignoreResources.has(strResource)) {
579 continue;
580 }
581 const editStack = this._editStacks.get(strResource)!;
582 editStack.splitPastWorkspaceElement(toRemove, individualMap);
583 }
584 }
586 > private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement & { actual: { split(): IResourceUndoRedoElement[] } }, ignoreResources: RemovedResources | null): void {
587 const individualArr = toRemove.actual.split();
588 const individualMap = new Map<string, ResourceStackElement>();
589 for (const _element of individualArr) {
590 const resourceLabel = getResourceLabel(_element.resource);
591 const strResource = this.getUriComparisonKey(_element.resource);
592 const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0, 0, 0);
593 individualMap.set(element.strResource, element);
594 }
595
596 for (const strResource of toRemove.strResources) {
597 if (ignoreResources && ignoreResources.has(strResource)) {
598 continue;
599 }
600 const editStack = this._editStacks.get(strResource)!;
601 editStack.splitFutureWorkspaceElement(toRemove, individualMap);
602 }
603 }
605 > public removeElements(resource: URI | string): void {
606 > const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource); textModel.ts ×3
607 > if (this._editStacks.has(strResource)) {
608 > const editStack = this._editStacks.get(strResource)!; undoRedoService.ts ×3
609 > editStack.dispose();
610 > this._editStacks.delete(strResource);
611 > }
612 > if (DEBUG) { textModel.ts ×3
613 this._print('removeElements');
614 }
617 > public setElementsValidFlag(resource: URI, isValid: boolean, filter: (element: IUndoRedoElement) => boolean): void {
618 > const strResource = this.getUriComparisonKey(resource); modelService.ts ×14
619 > if (this._editStacks.has(strResource)) {
620 > const editStack = this._editStacks.get(strResource)!;
621 > editStack.setElementsValidFlag(isValid, filter);
622 > }
623 > if (DEBUG) {
624 this._print('setElementsValidFlag');
625 }
628 > public hasElements(resource: URI): boolean {
629 > const strResource = this.getUriComparisonKey(resource); undoRedoService.ts ×24
630 > if (this._editStacks.has(strResource)) {
631 > const editStack = this._editStacks.get(strResource)!;
632 > return (editStack.hasPastElements() || editStack.hasFutureElements());
633 > }
634 > return false; undoRedoService.ts ×9
637 > public createSnapshot(resource: URI): ResourceEditStackSnapshot {
638 > const strResource = this.getUriComparisonKey(resource); undoRedoService.ts ×2
639 > if (this._editStacks.has(strResource)) {
640 > const editStack = this._editStacks.get(strResource)!; undoRedoService.ts ×3
641 > return editStack.createSnapshot(resource);
642 > }
643 > return new ResourceEditStackSnapshot(resource, []); editStack.ts ×15
646 > public restoreSnapshot(snapshot: ResourceEditStackSnapshot): void {
647 > const strResource = this.getUriComparisonKey(snapshot.resource); undoRedoService.ts ×3
648 > if (this._editStacks.has(strResource)) {
649 > const editStack = this._editStacks.get(strResource)!; undoRedoService.ts ×6
650 > editStack.restoreSnapshot(snapshot);
651 >
652 > if (!editStack.hasPastElements() && !editStack.hasFutureElements()) {
653 > // the edit stack is now empty, just remove it entirely modelService.ts ×1
654 > editStack.dispose();
655 > this._editStacks.delete(strResource);
656 > }
658 > if (DEBUG) { undoRedoService.ts ×3
659 this._print('restoreSnapshot');
660 }
663 > public getElements(resource: URI): IPastFutureElements {
664 > const strResource = this.getUriComparisonKey(resource); undoRedoService.ts ×2
665 > if (this._editStacks.has(strResource)) {
666 > const editStack = this._editStacks.get(strResource)!; undoRedoService.ts ×3
667 > return editStack.getElements();
668 > }
669 > return { past: [], future: [] }; modelService.ts ×2
672 > private _findClosestUndoElementWithSource(sourceId: number): [StackElement | null, string | null] {
673 if (!sourceId) {
674 return [null, null];
675 }
676
677 // find an element with the sourceId and with the highest sourceOrder ready to be undone
678 let matchedElement: StackElement | null = null;
679 let matchedStrResource: string | null = null;
680
681 for (const [strResource, editStack] of this._editStacks) {
682 const candidate = editStack.getClosestPastElement();
683 if (!candidate) {
684 continue;
685 }
686 if (candidate.sourceId === sourceId) {
687 if (!matchedElement || candidate.sourceOrder > matchedElement.sourceOrder) {
688 matchedElement = candidate;
689 matchedStrResource = strResource;
690 }
691 }
692 }
693
694 return [matchedElement, matchedStrResource];
695 }
697 > public canUndo(resourceOrSource: URI | UndoRedoSource): boolean {
698 > if (resourceOrSource instanceof UndoRedoSource) { undoRedoService.ts ×24
699 const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id);
700 return matchedStrResource ? true : false;
701 }
702 > const strResource = this.getUriComparisonKey(resourceOrSource); undoRedoService.ts ×24
703 > if (this._editStacks.has(strResource)) {
704 > const editStack = this._editStacks.get(strResource)!;
705 > return editStack.hasPastElements();
706 > }
707 > return false; undoRedoService.ts ×9
710 > private _onError(err: Error, element: StackElement): void {
711 onUnexpectedError(err);
712 // An error occurred while undoing or redoing => drop the undo/redo stack for all affected resources
713 for (const strResource of element.strResources) {
714 this.removeElements(strResource);
715 }
716 this._notificationService.error(err);
717 }
719 > private _acquireLocks(editStackSnapshot: EditStackSnapshot): () => void {
720 > // first, check if all locks can be acquired undoRedoService.ts ×18
721 > for (const editStack of editStackSnapshot.editStacks) {
722 > if (editStack.locked) {
723 throw new Error('Cannot acquire edit stack lock');
724 }
726 >
727 > // can acquire all locks
728 > for (const editStack of editStackSnapshot.editStacks) {
729 > editStack.locked = true;
730 > }
731 >
732 > return () => {
733 > // release all locks
734 > for (const editStack of editStackSnapshot.editStacks) {
735 > editStack.locked = false;
736 > }
737 > };
738 > }
740 > private _safeInvokeWithLocks(element: StackElement, invoke: () => Promise<void> | void, editStackSnapshot: EditStackSnapshot, cleanup: IDisposable, continuation: () => Promise<void> | void): Promise<void> | void {
741 > const releaseLocks = this._acquireLocks(editStackSnapshot); undoRedoService.ts ×18
742 >
743 > let result: Promise<void> | void;
744 > try {
745 > result = invoke();
746 > } catch (err) {
747 releaseLocks();
748 cleanup.dispose();
749 return this._onError(err, element);
750 }
752 > if (result) {
753 // result is Promise<void>
754 return result.then(
755 () => {
756 releaseLocks();
757 cleanup.dispose();
758 return continuation();
759 },
760 (err) => {
761 releaseLocks();
762 cleanup.dispose();
763 return this._onError(err, element);
764 }
765 );
766 > } else { undoRedoService.ts ×18
767 > // result is void
768 > releaseLocks();
769 > cleanup.dispose();
770 > return continuation();
771 > }
772 > }
774 > private async _invokeWorkspacePrepare(element: WorkspaceStackElement): Promise<IDisposable> {
775 > if (typeof element.actual.prepareUndoRedo === 'undefined') { undoRedoService.ts ×43
776 > return Disposable.None;
777 > }
778 const result = element.actual.prepareUndoRedo();
779 if (typeof result === 'undefined') {
780 return Disposable.None;
781 }
782 return result;
785 > private _invokeResourcePrepare(element: ResourceStackElement, callback: (disposable: IDisposable) => Promise<void> | void): void | Promise<void> {
786 > if (element.actual.type !== UndoRedoElementType.Workspace || typeof element.actual.prepareUndoRedo === 'undefined') { undoRedoService.ts ×6
787 > // no preparation needed
788 > return callback(Disposable.None);
789 > }
790
791 const r = element.actual.prepareUndoRedo();
792 if (!r) {
793 // nothing to clean up
794 return callback(Disposable.None);
795 }
796
797 if (isDisposable(r)) {
798 return callback(r);
799 }
800
801 return r.then((disposable) => {
802 return callback(disposable);
803 });
806 > private _getAffectedEditStacks(element: WorkspaceStackElement): EditStackSnapshot {
807 > const affectedEditStacks: ResourceEditStack[] = []; undoRedoService.ts ×43
808 > for (const strResource of element.strResources) {
809 > affectedEditStacks.push(this._editStacks.get(strResource) || missingEditStack);
810 > }
811 > return new EditStackSnapshot(affectedEditStacks);
812 > }
814 > private _tryToSplitAndUndo(strResource: string, element: WorkspaceStackElement, ignoreResources: RemovedResources | null, message: string): WorkspaceVerificationError {
815 if (element.canSplit()) {
816 this._splitPastWorkspaceElement(element, ignoreResources);
817 this._notificationService.warn(message);
818 return new WorkspaceVerificationError(this._undo(strResource, 0, true));
819 } else {
820 // Cannot safely split this workspace element => flush all undo/redo stacks
821 for (const strResource of element.strResources) {
822 this.removeElements(strResource);
823 }
824 this._notificationService.warn(message);
825 return new WorkspaceVerificationError();
826 }
827 }
829 > private _checkWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null {
830 > if (element.removedResources) { undoRedoService.ts ×43
831 return this._tryToSplitAndUndo(
832 strResource,
833 element,
834 element.removedResources,
835 nls.localize(
836 { key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] },
837 "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()
838 )
839 );
840 }
841 > if (checkInvalidatedResources && element.invalidatedResources) { undoRedoService.ts ×43
842 return this._tryToSplitAndUndo(
843 strResource,
844 element,
845 element.invalidatedResources,
846 nls.localize(
847 { key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] },
848 "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()
849 )
850 );
851 }
853 > // this must be the last past element in all the impacted resources!
854 > const cannotUndoDueToResources: string[] = [];
855 > for (const editStack of editStackSnapshot.editStacks) {
856 > if (editStack.getClosestPastElement() !== element) {
857 cannotUndoDueToResources.push(editStack.resourceLabel);
858 }
860 > if (cannotUndoDueToResources.length > 0) {
861 return this._tryToSplitAndUndo(
862 strResource,
863 element,
864 null,
865 nls.localize(
866 { key: 'cannotWorkspaceUndoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] },
867 "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', ')
868 )
869 );
870 }
872 > const cannotLockDueToResources: string[] = [];
873 > for (const editStack of editStackSnapshot.editStacks) {
874 > if (editStack.locked) {
875 cannotLockDueToResources.push(editStack.resourceLabel);
876 }
878 > if (cannotLockDueToResources.length > 0) {
879 return this._tryToSplitAndUndo(
880 strResource,
881 element,
882 null,
883 nls.localize(
884 { key: 'cannotWorkspaceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] },
885 "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')
886 )
887 );
888 }
890 > // check if new stack elements were added in the meantime...
891 > if (!editStackSnapshot.isValid()) {
892 return this._tryToSplitAndUndo(
893 strResource,
894 element,
895 null,
896 nls.localize(
897 { key: 'cannotWorkspaceUndoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] },
898 "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label
899 )
900 );
901 }
903 > return null;
904 > }
906 > private _workspaceUndo(strResource: string, element: WorkspaceStackElement, undoConfirmed: boolean): Promise<void> | void {
907 > const affectedEditStacks = this._getAffectedEditStacks(element); undoRedoService.ts ×43
908 > const verificationError = this._checkWorkspaceUndo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false);
909 > if (verificationError) {
910 return verificationError.returnValue;
911 }
912 > return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks, undoConfirmed); undoRedoService.ts ×43
913 > }
915 > private _isPartOfUndoGroup(element: WorkspaceStackElement): boolean {
916 > if (!element.groupId) { undoRedoService.ts ×43
917 > return false;
918 > }
919 // check that there is at least another element with the same groupId ready to be undone
920 for (const [, editStack] of this._editStacks) {
921 const pastElement = editStack.getClosestPastElement();
922 if (!pastElement) {
923 continue;
924 }
925 if (pastElement === element) {
926 const secondPastElement = editStack.getSecondClosestPastElement();
927 if (secondPastElement && secondPastElement.groupId === element.groupId) {
928 // there is another element with the same group id in the same stack!
929 return true;
930 }
931 }
932 if (pastElement.groupId === element.groupId) {
933 // there is another element with the same group id in another stack!
934 return true;
935 }
936 }
937 return false;
940 > private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, undoConfirmed: boolean): Promise<void> {
942 > if (element.canSplit() && !this._isPartOfUndoGroup(element)) {
943 > // this element can be split
944 >
945 > enum UndoChoice {
946 > All = 0,
947 > This = 1,
948 > Cancel = 2
949 > }
950 >
951 > const { result } = await this._dialogService.prompt<UndoChoice>({
952 > type: Severity.Info,
953 > message: nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label),
954 > buttons: [
955 > {
956 > label: nls.localize({ key: 'ok', comment: ['{0} denotes a number that is > 1, && denotes a mnemonic'] }, "&&Undo in {0} Files", editStackSnapshot.editStacks.length),
957 > run: () => UndoChoice.All
958 > },
959 > {
960 > label: nls.localize({ key: 'nok', comment: ['&& denotes a mnemonic'] }, "Undo this &&File"),
961 > run: () => UndoChoice.This
962 > }
963 > ],
964 > cancelButton: {
965 > run: () => UndoChoice.Cancel
966 > }
967 > });
968 >
969 > if (result === UndoChoice.Cancel) {
970 // choice: cancel
971 return;
972 }
974 > if (result === UndoChoice.This) {
975 // choice: undo this file
976 this._splitPastWorkspaceElement(element, null);
977 return this._undo(strResource, 0, true);
978 }
980 > // choice: undo in all files
981 >
982 > // At this point, it is possible that the element has been made invalid in the meantime (due to the confirmation await)
983 > const verificationError1 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*invalidated resources will be checked after the prepare call*/false);
984 > if (verificationError1) {
985 return verificationError1.returnValue;
986 }
988 > undoConfirmed = true;
989 > }
990 >
991 > // prepare
992 > let cleanup: IDisposable;
993 > try {
994 > cleanup = await this._invokeWorkspacePrepare(element);
995 > } catch (err) {
996 return this._onError(err, element);
997 }
999 > // At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await)
1000 > const verificationError2 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*now also check that there are no more invalidated resources*/true);
1001 > if (verificationError2) {
1002 cleanup.dispose();
1003 return verificationError2.returnValue;
1004 }
1006 > for (const editStack of editStackSnapshot.editStacks) {
1007 > editStack.moveBackward(element);
1008 > }
1009 > return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed));
1010 > }
1012 > private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement, undoConfirmed: boolean): Promise<void> | void {
1013 > if (!element.isValid) { undoRedoService.ts ×6
1014 // invalid element => immediately flush edit stack!
1015 editStack.flushAllElements();
1016 return;
1017 }
1018 > if (editStack.locked) { undoRedoService.ts ×6
1019 const message = nls.localize(
1020 { key: 'cannotResourceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] },
1021 "Could not undo '{0}' because there is already an undo or redo operation running.", element.label
1022 );
1023 this._notificationService.warn(message);
1024 return;
1025 }
1026 > return this._invokeResourcePrepare(element, (cleanup) => { undoRedoService.ts ×6
1027 > editStack.moveBackward(element);
1028 > return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed));
1029 > });
1030 > }
1032 > private _findClosestUndoElementInGroup(groupId: number): [StackElement | null, string | null] {
1033 if (!groupId) {
1034 return [null, null];
1035 }
1036
1037 // find another element with the same groupId and with the highest groupOrder ready to be undone
1038 let matchedElement: StackElement | null = null;
1039 let matchedStrResource: string | null = null;
1040
1041 for (const [strResource, editStack] of this._editStacks) {
1042 const candidate = editStack.getClosestPastElement();
1043 if (!candidate) {
1044 continue;
1045 }
1046 if (candidate.groupId === groupId) {
1047 if (!matchedElement || candidate.groupOrder > matchedElement.groupOrder) {
1048 matchedElement = candidate;
1049 matchedStrResource = strResource;
1050 }
1051 }
1052 }
1053
1054 return [matchedElement, matchedStrResource];
1055 }
1057 > private _continueUndoInGroup(groupId: number, undoConfirmed: boolean): Promise<void> | void {
1058 > if (!groupId) { undoRedoService.ts ×18
1059 > return;
1060 > }
1061
1062 const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId);
1063 if (matchedStrResource) {
1064 return this._undo(matchedStrResource, 0, undoConfirmed);
1065 }
1068 > public undo(resourceOrSource: URI | UndoRedoSource): Promise<void> | void {
1069 > if (resourceOrSource instanceof UndoRedoSource) { undoRedoService.ts ×7
1070 const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id);
1071 return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id, false) : undefined;
1072 }
1073 > if (typeof resourceOrSource === 'string') { undoRedoService.ts ×7
1074 return this._undo(resourceOrSource, 0, false);
1075 }
1076 > return this._undo(this.getUriComparisonKey(resourceOrSource), 0, false); undoRedoService.ts ×7
1077 > }
1079 > private _undo(strResource: string, sourceId: number = 0, undoConfirmed: boolean): Promise<void> | void {
1080 > if (!this._editStacks.has(strResource)) { undoRedoService.ts ×7
1081 > return; undoRedoService.ts ×4
1082 > }
1084 > const editStack = this._editStacks.get(strResource)!;
1085 > const element = editStack.getClosestPastElement();
1086 > if (!element) {
1087 return;
1088 }
1090 > if (element.groupId) {
1091 // this element is a part of a group, we need to make sure undoing in a group is in order
1092 const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId);
1093 if (element !== matchedElement && matchedStrResource) {
1094 // there is an element in the same group that should be undone before this one
1095 return this._undo(matchedStrResource, sourceId, undoConfirmed);
1096 }
1097 }
1099 > const shouldPromptForConfirmation = (element.sourceId !== sourceId || element.confirmBeforeUndo);
1100 > if (shouldPromptForConfirmation && !undoConfirmed) { undoRedoService.ts ×7
1101 // Hit a different source or the element asks for prompt before undo, prompt for confirmation
1102 return this._confirmAndContinueUndo(strResource, sourceId, element);
1103 }
1105 > try {
1106 > if (element.type === UndoRedoElementType.Workspace) {
1107 > return this._workspaceUndo(strResource, element, undoConfirmed); undoRedoService.ts ×43
1108 > } else { undoRedoService.ts ×18
1109 > return this._resourceUndo(editStack, element, undoConfirmed); undoRedoService.ts ×6
1110 > }
1111 > } finally { undoRedoService.ts ×7
1112 > if (DEBUG) { undoRedoService.ts ×18
1113 this._print('undo');
1114 }
1118 > private async _confirmAndContinueUndo(strResource: string, sourceId: number, element: StackElement): Promise<void> {
1119 const result = await this._dialogService.confirm({
1120 message: nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label),
1121 primaryButton: nls.localize({ key: 'confirmDifferentSource.yes', comment: ['&& denotes a mnemonic'] }, "&&Yes"),
1122 cancelButton: nls.localize('confirmDifferentSource.no', "No")
1123 });
1124
1125 if (!result.confirmed) {
1126 return;
1127 }
1128
1129 return this._undo(strResource, sourceId, true);
1130 }
1132 > private _findClosestRedoElementWithSource(sourceId: number): [StackElement | null, string | null] {
1133 if (!sourceId) {
1134 return [null, null];
1135 }
1136
1137 // find an element with sourceId and with the lowest sourceOrder ready to be redone
1138 let matchedElement: StackElement | null = null;
1139 let matchedStrResource: string | null = null;
1140
1141 for (const [strResource, editStack] of this._editStacks) {
1142 const candidate = editStack.getClosestFutureElement();
1143 if (!candidate) {
1144 continue;
1145 }
1146 if (candidate.sourceId === sourceId) {
1147 if (!matchedElement || candidate.sourceOrder < matchedElement.sourceOrder) {
1148 matchedElement = candidate;
1149 matchedStrResource = strResource;
1150 }
1151 }
1152 }
1153
1154 return [matchedElement, matchedStrResource];
1155 }
1157 > public canRedo(resourceOrSource: URI | UndoRedoSource): boolean {
1158 > if (resourceOrSource instanceof UndoRedoSource) { undoRedoService.ts ×24
1159 const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id);
1160 return matchedStrResource ? true : false;
1161 }
1162 > const strResource = this.getUriComparisonKey(resourceOrSource); undoRedoService.ts ×24
1163 > if (this._editStacks.has(strResource)) {
1164 > const editStack = this._editStacks.get(strResource)!;
1165 > return editStack.hasFutureElements();
1166 > }
1167 > return false; undoRedoService.ts ×9
1170 > private _tryToSplitAndRedo(strResource: string, element: WorkspaceStackElement, ignoreResources: RemovedResources | null, message: string): WorkspaceVerificationError {
1171 if (element.canSplit()) {
1172 this._splitFutureWorkspaceElement(element, ignoreResources);
1173 this._notificationService.warn(message);
1174 return new WorkspaceVerificationError(this._redo(strResource));
1175 } else {
1176 // Cannot safely split this workspace element => flush all undo/redo stacks
1177 for (const strResource of element.strResources) {
1178 this.removeElements(strResource);
1179 }
1180 this._notificationService.warn(message);
1181 return new WorkspaceVerificationError();
1182 }
1183 }
1185 > private _checkWorkspaceRedo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null {
1186 > if (element.removedResources) { undoRedoService.ts ×43
1187 return this._tryToSplitAndRedo(
1188 strResource,
1189 element,
1190 element.removedResources,
1191 nls.localize(
1192 { key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] },
1193 "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()
1194 )
1195 );
1196 }
1197 > if (checkInvalidatedResources && element.invalidatedResources) { undoRedoService.ts ×43
1198 return this._tryToSplitAndRedo(
1199 strResource,
1200 element,
1201 element.invalidatedResources,
1202 nls.localize(
1203 { key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] },
1204 "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()
1205 )
1206 );
1207 }
1209 > // this must be the last future element in all the impacted resources!
1210 > const cannotRedoDueToResources: string[] = [];
1211 > for (const editStack of editStackSnapshot.editStacks) {
1212 > if (editStack.getClosestFutureElement() !== element) {
1213 cannotRedoDueToResources.push(editStack.resourceLabel);
1214 }
1216 > if (cannotRedoDueToResources.length > 0) {
1217 return this._tryToSplitAndRedo(
1218 strResource,
1219 element,
1220 null,
1221 nls.localize(
1222 { key: 'cannotWorkspaceRedoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] },
1223 "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', ')
1224 )
1225 );
1226 }
1228 > const cannotLockDueToResources: string[] = [];
1229 > for (const editStack of editStackSnapshot.editStacks) {
1230 > if (editStack.locked) {
1231 cannotLockDueToResources.push(editStack.resourceLabel);
1232 }
1234 > if (cannotLockDueToResources.length > 0) {
1235 return this._tryToSplitAndRedo(
1236 strResource,
1237 element,
1238 null,
1239 nls.localize(
1240 { key: 'cannotWorkspaceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] },
1241 "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')
1242 )
1243 );
1244 }
1246 > // check if new stack elements were added in the meantime...
1247 > if (!editStackSnapshot.isValid()) {
1248 return this._tryToSplitAndRedo(
1249 strResource,
1250 element,
1251 null,
1252 nls.localize(
1253 { key: 'cannotWorkspaceRedoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] },
1254 "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label
1255 )
1256 );
1257 }
1259 > return null;
1260 > }
1262 > private _workspaceRedo(strResource: string, element: WorkspaceStackElement): Promise<void> | void {
1263 > const affectedEditStacks = this._getAffectedEditStacks(element); undoRedoService.ts ×43
1264 > const verificationError = this._checkWorkspaceRedo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false);
1265 > if (verificationError) {
1266 return verificationError.returnValue;
1267 }
1268 > return this._executeWorkspaceRedo(strResource, element, affectedEditStacks); undoRedoService.ts ×43
1269 > }
1271 > private async _executeWorkspaceRedo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise<void> {
1272 > // prepare undoRedoService.ts ×43
1273 > let cleanup: IDisposable;
1274 > try {
1275 > cleanup = await this._invokeWorkspacePrepare(element);
1276 > } catch (err) {
1277 return this._onError(err, element);
1278 }
1280 > // At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await)
1281 > const verificationError = this._checkWorkspaceRedo(strResource, element, editStackSnapshot, /*now also check that there are no more invalidated resources*/true);
1282 > if (verificationError) {
1283 cleanup.dispose();
1284 return verificationError.returnValue;
1285 }
1287 > for (const editStack of editStackSnapshot.editStacks) {
1288 > editStack.moveForward(element);
1289 > }
1290 > return this._safeInvokeWithLocks(element, () => element.actual.redo(), editStackSnapshot, cleanup, () => this._continueRedoInGroup(element.groupId));
1291 > }
1293 > private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
1294 > if (!element.isValid) { undoRedoService.ts ×9
1295 // invalid element => immediately flush edit stack!
1296 editStack.flushAllElements();
1297 return;
1298 }
1299 > if (editStack.locked) { undoRedoService.ts ×9
1300 const message = nls.localize(
1301 { key: 'cannotResourceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] },
1302 "Could not redo '{0}' because there is already an undo or redo operation running.", element.label
1303 );
1304 this._notificationService.warn(message);
1305 return;
1306 }
1308 > return this._invokeResourcePrepare(element, (cleanup) => {
1309 > editStack.moveForward(element);
1310 > return this._safeInvokeWithLocks(element, () => element.actual.redo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueRedoInGroup(element.groupId));
1311 > });
1312 > }
1314 > private _findClosestRedoElementInGroup(groupId: number): [StackElement | null, string | null] {
1315 if (!groupId) {
1316 return [null, null];
1317 }
1318
1319 // find another element with the same groupId and with the lowest groupOrder ready to be redone
1320 let matchedElement: StackElement | null = null;
1321 let matchedStrResource: string | null = null;
1322
1323 for (const [strResource, editStack] of this._editStacks) {
1324 const candidate = editStack.getClosestFutureElement();
1325 if (!candidate) {
1326 continue;
1327 }
1328 if (candidate.groupId === groupId) {
1329 if (!matchedElement || candidate.groupOrder < matchedElement.groupOrder) {
1330 matchedElement = candidate;
1331 matchedStrResource = strResource;
1332 }
1333 }
1334 }
1335
1336 return [matchedElement, matchedStrResource];
1337 }
1339 > private _continueRedoInGroup(groupId: number): Promise<void> | void {
1340 > if (!groupId) { undoRedoService.ts ×24
1341 > return;
1342 > }
1343
1344 const [, matchedStrResource] = this._findClosestRedoElementInGroup(groupId);
1345 if (matchedStrResource) {
1346 return this._redo(matchedStrResource);
1347 }
1350 > public redo(resourceOrSource: URI | UndoRedoSource | string): Promise<void> | void {
1351 > if (resourceOrSource instanceof UndoRedoSource) { undoRedoService.ts ×24
1352 const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id);
1353 return matchedStrResource ? this._redo(matchedStrResource) : undefined;
1354 }
1355 > if (typeof resourceOrSource === 'string') { undoRedoService.ts ×24
1356 return this._redo(resourceOrSource);
1357 }
1358 > return this._redo(this.getUriComparisonKey(resourceOrSource)); undoRedoService.ts ×24
1359 > }
1361 > private _redo(strResource: string): Promise<void> | void {
1362 > if (!this._editStacks.has(strResource)) { undoRedoService.ts ×24
1363 return;
1364 }
1366 > const editStack = this._editStacks.get(strResource)!;
1367 > const element = editStack.getClosestFutureElement();
1368 > if (!element) {
1369 return;
1370 }
1372 > if (element.groupId) {
1373 // this element is a part of a group, we need to make sure redoing in a group is in order
1374 const [matchedElement, matchedStrResource] = this._findClosestRedoElementInGroup(element.groupId);
1375 if (element !== matchedElement && matchedStrResource) {
1376 // there is an element in the same group that should be redone before this one
1377 return this._redo(matchedStrResource);
1378 }
1379 }
1381 > try {
1382 > if (element.type === UndoRedoElementType.Workspace) {
1383 > return this._workspaceRedo(strResource, element); undoRedoService.ts ×43
1384 > } else { undoRedoService.ts ×24
1385 > return this._resourceRedo(editStack, element); undoRedoService.ts ×9
1386 > }
1387 > } finally { undoRedoService.ts ×24
1388 > if (DEBUG) {
1389 this._print('redo');
1390 }
1392 > }
1394 >
1395 > class WorkspaceVerificationError {
1396 > constructor(public readonly returnValue: Promise<void> | void) { }
1397 > }
1398 >
1399 > registerSingleton(IUndoRedoService, UndoRedoService, InstantiationType.Delayed);