@theia/plugin-ext 1.74.0-next.9 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
  2. package/lib/hosted/browser/hosted-plugin.js +14 -3
  3. package/lib/hosted/browser/hosted-plugin.js.map +1 -1
  4. package/lib/hosted/common/hosted-plugin.d.ts.map +1 -1
  5. package/lib/hosted/common/hosted-plugin.js +4 -0
  6. package/lib/hosted/common/hosted-plugin.js.map +1 -1
  7. package/lib/hosted/node/plugin-reader.d.ts +6 -5
  8. package/lib/hosted/node/plugin-reader.d.ts.map +1 -1
  9. package/lib/hosted/node/plugin-reader.js +14 -7
  10. package/lib/hosted/node/plugin-reader.js.map +1 -1
  11. package/lib/hosted/node/plugin-reader.spec.d.ts +2 -0
  12. package/lib/hosted/node/plugin-reader.spec.d.ts.map +1 -0
  13. package/lib/hosted/node/plugin-reader.spec.js +72 -0
  14. package/lib/hosted/node/plugin-reader.spec.js.map +1 -0
  15. package/lib/main/browser/custom-editors/custom-editor-opener.d.ts +3 -1
  16. package/lib/main/browser/custom-editors/custom-editor-opener.d.ts.map +1 -1
  17. package/lib/main/browser/custom-editors/custom-editor-opener.js +41 -1
  18. package/lib/main/browser/custom-editors/custom-editor-opener.js.map +1 -1
  19. package/lib/main/browser/custom-editors/custom-editor-opener.spec.d.ts +2 -0
  20. package/lib/main/browser/custom-editors/custom-editor-opener.spec.d.ts.map +1 -0
  21. package/lib/main/browser/custom-editors/custom-editor-opener.spec.js +120 -0
  22. package/lib/main/browser/custom-editors/custom-editor-opener.spec.js.map +1 -0
  23. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts +12 -3
  24. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts.map +1 -1
  25. package/lib/main/browser/custom-editors/custom-editor-widget.js +31 -1
  26. package/lib/main/browser/custom-editors/custom-editor-widget.js.map +1 -1
  27. package/lib/main/browser/preference-registry-main.d.ts.map +1 -1
  28. package/lib/main/browser/preference-registry-main.js +8 -0
  29. package/lib/main/browser/preference-registry-main.js.map +1 -1
  30. package/lib/main/browser/tabs/tabs-main.d.ts.map +1 -1
  31. package/lib/main/browser/tabs/tabs-main.js +9 -1
  32. package/lib/main/browser/tabs/tabs-main.js.map +1 -1
  33. package/lib/main/node/plugin-http-resolver.d.ts.map +1 -1
  34. package/lib/main/node/plugin-http-resolver.js +5 -3
  35. package/lib/main/node/plugin-http-resolver.js.map +1 -1
  36. package/lib/main/node/plugin-http-resolver.spec.d.ts +2 -0
  37. package/lib/main/node/plugin-http-resolver.spec.d.ts.map +1 -0
  38. package/lib/main/node/plugin-http-resolver.spec.js +40 -0
  39. package/lib/main/node/plugin-http-resolver.spec.js.map +1 -0
  40. package/lib/main/node/plugin-service.d.ts.map +1 -1
  41. package/lib/main/node/plugin-service.js +8 -3
  42. package/lib/main/node/plugin-service.js.map +1 -1
  43. package/lib/plugin/telemetry-ext.d.ts +4 -1
  44. package/lib/plugin/telemetry-ext.d.ts.map +1 -1
  45. package/lib/plugin/telemetry-ext.js +19 -3
  46. package/lib/plugin/telemetry-ext.js.map +1 -1
  47. package/lib/plugin/telemetry-ext.spec.d.ts +2 -0
  48. package/lib/plugin/telemetry-ext.spec.d.ts.map +1 -0
  49. package/lib/plugin/telemetry-ext.spec.js +51 -0
  50. package/lib/plugin/telemetry-ext.spec.js.map +1 -0
  51. package/package.json +32 -32
  52. package/src/hosted/browser/hosted-plugin.ts +14 -3
  53. package/src/hosted/common/hosted-plugin.ts +4 -0
  54. package/src/hosted/node/plugin-reader.spec.ts +80 -0
  55. package/src/hosted/node/plugin-reader.ts +16 -7
  56. package/src/main/browser/custom-editors/custom-editor-opener.spec.ts +142 -0
  57. package/src/main/browser/custom-editors/custom-editor-opener.tsx +45 -2
  58. package/src/main/browser/custom-editors/custom-editor-widget.ts +43 -4
  59. package/src/main/browser/preference-registry-main.ts +8 -0
  60. package/src/main/browser/tabs/tabs-main.ts +8 -1
  61. package/src/main/node/plugin-http-resolver.spec.ts +41 -0
  62. package/src/main/node/plugin-http-resolver.ts +4 -3
  63. package/src/main/node/plugin-service.ts +7 -3
  64. package/src/plugin/telemetry-ext.spec.ts +61 -0
  65. package/src/plugin/telemetry-ext.ts +23 -4
@@ -17,21 +17,33 @@
17
17
  import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
18
18
  import URI from '@theia/core/lib/common/uri';
19
19
  import { FileOperation } from '@theia/filesystem/lib/common/files';
20
- import { ApplicationShell, DelegatingSaveable, Message, NavigatableWidget, Saveable, SaveableSource } from '@theia/core/lib/browser';
20
+ import { ApplicationShell, DelegatingSaveable, Message, NavigatableWidget, Saveable, SaveableSource, TabBarTracker } from '@theia/core/lib/browser';
21
21
  import { SaveableService } from '@theia/core/lib/browser/saveable-service';
22
- import { Disposable, SelectionService } from '@theia/core/lib/common';
22
+ import { Disposable, Event, SelectionService } from '@theia/core/lib/common';
23
23
  import { Reference } from '@theia/core/lib/common/reference';
24
+ import { PreviewTabWidget, PreviewTabSupport } from '@theia/editor-preview/lib/browser/preview-tab-widget';
24
25
  import { WebviewWidget } from '../webview/webview';
25
26
  import { CustomEditorModel } from './custom-editors-main';
26
27
  import { CustomEditorWidget as CustomEditorWidgetShape } from '@theia/editor/lib/browser';
27
28
 
28
29
  @injectable()
29
- export class CustomEditorWidget extends WebviewWidget implements CustomEditorWidgetShape, SaveableSource, NavigatableWidget {
30
+ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWidgetShape, SaveableSource, NavigatableWidget, PreviewTabWidget {
30
31
  static override FACTORY_ID = 'plugin-custom-editor';
31
32
  static readonly SIDE_BY_SIDE_FACTORY_ID = CustomEditorWidget.FACTORY_ID + '.side-by-side';
32
33
 
33
34
  resource: URI;
34
35
 
36
+ protected previewSupport: PreviewTabSupport;
37
+ protected tabBarTracker: TabBarTracker;
38
+
39
+ get isPreview(): boolean {
40
+ return this.previewSupport.isPreview;
41
+ }
42
+
43
+ get onDidChangePreviewState(): Event<void> {
44
+ return this.previewSupport.onDidChangePreviewState;
45
+ }
46
+
35
47
  protected _modelRef: Reference<CustomEditorModel | undefined> = { object: undefined, dispose: () => { } };
36
48
  get modelRef(): Reference<CustomEditorModel | undefined> {
37
49
  return this._modelRef;
@@ -62,6 +74,15 @@ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWid
62
74
  protected override init(): void {
63
75
  super.init();
64
76
  this.id = CustomEditorWidget.FACTORY_ID + ':' + this.identifier.id;
77
+ this.previewSupport = new PreviewTabSupport({
78
+ title: this.title,
79
+ saveable: this.saveable,
80
+ toDispose: this.toDispose,
81
+ // This tracker exists only to promote the preview on a tab-bar move; once promoted it has no further use.
82
+ onConvertToNonPreview: () => this.tabBarTracker.dispose()
83
+ });
84
+ this.tabBarTracker = new TabBarTracker(this, (oldTabBar, newTabBar) => this.previewSupport.handleTabBarChange(oldTabBar, newTabBar));
85
+ this.toDispose.push(this.tabBarTracker);
65
86
  this.toDispose.push(this.fileService.onDidRunOperation(e => {
66
87
  if (e.isOperation(FileOperation.MOVE)) {
67
88
  this.doMove(e.target.resource);
@@ -74,11 +95,24 @@ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWid
74
95
  }));
75
96
  }
76
97
 
98
+ initializePreview(): void {
99
+ this.previewSupport.initializePreview();
100
+ }
101
+
102
+ convertToNonPreview(): void {
103
+ this.previewSupport.convertToNonPreview();
104
+ }
105
+
77
106
  protected override onActivateRequest(msg: Message): void {
78
107
  super.onActivateRequest(msg);
79
108
  this.selectionService.selection = this;
80
109
  }
81
110
 
111
+ protected override onAfterAttach(msg: Message): void {
112
+ super.onAfterAttach(msg);
113
+ this.tabBarTracker.check();
114
+ }
115
+
82
116
  updateID(): void {
83
117
  this.id = CustomEditorWidget.FACTORY_ID + `:${this.resource.toString()}:${this.identifier.id}`;
84
118
  }
@@ -103,13 +137,17 @@ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWid
103
137
  return {
104
138
  ...super.storeState(),
105
139
  strResource: this.resource.toString(),
140
+ isPreview: this.isPreview
106
141
  };
107
142
  }
108
143
 
109
144
  override restoreState(oldState: CustomEditorWidget.State): void {
110
- const { strResource } = oldState;
145
+ const { strResource, isPreview } = oldState;
111
146
  this.resource = new URI(strResource);
112
147
  super.restoreState(oldState);
148
+ if (isPreview) {
149
+ this.initializePreview();
150
+ }
113
151
  }
114
152
 
115
153
  onMove(handler: (newResource: URI) => Promise<void>): void {
@@ -128,5 +166,6 @@ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWid
128
166
  export namespace CustomEditorWidget {
129
167
  export interface State extends WebviewWidget.State {
130
168
  strResource: string
169
+ isPreview?: boolean
131
170
  }
132
171
  }
@@ -36,8 +36,16 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
36
36
 
37
37
  export function getPreferences(preferenceProviderProvider: PreferenceProviderProvider, rootFolders: FileStat[]): PreferenceData {
38
38
  const folders = rootFolders.map(root => root.resource.toString());
39
+ // Session-scoped values are process-lifetime CLI overrides (see PreferenceScope.Session).
40
+ // The plugin-side `parse()` in `preference-registry.ts` only reads up to Folder, so
41
+ // shipping session data across would leak potentially security-sensitive overrides
42
+ // (e.g. AI tool auto-approval) into the plugin host without any consumer on the other
43
+ // side. Exclude the scope explicitly until plugins can opt in to session values.
39
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
45
  return PreferenceScope.getScopes().reduce((result: { [key: number]: any }, scope: PreferenceScope) => {
46
+ if (scope === PreferenceScope.Session) {
47
+ return result;
48
+ }
41
49
  result[scope] = {};
42
50
  const provider = preferenceProviderProvider(scope);
43
51
  if (provider) {
@@ -27,6 +27,7 @@ import { DisposableCollection } from '@theia/core';
27
27
  import { NotebookEditorWidget } from '@theia/notebook/lib/browser';
28
28
  import { Deferred } from '@theia/core/lib/common/promise-util';
29
29
  import { MergeEditor } from '@theia/scm/lib/browser/merge-editor/merge-editor';
30
+ import { CustomEditorWidget } from '../custom-editors/custom-editor-widget';
30
31
 
31
32
  interface TabInfo {
32
33
  tab: TabDto;
@@ -181,7 +182,7 @@ export class TabsMainImpl implements TabsMain, Disposable {
181
182
  const oldDto = this.tabGroupModel.get(tabBar);
182
183
  const groupId = oldDto?.groupId ?? this.groupIdCounter++;
183
184
  const tabs = tabBar.titles.map(title => this.createTabDto(title, groupId));
184
- const viewColumn = 0; // TODO: Implement correct viewColumn handling
185
+ const viewColumn = Math.max(0, this.applicationShell.mainAreaTabBars.indexOf(tabBar));
185
186
  return {
186
187
  groupId,
187
188
  tabs,
@@ -225,6 +226,12 @@ export class TabsMainImpl implements TabsMain, Disposable {
225
226
  uri: toUriComponents(widget.editor.uri.toString())
226
227
  };
227
228
  }
229
+ } else if (widget instanceof CustomEditorWidget) {
230
+ return {
231
+ kind: TabInputKind.CustomEditorInput,
232
+ viewType: widget.viewType,
233
+ uri: toUriComponents(widget.resource.toString())
234
+ };
228
235
  } else if (widget instanceof ViewContainer) {
229
236
  return {
230
237
  kind: TabInputKind.WebviewEditorInput,
@@ -0,0 +1,41 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource 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 { HttpPluginDeployerResolver } from './plugin-http-resolver';
19
+ import { PluginDeployerResolverContext } from '../../common';
20
+
21
+ describe('HttpPluginDeployerResolver', () => {
22
+
23
+ function createContext(originId: string): PluginDeployerResolverContext {
24
+ return {
25
+ getOriginId: () => originId
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ } as any as PluginDeployerResolverContext;
28
+ }
29
+
30
+ it('should reject a malformed link URI', async () => {
31
+ const resolver = new HttpPluginDeployerResolver();
32
+ let error: Error | undefined;
33
+ try {
34
+ await resolver.resolve(createContext(':::not a url'));
35
+ } catch (e) {
36
+ error = e as Error;
37
+ }
38
+ expect(error).to.be.an.instanceOf(Error);
39
+ expect(error!.message).to.contain('invalid link URI');
40
+ });
41
+ });
@@ -19,7 +19,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
19
19
  import { Deferred } from '@theia/core/lib/common/promise-util';
20
20
  import { promises as fs } from 'fs';
21
21
  import * as path from 'path';
22
- import * as url from 'url';
23
22
  import { PluginDeployerResolver, PluginDeployerResolverContext } from '../../common';
24
23
  import { getTempDirPathAsync } from './temp-dir-util';
25
24
 
@@ -56,8 +55,10 @@ export class HttpPluginDeployerResolver implements PluginDeployerResolver {
56
55
  // download the file
57
56
  // keep filename of the url
58
57
  const urlPath = pluginResolverContext.getOriginId();
59
- const link = url.parse(urlPath);
60
- if (!link.pathname) {
58
+ let link: URL;
59
+ try {
60
+ link = new URL(urlPath);
61
+ } catch {
61
62
  throw new Error('invalid link URI' + urlPath);
62
63
  }
63
64
 
@@ -16,7 +16,6 @@
16
16
 
17
17
  import * as http from 'http';
18
18
  import * as path from 'path';
19
- import * as url from 'url';
20
19
  const vhost = require('vhost');
21
20
  import * as express from '@theia/core/shared/express';
22
21
  import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
@@ -63,8 +62,13 @@ export class PluginApiContribution implements BackendApplicationContribution, Ws
63
62
 
64
63
  allowWsUpgrade(request: http.IncomingMessage): MaybePromise<boolean> {
65
64
  if (request.headers.origin && !this.serveSameOrigin) {
66
- const origin = url.parse(request.headers.origin);
67
- if (origin.host && this.webviewExternalEndpointRegExp.test(origin.host)) {
65
+ let host: string | undefined;
66
+ try {
67
+ host = new URL(request.headers.origin).host;
68
+ } catch {
69
+ host = undefined;
70
+ }
71
+ if (host && this.webviewExternalEndpointRegExp.test(host)) {
68
72
  // If the origin comes from the WebViews, refuse:
69
73
  return false;
70
74
  }
@@ -0,0 +1,61 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 suzunn 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 * as chai from 'chai';
18
+ import { TelemetryExtImpl } from './telemetry-ext';
19
+
20
+ const expect = chai.expect;
21
+
22
+ describe('TelemetryLogger', () => {
23
+ const sender = {
24
+ sendEventData: () => { },
25
+ sendErrorData: () => { }
26
+ };
27
+
28
+ it('reflects the current telemetry enabled state', () => {
29
+ const telemetry = new TelemetryExtImpl();
30
+ const logger = telemetry.createTelemetryLogger(sender);
31
+
32
+ expect(logger.telemetryEnabled).to.equal(false);
33
+ expect(logger.isUsageEnabled).to.equal(false);
34
+ expect(logger.isErrorsEnabled).to.equal(false);
35
+
36
+ telemetry.isTelemetryEnabled = true;
37
+
38
+ expect(logger.telemetryEnabled).to.equal(true);
39
+ expect(logger.isUsageEnabled).to.equal(true);
40
+ expect(logger.isErrorsEnabled).to.equal(true);
41
+
42
+ telemetry.isTelemetryEnabled = false;
43
+
44
+ expect(logger.telemetryEnabled).to.equal(false);
45
+ expect(logger.isUsageEnabled).to.equal(false);
46
+ expect(logger.isErrorsEnabled).to.equal(false);
47
+ });
48
+
49
+ it('emits enable-state changes when telemetry is toggled', () => {
50
+ const telemetry = new TelemetryExtImpl();
51
+ const logger = telemetry.createTelemetryLogger(sender);
52
+ let changes = 0;
53
+
54
+ logger.onDidChangeEnableStates(() => changes++);
55
+
56
+ telemetry.isTelemetryEnabled = true;
57
+ telemetry.isTelemetryEnabled = false;
58
+
59
+ expect(changes).to.equal(2);
60
+ });
61
+ });
@@ -49,7 +49,7 @@ export class TelemetryLogger {
49
49
  readonly options: TelemetryLoggerOptions | undefined;
50
50
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
51
  readonly commonProperties: Record<string, any>;
52
- telemetryEnabled: boolean;
52
+ private _telemetryEnabled: boolean;
53
53
 
54
54
  private readonly onDidChangeEnableStatesEmitter: Emitter<TelemetryLogger> = new Emitter();
55
55
  readonly onDidChangeEnableStates: Event<TelemetryLogger> = this.onDidChangeEnableStatesEmitter.event;
@@ -60,9 +60,20 @@ export class TelemetryLogger {
60
60
  this.sender = sender;
61
61
  this.options = options;
62
62
  this.commonProperties = this.getCommonProperties();
63
- this._isErrorsEnabled = true;
64
- this._isUsageEnabled = true;
65
- this.telemetryEnabled = telemetryEnabled;
63
+ this._telemetryEnabled = telemetryEnabled;
64
+ this._isErrorsEnabled = telemetryEnabled;
65
+ this._isUsageEnabled = telemetryEnabled;
66
+ }
67
+
68
+ get telemetryEnabled(): boolean {
69
+ return this._telemetryEnabled;
70
+ }
71
+
72
+ set telemetryEnabled(telemetryEnabled: boolean) {
73
+ if (this._telemetryEnabled !== telemetryEnabled) {
74
+ this._telemetryEnabled = telemetryEnabled;
75
+ this.updateEnableStates(telemetryEnabled, telemetryEnabled);
76
+ }
66
77
  }
67
78
 
68
79
  get isUsageEnabled(): boolean {
@@ -87,6 +98,14 @@ export class TelemetryLogger {
87
98
  }
88
99
  }
89
100
 
101
+ private updateEnableStates(isUsageEnabled: boolean, isErrorsEnabled: boolean): void {
102
+ if (this._isUsageEnabled !== isUsageEnabled || this._isErrorsEnabled !== isErrorsEnabled) {
103
+ this._isUsageEnabled = isUsageEnabled;
104
+ this._isErrorsEnabled = isErrorsEnabled;
105
+ this.onDidChangeEnableStatesEmitter.fire(this);
106
+ }
107
+ }
108
+
90
109
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
110
  logUsage(eventName: string, data?: Record<string, any | TelemetryTrustedValue<any>>): void {
92
111
  if (!this.telemetryEnabled || !this.isUsageEnabled) {