@theia/plugin-ext 1.76.0-next.7 → 1.77.0-next.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/common/paths-util.d.ts.map +1 -1
- package/lib/common/paths-util.js +9 -2
- package/lib/common/paths-util.js.map +1 -1
- package/lib/common/paths-util.spec.d.ts +2 -0
- package/lib/common/paths-util.spec.d.ts.map +1 -0
- package/lib/common/paths-util.spec.js +42 -0
- package/lib/common/paths-util.spec.js.map +1 -0
- package/lib/common/plugin-api-rpc.d.ts +14 -0
- package/lib/common/plugin-api-rpc.d.ts.map +1 -1
- package/lib/common/plugin-api-rpc.js +1 -1
- package/lib/common/plugin-api-rpc.js.map +1 -1
- package/lib/common/plugin-protocol.d.ts +2 -2
- package/lib/common/plugin-protocol.d.ts.map +1 -1
- package/lib/common/plugin-protocol.js +2 -1
- package/lib/common/plugin-protocol.js.map +1 -1
- package/lib/hosted/browser/hosted-plugin.d.ts +2 -0
- package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
- package/lib/hosted/browser/hosted-plugin.js +9 -3
- package/lib/hosted/browser/hosted-plugin.js.map +1 -1
- package/lib/hosted/browser/worker/worker-main.js +22 -12
- package/lib/hosted/browser/worker/worker-main.js.map +1 -1
- package/lib/hosted/browser/worker/worker-plugin-module.d.ts.map +1 -1
- package/lib/hosted/browser/worker/worker-plugin-module.js +2 -0
- package/lib/hosted/browser/worker/worker-plugin-module.js.map +1 -1
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.d.ts +37 -0
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.js +107 -0
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.js.map +1 -0
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.d.ts +2 -0
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.js +106 -0
- package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.js.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.d.ts +38 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.js +124 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.js.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.d.ts +2 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.js +137 -0
- package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.js.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.d.ts +43 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.js +145 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.js.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.d.ts +2 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.js +165 -0
- package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.js.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.d.ts +54 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.js +155 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.js.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.spec.d.ts +2 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.spec.d.ts.map +1 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.spec.js +190 -0
- package/lib/hosted/browser-only/browser-only-plugin-server.spec.js.map +1 -0
- package/lib/hosted/browser-only/test/navigator-locks-test-util.d.ts +24 -0
- package/lib/hosted/browser-only/test/navigator-locks-test-util.d.ts.map +1 -0
- package/lib/hosted/browser-only/test/navigator-locks-test-util.js +46 -0
- package/lib/hosted/browser-only/test/navigator-locks-test-util.js.map +1 -0
- package/lib/hosted/common/hosted-plugin.d.ts +2 -2
- package/lib/hosted/common/hosted-plugin.d.ts.map +1 -1
- package/lib/hosted/common/hosted-plugin.js +2 -2
- package/lib/hosted/common/hosted-plugin.js.map +1 -1
- package/lib/hosted/node/plugin-host-module.d.ts.map +1 -1
- package/lib/hosted/node/plugin-host-module.js +2 -0
- package/lib/hosted/node/plugin-host-module.js.map +1 -1
- package/lib/hosted/node/plugin-host-rpc.d.ts +3 -0
- package/lib/hosted/node/plugin-host-rpc.d.ts.map +1 -1
- package/lib/hosted/node/plugin-host-rpc.js +9 -3
- package/lib/hosted/node/plugin-host-rpc.js.map +1 -1
- package/lib/main/browser/editors-and-documents-main.d.ts +7 -0
- package/lib/main/browser/editors-and-documents-main.d.ts.map +1 -1
- package/lib/main/browser/editors-and-documents-main.js +19 -9
- package/lib/main/browser/editors-and-documents-main.js.map +1 -1
- package/lib/main/browser/editors-and-documents-main.spec.d.ts +2 -0
- package/lib/main/browser/editors-and-documents-main.spec.d.ts.map +1 -0
- package/lib/main/browser/editors-and-documents-main.spec.js +117 -0
- package/lib/main/browser/editors-and-documents-main.spec.js.map +1 -0
- package/lib/main/browser/languages-main.d.ts +7 -0
- package/lib/main/browser/languages-main.d.ts.map +1 -1
- package/lib/main/browser/languages-main.js +31 -9
- package/lib/main/browser/languages-main.js.map +1 -1
- package/lib/main/browser/languages-main.spec.d.ts +2 -0
- package/lib/main/browser/languages-main.spec.d.ts.map +1 -0
- package/lib/main/browser/languages-main.spec.js +82 -0
- package/lib/main/browser/languages-main.spec.js.map +1 -0
- package/lib/main/browser/main-context.d.ts.map +1 -1
- package/lib/main/browser/main-context.js +3 -0
- package/lib/main/browser/main-context.js.map +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts +5 -0
- package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts.map +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.js +5 -0
- package/lib/main/browser/menus/vscode-theia-menu-mappings.js.map +1 -1
- package/lib/main/browser/plugin-ext-frontend-module.d.ts.map +1 -1
- package/lib/main/browser/plugin-ext-frontend-module.js +1 -0
- package/lib/main/browser/plugin-ext-frontend-module.js.map +1 -1
- package/lib/main/browser/plugin-icon-service.d.ts +6 -0
- package/lib/main/browser/plugin-icon-service.d.ts.map +1 -1
- package/lib/main/browser/plugin-icon-service.js +14 -2
- package/lib/main/browser/plugin-icon-service.js.map +1 -1
- package/lib/main/browser/plugin-icon-service.spec.d.ts +2 -0
- package/lib/main/browser/plugin-icon-service.spec.d.ts.map +1 -0
- package/lib/main/browser/plugin-icon-service.spec.js +83 -0
- package/lib/main/browser/plugin-icon-service.spec.js.map +1 -0
- package/lib/main/browser/plugin-icon-theme-service.d.ts.map +1 -1
- package/lib/main/browser/plugin-icon-theme-service.js +2 -1
- package/lib/main/browser/plugin-icon-theme-service.js.map +1 -1
- package/lib/main/browser/plugin-icon-theme-service.spec.d.ts +2 -0
- package/lib/main/browser/plugin-icon-theme-service.spec.d.ts.map +1 -0
- package/lib/main/browser/plugin-icon-theme-service.spec.js +91 -0
- package/lib/main/browser/plugin-icon-theme-service.spec.js.map +1 -0
- package/lib/main/browser/telemetry-main.d.ts +20 -0
- package/lib/main/browser/telemetry-main.d.ts.map +1 -0
- package/lib/main/browser/telemetry-main.js +43 -0
- package/lib/main/browser/telemetry-main.js.map +1 -0
- package/lib/main/browser/telemetry-main.spec.d.ts +2 -0
- package/lib/main/browser/telemetry-main.spec.d.ts.map +1 -0
- package/lib/main/browser/telemetry-main.spec.js +66 -0
- package/lib/main/browser/telemetry-main.spec.js.map +1 -0
- package/lib/main/browser/webview/webview-context-keys.d.ts +3 -2
- package/lib/main/browser/webview/webview-context-keys.d.ts.map +1 -1
- package/lib/main/browser/webview/webview-context-keys.js +16 -2
- package/lib/main/browser/webview/webview-context-keys.js.map +1 -1
- package/lib/main/browser/webview/webview-environment.d.ts.map +1 -1
- package/lib/main/browser/webview/webview-environment.js +2 -1
- package/lib/main/browser/webview/webview-environment.js.map +1 -1
- package/lib/main/common/paths/const.d.ts +13 -0
- package/lib/main/common/paths/const.d.ts.map +1 -0
- package/lib/main/common/paths/const.js +32 -0
- package/lib/main/common/paths/const.js.map +1 -0
- package/lib/main/node/paths/const.d.ts +5 -5
- package/lib/main/node/paths/const.d.ts.map +1 -1
- package/lib/main/node/paths/const.js +6 -6
- package/lib/main/node/paths/const.js.map +1 -1
- package/lib/main/node/paths/plugin-paths-service.js +1 -1
- package/lib/main/node/paths/plugin-paths-service.js.map +1 -1
- package/lib/main/node/plugin-cli-contribution.d.ts +2 -2
- package/lib/main/node/plugin-cli-contribution.d.ts.map +1 -1
- package/lib/main/node/plugin-cli-contribution.js +4 -3
- package/lib/main/node/plugin-cli-contribution.js.map +1 -1
- package/lib/main/node/plugins-key-value-storage.js +1 -1
- package/lib/main/node/plugins-key-value-storage.js.map +1 -1
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.d.ts +5 -0
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.d.ts.map +1 -1
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.js +12 -1
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.js.map +1 -1
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.d.ts +2 -0
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.d.ts.map +1 -0
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.js +54 -0
- package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.js.map +1 -0
- package/lib/plugin/plugin-context.d.ts +2 -1
- package/lib/plugin/plugin-context.d.ts.map +1 -1
- package/lib/plugin/plugin-context.js +2 -3
- package/lib/plugin/plugin-context.js.map +1 -1
- package/lib/plugin/plugin-manager.d.ts +2 -0
- package/lib/plugin/plugin-manager.d.ts.map +1 -1
- package/lib/plugin/plugin-manager.js +12 -1
- package/lib/plugin/plugin-manager.js.map +1 -1
- package/lib/plugin/relative-pattern.spec.d.ts +2 -0
- package/lib/plugin/relative-pattern.spec.d.ts.map +1 -0
- package/lib/plugin/relative-pattern.spec.js +51 -0
- package/lib/plugin/relative-pattern.spec.js.map +1 -0
- package/lib/plugin/telemetry-ext.d.ts +23 -7
- package/lib/plugin/telemetry-ext.d.ts.map +1 -1
- package/lib/plugin/telemetry-ext.js +60 -33
- package/lib/plugin/telemetry-ext.js.map +1 -1
- package/lib/plugin/telemetry-ext.spec.js +121 -16
- package/lib/plugin/telemetry-ext.spec.js.map +1 -1
- package/lib/plugin-ext-frontend-only-module.d.ts +4 -0
- package/lib/plugin-ext-frontend-only-module.d.ts.map +1 -0
- package/lib/plugin-ext-frontend-only-module.js +34 -0
- package/lib/plugin-ext-frontend-only-module.js.map +1 -0
- package/package.json +36 -32
- package/src/common/paths-util.spec.ts +47 -0
- package/src/common/paths-util.ts +11 -2
- package/src/common/plugin-api-rpc.ts +15 -1
- package/src/common/plugin-protocol.ts +2 -0
- package/src/hosted/browser/hosted-plugin.ts +9 -4
- package/src/hosted/browser/worker/worker-main.ts +23 -12
- package/src/hosted/browser/worker/worker-plugin-module.ts +2 -0
- package/src/hosted/browser-only/browser-only-hosted-plugin-server.spec.ts +133 -0
- package/src/hosted/browser-only/browser-only-hosted-plugin-server.ts +122 -0
- package/src/hosted/browser-only/browser-only-plugin-file-system-provider.spec.ts +178 -0
- package/src/hosted/browser-only/browser-only-plugin-file-system-provider.ts +129 -0
- package/src/hosted/browser-only/browser-only-plugin-paths-service.spec.ts +199 -0
- package/src/hosted/browser-only/browser-only-plugin-paths-service.ts +145 -0
- package/src/hosted/browser-only/browser-only-plugin-server.spec.ts +228 -0
- package/src/hosted/browser-only/browser-only-plugin-server.ts +165 -0
- package/src/hosted/browser-only/test/navigator-locks-test-util.ts +54 -0
- package/src/hosted/common/hosted-plugin.ts +4 -4
- package/src/hosted/node/plugin-host-module.ts +2 -0
- package/src/hosted/node/plugin-host-rpc.ts +10 -4
- package/src/main/browser/editors-and-documents-main.spec.ts +140 -0
- package/src/main/browser/editors-and-documents-main.ts +19 -7
- package/src/main/browser/languages-main.spec.ts +101 -0
- package/src/main/browser/languages-main.ts +31 -9
- package/src/main/browser/main-context.ts +4 -0
- package/src/main/browser/menus/vscode-theia-menu-mappings.ts +5 -0
- package/src/main/browser/plugin-ext-frontend-module.ts +2 -1
- package/src/main/browser/plugin-icon-service.spec.ts +101 -0
- package/src/main/browser/plugin-icon-service.ts +16 -3
- package/src/main/browser/plugin-icon-theme-service.spec.ts +103 -0
- package/src/main/browser/plugin-icon-theme-service.ts +2 -1
- package/src/main/browser/telemetry-main.spec.ts +77 -0
- package/src/main/browser/telemetry-main.ts +46 -0
- package/src/main/browser/webview/webview-context-keys.ts +20 -4
- package/src/main/browser/webview/webview-environment.ts +2 -1
- package/src/main/common/paths/const.ts +28 -0
- package/src/main/node/paths/const.ts +5 -5
- package/src/main/node/paths/plugin-paths-service.ts +1 -1
- package/src/main/node/plugin-cli-contribution.ts +4 -3
- package/src/main/node/plugins-key-value-storage.ts +1 -1
- package/src/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.ts +61 -0
- package/src/main/node/resolvers/local-directory-plugin-deployer-resolver.ts +14 -1
- package/src/plugin/plugin-context.ts +3 -2
- package/src/plugin/plugin-manager.ts +11 -1
- package/src/plugin/relative-pattern.spec.ts +55 -0
- package/src/plugin/telemetry-ext.spec.ts +158 -17
- package/src/plugin/telemetry-ext.ts +63 -33
- package/src/plugin-ext-frontend-only-module.ts +34 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 JuliaHub, Inc. and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
|
|
20
|
+
import { expect } from 'chai';
|
|
21
|
+
import { Emitter, URI } from '@theia/core';
|
|
22
|
+
import { BaseWidget, Navigatable, Saveable, SaveableSource, Widget } from '@theia/core/lib/browser';
|
|
23
|
+
import { SaveableService } from '@theia/core/lib/browser/saveable-service';
|
|
24
|
+
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
|
25
|
+
import { EditorsAndDocumentsMain } from './editors-and-documents-main';
|
|
26
|
+
|
|
27
|
+
disableJSDOM();
|
|
28
|
+
|
|
29
|
+
class TestSaveable implements Saveable {
|
|
30
|
+
dirty = true;
|
|
31
|
+
readonly onDirtyChanged = new Emitter<void>().event;
|
|
32
|
+
readonly onContentChanged = new Emitter<void>().event;
|
|
33
|
+
saveCount = 0;
|
|
34
|
+
|
|
35
|
+
async save(): Promise<void> {
|
|
36
|
+
this.saveCount++;
|
|
37
|
+
this.dirty = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Navigatable to the resource, but with nothing to save. */
|
|
42
|
+
class TestNavigatableWidget extends BaseWidget implements Navigatable {
|
|
43
|
+
constructor(private readonly uri: URI) {
|
|
44
|
+
super();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getResourceUri(): URI {
|
|
48
|
+
return this.uri;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
createMoveToUri(): URI {
|
|
52
|
+
return this.uri;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Stands in for a `CustomEditorWidget`: saveable and navigatable, but not an `EditorWidget`. */
|
|
57
|
+
class TestSaveableWidget extends TestNavigatableWidget implements SaveableSource {
|
|
58
|
+
readonly saveable = new TestSaveable();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createEditorsAndDocumentsMain(
|
|
62
|
+
editor: EditorWidget | undefined,
|
|
63
|
+
widgets: Widget[]
|
|
64
|
+
): EditorsAndDocumentsMain {
|
|
65
|
+
// Bypass the constructor's RPC/container wiring: `save` and `saveAs` only touch the
|
|
66
|
+
// editor manager, the shell and the saveable service.
|
|
67
|
+
const main = Object.create(EditorsAndDocumentsMain.prototype) as EditorsAndDocumentsMain;
|
|
68
|
+
const fields = main as unknown as Record<string, unknown>;
|
|
69
|
+
fields.editorManager = { getByUri: async () => editor } as unknown as EditorManager;
|
|
70
|
+
fields.shell = { widgets };
|
|
71
|
+
fields.saveResourceService = new SaveableService();
|
|
72
|
+
return main;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('EditorsAndDocumentsMain#save', () => {
|
|
76
|
+
|
|
77
|
+
before(() => {
|
|
78
|
+
disableJSDOM = enableJSDOM();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
after(() => {
|
|
82
|
+
disableJSDOM();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const uri = new URI('custom-editor:/some/resource?viewType=test');
|
|
86
|
+
|
|
87
|
+
it('saves a saveable widget that the editor manager does not know', async () => {
|
|
88
|
+
const widget = new TestSaveableWidget(uri);
|
|
89
|
+
const main = createEditorsAndDocumentsMain(undefined, [widget]);
|
|
90
|
+
|
|
91
|
+
const saved = await main.save(uri);
|
|
92
|
+
|
|
93
|
+
expect(widget.saveable.saveCount).to.equal(1);
|
|
94
|
+
expect(saved?.toString()).to.equal(uri.toString());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('prefers the text editor when the editor manager has one', async () => {
|
|
98
|
+
const editor = new TestSaveableWidget(uri);
|
|
99
|
+
const custom = new TestSaveableWidget(uri);
|
|
100
|
+
const main = createEditorsAndDocumentsMain(editor as unknown as EditorWidget, [custom]);
|
|
101
|
+
|
|
102
|
+
await main.save(uri);
|
|
103
|
+
|
|
104
|
+
expect(editor.saveable.saveCount).to.equal(1);
|
|
105
|
+
expect(custom.saveable.saveCount).to.equal(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('leaves widgets bound to another resource alone', async () => {
|
|
109
|
+
const other = new TestSaveableWidget(new URI('custom-editor:/other/resource'));
|
|
110
|
+
const main = createEditorsAndDocumentsMain(undefined, [other]);
|
|
111
|
+
|
|
112
|
+
const saved = await main.save(uri);
|
|
113
|
+
|
|
114
|
+
expect(other.saveable.saveCount).to.equal(0);
|
|
115
|
+
expect(saved).to.be.undefined;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('ignores a widget that is navigatable to the resource but not saveable', async () => {
|
|
119
|
+
const main = createEditorsAndDocumentsMain(undefined, [new TestNavigatableWidget(uri)]);
|
|
120
|
+
|
|
121
|
+
expect(await main.save(uri)).to.be.undefined;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('resolves saveAs through the same fallback', async () => {
|
|
125
|
+
const widget = new TestSaveableWidget(uri);
|
|
126
|
+
const main = createEditorsAndDocumentsMain(undefined, [widget]);
|
|
127
|
+
const saveAsTargets: Widget[] = [];
|
|
128
|
+
// `SaveableService.canSaveAs` is unconditionally false on the base class, so the
|
|
129
|
+
// service is stubbed to reach the `saveAs` branch at all.
|
|
130
|
+
(main as unknown as Record<string, unknown>).saveResourceService = {
|
|
131
|
+
canSaveAs: (candidate: Widget) => candidate === widget,
|
|
132
|
+
saveAs: async (target: Widget) => { saveAsTargets.push(target); return uri; }
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const saved = await main.saveAs(uri);
|
|
136
|
+
|
|
137
|
+
expect(saveAsTargets).to.deep.equal([widget]);
|
|
138
|
+
expect(saved?.toString()).to.equal(uri.toString());
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -33,6 +33,7 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
|
|
33
33
|
import { TextEditorMain } from './text-editor-main';
|
|
34
34
|
import { DisposableCollection, Emitter, URI } from '@theia/core';
|
|
35
35
|
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
|
36
|
+
import { ApplicationShell, NavigatableWidget, Saveable, Widget } from '@theia/core/lib/browser';
|
|
36
37
|
import { SaveableService } from '@theia/core/lib/browser/saveable-service';
|
|
37
38
|
import { TabsMainImpl } from './tabs/tabs-main';
|
|
38
39
|
import { NotebookCellEditorService, NotebookEditorWidgetService } from '@theia/notebook/lib/browser';
|
|
@@ -48,6 +49,7 @@ export class EditorsAndDocumentsMain implements Disposable {
|
|
|
48
49
|
|
|
49
50
|
private readonly modelService: EditorModelService;
|
|
50
51
|
private readonly editorManager: EditorManager;
|
|
52
|
+
private readonly shell: ApplicationShell;
|
|
51
53
|
private readonly saveResourceService: SaveableService;
|
|
52
54
|
private readonly encodingRegistry: EncodingRegistry;
|
|
53
55
|
|
|
@@ -69,6 +71,7 @@ export class EditorsAndDocumentsMain implements Disposable {
|
|
|
69
71
|
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT);
|
|
70
72
|
|
|
71
73
|
this.editorManager = container.get(EditorManager);
|
|
74
|
+
this.shell = container.get(ApplicationShell);
|
|
72
75
|
this.modelService = container.get(EditorModelService);
|
|
73
76
|
this.saveResourceService = container.get(SaveableService);
|
|
74
77
|
this.encodingRegistry = container.get(EncodingRegistry);
|
|
@@ -183,22 +186,31 @@ export class EditorsAndDocumentsMain implements Disposable {
|
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
async save(uri: URI): Promise<URI | undefined> {
|
|
186
|
-
const
|
|
187
|
-
if (!
|
|
189
|
+
const widget = await this.getSaveTarget(uri);
|
|
190
|
+
if (!widget) {
|
|
188
191
|
return undefined;
|
|
189
192
|
}
|
|
190
|
-
return this.saveResourceService.save(
|
|
193
|
+
return this.saveResourceService.save(widget);
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
async saveAs(uri: URI): Promise<URI | undefined> {
|
|
194
|
-
const
|
|
195
|
-
if (!
|
|
197
|
+
const widget = await this.getSaveTarget(uri);
|
|
198
|
+
if (!widget) {
|
|
196
199
|
return undefined;
|
|
197
200
|
}
|
|
198
|
-
if (!this.saveResourceService.canSaveAs(
|
|
201
|
+
if (!this.saveResourceService.canSaveAs(widget)) {
|
|
199
202
|
return undefined;
|
|
200
203
|
}
|
|
201
|
-
return this.saveResourceService.saveAs(
|
|
204
|
+
return this.saveResourceService.saveAs(widget);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the widget holding `uri`, preferring a text editor. Custom editors and notebooks
|
|
209
|
+
* are not opened by the `EditorManager`, so they are only reachable through the shell.
|
|
210
|
+
*/
|
|
211
|
+
protected async getSaveTarget(uri: URI): Promise<Widget | undefined> {
|
|
212
|
+
return await this.editorManager.getByUri(uri)
|
|
213
|
+
?? this.shell.widgets.find(widget => Saveable.get(widget) && NavigatableWidget.getUri(widget)?.isEqual(uri));
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
saveAll(includeUntitled?: boolean): Promise<boolean> {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 JuliaHub, Inc. and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
// `languages-main` transitively pulls in Monaco and several frontend modules that touch `document`
|
|
19
|
+
// at module-load time.
|
|
20
|
+
const disableJSDOM = enableJSDOM();
|
|
21
|
+
import { expect } from 'chai';
|
|
22
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
23
|
+
import { mainWindow } from '@theia/monaco-editor-core/esm/vs/base/browser/window';
|
|
24
|
+
import { URI } from '@theia/core/lib/common/uri';
|
|
25
|
+
import { expectThrowsAsync } from '@theia/core/lib/common/test/expect';
|
|
26
|
+
import { CellEditType, CellUri } from '@theia/notebook/lib/common';
|
|
27
|
+
import { CellEditOperation } from '@theia/notebook/lib/browser/notebook-types';
|
|
28
|
+
import type { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
|
|
29
|
+
import { LanguagesMainImpl } from './languages-main';
|
|
30
|
+
|
|
31
|
+
after(() => disableJSDOM());
|
|
32
|
+
|
|
33
|
+
const notebookUri = new URI('file:///notebook.ipynb');
|
|
34
|
+
const cellHandle = 3;
|
|
35
|
+
const cellComponents = CellUri.generate(notebookUri, cellHandle).toComponents();
|
|
36
|
+
|
|
37
|
+
type FakeNotebookModel = Pick<NotebookModel, 'getCellIndexByHandle' | 'applyEdits'> & {
|
|
38
|
+
edits: CellEditOperation[][];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function createNotebookModel(handles: number[]): FakeNotebookModel {
|
|
42
|
+
const edits: CellEditOperation[][] = [];
|
|
43
|
+
return {
|
|
44
|
+
edits,
|
|
45
|
+
getCellIndexByHandle: handle => handles.indexOf(handle),
|
|
46
|
+
applyEdits: applied => { edits.push(applied); }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Bypasses the constructor's RPC/container wiring.
|
|
52
|
+
*/
|
|
53
|
+
function createLanguagesMain(notebook?: FakeNotebookModel): LanguagesMainImpl {
|
|
54
|
+
const languagesMain = Object.create(LanguagesMainImpl.prototype) as LanguagesMainImpl;
|
|
55
|
+
(languagesMain as unknown as Record<string, unknown>).notebookService = {
|
|
56
|
+
getNotebookEditorModel: (uri: URI) => uri.toString() === notebookUri.toString() ? notebook : undefined
|
|
57
|
+
};
|
|
58
|
+
return languagesMain;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('LanguagesMainImpl#$changeLanguage', () => {
|
|
62
|
+
|
|
63
|
+
before(() => {
|
|
64
|
+
// Resolving a language id boots Monaco's standalone services, whose theme service reaches for
|
|
65
|
+
// DOM APIs JSDOM does not provide. Monaco captures its window at module load.
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
(global as any).CSS ??= { escape: (value: string) => value };
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
(mainWindow as any).matchMedia ??= () => ({ matches: false, addEventListener: () => { }, removeEventListener: () => { } });
|
|
70
|
+
monaco.languages.register({ id: 'julia' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('applies a cell language edit for a notebook cell', async () => {
|
|
74
|
+
const notebook = createNotebookModel([1, 2, cellHandle]);
|
|
75
|
+
const languagesMain = createLanguagesMain(notebook);
|
|
76
|
+
|
|
77
|
+
await languagesMain.$changeLanguage(cellComponents, 'julia');
|
|
78
|
+
|
|
79
|
+
expect(notebook.edits).to.deep.equal([[{ editType: CellEditType.CellLanguage, index: 2, language: 'julia' }]]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects for a cell of a notebook that is not open', async () => {
|
|
83
|
+
const languagesMain = createLanguagesMain();
|
|
84
|
+
|
|
85
|
+
await expectThrowsAsync(languagesMain.$changeLanguage(cellComponents, 'julia'), 'Invalid uri');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rejects for a cell handle the notebook does not know', async () => {
|
|
89
|
+
const languagesMain = createLanguagesMain(createNotebookModel([1, 2]));
|
|
90
|
+
|
|
91
|
+
await expectThrowsAsync(languagesMain.$changeLanguage(cellComponents, 'julia'), 'Invalid uri');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects an unknown language id before touching the notebook', async () => {
|
|
95
|
+
const notebook = createNotebookModel([cellHandle]);
|
|
96
|
+
const languagesMain = createLanguagesMain(notebook);
|
|
97
|
+
|
|
98
|
+
await expectThrowsAsync(languagesMain.$changeLanguage(cellComponents, 'no-such-language'), /Unknown language ID/);
|
|
99
|
+
expect(notebook.edits).to.be.empty;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -88,6 +88,8 @@ import { CodeActionTriggerKind } from '../../plugin/types-impl';
|
|
|
88
88
|
import { IReadonlyVSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer';
|
|
89
89
|
import { FileUploadService } from '@theia/filesystem/lib/common/upload/file-upload';
|
|
90
90
|
import { ILogger } from '@theia/core';
|
|
91
|
+
import { NotebookService } from '@theia/notebook/lib/browser';
|
|
92
|
+
import { CellEditType, CellUri } from '@theia/notebook/lib/common';
|
|
91
93
|
|
|
92
94
|
@injectable()
|
|
93
95
|
export class LanguagesMainImpl implements LanguagesMain, Disposable {
|
|
@@ -110,6 +112,9 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable {
|
|
|
110
112
|
@inject(FileUploadService)
|
|
111
113
|
protected readonly fileUploadService: FileUploadService;
|
|
112
114
|
|
|
115
|
+
@inject(NotebookService)
|
|
116
|
+
protected readonly notebookService: NotebookService;
|
|
117
|
+
|
|
113
118
|
@inject(ILogger) @named('plugin-ext:LanguagesMainImpl')
|
|
114
119
|
protected readonly logger: ILogger;
|
|
115
120
|
|
|
@@ -129,18 +134,35 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable {
|
|
|
129
134
|
return Promise.resolve(monaco.languages.getLanguages().map(l => l.id));
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
$changeLanguage(resource: UriComponents, languageId: string): Promise<void> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!model) {
|
|
136
|
-
return Promise.reject(new Error('Invalid uri'));
|
|
137
|
+
async $changeLanguage(resource: UriComponents, languageId: string): Promise<void> {
|
|
138
|
+
if (!monaco.languages.getEncodedLanguageId(languageId)) {
|
|
139
|
+
throw new Error(`Unknown language ID: ${languageId}`);
|
|
137
140
|
}
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
+
const cell = CellUri.parse(URI.fromComponents(resource));
|
|
142
|
+
if (cell) {
|
|
143
|
+
this.changeCellLanguage(cell.notebook, cell.handle, languageId);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const model = monaco.editor.getModel(monaco.Uri.revive(resource));
|
|
147
|
+
if (!model) {
|
|
148
|
+
throw new Error('Invalid uri');
|
|
141
149
|
}
|
|
142
150
|
monaco.editor.setModelLanguage(model, languageId);
|
|
143
|
-
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The cell model owns a cell's language; kernel selection, serialization, and the cell editor all follow it.
|
|
155
|
+
*/
|
|
156
|
+
protected changeCellLanguage(notebookUri: URI, handle: number, languageId: string): void {
|
|
157
|
+
const notebook = this.notebookService.getNotebookEditorModel(notebookUri);
|
|
158
|
+
if (!notebook) {
|
|
159
|
+
throw new Error('Invalid uri');
|
|
160
|
+
}
|
|
161
|
+
const index = notebook.getCellIndexByHandle(handle);
|
|
162
|
+
if (index < 0) {
|
|
163
|
+
throw new Error('Invalid uri');
|
|
164
|
+
}
|
|
165
|
+
notebook.applyEdits([{ editType: CellEditType.CellLanguage, index, language: languageId }], true);
|
|
144
166
|
}
|
|
145
167
|
|
|
146
168
|
protected register(handle: number, service: Disposable): void {
|
|
@@ -68,6 +68,7 @@ import { UriMainImpl } from './uri-main';
|
|
|
68
68
|
import { LoggerMainImpl } from './logger-main';
|
|
69
69
|
import { McpServerDefinitionRegistryMainImpl } from './lm-main';
|
|
70
70
|
import { LanguageModelToolsMainImpl } from './lm-tool-main';
|
|
71
|
+
import { TelemetryMainImpl } from './telemetry-main';
|
|
71
72
|
|
|
72
73
|
export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void {
|
|
73
74
|
const loggerMain = new LoggerMainImpl(container);
|
|
@@ -215,4 +216,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container
|
|
|
215
216
|
|
|
216
217
|
const languageModelToolsMain = new LanguageModelToolsMainImpl(rpc, container);
|
|
217
218
|
rpc.set(PLUGIN_RPC_CONTEXT.LM_TOOLS_MAIN, languageModelToolsMain);
|
|
219
|
+
|
|
220
|
+
const telemetryMain = new TelemetryMainImpl(rpc, container);
|
|
221
|
+
rpc.set(PLUGIN_RPC_CONTEXT.TELEMETRY_MAIN, telemetryMain);
|
|
218
222
|
}
|
|
@@ -128,6 +128,11 @@ export const codeToTheiaMappings = new Map<string, MenuPath[]>([
|
|
|
128
128
|
|
|
129
129
|
type CodeEditorWidget = EditorWidget | WebviewWidget;
|
|
130
130
|
export namespace CodeEditorWidgetUtil {
|
|
131
|
+
/**
|
|
132
|
+
* The widgets `editor/title` contributions apply to. `WebviewContextKeys.getContextKeyValues` describes
|
|
133
|
+
* exactly this set; a widget admitted to the menu delegate without also being described there reads a
|
|
134
|
+
* stale `activeCustomEditorId` from the focused editor.
|
|
135
|
+
*/
|
|
131
136
|
export function is(arg: unknown): arg is CodeEditorWidget {
|
|
132
137
|
return arg instanceof EditorWidget || arg instanceof WebviewWidget;
|
|
133
138
|
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeWidget, LabelProviderContribution, LabelProvider,
|
|
25
25
|
UndoRedoHandler, DiffUris, Navigatable, SplitWidget,
|
|
26
26
|
noopWidgetStatusBarContribution,
|
|
27
|
-
WidgetStatusBarContribution
|
|
27
|
+
WidgetStatusBarContribution, WidgetContextKeyContribution
|
|
28
28
|
} from '@theia/core/lib/browser';
|
|
29
29
|
import { MaybePromise, CommandContribution, ResourceResolver, bindRootContributionProvider, URI, generateUuid, PreferenceContribution, nls } from '@theia/core/lib/common';
|
|
30
30
|
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging';
|
|
@@ -198,6 +198,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|
|
198
198
|
bind(WebviewSecondaryWindowSupport).toSelf().inSingletonScope();
|
|
199
199
|
bind(FrontendApplicationContribution).toService(WebviewSecondaryWindowSupport);
|
|
200
200
|
bind(FrontendApplicationContribution).toService(WebviewContextKeys);
|
|
201
|
+
bind(WidgetContextKeyContribution).toService(WebviewContextKeys);
|
|
201
202
|
bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(WebviewWidget));
|
|
202
203
|
|
|
203
204
|
bind(PluginCustomEditorRegistry).toSelf().inSingletonScope();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Robert Jandow
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
// `PluginIconService` transitively imports monaco modules that touch `document` at load time,
|
|
19
|
+
// and `toPluginUrl` itself needs `self.location` (via `Endpoint`) at call time, so JSDOM is
|
|
20
|
+
// re-enabled for the test run below.
|
|
21
|
+
let disableJSDOM = enableJSDOM();
|
|
22
|
+
|
|
23
|
+
import { expect } from 'chai';
|
|
24
|
+
import { URI } from '@theia/core/shared/vscode-uri';
|
|
25
|
+
import { PLUGINS_SCHEME } from '@theia/plugin-utils/lib/common/constants';
|
|
26
|
+
import { PluginIconService } from './plugin-icon-service';
|
|
27
|
+
|
|
28
|
+
disableJSDOM();
|
|
29
|
+
|
|
30
|
+
/** Widens `protected` members to `public` so the tests can call them directly. */
|
|
31
|
+
class TestablePluginIconService extends PluginIconService {
|
|
32
|
+
public callToPluginRelativePath(fontUri: URI): string {
|
|
33
|
+
return this.toPluginRelativePath(fontUri);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public callToPluginUrl(id: string, relativePath: string): URI {
|
|
37
|
+
return this.toPluginUrl(id, relativePath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('PluginIconService', () => {
|
|
42
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
43
|
+
after(() => disableJSDOM());
|
|
44
|
+
|
|
45
|
+
let service: TestablePluginIconService;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
service = new TestablePluginIconService();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('toPluginRelativePath', () => {
|
|
52
|
+
|
|
53
|
+
it('drops the plugin id segment of a plugin asset URI and keeps the rest verbatim', () => {
|
|
54
|
+
const uri = URI.parse(`${PLUGINS_SCHEME}:/acme_ext/dist/font.woff`);
|
|
55
|
+
expect(service.callToPluginRelativePath(uri)).to.equal('dist/font.woff');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('resolves a deeply nested path under a plugin asset URI', () => {
|
|
59
|
+
const uri = URI.parse(`${PLUGINS_SCHEME}:/acme_ext/a/b/c/d/font.woff`);
|
|
60
|
+
expect(service.callToPluginRelativePath(uri)).to.equal('a/b/c/d/font.woff');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('resolves a single-segment path under a plugin asset URI', () => {
|
|
64
|
+
const uri = URI.parse(`${PLUGINS_SCHEME}:/acme_ext/font.woff`);
|
|
65
|
+
expect(service.callToPluginRelativePath(uri)).to.equal('font.woff');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('decodes characters that were percent-encoded in a plugin asset URI', () => {
|
|
69
|
+
const uri = URI.parse(`${PLUGINS_SCHEME}:/acme_ext/dist/my%20font%23file.woff`);
|
|
70
|
+
expect(service.callToPluginRelativePath(uri)).to.equal('dist/my font#file.woff');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('falls back to cutting a backend (non-plugin-asset) path at its `extension` segment', () => {
|
|
74
|
+
const uri = URI.parse('file:///plugins/acme.ext-1.0.0/extension/dist/font.woff');
|
|
75
|
+
expect(service.callToPluginRelativePath(uri)).to.equal('dist/font.woff');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns an empty string for a backend path that has no `extension` segment', () => {
|
|
79
|
+
const uri = URI.parse('file:///plugins/acme.ext-1.0.0/dist/font.woff');
|
|
80
|
+
expect(service.callToPluginRelativePath(uri)).to.equal('');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('toPluginUrl', () => {
|
|
86
|
+
|
|
87
|
+
it('keeps `/` literal while percent-encoding special characters within each segment', () => {
|
|
88
|
+
const url = service.callToPluginUrl('acme.ext', 'dist/sub dir/a#b?c%d.woff');
|
|
89
|
+
expect(url.toString()).to.match(/\/hostedPlugin\/acme_ext\/dist\/sub%20dir\/a%23b%3Fc%25d\.woff$/);
|
|
90
|
+
// the slashes between segments must not themselves be encoded
|
|
91
|
+
expect(url.toString()).to.not.include('%2F');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('replaces non-word characters in the plugin id to build the URL segment', () => {
|
|
95
|
+
const url = service.callToPluginUrl('acme.ext-1.0.0', 'font.woff');
|
|
96
|
+
expect(url.toString()).to.match(/\/hostedPlugin\/acme_ext_1_0_0\/font\.woff$/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
});
|
|
@@ -21,7 +21,8 @@ import { URI } from '@theia/core/shared/vscode-uri';
|
|
|
21
21
|
import { MonacoIconRegistry } from '@theia/monaco/lib/browser/monaco-icon-registry';
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { IconContribution, DeployedPlugin, IconDefinition } from '../../common/plugin-protocol';
|
|
24
|
-
import { PLUGINS_BASE_PATH } from '@theia/plugin-utils/lib/common/constants';
|
|
24
|
+
import { PLUGINS_BASE_PATH, PLUGINS_SCHEME } from '@theia/plugin-utils/lib/common/constants';
|
|
25
|
+
import { encodePluginAssetPath } from '@theia/plugin-utils/lib/common/plugin-model';
|
|
25
26
|
|
|
26
27
|
@injectable()
|
|
27
28
|
export class PluginIconService implements Disposable {
|
|
@@ -49,7 +50,7 @@ export class PluginIconService implements Disposable {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
protected registerFontIcon(contribution: IconContribution, defaultIcon: IconDefinition): void {
|
|
52
|
-
const location = this.toPluginUrl(contribution.extensionId,
|
|
53
|
+
const location = this.toPluginUrl(contribution.extensionId, this.toPluginRelativePath(URI.parse(defaultIcon.location)));
|
|
53
54
|
const format = getFileExtension(location.path);
|
|
54
55
|
const fontId = getFontId(contribution.extensionId, location.path);
|
|
55
56
|
|
|
@@ -67,9 +68,21 @@ export class PluginIconService implements Disposable {
|
|
|
67
68
|
this.iconRegistry.registerIcon(contribution.id, { id: defaultIconId }, contribution.description);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* With a backend the font sits in the extracted VSIX, so the path has to be cut at its
|
|
73
|
+
* `extension` segment. Browser-only emits `<PLUGINS_SCHEME>:/<id>/<path>` instead, where
|
|
74
|
+
* everything past the plugin id already is the path relative to the plugin root.
|
|
75
|
+
*/
|
|
76
|
+
protected toPluginRelativePath(fontUri: URI): string {
|
|
77
|
+
if (fontUri.scheme === PLUGINS_SCHEME) {
|
|
78
|
+
return fontUri.path.split('/').slice(2).join('/');
|
|
79
|
+
}
|
|
80
|
+
return getIconRelativePath(fontUri.path);
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
protected toPluginUrl(id: string, relativePath: string): URI {
|
|
71
84
|
return URI.from(new Endpoint({
|
|
72
|
-
path: `${PLUGINS_BASE_PATH}/${this.formatExtensionId(id)}/${
|
|
85
|
+
path: `${PLUGINS_BASE_PATH}/${this.formatExtensionId(id)}/${encodePluginAssetPath(relativePath)}`
|
|
73
86
|
}).getRestUrl().toComponents());
|
|
74
87
|
}
|
|
75
88
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Robert Jandow
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
// `PluginIconTheme` transitively imports monaco/filesystem/workspace modules that touch `document`
|
|
19
|
+
// at load time, and `toCSSUrl` needs `self.location` (via `Endpoint`) at call time, so JSDOM is
|
|
20
|
+
// re-enabled for the test run below.
|
|
21
|
+
let disableJSDOM = enableJSDOM();
|
|
22
|
+
|
|
23
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
24
|
+
// `WorkspaceService`, transitively imported for `PluginIconTheme`'s `@inject` property, reads the
|
|
25
|
+
// frontend application config at module load time.
|
|
26
|
+
FrontendApplicationConfigProvider.set({});
|
|
27
|
+
|
|
28
|
+
import { expect } from 'chai';
|
|
29
|
+
import URI from '@theia/core/lib/common/uri';
|
|
30
|
+
import { PLUGINS_SCHEME } from '@theia/plugin-utils/lib/common/constants';
|
|
31
|
+
import { PluginIconTheme } from './plugin-icon-theme-service';
|
|
32
|
+
|
|
33
|
+
disableJSDOM();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Widens `protected` members to `public`/settable so the tests can drive `toCSSUrl` without going
|
|
37
|
+
* through `@postConstruct` and the full Inversify container - none of the injected services
|
|
38
|
+
* (`FileService`, `LabelProvider`, etc.) are touched by it.
|
|
39
|
+
*/
|
|
40
|
+
class TestablePluginIconTheme extends PluginIconTheme {
|
|
41
|
+
|
|
42
|
+
setState(state: { uri: string; pluginId: string; packageUri: string }): void {
|
|
43
|
+
this.uri = state.uri;
|
|
44
|
+
this.pluginId = state.pluginId;
|
|
45
|
+
this.packageUri = state.packageUri;
|
|
46
|
+
this.locationUri = new URI(state.uri).parent;
|
|
47
|
+
this.packageRootUri = new URI(state.packageUri);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
callToCSSUrl(iconPath: string | undefined): string | undefined {
|
|
51
|
+
return this.toCSSUrl(iconPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('PluginIconTheme', () => {
|
|
56
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
57
|
+
after(() => disableJSDOM());
|
|
58
|
+
|
|
59
|
+
describe('toCSSUrl', () => {
|
|
60
|
+
|
|
61
|
+
it('resolves an icon path relative to a browser-only plugin package root into a static plugin asset URL', () => {
|
|
62
|
+
const theme = new TestablePluginIconTheme();
|
|
63
|
+
theme.setState({
|
|
64
|
+
uri: `${PLUGINS_SCHEME}:/acme_ext/themes/my-icon-theme.json`,
|
|
65
|
+
pluginId: 'acme_ext',
|
|
66
|
+
packageUri: `${PLUGINS_SCHEME}:/acme_ext/`
|
|
67
|
+
});
|
|
68
|
+
expect(theme.callToCSSUrl('icons/file.svg')).to.equal("url('http://localhost/hostedPlugin/acme_ext/themes/icons/file.svg')");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('resolves `..` segments in a browser-only icon path back within the plugin root', () => {
|
|
72
|
+
const theme = new TestablePluginIconTheme();
|
|
73
|
+
theme.setState({
|
|
74
|
+
uri: `${PLUGINS_SCHEME}:/acme_ext/themes/my-icon-theme.json`,
|
|
75
|
+
pluginId: 'acme_ext',
|
|
76
|
+
packageUri: `${PLUGINS_SCHEME}:/acme_ext/`
|
|
77
|
+
});
|
|
78
|
+
expect(theme.callToCSSUrl('../shared/icons/other.svg')).to.equal("url('http://localhost/hostedPlugin/acme_ext/shared/icons/other.svg')");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('still resolves icon paths for a backend (file-scheme) icon theme', () => {
|
|
82
|
+
const theme = new TestablePluginIconTheme();
|
|
83
|
+
theme.setState({
|
|
84
|
+
uri: 'file:///plugins/acme.ext-1.0.0/extension/themes/my-icon-theme.json',
|
|
85
|
+
pluginId: 'acme_ext',
|
|
86
|
+
packageUri: 'file:///plugins/acme.ext-1.0.0/extension/'
|
|
87
|
+
});
|
|
88
|
+
expect(theme.callToCSSUrl('icons/file.svg')).to.equal("url('http://localhost/hostedPlugin/acme_ext/themes/icons/file.svg')");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns undefined for an empty icon path', () => {
|
|
92
|
+
const theme = new TestablePluginIconTheme();
|
|
93
|
+
theme.setState({
|
|
94
|
+
uri: `${PLUGINS_SCHEME}:/acme_ext/themes/my-icon-theme.json`,
|
|
95
|
+
pluginId: 'acme_ext',
|
|
96
|
+
packageUri: `${PLUGINS_SCHEME}:/acme_ext/`
|
|
97
|
+
});
|
|
98
|
+
expect(theme.callToCSSUrl(undefined)).to.be.undefined;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
});
|