Unnamed repository; edit this file 'description' to name the repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import * as vscode from "vscode";
import type { Config } from "./config";
import { log } from "./util";
import { expectNotUndefined, unwrapUndefinable } from "./undefinable";

// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and
// our configuration should be compatible with it so use the same key.
export const TASK_TYPE = "cargo";

export const TASK_SOURCE = "rust";

export interface RustTargetDefinition extends vscode.TaskDefinition {
    // The cargo command, such as "run" or "check".
    command: string;
    // Additional arguments passed to the cargo command.
    args?: string[];
    // The working directory to run the cargo command in.
    cwd?: string;
    // The shell environment.
    env?: { [key: string]: string };
    // Override the cargo executable name, such as
    // "my_custom_cargo_bin".
    overrideCargo?: string;
}

class RustTaskProvider implements vscode.TaskProvider {
    private readonly config: Config;

    constructor(config: Config) {
        this.config = config;
    }

    async provideTasks(): Promise<vscode.Task[]> {
        // Detect Rust tasks. Currently we do not do any actual detection
        // of tasks (e.g. aliases in .cargo/config) and just return a fixed
        // set of tasks that always exist. These tasks cannot be removed in
        // tasks.json - only tweaked.

        const defs = [
            { command: "build", group: vscode.TaskGroup.Build },
            { command: "check", group: vscode.TaskGroup.Build },
            { command: "clippy", group: vscode.TaskGroup.Build },
            { command: "test", group: vscode.TaskGroup.Test },
            { command: "clean", group: vscode.TaskGroup.Clean },
            { command: "run", group: undefined },
        ];

        const tasks: vscode.Task[] = [];
        for (const workspaceTarget of vscode.workspace.workspaceFolders || []) {
            for (const def of defs) {
                const vscodeTask = await buildRustTask(
                    workspaceTarget,
                    { type: TASK_TYPE, command: def.command },
                    `cargo ${def.command}`,
                    this.config.problemMatcher,
                    this.config.cargoRunner,
                );
                vscodeTask.group = def.group;
                tasks.push(vscodeTask);
            }
        }

        return tasks;
    }

    async resolveTask(task: vscode.Task): Promise<vscode.Task | undefined> {
        // VSCode calls this for every cargo task in the user's tasks.json,
        // we need to inform VSCode how to execute that command by creating
        // a ShellExecution for it.

        const definition = task.definition as RustTargetDefinition;

        if (definition.type === TASK_TYPE) {
            return await buildRustTask(
                task.scope,
                definition,
                task.name,
                this.config.problemMatcher,
                this.config.cargoRunner,
            );
        }

        return undefined;
    }
}

export async function buildRustTask(
    scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
    definition: RustTargetDefinition,
    name: string,
    problemMatcher: string[],
    customRunner?: string,
    throwOnError: boolean = false,
): Promise<vscode.Task> {
    const exec = await cargoToExecution(definition, customRunner, throwOnError);

    return new vscode.Task(
        definition,
        // scope can sometimes be undefined. in these situations we default to the workspace taskscope as
        // recommended by the official docs: https://code.visualstudio.com/api/extension-guides/task-provider#task-provider)
        scope ?? vscode.TaskScope.Workspace,
        name,
        TASK_SOURCE,
        exec,
        problemMatcher,
    );
}

async function cargoToExecution(
    definition: RustTargetDefinition,
    customRunner: string | undefined,
    throwOnError: boolean,
): Promise<vscode.ProcessExecution | vscode.ShellExecution> {
    if (customRunner) {
        const runnerCommand = `${customRunner}.buildShellExecution`;

        try {
            const runnerArgs = {
                kind: TASK_TYPE,
                args: definition.args,
                cwd: definition.cwd,
                env: definition.env,
            };
            const customExec = await vscode.commands.executeCommand(runnerCommand, runnerArgs);
            if (customExec) {
                if (customExec instanceof vscode.ShellExecution) {
                    return customExec;
                } else {
                    log.debug("Invalid cargo ShellExecution", customExec);
                    throw "Invalid cargo ShellExecution.";
                }
            }
            // fallback to default processing
        } catch (e) {
            if (throwOnError) throw `Cargo runner '${customRunner}' failed! ${e}`;
            // fallback to default processing
        }
    }

    // this is a cargo task; do Cargo-esque processing
    if (definition.type === TASK_TYPE) {
        // Check whether we must use a user-defined substitute for cargo.
        // Split on spaces to allow overrides like "wrapper cargo".
        const cargoCommand = definition.overrideCargo?.split(" ") ?? [definition.command];

        const definitionArgs = expectNotUndefined(
            definition.args,
            "args were not provided via runnables; this is a bug.",
        );
        const args = [...cargoCommand.slice(1), ...definitionArgs];
        const processName = unwrapUndefinable(cargoCommand[0]);

        return new vscode.ProcessExecution(processName, args, {
            cwd: definition.cwd,
            env: definition.env,
        });
    } else {
        // we've been handed a process definition by rust-analyzer, trust all its inputs
        // and make a shell execution.
        const args = unwrapUndefinable(definition.args);

        return new vscode.ProcessExecution(definition.command, args, {
            cwd: definition.cwd,
            env: definition.env,
        });
    }
}

export function activateTaskProvider(config: Config): vscode.Disposable {
    const provider = new RustTaskProvider(config);
    return vscode.tasks.registerTaskProvider(TASK_TYPE, provider);
}