aboutsummaryrefslogtreecommitdiff
path: root/editors
diff options
context:
space:
mode:
Diffstat (limited to 'editors')
-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.ts4
-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
7 files changed, 104 insertions, 59 deletions
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 2a7a429cf..401cb76af 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: spec.env, 181 env: spec.env,
181 }; 182 };
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts
index c22d69362..9748824df 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -1,5 +1,5 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import { getCargoPathOrFail } from "./cargo"; 2import * as toolchain from "./toolchain";
3 3
4// 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
5// 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.
@@ -25,7 +25,7 @@ class CargoTaskProvider implements vscode.TaskProvider {
25 // 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
26 // tasks.json - only tweaked. 26 // tasks.json - only tweaked.
27 27
28 const cargoPath = getCargoPathOrFail(); 28 const cargoPath = toolchain.cargoPath();
29 29
30 return [ 30 return [
31 { command: 'build', group: vscode.TaskGroup.Build }, 31 { command: 'build', group: vscode.TaskGroup.Build },
diff --git a/editors/code/src/cargo.ts b/editors/code/src/toolchain.ts
index 46cd3d777..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_toolchain::cargo()` implementation 124/** Mirrors `ra_toolchain::cargo()` implementation */
130export function 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);