@theia/plugin-ext 1.73.0-next.7 → 1.73.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 (169) hide show
  1. package/lib/common/lm-tool-protocol.d.ts +58 -0
  2. package/lib/common/lm-tool-protocol.d.ts.map +1 -0
  3. package/lib/common/lm-tool-protocol.js +38 -0
  4. package/lib/common/lm-tool-protocol.js.map +1 -0
  5. package/lib/common/lm-tool-protocol.spec.d.ts +2 -0
  6. package/lib/common/lm-tool-protocol.spec.d.ts.map +1 -0
  7. package/lib/common/lm-tool-protocol.spec.js +80 -0
  8. package/lib/common/lm-tool-protocol.spec.js.map +1 -0
  9. package/lib/common/plugin-api-rpc-model.d.ts +4 -0
  10. package/lib/common/plugin-api-rpc-model.d.ts.map +1 -1
  11. package/lib/common/plugin-api-rpc-model.js.map +1 -1
  12. package/lib/common/plugin-api-rpc.d.ts +6 -1
  13. package/lib/common/plugin-api-rpc.d.ts.map +1 -1
  14. package/lib/common/plugin-api-rpc.js +4 -2
  15. package/lib/common/plugin-api-rpc.js.map +1 -1
  16. package/lib/common/plugin-protocol.d.ts +8 -0
  17. package/lib/common/plugin-protocol.d.ts.map +1 -1
  18. package/lib/common/plugin-protocol.js.map +1 -1
  19. package/lib/hosted/node/hosted-plugin-localization-service.d.ts +2 -1
  20. package/lib/hosted/node/hosted-plugin-localization-service.d.ts.map +1 -1
  21. package/lib/hosted/node/hosted-plugin-localization-service.js +6 -1
  22. package/lib/hosted/node/hosted-plugin-localization-service.js.map +1 -1
  23. package/lib/hosted/node/plugin-deployer-handler-impl.js +4 -3
  24. package/lib/hosted/node/plugin-deployer-handler-impl.js.map +1 -1
  25. package/lib/hosted/node/plugin-host-module.d.ts.map +1 -1
  26. package/lib/hosted/node/plugin-host-module.js +2 -0
  27. package/lib/hosted/node/plugin-host-module.js.map +1 -1
  28. package/lib/hosted/node/scanners/scanner-theia.d.ts +2 -0
  29. package/lib/hosted/node/scanners/scanner-theia.d.ts.map +1 -1
  30. package/lib/hosted/node/scanners/scanner-theia.js +55 -49
  31. package/lib/hosted/node/scanners/scanner-theia.js.map +1 -1
  32. package/lib/main/browser/comments/comment-thread-widget.d.ts +3 -3
  33. package/lib/main/browser/comments/comment-thread-widget.d.ts.map +1 -1
  34. package/lib/main/browser/comments/comment-thread-widget.js.map +1 -1
  35. package/lib/main/browser/custom-editors/custom-editor-opener.d.ts.map +1 -1
  36. package/lib/main/browser/custom-editors/custom-editor-opener.js +1 -0
  37. package/lib/main/browser/custom-editors/custom-editor-opener.js.map +1 -1
  38. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts +1 -0
  39. package/lib/main/browser/custom-editors/custom-editor-widget.d.ts.map +1 -1
  40. package/lib/main/browser/custom-editors/custom-editor-widget.js +3 -0
  41. package/lib/main/browser/custom-editors/custom-editor-widget.js.map +1 -1
  42. package/lib/main/browser/custom-editors/custom-editors-main.d.ts.map +1 -1
  43. package/lib/main/browser/custom-editors/custom-editors-main.js +2 -0
  44. package/lib/main/browser/custom-editors/custom-editors-main.js.map +1 -1
  45. package/lib/main/browser/debug/debug-main.d.ts +1 -11
  46. package/lib/main/browser/debug/debug-main.d.ts.map +1 -1
  47. package/lib/main/browser/debug/debug-main.js +3 -24
  48. package/lib/main/browser/debug/debug-main.js.map +1 -1
  49. package/lib/main/browser/debug/plugin-debug-service.d.ts +2 -1
  50. package/lib/main/browser/debug/plugin-debug-service.d.ts.map +1 -1
  51. package/lib/main/browser/debug/plugin-debug-service.js +8 -3
  52. package/lib/main/browser/debug/plugin-debug-service.js.map +1 -1
  53. package/lib/main/browser/debug/plugin-debug-session-factory.d.ts +7 -44
  54. package/lib/main/browser/debug/plugin-debug-session-factory.d.ts.map +1 -1
  55. package/lib/main/browser/debug/plugin-debug-session-factory.js +29 -35
  56. package/lib/main/browser/debug/plugin-debug-session-factory.js.map +1 -1
  57. package/lib/main/browser/languages-main.d.ts +6 -1
  58. package/lib/main/browser/languages-main.d.ts.map +1 -1
  59. package/lib/main/browser/languages-main.js +39 -2
  60. package/lib/main/browser/languages-main.js.map +1 -1
  61. package/lib/main/browser/lm-main.js +2 -2
  62. package/lib/main/browser/lm-main.js.map +1 -1
  63. package/lib/main/browser/lm-tool-main.d.ts +14 -0
  64. package/lib/main/browser/lm-tool-main.d.ts.map +1 -0
  65. package/lib/main/browser/lm-tool-main.js +101 -0
  66. package/lib/main/browser/lm-tool-main.js.map +1 -0
  67. package/lib/main/browser/lm-tool-main.spec.d.ts +2 -0
  68. package/lib/main/browser/lm-tool-main.spec.d.ts.map +1 -0
  69. package/lib/main/browser/lm-tool-main.spec.js +100 -0
  70. package/lib/main/browser/lm-tool-main.spec.js.map +1 -0
  71. package/lib/main/browser/main-context.d.ts.map +1 -1
  72. package/lib/main/browser/main-context.js +3 -0
  73. package/lib/main/browser/main-context.js.map +1 -1
  74. package/lib/main/browser/main-file-system-event-service.d.ts +21 -4
  75. package/lib/main/browser/main-file-system-event-service.d.ts.map +1 -1
  76. package/lib/main/browser/main-file-system-event-service.js +47 -17
  77. package/lib/main/browser/main-file-system-event-service.js.map +1 -1
  78. package/lib/main/browser/main-file-system-event-service.spec.js +61 -26
  79. package/lib/main/browser/main-file-system-event-service.spec.js.map +1 -1
  80. package/lib/main/browser/notebooks/renderers/cell-output-webview.d.ts +2 -1
  81. package/lib/main/browser/notebooks/renderers/cell-output-webview.d.ts.map +1 -1
  82. package/lib/main/browser/notebooks/renderers/cell-output-webview.js +11 -5
  83. package/lib/main/browser/notebooks/renderers/cell-output-webview.js.map +1 -1
  84. package/lib/main/browser/plugin-contribution-handler.d.ts +2 -0
  85. package/lib/main/browser/plugin-contribution-handler.d.ts.map +1 -1
  86. package/lib/main/browser/plugin-contribution-handler.js +10 -4
  87. package/lib/main/browser/plugin-contribution-handler.js.map +1 -1
  88. package/lib/main/browser/view/plugin-view-registry.d.ts +1 -0
  89. package/lib/main/browser/view/plugin-view-registry.d.ts.map +1 -1
  90. package/lib/main/browser/view/plugin-view-registry.js +28 -3
  91. package/lib/main/browser/view/plugin-view-registry.js.map +1 -1
  92. package/lib/main/browser/view/plugin-view-registry.spec.js +40 -0
  93. package/lib/main/browser/view/plugin-view-registry.spec.js.map +1 -1
  94. package/lib/main/browser/webview/webview-resource-cache.d.ts +2 -0
  95. package/lib/main/browser/webview/webview-resource-cache.d.ts.map +1 -1
  96. package/lib/main/browser/webview/webview-resource-cache.js +7 -1
  97. package/lib/main/browser/webview/webview-resource-cache.js.map +1 -1
  98. package/lib/main/browser/webview/webview.d.ts +2 -1
  99. package/lib/main/browser/webview/webview.d.ts.map +1 -1
  100. package/lib/main/browser/webview/webview.js +7 -1
  101. package/lib/main/browser/webview/webview.js.map +1 -1
  102. package/lib/main/node/plugin-deployer-impl.js +2 -1
  103. package/lib/main/node/plugin-deployer-impl.js.map +1 -1
  104. package/lib/plugin/languages/diagnostics.d.ts +4 -1
  105. package/lib/plugin/languages/diagnostics.d.ts.map +1 -1
  106. package/lib/plugin/languages/diagnostics.js +22 -9
  107. package/lib/plugin/languages/diagnostics.js.map +1 -1
  108. package/lib/plugin/languages/multi-document-highlight.d.ts +14 -0
  109. package/lib/plugin/languages/multi-document-highlight.d.ts.map +1 -0
  110. package/lib/plugin/languages/multi-document-highlight.js +60 -0
  111. package/lib/plugin/languages/multi-document-highlight.js.map +1 -0
  112. package/lib/plugin/languages.d.ts +3 -1
  113. package/lib/plugin/languages.d.ts.map +1 -1
  114. package/lib/plugin/languages.js +11 -0
  115. package/lib/plugin/languages.js.map +1 -1
  116. package/lib/plugin/lm-tool-ext.d.ts +28 -0
  117. package/lib/plugin/lm-tool-ext.d.ts.map +1 -0
  118. package/lib/plugin/lm-tool-ext.js +171 -0
  119. package/lib/plugin/lm-tool-ext.js.map +1 -0
  120. package/lib/plugin/lm-tool-ext.spec.d.ts +2 -0
  121. package/lib/plugin/lm-tool-ext.spec.d.ts.map +1 -0
  122. package/lib/plugin/lm-tool-ext.spec.js +124 -0
  123. package/lib/plugin/lm-tool-ext.spec.js.map +1 -0
  124. package/lib/plugin/plugin-context.d.ts.map +1 -1
  125. package/lib/plugin/plugin-context.js +10 -11
  126. package/lib/plugin/plugin-context.js.map +1 -1
  127. package/lib/plugin/types-impl.d.ts +0 -9
  128. package/lib/plugin/types-impl.d.ts.map +1 -1
  129. package/lib/plugin/types-impl.js +22 -15
  130. package/lib/plugin/types-impl.js.map +1 -1
  131. package/package.json +32 -31
  132. package/src/common/lm-tool-protocol.spec.ts +89 -0
  133. package/src/common/lm-tool-protocol.ts +102 -0
  134. package/src/common/plugin-api-rpc-model.ts +5 -0
  135. package/src/common/plugin-api-rpc.ts +9 -2
  136. package/src/common/plugin-protocol.ts +9 -0
  137. package/src/hosted/node/hosted-plugin-localization-service.ts +6 -3
  138. package/src/hosted/node/plugin-deployer-handler-impl.ts +5 -5
  139. package/src/hosted/node/plugin-host-module.ts +2 -0
  140. package/src/hosted/node/scanners/scanner-theia.ts +54 -50
  141. package/src/main/browser/comments/comment-thread-widget.tsx +5 -5
  142. package/src/main/browser/custom-editors/custom-editor-opener.tsx +1 -0
  143. package/src/main/browser/custom-editors/custom-editor-widget.ts +4 -0
  144. package/src/main/browser/custom-editors/custom-editors-main.ts +2 -0
  145. package/src/main/browser/debug/debug-main.ts +3 -45
  146. package/src/main/browser/debug/plugin-debug-service.ts +7 -5
  147. package/src/main/browser/debug/plugin-debug-session-factory.ts +25 -71
  148. package/src/main/browser/languages-main.ts +49 -4
  149. package/src/main/browser/lm-main.ts +2 -2
  150. package/src/main/browser/lm-tool-main.spec.ts +126 -0
  151. package/src/main/browser/lm-tool-main.ts +116 -0
  152. package/src/main/browser/main-context.ts +4 -0
  153. package/src/main/browser/main-file-system-event-service.spec.ts +76 -29
  154. package/src/main/browser/main-file-system-event-service.ts +46 -16
  155. package/src/main/browser/notebooks/renderers/cell-output-webview.tsx +11 -7
  156. package/src/main/browser/plugin-contribution-handler.ts +8 -4
  157. package/src/main/browser/plugin-ext-widget.tsx +1 -1
  158. package/src/main/browser/view/plugin-view-registry.spec.ts +69 -1
  159. package/src/main/browser/view/plugin-view-registry.ts +30 -3
  160. package/src/main/browser/webview/webview-resource-cache.ts +6 -2
  161. package/src/main/browser/webview/webview.ts +6 -3
  162. package/src/main/node/plugin-deployer-impl.ts +2 -2
  163. package/src/plugin/languages/diagnostics.ts +28 -10
  164. package/src/plugin/languages/multi-document-highlight.ts +75 -0
  165. package/src/plugin/languages.ts +20 -0
  166. package/src/plugin/lm-tool-ext.spec.ts +152 -0
  167. package/src/plugin/lm-tool-ext.ts +197 -0
  168. package/src/plugin/plugin-context.ts +11 -11
  169. package/src/plugin/types-impl.ts +22 -15
@@ -27,18 +27,20 @@ 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
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
31
31
 
32
32
  export class MainFileSystemEventService implements MainFileSystemEventServiceShape {
33
33
 
34
34
  private readonly toDispose = new DisposableCollection();
35
35
  private readonly watches = new Map<number, Disposable>();
36
+ /** Ancestor-of-workspace roots already skipped, to avoid logging on every re-registration. */
37
+ private readonly skippedWatchRoots = new Set<string>();
36
38
 
37
39
  constructor(
38
40
  rpc: RPCProtocol,
39
41
  container: interfaces.Container,
40
42
  private readonly fileService = container.get(FileService),
41
- private readonly preferences = container.get<FileSystemPreferences>(FileSystemPreferences)
43
+ private readonly workspaceService = container.get(WorkspaceService)
42
44
  ) {
43
45
  const proxy = rpc.getProxy(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService);
44
46
 
@@ -84,26 +86,54 @@ export class MainFileSystemEventService implements MainFileSystemEventServiceSha
84
86
  throw new Error(`There is already a watch request for the key ${session}`);
85
87
  }
86
88
  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) });
89
+ if (this.shouldSkipWatch(uri, options)) {
90
+ // Register a no-op disposable so the session is tracked and `$unwatch` still works.
91
+ this.watches.set(session, Disposable.NULL);
92
+ return;
93
+ }
94
+ // Plugin/language-server watchers (`vscode.workspace.createFileSystemWatcher`) arrive here
95
+ // with an empty `excludes` list; `FileService.watch` applies `files.watcherExclude` centrally
96
+ // for all watchers, so they stay bounded without merging the excludes here.
97
+ const watch = this.fileService.watch(uri, options);
92
98
  this.toDispose.push(watch);
93
99
  this.watches.set(session, watch);
94
100
  }
95
101
 
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
- }
102
+ /**
103
+ * Whether a plugin-requested watch should not be registered at all.
104
+ *
105
+ * Theia's backend ignores the `recursive` flag and always watches recursively. A NON-recursive
106
+ * watch rooted at a strict ancestor of a workspace root - e.g. a language server (such as
107
+ * `redhat.java` / JDT-LS) watching the PARENT of the workspace folder via
108
+ * `RelativePattern(parentDir, folderName)` purely to detect deletion of the folder itself -
109
+ * would therefore be turned into a recursive crawl of every sibling subtree under that parent,
110
+ * i.e. thousands of inodes the workspace does not own, which can exhaust the OS file-watch
111
+ * budget. `files.watcherExclude` cannot bound it because the root is outside the workspace, so
112
+ * the only effective mitigation is to not register the watch.
113
+ *
114
+ * Explicit recursive requests are honored as-is, and watches on or inside a workspace root are
115
+ * left untouched.
116
+ */
117
+ protected shouldSkipWatch(uri: URI, options: WatchOptions): boolean {
118
+ if (options.recursive) {
119
+ return false;
120
+ }
121
+ const roots = this.workspaceService.tryGetRoots();
122
+ // A folder that is itself a workspace root must always be watched, even if it also happens to
123
+ // be a (strict) ancestor of another root in a multi-root workspace where one root is nested
124
+ // inside another. Only watches rooted strictly above every root are dropped.
125
+ const isWorkspaceRoot = roots.some(root => uri.isEqual(root.resource));
126
+ const isAncestorOfWorkspace = !isWorkspaceRoot && roots.some(root => uri.isEqualOrParent(root.resource));
127
+ if (isAncestorOfWorkspace) {
128
+ const key = uri.toString();
129
+ if (!this.skippedWatchRoots.has(key)) {
130
+ this.skippedWatchRoots.add(key);
131
+ console.warn('[MainFileSystemEventService] skipping non-recursive watch rooted at an ancestor of the '
132
+ + `workspace (the backend would recursively crawl sibling trees): ${key}`);
104
133
  }
134
+ return true;
105
135
  }
106
- return Array.from(excludes);
136
+ return false;
107
137
  }
108
138
 
109
139
  $unwatch(session: number): void {
@@ -19,7 +19,7 @@
19
19
  *--------------------------------------------------------------------------------------------*/
20
20
 
21
21
  import * as React from '@theia/core/shared/react';
22
- import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
22
+ import { inject, injectable, interfaces, named } from '@theia/core/shared/inversify';
23
23
  import { generateUuid } from '@theia/core/lib/common/uuid';
24
24
  import {
25
25
  NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry,
@@ -36,7 +36,7 @@ import {
36
36
  CellOutputChange, CellsChangedMessage, CellsMoved, CellsSpliced,
37
37
  ChangePreferredMimetypeMessage, FromWebviewMessage, Output, OutputChangedMessage
38
38
  } from './webview-communication';
39
- import { Disposable, DisposableCollection, Emitter, QuickPickService, nls } from '@theia/core';
39
+ import { Disposable, DisposableCollection, Emitter, ILogger, QuickPickService, nls } from '@theia/core';
40
40
  import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
41
41
  import { NotebookOptionsService, NotebookOutputOptions } from '@theia/notebook/lib/browser/service/notebook-options';
42
42
  import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model';
@@ -224,6 +224,9 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
224
224
  @inject(QuickPickService)
225
225
  protected readonly quickPickService: QuickPickService;
226
226
 
227
+ @inject(ILogger) @named('plugin-ext:CellOutputWebviewImpl')
228
+ protected readonly logger: ILogger;
229
+
227
230
  @inject(AdditionalNotebookCellOutputCss)
228
231
  protected readonly additionalOutputCss: string;
229
232
 
@@ -341,17 +344,18 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
341
344
  }
342
345
 
343
346
  render(): React.JSX.Element {
344
- return <div className='theia-notebook-cell-output-webview' ref={async element => {
347
+ return <div className='theia-notebook-cell-output-webview' ref={element => {
348
+ this.element = element ?? undefined;
345
349
  if (element) {
346
- this.element = element;
347
- await this.webviewWidgetInitialized.promise;
348
- this.attachWebview();
350
+ this.webviewWidgetInitialized.promise
351
+ .then(() => this.attachWebview())
352
+ .catch(error => this.logger.error('Failed to attach notebook output webview.', error));
349
353
  }
350
354
  }}></div>;
351
355
  }
352
356
 
353
357
  protected attachWebview(): void {
354
- if (this.element) {
358
+ if (this.element && !this.isAttached()) {
355
359
  this.webviewWidget.processMessage(new Message('before-attach'));
356
360
  this.element.appendChild(this.webviewWidget.node);
357
361
  this.webviewWidget.processMessage(new Message('after-attach'));
@@ -52,6 +52,7 @@ import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
52
52
  import { LanguageService } from '@theia/core/lib/browser/language-service';
53
53
  import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
54
54
  import { JSONObject, JSONValue } from '@theia/core/shared/@lumino/coreutils';
55
+ import { ILogger } from '@theia/core';
55
56
 
56
57
  // The enum export is missing from `vscode-textmate@9.2.0`
57
58
  const enum StandardTokenType {
@@ -147,6 +148,9 @@ export class PluginContributionHandler {
147
148
  @inject(ContextKeyService)
148
149
  protected readonly contextKeyService: ContextKeyService;
149
150
 
151
+ @inject(ILogger) @named('plugin-ext:PluginContributionHandler')
152
+ protected readonly logger: ILogger;
153
+
150
154
  protected readonly commandHandlers = new Map<string, CommandHandler['execute'] | undefined>();
151
155
 
152
156
  protected readonly onDidRegisterCommandHandlerEmitter = new Emitter<string>();
@@ -164,8 +168,8 @@ export class PluginContributionHandler {
164
168
  }
165
169
  const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ }));
166
170
  /* eslint-disable @typescript-eslint/no-explicit-any */
167
- const logError = (message: string, ...args: any[]) => console.error(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args);
168
- const logWarning = (message: string, ...args: any[]) => console.warn(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args);
171
+ const logError = (message: string, ...args: any[]) => this.logger.error(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args);
172
+ const logWarning = (message: string, ...args: any[]) => this.logger.warn(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args);
169
173
  const pushContribution = (id: string, contribute: () => Disposable) => {
170
174
  if (toDispose.disposed) {
171
175
  return;
@@ -517,7 +521,7 @@ export class PluginContributionHandler {
517
521
 
518
522
  registerCommand(command: Command, enablement?: string): Disposable {
519
523
  if (this.hasCommand(command.id)) {
520
- console.warn(`command '${command.id}' already registered`);
524
+ this.logger.warn(`command '${command.id}' already registered`);
521
525
  return Disposable.NULL;
522
526
  }
523
527
 
@@ -565,7 +569,7 @@ export class PluginContributionHandler {
565
569
 
566
570
  registerCommandHandler(id: string, execute: CommandHandler['execute']): Disposable {
567
571
  if (this.hasCommandHandler(id)) {
568
- console.warn(`command handler '${id}' already registered`);
572
+ this.logger.warn(`command handler '${id}' already registered`);
569
573
  return Disposable.NULL;
570
574
  }
571
575
 
@@ -86,7 +86,7 @@ export class PluginWidget extends ReactWidget {
86
86
  </div>;
87
87
  }
88
88
 
89
- private renderPlugin(plugin: PluginMetadata): JSX.Element {
89
+ private renderPlugin(plugin: PluginMetadata): React.JSX.Element {
90
90
  const unversionedId = PluginIdentifiers.componentsToUnversionedId(plugin.model);
91
91
  const isRestrictedByTrust = this.pluginService.disabledByTrust.has(unversionedId);
92
92
  return <div key={plugin.model.name} className={this.createPluginClassName(plugin, isRestrictedByTrust)}>
@@ -24,7 +24,8 @@ FrontendApplicationConfigProvider.set({});
24
24
 
25
25
  import { expect } from 'chai';
26
26
  import { Disposable } from '@theia/core/lib/common';
27
- import { PluginViewRegistry, ViewContainerInfo } from './plugin-view-registry';
27
+ import { PluginViewRegistry, ViewContainerInfo, PLUGIN_VIEW_DATA_FACTORY_ID } from './plugin-view-registry';
28
+ import type { ViewWelcome } from '../../../common';
28
29
 
29
30
  disableJSDOM();
30
31
 
@@ -103,3 +104,70 @@ describe('PluginViewRegistry - view menu labels', () => {
103
104
  });
104
105
 
105
106
  });
107
+
108
+ describe('PluginViewRegistry - welcome-only views', () => {
109
+
110
+ interface FakeTreeViewWidget {
111
+ model: { root?: unknown };
112
+ welcomeArg?: ViewWelcome[];
113
+ handleViewWelcomeContentChange(welcomes: ViewWelcome[]): void;
114
+ }
115
+
116
+ let registry: PluginViewRegistry;
117
+ let createdWidget: FakeTreeViewWidget;
118
+ let getOrCreateCalls: Array<{ factoryId: string; options: unknown }>;
119
+
120
+ const welcome = (view: string): ViewWelcome => ({ view, content: `[Hi](command:${view}.hi)`, order: 0 });
121
+
122
+ const internals = (): {
123
+ views: Map<string, [string, { type?: unknown }]>;
124
+ viewDataProviders: Map<string, unknown>;
125
+ viewsWelcome: Map<string, ViewWelcome[]>;
126
+ createViewDataWidget(viewId: string): Promise<unknown>;
127
+ } => registry as unknown as {
128
+ views: Map<string, [string, { type?: unknown }]>;
129
+ viewDataProviders: Map<string, unknown>;
130
+ viewsWelcome: Map<string, ViewWelcome[]>;
131
+ createViewDataWidget(viewId: string): Promise<unknown>;
132
+ };
133
+
134
+ beforeEach(() => {
135
+ registry = new PluginViewRegistry();
136
+ getOrCreateCalls = [];
137
+ createdWidget = {
138
+ model: {},
139
+ handleViewWelcomeContentChange(welcomes: ViewWelcome[]): void {
140
+ this.welcomeArg = welcomes;
141
+ }
142
+ };
143
+ (registry as unknown as { widgetManager: unknown }).widgetManager = {
144
+ getOrCreateWidget: async (factoryId: string, options: unknown) => {
145
+ getOrCreateCalls.push({ factoryId, options });
146
+ return createdWidget;
147
+ }
148
+ };
149
+ internals().views.set('actions', ['container', {}]);
150
+ });
151
+
152
+ it('creates a welcome widget for a view with welcomes but no data provider', async () => {
153
+ const welcomes = [welcome('actions')];
154
+ internals().viewsWelcome.set('actions', welcomes);
155
+
156
+ const widget = await internals().createViewDataWidget('actions');
157
+
158
+ expect(widget).to.equal(createdWidget);
159
+ expect(getOrCreateCalls).to.have.lengthOf(1);
160
+ expect(getOrCreateCalls[0].factoryId).to.equal(PLUGIN_VIEW_DATA_FACTORY_ID);
161
+ expect(getOrCreateCalls[0].options).to.deep.equal({ id: 'actions' });
162
+ expect(createdWidget.welcomeArg).to.equal(welcomes);
163
+ expect(createdWidget.model.root).to.not.equal(undefined);
164
+ });
165
+
166
+ it('returns undefined for a view with neither a provider nor welcomes', async () => {
167
+ const widget = await internals().createViewDataWidget('actions');
168
+
169
+ expect(widget).to.equal(undefined);
170
+ expect(getOrCreateCalls).to.have.lengthOf(0);
171
+ });
172
+
173
+ });
@@ -19,6 +19,7 @@ import {
19
19
  ApplicationShell, ViewContainer as ViewContainerWidget, WidgetManager, QuickViewService,
20
20
  ViewContainerIdentifier, ViewContainerTitleOptions, Widget, FrontendApplicationContribution,
21
21
  StatefulWidget, CommonMenus, TreeViewWelcomeWidget, ViewContainerPart, BaseWidget,
22
+ CompositeTreeNode, ExpandableTreeNode,
22
23
  } from '@theia/core/lib/browser';
23
24
  import { ViewContainer, View, ViewWelcome, PluginViewType } from '../../../common';
24
25
  import { PluginSharedStyle } from '../plugin-shared-style';
@@ -643,7 +644,9 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
643
644
  });
644
645
  return _pendingResolution;
645
646
  },
646
- show: webview.show
647
+ show: (preserveFocus: boolean) => {
648
+ this.openView(viewId, preserveFocus ? { reveal: true } : { activate: true });
649
+ }
647
650
  };
648
651
 
649
652
  const toDispose = this.onNewResolverRegistered(resolver => {
@@ -1053,10 +1056,15 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
1053
1056
  if (view?.[1]?.type === PluginViewType.Webview) {
1054
1057
  return this.createWebviewWidget(viewId, webviewId);
1055
1058
  }
1056
- const provider = this.viewDataProviders.get(viewId);
1057
- if (!view || !provider) {
1059
+ if (!view) {
1058
1060
  return undefined;
1059
1061
  }
1062
+ const provider = this.viewDataProviders.get(viewId);
1063
+ if (!provider) {
1064
+ return this.getViewWelcomes(viewId).length > 0
1065
+ ? this.createViewWelcomeWidget(viewId)
1066
+ : undefined;
1067
+ }
1060
1068
  const [, viewInfo] = view;
1061
1069
  const state = this.viewDataState.get(viewId);
1062
1070
  const widget = await provider({ state, viewInfo });
@@ -1069,6 +1077,25 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
1069
1077
  return widget;
1070
1078
  }
1071
1079
 
1080
+ protected async createViewWelcomeWidget(viewId: string): Promise<TreeViewWidget> {
1081
+ const widget = await this.widgetManager.getOrCreateWidget<TreeViewWidget>(PLUGIN_VIEW_DATA_FACTORY_ID, { id: viewId });
1082
+ if (!widget.model.root) {
1083
+ const root: CompositeTreeNode & ExpandableTreeNode = {
1084
+ id: '',
1085
+ parent: undefined,
1086
+ name: '',
1087
+ visible: false,
1088
+ expanded: true,
1089
+ children: []
1090
+ };
1091
+ widget.model.root = root;
1092
+ }
1093
+ // An undefined `model.proxy` keeps `shouldShowWelcomeView()` true, so the welcome content
1094
+ // renders instead of an empty node list.
1095
+ widget.handleViewWelcomeContentChange(this.getViewWelcomes(viewId));
1096
+ return widget;
1097
+ }
1098
+
1072
1099
  protected async createWebviewWidget(viewId: string, webviewId?: string): Promise<Widget | undefined> {
1073
1100
  if (!webviewId) {
1074
1101
  const webviewView = await this.createNewWebviewView(viewId);
@@ -14,9 +14,10 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { injectable } from '@theia/core/shared/inversify';
17
+ import { injectable, inject, named } from '@theia/core/shared/inversify';
18
18
  import { Deferred } from '@theia/core/lib/common/promise-util';
19
19
  import { MaybePromise } from '@theia/core/lib/common/types';
20
+ import { ILogger } from '@theia/core';
20
21
 
21
22
  export interface WebviewResourceResponse {
22
23
  eTag: string | undefined,
@@ -29,6 +30,9 @@ export interface WebviewResourceResponse {
29
30
  @injectable()
30
31
  export class WebviewResourceCache {
31
32
 
33
+ @inject(ILogger) @named('plugin-ext:WebviewResourceCache')
34
+ protected readonly logger: ILogger;
35
+
32
36
  protected readonly cache = new Deferred<Cache | undefined>();
33
37
 
34
38
  constructor() {
@@ -39,7 +43,7 @@ export class WebviewResourceCache {
39
43
  try {
40
44
  this.cache.resolve(await caches.open('webview:v1'));
41
45
  } catch (e) {
42
- console.error('Failed to enable webview caching: ', e);
46
+ this.logger.error('Failed to enable webview caching: ', e);
43
47
  this.cache.resolve(undefined);
44
48
  }
45
49
  }
@@ -22,7 +22,7 @@
22
22
 
23
23
  import * as mime from 'mime';
24
24
  import { JSONExt } from '@theia/core/shared/@lumino/coreutils';
25
- import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
25
+ import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify';
26
26
  import { WebviewPanelOptions, WebviewPortMapping } from '@theia/plugin';
27
27
  import { BaseWidget, Message, codicon } from '@theia/core/lib/browser/widgets/widget';
28
28
  import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
@@ -49,7 +49,7 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
49
49
  import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files';
50
50
  import { BinaryBufferReadableStream } from '@theia/core/lib/common/buffer';
51
51
  import { ExtractableWidget } from '@theia/core/lib/browser/widgets/extractable-widget';
52
- import { MenuPath } from '@theia/core';
52
+ import { MenuPath, ILogger } from '@theia/core';
53
53
  import { ContextMenuRenderer } from '@theia/core/lib/browser';
54
54
  import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
55
55
  import { PluginViewWidget } from '../view/plugin-view-widget';
@@ -166,6 +166,9 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract
166
166
  @inject(ContextKeyService)
167
167
  protected readonly contextKeyService: ContextKeyService;
168
168
 
169
+ @inject(ILogger) @named('plugin-ext:WebviewWidget')
170
+ protected readonly logger: ILogger;
171
+
169
172
  viewState: WebviewPanelViewState = {
170
173
  visible: false,
171
174
  active: false,
@@ -626,7 +629,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget, Extract
626
629
  await this.ready.promise;
627
630
  this.postMessage(channel, data);
628
631
  } catch (e) {
629
- console.error(e);
632
+ this.logger.error(e);
630
633
  }
631
634
  }
632
635
 
@@ -42,7 +42,7 @@ export class PluginDeployerImpl implements PluginDeployer {
42
42
  protected readonly onDidDeployEmitter = new Emitter<void>();
43
43
  readonly onDidDeploy = this.onDidDeployEmitter.event;
44
44
 
45
- @inject(ILogger)
45
+ @inject(ILogger) @named('plugin-ext:PluginDeployerImpl')
46
46
  protected readonly logger: ILogger;
47
47
 
48
48
  @inject(PluginDeployerHandler)
@@ -215,7 +215,7 @@ export class PluginDeployerImpl implements PluginDeployer {
215
215
  }
216
216
  }
217
217
  } catch (e) {
218
- console.error(`Failed to resolve plugins from '${entry.id}'`, e);
218
+ this.logger.error(`Failed to resolve plugins from '${entry.id}'`, e);
219
219
  errors.push(e instanceof Error ? e : new Error(String(e)));
220
220
  }
221
221
  }));
@@ -30,6 +30,7 @@ export class DiagnosticCollection implements theia.DiagnosticCollection {
30
30
  ];
31
31
 
32
32
  private collectionName: string;
33
+ private ownerId: string;
33
34
  private diagnosticsLimitPerResource: number;
34
35
  private proxy: LanguagesMain;
35
36
  private onDidChangeDiagnosticsEmitter: Emitter<theia.DiagnosticChangeEvent>;
@@ -38,8 +39,9 @@ export class DiagnosticCollection implements theia.DiagnosticCollection {
38
39
  private isDisposed: boolean;
39
40
  private onDisposeCallback: (() => void) | undefined;
40
41
 
41
- constructor(name: string, maxCountPerFile: number, proxy: LanguagesMain, onDidChangeDiagnosticsEmitter: Emitter<theia.DiagnosticChangeEvent>) {
42
+ constructor(name: string, owner: string, maxCountPerFile: number, proxy: LanguagesMain, onDidChangeDiagnosticsEmitter: Emitter<theia.DiagnosticChangeEvent>) {
42
43
  this.collectionName = name;
44
+ this.ownerId = owner;
43
45
  this.diagnosticsLimitPerResource = maxCountPerFile;
44
46
  this.proxy = proxy;
45
47
  this.onDidChangeDiagnosticsEmitter = onDidChangeDiagnosticsEmitter;
@@ -53,6 +55,10 @@ export class DiagnosticCollection implements theia.DiagnosticCollection {
53
55
  return this.collectionName;
54
56
  }
55
57
 
58
+ get owner(): string {
59
+ return this.ownerId;
60
+ }
61
+
56
62
  set(uri: theia.Uri, diagnostics: theia.Diagnostic[] | undefined): void;
57
63
  set(entries: [theia.Uri, theia.Diagnostic[] | undefined][]): void;
58
64
  set(arg: theia.Uri | [theia.Uri, theia.Diagnostic[] | undefined][], diagnostics?: theia.Diagnostic[] | undefined): void {
@@ -115,7 +121,7 @@ export class DiagnosticCollection implements theia.DiagnosticCollection {
115
121
  if (this.has(uri)) {
116
122
  this.fireDiagnosticChangeEvent(uri);
117
123
  this.diagnostics.delete(uri.toString());
118
- this.proxy.$changeDiagnostics(this.name, [[uri.toString(), []]]);
124
+ this.proxy.$changeDiagnostics(this.ownerId, [[uri.toString(), []]]);
119
125
  }
120
126
  }
121
127
 
@@ -123,7 +129,7 @@ export class DiagnosticCollection implements theia.DiagnosticCollection {
123
129
  this.ensureNotDisposed();
124
130
  this.fireDiagnosticChangeEvent(this.getAllResourcesUris());
125
131
  this.diagnostics.clear();
126
- this.proxy.$clearDiagnostics(this.name);
132
+ this.proxy.$clearDiagnostics(this.ownerId);
127
133
  }
128
134
 
129
135
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -247,7 +253,7 @@ export class DiagnosticCollection implements theia.DiagnosticCollection {
247
253
  }
248
254
  }
249
255
 
250
- this.proxy.$changeDiagnostics(this.name, markers);
256
+ this.proxy.$changeDiagnostics(this.ownerId, markers);
251
257
  }
252
258
  }
253
259
 
@@ -256,7 +262,12 @@ export class Diagnostics {
256
262
  private static GENERATED_DIAGNOSTIC_COLLECTION_NAME_PREFIX = '_generated_diagnostic_collection_name_#';
257
263
 
258
264
  private proxy: LanguagesMain;
259
- private diagnosticCollections: Map<string, DiagnosticCollection>; // id -> diagnostic collection
265
+ // Collections are keyed by their unique `owner` id (which is the same as `name` for the first
266
+ // collection with that name, but is suffixed when a duplicate name is requested). This matches
267
+ // VS Code's behavior and prevents collisions on the main side's marker owner namespace as well
268
+ // as a latent bug where disposing the first collection would evict the second from the map.
269
+ private diagnosticCollections: Map<string, DiagnosticCollection>;
270
+ private nextOwnerSuffix = 1;
260
271
 
261
272
  private diagnosticsChangedEmitter = new Emitter<theia.DiagnosticChangeEvent>();
262
273
  public readonly onDidChangeDiagnostics: Event<theia.DiagnosticChangeEvent> = this.diagnosticsChangedEmitter.event;
@@ -278,19 +289,26 @@ export class Diagnostics {
278
289
  }
279
290
 
280
291
  createDiagnosticCollection(name?: string): theia.DiagnosticCollection {
292
+ let owner: string;
281
293
  if (!name) {
282
294
  do {
283
295
  name = Diagnostics.GENERATED_DIAGNOSTIC_COLLECTION_NAME_PREFIX + this.getNextId();
284
296
  } while (this.diagnosticCollections.has(name));
285
- } else if (this.diagnosticCollections.has(name)) {
286
- console.warn(`Diagnostic collection with name '${name}' already exist.`);
297
+ owner = name;
298
+ } else if (!this.diagnosticCollections.has(name)) {
299
+ owner = name;
300
+ } else {
301
+ console.warn(`Diagnostic collection with name '${name}' already exists.`);
302
+ do {
303
+ owner = name + this.nextOwnerSuffix++;
304
+ } while (this.diagnosticCollections.has(owner));
287
305
  }
288
306
 
289
- const diagnosticCollection = new DiagnosticCollection(name, Diagnostics.MAX_DIAGNOSTICS_PER_FILE, this.proxy, this.diagnosticsChangedEmitter);
307
+ const diagnosticCollection = new DiagnosticCollection(name, owner, Diagnostics.MAX_DIAGNOSTICS_PER_FILE, this.proxy, this.diagnosticsChangedEmitter);
290
308
  diagnosticCollection.setOnDisposeCallback(() => {
291
- this.diagnosticCollections.delete(name!);
309
+ this.diagnosticCollections.delete(owner);
292
310
  });
293
- this.diagnosticCollections.set(name, diagnosticCollection);
311
+ this.diagnosticCollections.set(owner, diagnosticCollection);
294
312
  return diagnosticCollection;
295
313
  }
296
314
 
@@ -0,0 +1,75 @@
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 { URI } from '@theia/core/shared/vscode-uri';
18
+ import * as theia from '@theia/plugin';
19
+ import { DocumentsExtImpl } from '../documents';
20
+ import * as types from '../types-impl';
21
+ import * as Converter from '../type-converters';
22
+ import { Position } from '../../common/plugin-api-rpc';
23
+ import { MultiDocumentHighlightDto } from '../../common/plugin-api-rpc-model';
24
+ import { UriComponents } from '../../common/uri-components';
25
+
26
+ export class MultiDocumentHighlightAdapter {
27
+
28
+ constructor(
29
+ private readonly provider: theia.MultiDocumentHighlightProvider,
30
+ private readonly documents: DocumentsExtImpl) {
31
+ }
32
+
33
+ provideMultiDocumentHighlights(
34
+ resource: URI,
35
+ position: Position,
36
+ otherResources: UriComponents[],
37
+ token: theia.CancellationToken
38
+ ): Promise<MultiDocumentHighlightDto[] | undefined> {
39
+ const documentData = this.documents.getDocumentData(resource);
40
+ if (!documentData) {
41
+ return Promise.reject(new Error(`There is no document for ${resource}`));
42
+ }
43
+
44
+ const document = documentData.document;
45
+ const zeroBasedPosition = Converter.toPosition(position);
46
+
47
+ const otherDocuments: theia.TextDocument[] = [];
48
+ for (const otherResource of otherResources) {
49
+ const otherUri = URI.revive(otherResource);
50
+ const otherDocData = this.documents.getDocumentData(otherUri);
51
+ if (otherDocData) {
52
+ otherDocuments.push(otherDocData.document);
53
+ }
54
+ }
55
+
56
+ return Promise.resolve(this.provider.provideMultiDocumentHighlights(document, zeroBasedPosition, otherDocuments, token)).then(result => {
57
+ if (!result) {
58
+ return undefined;
59
+ }
60
+
61
+ if (!this.isMultiDocumentHighlightArray(result)) {
62
+ return undefined;
63
+ }
64
+
65
+ return result.map(multiHighlight => ({
66
+ uri: multiHighlight.uri,
67
+ highlights: multiHighlight.highlights.map(h => Converter.fromDocumentHighlight(h))
68
+ }));
69
+ });
70
+ }
71
+
72
+ private isMultiDocumentHighlightArray(array: unknown): array is types.MultiDocumentHighlight[] {
73
+ return Array.isArray(array) && array.length > 0 && array[0] instanceof types.MultiDocumentHighlight;
74
+ }
75
+ }
@@ -44,6 +44,7 @@ import {
44
44
  SerializedDocumentFilter,
45
45
  SignatureHelp,
46
46
  DocumentHighlight,
47
+ MultiDocumentHighlightDto,
47
48
  Range,
48
49
  TextEdit,
49
50
  FormattingOptions,
@@ -81,6 +82,7 @@ import { HoverAdapter } from './languages/hover';
81
82
  import { EvaluatableExpressionAdapter } from './languages/evaluatable-expression';
82
83
  import { InlineValuesAdapter } from './languages/inline-values';
83
84
  import { DocumentHighlightAdapter } from './languages/document-highlight';
85
+ import { MultiDocumentHighlightAdapter } from './languages/multi-document-highlight';
84
86
  import { DocumentFormattingAdapter } from './languages/document-formatting';
85
87
  import { RangeFormattingAdapter } from './languages/range-formatting';
86
88
  import { OnTypeFormattingAdapter } from './languages/on-type-formatting';
@@ -122,6 +124,7 @@ type Adapter = CompletionAdapter |
122
124
  EvaluatableExpressionAdapter |
123
125
  InlineValuesAdapter |
124
126
  DocumentHighlightAdapter |
127
+ MultiDocumentHighlightAdapter |
125
128
  DocumentFormattingAdapter |
126
129
  RangeFormattingAdapter |
127
130
  OnTypeFormattingAdapter |
@@ -466,6 +469,23 @@ export class LanguagesExtImpl implements LanguagesExt {
466
469
  }
467
470
  // ### Document Highlight Provider end
468
471
 
472
+ // ### Multi Document Highlight Provider begin
473
+ registerMultiDocumentHighlightProvider(
474
+ selector: theia.DocumentSelector, provider: theia.MultiDocumentHighlightProvider, pluginInfo: PluginInfo
475
+ ): theia.Disposable {
476
+ const callId = this.addNewAdapter(new MultiDocumentHighlightAdapter(provider, this.documents));
477
+ this.proxy.$registerMultiDocumentHighlightProvider(callId, pluginInfo, this.transformDocumentSelector(selector));
478
+ return this.createDisposable(callId);
479
+ }
480
+
481
+ $provideMultiDocumentHighlights(
482
+ handle: number, resource: UriComponents, position: Position, otherResources: UriComponents[], token: theia.CancellationToken
483
+ ): Promise<MultiDocumentHighlightDto[] | undefined> {
484
+ return this.withAdapter(handle, MultiDocumentHighlightAdapter,
485
+ adapter => adapter.provideMultiDocumentHighlights(URI.revive(resource), position, otherResources, token), undefined);
486
+ }
487
+ // ### Multi Document Highlight Provider end
488
+
469
489
  // ### WorkspaceSymbol Provider begin
470
490
  registerWorkspaceSymbolProvider(provider: theia.WorkspaceSymbolProvider, pluginInfo: PluginInfo): theia.Disposable {
471
491
  const callId = this.addNewAdapter(new WorkspaceSymbolAdapter(provider));