1 import path = require('path');
2 import * as vscode from 'vscode';
3 import { Env } from './client';
4 import { log } from "./util";
6 export type UpdatesChannel = "stable" | "nightly";
8 const NIGHTLY_TAG = "nightly";
10 export type RunnableEnvCfg = undefined | Record<string, string> | { mask?: string; env: Record<string, string> }[];
13 readonly extensionId = "matklad.rust-analyzer";
15 readonly rootSection = "rust-analyzer";
16 private readonly requiresReloadOpts = [
22 "lens", // works as lens.*
24 .map(opt => `${this.rootSection}.${opt}`);
28 releaseTag: string | null;
29 enableProposedApi: boolean | undefined;
30 } = vscode.extensions.getExtension(this.extensionId)!.packageJSON;
32 readonly globalStorageUri: vscode.Uri;
34 constructor(ctx: vscode.ExtensionContext) {
35 this.globalStorageUri = ctx.globalStorageUri;
36 vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, ctx.subscriptions);
37 this.refreshLogging();
40 private refreshLogging() {
41 log.setEnabled(this.traceExtension);
42 log.info("Extension version:", this.package.version);
44 const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function));
45 log.info("Using configuration", Object.fromEntries(cfg));
48 private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) {
49 this.refreshLogging();
51 const requiresReloadOpt = this.requiresReloadOpts.find(
52 opt => event.affectsConfiguration(opt)
55 if (!requiresReloadOpt) return;
57 const userResponse = await vscode.window.showInformationMessage(
58 `Changing "${requiresReloadOpt}" requires a reload`,
62 if (userResponse === "Reload now") {
63 await vscode.commands.executeCommand("workbench.action.reloadWindow");
67 // We don't do runtime config validation here for simplicity. More on stackoverflow:
68 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
70 private get cfg(): vscode.WorkspaceConfiguration {
71 return vscode.workspace.getConfiguration(this.rootSection);
75 * Beware that postfix `!` operator erases both `null` and `undefined`.
76 * This is why the following doesn't work as expected:
79 * const nullableNum = vscode
82 * .getConfiguration("rust-analyzer")
83 * .get<number | null>(path)!;
85 * // What happens is that type of `nullableNum` is `number` but not `null | number`:
86 * const fullFledgedNum: number = nullableNum;
88 * So this getter handles this quirk by not requiring the caller to use postfix `!`
90 private get<T>(path: string): T {
91 return this.cfg.get<T>(path)!;
95 return this.get<null | string>("server.path") ?? this.get<null | string>("serverPath");
97 get serverExtraEnv() { return this.get<Env | null>("server.extraEnv") ?? {}; }
98 get traceExtension() { return this.get<boolean>("trace.extension"); }
101 return this.get<string | undefined>("cargoRunner");
105 return this.get<RunnableEnvCfg>("runnableEnv");
109 let sourceFileMap = this.get<Record<string, string> | "auto">("debug.sourceFileMap");
110 if (sourceFileMap !== "auto") {
111 // "/rustc/<id>" used by suggestions only.
112 const { ["/rustc/<id>"]: _, ...trimmed } = this.get<Record<string, string>>("debug.sourceFileMap");
113 sourceFileMap = trimmed;
117 engine: this.get<string>("debug.engine"),
118 engineSettings: this.get<object>("debug.engineSettings"),
119 openDebugPane: this.get<boolean>("debug.openDebugPane"),
120 sourceFileMap: sourceFileMap
126 enable: this.get<boolean>("hoverActions.enable"),
127 implementations: this.get<boolean>("hoverActions.implementations.enable"),
128 references: this.get<boolean>("hoverActions.references.enable"),
129 run: this.get<boolean>("hoverActions.run.enable"),
130 debug: this.get<boolean>("hoverActions.debug.enable"),
131 gotoTypeDef: this.get<boolean>("hoverActions.gotoTypeDef.enable"),
135 get currentExtensionIsNightly() {
136 return this.package.releaseTag === NIGHTLY_TAG;
140 export async function updateConfig(config: vscode.WorkspaceConfiguration) {
142 ["assist.allowMergingIntoGlobImports", "imports.merge.glob",],
143 ["assist.exprFillDefault", "assist.expressionFillDefault",],
144 ["assist.importEnforceGranularity", "imports.granularity.enforce",],
145 ["assist.importGranularity", "imports.granularity.group",],
146 ["assist.importMergeBehavior", "imports.granularity.group",],
147 ["assist.importMergeBehaviour", "imports.granularity.group",],
148 ["assist.importGroup", "imports.group.enable",],
149 ["assist.importPrefix", "imports.prefix",],
150 ["cache.warmup", "primeCaches.enable",],
151 ["cargo.loadOutDirsFromCheck", "cargo.buildScripts.enable",],
152 ["cargo.runBuildScripts", "cargo.buildScripts.enable",],
153 ["cargo.runBuildScriptsCommand", "cargo.buildScripts.overrideCommand",],
154 ["cargo.useRustcWrapperForBuildScripts", "cargo.buildScripts.useRustcWrapper",],
155 ["completion.snippets", "completion.snippets.custom",],
156 ["diagnostics.enableExperimental", "diagnostics.experimental.enable",],
157 ["experimental.procAttrMacros", "procMacro.attributes.enable",],
158 ["highlighting.strings", "semanticHighlighting.strings.enable",],
159 ["highlightRelated.breakPoints", "highlightRelated.breakPoints.enable",],
160 ["highlightRelated.exitPoints", "highlightRelated.exitPoints.enable",],
161 ["highlightRelated.yieldPoints", "highlightRelated.yieldPoints.enable",],
162 ["highlightRelated.references", "highlightRelated.references.enable",],
163 ["hover.documentation", "hover.documentation.enable",],
164 ["hover.linksInHover", "hover.links.enable",],
165 ["hoverActions.linksInHover", "hover.links.enable",],
166 ["hoverActions.debug", "hoverActions.debug.enable",],
167 ["hoverActions.enable", "hoverActions.enable.enable",],
168 ["hoverActions.gotoTypeDef", "hoverActions.gotoTypeDef.enable",],
169 ["hoverActions.implementations", "hoverActions.implementations.enable",],
170 ["hoverActions.references", "hoverActions.references.enable",],
171 ["hoverActions.run", "hoverActions.run.enable",],
172 ["inlayHints.chainingHints", "inlayHints.chainingHints.enable",],
173 ["inlayHints.closureReturnTypeHints", "inlayHints.closureReturnTypeHints.enable",],
174 ["inlayHints.hideNamedConstructorHints", "inlayHints.typeHints.hideNamedConstructorHints",],
175 ["inlayHints.parameterHints", "inlayHints.parameterHints.enable",],
176 ["inlayHints.reborrowHints", "inlayHints.reborrowHints.enable",],
177 ["inlayHints.typeHints", "inlayHints.typeHints.enable",],
178 ["lruCapacity", "lru.capacity",],
179 ["runnables.cargoExtraArgs", "runnables.extraArgs",],
180 ["runnables.overrideCargo", "runnables.command",],
181 ["rustcSource", "rustc.source",],
182 ["rustfmt.enableRangeFormatting", "rustfmt.rangeFormatting.enable"]
185 for (const [oldKey, newKey] of renames) {
186 const inspect = config.inspect(oldKey);
187 if (inspect !== undefined) {
189 { val: inspect.globalValue, langVal: inspect.globalLanguageValue, target: vscode.ConfigurationTarget.Global },
190 { val: inspect.workspaceFolderValue, langVal: inspect.workspaceFolderLanguageValue, target: vscode.ConfigurationTarget.WorkspaceFolder },
191 { val: inspect.workspaceValue, langVal: inspect.workspaceLanguageValue, target: vscode.ConfigurationTarget.Workspace }
193 for (const { val, langVal, target } of valMatrix) {
194 const pred = (val: unknown) => {
195 // some of the updates we do only append "enable" or "custom"
196 // that means on the next run we would find these again, but as objects with
197 // these properties causing us to destroy the config
198 // so filter those already updated ones out
199 return val !== undefined && !(typeof val === "object" && val !== null && (val.hasOwnProperty("enable") || val.hasOwnProperty("custom")));
202 await config.update(newKey, val, target, false);
203 await config.update(oldKey, undefined, target, false);
206 await config.update(newKey, langVal, target, true);
207 await config.update(oldKey, undefined, target, true);
214 export function substituteVariablesInEnv(env: Env): Env {
215 const missingDeps = new Set<string>();
216 // vscode uses `env:ENV_NAME` for env vars resolution, and it's easier
217 // to follow the same convention for our dependency tracking
218 const definedEnvKeys = new Set(Object.keys(env).map(key => `env:${key}`));
219 const envWithDeps = Object.fromEntries(Object.entries(env).map(([key, value]) => {
220 const deps = new Set<string>();
221 const depRe = new RegExp(/\${(?<depName>.+?)}/g);
222 let match = undefined;
223 while ((match = depRe.exec(value))) {
224 const depName = match.groups!.depName;
226 // `depName` at this point can have a form of `expression` or
227 // `prefix:expression`
228 if (!definedEnvKeys.has(depName)) {
229 missingDeps.add(depName);
232 return [`env:${key}`, { deps: [...deps], value }];
235 const resolved = new Set<string>();
236 for (const dep of missingDeps) {
237 const match = /(?<prefix>.*?):(?<body>.+)/.exec(dep);
239 const { prefix, body } = match.groups!;
240 if (prefix === 'env') {
241 const envName = body;
243 value: process.env[envName] ?? '',
248 // we can't handle other prefixes at the moment
249 // leave values as is, but still mark them as resolved
251 value: '${' + dep + '}',
258 value: computeVscodeVar(dep),
263 const toResolve = new Set(Object.keys(envWithDeps));
265 let leftToResolveSize;
267 leftToResolveSize = toResolve.size;
268 for (const key of toResolve) {
269 if (envWithDeps[key].deps.every(dep => resolved.has(dep))) {
270 envWithDeps[key].value = envWithDeps[key].value.replace(
271 /\${(?<depName>.+?)}/g, (_wholeMatch, depName) => {
272 return envWithDeps[depName].value;
275 toResolve.delete(key);
278 } while (toResolve.size > 0 && toResolve.size < leftToResolveSize);
280 const resolvedEnv: Env = {};
281 for (const key of Object.keys(env)) {
282 resolvedEnv[key] = envWithDeps[`env:${key}`].value;
287 function computeVscodeVar(varName: string): string {
288 // https://code.visualstudio.com/docs/editor/variables-reference
289 const supportedVariables: { [k: string]: () => string } = {
290 workspaceFolder: () => {
291 const folders = vscode.workspace.workspaceFolders ?? [];
292 if (folders.length === 1) {
293 // TODO: support for remote workspaces?
294 return folders[0].uri.fsPath;
295 } else if (folders.length > 1) {
296 // could use currently opened document to detect the correct
297 // workspace. However, that would be determined by the document
298 // user has opened on Editor startup. Could lead to
299 // unpredictable workspace selection in practice.
300 // It's better to pick the first one
301 return folders[0].uri.fsPath;
303 // no workspace opened
308 workspaceFolderBasename: () => {
309 const workspaceFolder = computeVscodeVar('workspaceFolder');
310 if (workspaceFolder) {
311 return path.basename(workspaceFolder);
317 cwd: () => process.cwd(),
320 // https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81
322 // https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56
323 execPath: () => process.env.VSCODE_EXEC_PATH ?? process.execPath,
325 pathSeparator: () => path.sep
328 if (varName in supportedVariables) {
329 return supportedVariables[varName]();
331 // can't resolve, keep the expression as is
332 return '${' + varName + '}';