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.