-
-
Notifications
You must be signed in to change notification settings - Fork 12
feat(vscode): support workspaces and monorepo #688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,66 +1,69 @@ | ||||||||
| import * as v from 'valibot'; | ||||||||
| import vscode from 'vscode'; | ||||||||
|
|
||||||||
| // Centralized configuration types for the extension. | ||||||||
| // Add new keys here to extend configuration in a type-safe way. | ||||||||
| export type ExtensionConfig = { | ||||||||
| const configSchema = v.object({ | ||||||||
| // Glob patterns that determine which files are considered tests. | ||||||||
| // Must be an array of strings. | ||||||||
| testFileGlobPattern: string[]; | ||||||||
| // The path to a package.json file of a Rstest executable. | ||||||||
| // Used as a last resort if the extension cannot auto-detect @rstest/core. | ||||||||
| rstestPackagePath?: string; | ||||||||
| }; | ||||||||
|
|
||||||||
| export const defaultConfig: ExtensionConfig = { | ||||||||
| // https://code.visualstudio.com/docs/editor/glob-patterns | ||||||||
| testFileGlobPattern: [ | ||||||||
| testFileGlobPattern: v.fallback(v.array(v.string()), [ | ||||||||
| '**/*.{test,spec}.[jt]s', | ||||||||
| '**/*.{test,spec}.[cm][jt]s', | ||||||||
| '**/*.{test,spec}.[jt]sx', | ||||||||
| '**/*.{test,spec}.[cm][jt]sx', | ||||||||
| ], | ||||||||
| }; | ||||||||
| ]), | ||||||||
| // The path to a package.json file of a Rstest executable. | ||||||||
| // Used as a last resort if the extension cannot auto-detect @rstest/core. | ||||||||
| rstestPackagePath: v.fallback(v.optional(v.string()), undefined), | ||||||||
| configFileGlobPattern: v.fallback(v.array(v.string()), [ | ||||||||
| '**/rstest.config.{mjs,ts,js,cjs,mts,cts}', | ||||||||
| ]), | ||||||||
| }); | ||||||||
|
|
||||||||
| export type ExtensionConfig = v.InferOutput<typeof configSchema>; | ||||||||
|
|
||||||||
| // Type-safe getter for a single config value with priority: | ||||||||
| // workspaceFolder > workspace > user (global) > default. | ||||||||
| // Type-safe getter for a single config value | ||||||||
| export function getConfigValue<K extends keyof ExtensionConfig>( | ||||||||
| key: K, | ||||||||
| folder?: vscode.WorkspaceFolder, | ||||||||
| scope?: vscode.ConfigurationScope | null, | ||||||||
| ): ExtensionConfig[K] { | ||||||||
| const section = vscode.workspace.getConfiguration('rstest', folder); | ||||||||
| const inspected = section.inspect<ExtensionConfig[K]>(key); | ||||||||
|
|
||||||||
| // Priority order (highest first): folder, workspace, user, default | ||||||||
| const value = | ||||||||
| inspected?.workspaceFolderValue ?? | ||||||||
| inspected?.workspaceValue ?? | ||||||||
| inspected?.globalValue ?? | ||||||||
| inspected?.defaultValue ?? | ||||||||
| defaultConfig[key]; | ||||||||
|
|
||||||||
| if (key === 'testFileGlobPattern') { | ||||||||
| const v = value as unknown; | ||||||||
| return (isStringArray(v) ? v : defaultConfig[key]) as ExtensionConfig[K]; | ||||||||
| } | ||||||||
|
|
||||||||
| if (key === 'rstestPackagePath') { | ||||||||
| const v = value as unknown; | ||||||||
| return ( | ||||||||
| typeof v === 'string' && v.trim().length > 0 ? v : undefined | ||||||||
| ) as ExtensionConfig[K]; | ||||||||
| } | ||||||||
|
|
||||||||
| return value as ExtensionConfig[K]; | ||||||||
| const value = vscode.workspace.getConfiguration('rstest', scope).get(key); | ||||||||
| return v.parse(configSchema.entries[key], value) as ExtensionConfig[K]; | ||||||||
| } | ||||||||
|
|
||||||||
| function isStringArray(v: unknown): v is string[] { | ||||||||
| return Array.isArray(v) && v.every((x) => typeof x === 'string'); | ||||||||
| // Convenience to get a full, normalized config object at the given scope. | ||||||||
| export function getConfig( | ||||||||
| scope?: vscode.ConfigurationScope | null, | ||||||||
| ): ExtensionConfig { | ||||||||
| return Object.fromEntries( | ||||||||
| Object.keys(configSchema.entries).map((key) => [ | ||||||||
| key, | ||||||||
| getConfigValue(key as keyof ExtensionConfig, scope), | ||||||||
| ]), | ||||||||
| ) as ExtensionConfig; | ||||||||
|
Comment on lines
+38
to
+43
|
||||||||
| } | ||||||||
|
|
||||||||
| // Convenience to get a full, normalized config object at the given scope. | ||||||||
| export function getConfig(folder?: vscode.WorkspaceFolder): ExtensionConfig { | ||||||||
| export function watchConfigValue<K extends keyof ExtensionConfig>( | ||||||||
| key: K, | ||||||||
| scope: vscode.ConfigurationScope | null | undefined, | ||||||||
| listener: ( | ||||||||
| value: ExtensionConfig[K], | ||||||||
| token: vscode.CancellationToken, | ||||||||
| ) => void, | ||||||||
| ): vscode.Disposable { | ||||||||
| let cancelSource = new vscode.CancellationTokenSource(); | ||||||||
| listener(getConfigValue(key, scope), cancelSource.token); | ||||||||
| const disposable = vscode.workspace.onDidChangeConfiguration((e) => { | ||||||||
| if (e.affectsConfiguration(`rstest.${key}`, scope ?? undefined)) { | ||||||||
| cancelSource.cancel(); | ||||||||
| cancelSource = new vscode.CancellationTokenSource(); | ||||||||
| listener(getConfigValue(key, scope), cancelSource.token); | ||||||||
|
Comment on lines
+54
to
+60
|
||||||||
| } | ||||||||
| }); | ||||||||
| return { | ||||||||
| testFileGlobPattern: getConfigValue('testFileGlobPattern', folder), | ||||||||
| rstestPackagePath: getConfigValue('rstestPackagePath', folder), | ||||||||
| } satisfies ExtensionConfig; | ||||||||
| dispose: () => { | ||||||||
| disposable.dispose(); | ||||||||
| cancelSource.cancel(); | ||||||||
|
||||||||
| cancelSource.cancel(); | |
| cancelSource.cancel(); | |
| cancelSource.dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of
v.parsehere may throw an error if the configuration value doesn't match the schema, which could crash the extension. Consider usingv.safeParseinstead and handling validation errors gracefully, or providing a fallback value.