1 import path = require("path");
2 import * as vscode from "vscode";
3 import { Env } from "./client";
4 import { log } from "./util";
6 export type RunnableEnvCfg =
8 | Record<string, string>
9 | { mask?: string; env: Record<string, string> }[];
12 readonly extensionId = "rust-lang.rust-analyzer";
14 readonly rootSection = "rust-analyzer";
15 private readonly requiresWorkspaceReloadOpts = [
18 // FIXME: This shouldn't be here, changing this setting should reload
19 // `continueCommentsOnNewline` behavior without restart
21 ].map((opt) => `${this.rootSection}.${opt}`);
22 private readonly requiresReloadOpts = [
26 "lens", // works as lens.*
28 .map((opt) => `${this.rootSection}.${opt}`)
29 .concat(this.requiresWorkspaceReloadOpts);
33 releaseTag: string | null;
34 enableProposedApi: boolean | undefined;
35 } = vscode.extensions.getExtension(this.extensionId)!.packageJSON;
37 readonly globalStorageUri: vscode.Uri;
39 constructor(ctx: vscode.ExtensionContext) {
40 this.globalStorageUri = ctx.globalStorageUri;
41 vscode.workspace.onDidChangeConfiguration(
42 this.onDidChangeConfiguration,
46 this.refreshLogging();
49 private refreshLogging() {
50 log.setEnabled(this.traceExtension);
51 log.info("Extension version:", this.package.version);
53 const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function));
54 log.info("Using configuration", Object.fromEntries(cfg));
57 private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) {
58 this.refreshLogging();
60 const requiresReloadOpt = this.requiresReloadOpts.find((opt) =>
61 event.affectsConfiguration(opt)
64 if (!requiresReloadOpt) return;
66 const requiresWorkspaceReloadOpt = this.requiresWorkspaceReloadOpts.find((opt) =>
67 event.affectsConfiguration(opt)
70 if (!requiresWorkspaceReloadOpt && this.restartServerOnConfigChange) {
71 await vscode.commands.executeCommand("rust-analyzer.reload");
75 const message = requiresWorkspaceReloadOpt
76 ? `Changing "${requiresWorkspaceReloadOpt}" requires a window reload`
77 : `Changing "${requiresReloadOpt}" requires a reload`;
78 const userResponse = await vscode.window.showInformationMessage(message, "Reload now");
80 if (userResponse === "Reload now") {
81 const command = requiresWorkspaceReloadOpt
82 ? "workbench.action.reloadWindow"
83 : "rust-analyzer.reload";
84 if (userResponse === "Reload now") {
85 await vscode.commands.executeCommand(command);
90 // We don't do runtime config validation here for simplicity. More on stackoverflow:
91 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
93 private get cfg(): vscode.WorkspaceConfiguration {
94 return vscode.workspace.getConfiguration(this.rootSection);
98 * Beware that postfix `!` operator erases both `null` and `undefined`.
99 * This is why the following doesn't work as expected:
102 * const nullableNum = vscode
105 * .getConfiguration("rust-analyzer")
106 * .get<number | null>(path)!;
108 * // What happens is that type of `nullableNum` is `number` but not `null | number`:
109 * const fullFledgedNum: number = nullableNum;
111 * So this getter handles this quirk by not requiring the caller to use postfix `!`
113 private get<T>(path: string): T {
114 return this.cfg.get<T>(path)!;
118 return this.get<null | string>("server.path") ?? this.get<null | string>("serverPath");
120 get serverExtraEnv(): Env {
122 this.get<{ [key: string]: string | number } | null>("server.extraEnv") ?? {};
123 return Object.fromEntries(
124 Object.entries(extraEnv).map(([k, v]) => [k, typeof v !== "string" ? v.toString() : v])
127 get traceExtension() {
128 return this.get<boolean>("trace.extension");
132 return this.get<string | undefined>("cargoRunner");
136 const item = this.get<any>("runnableEnv");
137 if (!item) return item;
138 const fixRecord = (r: Record<string, any>) => {
139 for (const key in r) {
140 if (typeof r[key] !== "string") {
141 r[key] = String(r[key]);
145 if (item instanceof Array) {
146 item.forEach((x) => fixRecord(x.env));
153 get restartServerOnConfigChange() {
154 return this.get<boolean>("restartServerOnConfigChange");
157 get typingContinueCommentsOnNewline() {
158 return this.get<boolean>("typing.continueCommentsOnNewline");
162 let sourceFileMap = this.get<Record<string, string> | "auto">("debug.sourceFileMap");
163 if (sourceFileMap !== "auto") {
164 // "/rustc/<id>" used by suggestions only.
165 const { ["/rustc/<id>"]: _, ...trimmed } =
166 this.get<Record<string, string>>("debug.sourceFileMap");
167 sourceFileMap = trimmed;
171 engine: this.get<string>("debug.engine"),
172 engineSettings: this.get<object>("debug.engineSettings"),
173 openDebugPane: this.get<boolean>("debug.openDebugPane"),
174 sourceFileMap: sourceFileMap,
180 enable: this.get<boolean>("hover.actions.enable"),
181 implementations: this.get<boolean>("hover.actions.implementations.enable"),
182 references: this.get<boolean>("hover.actions.references.enable"),
183 run: this.get<boolean>("hover.actions.run.enable"),
184 debug: this.get<boolean>("hover.actions.debug.enable"),
185 gotoTypeDef: this.get<boolean>("hover.actions.gotoTypeDef.enable"),
190 export function substituteVariablesInEnv(env: Env): Env {
191 const missingDeps = new Set<string>();
192 // vscode uses `env:ENV_NAME` for env vars resolution, and it's easier
193 // to follow the same convention for our dependency tracking
194 const definedEnvKeys = new Set(Object.keys(env).map((key) => `env:${key}`));
195 const envWithDeps = Object.fromEntries(
196 Object.entries(env).map(([key, value]) => {
197 const deps = new Set<string>();
198 const depRe = new RegExp(/\${(?<depName>.+?)}/g);
199 let match = undefined;
200 while ((match = depRe.exec(value))) {
201 const depName = match.groups!.depName;
203 // `depName` at this point can have a form of `expression` or
204 // `prefix:expression`
205 if (!definedEnvKeys.has(depName)) {
206 missingDeps.add(depName);
209 return [`env:${key}`, { deps: [...deps], value }];
213 const resolved = new Set<string>();
214 for (const dep of missingDeps) {
215 const match = /(?<prefix>.*?):(?<body>.+)/.exec(dep);
217 const { prefix, body } = match.groups!;
218 if (prefix === "env") {
219 const envName = body;
221 value: process.env[envName] ?? "",
226 // we can't handle other prefixes at the moment
227 // leave values as is, but still mark them as resolved
229 value: "${" + dep + "}",
236 value: computeVscodeVar(dep),
241 const toResolve = new Set(Object.keys(envWithDeps));
243 let leftToResolveSize;
245 leftToResolveSize = toResolve.size;
246 for (const key of toResolve) {
247 if (envWithDeps[key].deps.every((dep) => resolved.has(dep))) {
248 envWithDeps[key].value = envWithDeps[key].value.replace(
249 /\${(?<depName>.+?)}/g,
250 (_wholeMatch, depName) => {
251 return envWithDeps[depName].value;
255 toResolve.delete(key);
258 } while (toResolve.size > 0 && toResolve.size < leftToResolveSize);
260 const resolvedEnv: Env = {};
261 for (const key of Object.keys(env)) {
262 resolvedEnv[key] = envWithDeps[`env:${key}`].value;
267 function computeVscodeVar(varName: string): string {
268 // https://code.visualstudio.com/docs/editor/variables-reference
269 const supportedVariables: { [k: string]: () => string } = {
270 workspaceFolder: () => {
271 const folders = vscode.workspace.workspaceFolders ?? [];
272 if (folders.length === 1) {
273 // TODO: support for remote workspaces?
274 return folders[0].uri.fsPath;
275 } else if (folders.length > 1) {
276 // could use currently opened document to detect the correct
277 // workspace. However, that would be determined by the document
278 // user has opened on Editor startup. Could lead to
279 // unpredictable workspace selection in practice.
280 // It's better to pick the first one
281 return folders[0].uri.fsPath;
283 // no workspace opened
288 workspaceFolderBasename: () => {
289 const workspaceFolder = computeVscodeVar("workspaceFolder");
290 if (workspaceFolder) {
291 return path.basename(workspaceFolder);
297 cwd: () => process.cwd(),
300 // https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81
302 // https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56
303 execPath: () => process.env.VSCODE_EXEC_PATH ?? process.execPath,
305 pathSeparator: () => path.sep,
308 if (varName in supportedVariables) {
309 return supportedVariables[varName]();
311 // can't resolve, keep the expression as is
312 return "${" + varName + "}";