@workbench-kit/workbench-config 0.0.2-prototype.0.1.3 → 0.0.2-prototype.0.2.2
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/package.json +3 -3
- package/src/index.ts +109 -11
- package/src/keybindings-config.ts +100 -0
- package/src/preference-scopes.ts +65 -0
- package/src/settings-config.ts +23 -0
- package/src/user-commands-config.ts +154 -0
- package/src/validation-error.ts +6 -0
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workbench-kit/workbench-config",
|
|
3
|
-
"version": "0.0.2-prototype.0.
|
|
3
|
+
"version": "0.0.2-prototype.0.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@workbench-kit/base": "0.0.2-prototype.0.
|
|
11
|
-
"@workbench-kit/platform": "0.0.2-prototype.0.
|
|
10
|
+
"@workbench-kit/base": "0.0.2-prototype.0.2.2",
|
|
11
|
+
"@workbench-kit/platform": "0.0.2-prototype.0.2.2"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { WorkbenchConfigValidationError } from './validation-error.js';
|
|
2
|
+
|
|
1
3
|
export const WORKBENCH_KIT_WORKBENCH_CONFIG_VERSION = '0.0.0' as const;
|
|
2
4
|
|
|
3
5
|
export const WORKBENCH_CONFIG_DIR = '.workbench' as const;
|
|
@@ -6,6 +8,7 @@ export type WorkbenchConfigFileName =
|
|
|
6
8
|
| 'workspace.json'
|
|
7
9
|
| 'settings.json'
|
|
8
10
|
| 'keybindings.json'
|
|
11
|
+
| 'user-commands.json'
|
|
9
12
|
| 'extensions.json'
|
|
10
13
|
| 'extensions.lock.json'
|
|
11
14
|
| 'layout.default.json'
|
|
@@ -18,6 +21,11 @@ export interface WorkbenchExtensionsConfig {
|
|
|
18
21
|
|
|
19
22
|
export interface WorkbenchLayoutConfig {
|
|
20
23
|
readonly activityBar: {
|
|
24
|
+
readonly hiddenItemIds?: readonly string[];
|
|
25
|
+
readonly itemOrder?: readonly string[];
|
|
26
|
+
readonly visible: boolean;
|
|
27
|
+
};
|
|
28
|
+
readonly auxiliaryBar: {
|
|
21
29
|
readonly visible: boolean;
|
|
22
30
|
};
|
|
23
31
|
readonly panel: {
|
|
@@ -25,12 +33,14 @@ export interface WorkbenchLayoutConfig {
|
|
|
25
33
|
};
|
|
26
34
|
readonly sideBar: {
|
|
27
35
|
readonly activeViewContainer?: string;
|
|
36
|
+
readonly sizePercent?: number;
|
|
28
37
|
readonly visible: boolean;
|
|
29
38
|
};
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export type WorkbenchLayoutConfigInput = Partial<{
|
|
33
42
|
activityBar: Partial<WorkbenchLayoutConfig['activityBar']>;
|
|
43
|
+
auxiliaryBar: Partial<WorkbenchLayoutConfig['auxiliaryBar']>;
|
|
34
44
|
panel: Partial<WorkbenchLayoutConfig['panel']>;
|
|
35
45
|
sideBar: Partial<WorkbenchLayoutConfig['sideBar']>;
|
|
36
46
|
}>;
|
|
@@ -39,6 +49,9 @@ export const DEFAULT_WORKBENCH_LAYOUT_CONFIG: WorkbenchLayoutConfig = {
|
|
|
39
49
|
activityBar: {
|
|
40
50
|
visible: true,
|
|
41
51
|
},
|
|
52
|
+
auxiliaryBar: {
|
|
53
|
+
visible: false,
|
|
54
|
+
},
|
|
42
55
|
panel: {
|
|
43
56
|
visible: false,
|
|
44
57
|
},
|
|
@@ -47,19 +60,14 @@ export const DEFAULT_WORKBENCH_LAYOUT_CONFIG: WorkbenchLayoutConfig = {
|
|
|
47
60
|
},
|
|
48
61
|
};
|
|
49
62
|
|
|
50
|
-
export
|
|
51
|
-
constructor(message: string) {
|
|
52
|
-
super(message);
|
|
53
|
-
this.name = 'WorkbenchConfigValidationError';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
63
|
+
export { WorkbenchConfigValidationError };
|
|
56
64
|
|
|
57
65
|
export function parseWorkbenchExtensionsConfig(input: unknown): WorkbenchExtensionsConfig {
|
|
58
66
|
const record = assertRecord(input, 'extensions config');
|
|
59
67
|
|
|
60
68
|
return {
|
|
61
|
-
enabled:
|
|
62
|
-
recommendations:
|
|
69
|
+
enabled: readOptionalStringArrayFromExtensionsConfig(record, 'enabled'),
|
|
70
|
+
recommendations: readOptionalStringArrayFromExtensionsConfig(record, 'recommendations'),
|
|
63
71
|
};
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -77,29 +85,49 @@ export function parseWorkbenchExtensionsConfigJson(jsonText: string): WorkbenchE
|
|
|
77
85
|
|
|
78
86
|
export function parseWorkbenchLayoutConfig(input: unknown): WorkbenchLayoutConfig {
|
|
79
87
|
const record = assertRecord(input, 'layout config');
|
|
80
|
-
assertKnownKeys(record, ['activityBar', 'panel', 'sideBar'], 'layout config');
|
|
88
|
+
assertKnownKeys(record, ['activityBar', 'auxiliaryBar', 'panel', 'sideBar'], 'layout config');
|
|
81
89
|
|
|
82
90
|
const activityBar = readOptionalRecord(record, 'activityBar');
|
|
91
|
+
const auxiliaryBar = readOptionalRecord(record, 'auxiliaryBar');
|
|
83
92
|
const panel = readOptionalRecord(record, 'panel');
|
|
84
93
|
const sideBar = readOptionalRecord(record, 'sideBar');
|
|
85
94
|
|
|
86
|
-
assertKnownKeys(
|
|
95
|
+
assertKnownKeys(
|
|
96
|
+
activityBar,
|
|
97
|
+
['hiddenItemIds', 'itemOrder', 'visible'],
|
|
98
|
+
'layout config activityBar',
|
|
99
|
+
);
|
|
100
|
+
assertKnownKeys(auxiliaryBar, ['visible'], 'layout config auxiliaryBar');
|
|
87
101
|
assertKnownKeys(panel, ['visible'], 'layout config panel');
|
|
88
|
-
assertKnownKeys(
|
|
102
|
+
assertKnownKeys(
|
|
103
|
+
sideBar,
|
|
104
|
+
['activeViewContainer', 'sizePercent', 'visible'],
|
|
105
|
+
'layout config sideBar',
|
|
106
|
+
);
|
|
89
107
|
|
|
90
108
|
return {
|
|
91
109
|
activityBar: {
|
|
110
|
+
hiddenItemIds: readOptionalStringArray(activityBar, 'hiddenItemIds'),
|
|
111
|
+
itemOrder: readOptionalStringArray(activityBar, 'itemOrder'),
|
|
92
112
|
visible: readOptionalBoolean(
|
|
93
113
|
activityBar,
|
|
94
114
|
'visible',
|
|
95
115
|
DEFAULT_WORKBENCH_LAYOUT_CONFIG.activityBar.visible,
|
|
96
116
|
),
|
|
97
117
|
},
|
|
118
|
+
auxiliaryBar: {
|
|
119
|
+
visible: readOptionalBoolean(
|
|
120
|
+
auxiliaryBar,
|
|
121
|
+
'visible',
|
|
122
|
+
DEFAULT_WORKBENCH_LAYOUT_CONFIG.auxiliaryBar.visible,
|
|
123
|
+
),
|
|
124
|
+
},
|
|
98
125
|
panel: {
|
|
99
126
|
visible: readOptionalBoolean(panel, 'visible', DEFAULT_WORKBENCH_LAYOUT_CONFIG.panel.visible),
|
|
100
127
|
},
|
|
101
128
|
sideBar: {
|
|
102
129
|
...readOptionalLayoutId(sideBar, 'activeViewContainer'),
|
|
130
|
+
...readOptionalSizePercent(sideBar, 'sizePercent'),
|
|
103
131
|
visible: readOptionalBoolean(
|
|
104
132
|
sideBar,
|
|
105
133
|
'visible',
|
|
@@ -121,6 +149,38 @@ export function parseWorkbenchLayoutConfigJson(jsonText: string): WorkbenchLayou
|
|
|
121
149
|
}
|
|
122
150
|
}
|
|
123
151
|
|
|
152
|
+
export {
|
|
153
|
+
parseWorkbenchKeybindingsConfig,
|
|
154
|
+
parseWorkbenchKeybindingsConfigJson,
|
|
155
|
+
type WorkbenchKeybindingDefinition,
|
|
156
|
+
} from './keybindings-config.js';
|
|
157
|
+
export {
|
|
158
|
+
parseWorkbenchSettingsConfig,
|
|
159
|
+
parseWorkbenchSettingsConfigJson,
|
|
160
|
+
type WorkbenchSettingsConfig,
|
|
161
|
+
} from './settings-config.js';
|
|
162
|
+
export {
|
|
163
|
+
createEmptyPreferenceValuesByScope,
|
|
164
|
+
FUTURE_PREFERENCE_SCOPES,
|
|
165
|
+
isPreferenceScope,
|
|
166
|
+
mergePreferenceValuesByScope,
|
|
167
|
+
mergeScopedPreferences,
|
|
168
|
+
PREFERENCE_SCOPE_MERGE_ORDER,
|
|
169
|
+
type FuturePreferenceScope,
|
|
170
|
+
type PreferenceScope,
|
|
171
|
+
type PreferenceValuesByScope,
|
|
172
|
+
type ScopedPreferenceLayer,
|
|
173
|
+
} from './preference-scopes.js';
|
|
174
|
+
export {
|
|
175
|
+
parseWorkbenchUserCommandsConfig,
|
|
176
|
+
parseWorkbenchUserCommandsConfigJson,
|
|
177
|
+
type WorkbenchUserCommandAction,
|
|
178
|
+
type WorkbenchUserCommandDefinition,
|
|
179
|
+
type WorkbenchUserCommandExecuteAction,
|
|
180
|
+
type WorkbenchUserCommandSequenceAction,
|
|
181
|
+
type WorkbenchUserCommandsConfig,
|
|
182
|
+
} from './user-commands-config.js';
|
|
183
|
+
|
|
124
184
|
function assertRecord(value: unknown, label: string): Record<string, unknown> {
|
|
125
185
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
126
186
|
throw new WorkbenchConfigValidationError(`Expected ${label} to be an object.`);
|
|
@@ -143,6 +203,22 @@ function readOptionalRecord(record: Record<string, unknown>, key: string): Recor
|
|
|
143
203
|
}
|
|
144
204
|
|
|
145
205
|
function readOptionalStringArray(
|
|
206
|
+
record: Record<string, unknown>,
|
|
207
|
+
key: string,
|
|
208
|
+
): readonly string[] | undefined {
|
|
209
|
+
const value = record[key];
|
|
210
|
+
if (value === undefined) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
215
|
+
throw new WorkbenchConfigValidationError(`Expected "${key}" to be an array of strings.`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readOptionalStringArrayFromExtensionsConfig(
|
|
146
222
|
record: Record<string, unknown>,
|
|
147
223
|
key: keyof WorkbenchExtensionsConfig,
|
|
148
224
|
): readonly string[] {
|
|
@@ -193,6 +269,28 @@ function readOptionalLayoutId(
|
|
|
193
269
|
};
|
|
194
270
|
}
|
|
195
271
|
|
|
272
|
+
function readOptionalSizePercent(
|
|
273
|
+
record: Record<string, unknown>,
|
|
274
|
+
key: string,
|
|
275
|
+
): { readonly sizePercent?: number } {
|
|
276
|
+
const value = record[key];
|
|
277
|
+
if (value === undefined) {
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
282
|
+
throw new WorkbenchConfigValidationError(`Expected "${key}" to be a finite number.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
sizePercent: clampLayoutSizePercent(value),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function clampLayoutSizePercent(value: number): number {
|
|
291
|
+
return Math.min(90, Math.max(10, value));
|
|
292
|
+
}
|
|
293
|
+
|
|
196
294
|
function assertKnownKeys(record: Record<string, unknown>, keys: readonly string[], label: string) {
|
|
197
295
|
const knownKeys = new Set(keys);
|
|
198
296
|
const unknownKeys = Object.keys(record).filter((key) => !knownKeys.has(key));
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { WorkbenchConfigValidationError } from './validation-error.js';
|
|
2
|
+
|
|
3
|
+
export interface WorkbenchKeybindingDefinition {
|
|
4
|
+
readonly args?: readonly unknown[];
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly when?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseWorkbenchKeybindingsConfig(
|
|
11
|
+
input: unknown,
|
|
12
|
+
): readonly WorkbenchKeybindingDefinition[] {
|
|
13
|
+
if (!Array.isArray(input)) {
|
|
14
|
+
throw new WorkbenchConfigValidationError('Expected keybindings config to be an array.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return input.map((entry, index) => parseWorkbenchKeybindingDefinition(entry, index));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseWorkbenchKeybindingsConfigJson(
|
|
21
|
+
jsonText: string,
|
|
22
|
+
): readonly WorkbenchKeybindingDefinition[] {
|
|
23
|
+
try {
|
|
24
|
+
return parseWorkbenchKeybindingsConfig(JSON.parse(jsonText) as unknown);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof WorkbenchConfigValidationError) {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new WorkbenchConfigValidationError('Expected keybindings config to be valid JSON.');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseWorkbenchKeybindingDefinition(
|
|
35
|
+
input: unknown,
|
|
36
|
+
index: number,
|
|
37
|
+
): WorkbenchKeybindingDefinition {
|
|
38
|
+
const record = assertRecord(input, `keybindings[${index}]`);
|
|
39
|
+
assertKnownKeys(record, ['args', 'command', 'key', 'when'], `keybindings[${index}]`);
|
|
40
|
+
|
|
41
|
+
const args = readOptionalArgs(record);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...(args ? { args } : {}),
|
|
45
|
+
command: readRequiredString(record, 'command'),
|
|
46
|
+
key: readRequiredString(record, 'key'),
|
|
47
|
+
...(readOptionalString(record, 'when') ? { when: readOptionalString(record, 'when') } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertRecord(value: unknown, label: string): Record<string, unknown> {
|
|
52
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
53
|
+
throw new WorkbenchConfigValidationError(`Expected ${label} to be an object.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return value as Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readRequiredString(record: Record<string, unknown>, key: string): string {
|
|
60
|
+
const value = record[key];
|
|
61
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
62
|
+
throw new WorkbenchConfigValidationError(`Expected "${key}" to be a non-empty string.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return value.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readOptionalString(record: Record<string, unknown>, key: string): string | undefined {
|
|
69
|
+
const value = record[key];
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
75
|
+
throw new WorkbenchConfigValidationError(`Expected "${key}" to be a non-empty string.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return value.trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readOptionalArgs(record: Record<string, unknown>): readonly unknown[] | undefined {
|
|
82
|
+
const value = record.args;
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!Array.isArray(value)) {
|
|
88
|
+
throw new WorkbenchConfigValidationError('Expected "args" to be an array.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...value];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertKnownKeys(record: Record<string, unknown>, keys: readonly string[], label: string) {
|
|
95
|
+
const knownKeys = new Set(keys);
|
|
96
|
+
const unknownKeys = Object.keys(record).filter((key) => !knownKeys.has(key));
|
|
97
|
+
if (unknownKeys.length > 0) {
|
|
98
|
+
throw new WorkbenchConfigValidationError(`Unexpected ${label} field "${unknownKeys[0]}".`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { WorkbenchSettingsConfig } from './settings-config.js';
|
|
2
|
+
|
|
3
|
+
/** Runtime preference scopes supported in v1. */
|
|
4
|
+
export type PreferenceScope = 'default' | 'workspace' | 'local';
|
|
5
|
+
|
|
6
|
+
/** Documented future scopes — not merged or persisted yet. */
|
|
7
|
+
export const FUTURE_PREFERENCE_SCOPES = ['user', 'resource', 'secret'] as const;
|
|
8
|
+
|
|
9
|
+
export type FuturePreferenceScope = (typeof FUTURE_PREFERENCE_SCOPES)[number];
|
|
10
|
+
|
|
11
|
+
/** Lower index = lower precedence when merging effective values. */
|
|
12
|
+
export const PREFERENCE_SCOPE_MERGE_ORDER: readonly PreferenceScope[] = [
|
|
13
|
+
'default',
|
|
14
|
+
'workspace',
|
|
15
|
+
'local',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export type PreferenceValuesByScope = Partial<Record<PreferenceScope, WorkbenchSettingsConfig>>;
|
|
19
|
+
|
|
20
|
+
export interface ScopedPreferenceLayer {
|
|
21
|
+
readonly scope: PreferenceScope;
|
|
22
|
+
readonly values: WorkbenchSettingsConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isPreferenceScope(value: unknown): value is PreferenceScope {
|
|
26
|
+
return value === 'default' || value === 'workspace' || value === 'local';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function mergeScopedPreferences(
|
|
30
|
+
layers: readonly ScopedPreferenceLayer[],
|
|
31
|
+
): WorkbenchSettingsConfig {
|
|
32
|
+
const merged: Record<string, unknown> = {};
|
|
33
|
+
|
|
34
|
+
for (const scope of PREFERENCE_SCOPE_MERGE_ORDER) {
|
|
35
|
+
const layer = layers.find((candidate) => candidate.scope === scope);
|
|
36
|
+
if (!layer) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(layer.values)) {
|
|
41
|
+
merged[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function mergePreferenceValuesByScope(
|
|
49
|
+
valuesByScope: PreferenceValuesByScope,
|
|
50
|
+
): WorkbenchSettingsConfig {
|
|
51
|
+
return mergeScopedPreferences(
|
|
52
|
+
PREFERENCE_SCOPE_MERGE_ORDER.flatMap((scope) => {
|
|
53
|
+
const values = valuesByScope[scope];
|
|
54
|
+
return values ? [{ scope, values }] : [];
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createEmptyPreferenceValuesByScope(): PreferenceValuesByScope {
|
|
60
|
+
return {
|
|
61
|
+
default: {},
|
|
62
|
+
local: {},
|
|
63
|
+
workspace: {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { WorkbenchConfigValidationError } from './validation-error.js';
|
|
2
|
+
|
|
3
|
+
export type WorkbenchSettingsConfig = Readonly<Record<string, unknown>>;
|
|
4
|
+
|
|
5
|
+
export function parseWorkbenchSettingsConfig(input: unknown): WorkbenchSettingsConfig {
|
|
6
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
7
|
+
throw new WorkbenchConfigValidationError('Expected settings config to be an object.');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return { ...(input as Record<string, unknown>) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseWorkbenchSettingsConfigJson(jsonText: string): WorkbenchSettingsConfig {
|
|
14
|
+
try {
|
|
15
|
+
return parseWorkbenchSettingsConfig(JSON.parse(jsonText) as unknown);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof WorkbenchConfigValidationError) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new WorkbenchConfigValidationError('Expected settings config to be valid JSON.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { WorkbenchConfigValidationError } from './validation-error.js';
|
|
2
|
+
|
|
3
|
+
export interface WorkbenchUserCommandExecuteAction {
|
|
4
|
+
readonly args?: unknown;
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly type: 'executeCommand';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WorkbenchUserCommandSequenceAction {
|
|
10
|
+
readonly steps: readonly WorkbenchUserCommandAction[];
|
|
11
|
+
readonly type: 'sequence';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type WorkbenchUserCommandAction =
|
|
15
|
+
WorkbenchUserCommandExecuteAction | WorkbenchUserCommandSequenceAction;
|
|
16
|
+
|
|
17
|
+
export interface WorkbenchUserCommandDefinition {
|
|
18
|
+
readonly action: WorkbenchUserCommandAction;
|
|
19
|
+
readonly category?: string | undefined;
|
|
20
|
+
readonly command: string;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorkbenchUserCommandsConfig {
|
|
25
|
+
readonly commands: readonly WorkbenchUserCommandDefinition[];
|
|
26
|
+
readonly version?: number | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseWorkbenchUserCommandsConfig(input: unknown): WorkbenchUserCommandsConfig {
|
|
30
|
+
const record = assertRecord(input, 'user commands config');
|
|
31
|
+
assertKnownKeys(record, ['commands', 'version'], 'user commands config');
|
|
32
|
+
|
|
33
|
+
const commands = record.commands;
|
|
34
|
+
if (!Array.isArray(commands)) {
|
|
35
|
+
throw new WorkbenchConfigValidationError('Expected "commands" to be an array.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
commands: commands.map((entry, index) => parseUserCommandDefinition(entry, index)),
|
|
40
|
+
...(readOptionalNumber(record, 'version') !== undefined
|
|
41
|
+
? { version: readOptionalNumber(record, 'version') }
|
|
42
|
+
: {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseWorkbenchUserCommandsConfigJson(
|
|
47
|
+
jsonText: string,
|
|
48
|
+
): WorkbenchUserCommandsConfig {
|
|
49
|
+
try {
|
|
50
|
+
return parseWorkbenchUserCommandsConfig(JSON.parse(jsonText) as unknown);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof WorkbenchConfigValidationError) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new WorkbenchConfigValidationError('Expected user commands config to be valid JSON.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseUserCommandDefinition(input: unknown, index: number): WorkbenchUserCommandDefinition {
|
|
61
|
+
const record = assertRecord(input, `user commands[${index}]`);
|
|
62
|
+
assertKnownKeys(record, ['action', 'category', 'command', 'title'], `user commands[${index}]`);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
action: parseUserCommandAction(record.action, `user commands[${index}].action`),
|
|
66
|
+
...(readOptionalString(record, 'category')
|
|
67
|
+
? { category: readOptionalString(record, 'category') }
|
|
68
|
+
: {}),
|
|
69
|
+
command: readRequiredString(record, 'command'),
|
|
70
|
+
title: readRequiredString(record, 'title'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseUserCommandAction(input: unknown, label: string): WorkbenchUserCommandAction {
|
|
75
|
+
const record = assertRecord(input, label);
|
|
76
|
+
const type = readRequiredString(record, 'type');
|
|
77
|
+
|
|
78
|
+
if (type === 'executeCommand') {
|
|
79
|
+
assertKnownKeys(record, ['args', 'command', 'type'], label);
|
|
80
|
+
return {
|
|
81
|
+
...(record.args !== undefined ? { args: record.args } : {}),
|
|
82
|
+
command: readRequiredString(record, 'command'),
|
|
83
|
+
type,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (type === 'sequence') {
|
|
88
|
+
assertKnownKeys(record, ['steps', 'type'], label);
|
|
89
|
+
const steps = record.steps;
|
|
90
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
91
|
+
throw new WorkbenchConfigValidationError(
|
|
92
|
+
`Expected "${label}.steps" to be a non-empty array.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
steps: steps.map((step, index) => parseUserCommandAction(step, `${label}.steps[${index}]`)),
|
|
98
|
+
type,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new WorkbenchConfigValidationError(`Unexpected ${label} type "${type}".`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertRecord(value: unknown, label: string): Record<string, unknown> {
|
|
106
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
107
|
+
throw new WorkbenchConfigValidationError(`Expected ${label} to be an object.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return value as Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readRequiredString(record: Record<string, unknown>, key: string): string {
|
|
114
|
+
const value = record[key];
|
|
115
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
116
|
+
throw new WorkbenchConfigValidationError(`Expected "${key}" to be a non-empty string.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value.trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readOptionalString(record: Record<string, unknown>, key: string): string | undefined {
|
|
123
|
+
const value = record[key];
|
|
124
|
+
if (value === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
129
|
+
throw new WorkbenchConfigValidationError(`Expected "${key}" to be a non-empty string.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return value.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readOptionalNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
136
|
+
const value = record[key];
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
142
|
+
throw new WorkbenchConfigValidationError(`Expected "${key}" to be a finite number.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function assertKnownKeys(record: Record<string, unknown>, keys: readonly string[], label: string) {
|
|
149
|
+
const knownKeys = new Set(keys);
|
|
150
|
+
const unknownKeys = Object.keys(record).filter((key) => !knownKeys.has(key));
|
|
151
|
+
if (unknownKeys.length > 0) {
|
|
152
|
+
throw new WorkbenchConfigValidationError(`Unexpected ${label} field "${unknownKeys[0]}".`);
|
|
153
|
+
}
|
|
154
|
+
}
|