aboutsummaryrefslogtreecommitdiff
path: root/editors/code
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code')
-rw-r--r--editors/code/package.json15
-rw-r--r--editors/code/rollup.config.js7
-rw-r--r--editors/code/src/commands.ts18
-rw-r--r--editors/code/src/config.ts9
-rw-r--r--editors/code/src/debug.ts2
-rw-r--r--editors/code/src/main.ts39
-rw-r--r--editors/code/src/net.ts39
-rw-r--r--editors/code/src/run.ts58
-rw-r--r--editors/code/src/status_display.ts100
-rw-r--r--editors/code/src/tasks.ts111
-rw-r--r--editors/code/src/util.ts1
11 files changed, 185 insertions, 214 deletions
diff --git a/editors/code/package.json b/editors/code/package.json
index 3acc375f6..f542a490a 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -336,6 +336,14 @@
336 "default": null, 336 "default": null,
337 "description": "List of features to activate. Defaults to `rust-analyzer.cargo.features`." 337 "description": "List of features to activate. Defaults to `rust-analyzer.cargo.features`."
338 }, 338 },
339 "rust-analyzer.cargoRunner": {
340 "type": [
341 "null",
342 "string"
343 ],
344 "default": null,
345 "description": "Custom cargo runner extension ID."
346 },
339 "rust-analyzer.inlayHints.enable": { 347 "rust-analyzer.inlayHints.enable": {
340 "type": "boolean", 348 "type": "boolean",
341 "default": true, 349 "default": true,
@@ -426,7 +434,7 @@
426 "Full log" 434 "Full log"
427 ], 435 ],
428 "default": "off", 436 "default": "off",
429 "description": "Trace requests to the rust-analyzer" 437 "description": "Trace requests to the rust-analyzer (this is usually overly verbose and not recommended for regular users)"
430 }, 438 },
431 "rust-analyzer.trace.extension": { 439 "rust-analyzer.trace.extension": {
432 "description": "Enable logging of VS Code extensions itself", 440 "description": "Enable logging of VS Code extensions itself",
@@ -510,6 +518,11 @@
510 "type": "boolean", 518 "type": "boolean",
511 "default": true 519 "default": true
512 }, 520 },
521 "rust-analyzer.hoverActions.gotoTypeDef": {
522 "markdownDescription": "Whether to show `Go to Type Definition` action. Only applies when `#rust-analyzer.hoverActions.enable#` is set.",
523 "type": "boolean",
524 "default": true
525 },
513 "rust-analyzer.linkedProjects": { 526 "rust-analyzer.linkedProjects": {
514 "markdownDescription": "Disable project auto-discovery in favor of explicitly specified set of projects. \nElements must be paths pointing to Cargo.toml, rust-project.json, or JSON objects in rust-project.json format", 527 "markdownDescription": "Disable project auto-discovery in favor of explicitly specified set of projects. \nElements must be paths pointing to Cargo.toml, rust-project.json, or JSON objects in rust-project.json format",
515 "type": "array", 528 "type": "array",
diff --git a/editors/code/rollup.config.js b/editors/code/rollup.config.js
index 58360eabb..4b4c47f4a 100644
--- a/editors/code/rollup.config.js
+++ b/editors/code/rollup.config.js
@@ -11,12 +11,7 @@ export default {
11 resolve({ 11 resolve({
12 preferBuiltins: true 12 preferBuiltins: true
13 }), 13 }),
14 commonjs({ 14 commonjs()
15 namedExports: {
16 // squelch missing import warnings
17 'vscode-languageclient': ['CreateFile', 'RenameFile', 'ErrorCodes', 'WorkDoneProgress', 'WorkDoneProgressBegin', 'WorkDoneProgressReport', 'WorkDoneProgressEnd']
18 }
19 })
20 ], 15 ],
21 external: [...nodeBuiltins, 'vscode'], 16 external: [...nodeBuiltins, 'vscode'],
22 output: { 17 output: {
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 3e9c3aa0e..8c9d7802f 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -353,6 +353,20 @@ export function applyActionGroup(_ctx: Ctx): Cmd {
353 }; 353 };
354} 354}
355 355
356export function gotoLocation(ctx: Ctx): Cmd {
357 return async (locationLink: lc.LocationLink) => {
358 const client = ctx.client;
359 if (client) {
360 const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
361 let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
362 // collapse the range to a cursor position
363 range = range.with({ end: range.start });
364
365 await vscode.window.showTextDocument(uri, { selection: range });
366 }
367 };
368}
369
356export function resolveCodeAction(ctx: Ctx): Cmd { 370export function resolveCodeAction(ctx: Ctx): Cmd {
357 const client = ctx.client; 371 const client = ctx.client;
358 return async (params: ra.ResolveCodeActionParams) => { 372 return async (params: ra.ResolveCodeActionParams) => {
@@ -380,7 +394,7 @@ export function run(ctx: Ctx): Cmd {
380 394
381 item.detail = 'rerun'; 395 item.detail = 'rerun';
382 prevRunnable = item; 396 prevRunnable = item;
383 const task = createTask(item.runnable); 397 const task = await createTask(item.runnable, ctx.config);
384 return await vscode.tasks.executeTask(task); 398 return await vscode.tasks.executeTask(task);
385 }; 399 };
386} 400}
@@ -390,7 +404,7 @@ export function runSingle(ctx: Ctx): Cmd {
390 const editor = ctx.activeRustEditor; 404 const editor = ctx.activeRustEditor;
391 if (!editor) return; 405 if (!editor) return;
392 406
393 const task = createTask(runnable); 407 const task = await createTask(runnable, ctx.config);
394 task.group = vscode.TaskGroup.Build; 408 task.group = vscode.TaskGroup.Build;
395 task.presentationOptions = { 409 task.presentationOptions = {
396 reveal: vscode.TaskRevealKind.Always, 410 reveal: vscode.TaskRevealKind.Always,
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index d8f0037d4..fc95a7de6 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -110,6 +110,10 @@ export class Config {
110 }; 110 };
111 } 111 }
112 112
113 get cargoRunner() {
114 return this.get<string | undefined>("cargoRunner");
115 }
116
113 get debug() { 117 get debug() {
114 // "/rustc/<id>" used by suggestions only. 118 // "/rustc/<id>" used by suggestions only.
115 const { ["/rustc/<id>"]: _, ...sourceFileMap } = this.get<Record<string, string>>("debug.sourceFileMap"); 119 const { ["/rustc/<id>"]: _, ...sourceFileMap } = this.get<Record<string, string>>("debug.sourceFileMap");
@@ -117,7 +121,7 @@ export class Config {
117 return { 121 return {
118 engine: this.get<string>("debug.engine"), 122 engine: this.get<string>("debug.engine"),
119 engineSettings: this.get<object>("debug.engineSettings"), 123 engineSettings: this.get<object>("debug.engineSettings"),
120 openUpDebugPane: this.get<boolean>("debug.openUpDebugPane"), 124 openDebugPane: this.get<boolean>("debug.openDebugPane"),
121 sourceFileMap: sourceFileMap 125 sourceFileMap: sourceFileMap
122 }; 126 };
123 } 127 }
@@ -135,6 +139,9 @@ export class Config {
135 return { 139 return {
136 enable: this.get<boolean>("hoverActions.enable"), 140 enable: this.get<boolean>("hoverActions.enable"),
137 implementations: this.get<boolean>("hoverActions.implementations"), 141 implementations: this.get<boolean>("hoverActions.implementations"),
142 run: this.get<boolean>("hoverActions.run"),
143 debug: this.get<boolean>("hoverActions.debug"),
144 gotoTypeDef: this.get<boolean>("hoverActions.gotoTypeDef"),
138 }; 145 };
139 } 146 }
140} 147}
diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts
index a0c9b3ab2..61c12dbe0 100644
--- a/editors/code/src/debug.ts
+++ b/editors/code/src/debug.ts
@@ -82,7 +82,7 @@ async function getDebugConfiguration(ctx: Ctx, runnable: ra.Runnable): Promise<v
82 } 82 }
83 83
84 debugOutput.clear(); 84 debugOutput.clear();
85 if (ctx.config.debug.openUpDebugPane) { 85 if (ctx.config.debug.openDebugPane) {
86 debugOutput.show(true); 86 debugOutput.show(true);
87 } 87 }
88 88
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index a92c676fa..5ceab8b44 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -5,7 +5,6 @@ import { promises as fs, PathLike } from "fs";
5 5
6import * as commands from './commands'; 6import * as commands from './commands';
7import { activateInlayHints } from './inlay_hints'; 7import { activateInlayHints } from './inlay_hints';
8import { activateStatusDisplay } from './status_display';
9import { Ctx } from './ctx'; 8import { Ctx } from './ctx';
10import { Config, NIGHTLY_TAG } from './config'; 9import { Config, NIGHTLY_TAG } from './config';
11import { log, assert, isValidExecutable } from './util'; 10import { log, assert, isValidExecutable } from './util';
@@ -42,7 +41,20 @@ export async function activate(context: vscode.ExtensionContext) {
42 41
43 const config = new Config(context); 42 const config = new Config(context);
44 const state = new PersistentState(context.globalState); 43 const state = new PersistentState(context.globalState);
45 const serverPath = await bootstrap(config, state); 44 const serverPath = await bootstrap(config, state).catch(err => {
45 let message = "bootstrap error. ";
46
47 if (err.code === "EBUSY" || err.code === "ETXTBSY") {
48 message += "Other vscode windows might be using rust-analyzer, ";
49 message += "you should close them and reload this window to retry. ";
50 }
51
52 message += 'Open "Help > Toggle Developer Tools > Console" to see the logs ';
53 message += '(enable verbose logs with "rust-analyzer.trace.extension")';
54
55 log.error("Bootstrap error", err);
56 throw new Error(message);
57 });
46 58
47 const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 59 const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
48 if (workspaceFolder === undefined) { 60 if (workspaceFolder === undefined) {
@@ -100,10 +112,9 @@ export async function activate(context: vscode.ExtensionContext) {
100 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand); 112 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
101 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction); 113 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
102 ctx.registerCommand('applyActionGroup', commands.applyActionGroup); 114 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
115 ctx.registerCommand('gotoLocation', commands.gotoLocation);
103 116
104 ctx.pushCleanup(activateTaskProvider(workspaceFolder)); 117 ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
105
106 activateStatusDisplay(ctx);
107 118
108 activateInlayHints(ctx); 119 activateInlayHints(ctx);
109 120
@@ -168,7 +179,11 @@ async function bootstrapExtension(config: Config, state: PersistentState): Promi
168 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); 179 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
169 180
170 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix"); 181 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
171 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension"); 182 await download({
183 url: artifact.browser_download_url,
184 dest,
185 progressTitle: "Downloading rust-analyzer extension",
186 });
172 187
173 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest)); 188 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
174 await fs.unlink(dest); 189 await fs.unlink(dest);
@@ -284,7 +299,17 @@ async function getServer(config: Config, state: PersistentState): Promise<string
284 const artifact = release.assets.find(artifact => artifact.name === binaryName); 299 const artifact = release.assets.find(artifact => artifact.name === binaryName);
285 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); 300 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
286 301
287 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 }); 302 // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
303 await fs.unlink(dest).catch(err => {
304 if (err.code !== "ENOENT") throw err;
305 });
306
307 await download({
308 url: artifact.browser_download_url,
309 dest,
310 progressTitle: "Downloading rust-analyzer server",
311 mode: 0o755
312 });
288 313
289 // Patching executable if that's NixOS. 314 // Patching executable if that's NixOS.
290 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) { 315 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
diff --git a/editors/code/src/net.ts b/editors/code/src/net.ts
index 492213937..866092882 100644
--- a/editors/code/src/net.ts
+++ b/editors/code/src/net.ts
@@ -1,8 +1,10 @@
1import fetch from "node-fetch"; 1import fetch from "node-fetch";
2import * as vscode from "vscode"; 2import * as vscode from "vscode";
3import * as fs from "fs";
4import * as stream from "stream"; 3import * as stream from "stream";
4import * as crypto from "crypto";
5import * as fs from "fs";
5import * as util from "util"; 6import * as util from "util";
7import * as path from "path";
6import { log, assert } from "./util"; 8import { log, assert } from "./util";
7 9
8const pipeline = util.promisify(stream.pipeline); 10const pipeline = util.promisify(stream.pipeline);
@@ -58,22 +60,29 @@ export interface GithubRelease {
58 }>; 60 }>;
59} 61}
60 62
63interface DownloadOpts {
64 progressTitle: string;
65 url: string;
66 dest: string;
67 mode?: number;
68}
69
70export async function download(opts: DownloadOpts) {
71 // Put artifact into a temporary file (in the same dir for simplicity)
72 // to prevent partially downloaded files when user kills vscode
73 const dest = path.parse(opts.dest);
74 const randomHex = crypto.randomBytes(5).toString("hex");
75 const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`);
61 76
62export async function download(
63 downloadUrl: string,
64 destinationPath: string,
65 progressTitle: string,
66 { mode }: { mode?: number } = {},
67) {
68 await vscode.window.withProgress( 77 await vscode.window.withProgress(
69 { 78 {
70 location: vscode.ProgressLocation.Notification, 79 location: vscode.ProgressLocation.Notification,
71 cancellable: false, 80 cancellable: false,
72 title: progressTitle 81 title: opts.progressTitle
73 }, 82 },
74 async (progress, _cancellationToken) => { 83 async (progress, _cancellationToken) => {
75 let lastPercentage = 0; 84 let lastPercentage = 0;
76 await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => { 85 await downloadFile(opts.url, tempFile, opts.mode, (readBytes, totalBytes) => {
77 const newPercentage = (readBytes / totalBytes) * 100; 86 const newPercentage = (readBytes / totalBytes) * 100;
78 progress.report({ 87 progress.report({
79 message: newPercentage.toFixed(0) + "%", 88 message: newPercentage.toFixed(0) + "%",
@@ -84,10 +93,12 @@ export async function download(
84 }); 93 });
85 } 94 }
86 ); 95 );
96
97 await fs.promises.rename(tempFile, opts.dest);
87} 98}
88 99
89/** 100/**
90 * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. 101 * Downloads file from `url` and stores it at `destFilePath` with `mode` (unix permissions).
91 * `onProgress` callback is called on recieveing each chunk of bytes 102 * `onProgress` callback is called on recieveing each chunk of bytes
92 * to track the progress of downloading, it gets the already read and total 103 * to track the progress of downloading, it gets the already read and total
93 * amount of bytes to read as its parameters. 104 * amount of bytes to read as its parameters.
@@ -119,13 +130,11 @@ async function downloadFile(
119 }); 130 });
120 131
121 const destFileStream = fs.createWriteStream(destFilePath, { mode }); 132 const destFileStream = fs.createWriteStream(destFilePath, { mode });
122
123 await pipeline(res.body, destFileStream); 133 await pipeline(res.body, destFileStream);
124 return new Promise<void>(resolve => { 134 await new Promise<void>(resolve => {
125 destFileStream.on("close", resolve); 135 destFileStream.on("close", resolve);
126 destFileStream.destroy(); 136 destFileStream.destroy();
127 137 // This workaround is awaiting to be removed when vscode moves to newer nodejs version:
128 // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 138 // https://github.com/rust-analyzer/rust-analyzer/issues/3167
129 // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776
130 }); 139 });
131} 140}
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
index bb060cfe1..766b05112 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -1,10 +1,11 @@
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"; 4import * as tasks from './tasks';
5 5
6import { Ctx } from './ctx'; 6import { Ctx } from './ctx';
7import { makeDebugConfig } from './debug'; 7import { makeDebugConfig } from './debug';
8import { Config } from './config';
8 9
9const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; 10const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }];
10 11
@@ -95,52 +96,29 @@ export class RunnableQuickPick implements vscode.QuickPickItem {
95 } 96 }
96} 97}
97 98
98interface CargoTaskDefinition extends vscode.TaskDefinition { 99export async function createTask(runnable: ra.Runnable, config: Config): Promise<vscode.Task> {
99 type: 'cargo'; 100 if (runnable.kind !== "cargo") {
100 label: string; 101 // rust-analyzer supports only one kind, "cargo"
101 command: string; 102 // do not use tasks.TASK_TYPE here, these are completely different meanings.
102 args: string[];
103 env?: { [key: string]: string };
104}
105
106export function createTask(runnable: ra.Runnable): vscode.Task {
107 const TASK_SOURCE = 'Rust';
108 103
109 let command; 104 throw `Unexpected runnable kind: ${runnable.kind}`;
110 switch (runnable.kind) {
111 case "cargo": command = toolchain.getPathForExecutable("cargo");
112 } 105 }
106
113 const args = [...runnable.args.cargoArgs]; // should be a copy! 107 const args = [...runnable.args.cargoArgs]; // should be a copy!
114 if (runnable.args.executableArgs.length > 0) { 108 if (runnable.args.executableArgs.length > 0) {
115 args.push('--', ...runnable.args.executableArgs); 109 args.push('--', ...runnable.args.executableArgs);
116 } 110 }
117 const definition: CargoTaskDefinition = { 111 const definition: tasks.CargoTaskDefinition = {
118 type: 'cargo', 112 type: tasks.TASK_TYPE,
119 label: runnable.label, 113 command: args[0], // run, test, etc...
120 command, 114 args: args.slice(1),
121 args, 115 cwd: runnable.args.workspaceRoot,
122 env: Object.assign({}, process.env as { [key: string]: string }, { "RUST_BACKTRACE": "short" }), 116 env: Object.assign({}, process.env as { [key: string]: string }, { "RUST_BACKTRACE": "short" }),
123 }; 117 };
124 118
125 const execOption: vscode.ShellExecutionOptions = { 119 const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate()
126 cwd: runnable.args.workspaceRoot || '.', 120 const cargoTask = await tasks.buildCargoTask(target, definition, runnable.label, args, config.cargoRunner, true);
127 env: definition.env, 121 cargoTask.presentationOptions.clear = true;
128 }; 122
129 const exec = new vscode.ShellExecution( 123 return cargoTask;
130 definition.command,
131 definition.args,
132 execOption,
133 );
134
135 const f = vscode.workspace.workspaceFolders![0];
136 const t = new vscode.Task(
137 definition,
138 f,
139 definition.label,
140 TASK_SOURCE,
141 exec,
142 ['$rustc'],
143 );
144 t.presentationOptions.clear = true;
145 return t;
146} 124}
diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts
deleted file mode 100644
index f9cadc8a2..000000000
--- a/editors/code/src/status_display.ts
+++ /dev/null
@@ -1,100 +0,0 @@
1import * as vscode from 'vscode';
2
3import { WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressReport, WorkDoneProgressEnd, Disposable } from 'vscode-languageclient';
4
5import { Ctx } from './ctx';
6
7const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
8
9export function activateStatusDisplay(ctx: Ctx) {
10 const statusDisplay = new StatusDisplay(ctx.config.checkOnSave.command);
11 ctx.pushCleanup(statusDisplay);
12 const client = ctx.client;
13 if (client != null) {
14 ctx.pushCleanup(client.onProgress(
15 WorkDoneProgress.type,
16 'rustAnalyzer/cargoWatcher',
17 params => statusDisplay.handleProgressNotification(params)
18 ));
19 }
20}
21
22class StatusDisplay implements Disposable {
23 packageName?: string;
24
25 private i: number = 0;
26 private statusBarItem: vscode.StatusBarItem;
27 private command: string;
28 private timer?: NodeJS.Timeout;
29
30 constructor(command: string) {
31 this.statusBarItem = vscode.window.createStatusBarItem(
32 vscode.StatusBarAlignment.Left,
33 10,
34 );
35 this.command = command;
36 this.statusBarItem.hide();
37 }
38
39 show() {
40 this.packageName = undefined;
41
42 this.timer =
43 this.timer ||
44 setInterval(() => {
45 this.tick();
46 this.refreshLabel();
47 }, 300);
48
49 this.statusBarItem.show();
50 }
51
52 hide() {
53 if (this.timer) {
54 clearInterval(this.timer);
55 this.timer = undefined;
56 }
57
58 this.statusBarItem.hide();
59 }
60
61 dispose() {
62 if (this.timer) {
63 clearInterval(this.timer);
64 this.timer = undefined;
65 }
66
67 this.statusBarItem.dispose();
68 }
69
70 refreshLabel() {
71 if (this.packageName) {
72 this.statusBarItem.text = `${spinnerFrames[this.i]} cargo ${this.command} [${this.packageName}]`;
73 } else {
74 this.statusBarItem.text = `${spinnerFrames[this.i]} cargo ${this.command}`;
75 }
76 }
77
78 handleProgressNotification(params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd) {
79 switch (params.kind) {
80 case 'begin':
81 this.show();
82 break;
83
84 case 'report':
85 if (params.message) {
86 this.packageName = params.message;
87 this.refreshLabel();
88 }
89 break;
90
91 case 'end':
92 this.hide();
93 break;
94 }
95 }
96
97 private tick() {
98 this.i = (this.i + 1) % spinnerFrames.length;
99 }
100}
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts
index 9748824df..14abbd5b7 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -1,11 +1,14 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as toolchain from "./toolchain"; 2import * as toolchain from "./toolchain";
3import { Config } from './config';
4import { log } from './util';
3 5
4// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and 6// 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. 7// our configuration should be compatible with it so use the same key.
6const TASK_TYPE = 'cargo'; 8export const TASK_TYPE = 'cargo';
9export const TASK_SOURCE = 'rust';
7 10
8interface CargoTaskDefinition extends vscode.TaskDefinition { 11export interface CargoTaskDefinition extends vscode.TaskDefinition {
9 command?: string; 12 command?: string;
10 args?: string[]; 13 args?: string[];
11 cwd?: string; 14 cwd?: string;
@@ -14,73 +17,101 @@ interface CargoTaskDefinition extends vscode.TaskDefinition {
14 17
15class CargoTaskProvider implements vscode.TaskProvider { 18class CargoTaskProvider implements vscode.TaskProvider {
16 private readonly target: vscode.WorkspaceFolder; 19 private readonly target: vscode.WorkspaceFolder;
20 private readonly config: Config;
17 21
18 constructor(target: vscode.WorkspaceFolder) { 22 constructor(target: vscode.WorkspaceFolder, config: Config) {
19 this.target = target; 23 this.target = target;
24 this.config = config;
20 } 25 }
21 26
22 provideTasks(): vscode.Task[] { 27 async provideTasks(): Promise<vscode.Task[]> {
23 // Detect Rust tasks. Currently we do not do any actual detection 28 // Detect Rust tasks. Currently we do not do any actual detection
24 // of tasks (e.g. aliases in .cargo/config) and just return a fixed 29 // of tasks (e.g. aliases in .cargo/config) and just return a fixed
25 // set of tasks that always exist. These tasks cannot be removed in 30 // set of tasks that always exist. These tasks cannot be removed in
26 // tasks.json - only tweaked. 31 // tasks.json - only tweaked.
27 32
28 const cargoPath = toolchain.cargoPath(); 33 const defs = [
29
30 return [
31 { command: 'build', group: vscode.TaskGroup.Build }, 34 { command: 'build', group: vscode.TaskGroup.Build },
32 { command: 'check', group: vscode.TaskGroup.Build }, 35 { command: 'check', group: vscode.TaskGroup.Build },
33 { command: 'test', group: vscode.TaskGroup.Test }, 36 { command: 'test', group: vscode.TaskGroup.Test },
34 { command: 'clean', group: vscode.TaskGroup.Clean }, 37 { command: 'clean', group: vscode.TaskGroup.Clean },
35 { command: 'run', group: undefined }, 38 { command: 'run', group: undefined },
36 ] 39 ];
37 .map(({ command, group }) => { 40
38 const vscodeTask = new vscode.Task( 41 const tasks: vscode.Task[] = [];
39 // The contents of this object end up in the tasks.json entries. 42 for (const def of defs) {
40 { 43 const vscodeTask = await buildCargoTask(this.target, { type: TASK_TYPE, command: def.command }, `cargo ${def.command}`, [def.command], this.config.cargoRunner);
41 type: TASK_TYPE, 44 vscodeTask.group = def.group;
42 command, 45 tasks.push(vscodeTask);
43 }, 46 }
44 // The scope of the task - workspace or specific folder (global 47
45 // is not supported). 48 return tasks;
46 this.target,
47 // The task name, and task source. These are shown in the UI as
48 // `${source}: ${name}`, e.g. `rust: cargo build`.
49 `cargo ${command}`,
50 'rust',
51 // What to do when this command is executed.
52 new vscode.ShellExecution(cargoPath, [command]),
53 // Problem matchers.
54 ['$rustc'],
55 );
56 vscodeTask.group = group;
57 return vscodeTask;
58 });
59 } 49 }
60 50
61 resolveTask(task: vscode.Task): vscode.Task | undefined { 51 async resolveTask(task: vscode.Task): Promise<vscode.Task | undefined> {
62 // VSCode calls this for every cargo task in the user's tasks.json, 52 // VSCode calls this for every cargo task in the user's tasks.json,
63 // we need to inform VSCode how to execute that command by creating 53 // we need to inform VSCode how to execute that command by creating
64 // a ShellExecution for it. 54 // a ShellExecution for it.
65 55
66 const definition = task.definition as CargoTaskDefinition; 56 const definition = task.definition as CargoTaskDefinition;
67 57
68 if (definition.type === 'cargo' && definition.command) { 58 if (definition.type === TASK_TYPE && definition.command) {
69 const args = [definition.command].concat(definition.args ?? []); 59 const args = [definition.command].concat(definition.args ?? []);
70 60
71 return new vscode.Task( 61 return await buildCargoTask(this.target, definition, task.name, args, this.config.cargoRunner);
72 definition,
73 task.name,
74 'rust',
75 new vscode.ShellExecution('cargo', args, definition),
76 );
77 } 62 }
78 63
79 return undefined; 64 return undefined;
80 } 65 }
81} 66}
82 67
83export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable { 68export async function buildCargoTask(
84 const provider = new CargoTaskProvider(target); 69 target: vscode.WorkspaceFolder,
70 definition: CargoTaskDefinition,
71 name: string,
72 args: string[],
73 customRunner?: string,
74 throwOnError: boolean = false
75): Promise<vscode.Task> {
76
77 let exec: vscode.ShellExecution | undefined = undefined;
78
79 if (customRunner) {
80 const runnerCommand = `${customRunner}.buildShellExecution`;
81 try {
82 const runnerArgs = { kind: TASK_TYPE, args, cwd: definition.cwd, env: definition.env };
83 const customExec = await vscode.commands.executeCommand(runnerCommand, runnerArgs);
84 if (customExec) {
85 if (customExec instanceof vscode.ShellExecution) {
86 exec = customExec;
87 } else {
88 log.debug("Invalid cargo ShellExecution", customExec);
89 throw "Invalid cargo ShellExecution.";
90 }
91 }
92 // fallback to default processing
93
94 } catch (e) {
95 if (throwOnError) throw `Cargo runner '${customRunner}' failed! ${e}`;
96 // fallback to default processing
97 }
98 }
99
100 if (!exec) {
101 exec = new vscode.ShellExecution(toolchain.cargoPath(), args, definition);
102 }
103
104 return new vscode.Task(
105 definition,
106 target,
107 name,
108 TASK_SOURCE,
109 exec,
110 ['$rustc']
111 );
112}
113
114export function activateTaskProvider(target: vscode.WorkspaceFolder, config: Config): vscode.Disposable {
115 const provider = new CargoTaskProvider(target, config);
85 return vscode.tasks.registerTaskProvider(TASK_TYPE, provider); 116 return vscode.tasks.registerTaskProvider(TASK_TYPE, provider);
86} 117}
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index fe3fb71cd..fec4c3295 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -26,7 +26,6 @@ export const log = new class {
26 } 26 }
27 27
28 error(message?: any, ...optionalParams: any[]): void { 28 error(message?: any, ...optionalParams: any[]): void {
29 if (!log.enabled) return;
30 debugger; 29 debugger;
31 // eslint-disable-next-line no-console 30 // eslint-disable-next-line no-console
32 console.error(message, ...optionalParams); 31 console.error(message, ...optionalParams);