@theia/core 1.74.0-next.8 → 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 (209) hide show
  1. package/README.md +10 -10
  2. package/i18n/nls.cs.json +143 -16
  3. package/i18n/nls.de.json +143 -16
  4. package/i18n/nls.es.json +143 -16
  5. package/i18n/nls.fr.json +143 -16
  6. package/i18n/nls.hu.json +143 -16
  7. package/i18n/nls.it.json +143 -16
  8. package/i18n/nls.ja.json +143 -16
  9. package/i18n/nls.json +143 -16
  10. package/i18n/nls.ko.json +143 -16
  11. package/i18n/nls.pl.json +143 -16
  12. package/i18n/nls.pt-br.json +143 -16
  13. package/i18n/nls.ru.json +143 -16
  14. package/i18n/nls.tr.json +143 -16
  15. package/i18n/nls.zh-cn.json +143 -16
  16. package/i18n/nls.zh-tw.json +143 -16
  17. package/lib/browser/browser-clipboard-service.d.ts +3 -0
  18. package/lib/browser/browser-clipboard-service.d.ts.map +1 -1
  19. package/lib/browser/browser-clipboard-service.js +8 -1
  20. package/lib/browser/browser-clipboard-service.js.map +1 -1
  21. package/lib/browser/catalog.json +419 -41
  22. package/lib/browser/clipboard-service.d.ts +11 -0
  23. package/lib/browser/clipboard-service.d.ts.map +1 -1
  24. package/lib/browser/clipboard-service.js.map +1 -1
  25. package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
  26. package/lib/browser/common-frontend-contribution.js +7 -7
  27. package/lib/browser/common-frontend-contribution.js.map +1 -1
  28. package/lib/browser/frontend-application-module.d.ts.map +1 -1
  29. package/lib/browser/frontend-application-module.js +9 -0
  30. package/lib/browser/frontend-application-module.js.map +1 -1
  31. package/lib/browser/frontend-application.d.ts.map +1 -1
  32. package/lib/browser/frontend-application.js +12 -2
  33. package/lib/browser/frontend-application.js.map +1 -1
  34. package/lib/browser/index.d.ts +1 -0
  35. package/lib/browser/index.d.ts.map +1 -1
  36. package/lib/browser/index.js +1 -0
  37. package/lib/browser/index.js.map +1 -1
  38. package/lib/browser/keybinding.d.ts.map +1 -1
  39. package/lib/browser/keybinding.js +12 -7
  40. package/lib/browser/keybinding.js.map +1 -1
  41. package/lib/browser/perspective-service.d.ts +173 -0
  42. package/lib/browser/perspective-service.d.ts.map +1 -0
  43. package/lib/browser/perspective-service.js +484 -0
  44. package/lib/browser/perspective-service.js.map +1 -0
  45. package/lib/browser/perspective-service.spec.d.ts +2 -0
  46. package/lib/browser/perspective-service.spec.d.ts.map +1 -0
  47. package/lib/browser/perspective-service.spec.js +1806 -0
  48. package/lib/browser/perspective-service.spec.js.map +1 -0
  49. package/lib/browser/preferences/preference-contribution.d.ts.map +1 -1
  50. package/lib/browser/preferences/preference-contribution.js +9 -1
  51. package/lib/browser/preferences/preference-contribution.js.map +1 -1
  52. package/lib/browser/preferences/preference-service.spec.js +63 -0
  53. package/lib/browser/preferences/preference-service.spec.js.map +1 -1
  54. package/lib/browser/shell/application-shell.d.ts +7 -0
  55. package/lib/browser/shell/application-shell.d.ts.map +1 -1
  56. package/lib/browser/shell/application-shell.js +38 -28
  57. package/lib/browser/shell/application-shell.js.map +1 -1
  58. package/lib/browser/shell/shell-layout-restorer.d.ts +9 -0
  59. package/lib/browser/shell/shell-layout-restorer.d.ts.map +1 -1
  60. package/lib/browser/shell/shell-layout-restorer.js +124 -14
  61. package/lib/browser/shell/shell-layout-restorer.js.map +1 -1
  62. package/lib/browser/shell/shell-layout-restorer.spec.d.ts +2 -0
  63. package/lib/browser/shell/shell-layout-restorer.spec.d.ts.map +1 -0
  64. package/lib/browser/shell/shell-layout-restorer.spec.js +539 -0
  65. package/lib/browser/shell/shell-layout-restorer.spec.js.map +1 -0
  66. package/lib/browser/shell/view-contribution.d.ts +2 -0
  67. package/lib/browser/shell/view-contribution.d.ts.map +1 -1
  68. package/lib/browser/shell/view-contribution.js +5 -1
  69. package/lib/browser/shell/view-contribution.js.map +1 -1
  70. package/lib/browser/status-bar/status-bar.d.ts +2 -0
  71. package/lib/browser/status-bar/status-bar.d.ts.map +1 -1
  72. package/lib/browser/status-bar/status-bar.js +11 -3
  73. package/lib/browser/status-bar/status-bar.js.map +1 -1
  74. package/lib/browser/tree/tree-widget.d.ts +7 -33
  75. package/lib/browser/tree/tree-widget.d.ts.map +1 -1
  76. package/lib/browser/tree/tree-widget.js +7 -54
  77. package/lib/browser/tree/tree-widget.js.map +1 -1
  78. package/lib/browser/widgets/index.d.ts +1 -0
  79. package/lib/browser/widgets/index.d.ts.map +1 -1
  80. package/lib/browser/widgets/index.js +1 -0
  81. package/lib/browser/widgets/index.js.map +1 -1
  82. package/lib/browser/widgets/select-component.d.ts +2 -2
  83. package/lib/browser/widgets/select-component.d.ts.map +1 -1
  84. package/lib/browser/widgets/select-component.js +42 -12
  85. package/lib/browser/widgets/select-component.js.map +1 -1
  86. package/lib/browser/widgets/tab-bar-tracker.d.ts +23 -0
  87. package/lib/browser/widgets/tab-bar-tracker.d.ts.map +1 -0
  88. package/lib/browser/widgets/tab-bar-tracker.js +65 -0
  89. package/lib/browser/widgets/tab-bar-tracker.js.map +1 -0
  90. package/lib/browser/widgets/tab-bar-tracker.spec.d.ts +2 -0
  91. package/lib/browser/widgets/tab-bar-tracker.spec.d.ts.map +1 -0
  92. package/lib/browser/widgets/tab-bar-tracker.spec.js +109 -0
  93. package/lib/browser/widgets/tab-bar-tracker.spec.js.map +1 -0
  94. package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts +13 -2
  95. package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts.map +1 -1
  96. package/lib/browser-only/messaging/frontend-only-service-connection-provider.js +31 -3
  97. package/lib/browser-only/messaging/frontend-only-service-connection-provider.js.map +1 -1
  98. package/lib/common/preferences/index.d.ts +1 -0
  99. package/lib/common/preferences/index.d.ts.map +1 -1
  100. package/lib/common/preferences/index.js +1 -0
  101. package/lib/common/preferences/index.js.map +1 -1
  102. package/lib/common/preferences/preference-provider.d.ts +15 -0
  103. package/lib/common/preferences/preference-provider.d.ts.map +1 -1
  104. package/lib/common/preferences/preference-provider.js +21 -1
  105. package/lib/common/preferences/preference-provider.js.map +1 -1
  106. package/lib/common/preferences/preference-provider.spec.js +17 -0
  107. package/lib/common/preferences/preference-provider.spec.js.map +1 -1
  108. package/lib/common/preferences/preference-scope.d.ts +6 -1
  109. package/lib/common/preferences/preference-scope.d.ts.map +1 -1
  110. package/lib/common/preferences/preference-scope.js +5 -0
  111. package/lib/common/preferences/preference-scope.js.map +1 -1
  112. package/lib/common/preferences/preference-scope.spec.js +5 -3
  113. package/lib/common/preferences/preference-scope.spec.js.map +1 -1
  114. package/lib/common/preferences/preference-service.d.ts +19 -0
  115. package/lib/common/preferences/preference-service.d.ts.map +1 -1
  116. package/lib/common/preferences/preference-service.js +34 -5
  117. package/lib/common/preferences/preference-service.js.map +1 -1
  118. package/lib/common/preferences/session-preference-provider.d.ts +17 -0
  119. package/lib/common/preferences/session-preference-provider.d.ts.map +1 -0
  120. package/lib/common/preferences/session-preference-provider.js +77 -0
  121. package/lib/common/preferences/session-preference-provider.js.map +1 -0
  122. package/lib/common/preferences/session-preference-provider.spec.d.ts +2 -0
  123. package/lib/common/preferences/session-preference-provider.spec.d.ts.map +1 -0
  124. package/lib/common/preferences/session-preference-provider.spec.js +71 -0
  125. package/lib/common/preferences/session-preference-provider.spec.js.map +1 -0
  126. package/lib/electron-browser/electron-clipboard-service.d.ts +3 -0
  127. package/lib/electron-browser/electron-clipboard-service.d.ts.map +1 -1
  128. package/lib/electron-browser/electron-clipboard-service.js +6 -0
  129. package/lib/electron-browser/electron-clipboard-service.js.map +1 -1
  130. package/lib/electron-browser/preload.d.ts.map +1 -1
  131. package/lib/electron-browser/preload.js +2 -1
  132. package/lib/electron-browser/preload.js.map +1 -1
  133. package/lib/electron-common/electron-api.d.ts +2 -0
  134. package/lib/electron-common/electron-api.d.ts.map +1 -1
  135. package/lib/electron-common/electron-api.js +2 -1
  136. package/lib/electron-common/electron-api.js.map +1 -1
  137. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  138. package/lib/electron-main/electron-api-main.js +43 -0
  139. package/lib/electron-main/electron-api-main.js.map +1 -1
  140. package/lib/electron-main/electron-main-application.d.ts.map +1 -1
  141. package/lib/electron-main/electron-main-application.js +3 -0
  142. package/lib/electron-main/electron-main-application.js.map +1 -1
  143. package/lib/electron-node/hosting/electron-backend-hosting-module.d.ts.map +1 -1
  144. package/lib/electron-node/hosting/electron-backend-hosting-module.js +7 -0
  145. package/lib/electron-node/hosting/electron-backend-hosting-module.js.map +1 -1
  146. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts +2 -0
  147. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts.map +1 -0
  148. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js +39 -0
  149. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js.map +1 -0
  150. package/lib/node/hosting/backend-hosting-module.d.ts.map +1 -1
  151. package/lib/node/hosting/backend-hosting-module.js +4 -1
  152. package/lib/node/hosting/backend-hosting-module.js.map +1 -1
  153. package/lib/node/hosting/browser-connection-token.d.ts +29 -1
  154. package/lib/node/hosting/browser-connection-token.d.ts.map +1 -1
  155. package/lib/node/hosting/browser-connection-token.js +25 -1
  156. package/lib/node/hosting/browser-connection-token.js.map +1 -1
  157. package/lib/node/hosting/browser-connection-token.spec.js +70 -0
  158. package/lib/node/hosting/browser-connection-token.spec.js.map +1 -1
  159. package/lib/node/index.d.ts +1 -0
  160. package/lib/node/index.d.ts.map +1 -1
  161. package/lib/node/index.js +3 -1
  162. package/lib/node/index.js.map +1 -1
  163. package/package.json +11 -11
  164. package/src/browser/browser-clipboard-service.ts +7 -1
  165. package/src/browser/clipboard-service.ts +11 -0
  166. package/src/browser/common-frontend-contribution.ts +7 -7
  167. package/src/browser/frontend-application-module.ts +11 -1
  168. package/src/browser/frontend-application.ts +12 -4
  169. package/src/browser/index.ts +1 -0
  170. package/src/browser/keybinding.ts +12 -7
  171. package/src/browser/perspective-service.spec.ts +2270 -0
  172. package/src/browser/perspective-service.ts +625 -0
  173. package/src/browser/preferences/preference-contribution.ts +9 -1
  174. package/src/browser/preferences/preference-service.spec.ts +72 -0
  175. package/src/browser/shell/application-shell.ts +41 -26
  176. package/src/browser/shell/shell-layout-restorer.spec.ts +746 -0
  177. package/src/browser/shell/shell-layout-restorer.ts +134 -13
  178. package/src/browser/shell/view-contribution.ts +6 -1
  179. package/src/browser/status-bar/status-bar.tsx +13 -3
  180. package/src/browser/style/select-component.css +16 -0
  181. package/src/browser/style/sidepanel.css +5 -0
  182. package/src/browser/style/status-bar.css +1 -1
  183. package/src/browser/tree/tree-widget.tsx +12 -78
  184. package/src/browser/widgets/index.ts +1 -0
  185. package/src/browser/widgets/select-component.tsx +41 -12
  186. package/src/browser/widgets/tab-bar-tracker.spec.ts +121 -0
  187. package/src/browser/widgets/tab-bar-tracker.ts +68 -0
  188. package/src/browser-only/messaging/frontend-only-service-connection-provider.ts +32 -3
  189. package/src/common/i18n/nls.metadata.json +4149 -888
  190. package/src/common/preferences/index.ts +1 -0
  191. package/src/common/preferences/preference-provider.spec.ts +17 -0
  192. package/src/common/preferences/preference-provider.ts +22 -1
  193. package/src/common/preferences/preference-scope.spec.ts +5 -3
  194. package/src/common/preferences/preference-scope.ts +6 -1
  195. package/src/common/preferences/preference-service.ts +39 -5
  196. package/src/common/preferences/session-preference-provider.spec.ts +78 -0
  197. package/src/common/preferences/session-preference-provider.ts +69 -0
  198. package/src/electron-browser/electron-clipboard-service.ts +5 -0
  199. package/src/electron-browser/menu/electron-menu-style.css +3 -1
  200. package/src/electron-browser/preload.ts +5 -2
  201. package/src/electron-common/electron-api.ts +4 -0
  202. package/src/electron-main/electron-api-main.ts +56 -3
  203. package/src/electron-main/electron-main-application.ts +3 -0
  204. package/src/electron-node/hosting/electron-backend-hosting-module.spec.ts +42 -0
  205. package/src/electron-node/hosting/electron-backend-hosting-module.ts +8 -0
  206. package/src/node/hosting/backend-hosting-module.ts +5 -2
  207. package/src/node/hosting/browser-connection-token.spec.ts +92 -0
  208. package/src/node/hosting/browser-connection-token.ts +45 -1
  209. package/src/node/index.ts +1 -0
@@ -28,6 +28,7 @@ import { CommonCommands } from '../common-commands';
28
28
  import { WindowService } from '../window/window-service';
29
29
  import { StopReason } from '../../common/frontend-application-state';
30
30
  import { isFunction, isObject, MaybePromise } from '../../common';
31
+ import { PerspectiveServiceInternal } from '../perspective-service';
31
32
 
32
33
  /**
33
34
  * A contract for widgets that want to store and restore their inner state, between sessions.
@@ -124,6 +125,13 @@ export interface ShellLayoutTransformer {
124
125
  transformLayoutOnRestore(layoutData: ApplicationShell.LayoutData): void;
125
126
  }
126
127
 
128
+ export const PERSPECTIVE_LAYOUTS_STORAGE_KEY = 'perspective-layouts';
129
+
130
+ export interface PersistedPerspectiveData {
131
+ activePerspectiveId: string;
132
+ layouts: Record<string, string>;
133
+ }
134
+
127
135
  export const RESET_LAYOUT = Command.toLocalizedCommand({
128
136
  id: 'reset.layout',
129
137
  category: CommonCommands.VIEW_CATEGORY,
@@ -141,6 +149,9 @@ export class ShellLayoutRestorer implements CommandContribution {
141
149
  @inject(WindowService) protected readonly windowService: WindowService;
142
150
  @inject(ThemeService) protected readonly themeService: ThemeService;
143
151
 
152
+ @inject(PerspectiveServiceInternal)
153
+ protected readonly perspectiveService: PerspectiveServiceInternal;
154
+
144
155
  constructor(
145
156
  @inject(WidgetManager) protected widgetManager: WidgetManager,
146
157
  @inject(ILogger) protected logger: ILogger,
@@ -157,6 +168,8 @@ export class ShellLayoutRestorer implements CommandContribution {
157
168
  this.logger.info('>>> Resetting layout...');
158
169
  this.shouldStoreLayout = false;
159
170
  this.storageService.setData(this.storageKey, undefined);
171
+ this.perspectiveService.clearSavedLayouts();
172
+ this.storageService.setData(PERSPECTIVE_LAYOUTS_STORAGE_KEY, undefined);
160
173
  this.themeService.reset();
161
174
  this.logger.info('<<< The layout has been successfully reset.');
162
175
  this.windowService.reload();
@@ -167,29 +180,137 @@ export class ShellLayoutRestorer implements CommandContribution {
167
180
  if (this.shouldStoreLayout) {
168
181
  try {
169
182
  this.logger.info('>>> Storing the layout...');
170
- const layoutData = app.shell.getLayoutData();
171
- const serializedLayoutData = this.deflate(layoutData);
172
- this.storageService.setData(this.storageKey, serializedLayoutData);
183
+ this.storePerspectiveLayouts(app);
173
184
  this.logger.info('<<< The layout has been successfully stored.');
174
185
  } catch (error) {
175
- this.storageService.setData(this.storageKey, undefined);
176
186
  this.logger.error('Error during serialization of layout data', error);
177
187
  }
178
188
  }
179
189
  }
180
190
 
191
+ protected storePerspectiveLayouts(app: FrontendApplication): void {
192
+ const provider = this.perspectiveService;
193
+ const activeId = provider.getActivePerspectiveId();
194
+ const layouts: Record<string, string> = {};
195
+
196
+ // Snapshot current shell as the active perspective's layout
197
+ try {
198
+ const currentLayout = app.shell.getLayoutData();
199
+ layouts[activeId] = this.deflate(currentLayout);
200
+ } catch (error) {
201
+ this.logger.warn(`Could not deflate layout for active perspective '${activeId}'`, error);
202
+ }
203
+
204
+ // Deflate all other saved (inactive) perspective layouts
205
+ for (const perspId of provider.getSavedPerspectiveIds()) {
206
+ if (perspId === activeId) {
207
+ continue; // already handled above from live shell
208
+ }
209
+ try {
210
+ const layout = provider.getSavedLayout(perspId);
211
+ if (layout) {
212
+ layouts[perspId] = this.deflate(layout);
213
+ }
214
+ } catch (error) {
215
+ this.logger.warn(`Could not deflate layout for perspective '${perspId}'`, error);
216
+ }
217
+ }
218
+
219
+ const perspectiveIds = Object.keys(layouts);
220
+ const onlyDefault = perspectiveIds.length <= 1 && (perspectiveIds.length === 0 || perspectiveIds[0] === provider.defaultPerspectiveId);
221
+ if (onlyDefault) {
222
+ // If only the default perspective has ever been used, continue writing
223
+ // to the legacy key for backward compatibility.
224
+ const defaultLayout = layouts[provider.defaultPerspectiveId];
225
+ if (defaultLayout) {
226
+ this.storageService.setData(this.storageKey, defaultLayout).catch(error => {
227
+ this.logger.error('Error persisting default layout', error);
228
+ });
229
+ }
230
+ this.storageService.setData(PERSPECTIVE_LAYOUTS_STORAGE_KEY, undefined);
231
+ } else {
232
+ const data: PersistedPerspectiveData = { activePerspectiveId: activeId, layouts };
233
+ this.storageService.setData(PERSPECTIVE_LAYOUTS_STORAGE_KEY, data).catch(error => {
234
+ this.logger.error('Error persisting perspective layouts, clearing stored data', error);
235
+ this.storageService.setData(PERSPECTIVE_LAYOUTS_STORAGE_KEY, undefined);
236
+ });
237
+ // Clear the legacy key when perspectives are in use
238
+ this.storageService.setData(this.storageKey, undefined);
239
+ }
240
+ }
241
+
181
242
  async restoreLayout(app: FrontendApplication): Promise<boolean> {
182
243
  this.logger.info('>>> Restoring the layout state...');
183
- const serializedLayoutData = await this.storageService.getData<string>(this.storageKey);
184
- if (serializedLayoutData === undefined) {
185
- this.logger.info('<<< Nothing to restore.');
186
- return false;
244
+ return this.restorePerspectiveLayouts(app);
245
+ }
246
+
247
+ protected async restorePerspectiveLayouts(app: FrontendApplication): Promise<boolean> {
248
+ const persisted = await this.storageService.getData<PersistedPerspectiveData>(PERSPECTIVE_LAYOUTS_STORAGE_KEY);
249
+
250
+ let activeId: string | undefined;
251
+
252
+ if (persisted) {
253
+ activeId = persisted.activePerspectiveId;
254
+ // Inflate all perspective layouts, apply transforms, and push to provider
255
+ for (const [perspId, deflated] of Object.entries(persisted.layouts)) {
256
+ try {
257
+ const layout = await this.inflate(deflated);
258
+ this.transformations.getContributions()
259
+ .forEach(t => t.transformLayoutOnRestore(layout));
260
+ this.perspectiveService.setSavedLayout(perspId, layout);
261
+ } catch (error) {
262
+ this.logger.warn(`Could not inflate layout for perspective '${perspId}'`, error);
263
+ }
264
+ }
265
+ } else {
266
+ // Migration: try legacy single-layout key. The key is deliberately left in place here:
267
+ // `storePerspectiveLayouts` rewrites it (default perspective only) or clears it (perspectives
268
+ // in use) on shutdown, so an unclean exit right after startup doesn't lose the layout.
269
+ const legacyData = await this.storageService.getData<string>(this.storageKey);
270
+ if (legacyData) {
271
+ try {
272
+ const layout = await this.inflate(legacyData);
273
+ this.transformations.getContributions()
274
+ .forEach(t => t.transformLayoutOnRestore(layout));
275
+ this.perspectiveService.setSavedLayout(this.perspectiveService.defaultPerspectiveId, layout);
276
+ activeId = this.perspectiveService.defaultPerspectiveId;
277
+ } catch (error) {
278
+ this.logger.warn('Could not inflate legacy layout for migration', error);
279
+ }
280
+ }
281
+ }
282
+
283
+ if (activeId) {
284
+ const accepted = this.perspectiveService.setActivePerspectiveId(activeId);
285
+ if (!accepted) {
286
+ activeId = this.perspectiveService.getActivePerspectiveId();
287
+ }
288
+ }
289
+
290
+ // Apply the active perspective's layout to the shell
291
+ const effectiveId = activeId ?? this.perspectiveService.getActivePerspectiveId();
292
+ let activeLayout = this.perspectiveService.getSavedLayout(effectiveId);
293
+
294
+ // Fallback: if the active perspective has no saved layout, try the default
295
+ if (!activeLayout && effectiveId !== this.perspectiveService.defaultPerspectiveId) {
296
+ this.logger.warn(`No saved layout for perspective '${effectiveId}', falling back to default.`);
297
+ const defaultId = this.perspectiveService.defaultPerspectiveId;
298
+ this.perspectiveService.setActivePerspectiveId(defaultId);
299
+ activeLayout = this.perspectiveService.getSavedLayout(defaultId);
187
300
  }
188
- const layoutData = await this.inflate(serializedLayoutData);
189
- this.transformations.getContributions().forEach(transformation => transformation.transformLayoutOnRestore(layoutData));
190
- await app.shell.setLayoutData(layoutData);
191
- this.logger.info('<<< The layout has been successfully restored.');
192
- return true;
301
+
302
+ if (activeLayout) {
303
+ await app.shell.setLayoutData(activeLayout);
304
+ const restoredId = this.perspectiveService.getActivePerspectiveId();
305
+ this.perspectiveService.onLayoutRestored(restoredId);
306
+ this.logger.info('<<< The layout has been successfully restored.');
307
+ return true;
308
+ }
309
+
310
+ // Even with no layout to restore, ensure chrome is applied for whatever perspective is active
311
+ this.perspectiveService.onLayoutRestored(this.perspectiveService.getActivePerspectiveId());
312
+ this.logger.info('<<< Nothing to restore.');
313
+ return false;
193
314
  }
194
315
 
195
316
  protected isWidgetProperty(propertyName: string): boolean {
@@ -87,6 +87,11 @@ export abstract class AbstractViewContribution<T extends Widget> implements Comm
87
87
  return this.options.defaultWidgetOptions;
88
88
  }
89
89
 
90
+ /** The widget ID used for shell operations — `viewContainerId` if set, otherwise `widgetId`. */
91
+ get effectiveWidgetId(): string {
92
+ return this.options.viewContainerId || this.options.widgetId;
93
+ }
94
+
90
95
  get widget(): Promise<T> {
91
96
  return this.widgetManager.getOrCreateWidget(this.viewId);
92
97
  }
@@ -97,7 +102,7 @@ export abstract class AbstractViewContribution<T extends Widget> implements Comm
97
102
 
98
103
  async openView(args: Partial<OpenViewArguments> = {}): Promise<T> {
99
104
  const shell = this.shell;
100
- const widget = await this.widgetManager.getOrCreateWidget(this.options.viewContainerId || this.viewId);
105
+ const widget = await this.widgetManager.getOrCreateWidget(this.effectiveWidgetId);
101
106
  const tabBar = shell.getTabBarFor(widget);
102
107
  const area = shell.getAreaFor(widget);
103
108
  if (!tabBar) {
@@ -35,6 +35,8 @@ export class StatusBarImpl extends ReactWidget implements StatusBar {
35
35
  protected backgroundColor: string | undefined;
36
36
  protected color: string | undefined;
37
37
 
38
+ protected preferencesReady = false;
39
+
38
40
  constructor(
39
41
  @inject(CommandService) protected readonly commands: CommandService,
40
42
  @inject(LabelParser) protected readonly entryService: LabelParser,
@@ -50,19 +52,27 @@ export class StatusBarImpl extends ReactWidget implements StatusBar {
50
52
  // Hide the status bar until the `workbench.statusBar.visible` preference returns with a `true` value.
51
53
  this.hide();
52
54
  this.preferences.ready.then(() => {
53
- const preferenceValue = this.preferences.get<boolean>('workbench.statusBar.visible', true);
54
- this.setHidden(!preferenceValue);
55
+ this.preferencesReady = true;
56
+ this.updateVisibility();
55
57
  });
56
58
  this.toDispose.push(
57
59
  this.preferences.onPreferenceChanged(preference => {
58
60
  if (preference.preferenceName === 'workbench.statusBar.visible') {
59
- this.setHidden(!this.preferences.get('workbench.statusBar.visible', true));
61
+ this.updateVisibility();
60
62
  }
61
63
  })
62
64
  );
63
65
  this.toDispose.push(this.viewModel.onDidChange(() => this.debouncedUpdate()));
64
66
  }
65
67
 
68
+ protected updateVisibility(): void {
69
+ if (!this.preferencesReady) {
70
+ return; // Don't change visibility until preferences have loaded
71
+ }
72
+ const prefHides = !this.preferences.get<boolean>('workbench.statusBar.visible', true);
73
+ this.setHidden(prefHides);
74
+ }
75
+
66
76
  protected debouncedUpdate = debounce(() => this.update(), 50);
67
77
 
68
78
  protected get ready(): Promise<void> {
@@ -99,6 +99,22 @@
99
99
  background: var(--theia-list-activeSelectionBackground);
100
100
  }
101
101
 
102
+ .theia-select-component-dropdown .theia-select-component-option.disabled,
103
+ .theia-select-component-dropdown .theia-select-component-option.disabled .theia-select-component-option-detail {
104
+ color: var(--theia-disabledForeground);
105
+ cursor: default;
106
+ }
107
+
108
+ .theia-select-component-dropdown .theia-select-component-option.disabled .theia-select-component-option-value {
109
+ text-decoration: line-through;
110
+ }
111
+
112
+ /* Keep a hovered/selected disabled option readable against the active-selection background. */
113
+ .theia-select-component-dropdown .theia-select-component-option.disabled.selected,
114
+ .theia-select-component-dropdown .theia-select-component-option.disabled.selected .theia-select-component-option-detail {
115
+ color: var(--theia-list-activeSelectionForeground);
116
+ }
117
+
102
118
  .theia-select-component-dropdown .theia-select-component-separator {
103
119
  width: 84px;
104
120
  height: 1px;
@@ -276,6 +276,11 @@
276
276
  background: inherit;
277
277
  }
278
278
 
279
+ /* Ensure the empty bottom panel keeps a top border separating it from the main panel. */
280
+ #theia-bottom-content-panel:not(:has(.lm-TabBar)) {
281
+ border-top: var(--theia-border-width) solid var(--theia-panel-border);
282
+ }
283
+
279
284
  #theia-bottom-content-panel .lm-TabBar-tab {
280
285
  background: inherit;
281
286
  }
@@ -63,7 +63,7 @@ body.theia-no-open-workspace #theia-statusBar {
63
63
  padding-inline: var(--theia-ui-padding);
64
64
  }
65
65
 
66
- #theia-statusBar .area.left .element.has-background {
66
+ #theia-statusBar .area.left .element.has-background:not(#session-preference-status) {
67
67
  margin-left: calc(-1 * var(--theia-ui-padding));
68
68
  }
69
69
 
@@ -64,28 +64,10 @@ export const TREE_NODE_CAPTION_CLASS = 'theia-TreeNodeCaption';
64
64
  export const TREE_NODE_INDENT_GUIDE_CLASS = 'theia-tree-node-indent';
65
65
 
66
66
  /**
67
- * Threshold in pixels to consider the view as being scrolled to the bottom
67
+ * Threshold in pixels to consider the view as being scrolled to the bottom.
68
68
  */
69
69
  export const SCROLL_BOTTOM_THRESHOLD = 30;
70
70
 
71
- /**
72
- * Tree scroll event data.
73
- */
74
- export interface TreeScrollEvent {
75
- readonly scrollTop: number;
76
- readonly scrollLeft: number;
77
- }
78
-
79
- /**
80
- * Tree scroll state data.
81
- */
82
- export interface TreeScrollState {
83
- readonly scrollTop: number;
84
- readonly isAtBottom: boolean;
85
- readonly scrollHeight?: number;
86
- readonly clientHeight?: number;
87
- }
88
-
89
71
  export const TreeProps = Symbol('TreeProps');
90
72
 
91
73
  /**
@@ -189,8 +171,8 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
189
171
  protected searchBox: SearchBox;
190
172
  protected searchHighlights: Map<string, TreeDecoration.CaptionHighlight>;
191
173
 
192
- protected readonly onScrollEmitter = new Emitter<TreeScrollEvent>();
193
- readonly onScroll: TheiaEvent<TreeScrollEvent> = this.onScrollEmitter.event;
174
+ protected readonly onAtBottomStateChangeEmitter = new Emitter<boolean>();
175
+ readonly onAtBottomStateChange: TheiaEvent<boolean> = this.onAtBottomStateChangeEmitter.event;
194
176
 
195
177
  @inject(TreeDecoratorService)
196
178
  protected readonly decoratorService: TreeDecoratorService;
@@ -285,7 +267,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
285
267
  this.node.addEventListener('mouseup', this.handleMiddleClickEvent.bind(this));
286
268
  this.node.addEventListener('auxclick', this.handleMiddleClickEvent.bind(this));
287
269
  this.toDispose.pushAll([
288
- this.onScrollEmitter,
270
+ this.onAtBottomStateChangeEmitter,
289
271
  this.model,
290
272
  this.model.onChanged(() => this.updateRows()),
291
273
  this.model.onSelectionChanged(() => this.scheduleUpdateScrollToRow({ resize: false })),
@@ -551,7 +533,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
551
533
  rows={rows}
552
534
  renderNodeRow={this.renderNodeRow}
553
535
  scrollToRow={this.scrollToRow}
554
- onScrollEmitter={this.onScrollEmitter}
536
+ onAtBottomStateChangeEmitter={this.onAtBottomStateChangeEmitter}
555
537
  {...this.props.viewProps}
556
538
  />;
557
539
  }
@@ -1201,39 +1183,6 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
1201
1183
  return this.node;
1202
1184
  }
1203
1185
 
1204
- /**
1205
- * Get the current scroll state from the virtualized view.
1206
- * This should be used instead of accessing the DOM scroll properties directly
1207
- * when the tree is virtualized.
1208
- */
1209
- protected getVirtualizedScrollState(): TreeScrollState | undefined {
1210
- return this.view?.getScrollState();
1211
- }
1212
-
1213
- /**
1214
- * Check if the tree is scrolled to the bottom.
1215
- * Works with both virtualized and non-virtualized trees.
1216
- */
1217
- isScrolledToBottom(): boolean {
1218
- if (this.props.virtualized !== false && this.view) {
1219
- // Use virtualized scroll state
1220
- const scrollState = this.getVirtualizedScrollState();
1221
- return scrollState?.isAtBottom ?? true;
1222
- } else {
1223
- // Fallback to DOM-based calculation for non-virtualized trees
1224
- const scrollContainer = this.node;
1225
- const scrollHeight = scrollContainer.scrollHeight;
1226
- const scrollTop = scrollContainer.scrollTop;
1227
- const clientHeight = scrollContainer.clientHeight;
1228
-
1229
- if (scrollHeight <= clientHeight) {
1230
- return true;
1231
- }
1232
-
1233
- return scrollHeight - scrollTop - clientHeight <= SCROLL_BOTTOM_THRESHOLD;
1234
- }
1235
- }
1236
-
1237
1186
  protected override onAfterAttach(msg: Message): void {
1238
1187
  const up = [
1239
1188
  Key.ARROW_UP,
@@ -1658,11 +1607,14 @@ export namespace TreeWidget {
1658
1607
  /**
1659
1608
  * Optional scroll event emitter.
1660
1609
  */
1661
- onScrollEmitter?: Emitter<TreeScrollEvent>
1610
+ /**
1611
+ * Optional emitter fired when the at-bottom state changes.
1612
+ */
1613
+ onAtBottomStateChangeEmitter?: Emitter<boolean>
1662
1614
  }
1663
1615
  export class View extends React.Component<ViewProps> {
1664
1616
  list: VirtuosoHandle | undefined;
1665
- private lastScrollState: TreeScrollState = { scrollTop: 0, isAtBottom: true, scrollHeight: 0, clientHeight: 0 };
1617
+
1666
1618
  /**
1667
1619
  * Ensure the selected row is scrolled into view when virtualization finishes updating.
1668
1620
  */
@@ -1687,28 +1639,14 @@ export namespace TreeWidget {
1687
1639
  }
1688
1640
 
1689
1641
  override render(): React.ReactNode {
1690
- const { rows, width, height, scrollToRow, renderNodeRow, onScrollEmitter, ...other } = this.props;
1642
+ const { rows, width, height, scrollToRow, renderNodeRow, onAtBottomStateChangeEmitter, ...other } = this.props;
1691
1643
  return <Virtuoso
1692
1644
  ref={(list: VirtuosoHandle | null) => {
1693
1645
  this.list = (list || undefined);
1694
1646
  this.scrollIntoViewIfNeeded();
1695
1647
  }}
1696
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1697
- onScroll={(e: any) => {
1698
- const scrollTop = e.target.scrollTop;
1699
- const scrollHeight = e.target.scrollHeight;
1700
- const clientHeight = e.target.clientHeight;
1701
- const isAtBottom = scrollHeight - scrollTop - clientHeight <= SCROLL_BOTTOM_THRESHOLD;
1702
-
1703
- // Store scroll state before firing the event to prevent jitter during inference and scrolling
1704
- this.lastScrollState = { scrollTop, isAtBottom, scrollHeight, clientHeight };
1705
- onScrollEmitter?.fire({ scrollTop, scrollLeft: e.target.scrollLeft || 0 });
1706
- }}
1707
1648
  atBottomStateChange={(atBottom: boolean) => {
1708
- this.lastScrollState = {
1709
- ...this.lastScrollState,
1710
- isAtBottom: atBottom
1711
- };
1649
+ onAtBottomStateChangeEmitter?.fire(atBottom);
1712
1650
  }}
1713
1651
  atBottomThreshold={SCROLL_BOTTOM_THRESHOLD}
1714
1652
  totalCount={rows.length}
@@ -1721,9 +1659,5 @@ export namespace TreeWidget {
1721
1659
  {...other}
1722
1660
  />;
1723
1661
  }
1724
-
1725
- getScrollState(): TreeScrollState {
1726
- return { ...this.lastScrollState };
1727
- }
1728
1662
  }
1729
1663
  }
@@ -15,6 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  export * from './widget';
18
+ export * from './tab-bar-tracker';
18
19
  export * from './react-renderer';
19
20
  export * from './react-widget';
20
21
  export * from './extractable-widget';
@@ -91,7 +91,20 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
91
91
  return selected;
92
92
  }
93
93
 
94
- override componentDidUpdate(prevProps: SelectComponentProps): void {
94
+ override componentDidUpdate(prevProps: SelectComponentProps, prevState: SelectComponentState): void {
95
+ if (prevState.selected !== this.state.selected && this.state.dimensions) {
96
+ const dropdownContainer = this.dropdownRef?.current;
97
+ const selectedOption = dropdownContainer?.querySelector(`[data-option-index="${this.state.selected}"]`) as HTMLElement | undefined;
98
+ if (dropdownContainer && selectedOption) {
99
+ const optionBottom = selectedOption.offsetTop + selectedOption.offsetHeight;
100
+ const visibleBottom = dropdownContainer.scrollTop + dropdownContainer.clientHeight;
101
+ if (optionBottom > visibleBottom) {
102
+ dropdownContainer.scrollTop = optionBottom - dropdownContainer.clientHeight;
103
+ } else if (selectedOption.offsetTop < dropdownContainer.scrollTop) {
104
+ dropdownContainer.scrollTop = selectedOption.offsetTop;
105
+ }
106
+ }
107
+ }
95
108
  if (prevProps.defaultValue !== this.props.defaultValue || prevProps.options !== this.props.options) {
96
109
  const selected = this.getInitialSelectedIndex(this.props);
97
110
  this.setState({
@@ -230,13 +243,18 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
230
243
  selected = this.nextNotSeparator('forwards');
231
244
  }
232
245
  const selectedItemLabel = options[selected]?.label ?? options[selected]?.value;
246
+ // Expose when the currently shown option is disabled so consumers can style the field to match
247
+ // it (e.g. the option previewed while navigating), instead of a fixed committed-value class.
248
+ const selectionDisabled = !!options[selected]?.disabled;
249
+ const fieldClassName = `theia-select-component${this.props.className ? ` ${this.props.className}` : ''}`
250
+ + `${selectionDisabled ? ' theia-select-component-selection-disabled' : ''}`;
233
251
  return <>
234
252
  <div
235
253
  id={this.props.id}
236
254
  key="select-component"
237
255
  ref={this.fieldRef}
238
256
  tabIndex={0}
239
- className={`theia-select-component${this.props.className ? ` ${this.props.className}` : ''}`}
257
+ className={fieldClassName}
240
258
  onClick={e => this.handleClickEvent(e)}
241
259
  onBlur={
242
260
  () => {
@@ -254,11 +272,11 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
254
272
  </>;
255
273
  }
256
274
 
257
- protected nextNotSeparator(direction: 'forwards' | 'backwards'): number {
275
+ protected nextNotSeparator(direction: 'forwards' | 'backwards', startFrom?: number): number {
258
276
  const { options } = this.props;
259
277
  const step = direction === 'forwards' ? 1 : -1;
260
278
  const length = this.props.options.length;
261
- let selected = this.state.selected;
279
+ let selected = startFrom ?? this.state.selected;
262
280
  let count = 0;
263
281
  do {
264
282
  selected = (selected + step) % length;
@@ -267,7 +285,8 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
267
285
  }
268
286
  count++;
269
287
  }
270
- while (options[selected]?.separator && count < length);
288
+ // Skip separators and disabled options so keyboard navigation never lands on a non-selectable entry.
289
+ while ((options[selected]?.separator || options[selected]?.disabled) && count < length);
271
290
  return selected;
272
291
  }
273
292
 
@@ -276,23 +295,26 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
276
295
  return;
277
296
  }
278
297
  if (ev.key === 'ArrowUp') {
279
- const selected = this.nextNotSeparator('backwards');
298
+ const selected = this.nextNotSeparator('backwards', this.state.hover);
280
299
  this.setState({
281
300
  selected,
282
301
  hover: selected
283
302
  });
284
303
  } else if (ev.key === 'ArrowDown') {
285
304
  if (this.state.dimensions) {
286
- const selected = this.nextNotSeparator('forwards');
305
+ const selected = this.nextNotSeparator('forwards', this.state.hover);
287
306
  this.setState({
288
307
  selected,
289
308
  hover: selected
290
309
  });
291
310
  } else {
292
311
  this.toggleVisibility();
312
+ // Start on the first selectable option (startFrom -1 evaluates index 0 first), so a
313
+ // leading separator or disabled option is skipped instead of becoming the active row.
314
+ const selected = this.nextNotSeparator('forwards', -1);
293
315
  this.setState({
294
- selected: 0,
295
- hover: 0,
316
+ selected,
317
+ hover: selected,
296
318
  });
297
319
  }
298
320
  } else if (ev.key === 'Enter') {
@@ -300,7 +322,10 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
300
322
  this.toggleVisibility();
301
323
  } else {
302
324
  const selected = this.state.selected;
303
- this.selectOption(selected, this.props.options[selected]);
325
+ const option = this.props.options[selected];
326
+ if (!option?.disabled) {
327
+ this.selectOption(selected, option);
328
+ }
304
329
  }
305
330
  } else if (ev.key === 'Escape' || ev.key === 'Tab') {
306
331
  this.hide(undefined, true);
@@ -401,17 +426,21 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
401
426
  return <div key={index} className="theia-select-component-separator" />;
402
427
  }
403
428
  const selected = this.state.hover;
429
+ const disabled = !!option.disabled;
404
430
  return (
405
431
  <div
406
432
  key={index}
407
- className={`theia-select-component-option${index === selected ? ' selected' : ''}`}
433
+ data-option-index={index}
434
+ className={`theia-select-component-option${index === selected ? ' selected' : ''}${disabled ? ' disabled' : ''}`}
408
435
  onMouseOver={() => {
409
436
  this.setState({
410
437
  hover: index
411
438
  });
412
439
  }}
413
440
  onMouseDown={() => {
414
- this.selectOption(index, option);
441
+ if (!disabled) {
442
+ this.selectOption(index, option);
443
+ }
415
444
  }}
416
445
  >
417
446
  <div key="value" className="theia-select-component-option-value">{option.label ?? option.value}</div>