|
1 |
| -import { inject, injectable } from '@theia/core/shared/inversify'; |
2 |
| -import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; |
| 1 | +import { WidgetDescription } from '@theia/core/lib/browser'; |
3 | 2 | import { ShellLayoutRestorer as TheiaShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
|
4 |
| -import { EditorManager } from '@theia/editor/lib/browser'; |
| 3 | +import { injectable } from '@theia/core/shared/inversify'; |
| 4 | +import { EditorPreviewWidgetFactory } from '@theia/editor-preview/lib/browser/editor-preview-widget-factory'; |
| 5 | +import { EditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory'; |
5 | 6 |
|
6 | 7 | @injectable()
|
7 | 8 | export class ShellLayoutRestorer extends TheiaShellLayoutRestorer {
|
8 |
| - // The editor manager is unused in the layout restorer. |
9 |
| - // We inject the editor manager to achieve better logging when filtering duplicate editor tabs. |
10 |
| - // Feel free to remove it in later IDE2 releases if the duplicate editor tab issues do not occur anymore. |
11 |
| - @inject(EditorManager) |
12 |
| - private readonly editorManager: EditorManager; |
13 |
| - |
14 |
| - // Workaround for https://github.com/eclipse-theia/theia/issues/6579. |
15 |
| - async storeLayoutAsync(app: FrontendApplication): Promise<void> { |
16 |
| - if (this.shouldStoreLayout) { |
17 |
| - try { |
18 |
| - this.logger.info('>>> Storing the layout...'); |
19 |
| - const layoutData = app.shell.getLayoutData(); |
20 |
| - const serializedLayoutData = this.deflate(layoutData); |
21 |
| - await this.storageService.setData( |
22 |
| - this.storageKey, |
23 |
| - serializedLayoutData |
24 |
| - ); |
25 |
| - this.logger.info('<<< The layout has been successfully stored.'); |
26 |
| - } catch (error) { |
27 |
| - await this.storageService.setData(this.storageKey, undefined); |
28 |
| - this.logger.error('Error during serialization of layout data', error); |
| 9 | + /** |
| 10 | + * Customized to filter out duplicate editor tabs. |
| 11 | + */ |
| 12 | + protected override parse<T>( |
| 13 | + layoutData: string, |
| 14 | + parseContext: TheiaShellLayoutRestorer.ParseContext |
| 15 | + ): T { |
| 16 | + return JSON.parse(layoutData, (property: string, value) => { |
| 17 | + if (this.isWidgetsProperty(property)) { |
| 18 | + const widgets = parseContext.filteredArray(); |
| 19 | + const descs = this.filterDescriptions(value); // <--- customization to filter out editor preview construction options. |
| 20 | + for (let i = 0; i < descs.length; i++) { |
| 21 | + parseContext.push(async (context) => { |
| 22 | + widgets[i] = await this.convertToWidget(descs[i], context); |
| 23 | + }); |
| 24 | + } |
| 25 | + return widgets; |
| 26 | + } else if (value && typeof value === 'object' && !Array.isArray(value)) { |
| 27 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 28 | + const copy: any = {}; |
| 29 | + for (const p in value) { |
| 30 | + if (this.isWidgetProperty(p)) { |
| 31 | + parseContext.push(async (context) => { |
| 32 | + copy[p] = await this.convertToWidget(value[p], context); |
| 33 | + }); |
| 34 | + } else { |
| 35 | + copy[p] = value[p]; |
| 36 | + } |
| 37 | + } |
| 38 | + return copy; |
29 | 39 | }
|
30 |
| - } |
| 40 | + return value; |
| 41 | + }); |
31 | 42 | }
|
32 | 43 |
|
33 |
| - override async restoreLayout(app: FrontendApplication): Promise<boolean> { |
34 |
| - this.logger.info('>>> Restoring the layout state...'); |
35 |
| - const serializedLayoutData = await this.storageService.getData<string>( |
36 |
| - this.storageKey |
37 |
| - ); |
38 |
| - if (serializedLayoutData === undefined) { |
39 |
| - this.logger.info('<<< Nothing to restore.'); |
40 |
| - return false; |
41 |
| - } |
42 |
| - |
43 |
| - const layoutData = await this.inflate(serializedLayoutData); |
44 |
| - // workaround to remove duplicated tabs |
45 |
| - console.log( |
46 |
| - '>>> Filtering persisted layout data to eliminate duplicate editor tabs...' |
47 |
| - ); |
48 |
| - const filesUri: string[] = []; |
49 |
| - if ((layoutData as any)?.mainPanel?.main?.widgets) { |
50 |
| - (layoutData as any).mainPanel.main.widgets = ( |
51 |
| - layoutData as any |
52 |
| - ).mainPanel.main.widgets.filter((widget: any) => { |
53 |
| - const uri = widget.getResourceUri().toString(); |
54 |
| - if (filesUri.includes(uri)) { |
55 |
| - console.log(`[SKIP]: Already visited editor URI: '${uri}'.`); |
56 |
| - return false; |
| 44 | + /** |
| 45 | + * Workaround to avoid duplicate editor tabs on IDE2 startup. |
| 46 | + * |
| 47 | + * This function filters all widget construction options with `editor-preview-widget` |
| 48 | + * factory ID if another option has the same URI and `code-editor-opener` factory ID. |
| 49 | + * In other words, if a resource is about to open in the Code editor, the same resource won't open as a preview widget. |
| 50 | + * |
| 51 | + * We do not know yet how the `editor-preview-widget` widget factory persisted in |
| 52 | + * local storage when storing the app layout on shutdown, but IDE2 restores the following JSON: |
| 53 | + * ```json |
| 54 | + * [ |
| 55 | + * { |
| 56 | + * "constructionOptions": { |
| 57 | + * "factoryId": "editor-preview-widget", |
| 58 | + * "options": { |
| 59 | + * "kind": "navigatable", |
| 60 | + * "uri": "file:///Users/a.kitta/Documents/Arduino/sketch_jun3b/sketch_jun3b.ino", |
| 61 | + * "counter": 1, |
| 62 | + * "preview": false |
| 63 | + * } |
| 64 | + * }, |
| 65 | + * "innerWidgetState": "{\"isPreview\":false,\"editorState\":{\"cursorState\":[{\"inSelectionMode\":false,\"selectionStart\":{\"lineNumber\":10,\"column\":1},\"position\":{\"lineNumber\":10,\"column\":1}}],\"viewState\":{\"scrollLeft\":0,\"firstPosition\":{\"lineNumber\":1,\"column\":1},\"firstPositionDeltaTop\":0},\"contributionsState\":{\"editor.contrib.folding\":{\"lineCount\":10,\"provider\":\"indent\",\"foldedImports\":false},\"editor.contrib.wordHighlighter\":false}}}" |
| 66 | + * }, |
| 67 | + * { |
| 68 | + * "constructionOptions": { |
| 69 | + * "factoryId": "code-editor-opener", |
| 70 | + * "options": { |
| 71 | + * "kind": "navigatable", |
| 72 | + * "uri": "file:///Users/a.kitta/Documents/Arduino/sketch_jun3b/sketch_jun3b.ino", |
| 73 | + * "counter": 0 |
| 74 | + * } |
| 75 | + * }, |
| 76 | + * "innerWidgetState": "{\"cursorState\":[{\"inSelectionMode\":false,\"selectionStart\":{\"lineNumber\":1,\"column\":1},\"position\":{\"lineNumber\":1,\"column\":1}}],\"viewState\":{\"scrollLeft\":0,\"firstPosition\":{\"lineNumber\":1,\"column\":1},\"firstPositionDeltaTop\":0},\"contributionsState\":{\"editor.contrib.folding\":{\"lineCount\":10,\"provider\":\"indent\",\"foldedImports\":false},\"editor.contrib.wordHighlighter\":false}}" |
| 77 | + * } |
| 78 | + * ] |
| 79 | + * ``` |
| 80 | + */ |
| 81 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 82 | + private filterDescriptions(value: any): WidgetDescription[] { |
| 83 | + const descriptions = value as WidgetDescription[]; |
| 84 | + const codeEditorUris = new Set<string>(); |
| 85 | + descriptions.forEach(({ constructionOptions }) => { |
| 86 | + const { options, factoryId } = constructionOptions; |
| 87 | + if (isResourceWidgetOptions(options)) { |
| 88 | + const { uri } = options; |
| 89 | + // resource about to open in code editor |
| 90 | + if (factoryId === EditorWidgetFactory.ID) { |
| 91 | + codeEditorUris.add(uri); |
57 | 92 | }
|
58 |
| - console.log(`[OK]: Visited editor URI: '${uri}'.`); |
59 |
| - filesUri.push(uri); |
60 |
| - return true; |
61 |
| - }); |
62 |
| - } |
63 |
| - console.log('<<< Filtered the layout data before restoration.'); |
64 |
| - |
65 |
| - await app.shell.setLayoutData(layoutData); |
66 |
| - const allOpenedEditors = this.editorManager.all; |
67 |
| - // If any editor was visited during the layout data filtering, |
68 |
| - // but the editor manager does not know about opened editors, then |
69 |
| - // the IDE2 will show duplicate editors. |
70 |
| - if (filesUri.length && !allOpenedEditors.length) { |
71 |
| - console.warn( |
72 |
| - 'Inconsistency detected between the editor manager and the restored layout data. Editors were detected to be open in the layout data from the previous session, but the editor manager does not know about the opened editor.' |
73 |
| - ); |
74 |
| - } |
75 |
| - this.logger.info('<<< The layout has been successfully restored.'); |
76 |
| - return true; |
| 93 | + } |
| 94 | + }); |
| 95 | + return descriptions.filter(({ constructionOptions }) => { |
| 96 | + const { options, factoryId } = constructionOptions; |
| 97 | + if (factoryId === EditorPreviewWidgetFactory.ID) { |
| 98 | + // resource about to open in preview editor |
| 99 | + if (isResourceWidgetOptions(options)) { |
| 100 | + const { uri } = options; |
| 101 | + // if the resource is about to open in the code editor, do not open the resource in a preview widget too. |
| 102 | + if (codeEditorUris.has(uri)) { |
| 103 | + console.log( |
| 104 | + `Filtered a widget construction options to avoid duplicate editor tab. URI: ${options.uri}, factory ID: ${factoryId}.` |
| 105 | + ); |
| 106 | + return false; |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + return true; |
| 111 | + }); |
77 | 112 | }
|
78 | 113 | }
|
| 114 | + |
| 115 | +// eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 116 | +function isResourceWidgetOptions(options: any): options is { uri: string } { |
| 117 | + return !!options && 'uri' in options && typeof options.uri === 'string'; |
| 118 | +} |
0 commit comments