1import * as vscode from 'vscode';
2import * as toolchain from "./toolchain";
3import { Config } from './config';
4import { log } from './util';
5
6// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and
7// our configuration should be compatible with it so use the same key.
8export const TASK_TYPE = 'cargo';
9export const TASK_SOURCE = 'rust';
10
11export interface CargoTaskDefinition extends vscode.TaskDefinition {
12    command?: string;
13    args?: string[];
14    cwd?: string;
15    env?: { [key: string]: string };
16    overrideCargo?: string;
17}
18
19class CargoTaskProvider implements vscode.TaskProvider {
20    private readonly config: Config;
21
22    constructor(config: Config) {
23        this.config = config;
24    }
25
26    async provideTasks(): Promise<vscode.Task[]> {
27        // Detect Rust tasks. Currently we do not do any actual detection
28        // of tasks (e.g. aliases in .cargo/config) and just return a fixed
29        // set of tasks that always exist. These tasks cannot be removed in
30        // tasks.json - only tweaked.
31
32        const defs = [
33            { command: 'build', group: vscode.TaskGroup.Build },
34            { command: 'check', group: vscode.TaskGroup.Build },
35            { command: 'test', group: vscode.TaskGroup.Test },
36            { command: 'clean', group: vscode.TaskGroup.Clean },
37            { command: 'run', group: undefined },
38        ];
39
40        const tasks: vscode.Task[] = [];
41        for (const workspaceTarget of vscode.workspace.workspaceFolders || []) {
42            for (const def of defs) {
43                const vscodeTask = await buildCargoTask(workspaceTarget, { type: TASK_TYPE, command: def.command }, `cargo ${def.command}`, [def.command], this.config.cargoRunner);
44                vscodeTask.group = def.group;
45                tasks.push(vscodeTask);
46            }
47        }
48
49        return tasks;
50    }
51
52    async resolveTask(task: vscode.Task): Promise<vscode.Task | undefined> {
53        // VSCode calls this for every cargo task in the user's tasks.json,
54        // we need to inform VSCode how to execute that command by creating
55        // a ShellExecution for it.
56
57        const definition = task.definition as CargoTaskDefinition;
58
59        if (definition.type === TASK_TYPE && definition.command) {
60            const args = [definition.command].concat(definition.args ?? []);
61            return await buildCargoTask(task.scope, definition, task.name, args, this.config.cargoRunner);
62        }
63
64        return undefined;
65    }
66}
67
68export async function buildCargoTask(
69    scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
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.ProcessExecution | 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        // Check whether we must use a user-defined substitute for cargo.
102        // Split on spaces to allow overrides like "wrapper cargo".
103        const overrideCargo = definition.overrideCargo ?? definition.overrideCargo;
104        const cargoPath = await toolchain.cargoPath();
105        const cargoCommand = overrideCargo?.split(" ") ?? [cargoPath];
106
107        const fullCommand = [...cargoCommand, ...args];
108
109        exec = new vscode.ProcessExecution(fullCommand[0], fullCommand.slice(1), definition);
110    }
111
112    return new vscode.Task(
113        definition,
114        // scope can sometimes be undefined. in these situations we default to the workspace taskscope as
115        // recommended by the official docs: https://code.visualstudio.com/api/extension-guides/task-provider#task-provider)
116        scope ?? vscode.TaskScope.Workspace,
117        name,
118        TASK_SOURCE,
119        exec,
120        ['$rustc']
121    );
122}
123
124export function activateTaskProvider(config: Config): vscode.Disposable {
125    const provider = new CargoTaskProvider(config);
126    return vscode.tasks.registerTaskProvider(TASK_TYPE, provider);
127}
128