Skip to content

Commit 165065a

Browse files
authored
Merge pull request rust-lang#20837 from osdyne/extension-configuration
Add an Extension Config API
2 parents 3145078 + ce94044 commit 165065a

File tree

6 files changed

+126
-21
lines changed

6 files changed

+126
-21
lines changed

src/tools/rust-analyzer/editors/code/package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/rust-analyzer/editors/code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@stylistic/eslint-plugin": "^4.1.0",
5959
"@stylistic/eslint-plugin-js": "^4.1.0",
6060
"@tsconfig/strictest": "^2.0.5",
61+
"@types/lodash": "^4.17.20",
6162
"@types/node": "~22.13.4",
6263
"@types/vscode": "~1.93.0",
6364
"@typescript-eslint/eslint-plugin": "^8.25.0",

src/tools/rust-analyzer/editors/code/src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { RaLanguageClient } from "./lang_client";
1313
export async function createClient(
1414
traceOutputChannel: vscode.OutputChannel,
1515
outputChannel: vscode.OutputChannel,
16-
initializationOptions: vscode.WorkspaceConfiguration,
16+
initializationOptions: lc.LanguageClientOptions["initializationOptions"],
1717
serverOptions: lc.ServerOptions,
1818
config: Config,
1919
unlinkedFiles: vscode.Uri[],

src/tools/rust-analyzer/editors/code/src/config.ts

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,33 @@ import * as path from "path";
44
import * as vscode from "vscode";
55
import { expectNotUndefined, log, normalizeDriveLetter, unwrapUndefinable } from "./util";
66
import type { Env } from "./util";
7-
import type { Disposable } from "vscode";
7+
import { cloneDeep, get, pickBy, set } from "lodash";
88

99
export type RunnableEnvCfgItem = {
1010
mask?: string;
1111
env: { [key: string]: { toString(): string } | null };
1212
platform?: string | string[];
1313
};
1414

15+
export type ConfigurationTree = { [key: string]: ConfigurationValue };
16+
export type ConfigurationValue =
17+
| undefined
18+
| null
19+
| boolean
20+
| number
21+
| string
22+
| ConfigurationValue[]
23+
| ConfigurationTree;
24+
1525
type ShowStatusBar = "always" | "never" | { documentSelector: vscode.DocumentSelector };
1626

1727
export class Config {
1828
readonly extensionId = "rust-lang.rust-analyzer";
29+
1930
configureLang: vscode.Disposable | undefined;
31+
workspaceState: vscode.Memento;
2032

21-
readonly rootSection = "rust-analyzer";
33+
private readonly rootSection = "rust-analyzer";
2234
private readonly requiresServerReloadOpts = ["server", "files", "showSyntaxTree"].map(
2335
(opt) => `${this.rootSection}.${opt}`,
2436
);
@@ -27,8 +39,13 @@ export class Config {
2739
(opt) => `${this.rootSection}.${opt}`,
2840
);
2941

30-
constructor(disposables: Disposable[]) {
31-
vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, disposables);
42+
constructor(ctx: vscode.ExtensionContext) {
43+
this.workspaceState = ctx.workspaceState;
44+
vscode.workspace.onDidChangeConfiguration(
45+
this.onDidChangeConfiguration,
46+
this,
47+
ctx.subscriptions,
48+
);
3249
this.refreshLogging();
3350
this.configureLanguage();
3451
}
@@ -37,6 +54,44 @@ export class Config {
3754
this.configureLang?.dispose();
3855
}
3956

57+
private readonly extensionConfigurationStateKey = "extensionConfigurations";
58+
59+
/// Returns the rust-analyzer-specific workspace configuration, incl. any
60+
/// configuration items overridden by (present) extensions.
61+
get extensionConfigurations(): Record<string, Record<string, unknown>> {
62+
return pickBy(
63+
this.workspaceState.get<Record<string, ConfigurationTree>>(
64+
"extensionConfigurations",
65+
{},
66+
),
67+
// ignore configurations from disabled/removed extensions
68+
(_, extensionId) => vscode.extensions.getExtension(extensionId) !== undefined,
69+
);
70+
}
71+
72+
async addExtensionConfiguration(
73+
extensionId: string,
74+
configuration: Record<string, unknown>,
75+
): Promise<void> {
76+
const oldConfiguration = this.cfg;
77+
78+
const extCfgs = this.extensionConfigurations;
79+
extCfgs[extensionId] = configuration;
80+
await this.workspaceState.update(this.extensionConfigurationStateKey, extCfgs);
81+
82+
const newConfiguration = this.cfg;
83+
const prefix = `${this.rootSection}.`;
84+
await this.onDidChangeConfiguration({
85+
affectsConfiguration(section: string, _scope?: vscode.ConfigurationScope): boolean {
86+
return (
87+
section.startsWith(prefix) &&
88+
get(oldConfiguration, section.slice(prefix.length)) !==
89+
get(newConfiguration, section.slice(prefix.length))
90+
);
91+
},
92+
});
93+
}
94+
4095
private refreshLogging() {
4196
log.info(
4297
"Extension version:",
@@ -176,18 +231,43 @@ export class Config {
176231
// We don't do runtime config validation here for simplicity. More on stackoverflow:
177232
// https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
178233

179-
private get cfg(): vscode.WorkspaceConfiguration {
234+
// Returns the raw configuration for rust-analyzer as returned by vscode. This
235+
// should only be used when modifications to the user/workspace configuration
236+
// are required.
237+
private get rawCfg(): vscode.WorkspaceConfiguration {
180238
return vscode.workspace.getConfiguration(this.rootSection);
181239
}
182240

241+
// Returns the final configuration to use, with extension configuration overrides merged in.
242+
public get cfg(): ConfigurationTree {
243+
const finalConfig = cloneDeep<ConfigurationTree>(this.rawCfg);
244+
for (const [extensionId, items] of Object.entries(this.extensionConfigurations)) {
245+
for (const [k, v] of Object.entries(items)) {
246+
const i = this.rawCfg.inspect(k);
247+
if (
248+
i?.workspaceValue !== undefined ||
249+
i?.workspaceFolderValue !== undefined ||
250+
i?.globalValue !== undefined
251+
) {
252+
log.trace(
253+
`Ignoring configuration override for ${k} from extension ${extensionId}`,
254+
);
255+
continue;
256+
}
257+
log.trace(`Extension ${extensionId} overrides configuration ${k} to `, v);
258+
set(finalConfig, k, v);
259+
}
260+
}
261+
return finalConfig;
262+
}
263+
183264
/**
184265
* Beware that postfix `!` operator erases both `null` and `undefined`.
185266
* This is why the following doesn't work as expected:
186267
*
187268
* ```ts
188269
* const nullableNum = vscode
189270
* .workspace
190-
* .getConfiguration
191271
* .getConfiguration("rust-analyzer")
192272
* .get<number | null>(path)!;
193273
*
@@ -197,7 +277,7 @@ export class Config {
197277
* So this getter handles this quirk by not requiring the caller to use postfix `!`
198278
*/
199279
private get<T>(path: string): T | undefined {
200-
return prepareVSCodeConfig(this.cfg.get<T>(path));
280+
return prepareVSCodeConfig(get(this.cfg, path)) as T;
201281
}
202282

203283
get serverPath() {
@@ -223,7 +303,7 @@ export class Config {
223303
}
224304

225305
async toggleCheckOnSave() {
226-
const config = this.cfg.inspect<boolean>("checkOnSave") ?? { key: "checkOnSave" };
306+
const config = this.rawCfg.inspect<boolean>("checkOnSave") ?? { key: "checkOnSave" };
227307
let overrideInLanguage;
228308
let target;
229309
let value;
@@ -249,7 +329,12 @@ export class Config {
249329
overrideInLanguage = config.defaultLanguageValue;
250330
value = config.defaultValue || config.defaultLanguageValue;
251331
}
252-
await this.cfg.update("checkOnSave", !(value || false), target || null, overrideInLanguage);
332+
await this.rawCfg.update(
333+
"checkOnSave",
334+
!(value || false),
335+
target || null,
336+
overrideInLanguage,
337+
);
253338
}
254339

255340
get problemMatcher(): string[] {
@@ -367,26 +452,24 @@ export class Config {
367452
}
368453

369454
async setAskBeforeUpdateTest(value: boolean) {
370-
await this.cfg.update("runnables.askBeforeUpdateTest", value, true);
455+
await this.rawCfg.update("runnables.askBeforeUpdateTest", value, true);
371456
}
372457
}
373458

374-
export function prepareVSCodeConfig<T>(resp: T): T {
459+
export function prepareVSCodeConfig(resp: ConfigurationValue): ConfigurationValue {
375460
if (Is.string(resp)) {
376-
return substituteVSCodeVariableInString(resp) as T;
377-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
378-
} else if (resp && Is.array<any>(resp)) {
461+
return substituteVSCodeVariableInString(resp);
462+
} else if (resp && Is.array(resp)) {
379463
return resp.map((val) => {
380464
return prepareVSCodeConfig(val);
381-
}) as T;
465+
});
382466
} else if (resp && typeof resp === "object") {
383-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
384-
const res: { [key: string]: any } = {};
467+
const res: ConfigurationTree = {};
385468
for (const key in resp) {
386469
const val = resp[key];
387470
res[key] = prepareVSCodeConfig(val);
388471
}
389-
return res as T;
472+
return res;
390473
}
391474
return resp;
392475
}

src/tools/rust-analyzer/editors/code/src/ctx.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
125125
extCtx.subscriptions.push(this);
126126
this.version = extCtx.extension.packageJSON.version ?? "<unknown>";
127127
this._serverVersion = "<not running>";
128-
this.config = new Config(extCtx.subscriptions);
128+
this.config = new Config(extCtx);
129129
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
130130
this.updateStatusBarVisibility(vscode.window.activeTextEditor);
131131
this.statusBarActiveEditorListener = vscode.window.onDidChangeActiveTextEditor((editor) =>
@@ -150,6 +150,13 @@ export class Ctx implements RustAnalyzerExtensionApi {
150150
});
151151
}
152152

153+
async addConfiguration(
154+
extensionId: string,
155+
configuration: Record<string, unknown>,
156+
): Promise<void> {
157+
await this.config.addExtensionConfiguration(extensionId, configuration);
158+
}
159+
153160
dispose() {
154161
this.config.dispose();
155162
this.statusBar.dispose();
@@ -230,7 +237,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
230237
debug: run,
231238
};
232239

233-
let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
240+
let rawInitializationOptions = this.config.cfg;
234241

235242
if (this.workspace.kind === "Detached Files") {
236243
rawInitializationOptions = {

src/tools/rust-analyzer/editors/code/src/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
1313
export interface RustAnalyzerExtensionApi {
1414
// FIXME: this should be non-optional
1515
readonly client?: lc.LanguageClient;
16+
17+
// Allows adding a configuration override from another extension.
18+
// `extensionId` is used to only merge configuration override from present
19+
// extensions. `configuration` is map of rust-analyzer-specific setting
20+
// overrides, e.g., `{"cargo.cfgs": ["foo", "bar"]}`.
21+
addConfiguration(extensionId: string, configuration: Record<string, unknown>): Promise<void>;
1622
}
1723

1824
export async function deactivate() {

0 commit comments

Comments
 (0)