Auto merge of #12215 - listochkin:Support-variable-substitution-in-vscode-settings, r=Veykril

feat: Support variable substitution in VSCode settings

Currently support a subset of [variables provided by VSCode](https://code.visualstudio.com/docs/editor/variables-reference) in `server.extraEnv` section of Rust-Analyzer settings:

  * `workspaceFolder`
  * `workspaceFolderBasename`
  * `cwd`
  * `execPath`
  * `pathSeparator`

Also, this PR adds support for general environment variables resolution. You can declare environment variables and reference them from other variables like this:

```JSON
"rust-analyzer.server.extraEnv": {
    "RUSTFLAGS": "-L${env:OPEN_XR_SDK_PATH}",
    "OPEN_XR_SDK_PATH": "${workspaceFolder}\\..\\OpenXR-SDK\\build\\src\\loader\\Release"
},
```
The order of variable declaration doesn't matter, you can reference variables before defining them. If the variable is not present in `extraEnv` section, VSCode will search for them in your environment. Missing variables will be replaced with empty string. Circular references won't be resolved and will be passed to rust-analyzer server process as is.

Closes #9626, but doesn't address use cases where people want to use values provided by `rustc` or `cargo`, such as `${targetTriple}` proposal #11649
This commit is contained in:
bors 2022-05-12 11:05:21 +00:00
commit 927ef0ce7e
7 changed files with 225 additions and 9 deletions

View file

@ -1,3 +1,4 @@
import path = require('path');
import * as vscode from 'vscode';
import { Env } from './client';
import { log } from "./util";
@ -210,3 +211,125 @@ export async function updateConfig(config: vscode.WorkspaceConfiguration) {
}
}
}
export function substituteVariablesInEnv(env: Env): Env {
const missingDeps = new Set<string>();
// 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<string>();
const depRe = new RegExp(/\${(?<depName>.+?)}/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<string>();
for (const dep of missingDeps) {
const match = /(?<prefix>.*?):(?<body>.+)/.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(
/\${(?<depName>.+?)}/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 + '}';
}
}