Auto merge of #15896 - minestarks:run-quickpick, r=Veykril

Show placeholder while run command gets runnables from server

This PR fixes a UI annoyance in the VS Code extension when working in large codebases where rust-analyzer can take a few moments to interact with the server. Scenario:

1. Invoke "rust-analyzer: Run" from the command palette or hotkey
2. Quickly start typing to filter the list (or press Enter to accept the last runnable)

We often do this quickly from muscle memory without waiting to see the picker. The picker often takes several seconds to come up, causing us to type garbage into the currently open editor.

Fix:

Show a placeholder item before we call out to the server.

![image](https://github.com/rust-lang/rust-analyzer/assets/16928427/09de6a1c-6f3c-4d29-8031-ba4baeb43282)

Selecting this item does nothing so if the user accidentally hits Enter nothing happens.

The list is populated and the placeholder dismissed when the actual runnables are retrieved. From here the behavior is the same as before.

![image](https://github.com/rust-lang/rust-analyzer/assets/16928427/837c7dfc-c060-4d68-bbf6-df8aa3101b78)
This commit is contained in:
bors 2023-12-08 10:00:37 +00:00
commit c27fc0c945

View File

@ -7,6 +7,8 @@ import type { CtxInit } from "./ctx";
import { makeDebugConfig } from "./debug"; import { makeDebugConfig } from "./debug";
import type { Config, RunnableEnvCfg, RunnableEnvCfgItem } from "./config"; import type { Config, RunnableEnvCfg, RunnableEnvCfgItem } from "./config";
import { unwrapUndefinable } from "./undefinable"; import { unwrapUndefinable } from "./undefinable";
import type { LanguageClient } from "vscode-languageclient/node";
import type { RustEditor } from "./util";
const quickPickButtons = [ const quickPickButtons = [
{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configuration." }, { iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configuration." },
@ -21,73 +23,36 @@ export async function selectRunnable(
const editor = ctx.activeRustEditor; const editor = ctx.activeRustEditor;
if (!editor) return; if (!editor) return;
const client = ctx.client; // show a placeholder while we get the runnables from the server
const textDocument: lc.TextDocumentIdentifier = { const quickPick = vscode.window.createQuickPick();
uri: editor.document.uri.toString(), quickPick.title = "Select Runnable";
}; if (showButtons) {
quickPick.buttons = quickPickButtons;
const runnables = await client.sendRequest(ra.runnables, {
textDocument,
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
});
const items: RunnableQuickPick[] = [];
if (prevRunnable) {
items.push(prevRunnable);
} }
for (const r of runnables) { quickPick.items = [{ label: "Looking for runnables..." }];
if (prevRunnable && JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)) { quickPick.activeItems = [];
continue; quickPick.show();
}
if (debuggeeOnly && (r.label.startsWith("doctest") || r.label.startsWith("cargo"))) { const runnables = await getRunnables(ctx.client, editor, prevRunnable, debuggeeOnly);
continue;
}
items.push(new RunnableQuickPick(r));
}
if (items.length === 0) { if (runnables.length === 0) {
// it is the debug case, run always has at least 'cargo check ...' // it is the debug case, run always has at least 'cargo check ...'
// see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables
await vscode.window.showErrorMessage("There's no debug target!"); await vscode.window.showErrorMessage("There's no debug target!");
quickPick.dispose();
return; return;
} }
return await new Promise((resolve) => { // clear the list before we hook up listeners to to avoid invoking them
const disposables: vscode.Disposable[] = []; // if the user happens to accept the placeholder item
const close = (result?: RunnableQuickPick) => { quickPick.items = [];
resolve(result);
disposables.forEach((d) => d.dispose());
};
const quickPick = vscode.window.createQuickPick<RunnableQuickPick>(); return await populateAndGetSelection(
quickPick.items = items; quickPick as vscode.QuickPick<RunnableQuickPick>,
quickPick.title = "Select Runnable"; runnables,
if (showButtons) { ctx,
quickPick.buttons = quickPickButtons; showButtons,
} );
disposables.push(
quickPick.onDidHide(() => close()),
quickPick.onDidAccept(() => close(quickPick.selectedItems[0])),
quickPick.onDidTriggerButton(async (_button) => {
const runnable = unwrapUndefinable(quickPick.activeItems[0]).runnable;
await makeDebugConfig(ctx, runnable);
close();
}),
quickPick.onDidChangeActive((activeList) => {
if (showButtons && activeList.length > 0) {
const active = unwrapUndefinable(activeList[0]);
if (active.label.startsWith("cargo")) {
// save button makes no sense for `cargo test` or `cargo check`
quickPick.buttons = [];
} else if (quickPick.buttons.length === 0) {
quickPick.buttons = quickPickButtons;
}
}
}),
quickPick,
);
quickPick.show();
});
} }
export class RunnableQuickPick implements vscode.QuickPickItem { export class RunnableQuickPick implements vscode.QuickPickItem {
@ -187,3 +152,75 @@ export function createArgs(runnable: ra.Runnable): string[] {
} }
return args; return args;
} }
async function getRunnables(
client: LanguageClient,
editor: RustEditor,
prevRunnable?: RunnableQuickPick,
debuggeeOnly = false,
): Promise<RunnableQuickPick[]> {
const textDocument: lc.TextDocumentIdentifier = {
uri: editor.document.uri.toString(),
};
const runnables = await client.sendRequest(ra.runnables, {
textDocument,
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
});
const items: RunnableQuickPick[] = [];
if (prevRunnable) {
items.push(prevRunnable);
}
for (const r of runnables) {
if (prevRunnable && JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)) {
continue;
}
if (debuggeeOnly && (r.label.startsWith("doctest") || r.label.startsWith("cargo"))) {
continue;
}
items.push(new RunnableQuickPick(r));
}
return items;
}
async function populateAndGetSelection(
quickPick: vscode.QuickPick<RunnableQuickPick>,
runnables: RunnableQuickPick[],
ctx: CtxInit,
showButtons: boolean,
): Promise<RunnableQuickPick | undefined> {
return new Promise((resolve) => {
const disposables: vscode.Disposable[] = [];
const close = (result?: RunnableQuickPick) => {
resolve(result);
disposables.forEach((d) => d.dispose());
};
disposables.push(
quickPick.onDidHide(() => close()),
quickPick.onDidAccept(() => close(quickPick.selectedItems[0] as RunnableQuickPick)),
quickPick.onDidTriggerButton(async (_button) => {
const runnable = unwrapUndefinable(
quickPick.activeItems[0] as RunnableQuickPick,
).runnable;
await makeDebugConfig(ctx, runnable);
close();
}),
quickPick.onDidChangeActive((activeList) => {
if (showButtons && activeList.length > 0) {
const active = unwrapUndefinable(activeList[0]);
if (active.label.startsWith("cargo")) {
// save button makes no sense for `cargo test` or `cargo check`
quickPick.buttons = [];
} else if (quickPick.buttons.length === 0) {
quickPick.buttons = quickPickButtons;
}
}
}),
quickPick,
);
// populate the list with the actual runnables
quickPick.items = runnables;
});
}