diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index c960e283d35..bff9bc30f93 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -7,14 +7,46 @@ import { WorkspaceEdit } from "vscode"; import { Workspace } from "./ctx"; import { updateConfig } from "./config"; import { substituteVariablesInEnv } from "./config"; +import { randomUUID } from "crypto"; export interface Env { [name: string]: string; } -function renderCommand(cmd: ra.CommandLink) { - return `[${cmd.title}](command:${cmd.command}?${encodeURIComponent( - JSON.stringify(cmd.arguments) +// Command URIs have a form of command:command-name?arguments, where +// arguments is a percent-encoded array of data we want to pass along to +// the command function. For "Show References" this is a list of all file +// URIs with locations of every reference, and it can get quite long. +// +// To work around it we use an intermediary linkToCommand command. When +// we render a command link, a reference to a command with all its arguments +// is stored in a map, and instead a linkToCommand link is rendered +// with the key to that map. +export const LINKED_COMMANDS = new Map(); + +// For now the map is cleaned up periodically (I've set it to every +// 10 minutes). In general case we'll probably need to introduce TTLs or +// flags to denote ephemeral links (like these in hover popups) and +// persistent links and clean those separately. But for now simply keeping +// the last few links in the map should be good enough. Likewise, we could +// add code to remove a target command from the map after the link is +// clicked, but assuming most links in hover sheets won't be clicked anyway +// this code won't change the overall memory use much. +setInterval(function cleanupOlderCommandLinks() { + // keys are returned in insertion order, we'll keep a few + // of recent keys available, and clean the rest + const keys = [...LINKED_COMMANDS.keys()]; + const keysToRemove = keys.slice(0, keys.length - 10); + for (const key of keysToRemove) { + LINKED_COMMANDS.delete(key); + } +}, 10 * 60 * 1000); + +function renderCommand(cmd: ra.CommandLink): string { + const commandId = randomUUID(); + LINKED_COMMANDS.set(commandId, cmd); + return `[${cmd.title}](command:rust-analyzer.linkToCommand?${encodeURIComponent( + JSON.stringify([commandId]) )} '${cmd.tooltip}')`; } diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index ae94ecac6d5..ee8fa2eb51d 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -11,6 +11,7 @@ import { AstInspector } from "./ast_inspector"; import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor } from "./util"; import { startDebugSession, makeDebugConfig } from "./debug"; import { LanguageClient } from "vscode-languageclient/node"; +import { LINKED_COMMANDS } from "./client"; export * from "./ast_inspector"; export * from "./run"; @@ -928,3 +929,13 @@ export function newDebugConfig(ctx: Ctx): Cmd { await makeDebugConfig(ctx, item.runnable); }; } + +export function linkToCommand(ctx: Ctx): Cmd { + return async (commandId: string) => { + const link = LINKED_COMMANDS.get(commandId); + if (ctx.client && link) { + const { command, arguments: args = [] } = link; + await vscode.commands.executeCommand(command, ...args); + } + }; +} diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 61824fae21f..7c5be156eba 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -160,6 +160,8 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) { ctx.registerCommand("resolveCodeAction", commands.resolveCodeAction); ctx.registerCommand("applyActionGroup", commands.applyActionGroup); ctx.registerCommand("gotoLocation", commands.gotoLocation); + + ctx.registerCommand("linkToCommand", commands.linkToCommand); } export async function deactivate() {