From 98e7b944fcbf1439d9f99581ea35f37760e02aa9 Mon Sep 17 00:00:00 2001 From: Mark Pots Date: Mon, 23 Jun 2025 21:27:26 +0200 Subject: [PATCH 1/2] feat: Extend vscode 'run' command with optional mode argument for running test(s) or bin at keyboard cursor --- .../editors/code/src/commands.ts | 4 +- .../rust-analyzer/editors/code/src/ctx.ts | 13 ++++- .../rust-analyzer/editors/code/src/run.ts | 57 +++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/tools/rust-analyzer/editors/code/src/commands.ts b/src/tools/rust-analyzer/editors/code/src/commands.ts index 3ac1a933d9ec..25b30013fa1c 100644 --- a/src/tools/rust-analyzer/editors/code/src/commands.ts +++ b/src/tools/rust-analyzer/editors/code/src/commands.ts @@ -1114,11 +1114,11 @@ export function applySnippetWorkspaceEditCommand(_ctx: CtxInit): Cmd { }; } -export function run(ctx: CtxInit): Cmd { +export function run(ctx: CtxInit, mode?: "cursor"): Cmd { let prevRunnable: RunnableQuickPick | undefined; return async () => { - const item = await selectRunnable(ctx, prevRunnable); + const item = await selectRunnable(ctx, prevRunnable, false, true, mode); if (!item) return; item.detail = "rerun"; diff --git a/src/tools/rust-analyzer/editors/code/src/ctx.ts b/src/tools/rust-analyzer/editors/code/src/ctx.ts index e55754fb9f04..0fb0a1f17cbd 100644 --- a/src/tools/rust-analyzer/editors/code/src/ctx.ts +++ b/src/tools/rust-analyzer/editors/code/src/ctx.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import type * as lc from "vscode-languageclient/node"; import * as ra from "./lsp_ext"; +import * as commands from "./commands"; import { Config, prepareVSCodeConfig } from "./config"; import { createClient } from "./client"; @@ -462,9 +463,17 @@ export class Ctx implements RustAnalyzerExtensionApi { for (const [name, factory] of Object.entries(this.commandFactories)) { const fullName = `rust-analyzer.${name}`; let callback; + if (isClientRunning(this)) { - // we asserted that `client` is defined - callback = factory.enabled(this); + if (name === "run") { + // Special case: support optional argument for `run` + callback = (mode?: "cursor") => { + return commands.run(this, mode)(); + }; + } else { + // we asserted that `client` is defined + callback = factory.enabled(this); + } } else if (factory.disabled) { callback = factory.disabled(this); } else { diff --git a/src/tools/rust-analyzer/editors/code/src/run.ts b/src/tools/rust-analyzer/editors/code/src/run.ts index 40027cc7c857..95166c427b2b 100644 --- a/src/tools/rust-analyzer/editors/code/src/run.ts +++ b/src/tools/rust-analyzer/editors/code/src/run.ts @@ -18,10 +18,15 @@ export async function selectRunnable( prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true, + mode?: "cursor", ): Promise { const editor = ctx.activeRustEditor ?? ctx.activeCargoTomlEditor; if (!editor) return; + if (mode === "cursor") { + return selectRunnableAtCursor(ctx, editor, prevRunnable); + } + // show a placeholder while we get the runnables from the server const quickPick = vscode.window.createQuickPick(); quickPick.title = "Select Runnable"; @@ -54,6 +59,58 @@ export async function selectRunnable( ); } +async function selectRunnableAtCursor( + ctx: CtxInit, + editor: RustEditor, + prevRunnable?: RunnableQuickPick, +): Promise { + const runnableQuickPicks = await getRunnables(ctx.client, editor, prevRunnable, false); + let runnableQuickPickAtCursor = null; + const cursorPosition = ctx.client.code2ProtocolConverter.asPosition(editor.selection.active); + for (const runnableQuickPick of runnableQuickPicks) { + if (!runnableQuickPick.runnable.location?.targetRange) { + continue; + } + const runnableQuickPickRange = runnableQuickPick.runnable.location.targetRange; + if ( + runnableQuickPickAtCursor?.runnable?.location?.targetRange != null && + rangeContainsOtherRange( + runnableQuickPickRange, + runnableQuickPickAtCursor.runnable.location.targetRange, + ) + ) { + continue; + } + if (rangeContainsPosition(runnableQuickPickRange, cursorPosition)) { + runnableQuickPickAtCursor = runnableQuickPick; + } + } + if (runnableQuickPickAtCursor == null) { + return; + } + return Promise.resolve(runnableQuickPickAtCursor); +} + +function rangeContainsPosition(range: lc.Range, position: lc.Position): boolean { + return ( + (position.line > range.start.line || + (position.line === range.start.line && position.character >= range.start.character)) && + (position.line < range.end.line || + (position.line === range.end.line && position.character <= range.end.character)) + ); +} + +function rangeContainsOtherRange(range: lc.Range, otherRange: lc.Range) { + return ( + (range.start.line < otherRange.start.line || + (range.start.line === otherRange.start.line && + range.start.character <= otherRange.start.character)) && + (range.end.line > otherRange.end.line || + (range.end.line === otherRange.end.line && + range.end.character >= otherRange.end.character)) + ); +} + export class RunnableQuickPick implements vscode.QuickPickItem { public label: string; public description?: string | undefined; From 7eb776e22eff6704df27c7023f289ef04fec12d7 Mon Sep 17 00:00:00 2001 From: Mark Pots Date: Tue, 24 Jun 2025 11:56:22 +0200 Subject: [PATCH 2/2] Remove special casing in command factory (revert changes in ctx.ts), update main.createCommands instead --- src/tools/rust-analyzer/editors/code/src/ctx.ts | 13 ++----------- src/tools/rust-analyzer/editors/code/src/main.ts | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/tools/rust-analyzer/editors/code/src/ctx.ts b/src/tools/rust-analyzer/editors/code/src/ctx.ts index 0fb0a1f17cbd..e55754fb9f04 100644 --- a/src/tools/rust-analyzer/editors/code/src/ctx.ts +++ b/src/tools/rust-analyzer/editors/code/src/ctx.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; import type * as lc from "vscode-languageclient/node"; import * as ra from "./lsp_ext"; -import * as commands from "./commands"; import { Config, prepareVSCodeConfig } from "./config"; import { createClient } from "./client"; @@ -463,17 +462,9 @@ export class Ctx implements RustAnalyzerExtensionApi { for (const [name, factory] of Object.entries(this.commandFactories)) { const fullName = `rust-analyzer.${name}`; let callback; - if (isClientRunning(this)) { - if (name === "run") { - // Special case: support optional argument for `run` - callback = (mode?: "cursor") => { - return commands.run(this, mode)(); - }; - } else { - // we asserted that `client` is defined - callback = factory.enabled(this); - } + // we asserted that `client` is defined + callback = factory.enabled(this); } else if (factory.disabled) { callback = factory.disabled(this); } else { diff --git a/src/tools/rust-analyzer/editors/code/src/main.ts b/src/tools/rust-analyzer/editors/code/src/main.ts index 5e500730693f..996298524f11 100644 --- a/src/tools/rust-analyzer/editors/code/src/main.ts +++ b/src/tools/rust-analyzer/editors/code/src/main.ts @@ -167,7 +167,7 @@ function createCommands(): Record { viewCrateGraph: { enabled: commands.viewCrateGraph }, viewFullCrateGraph: { enabled: commands.viewFullCrateGraph }, expandMacro: { enabled: commands.expandMacro }, - run: { enabled: commands.run }, + run: { enabled: (ctx) => (mode?: "cursor") => commands.run(ctx, mode)() }, copyRunCommandLine: { enabled: commands.copyRunCommandLine }, debug: { enabled: commands.debug }, newDebugConfig: { enabled: commands.newDebugConfig },