@workbench-kit/shell-react 0.0.2-prototype.0.2.32 → 0.0.2-prototype.0.2.34
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.
- package/README.md +32 -0
- package/package.json +9 -9
- package/src/commands/use-command-descriptors.ts +7 -3
- package/src/commands/use-extension-registry-command-descriptors.ts +23 -6
- package/src/devtools/use-workbench-devtools-snapshot.ts +39 -14
- package/src/devtools/workbench-devtools-snapshot.ts +32 -13
- package/src/editor/area.tsx +3 -2
- package/src/editor/state-storage.ts +31 -0
- package/src/editor/tab-context-menu.ts +8 -8
- package/src/editor/use-editor.ts +6 -6
- package/src/explorer/context-menu.ts +8 -4
- package/src/explorer/view.tsx +4 -3
- package/src/extensions/canonical-extension-descriptions.ts +58 -0
- package/src/extensions/context-menu.ts +9 -6
- package/src/extensions/extension-enablement-context.ts +15 -0
- package/src/extensions/extension-enablement-controller.ts +453 -0
- package/src/extensions/management-model.ts +111 -47
- package/src/extensions/theme-selection-protection.ts +98 -0
- package/src/extensions/uninstall-eligibility.ts +103 -0
- package/src/extensions/use-extension-management.ts +151 -67
- package/src/extensions/view.tsx +4 -0
- package/src/field-remap/chrome-labels.ts +87 -2
- package/src/field-remap/convert-palette.tsx +164 -18
- package/src/field-remap/demo.tsx +48 -3
- package/src/field-remap/detail-panel.tsx +6 -5
- package/src/field-remap/flow.tsx +341 -155
- package/src/field-remap/history.ts +99 -0
- package/src/field-remap/index.ts +9 -1
- package/src/field-remap/panel.tsx +388 -97
- package/src/field-remap/preview-controller.ts +122 -0
- package/src/field-remap/preview.tsx +171 -0
- package/src/field-remap/view.css +154 -2
- package/src/index.ts +16 -1
- package/src/management/keybinding-overrides-storage.ts +28 -0
- package/src/management/preference-settings-storage.ts +28 -0
- package/src/management/settings.tsx +2 -0
- package/src/management/use-command-management.ts +31 -26
- package/src/management/use-keybinding-management.ts +25 -12
- package/src/shell/focused-extension-services.ts +138 -0
- package/src/shell/host-shell.tsx +13 -10
- package/src/shell/persistence-diagnostic-context.ts +11 -0
- package/src/shell/provider.tsx +373 -69
- package/src/shell/settings.tsx +25 -16
- package/src/shell/shell.tsx +119 -78
- package/src/shell/view-host.tsx +23 -22
- package/src/storage/local-json-storage.ts +43 -29
- package/src/storage/persistence-diagnostics.ts +72 -0
- package/src/workbench/appearance-storage.ts +28 -0
- package/src/workbench/command-host.tsx +19 -22
- package/src/workbench/command-palette.ts +14 -13
- package/src/workbench/layout-storage.ts +52 -5
- package/src/workbench/use-persisted-appearance.ts +38 -12
|
@@ -7,21 +7,35 @@ import {
|
|
|
7
7
|
type ExtensionFeatureSpec,
|
|
8
8
|
type ExtensionInstallPlan,
|
|
9
9
|
type ExtensionInstallPlanInstallSource,
|
|
10
|
-
type ExtensionRegistry,
|
|
11
10
|
type InstalledExtensionRecord,
|
|
12
11
|
type WorkbenchExtensionDescription,
|
|
13
12
|
} from '@workbench-kit/workbench-core';
|
|
13
|
+
import type { WorkbenchExtensionCatalogReader } from '../shell/provider.js';
|
|
14
14
|
import type {
|
|
15
15
|
ExtensionCatalogBrowseEntry,
|
|
16
16
|
ExtensionManagementDiagnosticSummary,
|
|
17
17
|
ExtensionManagementEntry,
|
|
18
18
|
ExtensionManagementFeatureSummary,
|
|
19
|
+
ExtensionManagementTransition,
|
|
19
20
|
} from '@workbench-kit/react/workbench/management';
|
|
21
|
+
import {
|
|
22
|
+
createExtensionUninstallEvaluation,
|
|
23
|
+
type ExtensionUninstallEligibility,
|
|
24
|
+
} from './uninstall-eligibility.js';
|
|
25
|
+
import { createCanonicalExtensionDescriptionSnapshot } from './canonical-extension-descriptions.js';
|
|
26
|
+
|
|
27
|
+
export interface ExtensionUninstallActionConstraint {
|
|
28
|
+
readonly eligibility: Exclude<ExtensionUninstallEligibility, { readonly kind: 'eligible' }>;
|
|
29
|
+
readonly extensionId: string;
|
|
30
|
+
}
|
|
20
31
|
|
|
21
32
|
export interface CreateExtensionManagementEntriesInput {
|
|
22
33
|
readonly availableExtensions: readonly WorkbenchExtensionDescription[];
|
|
23
|
-
readonly
|
|
34
|
+
readonly extensionCatalog: WorkbenchExtensionCatalogReader;
|
|
24
35
|
readonly installedRecords: readonly InstalledExtensionRecord[];
|
|
36
|
+
readonly transition?:
|
|
37
|
+
(ExtensionManagementTransition & { readonly extensionId: string }) | undefined;
|
|
38
|
+
readonly uninstallActionConstraint?: ExtensionUninstallActionConstraint | undefined;
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
export interface CreateExtensionCatalogBrowseEntriesInput extends CreateExtensionManagementEntriesInput {
|
|
@@ -37,27 +51,55 @@ export interface ExtensionInstallPlanningContext extends CreateExtensionManageme
|
|
|
37
51
|
|
|
38
52
|
export function createExtensionManagementEntries({
|
|
39
53
|
availableExtensions,
|
|
40
|
-
|
|
54
|
+
extensionCatalog,
|
|
41
55
|
installedRecords,
|
|
56
|
+
transition,
|
|
57
|
+
uninstallActionConstraint,
|
|
42
58
|
}: CreateExtensionManagementEntriesInput): readonly ExtensionManagementEntry[] {
|
|
43
59
|
const installedById = new Map(installedRecords.map((record) => [record.id, record]));
|
|
44
|
-
const
|
|
60
|
+
const liveExtensions = extensionCatalog.getExtensions();
|
|
61
|
+
const liveExtensionIds = new Set(liveExtensions.map((extension) => extension.manifest.id));
|
|
62
|
+
const canonicalDescriptions = createCanonicalExtensionDescriptionSnapshot({
|
|
45
63
|
availableExtensions,
|
|
46
|
-
|
|
64
|
+
liveExtensions,
|
|
65
|
+
});
|
|
66
|
+
const uninstallEvaluation = createExtensionUninstallEvaluation({
|
|
67
|
+
canonicalDescriptions,
|
|
68
|
+
installedRecords,
|
|
69
|
+
});
|
|
70
|
+
const extensionFeatures = createExtensionManagementFeatureMaps(
|
|
71
|
+
canonicalDescriptions.descriptions,
|
|
72
|
+
extensionCatalog,
|
|
47
73
|
);
|
|
48
|
-
|
|
74
|
+
return canonicalDescriptions.descriptions
|
|
49
75
|
.map((extension) => {
|
|
50
76
|
const installed = installedById.get(extension.manifest.id);
|
|
51
77
|
const isBuiltin = extension.manifest.id.startsWith('workbench-kit.builtin.');
|
|
78
|
+
const isLive = liveExtensionIds.has(extension.manifest.id);
|
|
52
79
|
const featureState = resolveExtensionManagementFeatureState(
|
|
53
80
|
extension.manifest.id,
|
|
54
81
|
extensionFeatures,
|
|
55
82
|
);
|
|
83
|
+
const uninstallEligibility =
|
|
84
|
+
installed && !isBuiltin
|
|
85
|
+
? uninstallEvaluation.getEligibility(extension.manifest.id)
|
|
86
|
+
: undefined;
|
|
87
|
+
const actionConstraint =
|
|
88
|
+
uninstallActionConstraint?.extensionId === extension.manifest.id
|
|
89
|
+
? uninstallActionConstraint.eligibility
|
|
90
|
+
: undefined;
|
|
56
91
|
|
|
57
92
|
return {
|
|
58
|
-
|
|
93
|
+
...(uninstallEligibility?.kind === 'eligible' && !actionConstraint
|
|
94
|
+
? { canUninstall: true }
|
|
95
|
+
: {}),
|
|
96
|
+
category: installed?.category ?? (isBuiltin ? 'builtin' : isLive ? 'installed' : 'sample'),
|
|
59
97
|
description: extension.manifest.displayName,
|
|
60
|
-
diagnostics:
|
|
98
|
+
diagnostics: mergeUninstallDiagnostics(
|
|
99
|
+
featureState.diagnostics,
|
|
100
|
+
uninstallEligibility,
|
|
101
|
+
actionConstraint,
|
|
102
|
+
),
|
|
61
103
|
displayName: extension.manifest.displayName,
|
|
62
104
|
enabled: isBuiltin ? true : (installed?.enabled ?? false),
|
|
63
105
|
features: featureState.features,
|
|
@@ -65,53 +107,77 @@ export function createExtensionManagementEntries({
|
|
|
65
107
|
installedAt: installed?.installedAt,
|
|
66
108
|
manifestUrl: installed?.manifestUrl,
|
|
67
109
|
source: isBuiltin ? ('bundled' as const) : ('installed' as const),
|
|
110
|
+
...(transition?.extensionId === extension.manifest.id
|
|
111
|
+
? { transition: { kind: transition.kind, message: transition.message } }
|
|
112
|
+
: {}),
|
|
68
113
|
} satisfies ExtensionManagementEntry;
|
|
69
114
|
})
|
|
70
|
-
.filter((entry) => entry.source === 'bundled' || installedById.has(entry.id));
|
|
71
|
-
|
|
72
|
-
const activeExtensions = extensionRegistry
|
|
73
|
-
.getExtensions()
|
|
74
115
|
.filter(
|
|
75
|
-
(
|
|
76
|
-
|
|
116
|
+
(entry) =>
|
|
117
|
+
entry.source === 'bundled' || installedById.has(entry.id) || liveExtensionIds.has(entry.id),
|
|
77
118
|
)
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
const featureState = resolveExtensionManagementFeatureState(
|
|
81
|
-
extension.manifest.id,
|
|
82
|
-
extensionFeatures,
|
|
83
|
-
);
|
|
119
|
+
.sort((left, right) => left.displayName.localeCompare(right.displayName));
|
|
120
|
+
}
|
|
84
121
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
122
|
+
function mergeUninstallDiagnostics(
|
|
123
|
+
diagnostics: readonly ExtensionManagementDiagnosticSummary[] | undefined,
|
|
124
|
+
eligibility: ExtensionUninstallEligibility | undefined,
|
|
125
|
+
actionConstraint?: ExtensionUninstallActionConstraint['eligibility'] | undefined,
|
|
126
|
+
): readonly ExtensionManagementDiagnosticSummary[] | undefined {
|
|
127
|
+
const merged = [
|
|
128
|
+
...(diagnostics ?? []),
|
|
129
|
+
...toUninstallDiagnostics(eligibility),
|
|
130
|
+
...toUninstallDiagnostics(actionConstraint),
|
|
131
|
+
];
|
|
132
|
+
const byMessage = new Map(merged.map((diagnostic) => [diagnostic.message, diagnostic]));
|
|
133
|
+
return byMessage.size > 0 ? [...byMessage.values()] : undefined;
|
|
134
|
+
}
|
|
98
135
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
function toUninstallDiagnostics(
|
|
137
|
+
eligibility: ExtensionUninstallEligibility | undefined,
|
|
138
|
+
): readonly ExtensionManagementDiagnosticSummary[] {
|
|
139
|
+
if (!eligibility || eligibility.kind === 'eligible') {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
if (eligibility.kind === 'ineligibleTarget') {
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
message:
|
|
146
|
+
eligibility.reason === 'builtin'
|
|
147
|
+
? `Cannot uninstall built-in extensions: ${eligibility.diagnosticExtensionIds.join(', ')}.`
|
|
148
|
+
: `Cannot uninstall because these persisted targets are no longer installed: ${eligibility.diagnosticExtensionIds.join(', ')}.`,
|
|
149
|
+
severity: 'error',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result: ExtensionManagementDiagnosticSummary[] = [];
|
|
155
|
+
if (eligibility.dependentExtensionIds.length > 0) {
|
|
156
|
+
result.push({
|
|
157
|
+
message: `Cannot uninstall because these installed extensions depend on it: ${eligibility.dependentExtensionIds.join(', ')}.`,
|
|
158
|
+
severity: 'error',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (eligibility.unresolvedExtensionIds.length > 0) {
|
|
162
|
+
result.push({
|
|
163
|
+
message: `Cannot verify uninstall safety because these extension manifests are unavailable or ambiguous: ${eligibility.unresolvedExtensionIds.join(', ')}.`,
|
|
164
|
+
severity: 'error',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
102
168
|
}
|
|
103
169
|
|
|
104
170
|
export function createExtensionCatalogBrowseEntries({
|
|
105
171
|
availableExtensions,
|
|
106
172
|
catalogEntries,
|
|
107
|
-
|
|
173
|
+
extensionCatalog,
|
|
108
174
|
installedRecords,
|
|
109
175
|
}: CreateExtensionCatalogBrowseEntriesInput): readonly ExtensionCatalogBrowseEntry[] {
|
|
110
176
|
const installedIds = new Set(installedRecords.map((record) => record.id));
|
|
111
177
|
const installContext = createExtensionInstallPlanningContext({
|
|
112
178
|
availableExtensions,
|
|
113
179
|
catalogEntries,
|
|
114
|
-
|
|
180
|
+
extensionCatalog,
|
|
115
181
|
installedRecords,
|
|
116
182
|
});
|
|
117
183
|
|
|
@@ -139,21 +205,19 @@ export function createExtensionCatalogBrowseEntries({
|
|
|
139
205
|
export function createExtensionInstallPlanningContext({
|
|
140
206
|
availableExtensions,
|
|
141
207
|
catalogEntries,
|
|
142
|
-
|
|
208
|
+
extensionCatalog,
|
|
143
209
|
installedRecords,
|
|
144
210
|
}: CreateExtensionCatalogBrowseEntriesInput): ExtensionInstallPlanningContext {
|
|
145
211
|
const installableExtensions = mergeUniqueExtensionDescriptions([
|
|
146
212
|
...availableExtensions,
|
|
147
|
-
...
|
|
213
|
+
...extensionCatalog.getExtensions(),
|
|
148
214
|
]);
|
|
149
215
|
|
|
150
216
|
return {
|
|
151
217
|
availableExtensions: installableExtensions,
|
|
152
|
-
enabledExtensionIds:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
extensionRegistry,
|
|
156
|
-
hostCapabilityIds: extensionRegistry.capabilityRegistry.listProviderIds(),
|
|
218
|
+
enabledExtensionIds: extensionCatalog.getExtensions().map((extension) => extension.manifest.id),
|
|
219
|
+
extensionCatalog,
|
|
220
|
+
hostCapabilityIds: extensionCatalog.listCapabilityProviderIds(),
|
|
157
221
|
installSources: createExtensionInstallSources(catalogEntries, installableExtensions),
|
|
158
222
|
installedRecords,
|
|
159
223
|
};
|
|
@@ -220,7 +284,7 @@ function mergeUniqueExtensionDescriptions(
|
|
|
220
284
|
|
|
221
285
|
function createExtensionManagementFeatureMaps(
|
|
222
286
|
availableExtensions: readonly WorkbenchExtensionDescription[],
|
|
223
|
-
|
|
287
|
+
extensionCatalog: WorkbenchExtensionCatalogReader,
|
|
224
288
|
) {
|
|
225
289
|
return {
|
|
226
290
|
bundledFeaturesById: new Map(
|
|
@@ -230,7 +294,7 @@ function createExtensionManagementFeatureMaps(
|
|
|
230
294
|
]),
|
|
231
295
|
),
|
|
232
296
|
inspectionsById: new Map(
|
|
233
|
-
|
|
297
|
+
extensionCatalog
|
|
234
298
|
.getFeatureInspections()
|
|
235
299
|
.map((inspection) => [inspection.feature.id, inspection]),
|
|
236
300
|
),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DARK_THEME_PRESET_OPTIONS,
|
|
3
|
+
LIGHT_THEME_PRESET_OPTIONS,
|
|
4
|
+
WORKBENCH_COLOR_SCHEME_OPTIONS,
|
|
5
|
+
} from '@workbench-kit/react/workbench';
|
|
6
|
+
import type { ThemeRegistry } from '@workbench-kit/workbench-core';
|
|
7
|
+
|
|
8
|
+
interface ThemeOptionLike {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ThemeSelectionProtectionSnapshot =
|
|
13
|
+
| {
|
|
14
|
+
readonly kind: 'known';
|
|
15
|
+
readonly protectedThemeIds: readonly string[];
|
|
16
|
+
readonly themeRegistryRevision: number;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
readonly kind: 'unknown';
|
|
20
|
+
readonly themeRegistryRevision: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface ThemeSelectionProtectionInput {
|
|
24
|
+
readonly darkPreset: string | undefined;
|
|
25
|
+
readonly lightPreset: string | undefined;
|
|
26
|
+
readonly theme: string | undefined;
|
|
27
|
+
readonly themeOptions: readonly ThemeOptionLike[] | undefined;
|
|
28
|
+
readonly themes: ThemeRegistry;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Captures only selections that resolve to exactly one current option source.
|
|
33
|
+
* Unknown values deliberately do not resolve to the first rendered option: the
|
|
34
|
+
* soft lifecycle must fail closed instead of guessing which fallback is active.
|
|
35
|
+
*/
|
|
36
|
+
export function createThemeSelectionProtectionSnapshot({
|
|
37
|
+
darkPreset,
|
|
38
|
+
lightPreset,
|
|
39
|
+
theme,
|
|
40
|
+
themeOptions,
|
|
41
|
+
themes,
|
|
42
|
+
}: ThemeSelectionProtectionInput): ThemeSelectionProtectionSnapshot {
|
|
43
|
+
const themeRegistryRevision = themes.getRevision();
|
|
44
|
+
const contributedThemes = themes.getThemes();
|
|
45
|
+
const hasLightPreset = lightPreset !== undefined;
|
|
46
|
+
const hasDarkPreset = darkPreset !== undefined;
|
|
47
|
+
|
|
48
|
+
if (hasLightPreset !== hasDarkPreset) {
|
|
49
|
+
return { kind: 'unknown', themeRegistryRevision };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (hasLightPreset && hasDarkPreset) {
|
|
53
|
+
const lightOptions = [
|
|
54
|
+
...LIGHT_THEME_PRESET_OPTIONS,
|
|
55
|
+
...contributedThemes.filter((candidate) => candidate.mode === 'light'),
|
|
56
|
+
];
|
|
57
|
+
const darkOptions = [
|
|
58
|
+
...DARK_THEME_PRESET_OPTIONS,
|
|
59
|
+
...contributedThemes.filter((candidate) => candidate.mode === 'dark'),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
!hasExactlyOneOption(theme, WORKBENCH_COLOR_SCHEME_OPTIONS) ||
|
|
64
|
+
!hasExactlyOneOption(lightPreset, lightOptions) ||
|
|
65
|
+
!hasExactlyOneOption(darkPreset, darkOptions)
|
|
66
|
+
) {
|
|
67
|
+
return { kind: 'unknown', themeRegistryRevision };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
kind: 'known',
|
|
72
|
+
protectedThemeIds: [theme, lightPreset, darkPreset],
|
|
73
|
+
themeRegistryRevision,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const themeSources = [...(themeOptions ?? []), ...contributedThemes];
|
|
78
|
+
if (!hasExactlyOneOption(theme, themeSources)) {
|
|
79
|
+
return { kind: 'unknown', themeRegistryRevision };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
kind: 'known',
|
|
84
|
+
protectedThemeIds: [theme],
|
|
85
|
+
themeRegistryRevision,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasExactlyOneOption(
|
|
90
|
+
selection: string | undefined,
|
|
91
|
+
options: readonly ThemeOptionLike[],
|
|
92
|
+
): selection is string {
|
|
93
|
+
if (!selection) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return options.filter((option) => option.id === selection).length === 1;
|
|
98
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { InstalledExtensionRecord } from '@workbench-kit/workbench-core';
|
|
2
|
+
|
|
3
|
+
import type { CanonicalExtensionDescriptionSnapshot } from './canonical-extension-descriptions.js';
|
|
4
|
+
|
|
5
|
+
const BUILTIN_EXTENSION_ID_PREFIX = 'workbench-kit.builtin.' as const;
|
|
6
|
+
|
|
7
|
+
export type ExtensionUninstallEligibility =
|
|
8
|
+
| {
|
|
9
|
+
readonly kind: 'eligible';
|
|
10
|
+
}
|
|
11
|
+
| {
|
|
12
|
+
readonly diagnosticExtensionIds: readonly string[];
|
|
13
|
+
readonly kind: 'ineligibleTarget';
|
|
14
|
+
readonly reason: 'builtin' | 'notInstalled';
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
readonly dependentExtensionIds: readonly string[];
|
|
18
|
+
readonly kind: 'blocked';
|
|
19
|
+
readonly unresolvedExtensionIds: readonly string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface ExtensionUninstallEvaluation {
|
|
23
|
+
getEligibility(extensionId: string): ExtensionUninstallEligibility;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createExtensionUninstallEvaluation({
|
|
27
|
+
canonicalDescriptions,
|
|
28
|
+
installedRecords,
|
|
29
|
+
}: {
|
|
30
|
+
readonly canonicalDescriptions: CanonicalExtensionDescriptionSnapshot;
|
|
31
|
+
readonly installedRecords: readonly InstalledExtensionRecord[];
|
|
32
|
+
}): ExtensionUninstallEvaluation {
|
|
33
|
+
const installedRecordsById = new Map<string, InstalledExtensionRecord>();
|
|
34
|
+
const duplicateInstalledExtensionIds = new Set<string>();
|
|
35
|
+
for (const record of installedRecords) {
|
|
36
|
+
if (installedRecordsById.has(record.id)) {
|
|
37
|
+
duplicateInstalledExtensionIds.add(record.id);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
installedRecordsById.set(record.id, record);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dependentsByTargetId = new Map<string, Set<string>>();
|
|
44
|
+
const unresolvedExtensionIdSet = new Set<string>();
|
|
45
|
+
|
|
46
|
+
for (const record of installedRecordsById.values()) {
|
|
47
|
+
const description = canonicalDescriptions.getDescription(record.id);
|
|
48
|
+
if (!description || duplicateInstalledExtensionIds.has(record.id)) {
|
|
49
|
+
unresolvedExtensionIdSet.add(record.id);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const dependencyId of description.manifest.extensionDependencies ?? []) {
|
|
54
|
+
if (dependencyId === record.id || !installedRecordsById.has(dependencyId)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const dependents = dependentsByTargetId.get(dependencyId) ?? new Set<string>();
|
|
58
|
+
dependents.add(record.id);
|
|
59
|
+
dependentsByTargetId.set(dependencyId, dependents);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const unresolvedExtensionIds = [...unresolvedExtensionIdSet].sort((left, right) =>
|
|
64
|
+
left.localeCompare(right),
|
|
65
|
+
);
|
|
66
|
+
const eligibilityByExtensionId = new Map<string, ExtensionUninstallEligibility>();
|
|
67
|
+
for (const extensionId of installedRecordsById.keys()) {
|
|
68
|
+
if (extensionId.startsWith(BUILTIN_EXTENSION_ID_PREFIX)) {
|
|
69
|
+
eligibilityByExtensionId.set(extensionId, {
|
|
70
|
+
diagnosticExtensionIds: [extensionId],
|
|
71
|
+
kind: 'ineligibleTarget',
|
|
72
|
+
reason: 'builtin',
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dependentExtensionIds = [...(dependentsByTargetId.get(extensionId) ?? [])].sort(
|
|
78
|
+
(left, right) => left.localeCompare(right),
|
|
79
|
+
);
|
|
80
|
+
eligibilityByExtensionId.set(
|
|
81
|
+
extensionId,
|
|
82
|
+
dependentExtensionIds.length === 0 && unresolvedExtensionIds.length === 0
|
|
83
|
+
? { kind: 'eligible' }
|
|
84
|
+
: {
|
|
85
|
+
dependentExtensionIds,
|
|
86
|
+
kind: 'blocked',
|
|
87
|
+
unresolvedExtensionIds,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
getEligibility(extensionId) {
|
|
94
|
+
return (
|
|
95
|
+
eligibilityByExtensionId.get(extensionId) ?? {
|
|
96
|
+
diagnosticExtensionIds: [extensionId],
|
|
97
|
+
kind: 'ineligibleTarget',
|
|
98
|
+
reason: 'notInstalled',
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|