Skip to content

Commit a3b4066

Browse files
committed
feat(vscode): support workspaces and monorepo
1 parent fb194b1 commit a3b4066

File tree

16 files changed

+463
-396
lines changed

16 files changed

+463
-396
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"${workspaceFolder}/packages/vscode/sample"
1616
],
1717
"outFiles": ["${workspaceFolder}/packages/vscode/dist/**/*.js"],
18-
"preLaunchTask": "npm: build:local",
18+
"preLaunchTask": "npm: watch:local",
1919
"autoAttachChildProcesses": true
2020
}
2121
]

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"**/.DS_Store": true
1515
},
1616
"editor.codeActionsOnSave": {
17+
"source.organizeImports": "never",
1718
"source.organizeImports.biome": "explicit"
1819
},
1920
"[javascript]": {

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"tasks": [
66
{
77
"type": "npm",
8-
"script": "build:local",
8+
"script": "watch:local",
99
"path": "packages/vscode",
1010
"problemMatcher": "$tsc-watch",
1111
"isBackground": true,

packages/vscode/package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@
4545
"markdownDescription": "The path to a `package.json` file of a Rstest executable (it's usually inside `node_modules`) in case the extension cannot find it. It will be used to resolve Rstest API paths. This should be used as a last resort fix. Supports `${workspaceFolder}` placeholder.",
4646
"scope": "resource",
4747
"type": "string"
48+
},
49+
"rstest.configFileGlobPattern": {
50+
"description": "Glob patterns used to discover config fiels. Must be an array of strings.",
51+
"type": "array",
52+
"items": {
53+
"type": "string"
54+
},
55+
"default": [
56+
"**/rstest.config.{mjs,ts,js,cjs,mts,cts}"
57+
]
4858
}
4959
}
5060
}
@@ -59,6 +69,7 @@
5969
"test": "npm run test:unit && npm run test:e2e",
6070
"typecheck": "tsc --noEmit",
6171
"watch": "rslib build --watch",
72+
"watch:local": "cross-env SOURCEMAP=true rslib build --watch",
6273
"package:vsce": "npm run build && vsce package"
6374
},
6475
"devDependencies": {
@@ -77,6 +88,7 @@
7788
"glob": "^7.2.3",
7889
"mocha": "^11.7.4",
7990
"ovsx": "^0.10.6",
80-
"typescript": "^5.9.3"
91+
"typescript": "^5.9.3",
92+
"valibot": "^1.1.0"
8193
}
8294
}

packages/vscode/src/config.ts

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,69 @@
1+
import * as v from 'valibot';
12
import vscode from 'vscode';
23

34
// Centralized configuration types for the extension.
45
// Add new keys here to extend configuration in a type-safe way.
5-
export type ExtensionConfig = {
6+
const configSchema = v.object({
67
// Glob patterns that determine which files are considered tests.
78
// 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()), [
1710
'**/*.{test,spec}.[jt]s',
1811
'**/*.{test,spec}.[cm][jt]s',
1912
'**/*.{test,spec}.[jt]sx',
2013
'**/*.{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>;
2324

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
2626
export function getConfigValue<K extends keyof ExtensionConfig>(
2727
key: K,
28-
folder?: vscode.WorkspaceFolder,
28+
scope?: vscode.ConfigurationScope | null,
2929
): 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];
5432
}
5533

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;
5844
}
5945

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+
});
6263
return {
63-
testFileGlobPattern: getConfigValue('testFileGlobPattern', folder),
64-
rstestPackagePath: getConfigValue('rstestPackagePath', folder),
65-
} satisfies ExtensionConfig;
64+
dispose: () => {
65+
disposable.dispose();
66+
cancelSource.cancel();
67+
},
68+
};
6669
}

0 commit comments

Comments
 (0)