]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/editors/code/src/config.ts
15846a5e8645e67fd6d2888a81b4b1ca862efefc
[rust.git] / src / tools / rust-analyzer / editors / code / src / config.ts
1 import path = require("path");
2 import * as vscode from "vscode";
3 import { Env } from "./client";
4 import { log } from "./util";
5
6 export type RunnableEnvCfg =
7     | undefined
8     | Record<string, string>
9     | { mask?: string; env: Record<string, string> }[];
10
11 export class Config {
12     readonly extensionId = "rust-lang.rust-analyzer";
13
14     readonly rootSection = "rust-analyzer";
15     private readonly requiresWorkspaceReloadOpts = [
16         "serverPath",
17         "server",
18         // FIXME: This shouldn't be here, changing this setting should reload
19         // `continueCommentsOnNewline` behavior without restart
20         "typing",
21     ].map((opt) => `${this.rootSection}.${opt}`);
22     private readonly requiresReloadOpts = [
23         "cargo",
24         "procMacro",
25         "files",
26         "lens", // works as lens.*
27     ]
28         .map((opt) => `${this.rootSection}.${opt}`)
29         .concat(this.requiresWorkspaceReloadOpts);
30
31     readonly package: {
32         version: string;
33         releaseTag: string | null;
34         enableProposedApi: boolean | undefined;
35     } = vscode.extensions.getExtension(this.extensionId)!.packageJSON;
36
37     readonly globalStorageUri: vscode.Uri;
38
39     constructor(ctx: vscode.ExtensionContext) {
40         this.globalStorageUri = ctx.globalStorageUri;
41         vscode.workspace.onDidChangeConfiguration(
42             this.onDidChangeConfiguration,
43             this,
44             ctx.subscriptions
45         );
46         this.refreshLogging();
47     }
48
49     private refreshLogging() {
50         log.setEnabled(this.traceExtension);
51         log.info("Extension version:", this.package.version);
52
53         const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function));
54         log.info("Using configuration", Object.fromEntries(cfg));
55     }
56
57     private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) {
58         this.refreshLogging();
59
60         const requiresReloadOpt = this.requiresReloadOpts.find((opt) =>
61             event.affectsConfiguration(opt)
62         );
63
64         if (!requiresReloadOpt) return;
65
66         const requiresWorkspaceReloadOpt = this.requiresWorkspaceReloadOpts.find((opt) =>
67             event.affectsConfiguration(opt)
68         );
69
70         if (!requiresWorkspaceReloadOpt && this.restartServerOnConfigChange) {
71             await vscode.commands.executeCommand("rust-analyzer.reload");
72             return;
73         }
74
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");
79
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);
86             }
87         }
88     }
89
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
92
93     private get cfg(): vscode.WorkspaceConfiguration {
94         return vscode.workspace.getConfiguration(this.rootSection);
95     }
96
97     /**
98      * Beware that postfix `!` operator erases both `null` and `undefined`.
99      * This is why the following doesn't work as expected:
100      *
101      * ```ts
102      * const nullableNum = vscode
103      *  .workspace
104      *  .getConfiguration
105      *  .getConfiguration("rust-analyzer")
106      *  .get<number | null>(path)!;
107      *
108      * // What happens is that type of `nullableNum` is `number` but not `null | number`:
109      * const fullFledgedNum: number = nullableNum;
110      * ```
111      * So this getter handles this quirk by not requiring the caller to use postfix `!`
112      */
113     private get<T>(path: string): T {
114         return this.cfg.get<T>(path)!;
115     }
116
117     get serverPath() {
118         return this.get<null | string>("server.path") ?? this.get<null | string>("serverPath");
119     }
120     get serverExtraEnv(): Env {
121         const extraEnv =
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])
125         );
126     }
127     get traceExtension() {
128         return this.get<boolean>("trace.extension");
129     }
130
131     get cargoRunner() {
132         return this.get<string | undefined>("cargoRunner");
133     }
134
135     get runnableEnv() {
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]);
142                 }
143             }
144         };
145         if (item instanceof Array) {
146             item.forEach((x) => fixRecord(x.env));
147         } else {
148             fixRecord(item);
149         }
150         return item;
151     }
152
153     get restartServerOnConfigChange() {
154         return this.get<boolean>("restartServerOnConfigChange");
155     }
156
157     get typingContinueCommentsOnNewline() {
158         return this.get<boolean>("typing.continueCommentsOnNewline");
159     }
160
161     get debug() {
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;
168         }
169
170         return {
171             engine: this.get<string>("debug.engine"),
172             engineSettings: this.get<object>("debug.engineSettings"),
173             openDebugPane: this.get<boolean>("debug.openDebugPane"),
174             sourceFileMap: sourceFileMap,
175         };
176     }
177
178     get hoverActions() {
179         return {
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"),
186         };
187     }
188 }
189
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;
202                 deps.add(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);
207                 }
208             }
209             return [`env:${key}`, { deps: [...deps], value }];
210         })
211     );
212
213     const resolved = new Set<string>();
214     for (const dep of missingDeps) {
215         const match = /(?<prefix>.*?):(?<body>.+)/.exec(dep);
216         if (match) {
217             const { prefix, body } = match.groups!;
218             if (prefix === "env") {
219                 const envName = body;
220                 envWithDeps[dep] = {
221                     value: process.env[envName] ?? "",
222                     deps: [],
223                 };
224                 resolved.add(dep);
225             } else {
226                 // we can't handle other prefixes at the moment
227                 // leave values as is, but still mark them as resolved
228                 envWithDeps[dep] = {
229                     value: "${" + dep + "}",
230                     deps: [],
231                 };
232                 resolved.add(dep);
233             }
234         } else {
235             envWithDeps[dep] = {
236                 value: computeVscodeVar(dep),
237                 deps: [],
238             };
239         }
240     }
241     const toResolve = new Set(Object.keys(envWithDeps));
242
243     let leftToResolveSize;
244     do {
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;
252                     }
253                 );
254                 resolved.add(key);
255                 toResolve.delete(key);
256             }
257         }
258     } while (toResolve.size > 0 && toResolve.size < leftToResolveSize);
259
260     const resolvedEnv: Env = {};
261     for (const key of Object.keys(env)) {
262         resolvedEnv[key] = envWithDeps[`env:${key}`].value;
263     }
264     return resolvedEnv;
265 }
266
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;
282             } else {
283                 // no workspace opened
284                 return "";
285             }
286         },
287
288         workspaceFolderBasename: () => {
289             const workspaceFolder = computeVscodeVar("workspaceFolder");
290             if (workspaceFolder) {
291                 return path.basename(workspaceFolder);
292             } else {
293                 return "";
294             }
295         },
296
297         cwd: () => process.cwd(),
298
299         // see
300         // https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81
301         // or
302         // https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56
303         execPath: () => process.env.VSCODE_EXEC_PATH ?? process.execPath,
304
305         pathSeparator: () => path.sep,
306     };
307
308     if (varName in supportedVariables) {
309         return supportedVariables[varName]();
310     } else {
311         // can't resolve, keep the expression as is
312         return "${" + varName + "}";
313     }
314 }