aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-06-02 12:58:28 +0100
committerGitHub <[email protected]>2020-06-02 12:58:28 +0100
commit131ccd95403e10131d6b8dfcad60ebc49c257c53 (patch)
tree3c7d02c26198f71bcb9d018b57024090c1ffa22c
parent21132a7a748a70351f5d1ae6d8c51a3a0065013e (diff)
parentd605ec9c321392d9c7ee4b440c560e1e405d92e6 (diff)
Merge #4580
4580: Fix invoking cargo without consulting CARGO env var or standard installation paths r=matklad a=Veetaha Followup for #4329 The pr essentially fixes [this bug](https://youtu.be/EzQ7YIIo1rY?t=2189) cc @lefticus Co-authored-by: veetaha <[email protected]>
-rw-r--r--Cargo.lock1
-rw-r--r--crates/ra_proc_macro_srv/Cargo.toml1
-rw-r--r--crates/ra_proc_macro_srv/src/tests/utils.rs3
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs11
-rw-r--r--crates/rust-analyzer/src/main_loop/handlers.rs7
-rw-r--r--crates/rust-analyzer/tests/heavy_tests/main.rs10
-rw-r--r--docs/dev/lsp-extensions.md2
-rw-r--r--editors/code/src/debug.ts2
-rw-r--r--editors/code/src/lsp_ext.ts5
-rw-r--r--editors/code/src/run.ts3
-rw-r--r--editors/code/src/tasks.ts7
-rw-r--r--editors/code/src/toolchain.ts (renamed from editors/code/src/cargo.ts)117
-rw-r--r--editors/code/src/util.ts18
-rw-r--r--editors/code/tests/unit/launch_config.test.ts14
14 files changed, 130 insertions, 71 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9981a2e33..af27bfc85 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1119,6 +1119,7 @@ dependencies = [
1119 "memmap", 1119 "memmap",
1120 "ra_mbe", 1120 "ra_mbe",
1121 "ra_proc_macro", 1121 "ra_proc_macro",
1122 "ra_toolchain",
1122 "ra_tt", 1123 "ra_tt",
1123 "serde_derive", 1124 "serde_derive",
1124 "test_utils", 1125 "test_utils",
diff --git a/crates/ra_proc_macro_srv/Cargo.toml b/crates/ra_proc_macro_srv/Cargo.toml
index bb3003278..582102945 100644
--- a/crates/ra_proc_macro_srv/Cargo.toml
+++ b/crates/ra_proc_macro_srv/Cargo.toml
@@ -22,3 +22,4 @@ cargo_metadata = "0.10.0"
22difference = "2.0.0" 22difference = "2.0.0"
23# used as proc macro test target 23# used as proc macro test target
24serde_derive = "1.0.106" 24serde_derive = "1.0.106"
25ra_toolchain = { path = "../ra_toolchain" }
diff --git a/crates/ra_proc_macro_srv/src/tests/utils.rs b/crates/ra_proc_macro_srv/src/tests/utils.rs
index 84348b5de..8d85f2d8a 100644
--- a/crates/ra_proc_macro_srv/src/tests/utils.rs
+++ b/crates/ra_proc_macro_srv/src/tests/utils.rs
@@ -2,7 +2,6 @@
2 2
3use crate::dylib; 3use crate::dylib;
4use crate::ProcMacroSrv; 4use crate::ProcMacroSrv;
5pub use difference::Changeset as __Changeset;
6use ra_proc_macro::ListMacrosTask; 5use ra_proc_macro::ListMacrosTask;
7use std::str::FromStr; 6use std::str::FromStr;
8use test_utils::assert_eq_text; 7use test_utils::assert_eq_text;
@@ -13,7 +12,7 @@ mod fixtures {
13 12
14 // Use current project metadata to get the proc-macro dylib path 13 // Use current project metadata to get the proc-macro dylib path
15 pub fn dylib_path(crate_name: &str, version: &str) -> std::path::PathBuf { 14 pub fn dylib_path(crate_name: &str, version: &str) -> std::path::PathBuf {
16 let command = Command::new("cargo") 15 let command = Command::new(ra_toolchain::cargo())
17 .args(&["check", "--message-format", "json"]) 16 .args(&["check", "--message-format", "json"])
18 .output() 17 .output()
19 .unwrap() 18 .unwrap()
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index acb1dacb6..173c23b9e 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -121,12 +121,21 @@ pub struct RunnablesParams {
121 pub position: Option<Position>, 121 pub position: Option<Position>,
122} 122}
123 123
124// Must strictly correspond to the executable name
125#[derive(Serialize, Deserialize, Debug)]
126#[serde(rename_all = "lowercase")]
127pub enum RunnableKind {
128 Cargo,
129 Rustc,
130 Rustup,
131}
132
124#[derive(Deserialize, Serialize, Debug)] 133#[derive(Deserialize, Serialize, Debug)]
125#[serde(rename_all = "camelCase")] 134#[serde(rename_all = "camelCase")]
126pub struct Runnable { 135pub struct Runnable {
127 pub range: Range, 136 pub range: Range,
128 pub label: String, 137 pub label: String,
129 pub bin: String, 138 pub kind: RunnableKind,
130 pub args: Vec<String>, 139 pub args: Vec<String>,
131 pub extra_args: Vec<String>, 140 pub extra_args: Vec<String>,
132 pub env: FxHashMap<String, String>, 141 pub env: FxHashMap<String, String>,
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index 1f910ff82..bc7c7f1ef 100644
--- a/crates/rust-analyzer/src/main_loop/handlers.rs
+++ b/crates/rust-analyzer/src/main_loop/handlers.rs
@@ -426,7 +426,7 @@ pub fn handle_runnables(
426 res.push(lsp_ext::Runnable { 426 res.push(lsp_ext::Runnable {
427 range: Default::default(), 427 range: Default::default(),
428 label: format!("cargo {} -p {}", cmd, spec.package), 428 label: format!("cargo {} -p {}", cmd, spec.package),
429 bin: "cargo".to_string(), 429 kind: lsp_ext::RunnableKind::Cargo,
430 args: vec![cmd.to_string(), "--package".to_string(), spec.package.clone()], 430 args: vec![cmd.to_string(), "--package".to_string(), spec.package.clone()],
431 extra_args: Vec::new(), 431 extra_args: Vec::new(),
432 env: FxHashMap::default(), 432 env: FxHashMap::default(),
@@ -438,7 +438,7 @@ pub fn handle_runnables(
438 res.push(lsp_ext::Runnable { 438 res.push(lsp_ext::Runnable {
439 range: Default::default(), 439 range: Default::default(),
440 label: "cargo check --workspace".to_string(), 440 label: "cargo check --workspace".to_string(),
441 bin: "cargo".to_string(), 441 kind: lsp_ext::RunnableKind::Cargo,
442 args: vec!["check".to_string(), "--workspace".to_string()], 442 args: vec!["check".to_string(), "--workspace".to_string()],
443 extra_args: Vec::new(), 443 extra_args: Vec::new(),
444 env: FxHashMap::default(), 444 env: FxHashMap::default(),
@@ -982,10 +982,11 @@ fn to_lsp_runnable(
982 target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t)) 982 target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t))
983 } 983 }
984 }; 984 };
985
985 Ok(lsp_ext::Runnable { 986 Ok(lsp_ext::Runnable {
986 range: to_proto::range(&line_index, runnable.range), 987 range: to_proto::range(&line_index, runnable.range),
987 label, 988 label,
988 bin: "cargo".to_string(), 989 kind: lsp_ext::RunnableKind::Cargo,
989 args, 990 args,
990 extra_args, 991 extra_args,
991 env: { 992 env: {
diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs
index 405ddb362..8b473ff74 100644
--- a/crates/rust-analyzer/tests/heavy_tests/main.rs
+++ b/crates/rust-analyzer/tests/heavy_tests/main.rs
@@ -79,7 +79,7 @@ fn foo() {
79 { 79 {
80 "args": [ "test" ], 80 "args": [ "test" ],
81 "extraArgs": [ "foo", "--nocapture" ], 81 "extraArgs": [ "foo", "--nocapture" ],
82 "bin": "cargo", 82 "kind": "cargo",
83 "env": { "RUST_BACKTRACE": "short" }, 83 "env": { "RUST_BACKTRACE": "short" },
84 "cwd": null, 84 "cwd": null,
85 "label": "test foo", 85 "label": "test foo",
@@ -91,7 +91,7 @@ fn foo() {
91 { 91 {
92 "args": ["check", "--workspace"], 92 "args": ["check", "--workspace"],
93 "extraArgs": [], 93 "extraArgs": [],
94 "bin": "cargo", 94 "kind": "cargo",
95 "env": {}, 95 "env": {},
96 "cwd": null, 96 "cwd": null,
97 "label": "cargo check --workspace", 97 "label": "cargo check --workspace",
@@ -141,7 +141,7 @@ fn main() {}
141 { 141 {
142 "args": [ "test", "--package", "foo", "--test", "spam" ], 142 "args": [ "test", "--package", "foo", "--test", "spam" ],
143 "extraArgs": [ "test_eggs", "--exact", "--nocapture" ], 143 "extraArgs": [ "test_eggs", "--exact", "--nocapture" ],
144 "bin": "cargo", 144 "kind": "cargo",
145 "env": { "RUST_BACKTRACE": "short" }, 145 "env": { "RUST_BACKTRACE": "short" },
146 "label": "test test_eggs", 146 "label": "test test_eggs",
147 "range": { 147 "range": {
@@ -153,7 +153,7 @@ fn main() {}
153 { 153 {
154 "args": [ "check", "--package", "foo" ], 154 "args": [ "check", "--package", "foo" ],
155 "extraArgs": [], 155 "extraArgs": [],
156 "bin": "cargo", 156 "kind": "cargo",
157 "env": {}, 157 "env": {},
158 "label": "cargo check -p foo", 158 "label": "cargo check -p foo",
159 "range": { 159 "range": {
@@ -165,7 +165,7 @@ fn main() {}
165 { 165 {
166 "args": [ "test", "--package", "foo" ], 166 "args": [ "test", "--package", "foo" ],
167 "extraArgs": [], 167 "extraArgs": [],
168 "bin": "cargo", 168 "kind": "cargo",
169 "env": {}, 169 "env": {},
170 "label": "cargo test -p foo", 170 "label": "cargo test -p foo",
171 "range": { 171 "range": {
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index c57a93f12..b7237ee90 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -427,7 +427,7 @@ interface Runnable {
427 /// The label to show in the UI. 427 /// The label to show in the UI.
428 label: string; 428 label: string;
429 /// The following fields describe a process to spawn. 429 /// The following fields describe a process to spawn.
430 bin: string; 430 kind: "cargo" | "rustc" | "rustup";
431 args: string[]; 431 args: string[];
432 /// Args for cargo after `--`. 432 /// Args for cargo after `--`.
433 extraArgs: string[]; 433 extraArgs: string[];
diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts
index 027504ecd..bdec5b735 100644
--- a/editors/code/src/debug.ts
+++ b/editors/code/src/debug.ts
@@ -3,7 +3,7 @@ import * as vscode from 'vscode';
3import * as path from 'path'; 3import * as path from 'path';
4import * as ra from './lsp_ext'; 4import * as ra from './lsp_ext';
5 5
6import { Cargo } from './cargo'; 6import { Cargo } from './toolchain';
7import { Ctx } from "./ctx"; 7import { Ctx } from "./ctx";
8 8
9const debugOutput = vscode.window.createOutputChannel("Debug"); 9const debugOutput = vscode.window.createOutputChannel("Debug");
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 4da12eb30..3e0b60699 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -45,10 +45,13 @@ export interface RunnablesParams {
45 textDocument: lc.TextDocumentIdentifier; 45 textDocument: lc.TextDocumentIdentifier;
46 position: lc.Position | null; 46 position: lc.Position | null;
47} 47}
48
49export type RunnableKind = "cargo" | "rustc" | "rustup";
50
48export interface Runnable { 51export interface Runnable {
49 range: lc.Range; 52 range: lc.Range;
50 label: string; 53 label: string;
51 bin: string; 54 kind: RunnableKind;
52 args: string[]; 55 args: string[];
53 extraArgs: string[]; 56 extraArgs: string[];
54 env: { [key: string]: string }; 57 env: { [key: string]: string };
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
index 8e1ba83ed..113354bab 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -1,6 +1,7 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
3import * as ra from './lsp_ext'; 3import * as ra from './lsp_ext';
4import * as toolchain from "./toolchain";
4 5
5import { Ctx, Cmd } from './ctx'; 6import { Ctx, Cmd } from './ctx';
6import { startDebugSession, getDebugConfiguration } from './debug'; 7import { startDebugSession, getDebugConfiguration } from './debug';
@@ -175,7 +176,7 @@ export function createTask(spec: ra.Runnable): vscode.Task {
175 const definition: CargoTaskDefinition = { 176 const definition: CargoTaskDefinition = {
176 type: 'cargo', 177 type: 'cargo',
177 label: spec.label, 178 label: spec.label,
178 command: spec.bin, 179 command: toolchain.getPathForExecutable(spec.kind),
179 args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args, 180 args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args,
180 env: Object.assign({}, process.env, spec.env), 181 env: Object.assign({}, process.env, spec.env),
181 }; 182 };
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts
index 1366c76d6..9748824df 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -1,4 +1,5 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as toolchain from "./toolchain";
2 3
3// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and 4// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and
4// our configuration should be compatible with it so use the same key. 5// our configuration should be compatible with it so use the same key.
@@ -24,6 +25,8 @@ class CargoTaskProvider implements vscode.TaskProvider {
24 // set of tasks that always exist. These tasks cannot be removed in 25 // set of tasks that always exist. These tasks cannot be removed in
25 // tasks.json - only tweaked. 26 // tasks.json - only tweaked.
26 27
28 const cargoPath = toolchain.cargoPath();
29
27 return [ 30 return [
28 { command: 'build', group: vscode.TaskGroup.Build }, 31 { command: 'build', group: vscode.TaskGroup.Build },
29 { command: 'check', group: vscode.TaskGroup.Build }, 32 { command: 'check', group: vscode.TaskGroup.Build },
@@ -46,7 +49,7 @@ class CargoTaskProvider implements vscode.TaskProvider {
46 `cargo ${command}`, 49 `cargo ${command}`,
47 'rust', 50 'rust',
48 // What to do when this command is executed. 51 // What to do when this command is executed.
49 new vscode.ShellExecution('cargo', [command]), 52 new vscode.ShellExecution(cargoPath, [command]),
50 // Problem matchers. 53 // Problem matchers.
51 ['$rustc'], 54 ['$rustc'],
52 ); 55 );
@@ -80,4 +83,4 @@ class CargoTaskProvider implements vscode.TaskProvider {
80export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable { 83export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable {
81 const provider = new CargoTaskProvider(target); 84 const provider = new CargoTaskProvider(target);
82 return vscode.tasks.registerTaskProvider(TASK_TYPE, provider); 85 return vscode.tasks.registerTaskProvider(TASK_TYPE, provider);
83} \ No newline at end of file 86}
diff --git a/editors/code/src/cargo.ts b/editors/code/src/toolchain.ts
index a55b2f860..80a7915e9 100644
--- a/editors/code/src/cargo.ts
+++ b/editors/code/src/toolchain.ts
@@ -1,9 +1,10 @@
1import * as cp from 'child_process'; 1import * as cp from 'child_process';
2import * as os from 'os'; 2import * as os from 'os';
3import * as path from 'path'; 3import * as path from 'path';
4import * as fs from 'fs';
4import * as readline from 'readline'; 5import * as readline from 'readline';
5import { OutputChannel } from 'vscode'; 6import { OutputChannel } from 'vscode';
6import { isValidExecutable } from './util'; 7import { log, memoize } from './util';
7 8
8interface CompilationArtifact { 9interface CompilationArtifact {
9 fileName: string; 10 fileName: string;
@@ -17,33 +18,34 @@ export interface ArtifactSpec {
17 filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[]; 18 filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
18} 19}
19 20
20export function artifactSpec(args: readonly string[]): ArtifactSpec { 21export class Cargo {
21 const cargoArgs = [...args, "--message-format=json"]; 22 constructor(readonly rootFolder: string, readonly output: OutputChannel) { }
22 23
23 // arguments for a runnable from the quick pick should be updated. 24 // Made public for testing purposes
24 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens 25 static artifactSpec(args: readonly string[]): ArtifactSpec {
25 switch (cargoArgs[0]) { 26 const cargoArgs = [...args, "--message-format=json"];
26 case "run": cargoArgs[0] = "build"; break; 27
27 case "test": { 28 // arguments for a runnable from the quick pick should be updated.
28 if (!cargoArgs.includes("--no-run")) { 29 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
29 cargoArgs.push("--no-run"); 30 switch (cargoArgs[0]) {
31 case "run": cargoArgs[0] = "build"; break;
32 case "test": {
33 if (!cargoArgs.includes("--no-run")) {
34 cargoArgs.push("--no-run");
35 }
36 break;
30 } 37 }
31 break;
32 } 38 }
33 }
34 39
35 const result: ArtifactSpec = { cargoArgs: cargoArgs }; 40 const result: ArtifactSpec = { cargoArgs: cargoArgs };
36 if (cargoArgs[0] === "test") { 41 if (cargoArgs[0] === "test") {
37 // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests 42 // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
38 // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"} 43 // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
39 result.filter = (artifacts) => artifacts.filter(it => it.isTest); 44 result.filter = (artifacts) => artifacts.filter(it => it.isTest);
40 } 45 }
41
42 return result;
43}
44 46
45export class Cargo { 47 return result;
46 constructor(readonly rootFolder: string, readonly output: OutputChannel) { } 48 }
47 49
48 private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> { 50 private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> {
49 const artifacts: CompilationArtifact[] = []; 51 const artifacts: CompilationArtifact[] = [];
@@ -77,7 +79,7 @@ export class Cargo {
77 } 79 }
78 80
79 async executableFromArgs(args: readonly string[]): Promise<string> { 81 async executableFromArgs(args: readonly string[]): Promise<string> {
80 const artifacts = await this.getArtifacts(artifactSpec(args)); 82 const artifacts = await this.getArtifacts(Cargo.artifactSpec(args));
81 83
82 if (artifacts.length === 0) { 84 if (artifacts.length === 0) {
83 throw new Error('No compilation artifacts'); 85 throw new Error('No compilation artifacts');
@@ -94,14 +96,7 @@ export class Cargo {
94 onStderrString: (data: string) => void 96 onStderrString: (data: string) => void
95 ): Promise<number> { 97 ): Promise<number> {
96 return new Promise((resolve, reject) => { 98 return new Promise((resolve, reject) => {
97 let cargoPath; 99 const cargo = cp.spawn(cargoPath(), cargoArgs, {
98 try {
99 cargoPath = getCargoPathOrFail();
100 } catch (err) {
101 return reject(err);
102 }
103
104 const cargo = cp.spawn(cargoPath, cargoArgs, {
105 stdio: ['ignore', 'pipe', 'pipe'], 100 stdio: ['ignore', 'pipe', 'pipe'],
106 cwd: this.rootFolder 101 cwd: this.rootFolder
107 }); 102 });
@@ -126,26 +121,54 @@ export class Cargo {
126 } 121 }
127} 122}
128 123
129// Mirrors `ra_env::get_path_for_executable` implementation 124/** Mirrors `ra_toolchain::cargo()` implementation */
130function getCargoPathOrFail(): string { 125export function cargoPath(): string {
131 const envVar = process.env.CARGO; 126 return getPathForExecutable("cargo");
132 const executableName = "cargo"; 127}
128
129/** Mirrors `ra_toolchain::get_path_for_executable()` implementation */
130export const getPathForExecutable = memoize(
131 // We apply caching to decrease file-system interactions
132 (executableName: "cargo" | "rustc" | "rustup"): string => {
133 {
134 const envVar = process.env[executableName.toUpperCase()];
135 if (envVar) return envVar;
136 }
137
138 if (lookupInPath(executableName)) return executableName;
133 139
134 if (envVar) { 140 try {
135 if (isValidExecutable(envVar)) return envVar; 141 // hmm, `os.homedir()` seems to be infallible
142 // it is not mentioned in docs and cannot be infered by the type signature...
143 const standardPath = path.join(os.homedir(), ".cargo", "bin", executableName);
136 144
137 throw new Error(`\`${envVar}\` environment variable points to something that's not a valid executable`); 145 if (isFile(standardPath)) return standardPath;
146 } catch (err) {
147 log.error("Failed to read the fs info", err);
148 }
149 return executableName;
138 } 150 }
151);
139 152
140 if (isValidExecutable(executableName)) return executableName; 153function lookupInPath(exec: string): boolean {
154 const paths = process.env.PATH ?? "";;
141 155
142 const standardLocation = path.join(os.homedir(), '.cargo', 'bin', executableName); 156 const candidates = paths.split(path.delimiter).flatMap(dirInPath => {
157 const candidate = path.join(dirInPath, exec);
158 return os.type() === "Windows_NT"
159 ? [candidate, `${candidate}.exe`]
160 : [candidate];
161 });
143 162
144 if (isValidExecutable(standardLocation)) return standardLocation; 163 return candidates.some(isFile);
164}
145 165
146 throw new Error( 166function isFile(suspectPath: string): boolean {
147 `Failed to find \`${executableName}\` executable. ` + 167 // It is not mentionned in docs, but `statSync()` throws an error when
148 `Make sure \`${executableName}\` is in \`$PATH\`, ` + 168 // the path doesn't exist
149 `or set \`${envVar}\` to point to a valid executable.` 169 try {
150 ); 170 return fs.statSync(suspectPath).isFile();
171 } catch {
172 return false;
173 }
151} 174}
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 352ef9162..fe3fb71cd 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -99,3 +99,21 @@ export function isValidExecutable(path: string): boolean {
99export function setContextValue(key: string, value: any): Thenable<void> { 99export function setContextValue(key: string, value: any): Thenable<void> {
100 return vscode.commands.executeCommand('setContext', key, value); 100 return vscode.commands.executeCommand('setContext', key, value);
101} 101}
102
103/**
104 * Returns a higher-order function that caches the results of invoking the
105 * underlying function.
106 */
107export function memoize<Ret, TThis, Param extends string>(func: (this: TThis, arg: Param) => Ret) {
108 const cache = new Map<string, Ret>();
109
110 return function(this: TThis, arg: Param) {
111 const cached = cache.get(arg);
112 if (cached) return cached;
113
114 const result = func.call(this, arg);
115 cache.set(arg, result);
116
117 return result;
118 };
119}
diff --git a/editors/code/tests/unit/launch_config.test.ts b/editors/code/tests/unit/launch_config.test.ts
index d5cf1b74e..68794d53e 100644
--- a/editors/code/tests/unit/launch_config.test.ts
+++ b/editors/code/tests/unit/launch_config.test.ts
@@ -1,25 +1,25 @@
1import * as assert from 'assert'; 1import * as assert from 'assert';
2import * as cargo from '../../src/cargo'; 2import { Cargo } from '../../src/toolchain';
3 3
4suite('Launch configuration', () => { 4suite('Launch configuration', () => {
5 5
6 suite('Lens', () => { 6 suite('Lens', () => {
7 test('A binary', async () => { 7 test('A binary', async () => {
8 const args = cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "pkg_name"]); 8 const args = Cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "pkg_name"]);
9 9
10 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); 10 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]);
11 assert.deepEqual(args.filter, undefined); 11 assert.deepEqual(args.filter, undefined);
12 }); 12 });
13 13
14 test('One of Multiple Binaries', async () => { 14 test('One of Multiple Binaries', async () => {
15 const args = cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "bin1"]); 15 const args = Cargo.artifactSpec(["build", "--package", "pkg_name", "--bin", "bin1"]);
16 16
17 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin1", "--message-format=json"]); 17 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin1", "--message-format=json"]);
18 assert.deepEqual(args.filter, undefined); 18 assert.deepEqual(args.filter, undefined);
19 }); 19 });
20 20
21 test('A test', async () => { 21 test('A test', async () => {
22 const args = cargo.artifactSpec(["test", "--package", "pkg_name", "--lib", "--no-run"]); 22 const args = Cargo.artifactSpec(["test", "--package", "pkg_name", "--lib", "--no-run"]);
23 23
24 assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--no-run", "--message-format=json"]); 24 assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--no-run", "--message-format=json"]);
25 assert.notDeepEqual(args.filter, undefined); 25 assert.notDeepEqual(args.filter, undefined);
@@ -28,7 +28,7 @@ suite('Launch configuration', () => {
28 28
29 suite('QuickPick', () => { 29 suite('QuickPick', () => {
30 test('A binary', async () => { 30 test('A binary', async () => {
31 const args = cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "pkg_name"]); 31 const args = Cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "pkg_name"]);
32 32
33 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]); 33 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "pkg_name", "--message-format=json"]);
34 assert.deepEqual(args.filter, undefined); 34 assert.deepEqual(args.filter, undefined);
@@ -36,14 +36,14 @@ suite('Launch configuration', () => {
36 36
37 37
38 test('One of Multiple Binaries', async () => { 38 test('One of Multiple Binaries', async () => {
39 const args = cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "bin2"]); 39 const args = Cargo.artifactSpec(["run", "--package", "pkg_name", "--bin", "bin2"]);
40 40
41 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin2", "--message-format=json"]); 41 assert.deepEqual(args.cargoArgs, ["build", "--package", "pkg_name", "--bin", "bin2", "--message-format=json"]);
42 assert.deepEqual(args.filter, undefined); 42 assert.deepEqual(args.filter, undefined);
43 }); 43 });
44 44
45 test('A test', async () => { 45 test('A test', async () => {
46 const args = cargo.artifactSpec(["test", "--package", "pkg_name", "--lib"]); 46 const args = Cargo.artifactSpec(["test", "--package", "pkg_name", "--lib"]);
47 47
48 assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--message-format=json", "--no-run"]); 48 assert.deepEqual(args.cargoArgs, ["test", "--package", "pkg_name", "--lib", "--message-format=json", "--no-run"]);
49 assert.notDeepEqual(args.filter, undefined); 49 assert.notDeepEqual(args.filter, undefined);