@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.
- package/lib/browser/editor-command.d.ts +19 -0
- package/lib/browser/editor-command.d.ts.map +1 -1
- package/lib/browser/editor-command.js +53 -4
- package/lib/browser/editor-command.js.map +1 -1
- package/lib/browser/editor-contribution.d.ts.map +1 -1
- package/lib/browser/editor-contribution.js +72 -0
- package/lib/browser/editor-contribution.js.map +1 -1
- package/lib/browser/editor-keybinding.d.ts +1 -0
- package/lib/browser/editor-keybinding.d.ts.map +1 -1
- package/lib/browser/editor-keybinding.js +7 -0
- package/lib/browser/editor-keybinding.js.map +1 -1
- package/lib/browser/editor-manager.d.ts +21 -1
- package/lib/browser/editor-manager.d.ts.map +1 -1
- package/lib/browser/editor-manager.js +111 -32
- package/lib/browser/editor-manager.js.map +1 -1
- package/lib/browser/editor-navigation-contribution.d.ts +9 -0
- package/lib/browser/editor-navigation-contribution.d.ts.map +1 -1
- package/lib/browser/editor-navigation-contribution.js +89 -1
- package/lib/browser/editor-navigation-contribution.js.map +1 -1
- package/lib/browser/editor-preferences.d.ts +1 -0
- package/lib/browser/editor-preferences.d.ts.map +1 -1
- package/lib/browser/editor-preferences.js +4 -0
- package/lib/browser/editor-preferences.js.map +1 -1
- package/lib/browser/editor-widget-factory.d.ts +1 -1
- package/lib/browser/editor-widget-factory.d.ts.map +1 -1
- package/lib/browser/editor-widget-factory.js +5 -2
- package/lib/browser/editor-widget-factory.js.map +1 -1
- package/lib/browser/navigation/navigation-location-service.d.ts +31 -3
- package/lib/browser/navigation/navigation-location-service.d.ts.map +1 -1
- package/lib/browser/navigation/navigation-location-service.js +47 -2
- package/lib/browser/navigation/navigation-location-service.js.map +1 -1
- package/lib/browser/navigation/navigation-location-service.spec.js +74 -2
- package/lib/browser/navigation/navigation-location-service.spec.js.map +1 -1
- package/lib/browser/navigation/navigation-location.d.ts +25 -0
- package/lib/browser/navigation/navigation-location.d.ts.map +1 -1
- package/lib/browser/navigation/navigation-location.js +39 -9
- package/lib/browser/navigation/navigation-location.js.map +1 -1
- package/package.json +4 -4
- package/src/browser/editor-command.ts +53 -4
- package/src/browser/editor-contribution.ts +29 -2
- package/src/browser/editor-keybinding.ts +9 -0
- package/src/browser/editor-manager.ts +104 -19
- package/src/browser/editor-navigation-contribution.ts +58 -2
- package/src/browser/editor-preferences.ts +6 -0
- package/src/browser/editor-widget-factory.ts +5 -2
- package/src/browser/navigation/navigation-location-service.spec.ts +98 -3
- package/src/browser/navigation/navigation-location-service.ts +51 -3
- 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.
|
|
54
|
-
this.shell.
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
100
|
+
this.revealSelection(editor, optionsWithCounter, uri);
|
|
91
101
|
}
|
|
92
|
-
return
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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.
|
|
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
|
|
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
|
}
|