From 224ed0de277653e59d6449d4b3b62d57bddb9219 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:13:26 +0000 Subject: [PATCH 1/2] Initial plan From 5e82e6e0a51d69d90e784ff2fb5b147a729e8a3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:24:37 +0000 Subject: [PATCH 2/2] Add per-folder enable support for multi-root singleton server --- README.md | 24 ++++++++ typescript/src/index.ts | 2 + typescript/src/settings.ts | 61 ++++++++++++++++++++- typescript/tests/settingsWorkspace.test.ts | 64 ++++++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 85b47ed..e546144 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,30 @@ npm run build npm test ``` +### Multi-root workspaces + +The TypeScript package uses a **singleton** language-server model: a single +server is started for the whole window (rooted at the folder returned by +`getProjectRoot()`), and settings for every workspace folder are forwarded to it +via `getExtensionSettings()`. This avoids spawning a redundant server process +per folder in multi-root workspaces. + +To let users disable a tool for individual folders (e.g. `pylint.enabled: false` +in one folder of a multi-root workspace), the package honours a per-folder +`.enabled` boolean: + +- `getExtensionSettings(namespace, toolConfig, resolveInterpreter)` automatically + omits folders where the tool is disabled, so the shared server never lints + opted-out folders. +- `isToolEnabledForWorkspace(namespace, workspaceFolder, settingKey?)` reports + whether the tool is enabled for a single folder. Pass `settingKey` when a tool + uses a different key (e.g. `'enable'`); it defaults to `'enabled'`. +- `getEnabledWorkspaceFolders(namespace, settingKey?)` returns only the folders + for which the tool is enabled — useful when registering per-folder providers. + +Folders default to enabled when the setting is unset, so behaviour is unchanged +for tools that don't expose an `enabled` setting. + ## Consuming in Extensions **Git submodule (current):** diff --git a/typescript/src/index.ts b/typescript/src/index.ts index b46e099..954aa32 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -44,10 +44,12 @@ export { getEnvFileVars } from './envFile'; export { checkIfConfigurationChanged, expandTilde, + getEnabledWorkspaceFolders, getExtensionSettings, getExtraPaths, getGlobalSettings, getWorkspaceSettings, + isToolEnabledForWorkspace, logLegacySettings, resolvePathSetting, resolveVariables, diff --git a/typescript/src/settings.ts b/typescript/src/settings.ts index 57157b6..f08aa86 100644 --- a/typescript/src/settings.ts +++ b/typescript/src/settings.ts @@ -270,8 +270,65 @@ export async function getGlobalSettings( return settings; } +/** + * Setting key used to enable/disable a tool per workspace folder. + * + * Tool extensions conventionally expose an `.enabled` boolean + * (see {@link ToolConfig.settingsDefaults}). Override via the `settingKey` + * argument of {@link isToolEnabledForWorkspace} when a tool uses a different + * key (e.g. `enable`). + */ +const DEFAULT_ENABLE_SETTING_KEY = 'enabled'; + +/** + * Determine whether the tool is enabled for a specific workspace folder. + * + * In a multi-root workspace a single language server is shared across all + * folders (see {@link getProjectRoot} / {@link getExtensionSettings}). This + * helper lets extensions honour a per-folder opt-out — e.g. `pylint.enabled: + * false` in one folder — so the shared server can skip work for that folder + * instead of spawning a redundant process per folder. + * + * Reads the `.` boolean setting scoped to the folder + * and defaults to enabled (`true`) when unset, so behaviour is unchanged for + * tools that don't define the setting. + * + * @param namespace - Extension configuration namespace (e.g. `"pylint"`). + * @param workspace - The workspace folder to check (omit for global scope). + * @param settingKey - The boolean setting key (defaults to `"enabled"`). + */ +export function isToolEnabledForWorkspace( + namespace: string, + workspace?: WorkspaceFolder, + settingKey: string = DEFAULT_ENABLE_SETTING_KEY, +): boolean { + const config = getConfiguration(namespace, workspace?.uri); + return config.get(settingKey, true) !== false; +} + +/** + * Return the subset of workspace folders for which the tool is enabled. + * + * Useful for extensions that want to register per-folder providers or skip + * disabled folders when deciding whether to start the shared server. + * + * @param namespace - Extension configuration namespace (e.g. `"pylint"`). + * @param settingKey - The boolean setting key (defaults to `"enabled"`). + */ +export function getEnabledWorkspaceFolders( + namespace: string, + settingKey: string = DEFAULT_ENABLE_SETTING_KEY, +): WorkspaceFolder[] { + return getWorkspaceFolders().filter((w) => isToolEnabledForWorkspace(namespace, w, settingKey)); +} + /** * Resolve settings for all workspace folders. + * + * Folders where the tool is disabled (`.enabled: false`) are + * omitted, so the shared (singleton) language server never receives — and + * therefore never lints — folders the user has opted out of. This avoids + * redundant diagnostics in multi-root workspaces. */ export function getExtensionSettings( namespace: string, @@ -279,7 +336,9 @@ export function getExtensionSettings( resolveInterpreter?: (resource?: import('vscode').Uri) => Promise<{ path?: string[] }>, ): Promise { return Promise.all( - getWorkspaceFolders().map((w) => getWorkspaceSettings(namespace, w, toolConfig, resolveInterpreter)), + getEnabledWorkspaceFolders(namespace).map((w) => + getWorkspaceSettings(namespace, w, toolConfig, resolveInterpreter), + ), ); } diff --git a/typescript/tests/settingsWorkspace.test.ts b/typescript/tests/settingsWorkspace.test.ts index 9246420..d4f897b 100644 --- a/typescript/tests/settingsWorkspace.test.ts +++ b/typescript/tests/settingsWorkspace.test.ts @@ -7,9 +7,12 @@ import { Uri, WorkspaceFolder } from 'vscode'; import { checkIfConfigurationChanged, expandTilde, + getEnabledWorkspaceFolders, + getExtensionSettings, getExtraPaths, getGlobalSettings, getWorkspaceSettings, + isToolEnabledForWorkspace, logLegacySettings, resolveVariables, } from '../src/settings'; @@ -294,4 +297,65 @@ suite('settings — workspace & global resolution', () => { ); }); }); + + suite('per-folder enable / multi-root', () => { + const wsA = makeWorkspace('folder-a', '/home/user/projects/a', 0); + const wsB = makeWorkspace('folder-b', '/home/user/projects/b', 1); + + function stubEnabledByPath(enabledByPath: Record) { + getConfigurationStub.callsFake((_namespace: string, scope?: { fsPath?: string }) => ({ + get: (key: string, def?: unknown) => { + if (key === 'enabled' && scope?.fsPath !== undefined && scope.fsPath in enabledByPath) { + return enabledByPath[scope.fsPath]; + } + return def; + }, + })); + } + + test('isToolEnabledForWorkspace defaults to enabled when unset', () => { + getConfigurationStub.returns({ get: (_key: string, def?: unknown) => def }); + assert.isTrue(isToolEnabledForWorkspace('flake8', wsA)); + }); + + test('isToolEnabledForWorkspace returns false when explicitly disabled', () => { + stubEnabledByPath({ [wsA.uri.fsPath]: false }); + assert.isFalse(isToolEnabledForWorkspace('flake8', wsA)); + }); + + test('isToolEnabledForWorkspace honours a custom setting key', () => { + getConfigurationStub.callsFake(() => ({ + get: (key: string, def?: unknown) => (key === 'enable' ? false : def), + })); + assert.isFalse(isToolEnabledForWorkspace('pylint', wsA, 'enable')); + }); + + test('getEnabledWorkspaceFolders filters out disabled folders', () => { + getWorkspaceFoldersStub.returns([wsA, wsB]); + stubEnabledByPath({ [wsB.uri.fsPath]: false }); + + const result = getEnabledWorkspaceFolders('flake8'); + assert.deepEqual( + result.map((w) => w.name), + ['folder-a'], + ); + }); + + test('getExtensionSettings skips disabled folders', async () => { + getWorkspaceFoldersStub.returns([wsA, wsB]); + stubEnabledByPath({ [wsB.uri.fsPath]: false }); + + const result = await getExtensionSettings('flake8', makeToolConfig()); + assert.lengthOf(result, 1); + assert.equal(result[0].workspace, wsA.uri.toString()); + }); + + test('getExtensionSettings includes all folders when none disabled', async () => { + getWorkspaceFoldersStub.returns([wsA, wsB]); + getConfigurationStub.returns({ get: (_key: string, def?: unknown) => def }); + + const result = await getExtensionSettings('flake8', makeToolConfig()); + assert.lengthOf(result, 2); + }); + }); });