@theia/plugin-ext 1.73.0-next.2 → 1.73.0-next.20

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 (38) hide show
  1. package/lib/hosted/node/scanners/scanner-theia.d.ts.map +1 -1
  2. package/lib/hosted/node/scanners/scanner-theia.js +8 -1
  3. package/lib/hosted/node/scanners/scanner-theia.js.map +1 -1
  4. package/lib/main/browser/custom-editors/custom-editor-opener.d.ts.map +1 -1
  5. package/lib/main/browser/custom-editors/custom-editor-opener.js +1 -0
  6. package/lib/main/browser/custom-editors/custom-editor-opener.js.map +1 -1
  7. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts +1 -0
  8. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts.map +1 -1
  9. package/lib/main/browser/custom-editors/custom-editor-widget.js +3 -0
  10. package/lib/main/browser/custom-editors/custom-editor-widget.js.map +1 -1
  11. package/lib/main/browser/custom-editors/custom-editors-main.d.ts.map +1 -1
  12. package/lib/main/browser/custom-editors/custom-editors-main.js +2 -0
  13. package/lib/main/browser/custom-editors/custom-editors-main.js.map +1 -1
  14. package/lib/main/browser/main-file-system-event-service.d.ts +5 -1
  15. package/lib/main/browser/main-file-system-event-service.d.ts.map +1 -1
  16. package/lib/main/browser/main-file-system-event-service.js +21 -2
  17. package/lib/main/browser/main-file-system-event-service.js.map +1 -1
  18. package/lib/main/browser/main-file-system-event-service.spec.d.ts +2 -0
  19. package/lib/main/browser/main-file-system-event-service.spec.d.ts.map +1 -0
  20. package/lib/main/browser/main-file-system-event-service.spec.js +65 -0
  21. package/lib/main/browser/main-file-system-event-service.spec.js.map +1 -0
  22. package/lib/main/browser/view/plugin-view-registry.d.ts +7 -0
  23. package/lib/main/browser/view/plugin-view-registry.d.ts.map +1 -1
  24. package/lib/main/browser/view/plugin-view-registry.js +85 -6
  25. package/lib/main/browser/view/plugin-view-registry.js.map +1 -1
  26. package/lib/main/browser/view/plugin-view-registry.spec.d.ts +2 -0
  27. package/lib/main/browser/view/plugin-view-registry.spec.d.ts.map +1 -0
  28. package/lib/main/browser/view/plugin-view-registry.spec.js +80 -0
  29. package/lib/main/browser/view/plugin-view-registry.spec.js.map +1 -0
  30. package/package.json +29 -29
  31. package/src/hosted/node/scanners/scanner-theia.ts +9 -1
  32. package/src/main/browser/custom-editors/custom-editor-opener.tsx +1 -0
  33. package/src/main/browser/custom-editors/custom-editor-widget.ts +4 -0
  34. package/src/main/browser/custom-editors/custom-editors-main.ts +2 -0
  35. package/src/main/browser/main-file-system-event-service.spec.ts +78 -0
  36. package/src/main/browser/main-file-system-event-service.ts +22 -2
  37. package/src/main/browser/view/plugin-view-registry.spec.ts +105 -0
  38. package/src/main/browser/view/plugin-view-registry.ts +91 -6
@@ -0,0 +1,78 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Safi Seid-Ahmad, K2view 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
+ const disableJSDOM = enableJSDOM();
19
+
20
+ import * as assert from 'assert';
21
+ import { Disposable } from '@theia/core/lib/common/disposable';
22
+ import { WatchOptions } from '@theia/filesystem/lib/common/files';
23
+ import { UriComponents } from '../../common/uri-components';
24
+ import { MainFileSystemEventService } from './main-file-system-event-service';
25
+
26
+ disableJSDOM();
27
+
28
+ /* eslint-disable @typescript-eslint/no-explicit-any */
29
+
30
+ // A VS Code plugin (e.g. `redhat.java` hosting JDT-LS) that calls
31
+ // `vscode.workspace.createFileSystemWatcher` with a `RelativePattern` rooted at an absolute base
32
+ // triggers `ExtHostFileSystemEventService.ensureWatching`, which sends `$watch` to this main-side
33
+ // service with an EMPTY `excludes` list. As a result the user's `files.watcherExclude` preference
34
+ // was never applied to these (often sibling-of-workspace) recursive watches, so they crawled and
35
+ // watched whole external trees, exhausting the OS file-watch budget.
36
+ //
37
+ // This test pins the fix: the main side must merge the configured `files.watcherExclude` patterns
38
+ // into the watch options before delegating to the `FileService`.
39
+ describe('MainFileSystemEventService files.watcherExclude handling', () => {
40
+
41
+ function componentsFor(path: string): UriComponents {
42
+ return { scheme: 'file', authority: '', path, query: '', fragment: '' };
43
+ }
44
+
45
+ it('applies files.watcherExclude to plugin-created watches that arrive with empty excludes', () => {
46
+ const watchCalls: WatchOptions[] = [];
47
+
48
+ const fileService: any = {
49
+ onDidFilesChange: () => Disposable.NULL,
50
+ onDidRunUserOperation: () => Disposable.NULL,
51
+ addFileOperationParticipant: () => Disposable.NULL,
52
+ watch: (_resource: any, options: WatchOptions) => {
53
+ watchCalls.push(options);
54
+ return Disposable.NULL;
55
+ }
56
+ };
57
+
58
+ const preferences: any = {
59
+ get: (preferenceName: string) => preferenceName === 'files.watcherExclude'
60
+ ? { '**/node_modules/**': true, '**/.git/objects/**': true, '**/disabled-exclude/**': false }
61
+ : undefined
62
+ };
63
+
64
+ const rpc: any = { getProxy: () => ({}) };
65
+
66
+ const service = new MainFileSystemEventService(rpc, {} as any, fileService, preferences);
67
+
68
+ // Mirrors what `ensureWatching` sends for an absolute RelativePattern base outside the workspace.
69
+ service.$watch(1, componentsFor('/outside/workspace/storage'), { recursive: true, excludes: [] });
70
+
71
+ assert.strictEqual(watchCalls.length, 1, 'FileService.watch should have been called once');
72
+ const excludes = watchCalls[0].excludes;
73
+ assert.ok(excludes.includes('**/node_modules/**'), 'enabled watcherExclude pattern should be applied');
74
+ assert.ok(excludes.includes('**/.git/objects/**'), 'enabled watcherExclude pattern should be applied');
75
+ assert.ok(!excludes.includes('**/disabled-exclude/**'), 'patterns set to `false` must not be applied');
76
+ });
77
+
78
+ });
@@ -27,6 +27,7 @@ import { URI } from '@theia/core';
27
27
  import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
28
28
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
29
29
  import { FileChangeType, WatchOptions } from '@theia/filesystem/lib/common/files';
30
+ import { FileSystemPreferences } from '@theia/filesystem/lib/common/filesystem-preferences';
30
31
 
31
32
  export class MainFileSystemEventService implements MainFileSystemEventServiceShape {
32
33
 
@@ -36,7 +37,8 @@ export class MainFileSystemEventService implements MainFileSystemEventServiceSha
36
37
  constructor(
37
38
  rpc: RPCProtocol,
38
39
  container: interfaces.Container,
39
- private readonly fileService = container.get(FileService)
40
+ private readonly fileService = container.get(FileService),
41
+ private readonly preferences = container.get<FileSystemPreferences>(FileSystemPreferences)
40
42
  ) {
41
43
  const proxy = rpc.getProxy(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService);
42
44
 
@@ -81,11 +83,29 @@ export class MainFileSystemEventService implements MainFileSystemEventServiceSha
81
83
  if (this.watches.has(session)) {
82
84
  throw new Error(`There is already a watch request for the key ${session}`);
83
85
  }
84
- const watch = this.fileService.watch(URI.fromComponents(resource), options);
86
+ const uri = URI.fromComponents(resource);
87
+ // Plugin-created watchers (`vscode.workspace.createFileSystemWatcher`) arrive here with an
88
+ // empty `excludes` list. Language servers frequently request recursive watches rooted at
89
+ // absolute paths outside the workspace (e.g. JDT-LS's per-project globs), so apply the
90
+ // user's `files.watcherExclude` here to keep the number of OS file watches bounded.
91
+ const watch = this.fileService.watch(uri, { ...options, excludes: this.getExcludes(uri, options.excludes) });
85
92
  this.toDispose.push(watch);
86
93
  this.watches.set(session, watch);
87
94
  }
88
95
 
96
+ protected getExcludes(uri: URI, requested: string[] = []): string[] {
97
+ const configured = this.preferences.get('files.watcherExclude', undefined, uri.toString());
98
+ const excludes = new Set(requested);
99
+ if (configured) {
100
+ for (const pattern of Object.keys(configured)) {
101
+ if (configured[pattern]) {
102
+ excludes.add(pattern);
103
+ }
104
+ }
105
+ }
106
+ return Array.from(excludes);
107
+ }
108
+
89
109
  $unwatch(session: number): void {
90
110
  const watch = this.watches.get(session);
91
111
  if (watch) {
@@ -0,0 +1,105 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Safi Seid-Ahmad, K2view 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
+ // PluginViewRegistry transitively imports browser widgets (Lumino) that touch `document` at load time.
19
+ const disableJSDOM = enableJSDOM();
20
+
21
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
22
+ // Some transitively imported modules read the frontend config at load time.
23
+ FrontendApplicationConfigProvider.set({});
24
+
25
+ import { expect } from 'chai';
26
+ import { Disposable } from '@theia/core/lib/common';
27
+ import { PluginViewRegistry, ViewContainerInfo } from './plugin-view-registry';
28
+
29
+ disableJSDOM();
30
+
31
+ interface RegisteredMenuAction {
32
+ commandId: string;
33
+ label: string;
34
+ when?: string;
35
+ }
36
+
37
+ /**
38
+ * Minimal stand-in for {@link MenuModelRegistry} that records the menu actions registered for the
39
+ * "Views" menu and removes them again when the returned disposable is disposed. This lets us assert
40
+ * on the labels {@link PluginViewRegistry} ends up showing without wiring the full DI container.
41
+ */
42
+ class RecordingMenuModelRegistry {
43
+ readonly actions = new Map<string, RegisteredMenuAction>();
44
+
45
+ registerMenuAction(_menuPath: unknown, action: RegisteredMenuAction): Disposable {
46
+ this.actions.set(action.commandId, { ...action });
47
+ return Disposable.create(() => this.actions.delete(action.commandId));
48
+ }
49
+ }
50
+
51
+ describe('PluginViewRegistry - view menu labels', () => {
52
+
53
+ let registry: PluginViewRegistry;
54
+ let menus: RecordingMenuModelRegistry;
55
+
56
+ const toggleCommandId = (id: string): string => `plugin.view-container.${id}.toggle`;
57
+ const internals = (): {
58
+ viewContainers: Map<string, ViewContainerInfo>;
59
+ registerViewMenuAction(containerId: string, label: string): Disposable;
60
+ } => registry as unknown as {
61
+ viewContainers: Map<string, ViewContainerInfo>;
62
+ registerViewMenuAction(containerId: string, label: string): Disposable;
63
+ };
64
+
65
+ function registerContainer(id: string, label: string, location: string): Disposable {
66
+ internals().viewContainers.set(id, { id, location, options: { label }, onViewAdded: () => { } });
67
+ return internals().registerViewMenuAction(id, label);
68
+ }
69
+
70
+ beforeEach(() => {
71
+ registry = new PluginViewRegistry();
72
+ menus = new RecordingMenuModelRegistry();
73
+ (registry as unknown as { menus: unknown }).menus = menus;
74
+ });
75
+
76
+ it('uses the plain label for a single container', () => {
77
+ registerContainer('a', 'Claude Code', 'left');
78
+ expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Claude Code');
79
+ });
80
+
81
+ it('leaves distinct labels unsuffixed', () => {
82
+ registerContainer('a', 'Explorer', 'left');
83
+ registerContainer('b', 'Claude Code', 'right');
84
+ expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Explorer');
85
+ expect(menus.actions.get(toggleCommandId('b'))?.label).to.equal('Claude Code');
86
+ });
87
+
88
+ it('suffixes the location when two containers share a label', () => {
89
+ registerContainer('a', 'Claude Code', 'left');
90
+ registerContainer('b', 'Claude Code', 'right');
91
+ expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Claude Code (Side Bar)');
92
+ expect(menus.actions.get(toggleCommandId('b'))?.label).to.equal('Claude Code (Secondary Side Bar)');
93
+ });
94
+
95
+ it('drops the suffix from the remaining container once the duplicate is removed', () => {
96
+ registerContainer('a', 'Claude Code', 'left');
97
+ const disposeB = registerContainer('b', 'Claude Code', 'right');
98
+
99
+ disposeB.dispose();
100
+
101
+ expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Claude Code');
102
+ expect(menus.actions.has(toggleCommandId('b'))).to.equal(false);
103
+ });
104
+
105
+ });
@@ -111,6 +111,9 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
111
111
  private readonly viewClauseContexts = new Map<string, Set<string> | undefined>();
112
112
  private readonly viewContainerClauseContexts = new Map<string, Set<string> | undefined>();
113
113
 
114
+ private readonly viewMenuLabelToContainerIds = new Map<string, Set<string>>();
115
+ private readonly viewMenuDisposables = new Map<string, Disposable>();
116
+
114
117
  private readonly viewDataProviders = new Map<string, ViewDataProvider>();
115
118
  private readonly viewDataState = new Map<string, object>();
116
119
 
@@ -374,13 +377,10 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
374
377
  }, {
375
378
  execute: () => this.toggleViewContainer(id)
376
379
  }));
377
- toDispose.push(this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
378
- commandId: toggleCommandId,
379
- label: options.label,
380
- when
381
- }));
380
+ toDispose.push(this.registerViewMenuAction(id, options.label));
382
381
  toDispose.push(this.quickView?.registerItem({
383
382
  label: options.label,
383
+ description: this.getLocationDescription(location),
384
384
  when,
385
385
  open: async () => {
386
386
  const widget = await this.openViewContainer(id);
@@ -408,6 +408,88 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
408
408
  return toDispose;
409
409
  }
410
410
 
411
+ protected getLocationDescription(location: string | undefined): string | undefined {
412
+ switch (location) {
413
+ case 'left': return nls.localizeByDefault('Side Bar');
414
+ case 'right': return nls.localizeByDefault('Secondary Side Bar');
415
+ case 'bottom': return nls.localizeByDefault('Panel');
416
+ default: return undefined;
417
+ }
418
+ }
419
+
420
+ protected getContainerLocation(viewContainerId: string): string | undefined {
421
+ if (PluginViewRegistry.BUILTIN_VIEW_CONTAINERS.has(viewContainerId)) {
422
+ return 'left';
423
+ }
424
+ return this.viewContainers.get(viewContainerId)?.location;
425
+ }
426
+
427
+ protected getViewQuickPickDescription(viewContainerId: string): string | undefined {
428
+ const locationDescription = this.getLocationDescription(this.getContainerLocation(viewContainerId));
429
+ const containerLabel = this.viewContainers.get(viewContainerId)?.options.label;
430
+ if (locationDescription && containerLabel) {
431
+ return `${locationDescription} / ${containerLabel}`;
432
+ }
433
+ return locationDescription;
434
+ }
435
+
436
+ protected registerViewMenuAction(containerId: string, label: string): Disposable {
437
+ let ids = this.viewMenuLabelToContainerIds.get(label);
438
+ if (!ids) {
439
+ ids = new Set();
440
+ this.viewMenuLabelToContainerIds.set(label, ids);
441
+ }
442
+ ids.add(containerId);
443
+ this.refreshViewMenuLabel(label);
444
+ return Disposable.create(() => {
445
+ const set = this.viewMenuLabelToContainerIds.get(label);
446
+ if (set) {
447
+ set.delete(containerId);
448
+ if (set.size === 0) {
449
+ this.viewMenuLabelToContainerIds.delete(label);
450
+ }
451
+ }
452
+ const disposable = this.viewMenuDisposables.get(containerId);
453
+ if (disposable) {
454
+ disposable.dispose();
455
+ this.viewMenuDisposables.delete(containerId);
456
+ }
457
+ this.refreshViewMenuLabel(label);
458
+ });
459
+ }
460
+
461
+ protected refreshViewMenuLabel(label: string): void {
462
+ const ids = this.viewMenuLabelToContainerIds.get(label);
463
+ if (!ids) {
464
+ return;
465
+ }
466
+ const useSuffix = ids.size > 1;
467
+ for (const id of ids) {
468
+ const existing = this.viewMenuDisposables.get(id);
469
+ if (existing) {
470
+ existing.dispose();
471
+ this.viewMenuDisposables.delete(id);
472
+ }
473
+ const containerInfo = this.viewContainers.get(id);
474
+ if (!containerInfo) {
475
+ continue;
476
+ }
477
+ let menuLabel = label;
478
+ if (useSuffix) {
479
+ const description = this.getLocationDescription(containerInfo.location);
480
+ if (description) {
481
+ menuLabel = `${label} (${description})`;
482
+ }
483
+ }
484
+ const disposable = this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
485
+ commandId: `plugin.view-container.${id}.toggle`,
486
+ label: menuLabel,
487
+ when: containerInfo.when
488
+ });
489
+ this.viewMenuDisposables.set(id, disposable);
490
+ }
491
+ }
492
+
411
493
  protected isViewContainerVisible(containerId: string): boolean {
412
494
  const info = this.viewContainers.get(containerId);
413
495
  if (!info) {
@@ -471,6 +553,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
471
553
  }
472
554
  toDispose.push(this.quickView?.registerItem({
473
555
  label: view.name,
556
+ description: this.getViewQuickPickDescription(viewContainerId),
474
557
  when: view.when,
475
558
  open: () => this.openView(view.id, { activate: true })
476
559
  }));
@@ -560,7 +643,9 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
560
643
  });
561
644
  return _pendingResolution;
562
645
  },
563
- show: webview.show
646
+ show: (preserveFocus: boolean) => {
647
+ this.openView(viewId, preserveFocus ? { reveal: true } : { activate: true });
648
+ }
564
649
  };
565
650
 
566
651
  const toDispose = this.onNewResolverRegistered(resolver => {