Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<namespace>.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):**
Expand Down
2 changes: 2 additions & 0 deletions typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ export { getEnvFileVars } from './envFile';
export {
checkIfConfigurationChanged,
expandTilde,
getEnabledWorkspaceFolders,
getExtensionSettings,
getExtraPaths,
getGlobalSettings,
getWorkspaceSettings,
isToolEnabledForWorkspace,
logLegacySettings,
resolvePathSetting,
resolveVariables,
Expand Down
61 changes: 60 additions & 1 deletion typescript/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,75 @@ export async function getGlobalSettings(
return settings;
}

/**
* Setting key used to enable/disable a tool per workspace folder.
*
* Tool extensions conventionally expose an `<namespace>.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 `<namespace>.<settingKey>` 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<boolean>(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 (`<namespace>.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,
toolConfig: ToolConfig,
resolveInterpreter?: (resource?: import('vscode').Uri) => Promise<{ path?: string[] }>,
): Promise<IBaseSettings[]> {
return Promise.all(
getWorkspaceFolders().map((w) => getWorkspaceSettings(namespace, w, toolConfig, resolveInterpreter)),
getEnabledWorkspaceFolders(namespace).map((w) =>
getWorkspaceSettings(namespace, w, toolConfig, resolveInterpreter),
),
);
}

Expand Down
64 changes: 64 additions & 0 deletions typescript/tests/settingsWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { Uri, WorkspaceFolder } from 'vscode';
import {
checkIfConfigurationChanged,
expandTilde,
getEnabledWorkspaceFolders,
getExtensionSettings,
getExtraPaths,
getGlobalSettings,
getWorkspaceSettings,
isToolEnabledForWorkspace,
logLegacySettings,
resolveVariables,
} from '../src/settings';
Expand Down Expand Up @@ -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<string, boolean>) {
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);
});
});
});