blob: e21f536f26aa3c5479f114847f778f773a84cbc3 [file] [log] [blame]
import * as vscode from "vscode";
import * as lc from "vscode-languageclient";
import * as ra from "./lsp_ext";
import * as path from "path";
import { type Ctx, type Cmd, type CtxInit, discoverWorkspace } from "./ctx";
import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
import { spawnSync } from "child_process";
import { type RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
import { AstInspector } from "./ast_inspector";
import {
isRustDocument,
isCargoTomlDocument,
sleep,
isRustEditor,
type RustEditor,
type RustDocument,
} from "./util";
import { startDebugSession, makeDebugConfig } from "./debug";
import type { LanguageClient } from "vscode-languageclient/node";
import { LINKED_COMMANDS } from "./client";
import type { DependencyId } from "./dependencies_provider";
import { unwrapUndefinable } from "./undefinable";
export * from "./ast_inspector";
export * from "./run";
export function analyzerStatus(ctx: CtxInit): Cmd {
const tdcp = new (class implements vscode.TextDocumentContentProvider {
readonly uri = vscode.Uri.parse("rust-analyzer-status://status");
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
if (!vscode.window.activeTextEditor) return "";
const client = ctx.client;
const params: ra.AnalyzerStatusParams = {};
const doc = ctx.activeRustEditor?.document;
if (doc != null) {
params.textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
}
return await client.sendRequest(ra.analyzerStatus, params);
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
})();
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-status", tdcp),
);
return async () => {
const document = await vscode.workspace.openTextDocument(tdcp.uri);
tdcp.eventEmitter.fire(tdcp.uri);
void (await vscode.window.showTextDocument(document, {
viewColumn: vscode.ViewColumn.Two,
preserveFocus: true,
}));
};
}
export function memoryUsage(ctx: CtxInit): Cmd {
const tdcp = new (class implements vscode.TextDocumentContentProvider {
readonly uri = vscode.Uri.parse("rust-analyzer-memory://memory");
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
if (!vscode.window.activeTextEditor) return "";
return ctx.client.sendRequest(ra.memoryUsage).then((mem: any) => {
return "Per-query memory usage:\n" + mem + "\n(note: database has been cleared)";
});
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
})();
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-memory", tdcp),
);
return async () => {
tdcp.eventEmitter.fire(tdcp.uri);
const document = await vscode.workspace.openTextDocument(tdcp.uri);
return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
};
}
export function shuffleCrateGraph(ctx: CtxInit): Cmd {
return async () => {
return ctx.client.sendRequest(ra.shuffleCrateGraph);
};
}
export function triggerParameterHints(_: CtxInit): Cmd {
return async () => {
const parameterHintsEnabled = vscode.workspace
.getConfiguration("editor")
.get<boolean>("parameterHints.enabled");
if (parameterHintsEnabled) {
await vscode.commands.executeCommand("editor.action.triggerParameterHints");
}
};
}
export function openLogs(ctx: CtxInit): Cmd {
return async () => {
if (ctx.client.outputChannel) {
ctx.client.outputChannel.show();
}
};
}
export function matchingBrace(ctx: CtxInit): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
if (!editor) return;
const client = ctx.client;
const response = await client.sendRequest(ra.matchingBrace, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
positions: editor.selections.map((s) =>
client.code2ProtocolConverter.asPosition(s.active),
),
});
editor.selections = editor.selections.map((sel, idx) => {
const position = unwrapUndefinable(response[idx]);
const active = client.protocol2CodeConverter.asPosition(position);
const anchor = sel.isEmpty ? active : sel.anchor;
return new vscode.Selection(anchor, active);
});
editor.revealRange(editor.selection);
};
}
export function joinLines(ctx: CtxInit): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
if (!editor) return;
const client = ctx.client;
const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
});
const textEdits = await client.protocol2CodeConverter.asTextEdits(items);
await editor.edit((builder) => {
textEdits.forEach((edit: any) => {
builder.replace(edit.range, edit.newText);
});
});
};
}
export function moveItemUp(ctx: CtxInit): Cmd {
return moveItem(ctx, "Up");
}
export function moveItemDown(ctx: CtxInit): Cmd {
return moveItem(ctx, "Down");
}
export function moveItem(ctx: CtxInit, direction: ra.Direction): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
if (!editor) return;
const client = ctx.client;
const lcEdits = await client.sendRequest(ra.moveItem, {
range: client.code2ProtocolConverter.asRange(editor.selection),
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
direction,
});
if (!lcEdits) return;
const edits = await client.protocol2CodeConverter.asTextEdits(lcEdits);
await applySnippetTextEdits(editor, edits);
};
}
export function onEnter(ctx: CtxInit): Cmd {
async function handleKeypress() {
const editor = ctx.activeRustEditor;
if (!editor) return false;
const client = ctx.client;
const lcEdits = await client
.sendRequest(ra.onEnter, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document,
),
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
})
.catch((_error: any) => {
// client.handleFailedRequest(OnEnterRequest.type, error, null);
return null;
});
if (!lcEdits) return false;
const edits = await client.protocol2CodeConverter.asTextEdits(lcEdits);
await applySnippetTextEdits(editor, edits);
return true;
}
return async () => {
if (await handleKeypress()) return;
await vscode.commands.executeCommand("default:type", { text: "\n" });
};
}
export function parentModule(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
if (!(isRustDocument(editor.document) || isCargoTomlDocument(editor.document))) return;
const client = ctx.client;
const locations = await client.sendRequest(ra.parentModule, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
});
if (!locations) return;
if (locations.length === 1) {
const loc = unwrapUndefinable(locations[0]);
const uri = client.protocol2CodeConverter.asUri(loc.targetUri);
const range = client.protocol2CodeConverter.asRange(loc.targetRange);
const doc = await vscode.workspace.openTextDocument(uri);
const e = await vscode.window.showTextDocument(doc);
e.selection = new vscode.Selection(range.start, range.start);
e.revealRange(range, vscode.TextEditorRevealType.InCenter);
} else {
const uri = editor.document.uri.toString();
const position = client.code2ProtocolConverter.asPosition(editor.selection.active);
await showReferencesImpl(
client,
uri,
position,
locations.map((loc) => lc.Location.create(loc.targetUri, loc.targetRange)),
);
}
};
}
export function openCargoToml(ctx: CtxInit): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
if (!editor) return;
const client = ctx.client;
const response = await client.sendRequest(ra.openCargoToml, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
});
if (!response) return;
const uri = client.protocol2CodeConverter.asUri(response.uri);
const range = client.protocol2CodeConverter.asRange(response.range);
const doc = await vscode.workspace.openTextDocument(uri);
const e = await vscode.window.showTextDocument(doc);
e.selection = new vscode.Selection(range.start, range.start);
e.revealRange(range, vscode.TextEditorRevealType.InCenter);
};
}
export function revealDependency(ctx: CtxInit): Cmd {
return async (editor: RustEditor) => {
if (!ctx.dependencies?.isInitialized()) {
return;
}
const documentPath = editor.document.uri.fsPath;
const dep = ctx.dependencies?.getDependency(documentPath);
if (dep) {
await ctx.treeView?.reveal(dep, { select: true, expand: true });
} else {
await revealParentChain(editor.document, ctx);
}
};
}
/**
* This function calculates the parent chain of a given file until it reaches it crate root contained in ctx.dependencies.
* This is need because the TreeView is Lazy, so at first it only has the root dependencies: For example if we have the following crates:
* - core
* - alloc
* - std
*
* if I want to reveal alloc/src/str.rs, I have to:
* 1. reveal every children of alloc
* - core
* - alloc\
* &emsp;|-beches\
* &emsp;|-src\
* &emsp;|- ...
* - std
* 2. reveal every children of src:
* core
* alloc\
* &emsp;|-beches\
* &emsp;|-src\
* &emsp;&emsp;|- lib.rs\
* &emsp;&emsp;|- str.rs <------- FOUND IT!\
* &emsp;&emsp;|- ...\
* &emsp;|- ...\
* std
*/
async function revealParentChain(document: RustDocument, ctx: CtxInit) {
let documentPath = document.uri.fsPath;
const maxDepth = documentPath.split(path.sep).length - 1;
const parentChain: DependencyId[] = [{ id: documentPath.toLowerCase() }];
do {
documentPath = path.dirname(documentPath);
parentChain.push({ id: documentPath.toLowerCase() });
if (parentChain.length >= maxDepth) {
// this is an odd case that can happen when we change a crate version but we'd still have
// a open file referencing the old version
return;
}
} while (!ctx.dependencies?.contains(documentPath));
parentChain.reverse();
for (const idx in parentChain) {
const treeView = ctx.treeView;
if (!treeView) {
continue;
}
const dependency = unwrapUndefinable(parentChain[idx]);
await treeView.reveal(dependency, { select: true, expand: true });
}
}
export async function execRevealDependency(e: RustEditor): Promise<void> {
await vscode.commands.executeCommand("rust-analyzer.revealDependency", e);
}
export function ssr(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const client = ctx.client;
const position = editor.selection.active;
const selections = editor.selections;
const textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document,
);
const options: vscode.InputBoxOptions = {
value: "() ==>> ()",
prompt: "Enter request, for example 'Foo($a) ==>> Foo::new($a)' ",
validateInput: async (x: string) => {
try {
await client.sendRequest(ra.ssr, {
query: x,
parseOnly: true,
textDocument,
position,
selections,
});
} catch (e) {
return String(e);
}
return null;
},
};
const request = await vscode.window.showInputBox(options);
if (!request) return;
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Structured search replace in progress...",
cancellable: false,
},
async (_progress, token) => {
const edit = await client.sendRequest(ra.ssr, {
query: request,
parseOnly: false,
textDocument,
position,
selections,
});
await vscode.workspace.applyEdit(
await client.protocol2CodeConverter.asWorkspaceEdit(edit, token),
);
},
);
};
}
export function serverVersion(ctx: CtxInit): Cmd {
return async () => {
if (!ctx.serverPath) {
void vscode.window.showWarningMessage(`rust-analyzer server is not running`);
return;
}
const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
const versionString = stdout.slice(`rust-analyzer `.length).trim();
void vscode.window.showInformationMessage(`rust-analyzer version: ${versionString}`);
};
}
// Opens the virtual file that will show the syntax tree
//
// The contents of the file come from the `TextDocumentContentProvider`
export function syntaxTree(ctx: CtxInit): Cmd {
const tdcp = new (class implements vscode.TextDocumentContentProvider {
readonly uri = vscode.Uri.parse("rust-analyzer-syntax-tree://syntaxtree/tree.rast");
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
constructor() {
vscode.workspace.onDidChangeTextDocument(
this.onDidChangeTextDocument,
this,
ctx.subscriptions,
);
vscode.window.onDidChangeActiveTextEditor(
this.onDidChangeActiveTextEditor,
this,
ctx.subscriptions,
);
}
private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
if (isRustDocument(event.document)) {
// We need to order this after language server updates, but there's no API for that.
// Hence, good old sleep().
void sleep(10).then(() => this.eventEmitter.fire(this.uri));
}
}
private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
if (editor && isRustEditor(editor)) {
this.eventEmitter.fire(this.uri);
}
}
async provideTextDocumentContent(
uri: vscode.Uri,
ct: vscode.CancellationToken,
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
if (!rustEditor) return "";
const client = ctx.client;
// When the range based query is enabled we take the range of the selection
const range =
uri.query === "range=true" && !rustEditor.selection.isEmpty
? client.code2ProtocolConverter.asRange(rustEditor.selection)
: null;
const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range };
return client.sendRequest(ra.syntaxTree, params, ct);
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
})();
ctx.pushExtCleanup(new AstInspector(ctx));
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-syntax-tree", tdcp),
);
ctx.pushExtCleanup(
vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
brackets: [["[", ")"]],
}),
);
return async () => {
const editor = vscode.window.activeTextEditor;
const rangeEnabled = !!editor && !editor.selection.isEmpty;
const uri = rangeEnabled ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) : tdcp.uri;
const document = await vscode.workspace.openTextDocument(uri);
tdcp.eventEmitter.fire(uri);
void (await vscode.window.showTextDocument(document, {
viewColumn: vscode.ViewColumn.Two,
preserveFocus: true,
}));
};
}
function viewHirOrMir(ctx: CtxInit, xir: "hir" | "mir"): Cmd {
const viewXir = xir === "hir" ? "viewHir" : "viewMir";
const requestType = xir === "hir" ? ra.viewHir : ra.viewMir;
const uri = `rust-analyzer-${xir}://${viewXir}/${xir}.rs`;
const scheme = `rust-analyzer-${xir}`;
return viewFileUsingTextDocumentContentProvider(ctx, requestType, uri, scheme, true);
}
function viewFileUsingTextDocumentContentProvider(
ctx: CtxInit,
requestType: lc.RequestType<lc.TextDocumentPositionParams, string, void>,
uri: string,
scheme: string,
shouldUpdate: boolean,
): Cmd {
const tdcp = new (class implements vscode.TextDocumentContentProvider {
readonly uri = vscode.Uri.parse(uri);
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
constructor() {
vscode.workspace.onDidChangeTextDocument(
this.onDidChangeTextDocument,
this,
ctx.subscriptions,
);
vscode.window.onDidChangeActiveTextEditor(
this.onDidChangeActiveTextEditor,
this,
ctx.subscriptions,
);
}
private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
if (isRustDocument(event.document) && shouldUpdate) {
// We need to order this after language server updates, but there's no API for that.
// Hence, good old sleep().
void sleep(10).then(() => this.eventEmitter.fire(this.uri));
}
}
private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
if (editor && isRustEditor(editor) && shouldUpdate) {
this.eventEmitter.fire(this.uri);
}
}
async provideTextDocumentContent(
_uri: vscode.Uri,
ct: vscode.CancellationToken,
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
if (!rustEditor) return "";
const client = ctx.client;
const params = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
rustEditor.document,
),
position: client.code2ProtocolConverter.asPosition(rustEditor.selection.active),
};
return client.sendRequest(requestType, params, ct);
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
})();
ctx.pushExtCleanup(vscode.workspace.registerTextDocumentContentProvider(scheme, tdcp));
return async () => {
const document = await vscode.workspace.openTextDocument(tdcp.uri);
tdcp.eventEmitter.fire(tdcp.uri);
void (await vscode.window.showTextDocument(document, {
viewColumn: vscode.ViewColumn.Two,
preserveFocus: true,
}));
};
}
// Opens the virtual file that will show the HIR of the function containing the cursor position
//
// The contents of the file come from the `TextDocumentContentProvider`
export function viewHir(ctx: CtxInit): Cmd {
return viewHirOrMir(ctx, "hir");
}
// Opens the virtual file that will show the MIR of the function containing the cursor position
//
// The contents of the file come from the `TextDocumentContentProvider`
export function viewMir(ctx: CtxInit): Cmd {
return viewHirOrMir(ctx, "mir");
}
// Opens the virtual file that will show the MIR of the function containing the cursor position
//
// The contents of the file come from the `TextDocumentContentProvider`
export function interpretFunction(ctx: CtxInit): Cmd {
const uri = `rust-analyzer-interpret-function://interpretFunction/result.log`;
return viewFileUsingTextDocumentContentProvider(
ctx,
ra.interpretFunction,
uri,
`rust-analyzer-interpret-function`,
false,
);
}
export function viewFileText(ctx: CtxInit): Cmd {
const tdcp = new (class implements vscode.TextDocumentContentProvider {
readonly uri = vscode.Uri.parse("rust-analyzer-file-text://viewFileText/file.rs");
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
constructor() {
vscode.workspace.onDidChangeTextDocument(
this.onDidChangeTextDocument,
this,
ctx.subscriptions,
);
vscode.window.onDidChangeActiveTextEditor(
this.onDidChangeActiveTextEditor,
this,
ctx.subscriptions,
);
}
private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
if (isRustDocument(event.document)) {
// We need to order this after language server updates, but there's no API for that.
// Hence, good old sleep().
void sleep(10).then(() => this.eventEmitter.fire(this.uri));
}
}
private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
if (editor && isRustEditor(editor)) {
this.eventEmitter.fire(this.uri);
}
}
async provideTextDocumentContent(
_uri: vscode.Uri,
ct: vscode.CancellationToken,
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
if (!rustEditor) return "";
const client = ctx.client;
const params = client.code2ProtocolConverter.asTextDocumentIdentifier(
rustEditor.document,
);
return client.sendRequest(ra.viewFileText, params, ct);
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
})();
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-file-text", tdcp),
);
return async () => {
const document = await vscode.workspace.openTextDocument(tdcp.uri);
tdcp.eventEmitter.fire(tdcp.uri);
void (await vscode.window.showTextDocument(document, {
viewColumn: vscode.ViewColumn.Two,
preserveFocus: true,
}));
};
}
export function viewItemTree(ctx: CtxInit): Cmd {
const tdcp = new (class implements vscode.TextDocumentContentProvider {
readonly uri = vscode.Uri.parse("rust-analyzer-item-tree://viewItemTree/itemtree.rs");
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
constructor() {
vscode.workspace.onDidChangeTextDocument(
this.onDidChangeTextDocument,
this,
ctx.subscriptions,
);
vscode.window.onDidChangeActiveTextEditor(
this.onDidChangeActiveTextEditor,
this,
ctx.subscriptions,
);
}
private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
if (isRustDocument(event.document)) {
// We need to order this after language server updates, but there's no API for that.
// Hence, good old sleep().
void sleep(10).then(() => this.eventEmitter.fire(this.uri));
}
}
private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
if (editor && isRustEditor(editor)) {
this.eventEmitter.fire(this.uri);
}
}
async provideTextDocumentContent(
_uri: vscode.Uri,
ct: vscode.CancellationToken,
): Promise<string> {
const rustEditor = ctx.activeRustEditor;
if (!rustEditor) return "";
const client = ctx.client;
const params = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
rustEditor.document,
),
};
return client.sendRequest(ra.viewItemTree, params, ct);
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
})();
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-item-tree", tdcp),
);
return async () => {
const document = await vscode.workspace.openTextDocument(tdcp.uri);
tdcp.eventEmitter.fire(tdcp.uri);
void (await vscode.window.showTextDocument(document, {
viewColumn: vscode.ViewColumn.Two,
preserveFocus: true,
}));
};
}
function crateGraph(ctx: CtxInit, full: boolean): Cmd {
return async () => {
const nodeModulesPath = vscode.Uri.file(path.join(ctx.extensionPath, "node_modules"));
const panel = vscode.window.createWebviewPanel(
"rust-analyzer.crate-graph",
"rust-analyzer crate graph",
vscode.ViewColumn.Two,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [nodeModulesPath],
},
);
const params = {
full: full,
};
const client = ctx.client;
const dot = await client.sendRequest(ra.viewCrateGraph, params);
const uri = panel.webview.asWebviewUri(nodeModulesPath);
const html = `
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
/* Fill the entire view */
html, body { margin:0; padding:0; overflow:hidden }
svg { position:fixed; top:0; left:0; height:100%; width:100% }
/* Disable the graphviz background and fill the polygons */
.graph > polygon { display:none; }
:is(.node,.edge) polygon { fill: white; }
/* Invert the line colours for dark themes */
body:not(.vscode-light) .edge path { stroke: white; }
</style>
</head>
<body>
<script type="text/javascript" src="${uri}/d3/dist/d3.min.js"></script>
<script type="text/javascript" src="${uri}/@hpcc-js/wasm/dist/graphviz.umd.js"></script>
<script type="text/javascript" src="${uri}/d3-graphviz/build/d3-graphviz.min.js"></script>
<div id="graph"></div>
<script>
let dot = \`${dot}\`;
let graph = d3.select("#graph")
.graphviz({ useWorker: false, useSharedWorker: false })
.fit(true)
.zoomScaleExtent([0.1, Infinity])
.renderDot(dot);
d3.select(window).on("click", (event) => {
if (event.ctrlKey) {
graph.resetZoom(d3.transition().duration(100));
}
});
d3.select(window).on("copy", (event) => {
event.clipboardData.setData("text/plain", dot);
event.preventDefault();
});
</script>
</body>
`;
panel.webview.html = html;
};
}
export function viewCrateGraph(ctx: CtxInit): Cmd {
return crateGraph(ctx, false);
}
export function viewFullCrateGraph(ctx: CtxInit): Cmd {
return crateGraph(ctx, true);
}
// Opens the virtual file that will show the syntax tree
//
// The contents of the file come from the `TextDocumentContentProvider`
export function expandMacro(ctx: CtxInit): Cmd {
function codeFormat(expanded: ra.ExpandedMacro): string {
let result = `// Recursive expansion of ${expanded.name} macro\n`;
result += "// " + "=".repeat(result.length - 3);
result += "\n\n";
result += expanded.expansion;
return result;
}
const tdcp = new (class implements vscode.TextDocumentContentProvider {
uri = vscode.Uri.parse("rust-analyzer-expand-macro://expandMacro/[EXPANSION].rs");
eventEmitter = new vscode.EventEmitter<vscode.Uri>();
async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
const editor = vscode.window.activeTextEditor;
if (!editor) return "";
const client = ctx.client;
const position = editor.selection.active;
const expanded = await client.sendRequest(ra.expandMacro, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
editor.document,
),
position,
});
if (expanded == null) return "Not available";
return codeFormat(expanded);
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
})();
ctx.pushExtCleanup(
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-expand-macro", tdcp),
);
return async () => {
const document = await vscode.workspace.openTextDocument(tdcp.uri);
tdcp.eventEmitter.fire(tdcp.uri);
return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
};
}
export function reloadWorkspace(ctx: CtxInit): Cmd {
return async () => ctx.client.sendRequest(ra.reloadWorkspace);
}
export function rebuildProcMacros(ctx: CtxInit): Cmd {
return async () => ctx.client.sendRequest(ra.rebuildProcMacros);
}
export function addProject(ctx: CtxInit): Cmd {
return async () => {
const discoverProjectCommand = ctx.config.discoverProjectCommand;
if (!discoverProjectCommand) {
return;
}
const workspaces: JsonProject[] = await Promise.all(
vscode.workspace.textDocuments
.filter(isRustDocument)
.map(async (file): Promise<JsonProject> => {
return discoverWorkspace([file], discoverProjectCommand, {
cwd: path.dirname(file.uri.fsPath),
});
}),
);
ctx.addToDiscoveredWorkspaces(workspaces);
// this is a workaround to avoid needing writing the `rust-project.json` into
// a workspace-level VS Code-specific settings folder. We'd like to keep the
// `rust-project.json` entirely in-memory.
await ctx.client?.sendNotification(lc.DidChangeConfigurationNotification.type, {
settings: "",
});
};
}
async function showReferencesImpl(
client: LanguageClient | undefined,
uri: string,
position: lc.Position,
locations: lc.Location[],
) {
if (client) {
await vscode.commands.executeCommand(
"editor.action.showReferences",
vscode.Uri.parse(uri),
client.protocol2CodeConverter.asPosition(position),
locations.map(client.protocol2CodeConverter.asLocation),
);
}
}
export function showReferences(ctx: CtxInit): Cmd {
return async (uri: string, position: lc.Position, locations: lc.Location[]) => {
await showReferencesImpl(ctx.client, uri, position, locations);
};
}
export function applyActionGroup(_ctx: CtxInit): Cmd {
return async (actions: { label: string; arguments: lc.CodeAction }[]) => {
const selectedAction = await vscode.window.showQuickPick(actions);
if (!selectedAction) return;
await vscode.commands.executeCommand(
"rust-analyzer.resolveCodeAction",
selectedAction.arguments,
);
};
}
export function gotoLocation(ctx: CtxInit): Cmd {
return async (locationLink: lc.LocationLink) => {
const client = ctx.client;
const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
// collapse the range to a cursor position
range = range.with({ end: range.start });
await vscode.window.showTextDocument(uri, { selection: range });
};
}
export function openDocs(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const client = ctx.client;
const position = editor.selection.active;
const textDocument = { uri: editor.document.uri.toString() };
const doclink = await client.sendRequest(ra.openDocs, { position, textDocument });
if (doclink != null) {
await vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(doclink));
}
};
}
export function cancelFlycheck(ctx: CtxInit): Cmd {
return async () => {
await ctx.client.sendNotification(ra.cancelFlycheck);
};
}
export function clearFlycheck(ctx: CtxInit): Cmd {
return async () => {
await ctx.client.sendNotification(ra.clearFlycheck);
};
}
export function runFlycheck(ctx: CtxInit): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
const client = ctx.client;
const params = editor ? { uri: editor.document.uri.toString() } : null;
await client.sendNotification(ra.runFlycheck, { textDocument: params });
};
}
export function resolveCodeAction(ctx: CtxInit): Cmd {
return async (params: lc.CodeAction) => {
const client = ctx.client;
params.command = undefined;
const item = await client.sendRequest(lc.CodeActionResolveRequest.type, params);
if (!item?.edit) {
return;
}
const itemEdit = item.edit;
const edit = await client.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
// filter out all text edits and recreate the WorkspaceEdit without them so we can apply
// snippet edits on our own
const lcFileSystemEdit = {
...itemEdit,
documentChanges: itemEdit.documentChanges?.filter((change) => "kind" in change),
};
const fileSystemEdit = await client.protocol2CodeConverter.asWorkspaceEdit(
lcFileSystemEdit,
);
await vscode.workspace.applyEdit(fileSystemEdit);
await applySnippetWorkspaceEdit(edit);
if (item.command != null) {
await vscode.commands.executeCommand(item.command.command, item.command.arguments);
}
};
}
export function applySnippetWorkspaceEditCommand(_ctx: CtxInit): Cmd {
return async (edit: vscode.WorkspaceEdit) => {
await applySnippetWorkspaceEdit(edit);
};
}
export function run(ctx: CtxInit): Cmd {
let prevRunnable: RunnableQuickPick | undefined;
return async () => {
const item = await selectRunnable(ctx, prevRunnable);
if (!item) return;
item.detail = "rerun";
prevRunnable = item;
const task = await createTask(item.runnable, ctx.config);
return await vscode.tasks.executeTask(task);
};
}
export function peekTests(ctx: CtxInit): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
if (!editor) return;
const client = ctx.client;
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Looking for tests...",
cancellable: false,
},
async (_progress, _token) => {
const uri = editor.document.uri.toString();
const position = client.code2ProtocolConverter.asPosition(editor.selection.active);
const tests = await client.sendRequest(ra.relatedTests, {
textDocument: { uri: uri },
position: position,
});
const locations: lc.Location[] = tests.map((it) =>
lc.Location.create(
it.runnable.location!.targetUri,
it.runnable.location!.targetSelectionRange,
),
);
await showReferencesImpl(client, uri, position, locations);
},
);
};
}
export function runSingle(ctx: CtxInit): Cmd {
return async (runnable: ra.Runnable) => {
const editor = ctx.activeRustEditor;
if (!editor) return;
const task = await createTask(runnable, ctx.config);
task.group = vscode.TaskGroup.Build;
task.presentationOptions = {
reveal: vscode.TaskRevealKind.Always,
panel: vscode.TaskPanelKind.Dedicated,
clear: true,
};
return vscode.tasks.executeTask(task);
};
}
export function copyRunCommandLine(ctx: CtxInit) {
let prevRunnable: RunnableQuickPick | undefined;
return async () => {
const item = await selectRunnable(ctx, prevRunnable);
if (!item) return;
const args = createArgs(item.runnable);
const commandLine = ["cargo", ...args].join(" ");
await vscode.env.clipboard.writeText(commandLine);
await vscode.window.showInformationMessage("Cargo invocation copied to the clipboard.");
};
}
export function debug(ctx: CtxInit): Cmd {
let prevDebuggee: RunnableQuickPick | undefined;
return async () => {
const item = await selectRunnable(ctx, prevDebuggee, true);
if (!item) return;
item.detail = "restart";
prevDebuggee = item;
return await startDebugSession(ctx, item.runnable);
};
}
export function debugSingle(ctx: CtxInit): Cmd {
return async (config: ra.Runnable) => {
await startDebugSession(ctx, config);
};
}
export function newDebugConfig(ctx: CtxInit): Cmd {
return async () => {
const item = await selectRunnable(ctx, undefined, true, false);
if (!item) return;
await makeDebugConfig(ctx, item.runnable);
};
}
export function linkToCommand(_: Ctx): Cmd {
return async (commandId: string) => {
const link = LINKED_COMMANDS.get(commandId);
if (link) {
const { command, arguments: args = [] } = link;
await vscode.commands.executeCommand(command, ...args);
}
};
}
export function viewMemoryLayout(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const client = ctx.client;
const position = editor.selection.active;
const expanded = await client.sendRequest(ra.viewRecursiveMemoryLayout, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
position,
});
const document = vscode.window.createWebviewPanel(
"memory_layout",
"[Memory Layout]",
vscode.ViewColumn.Two,
{ enableScripts: true },
);
document.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
min-height: 100%;
height: 100vh;
padding: 32px;
position: relative;
display: block;
background-color: var(--vscode-editor-background);
font-family: var(--vscode-editor-font-family);
font-size: var(--vscode-editor-font-size);
color: var(--vscode-editor-foreground);
}
.container {
position: relative;
}
.trans {
transition: all 0.2s ease-in-out;
}
.grid {
height: 100%;
position: relative;
color: var(--vscode-commandCenter-activeBorder);
pointer-events: none;
}
.grid-line {
position: absolute;
width: 100%;
height: 1px;
background-color: var(--vscode-commandCenter-activeBorder);
}
#tooltip {
position: fixed;
display: none;
z-index: 1;
pointer-events: none;
padding: 4px 8px;
z-index: 2;
color: var(--vscode-editorHoverWidget-foreground);
background-color: var(--vscode-editorHoverWidget-background);
border: 1px solid var(--vscode-editorHoverWidget-border);
}
#tooltip b {
color: var(--vscode-editorInlayHint-typeForeground);
}
#tooltip ul {
margin-left: 0;
padding-left: 20px;
}
table {
position: absolute;
transform: rotateZ(90deg) rotateX(180deg);
transform-origin: top left;
border-collapse: collapse;
table-layout: fixed;
left: 48px;
top: 0;
max-height: calc(100vw - 64px - 48px);
z-index: 1;
}
td {
border: 1px solid var(--vscode-focusBorder);
writing-mode: vertical-rl;
text-orientation: sideways-right;
height: 80px;
}
td p {
height: calc(100% - 16px);
width: calc(100% - 8px);
margin: 8px 4px;
display: inline-block;
transform: rotateY(180deg);
pointer-events: none;
overflow: hidden;
}
td p * {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
height: 100%;
}
td p b {
color: var(--vscode-editorInlayHint-typeForeground);
}
td:hover {
background-color: var(--vscode-editor-hoverHighlightBackground);
}
td:empty {
visibility: hidden;
border: 0;
}
</style>
</head>
<body>
<div id="tooltip"></div>
</body>
<script>(function() {
const data = ${JSON.stringify(expanded)}
if (!(data && data.nodes.length)) {
document.body.innerText = "Not Available"
return
}
data.nodes.map(n => {
n.typename = n.typename.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', ' & quot; ').replaceAll("'", '&#039;')
return n
})
let height = window.innerHeight - 64
addEventListener("resize", e => {
const new_height = window.innerHeight - 64
height = new_height
container.classList.remove("trans")
table.classList.remove("trans")
locate()
setTimeout(() => { // give delay to redraw, annoying but needed
container.classList.add("trans")
table.classList.add("trans")
}, 0)
})
const container = document.createElement("div")
container.classList.add("container")
container.classList.add("trans")
document.body.appendChild(container)
const tooltip = document.getElementById("tooltip")
let y = 0
let zoom = 1.0
const table = document.createElement("table")
table.classList.add("trans")
container.appendChild(table)
const rows = []
function node_t(idx, depth, offset) {
if (!rows[depth]) {
rows[depth] = { el: document.createElement("tr"), offset: 0 }
}
if (rows[depth].offset < offset) {
const pad = document.createElement("td")
pad.colSpan = offset - rows[depth].offset
rows[depth].el.appendChild(pad)
rows[depth].offset += offset - rows[depth].offset
}
const td = document.createElement("td")
td.innerHTML = '<p><span>' + data.nodes[idx].itemName + ':</span> <b>' + data.nodes[idx].typename + '</b></p>'
td.colSpan = data.nodes[idx].size
td.addEventListener("mouseover", e => {
const node = data.nodes[idx]
tooltip.innerHTML = node.itemName + ": <b>" + node.typename + "</b><br/>"
+ "<ul>"
+ "<li>size = " + node.size + "</li>"
+ "<li>align = " + node.alignment + "</li>"
+ "<li>field offset = " + node.offset + "</li>"
+ "</ul>"
+ "<i>double click to focus</i>"
tooltip.style.display = "block"
})
td.addEventListener("mouseleave", _ => tooltip.style.display = "none")
const total_offset = rows[depth].offset
td.addEventListener("dblclick", e => {
const node = data.nodes[idx]
zoom = data.nodes[0].size / node.size
y = -(total_offset) / data.nodes[0].size * zoom
x = 0
locate()
})
rows[depth].el.appendChild(td)
rows[depth].offset += data.nodes[idx].size
if (data.nodes[idx].childrenStart != -1) {
for (let i = 0; i < data.nodes[idx].childrenLen; i++) {
if (data.nodes[data.nodes[idx].childrenStart + i].size) {
node_t(data.nodes[idx].childrenStart + i, depth + 1, offset + data.nodes[data.nodes[idx].childrenStart + i].offset)
}
}
}
}
node_t(0, 0, 0)
for (const row of rows) table.appendChild(row.el)
const grid = document.createElement("div")
grid.classList.add("grid")
container.appendChild(grid)
for (let i = 0; i < data.nodes[0].size / 8 + 1; i++) {
const el = document.createElement("div")
el.classList.add("grid-line")
el.style.top = (i / (data.nodes[0].size / 8) * 100) + "%"
el.innerText = i * 8
grid.appendChild(el)
}
addEventListener("mousemove", e => {
tooltip.style.top = e.clientY + 10 + "px"
tooltip.style.left = e.clientX + 10 + "px"
})
function locate() {
container.style.top = height * y + "px"
container.style.height = (height * zoom) + "px"
table.style.width = container.style.height
}
locate()
})()
</script>
</html>`;
ctx.pushExtCleanup(document);
};
}