Skip to content
Open
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
8 changes: 8 additions & 0 deletions editors/code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@stylistic/eslint-plugin": "^4.1.0",
"@stylistic/eslint-plugin-js": "^4.1.0",
"@tsconfig/strictest": "^2.0.5",
"@types/lodash": "^4.17.20",
"@types/node": "~22.13.4",
"@types/vscode": "~1.93.0",
"@typescript-eslint/eslint-plugin": "^8.25.0",
Expand Down
2 changes: 1 addition & 1 deletion editors/code/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { RaLanguageClient } from "./lang_client";
export async function createClient(
traceOutputChannel: vscode.OutputChannel,
outputChannel: vscode.OutputChannel,
initializationOptions: vscode.WorkspaceConfiguration,
initializationOptions: lc.LanguageClientOptions["initializationOptions"],
serverOptions: lc.ServerOptions,
config: Config,
unlinkedFiles: vscode.Uri[],
Expand Down
121 changes: 102 additions & 19 deletions editors/code/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,48 @@ import * as path from "path";
import * as vscode from "vscode";
import { expectNotUndefined, log, normalizeDriveLetter, unwrapUndefinable } from "./util";
import type { Env } from "./util";
import type { Disposable } from "vscode";
import { cloneDeep, get, pickBy, set } from "lodash";

export type RunnableEnvCfgItem = {
mask?: string;
env: { [key: string]: { toString(): string } | null };
platform?: string | string[];
};

export type ConfigurationTree = { [key: string]: ConfigurationValue };
export type ConfigurationValue =
| undefined
| null
| boolean
| number
| string
| ConfigurationValue[]
| ConfigurationTree;

type ShowStatusBar = "always" | "never" | { documentSelector: vscode.DocumentSelector };

export class Config {
readonly extensionId = "rust-lang.rust-analyzer";

configureLang: vscode.Disposable | undefined;
workspaceState: vscode.Memento;

readonly rootSection = "rust-analyzer";
private readonly requiresServerReloadOpts = ["server", "files", "showSyntaxTree"].map(
private readonly rootSection = "rust-analyzer";
private readonly requiresServerReloadOpts = ["cargo", "server", "files", "showSyntaxTree"].map(
(opt) => `${this.rootSection}.${opt}`,
);

private readonly requiresWindowReloadOpts = ["testExplorer"].map(
(opt) => `${this.rootSection}.${opt}`,
);

constructor(disposables: Disposable[]) {
vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, disposables);
constructor(ctx: vscode.ExtensionContext) {
this.workspaceState = ctx.workspaceState;
vscode.workspace.onDidChangeConfiguration(
this.onDidChangeConfiguration,
this,
ctx.subscriptions,
);
this.refreshLogging();
this.configureLanguage();
}
Expand All @@ -37,6 +54,44 @@ export class Config {
this.configureLang?.dispose();
}

private readonly extensionConfigurationStateKey = "extensionConfigurations";

/// Returns the rust-analyzer-specific workspace configuration, incl. any
/// configuration items overridden by (present) extensions.
get extensionConfigurations(): Record<string, Record<string, unknown>> {
return pickBy(
this.workspaceState.get<Record<string, ConfigurationTree>>(
"extensionConfigurations",
{},
),
// ignore configurations from disabled/removed extensions
(_, extensionId) => vscode.extensions.getExtension(extensionId) !== undefined,
);
}

async addExtensionConfiguration(
extensionId: string,
configuration: Record<string, unknown>,
): Promise<void> {
const oldConfiguration = this.cfg;

const extCfgs = this.extensionConfigurations;
extCfgs[extensionId] = configuration;
await this.workspaceState.update(this.extensionConfigurationStateKey, extCfgs);

const newConfiguration = this.cfg;
const prefix = `${this.rootSection}.`;
await this.onDidChangeConfiguration({
affectsConfiguration(section: string, _scope?: vscode.ConfigurationScope): boolean {
return (
section.startsWith(prefix) &&
get(oldConfiguration, section.slice(prefix.length)) !==
get(newConfiguration, section.slice(prefix.length))
);
},
});
}

private refreshLogging() {
log.info(
"Extension version:",
Expand Down Expand Up @@ -176,18 +231,43 @@ export class Config {
// We don't do runtime config validation here for simplicity. More on stackoverflow:
// https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension

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

// Returns the final configuration to use, with extension configuration overrides merged in.
public get cfg(): ConfigurationTree {
const finalConfig = cloneDeep<ConfigurationTree>(this.rawCfg);
for (const [extensionId, items] of Object.entries(this.extensionConfigurations)) {
for (const [k, v] of Object.entries(items)) {
const i = this.rawCfg.inspect(k);
if (
i?.workspaceValue !== undefined ||
i?.workspaceFolderValue !== undefined ||
i?.globalValue !== undefined
) {
log.trace(
`Ignoring configuration override for ${k} from extension ${extensionId}`,
);
continue;
}
log.trace(`Extension ${extensionId} overrides configuration ${k} to `, v);
set(finalConfig, k, v);
}
}
return finalConfig;
}

/**
* Beware that postfix `!` operator erases both `null` and `undefined`.
* This is why the following doesn't work as expected:
*
* ```ts
* const nullableNum = vscode
* .workspace
* .getConfiguration
* .getConfiguration("rust-analyzer")
* .get<number | null>(path)!;
*
Expand All @@ -197,7 +277,7 @@ export class Config {
* So this getter handles this quirk by not requiring the caller to use postfix `!`
*/
private get<T>(path: string): T | undefined {
return prepareVSCodeConfig(this.cfg.get<T>(path));
return prepareVSCodeConfig(get(this.cfg, path)) as T;
}

get serverPath() {
Expand All @@ -223,7 +303,7 @@ export class Config {
}

async toggleCheckOnSave() {
const config = this.cfg.inspect<boolean>("checkOnSave") ?? { key: "checkOnSave" };
const config = this.rawCfg.inspect<boolean>("checkOnSave") ?? { key: "checkOnSave" };
let overrideInLanguage;
let target;
let value;
Expand All @@ -249,7 +329,12 @@ export class Config {
overrideInLanguage = config.defaultLanguageValue;
value = config.defaultValue || config.defaultLanguageValue;
}
await this.cfg.update("checkOnSave", !(value || false), target || null, overrideInLanguage);
await this.rawCfg.update(
"checkOnSave",
!(value || false),
target || null,
overrideInLanguage,
);
}

get problemMatcher(): string[] {
Expand Down Expand Up @@ -367,26 +452,24 @@ export class Config {
}

async setAskBeforeUpdateTest(value: boolean) {
await this.cfg.update("runnables.askBeforeUpdateTest", value, true);
await this.rawCfg.update("runnables.askBeforeUpdateTest", value, true);
}
}

export function prepareVSCodeConfig<T>(resp: T): T {
export function prepareVSCodeConfig(resp: ConfigurationValue): ConfigurationValue {
if (Is.string(resp)) {
return substituteVSCodeVariableInString(resp) as T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} else if (resp && Is.array<any>(resp)) {
return substituteVSCodeVariableInString(resp);
} else if (resp && Is.array(resp)) {
return resp.map((val) => {
return prepareVSCodeConfig(val);
}) as T;
});
} else if (resp && typeof resp === "object") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const res: { [key: string]: any } = {};
const res: ConfigurationTree = {};
for (const key in resp) {
const val = resp[key];
res[key] = prepareVSCodeConfig(val);
}
return res as T;
return res;
}
return resp;
}
Expand Down
11 changes: 9 additions & 2 deletions editors/code/src/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
extCtx.subscriptions.push(this);
this.version = extCtx.extension.packageJSON.version ?? "<unknown>";
this._serverVersion = "<not running>";
this.config = new Config(extCtx.subscriptions);
this.config = new Config(extCtx);
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
this.updateStatusBarVisibility(vscode.window.activeTextEditor);
this.statusBarActiveEditorListener = vscode.window.onDidChangeActiveTextEditor((editor) =>
Expand All @@ -150,6 +150,13 @@ export class Ctx implements RustAnalyzerExtensionApi {
});
}

async addConfiguration(
extensionId: string,
configuration: Record<string, unknown>,
): Promise<void> {
await this.config.addExtensionConfiguration(extensionId, configuration);
}

dispose() {
this.config.dispose();
this.statusBar.dispose();
Expand Down Expand Up @@ -230,7 +237,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
debug: run,
};

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

if (this.workspace.kind === "Detached Files") {
rawInitializationOptions = {
Expand Down
6 changes: 6 additions & 0 deletions editors/code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
export interface RustAnalyzerExtensionApi {
// FIXME: this should be non-optional
readonly client?: lc.LanguageClient;

// Allows adding a configuration override from another extension.
// `extensionId` is used to only merge configuration override from present
// extensions. `configuration` is map of rust-analyzer-specific setting
// overrides, e.g., `{"cargo.cfgs": ["foo", "bar"]}`.
addConfiguration(extensionId: string, configuration: Record<string, unknown>): Promise<void>;
}

export async function deactivate() {
Expand Down