@workbench-kit/shell-react 0.0.2-prototype.0.2.33 → 0.0.2-prototype.0.2.35

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 (51) hide show
  1. package/README.md +32 -0
  2. package/package.json +9 -9
  3. package/src/commands/use-command-descriptors.ts +7 -3
  4. package/src/commands/use-extension-registry-command-descriptors.ts +23 -6
  5. package/src/devtools/use-workbench-devtools-snapshot.ts +39 -14
  6. package/src/devtools/workbench-devtools-snapshot.ts +32 -13
  7. package/src/editor/area.tsx +3 -2
  8. package/src/editor/state-storage.ts +31 -0
  9. package/src/editor/tab-context-menu.ts +8 -8
  10. package/src/editor/use-editor.ts +6 -6
  11. package/src/explorer/context-menu.ts +8 -4
  12. package/src/explorer/view.tsx +4 -3
  13. package/src/extensions/canonical-extension-descriptions.ts +58 -0
  14. package/src/extensions/context-menu.ts +9 -6
  15. package/src/extensions/extension-enablement-context.ts +15 -0
  16. package/src/extensions/extension-enablement-controller.ts +453 -0
  17. package/src/extensions/management-model.ts +111 -47
  18. package/src/extensions/theme-selection-protection.ts +98 -0
  19. package/src/extensions/uninstall-eligibility.ts +103 -0
  20. package/src/extensions/use-extension-management.ts +151 -67
  21. package/src/extensions/view.tsx +4 -0
  22. package/src/field-remap/chrome-labels.ts +58 -2
  23. package/src/field-remap/convert-palette.tsx +151 -6
  24. package/src/field-remap/demo.tsx +31 -2
  25. package/src/field-remap/flow.tsx +57 -13
  26. package/src/field-remap/history.ts +99 -0
  27. package/src/field-remap/index.ts +9 -1
  28. package/src/field-remap/panel.tsx +371 -97
  29. package/src/field-remap/preview-controller.ts +122 -0
  30. package/src/field-remap/preview.tsx +171 -0
  31. package/src/field-remap/view.css +72 -0
  32. package/src/index.ts +16 -1
  33. package/src/management/keybinding-overrides-storage.ts +28 -0
  34. package/src/management/preference-settings-storage.ts +28 -0
  35. package/src/management/settings.tsx +2 -0
  36. package/src/management/use-command-management.ts +31 -26
  37. package/src/management/use-keybinding-management.ts +25 -12
  38. package/src/shell/focused-extension-services.ts +138 -0
  39. package/src/shell/host-shell.tsx +13 -10
  40. package/src/shell/persistence-diagnostic-context.ts +11 -0
  41. package/src/shell/provider.tsx +373 -69
  42. package/src/shell/settings.tsx +25 -16
  43. package/src/shell/shell.tsx +119 -78
  44. package/src/shell/view-host.tsx +23 -22
  45. package/src/storage/local-json-storage.ts +43 -29
  46. package/src/storage/persistence-diagnostics.ts +72 -0
  47. package/src/workbench/appearance-storage.ts +28 -0
  48. package/src/workbench/command-host.tsx +19 -22
  49. package/src/workbench/command-palette.ts +14 -13
  50. package/src/workbench/layout-storage.ts +52 -5
  51. package/src/workbench/use-persisted-appearance.ts +38 -12
@@ -0,0 +1,58 @@
1
+ import {
2
+ computeWorkbenchExtensionManifestIntegrity,
3
+ type WorkbenchExtensionDescription,
4
+ } from '@workbench-kit/workbench-core';
5
+
6
+ export interface CanonicalExtensionDescriptionSnapshot {
7
+ readonly ambiguousExtensionIds: readonly string[];
8
+ readonly descriptions: readonly WorkbenchExtensionDescription[];
9
+ getDescription(extensionId: string): WorkbenchExtensionDescription | undefined;
10
+ }
11
+
12
+ export function createCanonicalExtensionDescriptionSnapshot({
13
+ availableExtensions,
14
+ liveExtensions,
15
+ }: {
16
+ readonly availableExtensions: readonly WorkbenchExtensionDescription[];
17
+ readonly liveExtensions: readonly WorkbenchExtensionDescription[];
18
+ }): CanonicalExtensionDescriptionSnapshot {
19
+ const descriptionsById = new Map<string, WorkbenchExtensionDescription>();
20
+ const ambiguousExtensionIds = new Set<string>();
21
+
22
+ for (const description of [...availableExtensions, ...liveExtensions]) {
23
+ const extensionId = description.manifest.id;
24
+ const existing = descriptionsById.get(extensionId);
25
+ if (!existing) {
26
+ descriptionsById.set(extensionId, description);
27
+ continue;
28
+ }
29
+ if (!areEquivalentDescriptions(existing, description)) {
30
+ ambiguousExtensionIds.add(extensionId);
31
+ }
32
+ }
33
+
34
+ const descriptions = [...descriptionsById.values()].sort((left, right) =>
35
+ left.manifest.id.localeCompare(right.manifest.id),
36
+ );
37
+ const ambiguousIds = [...ambiguousExtensionIds].sort((left, right) => left.localeCompare(right));
38
+
39
+ return {
40
+ ambiguousExtensionIds: ambiguousIds,
41
+ descriptions,
42
+ getDescription(extensionId) {
43
+ return ambiguousExtensionIds.has(extensionId) ? undefined : descriptionsById.get(extensionId);
44
+ },
45
+ };
46
+ }
47
+
48
+ function areEquivalentDescriptions(
49
+ left: WorkbenchExtensionDescription,
50
+ right: WorkbenchExtensionDescription,
51
+ ): boolean {
52
+ return (
53
+ left.extensionPath === right.extensionPath &&
54
+ left.module === right.module &&
55
+ computeWorkbenchExtensionManifestIntegrity(left.manifest) ===
56
+ computeWorkbenchExtensionManifestIntegrity(right.manifest)
57
+ );
58
+ }
@@ -1,33 +1,36 @@
1
1
  import type { ContextMenuItem } from '@workbench-kit/react/overlay';
2
2
  import { commandMenuItemsToContextMenuItems } from '@workbench-kit/react/workbench/commands';
3
+ import type { CommandRegistry } from '@workbench-kit/platform';
3
4
  import {
4
5
  resolveWorkbenchMenuContributions,
5
- type ExtensionRegistry,
6
+ type MenuRegistry,
6
7
  } from '@workbench-kit/workbench-core';
7
8
 
8
9
  export interface ExtensionContextMenuInput {
10
+ readonly commands?: CommandRegistry | undefined;
9
11
  readonly contextKeys?: object | undefined;
10
12
  readonly executeCommand?: ((commandId: string) => unknown) | undefined;
11
- readonly extensionRegistry?: ExtensionRegistry | undefined;
12
13
  readonly menu: string;
14
+ readonly menus?: MenuRegistry | undefined;
13
15
  }
14
16
 
15
17
  export function createExtensionContextMenuItems({
18
+ commands,
16
19
  contextKeys,
17
20
  executeCommand,
18
- extensionRegistry,
19
21
  menu,
22
+ menus,
20
23
  }: ExtensionContextMenuInput): ContextMenuItem[] {
21
- if (!extensionRegistry || !executeCommand) {
24
+ if (!commands || !menus || !executeCommand) {
22
25
  return [];
23
26
  }
24
27
 
25
28
  const menuItems = resolveWorkbenchMenuContributions({
26
- commandRegistry: extensionRegistry.commands,
29
+ commandRegistry: commands,
27
30
  context: undefined,
28
31
  contextKeys,
29
32
  menu,
30
- menuItems: extensionRegistry.menus.getMenuItems(menu),
33
+ menuItems: menus.getMenuItems(menu),
31
34
  });
32
35
 
33
36
  return commandMenuItemsToContextMenuItems([...menuItems], (commandId) => {
@@ -0,0 +1,15 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ import type { ExtensionEnablementController } from './extension-enablement-controller.js';
4
+
5
+ export const ExtensionEnablementContext = createContext<ExtensionEnablementController | undefined>(
6
+ undefined,
7
+ );
8
+
9
+ export function useExtensionEnablementController(): ExtensionEnablementController {
10
+ const controller = useContext(ExtensionEnablementContext);
11
+ if (!controller) {
12
+ throw new Error('Extension enablement must be used inside WorkbenchProvider.');
13
+ }
14
+ return controller;
15
+ }
@@ -0,0 +1,453 @@
1
+ import {
2
+ loadInstalledExtensionsResult,
3
+ saveInstalledExtensionsResult,
4
+ type ExtensionRegistry,
5
+ type InstalledExtensionRecord,
6
+ type WorkbenchExtensionDescription,
7
+ type WorkbenchPersistenceDiagnosticHandler,
8
+ type WorkbenchStorageAdapter,
9
+ } from '@workbench-kit/workbench-core';
10
+
11
+ import type { ThemeSelectionProtectionSnapshot } from './theme-selection-protection.js';
12
+ import { createCanonicalExtensionDescriptionSnapshot } from './canonical-extension-descriptions.js';
13
+ import {
14
+ createExtensionUninstallEvaluation,
15
+ type ExtensionUninstallEligibility,
16
+ } from './uninstall-eligibility.js';
17
+
18
+ interface DisposableLike {
19
+ dispose(): void;
20
+ }
21
+
22
+ export interface ExtensionRegistrationLifetime extends DisposableLike {
23
+ add<T extends DisposableLike>(disposable: T): T;
24
+ getRegistration(extensionId: string): DisposableLike | undefined;
25
+ }
26
+
27
+ export type ExtensionEnablementTransitionResult =
28
+ | {
29
+ readonly enabled: boolean;
30
+ readonly extensionId: string;
31
+ readonly kind: 'applied';
32
+ readonly message: string;
33
+ }
34
+ | {
35
+ readonly enabled: boolean;
36
+ readonly extensionId: string;
37
+ readonly kind: 'reloadRequired';
38
+ readonly message: string;
39
+ }
40
+ | {
41
+ readonly enabled: boolean;
42
+ readonly extensionId: string;
43
+ readonly kind: 'failed';
44
+ readonly message: string;
45
+ };
46
+
47
+ export type ExtensionUninstallActionResult =
48
+ | ExtensionEnablementTransitionResult
49
+ | ({ readonly extensionId: string } & Exclude<
50
+ ExtensionUninstallEligibility,
51
+ { readonly kind: 'eligible' }
52
+ >);
53
+
54
+ export interface ExtensionEnablementControllerOptions {
55
+ readonly availableExtensions: readonly WorkbenchExtensionDescription[];
56
+ readonly initialEnabledExtensions: readonly WorkbenchExtensionDescription[];
57
+ readonly initialInstalledRecords: readonly InstalledExtensionRecord[];
58
+ readonly installedExtensionsStorage?: WorkbenchStorageAdapter | undefined;
59
+ readonly installedExtensionsStorageKey: string;
60
+ readonly integrityAcceptedExtensionIds: ReadonlySet<string>;
61
+ readonly onPersistenceDiagnostic?: WorkbenchPersistenceDiagnosticHandler | undefined;
62
+ readonly registrationLifetime: ExtensionRegistrationLifetime;
63
+ readonly registry: ExtensionRegistry;
64
+ }
65
+
66
+ /** Provider-owned live installed/enabled state for the narrow theme lifecycle. */
67
+ export class ExtensionEnablementController implements DisposableLike {
68
+ private readonly availableExtensions: readonly WorkbenchExtensionDescription[];
69
+ private readonly availableExtensionsById: ReadonlyMap<string, WorkbenchExtensionDescription>;
70
+ private readonly integrityAcceptedExtensionIds: ReadonlySet<string>;
71
+ private readonly listeners = new Set<() => void>();
72
+ private readonly onPersistenceDiagnostic: WorkbenchPersistenceDiagnosticHandler | undefined;
73
+ private readonly registrationHandles = new Map<string, DisposableLike>();
74
+ private readonly registrationLifetime: ExtensionRegistrationLifetime;
75
+ private readonly registry: ExtensionRegistry;
76
+ private readonly storage: WorkbenchStorageAdapter | undefined;
77
+ private readonly storageKey: string;
78
+ private installedRecords: readonly InstalledExtensionRecord[];
79
+ private themeSelectionProtection: ThemeSelectionProtectionSnapshot | undefined;
80
+ private disposed = false;
81
+
82
+ constructor({
83
+ availableExtensions,
84
+ initialEnabledExtensions,
85
+ initialInstalledRecords,
86
+ installedExtensionsStorage,
87
+ installedExtensionsStorageKey,
88
+ integrityAcceptedExtensionIds,
89
+ onPersistenceDiagnostic,
90
+ registrationLifetime,
91
+ registry,
92
+ }: ExtensionEnablementControllerOptions) {
93
+ this.availableExtensions = [...availableExtensions];
94
+ this.availableExtensionsById = new Map(
95
+ availableExtensions.map((description) => [description.manifest.id, description]),
96
+ );
97
+ this.installedRecords = [...initialInstalledRecords];
98
+ this.integrityAcceptedExtensionIds = integrityAcceptedExtensionIds;
99
+ this.onPersistenceDiagnostic = onPersistenceDiagnostic;
100
+ this.registrationLifetime = registrationLifetime;
101
+ this.registry = registry;
102
+ this.storage = installedExtensionsStorage;
103
+ this.storageKey = installedExtensionsStorageKey;
104
+
105
+ for (const description of initialEnabledExtensions) {
106
+ const extensionId = description.manifest.id;
107
+ const registration = registrationLifetime.getRegistration(extensionId);
108
+ if (registration) {
109
+ this.registrationHandles.set(extensionId, registration);
110
+ }
111
+ }
112
+ }
113
+
114
+ getInstalledRecordsSnapshot = (): readonly InstalledExtensionRecord[] => this.installedRecords;
115
+
116
+ subscribeInstalledRecords = (listener: () => void): (() => void) => {
117
+ if (this.disposed) {
118
+ return () => undefined;
119
+ }
120
+ this.listeners.add(listener);
121
+ return () => {
122
+ this.listeners.delete(listener);
123
+ };
124
+ };
125
+
126
+ setThemeSelectionProtection(snapshot: ThemeSelectionProtectionSnapshot | undefined): void {
127
+ this.themeSelectionProtection =
128
+ snapshot?.kind === 'known'
129
+ ? { ...snapshot, protectedThemeIds: [...snapshot.protectedThemeIds] }
130
+ : snapshot;
131
+ }
132
+
133
+ commitInstalledRecords(
134
+ records: readonly InstalledExtensionRecord[],
135
+ extensionId: string,
136
+ ): ExtensionEnablementTransitionResult {
137
+ const persistence = this.persist(records);
138
+ if (!persistence) {
139
+ return this.failed(extensionId, this.isRecordEnabled(extensionId));
140
+ }
141
+
142
+ this.publishInstalledRecords(records);
143
+ return this.reloadRequired(extensionId, this.isRecordEnabled(extensionId));
144
+ }
145
+
146
+ uninstallInstalledExtension(extensionId: string): ExtensionUninstallActionResult {
147
+ const persisted = loadInstalledExtensionsResult(this.storageKey, this.storage, {
148
+ onDiagnostic: this.onPersistenceDiagnostic,
149
+ });
150
+ if (persisted.diagnostic) {
151
+ return this.failed(extensionId, this.isRecordEnabled(extensionId));
152
+ }
153
+ const canonicalDescriptions = createCanonicalExtensionDescriptionSnapshot({
154
+ availableExtensions: this.availableExtensions,
155
+ liveExtensions: this.registry.getExtensions(),
156
+ });
157
+ const eligibility = createExtensionUninstallEvaluation({
158
+ canonicalDescriptions,
159
+ installedRecords: persisted.value,
160
+ }).getEligibility(extensionId);
161
+ if (eligibility.kind !== 'eligible') {
162
+ return { ...eligibility, extensionId };
163
+ }
164
+
165
+ const target = persisted.value.find((record) => record.id === extensionId);
166
+ if (!target) {
167
+ return {
168
+ diagnosticExtensionIds: [extensionId],
169
+ extensionId,
170
+ kind: 'ineligibleTarget',
171
+ reason: 'notInstalled',
172
+ };
173
+ }
174
+
175
+ const next = persisted.value.filter((record) => record.id !== extensionId);
176
+ if (!this.persist(next)) {
177
+ return this.failed(extensionId, target.enabled);
178
+ }
179
+
180
+ this.publishInstalledRecords(next);
181
+ return this.reloadRequired(extensionId, false);
182
+ }
183
+
184
+ toggleInstalledExtension(
185
+ extensionId: string,
186
+ enabled: boolean,
187
+ ): ExtensionEnablementTransitionResult {
188
+ const current = this.installedRecords.find((record) => record.id === extensionId);
189
+ if (!current || extensionId.startsWith('workbench-kit.builtin.')) {
190
+ return this.failed(extensionId, current?.enabled ?? false);
191
+ }
192
+ if (current.enabled === enabled) {
193
+ return {
194
+ enabled,
195
+ extensionId,
196
+ kind: 'applied',
197
+ message: 'The extension is already in the requested state.',
198
+ };
199
+ }
200
+
201
+ const next = this.installedRecords.map((record) =>
202
+ record.id === extensionId ? { ...record, enabled } : record,
203
+ );
204
+ const eligibility = this.getSoftThemeEligibility(extensionId, enabled);
205
+ if (!eligibility.eligible) {
206
+ if (eligibility.kind === 'failed') {
207
+ return this.failed(extensionId, current.enabled);
208
+ }
209
+ if (!eligibility.commitRequestedState) {
210
+ return this.reloadRequired(extensionId, current.enabled);
211
+ }
212
+ if (!this.persist(next)) {
213
+ return this.failed(extensionId, current.enabled);
214
+ }
215
+ this.publishInstalledRecords(next);
216
+ return this.reloadRequired(extensionId, enabled);
217
+ }
218
+
219
+ return enabled
220
+ ? this.enableThemeExtension(eligibility.description, next)
221
+ : this.disableThemeExtension(eligibility.description, next);
222
+ }
223
+
224
+ dispose(): void {
225
+ if (this.disposed) {
226
+ return;
227
+ }
228
+ this.disposed = true;
229
+ this.listeners.clear();
230
+ this.registrationHandles.clear();
231
+ this.registrationLifetime.dispose();
232
+ }
233
+
234
+ private enableThemeExtension(
235
+ description: WorkbenchExtensionDescription,
236
+ nextRecords: readonly InstalledExtensionRecord[],
237
+ ): ExtensionEnablementTransitionResult {
238
+ const extensionId = description.manifest.id;
239
+ let registration: DisposableLike;
240
+ try {
241
+ registration = this.registry.registerExtension(description);
242
+ this.registrationLifetime.add(registration);
243
+ if (!this.areThemesVisible(description)) {
244
+ registration.dispose();
245
+ return this.failed(extensionId, false);
246
+ }
247
+ } catch {
248
+ return this.failed(extensionId, false);
249
+ }
250
+
251
+ if (!this.persist(nextRecords)) {
252
+ try {
253
+ registration.dispose();
254
+ } catch {
255
+ // The failed transition below never publishes the requested enabled state.
256
+ }
257
+ return this.failed(extensionId, false);
258
+ }
259
+
260
+ this.registrationHandles.set(extensionId, registration);
261
+ this.publishInstalledRecords(nextRecords);
262
+ return {
263
+ enabled: true,
264
+ extensionId,
265
+ kind: 'applied',
266
+ message: 'Applied without reloading the workbench.',
267
+ };
268
+ }
269
+
270
+ private disableThemeExtension(
271
+ description: WorkbenchExtensionDescription,
272
+ nextRecords: readonly InstalledExtensionRecord[],
273
+ ): ExtensionEnablementTransitionResult {
274
+ const extensionId = description.manifest.id;
275
+ const registration = this.registrationHandles.get(extensionId);
276
+ if (!registration) {
277
+ return this.reloadRequired(extensionId, false);
278
+ }
279
+
280
+ this.registrationHandles.delete(extensionId);
281
+ try {
282
+ registration.dispose();
283
+ } catch {
284
+ this.restoreRegistration(description, registration);
285
+ return this.failed(extensionId, true);
286
+ }
287
+ if (this.areThemesVisible(description)) {
288
+ this.restoreRegistration(description, registration);
289
+ return this.failed(extensionId, true);
290
+ }
291
+
292
+ if (!this.persist(nextRecords)) {
293
+ this.restoreRegistration(description);
294
+ return this.failed(extensionId, true);
295
+ }
296
+
297
+ this.publishInstalledRecords(nextRecords);
298
+ return {
299
+ enabled: false,
300
+ extensionId,
301
+ kind: 'applied',
302
+ message: 'Applied without reloading the workbench.',
303
+ };
304
+ }
305
+
306
+ private restoreRegistration(
307
+ description: WorkbenchExtensionDescription,
308
+ existingRegistration?: DisposableLike,
309
+ ): void {
310
+ const extensionId = description.manifest.id;
311
+ if (this.registry.getExtension(extensionId) && this.areThemesVisible(description)) {
312
+ if (existingRegistration) {
313
+ this.registrationHandles.set(extensionId, existingRegistration);
314
+ }
315
+ return;
316
+ }
317
+
318
+ try {
319
+ const restored = this.registry.registerExtension(description);
320
+ this.registrationLifetime.add(restored);
321
+ this.registrationHandles.set(extensionId, restored);
322
+ } catch {
323
+ // A failed compensation is still surfaced as `failed`; no success is claimed.
324
+ }
325
+ }
326
+
327
+ private getSoftThemeEligibility(
328
+ extensionId: string,
329
+ enabled: boolean,
330
+ ):
331
+ | { readonly eligible: true; readonly description: WorkbenchExtensionDescription }
332
+ | {
333
+ readonly commitRequestedState: boolean;
334
+ readonly eligible: false;
335
+ readonly kind: 'failed' | 'reloadRequired';
336
+ } {
337
+ const description = this.availableExtensionsById.get(extensionId);
338
+ if (!description) {
339
+ return { commitRequestedState: true, eligible: false, kind: 'reloadRequired' };
340
+ }
341
+ if (!this.integrityAcceptedExtensionIds.has(extensionId)) {
342
+ return { commitRequestedState: false, eligible: false, kind: 'failed' };
343
+ }
344
+ if (!isThemeOnlyDeclarativeExtension(description)) {
345
+ return { commitRequestedState: true, eligible: false, kind: 'reloadRequired' };
346
+ }
347
+
348
+ const selectionProtection = this.themeSelectionProtection;
349
+ if (
350
+ selectionProtection?.kind !== 'known' ||
351
+ selectionProtection.themeRegistryRevision !== this.registry.themes.getRevision()
352
+ ) {
353
+ return { commitRequestedState: false, eligible: false, kind: 'reloadRequired' };
354
+ }
355
+
356
+ const protectedThemeIds = new Set(selectionProtection.protectedThemeIds);
357
+
358
+ const themes = description.manifest.contributes?.themes ?? [];
359
+ if (themes.some((theme) => protectedThemeIds.has(theme.id))) {
360
+ return { commitRequestedState: true, eligible: false, kind: 'reloadRequired' };
361
+ }
362
+
363
+ const registeredHardDependent = [...this.registrationHandles.keys()].some((candidateId) => {
364
+ if (candidateId === extensionId) {
365
+ return false;
366
+ }
367
+ return this.availableExtensionsById
368
+ .get(candidateId)
369
+ ?.manifest.extensionDependencies?.includes(extensionId);
370
+ });
371
+ if (registeredHardDependent) {
372
+ return { commitRequestedState: true, eligible: false, kind: 'reloadRequired' };
373
+ }
374
+
375
+ const hasRegistration = this.registrationHandles.has(extensionId);
376
+ if ((enabled && hasRegistration) || (!enabled && !hasRegistration)) {
377
+ return { commitRequestedState: true, eligible: false, kind: 'reloadRequired' };
378
+ }
379
+
380
+ return { eligible: true, description };
381
+ }
382
+
383
+ private areThemesVisible(description: WorkbenchExtensionDescription): boolean {
384
+ return (description.manifest.contributes?.themes ?? []).every(
385
+ (theme) => this.registry.themes.getTheme(theme.id)?.extensionId === description.manifest.id,
386
+ );
387
+ }
388
+
389
+ private persist(records: readonly InstalledExtensionRecord[]): boolean {
390
+ return saveInstalledExtensionsResult(records, this.storageKey, this.storage, {
391
+ onDiagnostic: this.onPersistenceDiagnostic,
392
+ }).committed;
393
+ }
394
+
395
+ private publishInstalledRecords(records: readonly InstalledExtensionRecord[]): void {
396
+ this.installedRecords = [...records];
397
+ for (const listener of [...this.listeners]) {
398
+ listener();
399
+ }
400
+ }
401
+
402
+ private isRecordEnabled(extensionId: string): boolean {
403
+ return this.installedRecords.find((record) => record.id === extensionId)?.enabled ?? false;
404
+ }
405
+
406
+ private reloadRequired(
407
+ extensionId: string,
408
+ enabled: boolean,
409
+ ): ExtensionEnablementTransitionResult {
410
+ return {
411
+ enabled,
412
+ extensionId,
413
+ kind: 'reloadRequired',
414
+ message: 'Reload required to finish applying this extension change.',
415
+ };
416
+ }
417
+
418
+ private failed(extensionId: string, enabled: boolean): ExtensionEnablementTransitionResult {
419
+ return {
420
+ enabled,
421
+ extensionId,
422
+ kind: 'failed',
423
+ message: 'The extension change failed. The previous state was retained.',
424
+ };
425
+ }
426
+ }
427
+
428
+ function isThemeOnlyDeclarativeExtension(description: WorkbenchExtensionDescription): boolean {
429
+ const contributes = description.manifest.contributes;
430
+ if (!contributes?.themes?.length || description.module !== undefined) {
431
+ return false;
432
+ }
433
+ if (
434
+ (description.manifest.capabilities?.provides?.length ?? 0) > 0 ||
435
+ (description.manifest.capabilities?.requires?.length ?? 0) > 0 ||
436
+ (description.manifest.extensionDependencies?.length ?? 0) > 0
437
+ ) {
438
+ return false;
439
+ }
440
+
441
+ return Object.entries(contributes).every(([key, value]) => {
442
+ if (key === 'themes') {
443
+ return Array.isArray(value) && value.length > 0;
444
+ }
445
+ if (Array.isArray(value)) {
446
+ return value.length === 0;
447
+ }
448
+ if (value && typeof value === 'object') {
449
+ return Object.keys(value).length === 0;
450
+ }
451
+ return value === undefined;
452
+ });
453
+ }