|
| 1 | +import * as v from 'valibot'; |
1 | 2 | import vscode from 'vscode'; |
2 | 3 |
|
3 | 4 | // Centralized configuration types for the extension. |
4 | 5 | // Add new keys here to extend configuration in a type-safe way. |
5 | | -export type ExtensionConfig = { |
| 6 | +const configSchema = v.object({ |
6 | 7 | // Glob patterns that determine which files are considered tests. |
7 | 8 | // Must be an array of strings. |
8 | | - testFileGlobPattern: string[]; |
9 | | - // The path to a package.json file of a Rstest executable. |
10 | | - // Used as a last resort if the extension cannot auto-detect @rstest/core. |
11 | | - rstestPackagePath?: string; |
12 | | -}; |
13 | | - |
14 | | -export const defaultConfig: ExtensionConfig = { |
15 | | - // https://code.visualstudio.com/docs/editor/glob-patterns |
16 | | - testFileGlobPattern: [ |
| 9 | + testFileGlobPattern: v.fallback(v.array(v.string()), [ |
17 | 10 | '**/*.{test,spec}.[jt]s', |
18 | 11 | '**/*.{test,spec}.[cm][jt]s', |
19 | 12 | '**/*.{test,spec}.[jt]sx', |
20 | 13 | '**/*.{test,spec}.[cm][jt]sx', |
21 | | - ], |
22 | | -}; |
| 14 | + ]), |
| 15 | + // The path to a package.json file of a Rstest executable. |
| 16 | + // Used as a last resort if the extension cannot auto-detect @rstest/core. |
| 17 | + rstestPackagePath: v.fallback(v.optional(v.string()), undefined), |
| 18 | + configFileGlobPattern: v.fallback(v.array(v.string()), [ |
| 19 | + '**/rstest.config.{mjs,ts,js,cjs,mts,cts}', |
| 20 | + ]), |
| 21 | +}); |
| 22 | + |
| 23 | +export type ExtensionConfig = v.InferOutput<typeof configSchema>; |
23 | 24 |
|
24 | | -// Type-safe getter for a single config value with priority: |
25 | | -// workspaceFolder > workspace > user (global) > default. |
| 25 | +// Type-safe getter for a single config value |
26 | 26 | export function getConfigValue<K extends keyof ExtensionConfig>( |
27 | 27 | key: K, |
28 | | - folder?: vscode.WorkspaceFolder, |
| 28 | + scope?: vscode.ConfigurationScope | null, |
29 | 29 | ): ExtensionConfig[K] { |
30 | | - const section = vscode.workspace.getConfiguration('rstest', folder); |
31 | | - const inspected = section.inspect<ExtensionConfig[K]>(key); |
32 | | - |
33 | | - // Priority order (highest first): folder, workspace, user, default |
34 | | - const value = |
35 | | - inspected?.workspaceFolderValue ?? |
36 | | - inspected?.workspaceValue ?? |
37 | | - inspected?.globalValue ?? |
38 | | - inspected?.defaultValue ?? |
39 | | - defaultConfig[key]; |
40 | | - |
41 | | - if (key === 'testFileGlobPattern') { |
42 | | - const v = value as unknown; |
43 | | - return (isStringArray(v) ? v : defaultConfig[key]) as ExtensionConfig[K]; |
44 | | - } |
45 | | - |
46 | | - if (key === 'rstestPackagePath') { |
47 | | - const v = value as unknown; |
48 | | - return ( |
49 | | - typeof v === 'string' && v.trim().length > 0 ? v : undefined |
50 | | - ) as ExtensionConfig[K]; |
51 | | - } |
52 | | - |
53 | | - return value as ExtensionConfig[K]; |
| 30 | + const value = vscode.workspace.getConfiguration('rstest', scope).get(key); |
| 31 | + return v.parse(configSchema.entries[key], value) as ExtensionConfig[K]; |
54 | 32 | } |
55 | 33 |
|
56 | | -function isStringArray(v: unknown): v is string[] { |
57 | | - return Array.isArray(v) && v.every((x) => typeof x === 'string'); |
| 34 | +// Convenience to get a full, normalized config object at the given scope. |
| 35 | +export function getConfig( |
| 36 | + scope?: vscode.ConfigurationScope | null, |
| 37 | +): ExtensionConfig { |
| 38 | + return Object.fromEntries( |
| 39 | + Object.keys(configSchema.entries).map((key) => [ |
| 40 | + key, |
| 41 | + getConfigValue(key as keyof ExtensionConfig, scope), |
| 42 | + ]), |
| 43 | + ) as ExtensionConfig; |
58 | 44 | } |
59 | 45 |
|
60 | | -// Convenience to get a full, normalized config object at the given scope. |
61 | | -export function getConfig(folder?: vscode.WorkspaceFolder): ExtensionConfig { |
| 46 | +export function watchConfigValue<K extends keyof ExtensionConfig>( |
| 47 | + key: K, |
| 48 | + scope: vscode.ConfigurationScope | null | undefined, |
| 49 | + listener: ( |
| 50 | + value: ExtensionConfig[K], |
| 51 | + token: vscode.CancellationToken, |
| 52 | + ) => void, |
| 53 | +): vscode.Disposable { |
| 54 | + let cancelSource = new vscode.CancellationTokenSource(); |
| 55 | + listener(getConfigValue(key, scope), cancelSource.token); |
| 56 | + const disposable = vscode.workspace.onDidChangeConfiguration((e) => { |
| 57 | + if (e.affectsConfiguration(`rstest.${key}`, scope ?? undefined)) { |
| 58 | + cancelSource.cancel(); |
| 59 | + cancelSource = new vscode.CancellationTokenSource(); |
| 60 | + listener(getConfigValue(key, scope), cancelSource.token); |
| 61 | + } |
| 62 | + }); |
62 | 63 | return { |
63 | | - testFileGlobPattern: getConfigValue('testFileGlobPattern', folder), |
64 | | - rstestPackagePath: getConfigValue('rstestPackagePath', folder), |
65 | | - } satisfies ExtensionConfig; |
| 64 | + dispose: () => { |
| 65 | + disposable.dispose(); |
| 66 | + cancelSource.cancel(); |
| 67 | + }, |
| 68 | + }; |
66 | 69 | } |
0 commit comments