1import * as vscode from 'vscode'; 2import * as lc from 'vscode-languageclient'; 3import * as ra from './lsp_ext'; 4import * as tasks from './tasks'; 5 6import { Ctx } from './ctx'; 7import { makeDebugConfig } from './debug'; 8import { Config, RunnableEnvCfg } from './config'; 9 10const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; 11 12export async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> { 13 const editor = ctx.activeRustEditor; 14 const client = ctx.client; 15 if (!editor || !client) return; 16 17 const textDocument: lc.TextDocumentIdentifier = { 18 uri: editor.document.uri.toString(), 19 }; 20 21 const runnables = await client.sendRequest(ra.runnables, { 22 textDocument, 23 position: client.code2ProtocolConverter.asPosition( 24 editor.selection.active, 25 ), 26 }); 27 const items: RunnableQuickPick[] = []; 28 if (prevRunnable) { 29 items.push(prevRunnable); 30 } 31 for (const r of runnables) { 32 if ( 33 prevRunnable && 34 JSON.stringify(prevRunnable.runnable) === JSON.stringify(r) 35 ) { 36 continue; 37 } 38 39 if (debuggeeOnly && (r.label.startsWith('doctest') || r.label.startsWith('cargo'))) { 40 continue; 41 } 42 items.push(new RunnableQuickPick(r)); 43 } 44 45 if (items.length === 0) { 46 // it is the debug case, run always has at least 'cargo check ...' 47 // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables 48 await vscode.window.showErrorMessage("There's no debug target!"); 49 return; 50 } 51 52 return await new Promise((resolve) => { 53 const disposables: vscode.Disposable[] = []; 54 const close = (result?: RunnableQuickPick) => { 55 resolve(result); 56 disposables.forEach(d => d.dispose()); 57 }; 58 59 const quickPick = vscode.window.createQuickPick<RunnableQuickPick>(); 60 quickPick.items = items; 61 quickPick.title = "Select Runnable"; 62 if (showButtons) { 63 quickPick.buttons = quickPickButtons; 64 } 65 disposables.push( 66 quickPick.onDidHide(() => close()), 67 quickPick.onDidAccept(() => close(quickPick.selectedItems[0])), 68 quickPick.onDidTriggerButton(async (_button) => { 69 await makeDebugConfig(ctx, quickPick.activeItems[0].runnable); 70 close(); 71 }), 72 quickPick.onDidChangeActive((active) => { 73 if (showButtons && active.length > 0) { 74 if (active[0].label.startsWith('cargo')) { 75 // save button makes no sense for `cargo test` or `cargo check` 76 quickPick.buttons = []; 77 } else if (quickPick.buttons.length === 0) { 78 quickPick.buttons = quickPickButtons; 79 } 80 } 81 }), 82 quickPick 83 ); 84 quickPick.show(); 85 }); 86} 87 88export class RunnableQuickPick implements vscode.QuickPickItem { 89 public label: string; 90 public description?: string | undefined; 91 public detail?: string | undefined; 92 public picked?: boolean | undefined; 93 94 constructor(public runnable: ra.Runnable) { 95 this.label = runnable.label; 96 } 97} 98 99export function prepareEnv(runnable: ra.Runnable, runnableEnvCfg: RunnableEnvCfg): Record<string, string> { 100 const env: Record<string, string> = { "RUST_BACKTRACE": "short" }; 101 102 if (runnable.args.expectTest) { 103 env["UPDATE_EXPECT"] = "1"; 104 } 105 106 Object.assign(env, process.env as { [key: string]: string }); 107 108 if (runnableEnvCfg) { 109 if (Array.isArray(runnableEnvCfg)) { 110 for (const it of runnableEnvCfg) { 111 if (!it.mask || new RegExp(it.mask).test(runnable.label)) { 112 Object.assign(env, it.env); 113 } 114 } 115 } else { 116 Object.assign(env, runnableEnvCfg); 117 } 118 } 119 120 return env; 121} 122 123export async function createTask(runnable: ra.Runnable, config: Config): Promise<vscode.Task> { 124 if (runnable.kind !== "cargo") { 125 // rust-analyzer supports only one kind, "cargo" 126 // do not use tasks.TASK_TYPE here, these are completely different meanings. 127 128 throw `Unexpected runnable kind: ${runnable.kind}`; 129 } 130 131 const args = createArgs(runnable); 132 133 const definition: tasks.CargoTaskDefinition = { 134 type: tasks.TASK_TYPE, 135 command: args[0], // run, test, etc... 136 args: args.slice(1), 137 cwd: runnable.args.workspaceRoot || ".", 138 env: prepareEnv(runnable, config.runnableEnv), 139 overrideCargo: runnable.args.overrideCargo, 140 }; 141 142 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 143 const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate() 144 const cargoTask = await tasks.buildCargoTask(target, definition, runnable.label, args, config.cargoRunner, true); 145 146 cargoTask.presentationOptions.clear = true; 147 // Sadly, this doesn't prevent focus stealing if the terminal is currently 148 // hidden, and will become revealed due to task exucution. 149 cargoTask.presentationOptions.focus = false; 150 151 return cargoTask; 152} 153 154export function createArgs(runnable: ra.Runnable): string[] { 155 const args = [...runnable.args.cargoArgs]; // should be a copy! 156 if (runnable.args.cargoExtraArgs) { 157 args.push(...runnable.args.cargoExtraArgs); // Append user-specified cargo options. 158 } 159 if (runnable.args.executableArgs.length > 0) { 160 args.push('--', ...runnable.args.executableArgs); 161 } 162 return args; 163} 164