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
import * as vscode from "vscode";
import type { Config } from "./config";
import * as toolchain from "./toolchain";
import { Env } from "./util";

// 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 CARGO_TASK_TYPE = "cargo";
export const SHELL_TASK_TYPE = "shell";

export const RUST_TASK_SOURCE = "rust";

export type TaskDefinition = vscode.TaskDefinition & {
    readonly type: typeof CARGO_TASK_TYPE | typeof SHELL_TASK_TYPE;
    args?: string[];
    command: string;
};

export type CargoTaskDefinition = {
    env?: Record<string, string>;
    type: typeof CARGO_TASK_TYPE;
} & TaskDefinition;

function isCargoTask(definition: vscode.TaskDefinition): definition is CargoTaskDefinition {
    return definition.type === CARGO_TASK_TYPE;
}

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

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

    async provideTasks(): Promise<vscode.Task[]> {
        if (!vscode.workspace.workspaceFolders) {
            return [];
        }

        // 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 },
        ];

        // FIXME: The server should provide this
        const cargo = await toolchain.cargoPath();

        const tasks: vscode.Task[] = [];
        for (const workspaceTarget of vscode.workspace.workspaceFolders) {
            for (const def of defs) {
                const definition = {
                    command: def.command,
                    type: CARGO_TASK_TYPE,
                } as const;
                const exec = await targetToExecution(definition, {}, cargo);
                const vscodeTask = await buildRustTask(
                    workspaceTarget,
                    definition,
                    `cargo ${def.command}`,
                    this.config.problemMatcher,
                    exec,
                );
                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.
        if (isCargoTask(task.definition)) {
            const exec = await targetToExecution(task.definition, { env: task.definition.env });
            return buildRustTask(
                task.scope,
                task.definition,
                task.name,
                task.problemMatchers,
                exec,
            );
        }

        return undefined;
    }
}

export async function buildRustTask(
    scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
    definition: TaskDefinition,
    name: string,
    problemMatcher: string[],
    exec: vscode.ProcessExecution | vscode.ShellExecution,
): Promise<vscode.Task> {
    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,
        RUST_TASK_SOURCE,
        exec,
        problemMatcher,
    );
}

export async function targetToExecution(
    definition: TaskDefinition,
    options?: {
        cwd?: string;
        env?: Env;
    },
    cargo?: string,
): Promise<vscode.ProcessExecution | vscode.ShellExecution> {
    let command, args;
    if (isCargoTask(definition)) {
        // FIXME: The server should provide cargo
        command = cargo || (await toolchain.cargoPath(options?.env));
        args = [definition.command].concat(definition.args || []);
    } else {
        command = definition.command;
        args = definition.args || [];
    }
    return new vscode.ProcessExecution(command, args, {
        cwd: options?.cwd,
        env: Object.fromEntries(
            Object.entries(options?.env ?? {}).map(([key, value]) => [key, value ?? ""]),
        ),
    });
}

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