src/tools/rust-analyzer/editors/code/src/config.ts TYPESCRIPT 614 lines View on github.com → Search inside
1import * as Is from "vscode-languageclient/lib/common/utils/is";2import * as os from "os";3import * as path from "path";4import * as vscode from "vscode";5import { expectNotUndefined, log, normalizeDriveLetter, unwrapUndefinable } from "./util";6import type { Env } from "./util";7import { cloneDeep, get, pickBy, set } from "lodash";89export type RunnableEnvCfgItem = {10    mask?: string;11    env: { [key: string]: { toString(): string } | null };12    platform?: string | string[];13};1415export type ConfigurationTree = { [key: string]: ConfigurationValue };16export type ConfigurationValue =17    | undefined18    | null19    | boolean20    | number21    | string22    | ConfigurationValue[]23    | ConfigurationTree;2425type ShowStatusBar = "always" | "never" | { documentSelector: vscode.DocumentSelector };2627export class Config {28    readonly extensionId = "rust-lang.rust-analyzer";2930    configureLang: vscode.Disposable | undefined;31    workspaceState: vscode.Memento;3233    private readonly rootSection = "rust-analyzer";34    private readonly requiresServerReloadOpts = ["server", "files", "showSyntaxTree"].map(35        (opt) => `${this.rootSection}.${opt}`,36    );3738    private readonly requiresWindowReloadOpts = ["testExplorer"].map(39        (opt) => `${this.rootSection}.${opt}`,40    );4142    constructor(ctx: vscode.ExtensionContext) {43        this.workspaceState = ctx.workspaceState;44        vscode.workspace.onDidChangeConfiguration(45            this.onDidChangeConfiguration,46            this,47            ctx.subscriptions,48        );49        this.refreshLogging();50        this.configureLanguage();51    }5253    dispose() {54        this.configureLang?.dispose();55    }5657    private readonly extensionConfigurationStateKey = "extensionConfigurations";5859    /// Returns the rust-analyzer-specific workspace configuration, incl. any60    /// 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 extensions68            (_, extensionId) => vscode.extensions.getExtension(extensionId) !== undefined,69        );70    }7172    async addExtensionConfiguration(73        extensionId: string,74        configuration: Record<string, unknown>,75    ): Promise<void> {76        const oldConfiguration = this.cfg;7778        const extCfgs = this.extensionConfigurations;79        extCfgs[extensionId] = configuration;80        await this.workspaceState.update(this.extensionConfigurationStateKey, extCfgs);8182        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    }9495    private refreshLogging() {96        log.info(97            "Extension version:",98            vscode.extensions.getExtension(this.extensionId)!.packageJSON.version,99        );100101        const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function));102        log.info("Using configuration", Object.fromEntries(cfg));103    }104105    private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) {106        this.refreshLogging();107108        this.configureLanguage();109110        const requiresWindowReloadOpt = this.requiresWindowReloadOpts.find((opt) =>111            event.affectsConfiguration(opt),112        );113114        if (requiresWindowReloadOpt) {115            const message = `Changing "${requiresWindowReloadOpt}" requires a window reload`;116            const userResponse = await vscode.window.showInformationMessage(message, "Reload now");117118            if (userResponse) {119                await vscode.commands.executeCommand("workbench.action.reloadWindow");120            }121        }122123        const requiresServerReloadOpt = this.requiresServerReloadOpts.find((opt) =>124            event.affectsConfiguration(opt),125        );126127        if (!requiresServerReloadOpt) return;128129        if (this.restartServerOnConfigChange) {130            await vscode.commands.executeCommand("rust-analyzer.restartServer");131            return;132        }133134        const message = `Changing "${requiresServerReloadOpt}" requires a server restart`;135        const userResponse = await vscode.window.showInformationMessage(message, "Restart now");136137        if (userResponse) {138            const command = "rust-analyzer.restartServer";139            await vscode.commands.executeCommand(command);140        }141    }142143    /**144     * Sets up additional language configuration that's impossible to do via a145     * separate language-configuration.json file. See [1] for more information.146     *147     * [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076148     */149    private configureLanguage() {150        // Only need to dispose of the config if there's a change151        if (this.configureLang) {152            this.configureLang.dispose();153            this.configureLang = undefined;154        }155156        let onEnterRules: vscode.OnEnterRule[] = [157            {158                // Carry indentation from the previous line159                // if it's only whitespace160                beforeText: /^\s+$/,161                action: { indentAction: vscode.IndentAction.None },162            },163            {164                // After the end of a function/field chain,165                // with the semicolon on the same line166                beforeText: /^\s+\..*;/,167                action: { indentAction: vscode.IndentAction.Outdent },168            },169            {170                // After the end of a function/field chain,171                // with semicolon detached from the rest172                beforeText: /^\s+;/,173                previousLineText: /^\s+\..*/,174                action: { indentAction: vscode.IndentAction.Outdent },175            },176        ];177178        if (this.typingContinueCommentsOnNewline) {179            const indentAction = vscode.IndentAction.None;180181            onEnterRules = [182                ...onEnterRules,183                {184                    // Doc single-line comment185                    // e.g. ///|186                    beforeText: /^\s*\/{3}.*$/,187                    action: { indentAction, appendText: "/// " },188                },189                {190                    // Parent doc single-line comment191                    // e.g. //!|192                    beforeText: /^\s*\/{2}!.*$/,193                    action: { indentAction, appendText: "//! " },194                },195                {196                    // Begins an auto-closed multi-line comment (standard or parent doc)197                    // e.g. /** | */ or /*! | */198                    beforeText: /^\s*\/\*(\*|!)(?!\/)([^*]|\*(?!\/))*$/,199                    afterText: /^\s*\*\/$/,200                    action: {201                        indentAction: vscode.IndentAction.IndentOutdent,202                        appendText: " * ",203                    },204                },205                {206                    // Begins a multi-line comment (standard or parent doc)207                    // e.g. /** ...| or /*! ...|208                    beforeText: /^\s*\/\*(\*|!)(?!\/)([^*]|\*(?!\/))*$/,209                    action: { indentAction, appendText: " * " },210                },211                {212                    // Continues a multi-line comment213                    // e.g.  * ...|214                    beforeText: /^( {2})* \*( ([^*]|\*(?!\/))*)?$/,215                    action: { indentAction, appendText: "* " },216                },217                {218                    // Dedents after closing a multi-line comment219                    // e.g.  */|220                    beforeText: /^( {2})* \*\/\s*$/,221                    action: { indentAction, removeText: 1 },222                },223            ];224        }225226        this.configureLang = vscode.languages.setLanguageConfiguration("rust", {227            onEnterRules,228        });229    }230231    // We don't do runtime config validation here for simplicity. More on stackoverflow:232    // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension233234    // Returns the raw configuration for rust-analyzer as returned by vscode. This235    // should only be used when modifications to the user/workspace configuration236    // are required.237    private get rawCfg(): vscode.WorkspaceConfiguration {238        return vscode.workspace.getConfiguration(this.rootSection);239    }240241    // 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 !== undefined251                ) {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    }263264    /**265     * Beware that postfix `!` operator erases both `null` and `undefined`.266     * This is why the following doesn't work as expected:267     *268     * ```ts269     * const nullableNum = vscode270     *  .workspace271     *  .getConfiguration("rust-analyzer")272     *  .get<number | null>(path)!;273     *274     * // What happens is that type of `nullableNum` is `number` but not `null | number`:275     * const fullFledgedNum: number = nullableNum;276     * ```277     * So this getter handles this quirk by not requiring the caller to use postfix `!`278     */279    private get<T>(path: string): T | undefined {280        return prepareVSCodeConfig(get(this.cfg, path)) as T;281    }282283    get serverPath() {284        return this.get<null | string>("server.path");285    }286287    get serverExtraEnv(): Env {288        const extraEnv =289            this.get<{ [key: string]: { toString(): string } | null } | null>("server.extraEnv") ??290            {};291        return substituteVariablesInEnv(292            Object.fromEntries(293                Object.entries(extraEnv).map(([k, v]) => [294                    k,295                    typeof v === "string" ? v : v?.toString(),296                ]),297            ),298        );299    }300301    get checkOnSave() {302        return this.get<boolean>("checkOnSave") ?? false;303    }304305    async toggleCheckOnSave() {306        const config = this.rawCfg.inspect<boolean>("checkOnSave") ?? { key: "checkOnSave" };307        let overrideInLanguage;308        let target;309        let value;310        if (311            config.workspaceFolderValue !== undefined ||312            config.workspaceFolderLanguageValue !== undefined313        ) {314            target = vscode.ConfigurationTarget.WorkspaceFolder;315            overrideInLanguage = config.workspaceFolderLanguageValue;316            value = config.workspaceFolderValue || config.workspaceFolderLanguageValue;317        } else if (318            config.workspaceValue !== undefined ||319            config.workspaceLanguageValue !== undefined320        ) {321            target = vscode.ConfigurationTarget.Workspace;322            overrideInLanguage = config.workspaceLanguageValue;323            value = config.workspaceValue || config.workspaceLanguageValue;324        } else if (config.globalValue !== undefined || config.globalLanguageValue !== undefined) {325            target = vscode.ConfigurationTarget.Global;326            overrideInLanguage = config.globalLanguageValue;327            value = config.globalValue || config.globalLanguageValue;328        } else if (config.defaultValue !== undefined || config.defaultLanguageValue !== undefined) {329            overrideInLanguage = config.defaultLanguageValue;330            value = config.defaultValue || config.defaultLanguageValue;331        }332        await this.rawCfg.update(333            "checkOnSave",334            !(value || false),335            target || null,336            overrideInLanguage,337        );338    }339340    get problemMatcher(): string[] {341        return this.get<string[]>("runnables.problemMatcher") || [];342    }343344    get testExplorer() {345        return this.get<boolean | undefined>("testExplorer");346    }347348    runnablesExtraEnv(label: string): Env {349        const serverEnv = this.serverExtraEnv;350        let extraEnv =351            this.get<352                RunnableEnvCfgItem[] | { [key: string]: { toString(): string } | null } | null353            >("runnables.extraEnv") ?? {};354        if (!extraEnv) return serverEnv;355356        const platform = process.platform;357        const checkPlatform = (it: RunnableEnvCfgItem) => {358            if (it.platform) {359                const platforms = Array.isArray(it.platform) ? it.platform : [it.platform];360                return platforms.indexOf(platform) >= 0;361            }362            return true;363        };364365        if (extraEnv instanceof Array) {366            const env = {};367            for (const it of extraEnv) {368                const masked = !it.mask || new RegExp(it.mask).test(label);369                if (masked && checkPlatform(it)) {370                    Object.assign(env, it.env);371                }372            }373            extraEnv = env;374        }375        const runnableExtraEnv = substituteVariablesInEnv(376            Object.fromEntries(377                Object.entries(extraEnv).map(([k, v]) => [378                    k,379                    typeof v === "string" ? v : v?.toString(),380                ]),381            ),382        );383        return { ...runnableExtraEnv, ...serverEnv };384    }385386    get restartServerOnConfigChange() {387        return this.get<boolean>("restartServerOnConfigChange");388    }389390    get typingContinueCommentsOnNewline() {391        return this.get<boolean>("typing.continueCommentsOnNewline");392    }393394    get debug() {395        let sourceFileMap = this.get<Record<string, string> | "auto">("debug.sourceFileMap");396        if (sourceFileMap !== "auto") {397            // "/rustc/<id>" used by suggestions only.398            const { ["/rustc/<id>"]: _, ...trimmed } =399                this.get<Record<string, string>>("debug.sourceFileMap") ?? {};400            sourceFileMap = trimmed;401        }402403        return {404            engine: this.get<string>("debug.engine"),405            engineSettings: this.get<object>("debug.engineSettings") ?? {},406            buildBeforeRestart: this.get<boolean>("debug.buildBeforeRestart"),407            sourceFileMap: sourceFileMap,408        };409    }410411    get hoverActions() {412        return {413            enable: this.get<boolean>("hover.actions.enable"),414            implementations: this.get<boolean>("hover.actions.implementations.enable"),415            references: this.get<boolean>("hover.actions.references.enable"),416            run: this.get<boolean>("hover.actions.run.enable"),417            debug: this.get<boolean>("hover.actions.debug.enable"),418            gotoTypeDef: this.get<boolean>("hover.actions.gotoTypeDef.enable"),419        };420    }421422    get previewRustcOutput() {423        return this.get<boolean>("diagnostics.previewRustcOutput");424    }425426    get useRustcErrorCode() {427        return this.get<boolean>("diagnostics.useRustcErrorCode");428    }429430    get showDependenciesExplorer() {431        return this.get<boolean>("showDependenciesExplorer");432    }433434    get showSyntaxTree() {435        return this.get<boolean>("showSyntaxTree");436    }437438    get statusBarClickAction() {439        return this.get<string>("statusBar.clickAction");440    }441442    get statusBarShowStatusBar() {443        return this.get<ShowStatusBar>("statusBar.showStatusBar");444    }445446    get initializeStopped() {447        return this.get<boolean>("initializeStopped");448    }449450    get askBeforeUpdateTest() {451        return this.get<boolean>("runnables.askBeforeUpdateTest");452    }453454    async setAskBeforeUpdateTest(value: boolean) {455        await this.rawCfg.update("runnables.askBeforeUpdateTest", value, true);456    }457}458459export function prepareVSCodeConfig(resp: ConfigurationValue): ConfigurationValue {460    if (Is.string(resp)) {461        return substituteVSCodeVariableInString(resp);462    } else if (resp && Is.array(resp)) {463        return resp.map((val) => {464            return prepareVSCodeConfig(val);465        });466    } else if (resp && typeof resp === "object") {467        const res: ConfigurationTree = {};468        for (const key in resp) {469            const val = resp[key];470            res[key] = prepareVSCodeConfig(val);471        }472        return res;473    }474    return resp;475}476477// FIXME: Merge this with `substituteVSCodeVariables` above478export function substituteVariablesInEnv(env: Env): Env {479    const depRe = new RegExp(/\${(?<depName>.+?)}/g);480    const missingDeps = new Set<string>();481    // vscode uses `env:ENV_NAME` for env vars resolution, and it's easier482    // to follow the same convention for our dependency tracking483    const definedEnvKeys = new Set(Object.keys(env).map((key) => `env:${key}`));484    const envWithDeps = Object.fromEntries(485        Object.entries(env).map(([key, value]) => {486            const deps = new Set<string>();487            if (value) {488                let match;489                while ((match = depRe.exec(value))) {490                    const depName = unwrapUndefinable(match.groups?.["depName"]);491                    deps.add(depName);492                    // `depName` at this point can have a form of `expression` or493                    // `prefix:expression`494                    if (!definedEnvKeys.has(depName)) {495                        missingDeps.add(depName);496                    }497                }498            }499            return [`env:${key}`, { deps: [...deps], value }];500        }),501    );502503    const resolved = new Set<string>();504    for (const dep of missingDeps) {505        const match = /(?<prefix>.*?):(?<body>.+)/.exec(dep);506        if (match) {507            const { prefix, body } = match.groups!;508            if (prefix === "env") {509                const envName = unwrapUndefinable(body);510                envWithDeps[dep] = {511                    value: process.env[envName] ?? "",512                    deps: [],513                };514                resolved.add(dep);515            } else {516                // we can't handle other prefixes at the moment517                // leave values as is, but still mark them as resolved518                envWithDeps[dep] = {519                    value: "${" + dep + "}",520                    deps: [],521                };522                resolved.add(dep);523            }524        } else {525            envWithDeps[dep] = {526                value: computeVscodeVar(dep) || "${" + dep + "}",527                deps: [],528            };529        }530    }531    const toResolve = new Set(Object.keys(envWithDeps));532533    let leftToResolveSize;534    do {535        leftToResolveSize = toResolve.size;536        for (const key of toResolve) {537            const item = envWithDeps[key];538            if (item && item.deps.every((dep) => resolved.has(dep))) {539                item.value = item.value?.replace(/\${(?<depName>.+?)}/g, (_wholeMatch, depName) => {540                    return envWithDeps[depName]?.value ?? "";541                });542                resolved.add(key);543                toResolve.delete(key);544            }545        }546    } while (toResolve.size > 0 && toResolve.size < leftToResolveSize);547548    const resolvedEnv: Env = {};549    for (const key of Object.keys(env)) {550        const item = unwrapUndefinable(envWithDeps[`env:${key}`]);551        resolvedEnv[key] = item.value;552    }553    return resolvedEnv;554}555556const VarRegex = new RegExp(/\$\{(.+?)\}/g);557function substituteVSCodeVariableInString(val: string): string {558    return val.replace(VarRegex, (substring: string, varName) => {559        if (Is.string(varName)) {560            return computeVscodeVar(varName) || substring;561        } else {562            return substring;563        }564    });565}566567function computeVscodeVar(varName: string): string | null {568    const workspaceFolder = () => {569        const folders = vscode.workspace.workspaceFolders ?? [];570        const folder = folders[0];571        // TODO: support for remote workspaces?572        const fsPath: string =573            folder === undefined574                ? "" // no workspace opened575                : // could use currently opened document to detect the correct576                  // workspace. However, that would be determined by the document577                  // user has opened on Editor startup. Could lead to578                  // unpredictable workspace selection in practice.579                  // It's better to pick the first one580                  normalizeDriveLetter(folder.uri.fsPath);581        return fsPath;582    };583    // https://code.visualstudio.com/docs/editor/variables-reference584    const supportedVariables: { [k: string]: () => string } = {585        workspaceFolder,586587        workspaceFolderBasename: () => {588            return path.basename(workspaceFolder());589        },590591        cwd: () => process.cwd(),592        userHome: () => os.homedir(),593594        // see595        // https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81596        // or597        // https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56598        execPath: () => process.env["VSCODE_EXEC_PATH"] ?? process.execPath,599600        pathSeparator: () => path.sep,601    };602603    if (varName in supportedVariables) {604        const fn = expectNotUndefined(605            supportedVariables[varName],606            `${varName} should not be undefined here`,607        );608        return fn();609    } else {610        // return "${" + varName + "}";611        return null;612    }613}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.