undoRedoService.js 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097
  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. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. var __param = (this && this.__param) || function (paramIndex, decorator) {
  12. return function (target, key) { decorator(target, key, paramIndex); }
  13. };
  14. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  15. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  16. return new (P || (P = Promise))(function (resolve, reject) {
  17. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  18. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  19. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  20. step((generator = generator.apply(thisArg, _arguments || [])).next());
  21. });
  22. };
  23. import { onUnexpectedError } from '../../../base/common/errors.js';
  24. import { Disposable, isDisposable } from '../../../base/common/lifecycle.js';
  25. import { Schemas } from '../../../base/common/network.js';
  26. import Severity from '../../../base/common/severity.js';
  27. import * as nls from '../../../nls.js';
  28. import { IDialogService } from '../../dialogs/common/dialogs.js';
  29. import { registerSingleton } from '../../instantiation/common/extensions.js';
  30. import { INotificationService } from '../../notification/common/notification.js';
  31. import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup, UndoRedoSource } from './undoRedo.js';
  32. const DEBUG = false;
  33. function getResourceLabel(resource) {
  34. return resource.scheme === Schemas.file ? resource.fsPath : resource.path;
  35. }
  36. let stackElementCounter = 0;
  37. class ResourceStackElement {
  38. constructor(actual, resourceLabel, strResource, groupId, groupOrder, sourceId, sourceOrder) {
  39. this.id = (++stackElementCounter);
  40. this.type = 0 /* Resource */;
  41. this.actual = actual;
  42. this.label = actual.label;
  43. this.confirmBeforeUndo = actual.confirmBeforeUndo || false;
  44. this.resourceLabel = resourceLabel;
  45. this.strResource = strResource;
  46. this.resourceLabels = [this.resourceLabel];
  47. this.strResources = [this.strResource];
  48. this.groupId = groupId;
  49. this.groupOrder = groupOrder;
  50. this.sourceId = sourceId;
  51. this.sourceOrder = sourceOrder;
  52. this.isValid = true;
  53. }
  54. setValid(isValid) {
  55. this.isValid = isValid;
  56. }
  57. toString() {
  58. return `[id:${this.id}] [group:${this.groupId}] [${this.isValid ? ' VALID' : 'INVALID'}] ${this.actual.constructor.name} - ${this.actual}`;
  59. }
  60. }
  61. class ResourceReasonPair {
  62. constructor(resourceLabel, reason) {
  63. this.resourceLabel = resourceLabel;
  64. this.reason = reason;
  65. }
  66. }
  67. class RemovedResources {
  68. constructor() {
  69. this.elements = new Map();
  70. }
  71. createMessage() {
  72. const externalRemoval = [];
  73. const noParallelUniverses = [];
  74. for (const [, element] of this.elements) {
  75. const dest = (element.reason === 0 /* ExternalRemoval */
  76. ? externalRemoval
  77. : noParallelUniverses);
  78. dest.push(element.resourceLabel);
  79. }
  80. let messages = [];
  81. if (externalRemoval.length > 0) {
  82. messages.push(nls.localize({ key: 'externalRemoval', comment: ['{0} is a list of filenames'] }, "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', ')));
  83. }
  84. if (noParallelUniverses.length > 0) {
  85. messages.push(nls.localize({ key: 'noParallelUniverses', comment: ['{0} is a list of filenames'] }, "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', ')));
  86. }
  87. return messages.join('\n');
  88. }
  89. get size() {
  90. return this.elements.size;
  91. }
  92. has(strResource) {
  93. return this.elements.has(strResource);
  94. }
  95. set(strResource, value) {
  96. this.elements.set(strResource, value);
  97. }
  98. delete(strResource) {
  99. return this.elements.delete(strResource);
  100. }
  101. }
  102. class WorkspaceStackElement {
  103. constructor(actual, resourceLabels, strResources, groupId, groupOrder, sourceId, sourceOrder) {
  104. this.id = (++stackElementCounter);
  105. this.type = 1 /* Workspace */;
  106. this.actual = actual;
  107. this.label = actual.label;
  108. this.confirmBeforeUndo = actual.confirmBeforeUndo || false;
  109. this.resourceLabels = resourceLabels;
  110. this.strResources = strResources;
  111. this.groupId = groupId;
  112. this.groupOrder = groupOrder;
  113. this.sourceId = sourceId;
  114. this.sourceOrder = sourceOrder;
  115. this.removedResources = null;
  116. this.invalidatedResources = null;
  117. }
  118. canSplit() {
  119. return (typeof this.actual.split === 'function');
  120. }
  121. removeResource(resourceLabel, strResource, reason) {
  122. if (!this.removedResources) {
  123. this.removedResources = new RemovedResources();
  124. }
  125. if (!this.removedResources.has(strResource)) {
  126. this.removedResources.set(strResource, new ResourceReasonPair(resourceLabel, reason));
  127. }
  128. }
  129. setValid(resourceLabel, strResource, isValid) {
  130. if (isValid) {
  131. if (this.invalidatedResources) {
  132. this.invalidatedResources.delete(strResource);
  133. if (this.invalidatedResources.size === 0) {
  134. this.invalidatedResources = null;
  135. }
  136. }
  137. }
  138. else {
  139. if (!this.invalidatedResources) {
  140. this.invalidatedResources = new RemovedResources();
  141. }
  142. if (!this.invalidatedResources.has(strResource)) {
  143. this.invalidatedResources.set(strResource, new ResourceReasonPair(resourceLabel, 0 /* ExternalRemoval */));
  144. }
  145. }
  146. }
  147. toString() {
  148. return `[id:${this.id}] [group:${this.groupId}] [${this.invalidatedResources ? 'INVALID' : ' VALID'}] ${this.actual.constructor.name} - ${this.actual}`;
  149. }
  150. }
  151. class ResourceEditStack {
  152. constructor(resourceLabel, strResource) {
  153. this.resourceLabel = resourceLabel;
  154. this.strResource = strResource;
  155. this._past = [];
  156. this._future = [];
  157. this.locked = false;
  158. this.versionId = 1;
  159. }
  160. dispose() {
  161. for (const element of this._past) {
  162. if (element.type === 1 /* Workspace */) {
  163. element.removeResource(this.resourceLabel, this.strResource, 0 /* ExternalRemoval */);
  164. }
  165. }
  166. for (const element of this._future) {
  167. if (element.type === 1 /* Workspace */) {
  168. element.removeResource(this.resourceLabel, this.strResource, 0 /* ExternalRemoval */);
  169. }
  170. }
  171. this.versionId++;
  172. }
  173. toString() {
  174. let result = [];
  175. result.push(`* ${this.strResource}:`);
  176. for (let i = 0; i < this._past.length; i++) {
  177. result.push(` * [UNDO] ${this._past[i]}`);
  178. }
  179. for (let i = this._future.length - 1; i >= 0; i--) {
  180. result.push(` * [REDO] ${this._future[i]}`);
  181. }
  182. return result.join('\n');
  183. }
  184. flushAllElements() {
  185. this._past = [];
  186. this._future = [];
  187. this.versionId++;
  188. }
  189. _setElementValidFlag(element, isValid) {
  190. if (element.type === 1 /* Workspace */) {
  191. element.setValid(this.resourceLabel, this.strResource, isValid);
  192. }
  193. else {
  194. element.setValid(isValid);
  195. }
  196. }
  197. setElementsValidFlag(isValid, filter) {
  198. for (const element of this._past) {
  199. if (filter(element.actual)) {
  200. this._setElementValidFlag(element, isValid);
  201. }
  202. }
  203. for (const element of this._future) {
  204. if (filter(element.actual)) {
  205. this._setElementValidFlag(element, isValid);
  206. }
  207. }
  208. }
  209. pushElement(element) {
  210. // remove the future
  211. for (const futureElement of this._future) {
  212. if (futureElement.type === 1 /* Workspace */) {
  213. futureElement.removeResource(this.resourceLabel, this.strResource, 1 /* NoParallelUniverses */);
  214. }
  215. }
  216. this._future = [];
  217. this._past.push(element);
  218. this.versionId++;
  219. }
  220. createSnapshot(resource) {
  221. const elements = [];
  222. for (let i = 0, len = this._past.length; i < len; i++) {
  223. elements.push(this._past[i].id);
  224. }
  225. for (let i = this._future.length - 1; i >= 0; i--) {
  226. elements.push(this._future[i].id);
  227. }
  228. return new ResourceEditStackSnapshot(resource, elements);
  229. }
  230. restoreSnapshot(snapshot) {
  231. const snapshotLength = snapshot.elements.length;
  232. let isOK = true;
  233. let snapshotIndex = 0;
  234. let removePastAfter = -1;
  235. for (let i = 0, len = this._past.length; i < len; i++, snapshotIndex++) {
  236. const element = this._past[i];
  237. if (isOK && (snapshotIndex >= snapshotLength || element.id !== snapshot.elements[snapshotIndex])) {
  238. isOK = false;
  239. removePastAfter = 0;
  240. }
  241. if (!isOK && element.type === 1 /* Workspace */) {
  242. element.removeResource(this.resourceLabel, this.strResource, 0 /* ExternalRemoval */);
  243. }
  244. }
  245. let removeFutureBefore = -1;
  246. for (let i = this._future.length - 1; i >= 0; i--, snapshotIndex++) {
  247. const element = this._future[i];
  248. if (isOK && (snapshotIndex >= snapshotLength || element.id !== snapshot.elements[snapshotIndex])) {
  249. isOK = false;
  250. removeFutureBefore = i;
  251. }
  252. if (!isOK && element.type === 1 /* Workspace */) {
  253. element.removeResource(this.resourceLabel, this.strResource, 0 /* ExternalRemoval */);
  254. }
  255. }
  256. if (removePastAfter !== -1) {
  257. this._past = this._past.slice(0, removePastAfter);
  258. }
  259. if (removeFutureBefore !== -1) {
  260. this._future = this._future.slice(removeFutureBefore + 1);
  261. }
  262. this.versionId++;
  263. }
  264. getElements() {
  265. const past = [];
  266. const future = [];
  267. for (const element of this._past) {
  268. past.push(element.actual);
  269. }
  270. for (const element of this._future) {
  271. future.push(element.actual);
  272. }
  273. return { past, future };
  274. }
  275. getClosestPastElement() {
  276. if (this._past.length === 0) {
  277. return null;
  278. }
  279. return this._past[this._past.length - 1];
  280. }
  281. getSecondClosestPastElement() {
  282. if (this._past.length < 2) {
  283. return null;
  284. }
  285. return this._past[this._past.length - 2];
  286. }
  287. getClosestFutureElement() {
  288. if (this._future.length === 0) {
  289. return null;
  290. }
  291. return this._future[this._future.length - 1];
  292. }
  293. hasPastElements() {
  294. return (this._past.length > 0);
  295. }
  296. hasFutureElements() {
  297. return (this._future.length > 0);
  298. }
  299. splitPastWorkspaceElement(toRemove, individualMap) {
  300. for (let j = this._past.length - 1; j >= 0; j--) {
  301. if (this._past[j] === toRemove) {
  302. if (individualMap.has(this.strResource)) {
  303. // gets replaced
  304. this._past[j] = individualMap.get(this.strResource);
  305. }
  306. else {
  307. // gets deleted
  308. this._past.splice(j, 1);
  309. }
  310. break;
  311. }
  312. }
  313. this.versionId++;
  314. }
  315. splitFutureWorkspaceElement(toRemove, individualMap) {
  316. for (let j = this._future.length - 1; j >= 0; j--) {
  317. if (this._future[j] === toRemove) {
  318. if (individualMap.has(this.strResource)) {
  319. // gets replaced
  320. this._future[j] = individualMap.get(this.strResource);
  321. }
  322. else {
  323. // gets deleted
  324. this._future.splice(j, 1);
  325. }
  326. break;
  327. }
  328. }
  329. this.versionId++;
  330. }
  331. moveBackward(element) {
  332. this._past.pop();
  333. this._future.push(element);
  334. this.versionId++;
  335. }
  336. moveForward(element) {
  337. this._future.pop();
  338. this._past.push(element);
  339. this.versionId++;
  340. }
  341. }
  342. class EditStackSnapshot {
  343. constructor(editStacks) {
  344. this.editStacks = editStacks;
  345. this._versionIds = [];
  346. for (let i = 0, len = this.editStacks.length; i < len; i++) {
  347. this._versionIds[i] = this.editStacks[i].versionId;
  348. }
  349. }
  350. isValid() {
  351. for (let i = 0, len = this.editStacks.length; i < len; i++) {
  352. if (this._versionIds[i] !== this.editStacks[i].versionId) {
  353. return false;
  354. }
  355. }
  356. return true;
  357. }
  358. }
  359. const missingEditStack = new ResourceEditStack('', '');
  360. missingEditStack.locked = true;
  361. let UndoRedoService = class UndoRedoService {
  362. constructor(_dialogService, _notificationService) {
  363. this._dialogService = _dialogService;
  364. this._notificationService = _notificationService;
  365. this._editStacks = new Map();
  366. this._uriComparisonKeyComputers = [];
  367. }
  368. getUriComparisonKey(resource) {
  369. for (const uriComparisonKeyComputer of this._uriComparisonKeyComputers) {
  370. if (uriComparisonKeyComputer[0] === resource.scheme) {
  371. return uriComparisonKeyComputer[1].getComparisonKey(resource);
  372. }
  373. }
  374. return resource.toString();
  375. }
  376. _print(label) {
  377. console.log(`------------------------------------`);
  378. console.log(`AFTER ${label}: `);
  379. let str = [];
  380. for (const element of this._editStacks) {
  381. str.push(element[1].toString());
  382. }
  383. console.log(str.join('\n'));
  384. }
  385. pushElement(element, group = UndoRedoGroup.None, source = UndoRedoSource.None) {
  386. if (element.type === 0 /* Resource */) {
  387. const resourceLabel = getResourceLabel(element.resource);
  388. const strResource = this.getUriComparisonKey(element.resource);
  389. this._pushElement(new ResourceStackElement(element, resourceLabel, strResource, group.id, group.nextOrder(), source.id, source.nextOrder()));
  390. }
  391. else {
  392. const seen = new Set();
  393. const resourceLabels = [];
  394. const strResources = [];
  395. for (const resource of element.resources) {
  396. const resourceLabel = getResourceLabel(resource);
  397. const strResource = this.getUriComparisonKey(resource);
  398. if (seen.has(strResource)) {
  399. continue;
  400. }
  401. seen.add(strResource);
  402. resourceLabels.push(resourceLabel);
  403. strResources.push(strResource);
  404. }
  405. if (resourceLabels.length === 1) {
  406. this._pushElement(new ResourceStackElement(element, resourceLabels[0], strResources[0], group.id, group.nextOrder(), source.id, source.nextOrder()));
  407. }
  408. else {
  409. this._pushElement(new WorkspaceStackElement(element, resourceLabels, strResources, group.id, group.nextOrder(), source.id, source.nextOrder()));
  410. }
  411. }
  412. if (DEBUG) {
  413. this._print('pushElement');
  414. }
  415. }
  416. _pushElement(element) {
  417. for (let i = 0, len = element.strResources.length; i < len; i++) {
  418. const resourceLabel = element.resourceLabels[i];
  419. const strResource = element.strResources[i];
  420. let editStack;
  421. if (this._editStacks.has(strResource)) {
  422. editStack = this._editStacks.get(strResource);
  423. }
  424. else {
  425. editStack = new ResourceEditStack(resourceLabel, strResource);
  426. this._editStacks.set(strResource, editStack);
  427. }
  428. editStack.pushElement(element);
  429. }
  430. }
  431. getLastElement(resource) {
  432. const strResource = this.getUriComparisonKey(resource);
  433. if (this._editStacks.has(strResource)) {
  434. const editStack = this._editStacks.get(strResource);
  435. if (editStack.hasFutureElements()) {
  436. return null;
  437. }
  438. const closestPastElement = editStack.getClosestPastElement();
  439. return closestPastElement ? closestPastElement.actual : null;
  440. }
  441. return null;
  442. }
  443. _splitPastWorkspaceElement(toRemove, ignoreResources) {
  444. const individualArr = toRemove.actual.split();
  445. const individualMap = new Map();
  446. for (const _element of individualArr) {
  447. const resourceLabel = getResourceLabel(_element.resource);
  448. const strResource = this.getUriComparisonKey(_element.resource);
  449. const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0, 0, 0);
  450. individualMap.set(element.strResource, element);
  451. }
  452. for (const strResource of toRemove.strResources) {
  453. if (ignoreResources && ignoreResources.has(strResource)) {
  454. continue;
  455. }
  456. const editStack = this._editStacks.get(strResource);
  457. editStack.splitPastWorkspaceElement(toRemove, individualMap);
  458. }
  459. }
  460. _splitFutureWorkspaceElement(toRemove, ignoreResources) {
  461. const individualArr = toRemove.actual.split();
  462. const individualMap = new Map();
  463. for (const _element of individualArr) {
  464. const resourceLabel = getResourceLabel(_element.resource);
  465. const strResource = this.getUriComparisonKey(_element.resource);
  466. const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0, 0, 0);
  467. individualMap.set(element.strResource, element);
  468. }
  469. for (const strResource of toRemove.strResources) {
  470. if (ignoreResources && ignoreResources.has(strResource)) {
  471. continue;
  472. }
  473. const editStack = this._editStacks.get(strResource);
  474. editStack.splitFutureWorkspaceElement(toRemove, individualMap);
  475. }
  476. }
  477. removeElements(resource) {
  478. const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource);
  479. if (this._editStacks.has(strResource)) {
  480. const editStack = this._editStacks.get(strResource);
  481. editStack.dispose();
  482. this._editStacks.delete(strResource);
  483. }
  484. if (DEBUG) {
  485. this._print('removeElements');
  486. }
  487. }
  488. setElementsValidFlag(resource, isValid, filter) {
  489. const strResource = this.getUriComparisonKey(resource);
  490. if (this._editStacks.has(strResource)) {
  491. const editStack = this._editStacks.get(strResource);
  492. editStack.setElementsValidFlag(isValid, filter);
  493. }
  494. if (DEBUG) {
  495. this._print('setElementsValidFlag');
  496. }
  497. }
  498. createSnapshot(resource) {
  499. const strResource = this.getUriComparisonKey(resource);
  500. if (this._editStacks.has(strResource)) {
  501. const editStack = this._editStacks.get(strResource);
  502. return editStack.createSnapshot(resource);
  503. }
  504. return new ResourceEditStackSnapshot(resource, []);
  505. }
  506. restoreSnapshot(snapshot) {
  507. const strResource = this.getUriComparisonKey(snapshot.resource);
  508. if (this._editStacks.has(strResource)) {
  509. const editStack = this._editStacks.get(strResource);
  510. editStack.restoreSnapshot(snapshot);
  511. if (!editStack.hasPastElements() && !editStack.hasFutureElements()) {
  512. // the edit stack is now empty, just remove it entirely
  513. editStack.dispose();
  514. this._editStacks.delete(strResource);
  515. }
  516. }
  517. if (DEBUG) {
  518. this._print('restoreSnapshot');
  519. }
  520. }
  521. getElements(resource) {
  522. const strResource = this.getUriComparisonKey(resource);
  523. if (this._editStacks.has(strResource)) {
  524. const editStack = this._editStacks.get(strResource);
  525. return editStack.getElements();
  526. }
  527. return { past: [], future: [] };
  528. }
  529. _findClosestUndoElementWithSource(sourceId) {
  530. if (!sourceId) {
  531. return [null, null];
  532. }
  533. // find an element with the sourceId and with the highest sourceOrder ready to be undone
  534. let matchedElement = null;
  535. let matchedStrResource = null;
  536. for (const [strResource, editStack] of this._editStacks) {
  537. const candidate = editStack.getClosestPastElement();
  538. if (!candidate) {
  539. continue;
  540. }
  541. if (candidate.sourceId === sourceId) {
  542. if (!matchedElement || candidate.sourceOrder > matchedElement.sourceOrder) {
  543. matchedElement = candidate;
  544. matchedStrResource = strResource;
  545. }
  546. }
  547. }
  548. return [matchedElement, matchedStrResource];
  549. }
  550. canUndo(resourceOrSource) {
  551. if (resourceOrSource instanceof UndoRedoSource) {
  552. const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id);
  553. return matchedStrResource ? true : false;
  554. }
  555. const strResource = this.getUriComparisonKey(resourceOrSource);
  556. if (this._editStacks.has(strResource)) {
  557. const editStack = this._editStacks.get(strResource);
  558. return editStack.hasPastElements();
  559. }
  560. return false;
  561. }
  562. _onError(err, element) {
  563. onUnexpectedError(err);
  564. // An error occurred while undoing or redoing => drop the undo/redo stack for all affected resources
  565. for (const strResource of element.strResources) {
  566. this.removeElements(strResource);
  567. }
  568. this._notificationService.error(err);
  569. }
  570. _acquireLocks(editStackSnapshot) {
  571. // first, check if all locks can be acquired
  572. for (const editStack of editStackSnapshot.editStacks) {
  573. if (editStack.locked) {
  574. throw new Error('Cannot acquire edit stack lock');
  575. }
  576. }
  577. // can acquire all locks
  578. for (const editStack of editStackSnapshot.editStacks) {
  579. editStack.locked = true;
  580. }
  581. return () => {
  582. // release all locks
  583. for (const editStack of editStackSnapshot.editStacks) {
  584. editStack.locked = false;
  585. }
  586. };
  587. }
  588. _safeInvokeWithLocks(element, invoke, editStackSnapshot, cleanup, continuation) {
  589. const releaseLocks = this._acquireLocks(editStackSnapshot);
  590. let result;
  591. try {
  592. result = invoke();
  593. }
  594. catch (err) {
  595. releaseLocks();
  596. cleanup.dispose();
  597. return this._onError(err, element);
  598. }
  599. if (result) {
  600. // result is Promise<void>
  601. return result.then(() => {
  602. releaseLocks();
  603. cleanup.dispose();
  604. return continuation();
  605. }, (err) => {
  606. releaseLocks();
  607. cleanup.dispose();
  608. return this._onError(err, element);
  609. });
  610. }
  611. else {
  612. // result is void
  613. releaseLocks();
  614. cleanup.dispose();
  615. return continuation();
  616. }
  617. }
  618. _invokeWorkspacePrepare(element) {
  619. return __awaiter(this, void 0, void 0, function* () {
  620. if (typeof element.actual.prepareUndoRedo === 'undefined') {
  621. return Disposable.None;
  622. }
  623. const result = element.actual.prepareUndoRedo();
  624. if (typeof result === 'undefined') {
  625. return Disposable.None;
  626. }
  627. return result;
  628. });
  629. }
  630. _invokeResourcePrepare(element, callback) {
  631. if (element.actual.type !== 1 /* Workspace */ || typeof element.actual.prepareUndoRedo === 'undefined') {
  632. // no preparation needed
  633. return callback(Disposable.None);
  634. }
  635. const r = element.actual.prepareUndoRedo();
  636. if (!r) {
  637. // nothing to clean up
  638. return callback(Disposable.None);
  639. }
  640. if (isDisposable(r)) {
  641. return callback(r);
  642. }
  643. return r.then((disposable) => {
  644. return callback(disposable);
  645. });
  646. }
  647. _getAffectedEditStacks(element) {
  648. const affectedEditStacks = [];
  649. for (const strResource of element.strResources) {
  650. affectedEditStacks.push(this._editStacks.get(strResource) || missingEditStack);
  651. }
  652. return new EditStackSnapshot(affectedEditStacks);
  653. }
  654. _tryToSplitAndUndo(strResource, element, ignoreResources, message) {
  655. if (element.canSplit()) {
  656. this._splitPastWorkspaceElement(element, ignoreResources);
  657. this._notificationService.warn(message);
  658. return new WorkspaceVerificationError(this._undo(strResource, 0, true));
  659. }
  660. else {
  661. // Cannot safely split this workspace element => flush all undo/redo stacks
  662. for (const strResource of element.strResources) {
  663. this.removeElements(strResource);
  664. }
  665. this._notificationService.warn(message);
  666. return new WorkspaceVerificationError();
  667. }
  668. }
  669. _checkWorkspaceUndo(strResource, element, editStackSnapshot, checkInvalidatedResources) {
  670. if (element.removedResources) {
  671. return this._tryToSplitAndUndo(strResource, element, element.removedResources, nls.localize({ key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] }, "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()));
  672. }
  673. if (checkInvalidatedResources && element.invalidatedResources) {
  674. return this._tryToSplitAndUndo(strResource, element, element.invalidatedResources, nls.localize({ key: 'cannotWorkspaceUndo', comment: ['{0} is a label for an operation. {1} is another message.'] }, "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()));
  675. }
  676. // this must be the last past element in all the impacted resources!
  677. const cannotUndoDueToResources = [];
  678. for (const editStack of editStackSnapshot.editStacks) {
  679. if (editStack.getClosestPastElement() !== element) {
  680. cannotUndoDueToResources.push(editStack.resourceLabel);
  681. }
  682. }
  683. if (cannotUndoDueToResources.length > 0) {
  684. return this._tryToSplitAndUndo(strResource, element, null, nls.localize({ key: 'cannotWorkspaceUndoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', ')));
  685. }
  686. const cannotLockDueToResources = [];
  687. for (const editStack of editStackSnapshot.editStacks) {
  688. if (editStack.locked) {
  689. cannotLockDueToResources.push(editStack.resourceLabel);
  690. }
  691. }
  692. if (cannotLockDueToResources.length > 0) {
  693. return this._tryToSplitAndUndo(strResource, element, null, nls.localize({ key: 'cannotWorkspaceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')));
  694. }
  695. // check if new stack elements were added in the meantime...
  696. if (!editStackSnapshot.isValid()) {
  697. return this._tryToSplitAndUndo(strResource, element, null, nls.localize({ key: 'cannotWorkspaceUndoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label));
  698. }
  699. return null;
  700. }
  701. _workspaceUndo(strResource, element, undoConfirmed) {
  702. const affectedEditStacks = this._getAffectedEditStacks(element);
  703. const verificationError = this._checkWorkspaceUndo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/ false);
  704. if (verificationError) {
  705. return verificationError.returnValue;
  706. }
  707. return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks, undoConfirmed);
  708. }
  709. _isPartOfUndoGroup(element) {
  710. if (!element.groupId) {
  711. return false;
  712. }
  713. // check that there is at least another element with the same groupId ready to be undone
  714. for (const [, editStack] of this._editStacks) {
  715. const pastElement = editStack.getClosestPastElement();
  716. if (!pastElement) {
  717. continue;
  718. }
  719. if (pastElement === element) {
  720. const secondPastElement = editStack.getSecondClosestPastElement();
  721. if (secondPastElement && secondPastElement.groupId === element.groupId) {
  722. // there is another element with the same group id in the same stack!
  723. return true;
  724. }
  725. }
  726. if (pastElement.groupId === element.groupId) {
  727. // there is another element with the same group id in another stack!
  728. return true;
  729. }
  730. }
  731. return false;
  732. }
  733. _confirmAndExecuteWorkspaceUndo(strResource, element, editStackSnapshot, undoConfirmed) {
  734. return __awaiter(this, void 0, void 0, function* () {
  735. if (element.canSplit() && !this._isPartOfUndoGroup(element)) {
  736. // this element can be split
  737. const result = yield this._dialogService.show(Severity.Info, nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label), [
  738. nls.localize({ key: 'ok', comment: ['{0} denotes a number that is > 1'] }, "Undo in {0} Files", editStackSnapshot.editStacks.length),
  739. nls.localize('nok', "Undo this File"),
  740. nls.localize('cancel', "Cancel"),
  741. ], {
  742. cancelId: 2
  743. });
  744. if (result.choice === 2) {
  745. // choice: cancel
  746. return;
  747. }
  748. if (result.choice === 1) {
  749. // choice: undo this file
  750. this._splitPastWorkspaceElement(element, null);
  751. return this._undo(strResource, 0, true);
  752. }
  753. // choice: undo in all files
  754. // At this point, it is possible that the element has been made invalid in the meantime (due to the confirmation await)
  755. const verificationError1 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*invalidated resources will be checked after the prepare call*/ false);
  756. if (verificationError1) {
  757. return verificationError1.returnValue;
  758. }
  759. undoConfirmed = true;
  760. }
  761. // prepare
  762. let cleanup;
  763. try {
  764. cleanup = yield this._invokeWorkspacePrepare(element);
  765. }
  766. catch (err) {
  767. return this._onError(err, element);
  768. }
  769. // At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await)
  770. const verificationError2 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*now also check that there are no more invalidated resources*/ true);
  771. if (verificationError2) {
  772. cleanup.dispose();
  773. return verificationError2.returnValue;
  774. }
  775. for (const editStack of editStackSnapshot.editStacks) {
  776. editStack.moveBackward(element);
  777. }
  778. return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed));
  779. });
  780. }
  781. _resourceUndo(editStack, element, undoConfirmed) {
  782. if (!element.isValid) {
  783. // invalid element => immediately flush edit stack!
  784. editStack.flushAllElements();
  785. return;
  786. }
  787. if (editStack.locked) {
  788. const message = nls.localize({ key: 'cannotResourceUndoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] }, "Could not undo '{0}' because there is already an undo or redo operation running.", element.label);
  789. this._notificationService.warn(message);
  790. return;
  791. }
  792. return this._invokeResourcePrepare(element, (cleanup) => {
  793. editStack.moveBackward(element);
  794. return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed));
  795. });
  796. }
  797. _findClosestUndoElementInGroup(groupId) {
  798. if (!groupId) {
  799. return [null, null];
  800. }
  801. // find another element with the same groupId and with the highest groupOrder ready to be undone
  802. let matchedElement = null;
  803. let matchedStrResource = null;
  804. for (const [strResource, editStack] of this._editStacks) {
  805. const candidate = editStack.getClosestPastElement();
  806. if (!candidate) {
  807. continue;
  808. }
  809. if (candidate.groupId === groupId) {
  810. if (!matchedElement || candidate.groupOrder > matchedElement.groupOrder) {
  811. matchedElement = candidate;
  812. matchedStrResource = strResource;
  813. }
  814. }
  815. }
  816. return [matchedElement, matchedStrResource];
  817. }
  818. _continueUndoInGroup(groupId, undoConfirmed) {
  819. if (!groupId) {
  820. return;
  821. }
  822. const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId);
  823. if (matchedStrResource) {
  824. return this._undo(matchedStrResource, 0, undoConfirmed);
  825. }
  826. }
  827. undo(resourceOrSource) {
  828. if (resourceOrSource instanceof UndoRedoSource) {
  829. const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id);
  830. return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id, false) : undefined;
  831. }
  832. if (typeof resourceOrSource === 'string') {
  833. return this._undo(resourceOrSource, 0, false);
  834. }
  835. return this._undo(this.getUriComparisonKey(resourceOrSource), 0, false);
  836. }
  837. _undo(strResource, sourceId = 0, undoConfirmed) {
  838. if (!this._editStacks.has(strResource)) {
  839. return;
  840. }
  841. const editStack = this._editStacks.get(strResource);
  842. const element = editStack.getClosestPastElement();
  843. if (!element) {
  844. return;
  845. }
  846. if (element.groupId) {
  847. // this element is a part of a group, we need to make sure undoing in a group is in order
  848. const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId);
  849. if (element !== matchedElement && matchedStrResource) {
  850. // there is an element in the same group that should be undone before this one
  851. return this._undo(matchedStrResource, sourceId, undoConfirmed);
  852. }
  853. }
  854. const shouldPromptForConfirmation = (element.sourceId !== sourceId || element.confirmBeforeUndo);
  855. if (shouldPromptForConfirmation && !undoConfirmed) {
  856. // Hit a different source or the element asks for prompt before undo, prompt for confirmation
  857. return this._confirmAndContinueUndo(strResource, sourceId, element);
  858. }
  859. try {
  860. if (element.type === 1 /* Workspace */) {
  861. return this._workspaceUndo(strResource, element, undoConfirmed);
  862. }
  863. else {
  864. return this._resourceUndo(editStack, element, undoConfirmed);
  865. }
  866. }
  867. finally {
  868. if (DEBUG) {
  869. this._print('undo');
  870. }
  871. }
  872. }
  873. _confirmAndContinueUndo(strResource, sourceId, element) {
  874. return __awaiter(this, void 0, void 0, function* () {
  875. const result = yield this._dialogService.show(Severity.Info, nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label), [
  876. nls.localize('confirmDifferentSource.yes', "Yes"),
  877. nls.localize('cancel', "Cancel"),
  878. ], {
  879. cancelId: 1
  880. });
  881. if (result.choice === 1) {
  882. // choice: cancel
  883. return;
  884. }
  885. // choice: undo
  886. return this._undo(strResource, sourceId, true);
  887. });
  888. }
  889. _findClosestRedoElementWithSource(sourceId) {
  890. if (!sourceId) {
  891. return [null, null];
  892. }
  893. // find an element with sourceId and with the lowest sourceOrder ready to be redone
  894. let matchedElement = null;
  895. let matchedStrResource = null;
  896. for (const [strResource, editStack] of this._editStacks) {
  897. const candidate = editStack.getClosestFutureElement();
  898. if (!candidate) {
  899. continue;
  900. }
  901. if (candidate.sourceId === sourceId) {
  902. if (!matchedElement || candidate.sourceOrder < matchedElement.sourceOrder) {
  903. matchedElement = candidate;
  904. matchedStrResource = strResource;
  905. }
  906. }
  907. }
  908. return [matchedElement, matchedStrResource];
  909. }
  910. canRedo(resourceOrSource) {
  911. if (resourceOrSource instanceof UndoRedoSource) {
  912. const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id);
  913. return matchedStrResource ? true : false;
  914. }
  915. const strResource = this.getUriComparisonKey(resourceOrSource);
  916. if (this._editStacks.has(strResource)) {
  917. const editStack = this._editStacks.get(strResource);
  918. return editStack.hasFutureElements();
  919. }
  920. return false;
  921. }
  922. _tryToSplitAndRedo(strResource, element, ignoreResources, message) {
  923. if (element.canSplit()) {
  924. this._splitFutureWorkspaceElement(element, ignoreResources);
  925. this._notificationService.warn(message);
  926. return new WorkspaceVerificationError(this._redo(strResource));
  927. }
  928. else {
  929. // Cannot safely split this workspace element => flush all undo/redo stacks
  930. for (const strResource of element.strResources) {
  931. this.removeElements(strResource);
  932. }
  933. this._notificationService.warn(message);
  934. return new WorkspaceVerificationError();
  935. }
  936. }
  937. _checkWorkspaceRedo(strResource, element, editStackSnapshot, checkInvalidatedResources) {
  938. if (element.removedResources) {
  939. return this._tryToSplitAndRedo(strResource, element, element.removedResources, nls.localize({ key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] }, "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()));
  940. }
  941. if (checkInvalidatedResources && element.invalidatedResources) {
  942. return this._tryToSplitAndRedo(strResource, element, element.invalidatedResources, nls.localize({ key: 'cannotWorkspaceRedo', comment: ['{0} is a label for an operation. {1} is another message.'] }, "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()));
  943. }
  944. // this must be the last future element in all the impacted resources!
  945. const cannotRedoDueToResources = [];
  946. for (const editStack of editStackSnapshot.editStacks) {
  947. if (editStack.getClosestFutureElement() !== element) {
  948. cannotRedoDueToResources.push(editStack.resourceLabel);
  949. }
  950. }
  951. if (cannotRedoDueToResources.length > 0) {
  952. return this._tryToSplitAndRedo(strResource, element, null, nls.localize({ key: 'cannotWorkspaceRedoDueToChanges', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', ')));
  953. }
  954. const cannotLockDueToResources = [];
  955. for (const editStack of editStackSnapshot.editStacks) {
  956. if (editStack.locked) {
  957. cannotLockDueToResources.push(editStack.resourceLabel);
  958. }
  959. }
  960. if (cannotLockDueToResources.length > 0) {
  961. return this._tryToSplitAndRedo(strResource, element, null, nls.localize({ key: 'cannotWorkspaceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')));
  962. }
  963. // check if new stack elements were added in the meantime...
  964. if (!editStackSnapshot.isValid()) {
  965. return this._tryToSplitAndRedo(strResource, element, null, nls.localize({ key: 'cannotWorkspaceRedoDueToInMeantimeUndoRedo', comment: ['{0} is a label for an operation. {1} is a list of filenames.'] }, "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label));
  966. }
  967. return null;
  968. }
  969. _workspaceRedo(strResource, element) {
  970. const affectedEditStacks = this._getAffectedEditStacks(element);
  971. const verificationError = this._checkWorkspaceRedo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/ false);
  972. if (verificationError) {
  973. return verificationError.returnValue;
  974. }
  975. return this._executeWorkspaceRedo(strResource, element, affectedEditStacks);
  976. }
  977. _executeWorkspaceRedo(strResource, element, editStackSnapshot) {
  978. return __awaiter(this, void 0, void 0, function* () {
  979. // prepare
  980. let cleanup;
  981. try {
  982. cleanup = yield this._invokeWorkspacePrepare(element);
  983. }
  984. catch (err) {
  985. return this._onError(err, element);
  986. }
  987. // At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await)
  988. const verificationError = this._checkWorkspaceRedo(strResource, element, editStackSnapshot, /*now also check that there are no more invalidated resources*/ true);
  989. if (verificationError) {
  990. cleanup.dispose();
  991. return verificationError.returnValue;
  992. }
  993. for (const editStack of editStackSnapshot.editStacks) {
  994. editStack.moveForward(element);
  995. }
  996. return this._safeInvokeWithLocks(element, () => element.actual.redo(), editStackSnapshot, cleanup, () => this._continueRedoInGroup(element.groupId));
  997. });
  998. }
  999. _resourceRedo(editStack, element) {
  1000. if (!element.isValid) {
  1001. // invalid element => immediately flush edit stack!
  1002. editStack.flushAllElements();
  1003. return;
  1004. }
  1005. if (editStack.locked) {
  1006. const message = nls.localize({ key: 'cannotResourceRedoDueToInProgressUndoRedo', comment: ['{0} is a label for an operation.'] }, "Could not redo '{0}' because there is already an undo or redo operation running.", element.label);
  1007. this._notificationService.warn(message);
  1008. return;
  1009. }
  1010. return this._invokeResourcePrepare(element, (cleanup) => {
  1011. editStack.moveForward(element);
  1012. return this._safeInvokeWithLocks(element, () => element.actual.redo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueRedoInGroup(element.groupId));
  1013. });
  1014. }
  1015. _findClosestRedoElementInGroup(groupId) {
  1016. if (!groupId) {
  1017. return [null, null];
  1018. }
  1019. // find another element with the same groupId and with the lowest groupOrder ready to be redone
  1020. let matchedElement = null;
  1021. let matchedStrResource = null;
  1022. for (const [strResource, editStack] of this._editStacks) {
  1023. const candidate = editStack.getClosestFutureElement();
  1024. if (!candidate) {
  1025. continue;
  1026. }
  1027. if (candidate.groupId === groupId) {
  1028. if (!matchedElement || candidate.groupOrder < matchedElement.groupOrder) {
  1029. matchedElement = candidate;
  1030. matchedStrResource = strResource;
  1031. }
  1032. }
  1033. }
  1034. return [matchedElement, matchedStrResource];
  1035. }
  1036. _continueRedoInGroup(groupId) {
  1037. if (!groupId) {
  1038. return;
  1039. }
  1040. const [, matchedStrResource] = this._findClosestRedoElementInGroup(groupId);
  1041. if (matchedStrResource) {
  1042. return this._redo(matchedStrResource);
  1043. }
  1044. }
  1045. redo(resourceOrSource) {
  1046. if (resourceOrSource instanceof UndoRedoSource) {
  1047. const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id);
  1048. return matchedStrResource ? this._redo(matchedStrResource) : undefined;
  1049. }
  1050. if (typeof resourceOrSource === 'string') {
  1051. return this._redo(resourceOrSource);
  1052. }
  1053. return this._redo(this.getUriComparisonKey(resourceOrSource));
  1054. }
  1055. _redo(strResource) {
  1056. if (!this._editStacks.has(strResource)) {
  1057. return;
  1058. }
  1059. const editStack = this._editStacks.get(strResource);
  1060. const element = editStack.getClosestFutureElement();
  1061. if (!element) {
  1062. return;
  1063. }
  1064. if (element.groupId) {
  1065. // this element is a part of a group, we need to make sure redoing in a group is in order
  1066. const [matchedElement, matchedStrResource] = this._findClosestRedoElementInGroup(element.groupId);
  1067. if (element !== matchedElement && matchedStrResource) {
  1068. // there is an element in the same group that should be redone before this one
  1069. return this._redo(matchedStrResource);
  1070. }
  1071. }
  1072. try {
  1073. if (element.type === 1 /* Workspace */) {
  1074. return this._workspaceRedo(strResource, element);
  1075. }
  1076. else {
  1077. return this._resourceRedo(editStack, element);
  1078. }
  1079. }
  1080. finally {
  1081. if (DEBUG) {
  1082. this._print('redo');
  1083. }
  1084. }
  1085. }
  1086. };
  1087. UndoRedoService = __decorate([
  1088. __param(0, IDialogService),
  1089. __param(1, INotificationService)
  1090. ], UndoRedoService);
  1091. export { UndoRedoService };
  1092. class WorkspaceVerificationError {
  1093. constructor(returnValue) {
  1094. this.returnValue = returnValue;
  1095. }
  1096. }
  1097. registerSingleton(IUndoRedoService, UndoRedoService);