@theia/plugin-ext 1.76.0-next.4 → 1.76.0-next.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/lib/common/plugin-api-rpc.d.ts +14 -0
  2. package/lib/common/plugin-api-rpc.d.ts.map +1 -1
  3. package/lib/common/plugin-api-rpc.js +1 -1
  4. package/lib/common/plugin-api-rpc.js.map +1 -1
  5. package/lib/hosted/browser/hosted-plugin.d.ts +2 -0
  6. package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
  7. package/lib/hosted/browser/hosted-plugin.js +8 -2
  8. package/lib/hosted/browser/hosted-plugin.js.map +1 -1
  9. package/lib/hosted/browser/worker/worker-main.js +3 -1
  10. package/lib/hosted/browser/worker/worker-main.js.map +1 -1
  11. package/lib/hosted/browser/worker/worker-plugin-module.d.ts.map +1 -1
  12. package/lib/hosted/browser/worker/worker-plugin-module.js +2 -0
  13. package/lib/hosted/browser/worker/worker-plugin-module.js.map +1 -1
  14. package/lib/hosted/node/plugin-host-module.d.ts.map +1 -1
  15. package/lib/hosted/node/plugin-host-module.js +2 -0
  16. package/lib/hosted/node/plugin-host-module.js.map +1 -1
  17. package/lib/hosted/node/plugin-host-rpc.d.ts +3 -0
  18. package/lib/hosted/node/plugin-host-rpc.d.ts.map +1 -1
  19. package/lib/hosted/node/plugin-host-rpc.js +9 -3
  20. package/lib/hosted/node/plugin-host-rpc.js.map +1 -1
  21. package/lib/main/browser/editors-and-documents-main.d.ts +7 -0
  22. package/lib/main/browser/editors-and-documents-main.d.ts.map +1 -1
  23. package/lib/main/browser/editors-and-documents-main.js +19 -9
  24. package/lib/main/browser/editors-and-documents-main.js.map +1 -1
  25. package/lib/main/browser/editors-and-documents-main.spec.d.ts +2 -0
  26. package/lib/main/browser/editors-and-documents-main.spec.d.ts.map +1 -0
  27. package/lib/main/browser/editors-and-documents-main.spec.js +117 -0
  28. package/lib/main/browser/editors-and-documents-main.spec.js.map +1 -0
  29. package/lib/main/browser/languages-main.d.ts +7 -0
  30. package/lib/main/browser/languages-main.d.ts.map +1 -1
  31. package/lib/main/browser/languages-main.js +31 -9
  32. package/lib/main/browser/languages-main.js.map +1 -1
  33. package/lib/main/browser/languages-main.spec.d.ts +2 -0
  34. package/lib/main/browser/languages-main.spec.d.ts.map +1 -0
  35. package/lib/main/browser/languages-main.spec.js +82 -0
  36. package/lib/main/browser/languages-main.spec.js.map +1 -0
  37. package/lib/main/browser/main-context.d.ts.map +1 -1
  38. package/lib/main/browser/main-context.js +3 -0
  39. package/lib/main/browser/main-context.js.map +1 -1
  40. package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts +5 -0
  41. package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts.map +1 -1
  42. package/lib/main/browser/menus/vscode-theia-menu-mappings.js +5 -0
  43. package/lib/main/browser/menus/vscode-theia-menu-mappings.js.map +1 -1
  44. package/lib/main/browser/plugin-ext-frontend-module.d.ts.map +1 -1
  45. package/lib/main/browser/plugin-ext-frontend-module.js +1 -0
  46. package/lib/main/browser/plugin-ext-frontend-module.js.map +1 -1
  47. package/lib/main/browser/telemetry-main.d.ts +20 -0
  48. package/lib/main/browser/telemetry-main.d.ts.map +1 -0
  49. package/lib/main/browser/telemetry-main.js +43 -0
  50. package/lib/main/browser/telemetry-main.js.map +1 -0
  51. package/lib/main/browser/telemetry-main.spec.d.ts +2 -0
  52. package/lib/main/browser/telemetry-main.spec.d.ts.map +1 -0
  53. package/lib/main/browser/telemetry-main.spec.js +66 -0
  54. package/lib/main/browser/telemetry-main.spec.js.map +1 -0
  55. package/lib/main/browser/webview/webview-context-keys.d.ts +3 -2
  56. package/lib/main/browser/webview/webview-context-keys.d.ts.map +1 -1
  57. package/lib/main/browser/webview/webview-context-keys.js +16 -2
  58. package/lib/main/browser/webview/webview-context-keys.js.map +1 -1
  59. package/lib/plugin/plugin-context.d.ts +2 -1
  60. package/lib/plugin/plugin-context.d.ts.map +1 -1
  61. package/lib/plugin/plugin-context.js +2 -3
  62. package/lib/plugin/plugin-context.js.map +1 -1
  63. package/lib/plugin/plugin-manager.d.ts +2 -0
  64. package/lib/plugin/plugin-manager.d.ts.map +1 -1
  65. package/lib/plugin/plugin-manager.js +7 -0
  66. package/lib/plugin/plugin-manager.js.map +1 -1
  67. package/lib/plugin/telemetry-ext.d.ts +23 -7
  68. package/lib/plugin/telemetry-ext.d.ts.map +1 -1
  69. package/lib/plugin/telemetry-ext.js +60 -33
  70. package/lib/plugin/telemetry-ext.js.map +1 -1
  71. package/lib/plugin/telemetry-ext.spec.js +121 -16
  72. package/lib/plugin/telemetry-ext.spec.js.map +1 -1
  73. package/package.json +32 -31
  74. package/src/common/plugin-api-rpc.ts +15 -1
  75. package/src/hosted/browser/hosted-plugin.ts +7 -2
  76. package/src/hosted/browser/worker/worker-main.ts +4 -1
  77. package/src/hosted/browser/worker/worker-plugin-module.ts +2 -0
  78. package/src/hosted/node/plugin-host-module.ts +2 -0
  79. package/src/hosted/node/plugin-host-rpc.ts +10 -4
  80. package/src/main/browser/editors-and-documents-main.spec.ts +140 -0
  81. package/src/main/browser/editors-and-documents-main.ts +19 -7
  82. package/src/main/browser/languages-main.spec.ts +101 -0
  83. package/src/main/browser/languages-main.ts +31 -9
  84. package/src/main/browser/main-context.ts +4 -0
  85. package/src/main/browser/menus/vscode-theia-menu-mappings.ts +5 -0
  86. package/src/main/browser/plugin-ext-frontend-module.ts +2 -1
  87. package/src/main/browser/telemetry-main.spec.ts +77 -0
  88. package/src/main/browser/telemetry-main.ts +46 -0
  89. package/src/main/browser/webview/webview-context-keys.ts +20 -4
  90. package/src/plugin/plugin-context.ts +3 -2
  91. package/src/plugin/plugin-manager.ts +6 -0
  92. package/src/plugin/telemetry-ext.spec.ts +158 -17
  93. package/src/plugin/telemetry-ext.ts +63 -33
@@ -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
- const uri = monaco.Uri.revive(resource);
134
- const model = monaco.editor.getModel(uri);
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 langId = monaco.languages.getEncodedLanguageId(languageId);
139
- if (!langId) {
140
- return Promise.reject(new Error(`Unknown language ID: ${languageId}`));
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
- return Promise.resolve(undefined);
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,77 @@
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 { expect } from 'chai';
18
+ import { Emitter } from '@theia/core/lib/common/event';
19
+ import { Container } from '@theia/core/shared/inversify';
20
+ import { TelemetryConsentProvider } from '@theia/telemetry/lib/common/telemetry-consent-provider';
21
+ import { TelemetryLevel } from '@theia/telemetry/lib/common/telemetry-types';
22
+ import { RPCProtocol } from '../../common/rpc-protocol';
23
+ import { TelemetryMainImpl } from './telemetry-main';
24
+
25
+ describe('TelemetryMainImpl', () => {
26
+
27
+ let level: TelemetryLevel;
28
+ let pushed: TelemetryLevel[];
29
+ let levelChanged: Emitter<TelemetryLevel>;
30
+ let container: Container;
31
+ let rpc: RPCProtocol;
32
+
33
+ beforeEach(() => {
34
+ level = 'off';
35
+ pushed = [];
36
+ levelChanged = new Emitter<TelemetryLevel>();
37
+ container = new Container();
38
+ container.bind(TelemetryConsentProvider).toConstantValue(<TelemetryConsentProvider>{
39
+ get level(): TelemetryLevel { return level; },
40
+ onDidChangeTelemetryLevel: levelChanged.event,
41
+ ready: Promise.resolve()
42
+ });
43
+ rpc = {
44
+ getProxy<T>(_proxyId: unknown): T {
45
+ return {
46
+ $onDidChangeTelemetryLevel: (next: TelemetryLevel) => { pushed.push(next); }
47
+ } as unknown as T;
48
+ },
49
+ set: <T, R extends T>(_identifier: unknown, instance: R) => instance,
50
+ dispose: () => { }
51
+ } as RPCProtocol;
52
+ });
53
+
54
+ it('does not push a level on construction, the initial level is seeded through $init', () => {
55
+ new TelemetryMainImpl(rpc, container);
56
+
57
+ expect(pushed).to.be.empty;
58
+ });
59
+
60
+ it('pushes every level change to the plugin host', () => {
61
+ new TelemetryMainImpl(rpc, container);
62
+
63
+ levelChanged.fire('all');
64
+ levelChanged.fire('error');
65
+
66
+ expect(pushed).to.deep.equal(['all', 'error']);
67
+ });
68
+
69
+ it('stops pushing once disposed', () => {
70
+ const telemetryMain = new TelemetryMainImpl(rpc, container);
71
+
72
+ telemetryMain.dispose();
73
+ levelChanged.fire('all');
74
+
75
+ expect(pushed).to.be.empty;
76
+ });
77
+ });
@@ -0,0 +1,46 @@
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 { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
18
+ import { interfaces } from '@theia/core/shared/inversify';
19
+ import { TelemetryConsentProvider } from '@theia/telemetry/lib/common/telemetry-consent-provider';
20
+ import { MAIN_RPC_CONTEXT, TelemetryExt, TelemetryMain } from '../../common/plugin-api-rpc';
21
+ import { RPCProtocol } from '../../common/rpc-protocol';
22
+
23
+ /**
24
+ * Forwards the user's telemetry consent to the plugin host. The initial level is seeded through
25
+ * `PluginManagerInitializeParams` so that it is in place before any plugin activates.
26
+ *
27
+ * `TelemetryMain` declares no `$` methods; the instance is registered with the RPC protocol so that the consent
28
+ * subscription is disposed along with the channel.
29
+ *
30
+ * @experimental
31
+ */
32
+ export class TelemetryMainImpl implements TelemetryMain, Disposable {
33
+
34
+ protected readonly proxy: TelemetryExt;
35
+ protected readonly toDispose = new DisposableCollection();
36
+
37
+ constructor(rpc: RPCProtocol, container: interfaces.Container) {
38
+ this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TELEMETRY_EXT);
39
+ const consentProvider = container.get<TelemetryConsentProvider>(TelemetryConsentProvider);
40
+ this.toDispose.push(consentProvider.onDidChangeTelemetryLevel(level => this.proxy.$onDidChangeTelemetryLevel(level)));
41
+ }
42
+
43
+ dispose(): void {
44
+ this.toDispose.dispose();
45
+ }
46
+ }
@@ -14,14 +14,18 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { ApplicationShell, FocusTracker, Widget } from '@theia/core/lib/browser';
17
+ import { ApplicationShell, FocusTracker, Widget, WidgetContextKeyContribution } from '@theia/core/lib/browser';
18
18
  import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
19
19
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
20
20
  import { CustomEditorWidget } from '../custom-editors/custom-editor-widget';
21
+ import { CodeEditorWidgetUtil } from '../menus/vscode-theia-menu-mappings';
21
22
  import { WebviewWidget } from './webview';
22
23
 
24
+ const ACTIVE_WEBVIEW_PANEL_ID = 'activeWebviewPanelId';
25
+ const ACTIVE_CUSTOM_EDITOR_ID = 'activeCustomEditorId';
26
+
23
27
  @injectable()
24
- export class WebviewContextKeys {
28
+ export class WebviewContextKeys implements WidgetContextKeyContribution {
25
29
 
26
30
  /**
27
31
  * Context key representing the `viewType` of the active `WebviewWidget`, if any.
@@ -41,11 +45,23 @@ export class WebviewContextKeys {
41
45
 
42
46
  @postConstruct()
43
47
  protected init(): void {
44
- this.activeWebviewPanelId = this.contextKeyService.createKey('activeWebviewPanelId', '');
45
- this.activeCustomEditorId = this.contextKeyService.createKey('activeCustomEditorId', '');
48
+ this.activeWebviewPanelId = this.contextKeyService.createKey(ACTIVE_WEBVIEW_PANEL_ID, '');
49
+ this.activeCustomEditorId = this.contextKeyService.createKey(ACTIVE_CUSTOM_EDITOR_ID, '');
46
50
  this.applicationShell.onDidChangeCurrentWidget(this.handleDidChangeCurrentWidget, this);
47
51
  }
48
52
 
53
+ getContextKeyValues(widget: Widget): Iterable<[string, unknown]> | undefined {
54
+ // A view toolbar item may key on the *active* editor, so only the editor-like widgets these keys can
55
+ // describe get an answer; everything else keeps the ambient value.
56
+ if (!CodeEditorWidgetUtil.is(widget)) {
57
+ return undefined;
58
+ }
59
+ return [
60
+ [ACTIVE_CUSTOM_EDITOR_ID, widget instanceof CustomEditorWidget ? widget.viewType : ''],
61
+ [ACTIVE_WEBVIEW_PANEL_ID, widget instanceof WebviewWidget ? widget.viewType : '']
62
+ ];
63
+ }
64
+
49
65
  protected handleDidChangeCurrentWidget(change: FocusTracker.IChangedArgs<Widget>): void {
50
66
  const { newValue } = change;
51
67
  if (newValue instanceof CustomEditorWidget) {
@@ -325,7 +325,8 @@ export function createAPIFactory(
325
325
  messageRegistryExt: MessageRegistryExt,
326
326
  clipboard: ClipboardExt,
327
327
  webviewExt: WebviewsExtImpl,
328
- localizationExt: LocalizationExtImpl
328
+ localizationExt: LocalizationExtImpl,
329
+ telemetryExt: TelemetryExtImpl
329
330
  ): PluginAPIFactory {
330
331
 
331
332
  const authenticationExt = rpc.set(MAIN_RPC_CONTEXT.AUTHENTICATION_EXT, new AuthenticationExtImpl(rpc));
@@ -359,12 +360,12 @@ export function createAPIFactory(
359
360
  const tabsExt = rpc.set(MAIN_RPC_CONTEXT.TABS_EXT, new TabsExtImpl(rpc));
360
361
  const customEditorExt = rpc.set(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT, new CustomEditorsExtImpl(rpc, documents, webviewExt));
361
362
  const webviewViewsExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEW_VIEWS_EXT, new WebviewViewsExtImpl(rpc, webviewExt));
362
- const telemetryExt = rpc.set(MAIN_RPC_CONTEXT.TELEMETRY_EXT, new TelemetryExtImpl());
363
363
  const testingExt = rpc.set(MAIN_RPC_CONTEXT.TESTING_EXT, new TestingExtImpl(rpc, commandRegistry));
364
364
  const uriExt = rpc.set(MAIN_RPC_CONTEXT.URI_EXT, new UriExtImpl(rpc));
365
365
  const lmExt = rpc.set(MAIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_EXT, new LmExtImpl(rpc));
366
366
  const lmToolExt = rpc.set(MAIN_RPC_CONTEXT.LM_TOOLS_EXT, new LanguageModelToolsExtImpl(rpc));
367
367
  rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt);
368
+ rpc.set(MAIN_RPC_CONTEXT.TELEMETRY_EXT, telemetryExt);
368
369
 
369
370
  const commandLogger = new PluginLogger(rpc, 'commands-plugin');
370
371
 
@@ -37,6 +37,7 @@ import * as theia from '@theia/plugin';
37
37
  import * as types from './types-impl';
38
38
  import { join } from './path';
39
39
  import { EnvExtImpl } from './env';
40
+ import { TelemetryExtImpl } from './telemetry-ext';
40
41
  import { PreferenceRegistryExtImpl } from './preference-registry';
41
42
  import { InternalStorageExt, Memento, GlobalState } from './plugin-storage';
42
43
  import { ExtPluginApi } from '../common/plugin-ext-api-contribution';
@@ -474,6 +475,9 @@ export class PluginManagerExtImpl extends AbstractPluginManagerExtImpl<PluginMan
474
475
  @inject(WebviewsExtImpl)
475
476
  protected readonly webview: WebviewsExtImpl;
476
477
 
478
+ @inject(TelemetryExtImpl)
479
+ protected readonly telemetryExt: TelemetryExtImpl;
480
+
477
481
  private supportedActivationEvents: Set<string>;
478
482
 
479
483
  async $init(params: PluginManagerInitializeParams): Promise<void> {
@@ -489,6 +493,8 @@ export class PluginManagerExtImpl extends AbstractPluginManagerExtImpl<PluginMan
489
493
  this.envExt.setAppUriScheme(params.env.appUriScheme);
490
494
 
491
495
  this.preferencesManager.init(params.preferences);
496
+ // must be in place before activation: plugins create their telemetry loggers during `activate()`
497
+ this.telemetryExt.setLevel(params.telemetryLevel ?? 'off');
492
498
 
493
499
  if (params.extApi) {
494
500
  this.host.initExtApi(params.extApi);
@@ -15,47 +15,188 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import * as chai from 'chai';
18
+ import { TelemetryLevel } from '@theia/telemetry/lib/common/telemetry-types';
18
19
  import { TelemetryExtImpl } from './telemetry-ext';
19
20
 
20
21
  const expect = chai.expect;
21
22
 
22
- describe('TelemetryLogger', () => {
23
+ describe('TelemetryExtImpl', () => {
23
24
  const sender = {
24
25
  sendEventData: () => { },
25
26
  sendErrorData: () => { }
26
27
  };
27
28
 
28
- it('reflects the current telemetry enabled state', () => {
29
+ interface Expectation {
30
+ isTelemetryEnabled: boolean;
31
+ isUsageEnabled: boolean;
32
+ isErrorsEnabled: boolean;
33
+ }
34
+
35
+ const expectations: Record<TelemetryLevel, Expectation> = {
36
+ off: { isTelemetryEnabled: false, isUsageEnabled: false, isErrorsEnabled: false },
37
+ crash: { isTelemetryEnabled: false, isUsageEnabled: false, isErrorsEnabled: false },
38
+ error: { isTelemetryEnabled: false, isUsageEnabled: false, isErrorsEnabled: true },
39
+ all: { isTelemetryEnabled: true, isUsageEnabled: true, isErrorsEnabled: true }
40
+ };
41
+
42
+ for (const level of ['off', 'crash', 'error', 'all'] as const) {
43
+ const expected = expectations[level];
44
+
45
+ it(`derives the enablement states from telemetry level '${level}'`, () => {
46
+ const telemetry = new TelemetryExtImpl();
47
+ telemetry.$onDidChangeTelemetryLevel(level);
48
+ const logger = telemetry.createTelemetryLogger(sender);
49
+
50
+ expect(telemetry.level).to.equal(level);
51
+ expect(telemetry.isTelemetryEnabled).to.equal(expected.isTelemetryEnabled);
52
+ expect(logger.isUsageEnabled).to.equal(expected.isUsageEnabled);
53
+ expect(logger.isErrorsEnabled).to.equal(expected.isErrorsEnabled);
54
+ });
55
+
56
+ it(`applies telemetry level '${level}' to loggers created before the level arrived`, () => {
57
+ const telemetry = new TelemetryExtImpl();
58
+ const logger = telemetry.createTelemetryLogger(sender);
59
+ telemetry.$onDidChangeTelemetryLevel(level);
60
+
61
+ expect(logger.isUsageEnabled).to.equal(expected.isUsageEnabled);
62
+ expect(logger.isErrorsEnabled).to.equal(expected.isErrorsEnabled);
63
+ });
64
+ }
65
+
66
+ it('only fires onDidChangeTelemetryEnabled when the boolean it reports changes', () => {
67
+ const telemetry = new TelemetryExtImpl();
68
+ const fired: boolean[] = [];
69
+ telemetry.onDidChangeTelemetryEnabled(enabled => fired.push(enabled));
70
+
71
+ telemetry.$onDidChangeTelemetryLevel('crash');
72
+ telemetry.$onDidChangeTelemetryLevel('error');
73
+ telemetry.$onDidChangeTelemetryLevel('all');
74
+ telemetry.$onDidChangeTelemetryLevel('off');
75
+
76
+ expect(fired).to.deep.equal([true, false]);
77
+ });
78
+
79
+ it('fires onDidChangeEnableStates once per transition that changes an enablement state', () => {
29
80
  const telemetry = new TelemetryExtImpl();
30
81
  const logger = telemetry.createTelemetryLogger(sender);
82
+ let changes = 0;
83
+ logger.onDidChangeEnableStates(() => changes++);
31
84
 
32
- expect(logger.telemetryEnabled).to.equal(false);
33
- expect(logger.isUsageEnabled).to.equal(false);
34
- expect(logger.isErrorsEnabled).to.equal(false);
85
+ telemetry.$onDidChangeTelemetryLevel('crash'); // off -> crash: both stay false
86
+ expect(changes).to.equal(0);
35
87
 
36
- telemetry.isTelemetryEnabled = true;
88
+ telemetry.$onDidChangeTelemetryLevel('error'); // errors become enabled
89
+ expect(changes).to.equal(1);
37
90
 
38
- expect(logger.telemetryEnabled).to.equal(true);
39
- expect(logger.isUsageEnabled).to.equal(true);
40
- expect(logger.isErrorsEnabled).to.equal(true);
91
+ telemetry.$onDidChangeTelemetryLevel('all'); // usage becomes enabled
92
+ expect(changes).to.equal(2);
41
93
 
42
- telemetry.isTelemetryEnabled = false;
94
+ telemetry.$onDidChangeTelemetryLevel('off');
95
+ expect(changes).to.equal(3);
96
+ });
43
97
 
44
- expect(logger.telemetryEnabled).to.equal(false);
98
+ it('stops following the level once the logger is disposed', () => {
99
+ const telemetry = new TelemetryExtImpl();
100
+ const logger = telemetry.createTelemetryLogger(sender);
101
+ let changes = 0;
102
+ logger.onDidChangeEnableStates(() => changes++);
103
+
104
+ logger.dispose();
105
+ telemetry.$onDidChangeTelemetryLevel('all');
45
106
  expect(logger.isUsageEnabled).to.equal(false);
46
- expect(logger.isErrorsEnabled).to.equal(false);
107
+
108
+ logger.isUsageEnabled = true;
109
+ expect(changes).to.equal(0);
110
+ });
111
+
112
+ it('logs rather than propagates a sender that fails to flush on disposal', async () => {
113
+ const telemetry = new TelemetryExtImpl();
114
+ const logged: unknown[] = [];
115
+ const consoleError = console.error;
116
+ console.error = (...args: unknown[]) => { logged.push(args); };
117
+ try {
118
+ telemetry.createTelemetryLogger({ ...sender, flush: () => { throw new Error('boom'); } }).dispose();
119
+ telemetry.createTelemetryLogger({ ...sender, flush: () => Promise.reject(new Error('boom')) }).dispose();
120
+ await new Promise(resolve => setTimeout(resolve, 0));
121
+ } finally {
122
+ console.error = consoleError;
123
+ }
124
+
125
+ expect(logged).to.have.lengthOf(2);
47
126
  });
48
127
 
49
- it('emits enable-state changes when telemetry is toggled', () => {
128
+ it('ignores a repeated telemetry level', () => {
50
129
  const telemetry = new TelemetryExtImpl();
51
130
  const logger = telemetry.createTelemetryLogger(sender);
52
131
  let changes = 0;
53
-
54
132
  logger.onDidChangeEnableStates(() => changes++);
55
133
 
56
- telemetry.isTelemetryEnabled = true;
57
- telemetry.isTelemetryEnabled = false;
134
+ telemetry.$onDidChangeTelemetryLevel('all');
135
+ telemetry.$onDidChangeTelemetryLevel('all');
58
136
 
59
- expect(changes).to.equal(2);
137
+ expect(changes).to.equal(1);
138
+ });
139
+ });
140
+
141
+ describe('TelemetryLogger', () => {
142
+
143
+ interface RecordingSender {
144
+ events: string[];
145
+ errors: Error[];
146
+ sendEventData(eventName: string): void;
147
+ sendErrorData(error: Error): void;
148
+ }
149
+
150
+ function createSender(): RecordingSender {
151
+ const events: string[] = [];
152
+ const errors: Error[] = [];
153
+ return {
154
+ events,
155
+ errors,
156
+ sendEventData: (eventName: string) => events.push(eventName),
157
+ sendErrorData: (error: Error) => errors.push(error)
158
+ };
159
+ }
160
+
161
+ it('delivers errors but suppresses usage at telemetry level \'error\'', () => {
162
+ const telemetry = new TelemetryExtImpl();
163
+ telemetry.$onDidChangeTelemetryLevel('error');
164
+ const sender = createSender();
165
+ const logger = telemetry.createTelemetryLogger(sender);
166
+
167
+ logger.logUsage('usage-event');
168
+ logger.logError('error-event');
169
+ logger.logError(new Error('boom'));
170
+
171
+ expect(sender.events).to.deep.equal(['error-event']);
172
+ expect(sender.errors).to.have.lengthOf(1);
173
+ });
174
+
175
+ it('suppresses usage that a plugin re-enables while the telemetry level forbids it', () => {
176
+ const telemetry = new TelemetryExtImpl();
177
+ telemetry.$onDidChangeTelemetryLevel('off');
178
+ const sender = createSender();
179
+ const logger = telemetry.createTelemetryLogger(sender);
180
+
181
+ logger.isUsageEnabled = true;
182
+ logger.isErrorsEnabled = true;
183
+ logger.logUsage('usage-event');
184
+ logger.logError('error-event');
185
+
186
+ expect(sender.events).to.be.empty;
187
+ });
188
+
189
+ it('refuses to let a plugin assign the telemetry level of its logger', () => {
190
+ const telemetry = new TelemetryExtImpl();
191
+ telemetry.$onDidChangeTelemetryLevel('off');
192
+ const sender = createSender();
193
+ const logger = telemetry.createTelemetryLogger(sender);
194
+
195
+ expect(Reflect.set(logger, 'level', 'all')).to.be.false;
196
+ logger.logUsage('usage-event');
197
+
198
+ expect(logger.level).to.equal('off');
199
+ expect(logger.isUsageEnabled).to.be.false;
200
+ expect(sender.events).to.be.empty;
60
201
  });
61
202
  });