import path = require('path'); import * as vscode from 'vscode'; import { Env } from './client'; import { log } from "./util"; export type UpdatesChannel = "stable" | "nightly"; const NIGHTLY_TAG = "nightly"; export type RunnableEnvCfg = undefined | Record | { mask?: string; env: Record }[]; export class Config { readonly extensionId = "rust-lang.rust-analyzer"; readonly rootSection = "rust-analyzer"; private readonly requiresReloadOpts = [ "serverPath", "server", "cargo", "procMacro", "files", "lens", // works as lens.* ] .map(opt => `${this.rootSection}.${opt}`); readonly package: { version: string; releaseTag: string | null; enableProposedApi: boolean | undefined; } = vscode.extensions.getExtension(this.extensionId)!.packageJSON; readonly globalStorageUri: vscode.Uri; constructor(ctx: vscode.ExtensionContext) { this.globalStorageUri = ctx.globalStorageUri; vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, ctx.subscriptions); this.refreshLogging(); } private refreshLogging() { log.setEnabled(this.traceExtension); log.info("Extension version:", this.package.version); const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function)); log.info("Using configuration", Object.fromEntries(cfg)); } private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) { this.refreshLogging(); const requiresReloadOpt = this.requiresReloadOpts.find( opt => event.affectsConfiguration(opt) ); if (!requiresReloadOpt) return; const userResponse = await vscode.window.showInformationMessage( `Changing "${requiresReloadOpt}" requires a reload`, "Reload now" ); if (userResponse === "Reload now") { await vscode.commands.executeCommand("workbench.action.reloadWindow"); } } // 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 { return vscode.workspace.getConfiguration(this.rootSection); } /** * 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(path)!; * * // What happens is that type of `nullableNum` is `number` but not `null | number`: * const fullFledgedNum: number = nullableNum; * ``` * So this getter handles this quirk by not requiring the caller to use postfix `!` */ private get(path: string): T { return this.cfg.get(path)!; } get serverPath() { return this.get("server.path") ?? this.get("serverPath"); } get serverExtraEnv() { return this.get("server.extraEnv") ?? {}; } get traceExtension() { return this.get("trace.extension"); } get cargoRunner() { return this.get("cargoRunner"); } get runnableEnv() { return this.get("runnableEnv"); } get debug() { let sourceFileMap = this.get | "auto">("debug.sourceFileMap"); if (sourceFileMap !== "auto") { // "/rustc/" used by suggestions only. const { ["/rustc/"]: _, ...trimmed } = this.get>("debug.sourceFileMap"); sourceFileMap = trimmed; } return { engine: this.get("debug.engine"), engineSettings: this.get("debug.engineSettings"), openDebugPane: this.get("debug.openDebugPane"), sourceFileMap: sourceFileMap }; } get hoverActions() { return { enable: this.get("hoverActions.enable"), implementations: this.get("hoverActions.implementations.enable"), references: this.get("hoverActions.references.enable"), run: this.get("hoverActions.run.enable"), debug: this.get("hoverActions.debug.enable"), gotoTypeDef: this.get("hoverActions.gotoTypeDef.enable"), }; } get currentExtensionIsNightly() { return this.package.releaseTag === NIGHTLY_TAG; } } export async function updateConfig(config: vscode.WorkspaceConfiguration) { const renames = [ ["assist.allowMergingIntoGlobImports", "imports.merge.glob",], ["assist.exprFillDefault", "assist.expressionFillDefault",], ["assist.importEnforceGranularity", "imports.granularity.enforce",], ["assist.importGranularity", "imports.granularity.group",], ["assist.importMergeBehavior", "imports.granularity.group",], ["assist.importMergeBehaviour", "imports.granularity.group",], ["assist.importGroup", "imports.group.enable",], ["assist.importPrefix", "imports.prefix",], ["primeCaches.enable", "cachePriming.enable",], ["cache.warmup", "cachePriming.enable",], ["cargo.loadOutDirsFromCheck", "cargo.buildScripts.enable",], ["cargo.runBuildScripts", "cargo.buildScripts.enable",], ["cargo.runBuildScriptsCommand", "cargo.buildScripts.overrideCommand",], ["cargo.useRustcWrapperForBuildScripts", "cargo.buildScripts.useRustcWrapper",], ["completion.snippets", "completion.snippets.custom",], ["diagnostics.enableExperimental", "diagnostics.experimental.enable",], ["experimental.procAttrMacros", "procMacro.attributes.enable",], ["highlighting.strings", "semanticHighlighting.strings.enable",], ["highlightRelated.breakPoints", "highlightRelated.breakPoints.enable",], ["highlightRelated.exitPoints", "highlightRelated.exitPoints.enable",], ["highlightRelated.yieldPoints", "highlightRelated.yieldPoints.enable",], ["highlightRelated.references", "highlightRelated.references.enable",], ["hover.documentation", "hover.documentation.enable",], ["hover.linksInHover", "hover.links.enable",], ["hoverActions.linksInHover", "hover.links.enable",], ["hoverActions.debug", "hoverActions.debug.enable",], ["hoverActions.enable", "hoverActions.enable.enable",], ["hoverActions.gotoTypeDef", "hoverActions.gotoTypeDef.enable",], ["hoverActions.implementations", "hoverActions.implementations.enable",], ["hoverActions.references", "hoverActions.references.enable",], ["hoverActions.run", "hoverActions.run.enable",], ["inlayHints.chainingHints", "inlayHints.chainingHints.enable",], ["inlayHints.closureReturnTypeHints", "inlayHints.closureReturnTypeHints.enable",], ["inlayHints.hideNamedConstructorHints", "inlayHints.typeHints.hideNamedConstructorHints",], ["inlayHints.parameterHints", "inlayHints.parameterHints.enable",], ["inlayHints.reborrowHints", "inlayHints.reborrowHints.enable",], ["inlayHints.typeHints", "inlayHints.typeHints.enable",], ["lruCapacity", "lru.capacity",], ["runnables.cargoExtraArgs", "runnables.extraArgs",], ["runnables.overrideCargo", "runnables.command",], ["rustcSource", "rustc.source",], ["rustfmt.enableRangeFormatting", "rustfmt.rangeFormatting.enable"] ]; for (const [oldKey, newKey] of renames) { const inspect = config.inspect(oldKey); if (inspect !== undefined) { const valMatrix = [ { val: inspect.globalValue, langVal: inspect.globalLanguageValue, target: vscode.ConfigurationTarget.Global }, { val: inspect.workspaceFolderValue, langVal: inspect.workspaceFolderLanguageValue, target: vscode.ConfigurationTarget.WorkspaceFolder }, { val: inspect.workspaceValue, langVal: inspect.workspaceLanguageValue, target: vscode.ConfigurationTarget.Workspace } ]; for (const { val, langVal, target } of valMatrix) { const pred = (val: unknown) => { // some of the updates we do only append "enable" or "custom" // that means on the next run we would find these again, but as objects with // these properties causing us to destroy the config // so filter those already updated ones out return val !== undefined && !(typeof val === "object" && val !== null && (val.hasOwnProperty("enable") || val.hasOwnProperty("custom"))); }; if (pred(val)) { await config.update(newKey, val, target, false); await config.update(oldKey, undefined, target, false); } if (pred(langVal)) { await config.update(newKey, langVal, target, true); await config.update(oldKey, undefined, target, true); } } } } } export function substituteVariablesInEnv(env: Env): Env { const missingDeps = new Set(); // vscode uses `env:ENV_NAME` for env vars resolution, and it's easier // to follow the same convention for our dependency tracking const definedEnvKeys = new Set(Object.keys(env).map(key => `env:${key}`)); const envWithDeps = Object.fromEntries(Object.entries(env).map(([key, value]) => { const deps = new Set(); const depRe = new RegExp(/\${(?.+?)}/g); let match = undefined; while ((match = depRe.exec(value))) { const depName = match.groups!.depName; deps.add(depName); // `depName` at this point can have a form of `expression` or // `prefix:expression` if (!definedEnvKeys.has(depName)) { missingDeps.add(depName); } } return [`env:${key}`, { deps: [...deps], value }]; })); const resolved = new Set(); for (const dep of missingDeps) { const match = /(?.*?):(?.+)/.exec(dep); if (match) { const { prefix, body } = match.groups!; if (prefix === 'env') { const envName = body; envWithDeps[dep] = { value: process.env[envName] ?? '', deps: [] }; resolved.add(dep); } else { // we can't handle other prefixes at the moment // leave values as is, but still mark them as resolved envWithDeps[dep] = { value: '${' + dep + '}', deps: [] }; resolved.add(dep); } } else { envWithDeps[dep] = { value: computeVscodeVar(dep), deps: [] }; } } const toResolve = new Set(Object.keys(envWithDeps)); let leftToResolveSize; do { leftToResolveSize = toResolve.size; for (const key of toResolve) { if (envWithDeps[key].deps.every(dep => resolved.has(dep))) { envWithDeps[key].value = envWithDeps[key].value.replace( /\${(?.+?)}/g, (_wholeMatch, depName) => { return envWithDeps[depName].value; }); resolved.add(key); toResolve.delete(key); } } } while (toResolve.size > 0 && toResolve.size < leftToResolveSize); const resolvedEnv: Env = {}; for (const key of Object.keys(env)) { resolvedEnv[key] = envWithDeps[`env:${key}`].value; } return resolvedEnv; } function computeVscodeVar(varName: string): string { // https://code.visualstudio.com/docs/editor/variables-reference const supportedVariables: { [k: string]: () => string } = { workspaceFolder: () => { const folders = vscode.workspace.workspaceFolders ?? []; if (folders.length === 1) { // TODO: support for remote workspaces? return folders[0].uri.fsPath; } else if (folders.length > 1) { // could use currently opened document to detect the correct // workspace. However, that would be determined by the document // user has opened on Editor startup. Could lead to // unpredictable workspace selection in practice. // It's better to pick the first one return folders[0].uri.fsPath; } else { // no workspace opened return ''; } }, workspaceFolderBasename: () => { const workspaceFolder = computeVscodeVar('workspaceFolder'); if (workspaceFolder) { return path.basename(workspaceFolder); } else { return ''; } }, cwd: () => process.cwd(), // see // https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81 // or // https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56 execPath: () => process.env.VSCODE_EXEC_PATH ?? process.execPath, pathSeparator: () => path.sep }; if (varName in supportedVariables) { return supportedVariables[varName](); } else { // can't resolve, keep the expression as is return '${' + varName + '}'; } }