@theia/editor 1.13.0-next.fd91f213 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/lib/browser/editor-command.d.ts +19 -0
  2. package/lib/browser/editor-command.d.ts.map +1 -1
  3. package/lib/browser/editor-command.js +53 -4
  4. package/lib/browser/editor-command.js.map +1 -1
  5. package/lib/browser/editor-contribution.d.ts.map +1 -1
  6. package/lib/browser/editor-contribution.js +72 -0
  7. package/lib/browser/editor-contribution.js.map +1 -1
  8. package/lib/browser/editor-keybinding.d.ts +1 -0
  9. package/lib/browser/editor-keybinding.d.ts.map +1 -1
  10. package/lib/browser/editor-keybinding.js +7 -0
  11. package/lib/browser/editor-keybinding.js.map +1 -1
  12. package/lib/browser/editor-manager.d.ts +21 -1
  13. package/lib/browser/editor-manager.d.ts.map +1 -1
  14. package/lib/browser/editor-manager.js +111 -32
  15. package/lib/browser/editor-manager.js.map +1 -1
  16. package/lib/browser/editor-navigation-contribution.d.ts +9 -0
  17. package/lib/browser/editor-navigation-contribution.d.ts.map +1 -1
  18. package/lib/browser/editor-navigation-contribution.js +89 -1
  19. package/lib/browser/editor-navigation-contribution.js.map +1 -1
  20. package/lib/browser/editor-preferences.d.ts +1 -0
  21. package/lib/browser/editor-preferences.d.ts.map +1 -1
  22. package/lib/browser/editor-preferences.js +4 -0
  23. package/lib/browser/editor-preferences.js.map +1 -1
  24. package/lib/browser/editor-widget-factory.d.ts +1 -1
  25. package/lib/browser/editor-widget-factory.d.ts.map +1 -1
  26. package/lib/browser/editor-widget-factory.js +5 -2
  27. package/lib/browser/editor-widget-factory.js.map +1 -1
  28. package/lib/browser/navigation/navigation-location-service.d.ts +31 -3
  29. package/lib/browser/navigation/navigation-location-service.d.ts.map +1 -1
  30. package/lib/browser/navigation/navigation-location-service.js +47 -2
  31. package/lib/browser/navigation/navigation-location-service.js.map +1 -1
  32. package/lib/browser/navigation/navigation-location-service.spec.js +74 -2
  33. package/lib/browser/navigation/navigation-location-service.spec.js.map +1 -1
  34. package/lib/browser/navigation/navigation-location.d.ts +25 -0
  35. package/lib/browser/navigation/navigation-location.d.ts.map +1 -1
  36. package/lib/browser/navigation/navigation-location.js +39 -9
  37. package/lib/browser/navigation/navigation-location.js.map +1 -1
  38. package/package.json +4 -4
  39. package/src/browser/editor-command.ts +53 -4
  40. package/src/browser/editor-contribution.ts +29 -2
  41. package/src/browser/editor-keybinding.ts +9 -0
  42. package/src/browser/editor-manager.ts +104 -19
  43. package/src/browser/editor-navigation-contribution.ts +58 -2
  44. package/src/browser/editor-preferences.ts +6 -0
  45. package/src/browser/editor-widget-factory.ts +5 -2
  46. package/src/browser/navigation/navigation-location-service.spec.ts +98 -3
  47. package/src/browser/navigation/navigation-location-service.ts +51 -3
  48. package/src/browser/navigation/navigation-location.ts +55 -7
@@ -18,9 +18,9 @@ import { EditorManager } from './editor-manager';
18
18
  import { TextEditor } from './editor';
19
19
  import { injectable, inject } from '@theia/core/shared/inversify';
20
20
  import { StatusBarAlignment, StatusBar } from '@theia/core/lib/browser/status-bar/status-bar';
21
- import { FrontendApplicationContribution, DiffUris } from '@theia/core/lib/browser';
21
+ import { FrontendApplicationContribution, DiffUris, DockLayout } from '@theia/core/lib/browser';
22
22
  import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
23
- import { DisposableCollection } from '@theia/core';
23
+ import { CommandHandler, DisposableCollection } from '@theia/core';
24
24
  import { EditorCommands } from './editor-command';
25
25
  import { EditorQuickOpenService } from './editor-quick-open-service';
26
26
  import { CommandRegistry, CommandContribution } from '@theia/core/lib/common';
@@ -133,6 +133,25 @@ export class EditorContribution implements FrontendApplicationContribution, Comm
133
133
  commands.registerCommand(EditorCommands.SHOW_ALL_OPENED_EDITORS, {
134
134
  execute: () => this.editorQuickOpenService.open()
135
135
  });
136
+ const splitHandlerFactory = (splitMode: DockLayout.InsertMode): CommandHandler => ({
137
+ isEnabled: () => !!this.editorManager.currentEditor,
138
+ isVisible: () => !!this.editorManager.currentEditor,
139
+ execute: async () => {
140
+ const { currentEditor } = this.editorManager;
141
+ if (currentEditor) {
142
+ const selection = currentEditor.editor.selection;
143
+ const newEditor = await this.editorManager.openToSide(currentEditor.editor.uri, { selection, widgetOptions: { mode: splitMode } });
144
+ const oldEditorState = currentEditor.editor.storeViewState();
145
+ newEditor.editor.restoreViewState(oldEditorState);
146
+ }
147
+ }
148
+ });
149
+ commands.registerCommand(EditorCommands.SPLIT_EDITOR_HORIZONTAL, splitHandlerFactory('split-right'));
150
+ commands.registerCommand(EditorCommands.SPLIT_EDITOR_VERTICAL, splitHandlerFactory('split-bottom'));
151
+ commands.registerCommand(EditorCommands.SPLIT_EDITOR_RIGHT, splitHandlerFactory('split-right'));
152
+ commands.registerCommand(EditorCommands.SPLIT_EDITOR_DOWN, splitHandlerFactory('split-bottom'));
153
+ commands.registerCommand(EditorCommands.SPLIT_EDITOR_UP, splitHandlerFactory('split-top'));
154
+ commands.registerCommand(EditorCommands.SPLIT_EDITOR_LEFT, splitHandlerFactory('split-left'));
136
155
  }
137
156
 
138
157
  registerKeybindings(keybindings: KeybindingRegistry): void {
@@ -140,6 +159,14 @@ export class EditorContribution implements FrontendApplicationContribution, Comm
140
159
  command: EditorCommands.SHOW_ALL_OPENED_EDITORS.id,
141
160
  keybinding: 'ctrlcmd+k ctrlcmd+p'
142
161
  });
162
+ keybindings.registerKeybinding({
163
+ command: EditorCommands.SPLIT_EDITOR_HORIZONTAL.id,
164
+ keybinding: 'ctrlcmd+\\',
165
+ });
166
+ keybindings.registerKeybinding({
167
+ command: EditorCommands.SPLIT_EDITOR_VERTICAL.id,
168
+ keybinding: 'ctrlcmd+k ctrlcmd+\\',
169
+ });
143
170
  }
144
171
 
145
172
  registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void {
@@ -15,6 +15,7 @@
15
15
  ********************************************************************************/
16
16
 
17
17
  import { injectable } from '@theia/core/shared/inversify';
18
+ import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
18
19
  import { isOSX, isWindows } from '@theia/core/lib/common/os';
19
20
  import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
20
21
  import { EditorCommands } from './editor-command';
@@ -39,8 +40,16 @@ export class EditorKeybindingContribution implements KeybindingContribution {
39
40
  {
40
41
  command: EditorCommands.TOGGLE_WORD_WRAP.id,
41
42
  keybinding: 'alt+z'
43
+ },
44
+ {
45
+ command: EditorCommands.REOPEN_CLOSED_EDITOR.id,
46
+ keybinding: this.isElectron() ? 'ctrlcmd+shift+t' : 'alt+shift+t'
42
47
  }
43
48
  );
44
49
  }
45
50
 
51
+ private isElectron(): boolean {
52
+ return environment.electron.is();
53
+ }
54
+
46
55
  }
@@ -17,15 +17,21 @@
17
17
  import { injectable, postConstruct, inject } from '@theia/core/shared/inversify';
18
18
  import URI from '@theia/core/lib/common/uri';
19
19
  import { RecursivePartial, Emitter, Event } from '@theia/core/lib/common';
20
- import { WidgetOpenerOptions, NavigatableWidgetOpenHandler } from '@theia/core/lib/browser';
20
+ import { WidgetOpenerOptions, NavigatableWidgetOpenHandler, NavigatableWidgetOptions } from '@theia/core/lib/browser';
21
21
  import { EditorWidget } from './editor-widget';
22
22
  import { Range, Position, Location } from './editor';
23
23
  import { EditorWidgetFactory } from './editor-widget-factory';
24
24
  import { TextEditor } from './editor';
25
25
 
26
+ export interface WidgetId {
27
+ id: number;
28
+ uri: string;
29
+ }
30
+
26
31
  export interface EditorOpenerOptions extends WidgetOpenerOptions {
27
32
  selection?: RecursivePartial<Range>;
28
33
  preview?: boolean;
34
+ counter?: number
29
35
  }
30
36
 
31
37
  @injectable()
@@ -35,6 +41,8 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
35
41
 
36
42
  readonly label = 'Code Editor';
37
43
 
44
+ protected readonly editorCounters = new Map<string, number>();
45
+
38
46
  protected readonly onActiveEditorChangedEmitter = new Emitter<EditorWidget | undefined>();
39
47
  /**
40
48
  * Emit when the active editor is changed.
@@ -50,8 +58,8 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
50
58
  @postConstruct()
51
59
  protected init(): void {
52
60
  super.init();
53
- this.shell.activeChanged.connect(() => this.updateActiveEditor());
54
- this.shell.currentChanged.connect(() => this.updateCurrentEditor());
61
+ this.shell.onDidChangeActiveWidget(() => this.updateActiveEditor());
62
+ this.shell.onDidChangeCurrentWidget(() => this.updateCurrentEditor());
55
63
  this.onCreated(widget => {
56
64
  widget.onDidChangeVisibility(() => {
57
65
  if (widget.isVisible) {
@@ -61,7 +69,9 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
61
69
  }
62
70
  this.updateCurrentEditor();
63
71
  });
72
+ this.checkCounterForWidget(widget);
64
73
  widget.disposed.connect(() => {
74
+ this.removeFromCounter(widget);
65
75
  this.removeRecentlyVisible(widget);
66
76
  this.updateCurrentEditor();
67
77
  });
@@ -75,21 +85,30 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
75
85
  }
76
86
 
77
87
  async getByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget | undefined> {
78
- const widget = await super.getByUri(uri);
79
- if (widget) {
80
- // Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955)
81
- this.revealSelection(widget, options, uri);
82
- }
83
- return widget;
88
+ return this.getWidget(uri, options);
84
89
  }
85
90
 
86
- async getOrCreateByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
87
- const widget = await super.getOrCreateByUri(uri);
88
- if (widget) {
91
+ getOrCreateByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
92
+ return this.getOrCreateWidget(uri, options);
93
+ }
94
+
95
+ protected async getWidget(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget | undefined> {
96
+ const optionsWithCounter: EditorOpenerOptions = { counter: this.getCounterForUri(uri), ...options };
97
+ const editor = await super.getWidget(uri, optionsWithCounter);
98
+ if (editor) {
89
99
  // Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955)
90
- this.revealSelection(widget, options, uri);
100
+ this.revealSelection(editor, optionsWithCounter, uri);
91
101
  }
92
- return widget;
102
+ return editor;
103
+ }
104
+
105
+ protected async getOrCreateWidget(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
106
+ const counter = options?.counter === undefined ? this.getOrCreateCounterForUri(uri) : options.counter;
107
+ const optionsWithCounter: EditorOpenerOptions = { ...options, counter };
108
+ const editor = await super.getOrCreateWidget(uri, optionsWithCounter);
109
+ // Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955)
110
+ this.revealSelection(editor, options, uri);
111
+ return editor;
93
112
  }
94
113
 
95
114
  protected readonly recentlyVisibleIds: string[] = [];
@@ -154,14 +173,23 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
154
173
  return 100;
155
174
  }
156
175
 
157
- async open(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
158
- const editor = await this.getOrCreateByUri(uri, options);
159
- await super.open(uri, options);
160
- return editor;
176
+ // This override only serves to inform external callers that they can use EditorOpenerOptions.
177
+ open(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
178
+ return super.open(uri, options);
179
+ }
180
+
181
+ /**
182
+ * Opens an editor to the side of the current editor. Defaults to opening to the right.
183
+ * To modify direction, pass options with `{widgetOptions: {mode: ...}}`
184
+ */
185
+ openToSide(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
186
+ const counter = this.createCounterForUri(uri);
187
+ const splitOptions: EditorOpenerOptions = { widgetOptions: { mode: 'split-right' }, ...options, counter };
188
+ return this.open(uri, splitOptions);
161
189
  }
162
190
 
163
191
  protected revealSelection(widget: EditorWidget, input?: EditorOpenerOptions, uri?: URI): void {
164
- let inputSelection = input && input.selection;
192
+ let inputSelection = input?.selection;
165
193
  if (!inputSelection && uri) {
166
194
  const match = /^L?(\d+)(?:,(\d+))?/.exec(uri.fragment);
167
195
  if (match) {
@@ -207,6 +235,63 @@ export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
207
235
  };
208
236
  }
209
237
 
238
+ protected removeFromCounter(widget: EditorWidget): void {
239
+ const { id, uri } = this.extractIdFromWidget(widget);
240
+ if (uri && !Number.isNaN(id)) {
241
+ let max = -Infinity;
242
+ this.all.forEach(editor => {
243
+ const candidateID = this.extractIdFromWidget(editor);
244
+ if ((candidateID.uri === uri) && (candidateID.id > max)) {
245
+ max = candidateID.id!;
246
+ }
247
+ });
248
+
249
+ if (max > -Infinity) {
250
+ this.editorCounters.set(uri, max);
251
+ } else {
252
+ this.editorCounters.delete(uri);
253
+ }
254
+ }
255
+ }
256
+
257
+ protected extractIdFromWidget(widget: EditorWidget): WidgetId {
258
+ const uri = widget.editor.uri.toString();
259
+ const id = Number(widget.id.slice(widget.id.lastIndexOf(':') + 1));
260
+ return { id, uri };
261
+ }
262
+
263
+ protected checkCounterForWidget(widget: EditorWidget): void {
264
+ const { id, uri } = this.extractIdFromWidget(widget);
265
+ const numericalId = Number(id);
266
+ if (uri && !Number.isNaN(numericalId)) {
267
+ const highestKnownId = this.editorCounters.get(uri) ?? -Infinity;
268
+ if (numericalId > highestKnownId) {
269
+ this.editorCounters.set(uri, numericalId);
270
+ }
271
+ }
272
+ }
273
+
274
+ protected createCounterForUri(uri: URI): number {
275
+ const identifier = uri.toString();
276
+ const next = (this.editorCounters.get(identifier) ?? 0) + 1;
277
+ return next;
278
+ }
279
+
280
+ protected getCounterForUri(uri: URI): number | undefined {
281
+ return this.editorCounters.get(uri.toString());
282
+ }
283
+
284
+ protected getOrCreateCounterForUri(uri: URI): number {
285
+ return this.getCounterForUri(uri) ?? this.createCounterForUri(uri);
286
+ }
287
+
288
+ protected createWidgetOptions(uri: URI, options?: EditorOpenerOptions): NavigatableWidgetOptions {
289
+ const navigatableOptions = super.createWidgetOptions(uri, options);
290
+ if (options?.counter !== undefined) {
291
+ navigatableOptions.counter = options.counter;
292
+ }
293
+ return navigatableOptions;
294
+ }
210
295
  }
211
296
 
212
297
  /**
@@ -24,7 +24,7 @@ import { EditorCommands } from './editor-command';
24
24
  import { EditorWidget } from './editor-widget';
25
25
  import { EditorManager } from './editor-manager';
26
26
  import { TextEditor, Position, Range, TextDocumentChangeEvent } from './editor';
27
- import { NavigationLocation } from './navigation/navigation-location';
27
+ import { NavigationLocation, RecentlyClosedEditor } from './navigation/navigation-location';
28
28
  import { NavigationLocationService } from './navigation/navigation-location-service';
29
29
  import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser';
30
30
 
@@ -32,6 +32,7 @@ import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser';
32
32
  export class EditorNavigationContribution implements Disposable, FrontendApplicationContribution {
33
33
 
34
34
  private static ID = 'editor-navigation-contribution';
35
+ private static CLOSED_EDITORS_KEY = 'recently-closed-editors';
35
36
 
36
37
  protected readonly toDispose = new DisposableCollection();
37
38
  protected readonly toDisposePerCurrentEditor = new DisposableCollection();
@@ -59,7 +60,14 @@ export class EditorNavigationContribution implements Disposable, FrontendApplica
59
60
  this.toDispose.pushAll([
60
61
  // TODO listen on file resource changes, if a file gets deleted, remove the corresponding navigation locations (if any).
61
62
  // This would require introducing the FS dependency in the editor extension.
62
- this.editorManager.onCurrentEditorChanged(this.onCurrentEditorChanged.bind(this))
63
+ this.editorManager.onCurrentEditorChanged(this.onCurrentEditorChanged.bind(this)),
64
+ this.editorManager.onCreated(widget => {
65
+ this.locationStack.removeClosedEditor(widget.editor.uri);
66
+ widget.disposed.connect(() => this.locationStack.addClosedEditor({
67
+ uri: widget.editor.uri,
68
+ viewState: widget.editor.storeViewState()
69
+ }));
70
+ })
63
71
  ]);
64
72
  this.commandRegistry.registerHandler(EditorCommands.GO_BACK.id, {
65
73
  execute: () => this.locationStack.back(),
@@ -91,6 +99,28 @@ export class EditorNavigationContribution implements Disposable, FrontendApplica
91
99
  execute: () => this.toggleWordWrap(),
92
100
  isEnabled: () => true,
93
101
  });
102
+ this.commandRegistry.registerHandler(EditorCommands.REOPEN_CLOSED_EDITOR.id, {
103
+ execute: () => this.reopenLastClosedEditor()
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Reopens the last closed editor with its stored view state if possible from history.
109
+ * If the editor cannot be restored, continue to the next editor in history.
110
+ */
111
+ protected async reopenLastClosedEditor(): Promise<void> {
112
+ const lastClosedEditor = this.locationStack.getLastClosedEditor();
113
+ if (lastClosedEditor === undefined) {
114
+ return;
115
+ }
116
+
117
+ try {
118
+ const widget = await this.editorManager.open(lastClosedEditor.uri);
119
+ widget.editor.restoreViewState(lastClosedEditor.viewState);
120
+ } catch {
121
+ this.locationStack.removeClosedEditor(lastClosedEditor.uri);
122
+ this.reopenLastClosedEditor();
123
+ }
94
124
  }
95
125
 
96
126
  async onStart(): Promise<void> {
@@ -190,9 +220,17 @@ export class EditorNavigationContribution implements Disposable, FrontendApplica
190
220
  this.storageService.setData(EditorNavigationContribution.ID, {
191
221
  locations: this.locationStack.locations().map(NavigationLocation.toObject)
192
222
  });
223
+ this.storageService.setData(EditorNavigationContribution.CLOSED_EDITORS_KEY, {
224
+ closedEditors: this.shouldStoreClosedEditors() ? this.locationStack.closedEditorsStack.map(RecentlyClosedEditor.toObject) : []
225
+ });
193
226
  }
194
227
 
195
228
  protected async restoreState(): Promise<void> {
229
+ await this.restoreNavigationLocations();
230
+ await this.restoreClosedEditors();
231
+ }
232
+
233
+ protected async restoreNavigationLocations(): Promise<void> {
196
234
  const raw: { locations?: ArrayLike<object> } | undefined = await this.storageService.getData(EditorNavigationContribution.ID);
197
235
  if (raw && raw.locations) {
198
236
  const locations: NavigationLocation[] = [];
@@ -209,6 +247,20 @@ export class EditorNavigationContribution implements Disposable, FrontendApplica
209
247
  }
210
248
  }
211
249
 
250
+ protected async restoreClosedEditors(): Promise<void> {
251
+ const raw: { closedEditors?: ArrayLike<object> } | undefined = await this.storageService.getData(EditorNavigationContribution.CLOSED_EDITORS_KEY);
252
+ if (raw && raw.closedEditors) {
253
+ for (let i = 0; i < raw.closedEditors.length; i++) {
254
+ const editor = RecentlyClosedEditor.fromObject(raw.closedEditors[i]);
255
+ if (editor) {
256
+ this.locationStack.addClosedEditor(editor);
257
+ } else {
258
+ this.logger.warn('Could not restore the state of the closed editors stack.');
259
+ }
260
+ }
261
+ }
262
+ }
263
+
212
264
  private isMinimapEnabled(): boolean {
213
265
  return !!this.preferenceService.get('editor.minimap.enabled');
214
266
  }
@@ -218,4 +270,8 @@ export class EditorNavigationContribution implements Disposable, FrontendApplica
218
270
  return renderWhitespace === 'none' ? false : true;
219
271
  }
220
272
 
273
+ private shouldStoreClosedEditors(): boolean {
274
+ return !!this.preferenceService.get('editor.history.persistClosedEditors');
275
+ }
276
+
221
277
  }
@@ -1268,6 +1268,11 @@ export const editorPreferenceSchema: PreferenceSchema = {
1268
1268
  'default': 750,
1269
1269
  'description': 'Timeout in milliseconds after which the formatting that is run on file save is cancelled.'
1270
1270
  },
1271
+ 'editor.history.persistClosedEditors': {
1272
+ 'type': 'boolean',
1273
+ 'default': false,
1274
+ 'description': 'Controls whether to persist closed editor history for the workspace across window reloads.'
1275
+ },
1271
1276
  'files.eol': {
1272
1277
  'type': 'string',
1273
1278
  'enum': [
@@ -1299,6 +1304,7 @@ export interface EditorConfiguration extends CodeEditorConfiguration {
1299
1304
  'editor.autoSaveDelay': number
1300
1305
  'editor.formatOnSave': boolean
1301
1306
  'editor.formatOnSaveTimeout': number
1307
+ 'editor.history.persistClosedEditors': boolean
1302
1308
  'files.eol': EndOfLinePreference
1303
1309
  }
1304
1310
  export type EndOfLinePreference = '\n' | '\r\n' | 'auto';
@@ -39,10 +39,10 @@ export class EditorWidgetFactory implements WidgetFactory {
39
39
 
40
40
  createWidget(options: NavigatableWidgetOptions): Promise<EditorWidget> {
41
41
  const uri = new URI(options.uri);
42
- return this.createEditor(uri);
42
+ return this.createEditor(uri, options);
43
43
  }
44
44
 
45
- protected async createEditor(uri: URI): Promise<EditorWidget> {
45
+ protected async createEditor(uri: URI, options?: NavigatableWidgetOptions): Promise<EditorWidget> {
46
46
  const textEditor = await this.editorProvider(uri);
47
47
  const newEditor = new EditorWidget(textEditor, this.selectionService);
48
48
 
@@ -55,6 +55,9 @@ export class EditorWidgetFactory implements WidgetFactory {
55
55
  newEditor.onDispose(() => labelListener.dispose());
56
56
 
57
57
  newEditor.id = this.id + ':' + uri.toString();
58
+ if (options?.counter !== undefined) {
59
+ newEditor.id += `:${options.counter}`;
60
+ }
58
61
  newEditor.title.closable = true;
59
62
  return newEditor;
60
63
  }
@@ -19,6 +19,7 @@ let disableJSDOM = enableJSDOM();
19
19
 
20
20
  import { expect } from 'chai';
21
21
  import { Container } from '@theia/core/shared/inversify';
22
+ import URI from '@theia/core/lib/common/uri';
22
23
  import { ILogger } from '@theia/core/lib/common/logger';
23
24
  import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
24
25
  import { OpenerService } from '@theia/core/lib/browser/opener-service';
@@ -26,7 +27,7 @@ import { MockOpenerService } from '@theia/core/lib/browser/test/mock-opener-serv
26
27
  import { NavigationLocationUpdater } from './navigation-location-updater';
27
28
  import { NoopNavigationLocationUpdater } from './test/mock-navigation-location-updater';
28
29
  import { NavigationLocationSimilarity } from './navigation-location-similarity';
29
- import { CursorLocation, Position, NavigationLocation } from './navigation-location';
30
+ import { CursorLocation, Position, NavigationLocation, RecentlyClosedEditor } from './navigation-location';
30
31
  import { NavigationLocationService } from './navigation-location-service';
31
32
 
32
33
  disableJSDOM();
@@ -79,8 +80,28 @@ describe('navigation-location-service', () => {
79
80
  });
80
81
 
81
82
  it('should not exceed the max stack item', () => {
82
- stack.register(...[...Array(100).keys()].map(i => createCursorLocation({ line: i * 10, character: i }, `file://${i}`)));
83
- expect(stack.locations().length).to.be.lessThan(100);
83
+ const max = NavigationLocationService['MAX_STACK_ITEMS'];
84
+ const locations: NavigationLocation[] = [...Array(max + 10).keys()].map(i => createCursorLocation({ line: i * 10, character: i }, `file://${i}`));
85
+ stack.register(...locations);
86
+ expect(stack.locations().length).to.not.be.greaterThan(max);
87
+ });
88
+
89
+ it('should successfully clear the history', () => {
90
+ expect(stack['recentlyClosedEditors'].length).equal(0);
91
+ const editor = createMockClosedEditor(new URI('file://foo/a.ts'));
92
+ stack.addClosedEditor(editor);
93
+ expect(stack['recentlyClosedEditors'].length).equal(1);
94
+
95
+ expect(stack['stack'].length).equal(0);
96
+ stack.register(
97
+ createCursorLocation(),
98
+ );
99
+ expect(stack['stack'].length).equal(1);
100
+
101
+ stack['clearHistory']();
102
+
103
+ expect(stack['recentlyClosedEditors'].length).equal(0);
104
+ expect(stack['stack'].length).equal(0);
84
105
  });
85
106
 
86
107
  describe('last-edit-location', async () => {
@@ -132,6 +153,76 @@ describe('navigation-location-service', () => {
132
153
 
133
154
  });
134
155
 
156
+ describe('recently-closed-editors', () => {
157
+
158
+ describe('#getLastClosedEditor', () => {
159
+
160
+ it('should return the last closed editor from the history', () => {
161
+ const uri = new URI('file://foo/a.ts');
162
+ stack.addClosedEditor(createMockClosedEditor(uri));
163
+ const editor = stack.getLastClosedEditor();
164
+ expect(editor?.uri).equal(uri);
165
+ });
166
+
167
+ it('should return `undefined` when no history is found', () => {
168
+ expect(stack['recentlyClosedEditors'].length).equal(0);
169
+ const editor = stack.getLastClosedEditor();
170
+ expect(editor).equal(undefined);
171
+ });
172
+
173
+ it('should not exceed the max history', () => {
174
+ expect(stack['recentlyClosedEditors'].length).equal(0);
175
+ const max = NavigationLocationService['MAX_RECENTLY_CLOSED_EDITORS'];
176
+ for (let i = 0; i < max + 10; i++) {
177
+ const uri = new URI(`file://foo/bar-${i}.ts`);
178
+ stack.addClosedEditor(createMockClosedEditor(uri));
179
+ }
180
+ expect(stack['recentlyClosedEditors'].length <= max).be.true;
181
+ });
182
+
183
+ });
184
+
185
+ describe('#addToRecentlyClosedEditors', () => {
186
+
187
+ it('should include unique recently closed editors in the history', () => {
188
+ expect(stack['recentlyClosedEditors'].length).equal(0);
189
+ const a = createMockClosedEditor(new URI('file://foo/a.ts'));
190
+ const b = createMockClosedEditor(new URI('file://foo/b.ts'));
191
+ stack.addClosedEditor(a);
192
+ stack.addClosedEditor(b);
193
+ expect(stack['recentlyClosedEditors'].length).equal(2);
194
+ });
195
+
196
+ it('should not include duplicate recently closed editors in the history', () => {
197
+ const uri = new URI('file://foo/a.ts');
198
+ [1, 2, 3].forEach(i => {
199
+ stack.addClosedEditor(createMockClosedEditor(uri));
200
+ });
201
+ expect(stack['recentlyClosedEditors'].length).equal(1);
202
+ });
203
+
204
+ });
205
+
206
+ describe('#removeFromRecentlyClosedEditors', () => {
207
+
208
+ it('should successfully remove editors from the history that match the given editor uri', () => {
209
+ expect(stack['recentlyClosedEditors'].length).equal(0);
210
+ const editor = createMockClosedEditor(new URI('file://foo/a.ts'));
211
+
212
+ [1, 2, 3].forEach(() => {
213
+ stack['recentlyClosedEditors'].push(editor);
214
+ });
215
+ expect(stack['recentlyClosedEditors'].length).equal(3);
216
+
217
+ // Remove the given editor from the recently closed history.
218
+ stack['removeClosedEditor'](editor.uri);
219
+ expect(stack['recentlyClosedEditors'].length).equal(0);
220
+ });
221
+
222
+ });
223
+
224
+ });
225
+
135
226
  function createCursorLocation(context: Position = { line: 0, character: 0, }, uri: string = 'file://path/to/file'): CursorLocation {
136
227
  return NavigationLocation.create(uri, context);
137
228
  }
@@ -149,4 +240,8 @@ describe('navigation-location-service', () => {
149
240
  return container.get(NavigationLocationService);
150
241
  }
151
242
 
243
+ function createMockClosedEditor(uri: URI): RecentlyClosedEditor {
244
+ return { uri, viewState: {} };
245
+ }
246
+
152
247
  });
@@ -20,15 +20,18 @@ import { OpenerService, OpenerOptions, open } from '@theia/core/lib/browser/open
20
20
  import { EditorOpenerOptions } from '../editor-manager';
21
21
  import { NavigationLocationUpdater } from './navigation-location-updater';
22
22
  import { NavigationLocationSimilarity } from './navigation-location-similarity';
23
- import { NavigationLocation, Range, ContentChangeLocation } from './navigation-location';
23
+ import { NavigationLocation, Range, ContentChangeLocation, RecentlyClosedEditor } from './navigation-location';
24
+ import URI from '@theia/core/lib/common/uri';
24
25
 
25
26
  /**
26
- * The navigation location service. Also, stores and manages navigation locations.
27
+ * The navigation location service.
28
+ * It also stores and manages navigation locations and recently closed editors.
27
29
  */
28
30
  @injectable()
29
31
  export class NavigationLocationService {
30
32
 
31
33
  private static MAX_STACK_ITEMS = 30;
34
+ private static readonly MAX_RECENTLY_CLOSED_EDITORS = 20;
32
35
 
33
36
  @inject(ILogger)
34
37
  protected readonly logger: ILogger;
@@ -47,6 +50,8 @@ export class NavigationLocationService {
47
50
  protected canRegister = true;
48
51
  protected _lastEditLocation: ContentChangeLocation | undefined;
49
52
 
53
+ protected recentlyClosedEditors: RecentlyClosedEditor[] = [];
54
+
50
55
  /**
51
56
  * Registers the give locations into the service.
52
57
  */
@@ -165,12 +170,13 @@ export class NavigationLocationService {
165
170
  }
166
171
 
167
172
  /**
168
- * Clears the navigation history.
173
+ * Clears the total history.
169
174
  */
170
175
  clearHistory(): void {
171
176
  this.stack = [];
172
177
  this.pointer = -1;
173
178
  this._lastEditLocation = undefined;
179
+ this.recentlyClosedEditors = [];
174
180
  }
175
181
 
176
182
  /**
@@ -233,4 +239,46 @@ ${this.stack.map((location, i) => `${i}: ${JSON.stringify(NavigationLocation.toO
233
239
  ----- o -----`;
234
240
  }
235
241
 
242
+ /**
243
+ * Get the recently closed editors stack in chronological order.
244
+ *
245
+ * @returns readonly closed editors stack.
246
+ */
247
+ get closedEditorsStack(): ReadonlyArray<RecentlyClosedEditor> {
248
+ return this.recentlyClosedEditors;
249
+ }
250
+
251
+ /**
252
+ * Get the last recently closed editor.
253
+ *
254
+ * @returns the recently closed editor if it exists.
255
+ */
256
+ getLastClosedEditor(): RecentlyClosedEditor | undefined {
257
+ return this.recentlyClosedEditors[this.recentlyClosedEditors.length - 1];
258
+ }
259
+
260
+ /**
261
+ * Add the recently closed editor to the history.
262
+ *
263
+ * @param editor the recently closed editor.
264
+ */
265
+ addClosedEditor(editor: RecentlyClosedEditor): void {
266
+ this.removeClosedEditor(editor.uri);
267
+ this.recentlyClosedEditors.push(editor);
268
+
269
+ // Removes the oldest entry from the history if the maximum size is reached.
270
+ if (this.recentlyClosedEditors.length > NavigationLocationService.MAX_RECENTLY_CLOSED_EDITORS) {
271
+ this.recentlyClosedEditors.shift();
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Remove all occurrences of the given editor in the history if they exist.
277
+ *
278
+ * @param uri the uri of the editor that should be removed from the history.
279
+ */
280
+ removeClosedEditor(uri: URI): void {
281
+ this.recentlyClosedEditors = this.recentlyClosedEditors.filter(e => !uri.isEqual(e.uri));
282
+ }
283
+
236
284
  }