@workbench-kit/shell-react 0.0.2-prototype.0.2.40 → 0.0.2-prototype.0.2.43

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 (35) hide show
  1. package/README.md +42 -2
  2. package/package.json +13 -10
  3. package/src/extensions/extension-enablement-controller.ts +40 -9
  4. package/src/extensions/theme-selection-protection.ts +80 -55
  5. package/src/field-remap/chrome-labels.ts +18 -0
  6. package/src/field-remap/convert-note-editor.tsx +30 -24
  7. package/src/field-remap/convert-palette.tsx +6 -0
  8. package/src/field-remap/demo.tsx +8 -2
  9. package/src/field-remap/detail-panel.tsx +225 -192
  10. package/src/field-remap/drag-payload.ts +48 -0
  11. package/src/field-remap/flow-adapter.ts +216 -88
  12. package/src/field-remap/flow-ops.ts +150 -0
  13. package/src/field-remap/flow.tsx +1055 -162
  14. package/src/field-remap/index.ts +2 -0
  15. package/src/field-remap/io-class-browse.tsx +34 -22
  16. package/src/field-remap/keyboard.ts +16 -0
  17. package/src/field-remap/modal-detail.tsx +34 -0
  18. package/src/field-remap/panel.tsx +50 -14
  19. package/src/field-remap/transform-options-editor.tsx +5 -1
  20. package/src/field-remap/view.css +61 -42
  21. package/src/index.ts +7 -0
  22. package/src/management/keybinding-overrides-storage.ts +48 -23
  23. package/src/management/keybinding-settings.tsx +10 -1
  24. package/src/management/use-keybinding-management.ts +69 -19
  25. package/src/shell/appearance-catalog.ts +453 -0
  26. package/src/shell/appearance-controller.ts +331 -0
  27. package/src/shell/appearance-presentation.ts +269 -0
  28. package/src/shell/provider.tsx +147 -18
  29. package/src/shell/settings.tsx +282 -153
  30. package/src/shell/shell.tsx +88 -18
  31. package/src/workbench/appearance-storage.ts +2 -2
  32. package/src/workbench/command-host-controller.tsx +223 -0
  33. package/src/workbench/command-host.tsx +100 -150
  34. package/src/workbench/keybinding-bridge.ts +84 -38
  35. package/src/workbench/shell-command-registration.ts +27 -2
@@ -1,9 +1,10 @@
1
- import { useMemo } from 'react';
2
- import { buildKeybindingManagementEntries } from '@workbench-kit/platform';
1
+ import { useLayoutEffect, useMemo, useState } from 'react';
2
+ import { createKeybindingManagementModel } from '@workbench-kit/platform';
3
3
  import { createWorkbenchShellCommands } from '@workbench-kit/react/workbench';
4
4
 
5
5
  import { useWorkbench, type WorkbenchContextValue } from '../shell/provider.js';
6
6
  import { resolveShellCommandActivities } from '../workbench/command-palette.js';
7
+ import { normalizeExtensionKeybindingCandidates } from '../workbench/keybinding-bridge.js';
7
8
 
8
9
  export function useKeybindingManagementModel() {
9
10
  const {
@@ -11,37 +12,84 @@ export function useKeybindingManagementModel() {
11
12
  commands,
12
13
  extensionCatalog,
13
14
  keybindings,
15
+ keybindingEditingDisabledReason,
14
16
  keybindingOverrides,
17
+ keybindingPlatform,
18
+ keybindingProjection,
15
19
  resetCommandKeybindingOverride,
16
20
  setCommandKeybindingOverride,
17
21
  views,
18
22
  } = useWorkbench();
19
23
 
20
- const defaults = useMemo(
21
- () => keybindings.getKeybindings(),
22
- [keybindingOverrides.length, keybindings],
23
- );
24
+ const [keybindingRevision, setKeybindingRevision] = useState(() => keybindings.revision);
25
+ useLayoutEffect(() => {
26
+ const disposable = keybindings.onDidChangeKeybindings(() => {
27
+ setKeybindingRevision(keybindings.revision);
28
+ });
29
+ setKeybindingRevision(keybindings.revision);
24
30
 
25
- const entries = useMemo(
31
+ return () => {
32
+ disposable.dispose();
33
+ };
34
+ }, [keybindings]);
35
+ const extensionDefaults = useMemo(
36
+ () =>
37
+ keybindings.getKeybindings().flatMap((binding) =>
38
+ normalizeExtensionKeybindingCandidates(binding.key, keybindingPlatform, true).map(
39
+ (key) => ({
40
+ ...binding,
41
+ key,
42
+ }),
43
+ ),
44
+ ),
45
+ [keybindingPlatform, keybindingRevision, keybindings],
46
+ );
47
+ const defaults = useMemo(() => {
48
+ const genericCommandIds = new Set(
49
+ keybindingProjection.defaults.map((binding) => binding.command),
50
+ );
51
+ return [
52
+ ...keybindingProjection.defaults,
53
+ ...extensionDefaults.filter((binding) => !genericCommandIds.has(binding.command)),
54
+ ];
55
+ }, [extensionDefaults, keybindingProjection.defaults]);
56
+
57
+ const model = useMemo(
26
58
  () =>
27
- buildKeybindingManagementEntries({
28
- commands: collectKeybindingManagementCommands({
29
- activities,
30
- commands,
31
- extensionCatalog,
32
- views,
33
- }),
34
- defaults,
59
+ createKeybindingManagementModel({
60
+ editingDisabledReason: keybindingEditingDisabledReason,
61
+ onOverridesChange: () => undefined,
35
62
  overrides: keybindingOverrides,
63
+ platform: keybindingPlatform,
64
+ projection: {
65
+ ...keybindingProjection,
66
+ commands: collectKeybindingManagementCommands({
67
+ activities,
68
+ commands,
69
+ extensionCatalog,
70
+ views,
71
+ }),
72
+ defaults,
73
+ },
36
74
  }),
37
- [activities, commands, defaults, extensionCatalog, keybindingOverrides, views],
75
+ [
76
+ activities,
77
+ commands,
78
+ defaults,
79
+ extensionCatalog,
80
+ keybindingEditingDisabledReason,
81
+ keybindingOverrides,
82
+ keybindingPlatform,
83
+ keybindingProjection,
84
+ views,
85
+ ],
38
86
  );
39
87
 
40
88
  const overrideCount = keybindingOverrides.length;
41
89
 
42
90
  const setKeybinding = (commandId: string, key: string | undefined) => {
43
- const defaultKey = defaults.find((binding) => binding.command === commandId)?.key;
44
- if (!key || key === defaultKey) {
91
+ const commandDefaults = defaults.filter((binding) => binding.command === commandId);
92
+ if (!key || (commandDefaults.length === 1 && key === commandDefaults[0]?.key)) {
45
93
  resetCommandKeybindingOverride(commandId);
46
94
  return;
47
95
  }
@@ -50,8 +98,10 @@ export function useKeybindingManagementModel() {
50
98
  };
51
99
 
52
100
  return {
53
- entries,
101
+ editingDisabledReason: keybindingEditingDisabledReason,
102
+ entries: model.entries,
54
103
  overrideCount,
104
+ platform: keybindingPlatform,
55
105
  resetKeybinding: resetCommandKeybindingOverride,
56
106
  setKeybinding,
57
107
  };
@@ -0,0 +1,453 @@
1
+ import {
2
+ DARK_THEME_PRESET_MANIFEST,
3
+ LIGHT_THEME_PRESET_MANIFEST,
4
+ } from '@workbench-kit/react/workbench';
5
+ import {
6
+ HOST_WORKBENCH_THEME_EXTENSION_ID,
7
+ REQUIRED_THEME_TOKEN_KEYS,
8
+ type ThemeRegistry,
9
+ type WorkbenchThemeContribution,
10
+ } from '@workbench-kit/workbench-core';
11
+
12
+ export type WorkbenchAppearanceCatalogSource =
13
+ 'builtin-preset' | 'legacy-host-theme' | 'legacy-extension-theme' | 'host-option';
14
+
15
+ export type WorkbenchAppearanceCatalogMode = 'light' | 'dark' | undefined;
16
+
17
+ export type WorkbenchAppearanceSelectionTarget = 'flat-theme' | 'light-preset' | 'dark-preset';
18
+
19
+ export interface WorkbenchAppearanceCatalogEntry {
20
+ readonly id: string;
21
+ readonly label: string;
22
+ readonly source: WorkbenchAppearanceCatalogSource;
23
+ readonly sourceOrdinal: number;
24
+ readonly mode: WorkbenchAppearanceCatalogMode;
25
+ readonly extensionId?: string;
26
+ readonly hasLegacyCssOverrides: boolean;
27
+ readonly legacyTokenOverrides?: Readonly<Record<string, string>>;
28
+ }
29
+
30
+ export interface WorkbenchAppearanceCatalogDiagnostic {
31
+ readonly code: 'appearance-id-conflict';
32
+ readonly id: string;
33
+ readonly target: WorkbenchAppearanceSelectionTarget;
34
+ readonly sources: readonly WorkbenchAppearanceCatalogSource[];
35
+ }
36
+
37
+ export interface WorkbenchAppearanceCatalogSnapshot {
38
+ readonly themeRegistryRevision: number;
39
+ readonly sourceFingerprint: string;
40
+ readonly entries: readonly WorkbenchAppearanceCatalogEntry[];
41
+ readonly diagnostics: readonly WorkbenchAppearanceCatalogDiagnostic[];
42
+ }
43
+
44
+ export type WorkbenchAppearanceSelectionResolution =
45
+ | { readonly status: 'resolved'; readonly entry: WorkbenchAppearanceCatalogEntry }
46
+ | { readonly status: 'unresolved'; readonly id: string }
47
+ | {
48
+ readonly status: 'conflicted';
49
+ readonly id: string;
50
+ readonly candidates: readonly WorkbenchAppearanceCatalogEntry[];
51
+ }
52
+ | {
53
+ readonly status: 'wrong-scheme';
54
+ readonly id: string;
55
+ readonly expected: 'light' | 'dark';
56
+ };
57
+
58
+ export interface WorkbenchAppearanceHostOptionInput {
59
+ readonly id: string;
60
+ readonly label: string;
61
+ }
62
+
63
+ export interface CreateWorkbenchAppearanceCatalogSnapshotInput {
64
+ readonly themes: ThemeRegistry;
65
+ readonly hostOptions?: readonly WorkbenchAppearanceHostOptionInput[] | undefined;
66
+ }
67
+
68
+ interface TargetIndex {
69
+ readonly entries: readonly WorkbenchAppearanceCatalogEntry[];
70
+ readonly entriesById: ReadonlyMap<string, readonly WorkbenchAppearanceCatalogEntry[]>;
71
+ }
72
+
73
+ interface SnapshotIndexes {
74
+ readonly targets: Readonly<Record<WorkbenchAppearanceSelectionTarget, TargetIndex>>;
75
+ }
76
+
77
+ const TARGET_ORDER = Object.freeze([
78
+ 'flat-theme',
79
+ 'light-preset',
80
+ 'dark-preset',
81
+ ] as const satisfies readonly WorkbenchAppearanceSelectionTarget[]);
82
+
83
+ const SNAPSHOT_INDEXES = new WeakMap<WorkbenchAppearanceCatalogSnapshot, SnapshotIndexes>();
84
+
85
+ const ABSENT_OWN_DATA = Symbol('absent-own-data');
86
+ const INVALID_OWN_DATA = Symbol('invalid-own-data');
87
+
88
+ type OwnDataValue = unknown | typeof ABSENT_OWN_DATA | typeof INVALID_OWN_DATA;
89
+
90
+ function readOwnDataProperty(value: object, key: string): OwnDataValue {
91
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
92
+ if (descriptor === undefined) {
93
+ return ABSENT_OWN_DATA;
94
+ }
95
+ return 'value' in descriptor ? descriptor.value : INVALID_OWN_DATA;
96
+ }
97
+
98
+ function readOwnString(value: object, key: string): string | typeof INVALID_OWN_DATA {
99
+ const candidate = readOwnDataProperty(value, key);
100
+ return typeof candidate === 'string' ? candidate : INVALID_OWN_DATA;
101
+ }
102
+
103
+ type TokenOverrideSnapshot =
104
+ | { readonly kind: 'absent' }
105
+ | { readonly kind: 'invalid' }
106
+ | { readonly kind: 'present'; readonly value: Readonly<Record<string, string>> };
107
+
108
+ function snapshotTokenOverrides(theme: object): TokenOverrideSnapshot {
109
+ const candidate = readOwnDataProperty(theme, 'tokenOverrides');
110
+ if (candidate === ABSENT_OWN_DATA || candidate === undefined) {
111
+ return { kind: 'absent' };
112
+ }
113
+ if (
114
+ candidate === INVALID_OWN_DATA ||
115
+ candidate === null ||
116
+ typeof candidate !== 'object' ||
117
+ Array.isArray(candidate)
118
+ ) {
119
+ return { kind: 'invalid' };
120
+ }
121
+
122
+ const descriptors = Object.getOwnPropertyDescriptors(candidate);
123
+ const copiedPairs: [string, string][] = [];
124
+ for (const key of Object.keys(descriptors)) {
125
+ const descriptor = descriptors[key];
126
+ if (!descriptor?.enumerable) {
127
+ continue;
128
+ }
129
+ if (!('value' in descriptor) || typeof descriptor.value !== 'string') {
130
+ return { kind: 'invalid' };
131
+ }
132
+ copiedPairs.push([key, descriptor.value]);
133
+ }
134
+
135
+ const copy = Object.freeze(Object.fromEntries(copiedPairs));
136
+ if (!REQUIRED_THEME_TOKEN_KEYS.every((key) => Object.prototype.hasOwnProperty.call(copy, key))) {
137
+ return { kind: 'invalid' };
138
+ }
139
+
140
+ return { kind: 'present', value: copy };
141
+ }
142
+
143
+ function createBuiltinEntries(): readonly WorkbenchAppearanceCatalogEntry[] {
144
+ let sourceOrdinal = 0;
145
+ const entries: WorkbenchAppearanceCatalogEntry[] = [];
146
+
147
+ for (const preset of LIGHT_THEME_PRESET_MANIFEST) {
148
+ const id = readOwnString(preset, 'id');
149
+ const label = readOwnString(preset, 'label');
150
+ if (id !== INVALID_OWN_DATA && label !== INVALID_OWN_DATA) {
151
+ entries.push(
152
+ Object.freeze({
153
+ hasLegacyCssOverrides: false,
154
+ id,
155
+ label,
156
+ mode: 'light',
157
+ source: 'builtin-preset',
158
+ sourceOrdinal,
159
+ }),
160
+ );
161
+ }
162
+ sourceOrdinal += 1;
163
+ }
164
+
165
+ for (const preset of DARK_THEME_PRESET_MANIFEST) {
166
+ const id = readOwnString(preset, 'id');
167
+ const label = readOwnString(preset, 'label');
168
+ if (id !== INVALID_OWN_DATA && label !== INVALID_OWN_DATA) {
169
+ entries.push(
170
+ Object.freeze({
171
+ hasLegacyCssOverrides: false,
172
+ id,
173
+ label,
174
+ mode: 'dark',
175
+ source: 'builtin-preset',
176
+ sourceOrdinal,
177
+ }),
178
+ );
179
+ }
180
+ sourceOrdinal += 1;
181
+ }
182
+
183
+ return entries;
184
+ }
185
+
186
+ function createHostOptionEntries(
187
+ hostOptions: readonly WorkbenchAppearanceHostOptionInput[],
188
+ ): readonly WorkbenchAppearanceCatalogEntry[] {
189
+ const entries: WorkbenchAppearanceCatalogEntry[] = [];
190
+
191
+ hostOptions.forEach((option, sourceOrdinal) => {
192
+ const id = readOwnString(option, 'id');
193
+ const label = readOwnString(option, 'label');
194
+ if (id === INVALID_OWN_DATA || label === INVALID_OWN_DATA) {
195
+ return;
196
+ }
197
+
198
+ entries.push(
199
+ Object.freeze({
200
+ hasLegacyCssOverrides: false,
201
+ id,
202
+ label,
203
+ mode: undefined,
204
+ source: 'host-option' as const,
205
+ sourceOrdinal,
206
+ }),
207
+ );
208
+ });
209
+
210
+ return entries;
211
+ }
212
+
213
+ function createRegisteredThemeEntry(
214
+ theme: WorkbenchThemeContribution,
215
+ sourceOrdinal: number,
216
+ ): WorkbenchAppearanceCatalogEntry | undefined {
217
+ const extensionId = readOwnString(theme, 'extensionId');
218
+ const id = readOwnString(theme, 'id');
219
+ const label = readOwnString(theme, 'label');
220
+ const mode = readOwnString(theme, 'mode');
221
+ const tokenOverrides = snapshotTokenOverrides(theme);
222
+
223
+ if (
224
+ extensionId === INVALID_OWN_DATA ||
225
+ id === INVALID_OWN_DATA ||
226
+ label === INVALID_OWN_DATA ||
227
+ (mode !== 'dark' && mode !== 'light') ||
228
+ tokenOverrides.kind === 'invalid'
229
+ ) {
230
+ return undefined;
231
+ }
232
+
233
+ const base: WorkbenchAppearanceCatalogEntry = {
234
+ extensionId,
235
+ hasLegacyCssOverrides: tokenOverrides.kind === 'present',
236
+ id,
237
+ label,
238
+ mode,
239
+ source:
240
+ extensionId === HOST_WORKBENCH_THEME_EXTENSION_ID
241
+ ? ('legacy-host-theme' as const)
242
+ : ('legacy-extension-theme' as const),
243
+ sourceOrdinal,
244
+ };
245
+
246
+ return tokenOverrides.kind === 'absent'
247
+ ? Object.freeze(base)
248
+ : Object.freeze({ ...base, legacyTokenOverrides: tokenOverrides.value });
249
+ }
250
+
251
+ function createRegisteredThemeEntries(
252
+ themes: readonly WorkbenchThemeContribution[],
253
+ ): readonly WorkbenchAppearanceCatalogEntry[] {
254
+ const entries: WorkbenchAppearanceCatalogEntry[] = [];
255
+
256
+ themes.forEach((theme, sourceOrdinal) => {
257
+ const entry = createRegisteredThemeEntry(theme, sourceOrdinal);
258
+ if (entry) {
259
+ entries.push(entry);
260
+ }
261
+ });
262
+
263
+ return entries;
264
+ }
265
+
266
+ function isRegisteredTheme(
267
+ entry: WorkbenchAppearanceCatalogEntry,
268
+ ): entry is WorkbenchAppearanceCatalogEntry & {
269
+ readonly source: 'legacy-host-theme' | 'legacy-extension-theme';
270
+ } {
271
+ return entry.source === 'legacy-host-theme' || entry.source === 'legacy-extension-theme';
272
+ }
273
+
274
+ function isEligibleForTarget(
275
+ entry: WorkbenchAppearanceCatalogEntry,
276
+ target: WorkbenchAppearanceSelectionTarget,
277
+ ): boolean {
278
+ switch (target) {
279
+ case 'flat-theme':
280
+ return entry.source === 'host-option' || isRegisteredTheme(entry);
281
+ case 'light-preset':
282
+ return entry.mode === 'light' && entry.source !== 'host-option';
283
+ case 'dark-preset':
284
+ return entry.mode === 'dark' && entry.source !== 'host-option';
285
+ }
286
+ }
287
+
288
+ function buildTargetIndex(
289
+ entries: readonly WorkbenchAppearanceCatalogEntry[],
290
+ target: WorkbenchAppearanceSelectionTarget,
291
+ ): TargetIndex {
292
+ const eligibleEntries = Object.freeze(
293
+ entries.filter((entry) => isEligibleForTarget(entry, target)),
294
+ );
295
+ const mutableEntriesById = new Map<string, WorkbenchAppearanceCatalogEntry[]>();
296
+
297
+ for (const entry of eligibleEntries) {
298
+ const candidates = mutableEntriesById.get(entry.id);
299
+ if (candidates) {
300
+ candidates.push(entry);
301
+ } else {
302
+ mutableEntriesById.set(entry.id, [entry]);
303
+ }
304
+ }
305
+
306
+ const entriesById = new Map<string, readonly WorkbenchAppearanceCatalogEntry[]>();
307
+ for (const [id, candidates] of mutableEntriesById) {
308
+ entriesById.set(id, Object.freeze([...candidates]));
309
+ }
310
+
311
+ return Object.freeze({ entries: eligibleEntries, entriesById });
312
+ }
313
+
314
+ function createSnapshotIndexes(
315
+ entries: readonly WorkbenchAppearanceCatalogEntry[],
316
+ ): SnapshotIndexes {
317
+ return Object.freeze({
318
+ targets: Object.freeze({
319
+ 'dark-preset': buildTargetIndex(entries, 'dark-preset'),
320
+ 'flat-theme': buildTargetIndex(entries, 'flat-theme'),
321
+ 'light-preset': buildTargetIndex(entries, 'light-preset'),
322
+ }),
323
+ });
324
+ }
325
+
326
+ function createConflictDiagnostics(
327
+ indexes: SnapshotIndexes,
328
+ ): readonly WorkbenchAppearanceCatalogDiagnostic[] {
329
+ const diagnostics: WorkbenchAppearanceCatalogDiagnostic[] = [];
330
+
331
+ for (const target of TARGET_ORDER) {
332
+ for (const [id, candidates] of indexes.targets[target].entriesById) {
333
+ if (candidates.length < 2) {
334
+ continue;
335
+ }
336
+
337
+ diagnostics.push(
338
+ Object.freeze({
339
+ code: 'appearance-id-conflict',
340
+ id,
341
+ sources: Object.freeze(candidates.map((candidate) => candidate.source)),
342
+ target,
343
+ }),
344
+ );
345
+ }
346
+ }
347
+
348
+ return Object.freeze(diagnostics);
349
+ }
350
+
351
+ function optionalFingerprintPart(
352
+ tag: 'extension' | 'mode',
353
+ value: string | undefined,
354
+ ): readonly [typeof tag, 'absent'] | readonly [typeof tag, 'value', string] {
355
+ return value === undefined
356
+ ? Object.freeze([tag, 'absent'] as const)
357
+ : Object.freeze([tag, 'value', value] as const);
358
+ }
359
+
360
+ function overridesFingerprintPart(
361
+ overrides: Readonly<Record<string, string>> | undefined,
362
+ ): readonly ['overrides', 'absent'] | readonly ['overrides', 'value', readonly unknown[]] {
363
+ if (overrides === undefined) {
364
+ return Object.freeze(['overrides', 'absent'] as const);
365
+ }
366
+
367
+ const pairs = Object.freeze(
368
+ Object.keys(overrides)
369
+ .sort()
370
+ .map((key) => Object.freeze(['override', key, overrides[key]] as const)),
371
+ );
372
+ return Object.freeze(['overrides', 'value', pairs] as const);
373
+ }
374
+
375
+ function createSourceFingerprint(entries: readonly WorkbenchAppearanceCatalogEntry[]): string {
376
+ const tuples = entries.map((entry) =>
377
+ Object.freeze([
378
+ 'appearance-entry',
379
+ Object.freeze(['source', entry.source] as const),
380
+ Object.freeze(['source-ordinal', entry.sourceOrdinal] as const),
381
+ Object.freeze(['id', entry.id] as const),
382
+ Object.freeze(['label', entry.label] as const),
383
+ optionalFingerprintPart('mode', entry.mode),
384
+ optionalFingerprintPart('extension', entry.extensionId),
385
+ overridesFingerprintPart(entry.legacyTokenOverrides),
386
+ ] as const),
387
+ );
388
+ return JSON.stringify(tuples);
389
+ }
390
+
391
+ export function createWorkbenchAppearanceCatalogSnapshot({
392
+ hostOptions = [],
393
+ themes,
394
+ }: CreateWorkbenchAppearanceCatalogSnapshotInput): WorkbenchAppearanceCatalogSnapshot {
395
+ const entries = Object.freeze([
396
+ ...createBuiltinEntries(),
397
+ ...createHostOptionEntries(hostOptions),
398
+ ...createRegisteredThemeEntries(themes.getThemes()),
399
+ ]);
400
+ const indexes = createSnapshotIndexes(entries);
401
+ const snapshot = Object.freeze({
402
+ diagnostics: createConflictDiagnostics(indexes),
403
+ entries,
404
+ sourceFingerprint: createSourceFingerprint(entries),
405
+ themeRegistryRevision: themes.getRevision(),
406
+ });
407
+
408
+ SNAPSHOT_INDEXES.set(snapshot, indexes);
409
+ return snapshot;
410
+ }
411
+
412
+ function getSnapshotIndexes(snapshot: WorkbenchAppearanceCatalogSnapshot): SnapshotIndexes {
413
+ const indexes = SNAPSHOT_INDEXES.get(snapshot);
414
+ if (!indexes) {
415
+ throw new Error('Workbench appearance catalog snapshot was not created by this module.');
416
+ }
417
+ return indexes;
418
+ }
419
+
420
+ export function getWorkbenchAppearanceCatalogEntries(
421
+ snapshot: WorkbenchAppearanceCatalogSnapshot,
422
+ target: WorkbenchAppearanceSelectionTarget,
423
+ ): readonly WorkbenchAppearanceCatalogEntry[] {
424
+ return getSnapshotIndexes(snapshot).targets[target].entries;
425
+ }
426
+
427
+ export function resolveWorkbenchAppearanceSelection(
428
+ snapshot: WorkbenchAppearanceCatalogSnapshot,
429
+ target: WorkbenchAppearanceSelectionTarget,
430
+ id: string,
431
+ ): WorkbenchAppearanceSelectionResolution {
432
+ const indexes = getSnapshotIndexes(snapshot);
433
+ const candidates = indexes.targets[target].entriesById.get(id);
434
+
435
+ if (candidates?.length === 1) {
436
+ return Object.freeze({ entry: candidates[0], status: 'resolved' });
437
+ }
438
+
439
+ if (candidates && candidates.length > 1) {
440
+ return Object.freeze({ candidates, id, status: 'conflicted' });
441
+ }
442
+
443
+ if (target === 'light-preset' || target === 'dark-preset') {
444
+ const expected = target === 'light-preset' ? 'light' : 'dark';
445
+ const oppositeTarget = target === 'light-preset' ? 'dark-preset' : 'light-preset';
446
+ const oppositeCandidates = indexes.targets[oppositeTarget].entriesById.get(id);
447
+ if (oppositeCandidates?.length === 1) {
448
+ return Object.freeze({ expected, id, status: 'wrong-scheme' });
449
+ }
450
+ }
451
+
452
+ return Object.freeze({ id, status: 'unresolved' });
453
+ }