diff options
Diffstat (limited to 'src/tools/rust-analyzer/editors/code')
14 files changed, 768 insertions, 68 deletions
diff --git a/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json index c5eb08748bf..ee1f832d323 100644 --- a/src/tools/rust-analyzer/editors/code/package.json +++ b/src/tools/rust-analyzer/editors/code/package.json @@ -44,7 +44,8 @@ "anser": "^2.1.1", "d3": "^7.6.1", "d3-graphviz": "^5.0.2", - "vscode-languageclient": "^8.0.2" + "vscode-languageclient": "^8.0.2", + "@hpcc-js/wasm": "2.5.0" }, "devDependencies": { "@types/node": "~16.11.7", @@ -120,6 +121,11 @@ "category": "rust-analyzer (debug command)" }, { + "command": "rust-analyzer.interpretFunction", + "title": "Interpret Function", + "category": "rust-analyzer (debug command)" + }, + { "command": "rust-analyzer.viewFileText", "title": "View File Text (as seen by the server)", "category": "rust-analyzer (debug command)" @@ -200,12 +206,17 @@ "category": "rust-analyzer" }, { + "command": "rust-analyzer.rebuildProcMacros", + "title": "Rebuild proc macros and build scripts", + "category": "rust-analyzer" + }, + { "command": "rust-analyzer.addProject", "title": "Add current file's crate to workspace", "category": "rust-analyzer" }, { - "command": "rust-analyzer.reload", + "command": "rust-analyzer.restartServer", "title": "Restart server", "category": "rust-analyzer" }, @@ -273,6 +284,11 @@ "command": "rust-analyzer.clearFlycheck", "title": "Clear flycheck diagnostics", "category": "rust-analyzer" + }, + { + "command": "rust-analyzer.revealDependency", + "title": "Reveal File", + "category": "rust-analyzer" } ], "keybindings": [ @@ -444,6 +460,16 @@ "type": "string" } }, + "rust-analyzer.showUnlinkedFileNotification": { + "markdownDescription": "Whether to show a notification for unlinked files asking the user to add the corresponding Cargo.toml to the linked projects setting.", + "default": true, + "type": "boolean" + }, + "rust-analyzer.showDependenciesExplorer": { + "markdownDescription": "Whether to show the dependencies view.", + "default": true, + "type": "boolean" + }, "$generated-start": {}, "rust-analyzer.assist.emitMustUse": { "markdownDescription": "Whether to insert #[must_use] when generating `as_` methods\nfor enum variants.", @@ -527,6 +553,11 @@ "default": true, "type": "boolean" }, + "rust-analyzer.cargo.cfgs": { + "markdownDescription": "List of cfg options to enable with the given values.", + "default": {}, + "type": "object" + }, "rust-analyzer.cargo.extraArgs": { "markdownDescription": "Extra arguments that are passed to every cargo invocation.", "default": [], @@ -591,7 +622,7 @@ ] }, "rust-analyzer.cargo.unsetTest": { - "markdownDescription": "Unsets `#[cfg(test)]` for the specified crates.", + "markdownDescription": "Unsets the implicit `#[cfg(test)]` for the specified crates.", "default": [ "core" ], @@ -870,6 +901,11 @@ "default": true, "type": "boolean" }, + "rust-analyzer.highlightRelated.closureCaptures.enable": { + "markdownDescription": "Enables highlighting of all captures of a closure while the cursor is on the `|` or move keyword of a closure.", + "default": true, + "type": "boolean" + }, "rust-analyzer.highlightRelated.exitPoints.enable": { "markdownDescription": "Enables highlighting of all exit points while the cursor is on any `return`, `?`, `fn`, or return type arrow (`->`).", "default": true, @@ -926,10 +962,89 @@ "type": "boolean" }, "rust-analyzer.hover.links.enable": { - "markdownDescription": "Use markdown syntax for links in hover.", + "markdownDescription": "Use markdown syntax for links on hover.", + "default": true, + "type": "boolean" + }, + "rust-analyzer.hover.memoryLayout.alignment": { + "markdownDescription": "How to render the align information in a memory layout hover.", + "default": "hexadecimal", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "both", + "decimal", + "hexadecimal" + ], + "enumDescriptions": [ + "Render as 12 (0xC)", + "Render as 12", + "Render as 0xC" + ] + } + ] + }, + "rust-analyzer.hover.memoryLayout.enable": { + "markdownDescription": "Whether to show memory layout data on hover.", "default": true, "type": "boolean" }, + "rust-analyzer.hover.memoryLayout.niches": { + "markdownDescription": "How to render the niche information in a memory layout hover.", + "default": false, + "type": [ + "null", + "boolean" + ] + }, + "rust-analyzer.hover.memoryLayout.offset": { + "markdownDescription": "How to render the offset information in a memory layout hover.", + "default": "hexadecimal", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "both", + "decimal", + "hexadecimal" + ], + "enumDescriptions": [ + "Render as 12 (0xC)", + "Render as 12", + "Render as 0xC" + ] + } + ] + }, + "rust-analyzer.hover.memoryLayout.size": { + "markdownDescription": "How to render the size information in a memory layout hover.", + "default": "both", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "both", + "decimal", + "hexadecimal" + ], + "enumDescriptions": [ + "Render as 12 (0xC)", + "Render as 12", + "Render as 0xC" + ] + } + ] + }, "rust-analyzer.imports.granularity.enforce": { "markdownDescription": "Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file.", "default": false, @@ -1003,6 +1118,11 @@ "type": "integer", "minimum": 0 }, + "rust-analyzer.inlayHints.closureCaptureHints.enable": { + "markdownDescription": "Whether to show inlay hints for closure captures.", + "default": false, + "type": "boolean" + }, "rust-analyzer.inlayHints.closureReturnTypeHints.enable": { "markdownDescription": "Whether to show inlay type hints for return types of closures.", "default": "never", @@ -1018,6 +1138,23 @@ "Only show type hints for return types of closures with blocks." ] }, + "rust-analyzer.inlayHints.closureStyle": { + "markdownDescription": "Closure notation in type and chaining inlay hints.", + "default": "impl_fn", + "type": "string", + "enum": [ + "impl_fn", + "rust_analyzer", + "with_id", + "hide" + ], + "enumDescriptions": [ + "`impl_fn`: `impl FnMut(i32, u64) -> i8`", + "`rust_analyzer`: `|i32, u64| -> i8`", + "`with_id`: `{closure#14352}`, where that id is the unique number of the closure in r-a internals", + "`hide`: Shows `...` for every closure type" + ] + }, "rust-analyzer.inlayHints.discriminantHints.enable": { "markdownDescription": "Whether to show enum variant discriminant hints.", "default": "never", @@ -1066,8 +1203,8 @@ "enumDescriptions": [ "Always show adjustment hints as prefix (`*expr`).", "Always show adjustment hints as postfix (`expr.*`).", - "Show prefix or postfix depending on which uses less parenthesis, prefering prefix.", - "Show prefix or postfix depending on which uses less parenthesis, prefering postfix." + "Show prefix or postfix depending on which uses less parenthesis, preferring prefix.", + "Show prefix or postfix depending on which uses less parenthesis, preferring postfix." ] }, "rust-analyzer.inlayHints.lifetimeElisionHints.enable": { @@ -1242,6 +1379,11 @@ ], "minimum": 0 }, + "rust-analyzer.lru.query.capacities": { + "markdownDescription": "Sets the LRU capacity of the specified queries.", + "default": {}, + "type": "object" + }, "rust-analyzer.notifications.cargoTomlNotFound": { "markdownDescription": "Whether to show `can't find Cargo.toml` error message.", "default": true, @@ -1272,7 +1414,7 @@ "type": "object" }, "rust-analyzer.procMacro.server": { - "markdownDescription": "Internal config, path to proc-macro server executable (typically,\nthis is rust-analyzer itself, but we override this in tests).", + "markdownDescription": "Internal config, path to proc-macro server executable.", "default": null, "type": [ "null", @@ -1337,6 +1479,11 @@ "default": true, "type": "boolean" }, + "rust-analyzer.semanticHighlighting.nonStandardTokens": { + "markdownDescription": "Whether the server is allowed to emit non-standard tokens and modifiers.", + "default": true, + "type": "boolean" + }, "rust-analyzer.semanticHighlighting.operator.enable": { "markdownDescription": "Use semantic tokens for operators.\n\nWhen disabled, rust-analyzer will emit semantic tokens only for operator tokens when\nthey are tagged with modifiers.", "default": true, @@ -1348,7 +1495,7 @@ "type": "boolean" }, "rust-analyzer.semanticHighlighting.punctuation.enable": { - "markdownDescription": "Use semantic tokens for punctuations.\n\nWhen disabled, rust-analyzer will emit semantic tokens only for punctuation tokens when\nthey are tagged with modifiers or have a special role.", + "markdownDescription": "Use semantic tokens for punctuation.\n\nWhen disabled, rust-analyzer will emit semantic tokens only for punctuation tokens when\nthey are tagged with modifiers or have a special role.", "default": false, "type": "boolean" }, @@ -1358,7 +1505,7 @@ "type": "boolean" }, "rust-analyzer.semanticHighlighting.punctuation.specialization.enable": { - "markdownDescription": "Use specialized semantic tokens for punctuations.\n\nWhen enabled, rust-analyzer will emit special token types for punctuation tokens instead\nof the generic `punctuation` token type.", + "markdownDescription": "Use specialized semantic tokens for punctuation.\n\nWhen enabled, rust-analyzer will emit special token types for punctuation tokens instead\nof the generic `punctuation` token type.", "default": false, "type": "boolean" }, @@ -1461,6 +1608,18 @@ "endColumn": 6 } ] + }, + { + "name": "rust-panic", + "patterns": [ + { + "regexp": "^thread '.*' panicked at '(.*)', (.*):(\\d*):(\\d*)$", + "message": 1, + "file": 2, + "line": 3, + "column": 4 + } + ] } ], "languages": [ @@ -1487,6 +1646,16 @@ "language": "ra_syntax_tree", "scopeName": "source.ra_syntax_tree", "path": "ra_syntax_tree.tmGrammar.json" + }, + { + "scopeName": "rustdoc.markdown.injection", + "path": "rustdoc.markdown.injection.tmGrammar.json", + "injectTo": [ + "source.rust" + ], + "embeddedLanguages": { + "meta.embedded.block.markdown": "text.html.markdown" + } } ], "problemMatchers": [ @@ -1511,6 +1680,16 @@ "pattern": "$rustc-json" }, { + "name": "rust-panic", + "owner": "rust-panic", + "source": "panic", + "fileLocation": [ + "autoDetect", + "${workspaceRoot}" + ], + "pattern": "$rust-panic" + }, + { "name": "rustc-watch", "owner": "rustc", "source": "rustc", @@ -1876,7 +2055,7 @@ "when": "inRustProject" }, { - "command": "rust-analyzer.reload", + "command": "rust-analyzer.restartServer", "when": "inRustProject" }, { @@ -1913,6 +2092,15 @@ } ] }, + "views": { + "explorer": [ + { + "id": "rustDependencies", + "name": "Rust Dependencies", + "when": "inRustProject && config.rust-analyzer.showDependenciesExplorer" + } + ] + }, "jsonValidation": [ { "fileMatch": "rust-project.json", diff --git a/src/tools/rust-analyzer/editors/code/rustdoc.markdown.injection.tmGrammar.json b/src/tools/rust-analyzer/editors/code/rustdoc.markdown.injection.tmGrammar.json new file mode 100644 index 00000000000..3ee49bff31e --- /dev/null +++ b/src/tools/rust-analyzer/editors/code/rustdoc.markdown.injection.tmGrammar.json @@ -0,0 +1,36 @@ +{ + "scopeName": "rustdoc.markdown.injection", + "injectionSelector": "L:source.rust", + "patterns": [ + { + "include": "#doc-comment-line" + }, + { + "include": "#doc-comment-block" + } + ], + "repository": { + "doc-comment-line": { + "name": "comment.line.documentation.rust", + "begin": "^\\s*//(/|!)", + "while": "^\\s*//(/|!)", + "contentName": "meta.embedded.block.markdown", + "patterns": [ + { + "include": "text.html.markdown" + } + ] + }, + "doc-comment-block": { + "name": "comment.block.documentation.rust", + "begin": "/\\*(\\*|!)", + "end": "\\s*\\*/", + "contentName": "meta.embedded.block.markdown", + "patterns": [ + { + "include": "text.html.markdown" + } + ] + } + } +} diff --git a/src/tools/rust-analyzer/editors/code/src/client.ts b/src/tools/rust-analyzer/editors/code/src/client.ts index 565cb9c6432..f721fcce766 100644 --- a/src/tools/rust-analyzer/editors/code/src/client.ts +++ b/src/tools/rust-analyzer/editors/code/src/client.ts @@ -8,6 +8,7 @@ import * as diagnostics from "./diagnostics"; import { WorkspaceEdit } from "vscode"; import { Config, prepareVSCodeConfig } from "./config"; import { randomUUID } from "crypto"; +import { sep as pathSeparator } from "path"; export interface Env { [name: string]: string; @@ -69,7 +70,8 @@ export async function createClient( outputChannel: vscode.OutputChannel, initializationOptions: vscode.WorkspaceConfiguration, serverOptions: lc.ServerOptions, - config: Config + config: Config, + unlinkedFiles: vscode.Uri[] ): Promise<lc.LanguageClient> { const clientOptions: lc.LanguageClientOptions = { documentSelector: [{ scheme: "file", language: "rust" }], @@ -119,6 +121,69 @@ export async function createClient( const preview = config.previewRustcOutput; const errorCode = config.useRustcErrorCode; diagnosticList.forEach((diag, idx) => { + const value = + typeof diag.code === "string" || typeof diag.code === "number" + ? diag.code + : diag.code?.value; + if ( + value === "unlinked-file" && + !unlinkedFiles.includes(uri) && + diag.message !== "file not included in module tree" + ) { + const config = vscode.workspace.getConfiguration("rust-analyzer"); + if (config.get("showUnlinkedFileNotification")) { + unlinkedFiles.push(uri); + const folder = vscode.workspace.getWorkspaceFolder(uri)?.uri.fsPath; + if (folder) { + const parentBackslash = uri.fsPath.lastIndexOf( + pathSeparator + "src" + ); + const parent = uri.fsPath.substring(0, parentBackslash); + + if (parent.startsWith(folder)) { + const path = vscode.Uri.file( + parent + pathSeparator + "Cargo.toml" + ); + void vscode.workspace.fs.stat(path).then(async () => { + const choice = await vscode.window.showInformationMessage( + `This rust file does not belong to a loaded cargo project. It looks like it might belong to the workspace at ${path.path}, do you want to add it to the linked Projects?`, + "Yes", + "No", + "Don't show this again" + ); + switch (choice) { + case undefined: + break; + case "No": + break; + case "Yes": + const pathToInsert = + "." + + parent.substring(folder.length) + + pathSeparator + + "Cargo.toml"; + await config.update( + "linkedProjects", + config + .get<any[]>("linkedProjects") + ?.concat(pathToInsert), + false + ); + break; + case "Don't show this again": + await config.update( + "showUnlinkedFileNotification", + false, + false + ); + break; + } + }); + } + } + } + } + // Abuse the fact that VSCode leaks the LSP diagnostics data field through the // Diagnostic class, if they ever break this we are out of luck and have to go // back to the worst diagnostics experience ever:) @@ -138,14 +203,6 @@ export async function createClient( .substring(0, index) .replace(/^ -->[^\n]+\n/m, ""); } - let value; - if (errorCode) { - if (typeof diag.code === "string" || typeof diag.code === "number") { - value = diag.code; - } else { - value = diag.code?.value; - } - } diag.code = { target: vscode.Uri.from({ scheme: diagnostics.URI_SCHEME, @@ -153,7 +210,8 @@ export async function createClient( fragment: uri.toString(), query: idx.toString(), }), - value: value ?? "Click for full compiler diagnostic", + value: + errorCode && value ? value : "Click for full compiler diagnostic", }; } }); @@ -308,6 +366,7 @@ export async function createClient( // To turn on all proposed features use: client.registerProposedFeatures(); client.registerFeature(new ExperimentalFeatures()); + client.registerFeature(new OverrideFeatures()); return client; } @@ -343,6 +402,25 @@ class ExperimentalFeatures implements lc.StaticFeature { dispose(): void {} } +class OverrideFeatures implements lc.StaticFeature { + getState(): lc.FeatureState { + return { kind: "static" }; + } + fillClientCapabilities(capabilities: lc.ClientCapabilities): void { + // Force disable `augmentsSyntaxTokens`, VSCode's textmate grammar is somewhat incomplete + // making the experience generally worse + const caps = capabilities.textDocument?.semanticTokens; + if (caps) { + caps.augmentsSyntaxTokens = false; + } + } + initialize( + _capabilities: lc.ServerCapabilities, + _documentSelector: lc.DocumentSelector | undefined + ): void {} + dispose(): void {} +} + function isCodeActionWithoutEditsAndCommands(value: any): boolean { const candidate: lc.CodeAction = value; return ( diff --git a/src/tools/rust-analyzer/editors/code/src/commands.ts b/src/tools/rust-analyzer/editors/code/src/commands.ts index 8a953577e99..98ccd50dc04 100644 --- a/src/tools/rust-analyzer/editors/code/src/commands.ts +++ b/src/tools/rust-analyzer/editors/code/src/commands.ts @@ -8,10 +8,18 @@ import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets"; import { spawnSync } from "child_process"; import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run"; import { AstInspector } from "./ast_inspector"; -import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor } from "./util"; +import { + isRustDocument, + isCargoTomlDocument, + sleep, + isRustEditor, + RustEditor, + RustDocument, +} from "./util"; import { startDebugSession, makeDebugConfig } from "./debug"; import { LanguageClient } from "vscode-languageclient/node"; import { LINKED_COMMANDS } from "./client"; +import { DependencyId } from "./dependencies_provider"; export * from "./ast_inspector"; export * from "./run"; @@ -89,7 +97,13 @@ export function shuffleCrateGraph(ctx: CtxInit): Cmd { export function triggerParameterHints(_: CtxInit): Cmd { return async () => { - await vscode.commands.executeCommand("editor.action.triggerParameterHints"); + const parameterHintsEnabled = vscode.workspace + .getConfiguration("editor") + .get<boolean>("parameterHints.enabled"); + + if (parameterHintsEnabled) { + await vscode.commands.executeCommand("editor.action.triggerParameterHints"); + } }; } @@ -260,6 +274,71 @@ export function openCargoToml(ctx: CtxInit): Cmd { }; } +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\ + *  |-beches\ + *  |-src\ + *  |- ... + * - std + * 2. reveal every children of src: + * core + * alloc\ + *  |-beches\ + *  |-src\ + *   |- lib.rs\ + *   |- str.rs <------- FOUND IT!\ + *   |- ...\ + *  |- ...\ + * 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) { + await ctx.treeView?.reveal(parentChain[idx], { 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; @@ -416,8 +495,20 @@ export function syntaxTree(ctx: CtxInit): Cmd { 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(`rust-analyzer-${xir}://${viewXir}/${xir}.rs`); + readonly uri = vscode.Uri.parse(uri); readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>(); constructor() { vscode.workspace.onDidChangeTextDocument( @@ -433,14 +524,14 @@ function viewHirOrMir(ctx: CtxInit, xir: "hir" | "mir"): Cmd { } private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { - if (isRustDocument(event.document)) { + 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)) { + if (editor && isRustEditor(editor) && shouldUpdate) { this.eventEmitter.fire(this.uri); } } @@ -467,9 +558,7 @@ function viewHirOrMir(ctx: CtxInit, xir: "hir" | "mir"): Cmd { } })(); - ctx.pushExtCleanup( - vscode.workspace.registerTextDocumentContentProvider(`rust-analyzer-${xir}`, tdcp) - ); + ctx.pushExtCleanup(vscode.workspace.registerTextDocumentContentProvider(scheme, tdcp)); return async () => { const document = await vscode.workspace.openTextDocument(tdcp.uri); @@ -495,6 +584,20 @@ 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"); @@ -663,21 +766,26 @@ function crateGraph(ctx: CtxInit, full: boolean): Cmd { </head> <body> <script type="text/javascript" src="${uri}/d3/dist/d3.min.js"></script> - <script type="text/javascript" src="${uri}/@hpcc-js/wasm/dist/index.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() + .graphviz({ useWorker: false, useSharedWorker: false }) .fit(true) .zoomScaleExtent([0.1, Infinity]) - .renderDot(\`${dot}\`); + .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> `; @@ -699,7 +807,7 @@ export function viewFullCrateGraph(ctx: CtxInit): Cmd { // 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`; + let result = `// Recursive expansion of ${expanded.name} macro\n`; result += "// " + "=".repeat(result.length - 3); result += "\n\n"; result += expanded.expansion; @@ -749,6 +857,10 @@ 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; @@ -757,12 +869,13 @@ export function addProject(ctx: CtxInit): Cmd { } const workspaces: JsonProject[] = await Promise.all( - vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => { - const rustDocuments = vscode.workspace.textDocuments.filter(isRustDocument); - return discoverWorkspace(rustDocuments, discoverProjectCommand, { - cwd: folder.uri.fsPath, - }); - }) + vscode.workspace.textDocuments + .filter(isRustDocument) + .map(async (file): Promise<JsonProject> => { + return discoverWorkspace([file], discoverProjectCommand, { + cwd: path.dirname(file.uri.fsPath), + }); + }) ); ctx.addToDiscoveredWorkspaces(workspaces); diff --git a/src/tools/rust-analyzer/editors/code/src/config.ts b/src/tools/rust-analyzer/editors/code/src/config.ts index da7c74c28ba..c6d2bcc2b2a 100644 --- a/src/tools/rust-analyzer/editors/code/src/config.ts +++ b/src/tools/rust-analyzer/editors/code/src/config.ts @@ -21,7 +21,6 @@ export class Config { "serverPath", "server", "files", - "lens", // works as lens.* ].map((opt) => `${this.rootSection}.${opt}`); readonly package: { @@ -70,7 +69,7 @@ export class Config { if (!requiresReloadOpt) return; if (this.restartServerOnConfigChange) { - await vscode.commands.executeCommand("rust-analyzer.reload"); + await vscode.commands.executeCommand("rust-analyzer.restartServer"); return; } @@ -78,7 +77,7 @@ export class Config { const userResponse = await vscode.window.showInformationMessage(message, "Restart now"); if (userResponse) { - const command = "rust-analyzer.reload"; + const command = "rust-analyzer.restartServer"; await vscode.commands.executeCommand(command); } } @@ -285,6 +284,10 @@ export class Config { get useRustcErrorCode() { return this.get<boolean>("diagnostics.useRustcErrorCode"); } + + get showDependenciesExplorer() { + return this.get<boolean>("showDependenciesExplorer"); + } } // the optional `cb?` parameter is meant to be used to add additional diff --git a/src/tools/rust-analyzer/editors/code/src/ctx.ts b/src/tools/rust-analyzer/editors/code/src/ctx.ts index c2dca733df8..a72b5391ff1 100644 --- a/src/tools/rust-analyzer/editors/code/src/ctx.ts +++ b/src/tools/rust-analyzer/editors/code/src/ctx.ts @@ -1,11 +1,13 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import * as ra from "./lsp_ext"; +import * as path from "path"; import { Config, prepareVSCodeConfig } from "./config"; import { createClient } from "./client"; import { executeDiscoverProject, + isDocumentInWorkspace, isRustDocument, isRustEditor, LazyOutputChannel, @@ -13,6 +15,13 @@ import { RustEditor, } from "./util"; import { ServerStatusParams } from "./lsp_ext"; +import { + Dependency, + DependencyFile, + RustDependenciesProvider, + DependencyId, +} from "./dependencies_provider"; +import { execRevealDependency } from "./commands"; import { PersistentState } from "./persistent_state"; import { bootstrap } from "./bootstrap"; import { ExecOptions } from "child_process"; @@ -82,11 +91,22 @@ export class Ctx { private state: PersistentState; private commandFactories: Record<string, CommandFactory>; private commandDisposables: Disposable[]; + private unlinkedFiles: vscode.Uri[]; + private _dependencies: RustDependenciesProvider | undefined; + private _treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | undefined; get client() { return this._client; } + get treeView() { + return this._treeView; + } + + get dependencies() { + return this._dependencies; + } + constructor( readonly extCtx: vscode.ExtensionContext, commandFactories: Record<string, CommandFactory>, @@ -94,12 +114,11 @@ export class Ctx { ) { extCtx.subscriptions.push(this); this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - this.statusBar.show(); this.workspace = workspace; this.clientSubscriptions = []; this.commandDisposables = []; this.commandFactories = commandFactories; - + this.unlinkedFiles = []; this.state = new PersistentState(extCtx.globalState); this.config = new Config(extCtx); @@ -191,12 +210,13 @@ export class Ctx { const discoverProjectCommand = this.config.discoverProjectCommand; if (discoverProjectCommand) { const workspaces: JsonProject[] = await Promise.all( - vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => { - const rustDocuments = vscode.workspace.textDocuments.filter(isRustDocument); - return discoverWorkspace(rustDocuments, discoverProjectCommand, { - cwd: folder.uri.fsPath, - }); - }) + vscode.workspace.textDocuments + .filter(isRustDocument) + .map(async (file): Promise<JsonProject> => { + return discoverWorkspace([file], discoverProjectCommand, { + cwd: path.dirname(file.uri.fsPath), + }); + }) ); this.addToDiscoveredWorkspaces(workspaces); @@ -218,7 +238,8 @@ export class Ctx { this.outputChannel, initializationOptions, serverOptions, - this.config + this.config, + this.unlinkedFiles ); this.pushClientCleanup( this._client.onNotification(ra.serverStatus, (params) => @@ -242,6 +263,56 @@ export class Ctx { } await client.start(); this.updateCommands(); + + if (this.config.showDependenciesExplorer) { + this.prepareTreeDependenciesView(client); + } + } + + private prepareTreeDependenciesView(client: lc.LanguageClient) { + const ctxInit: CtxInit = { + ...this, + client: client, + }; + this._dependencies = new RustDependenciesProvider(ctxInit); + this._treeView = vscode.window.createTreeView("rustDependencies", { + treeDataProvider: this._dependencies, + showCollapseAll: true, + }); + + this.pushExtCleanup(this._treeView); + vscode.window.onDidChangeActiveTextEditor(async (e) => { + // we should skip documents that belong to the current workspace + if (this.shouldRevealDependency(e)) { + try { + await execRevealDependency(e); + } catch (reason) { + await vscode.window.showErrorMessage(`Dependency error: ${reason}`); + } + } + }); + + this.treeView?.onDidChangeVisibility(async (e) => { + if (e.visible) { + const activeEditor = vscode.window.activeTextEditor; + if (this.shouldRevealDependency(activeEditor)) { + try { + await execRevealDependency(activeEditor); + } catch (reason) { + await vscode.window.showErrorMessage(`Dependency error: ${reason}`); + } + } + } + }); + } + + private shouldRevealDependency(e: vscode.TextEditor | undefined): e is RustEditor { + return ( + e !== undefined && + isRustEditor(e) && + !isDocumentInWorkspace(e.document) && + (this.treeView?.visible || false) + ); } async restart() { @@ -335,6 +406,7 @@ export class Ctx { setServerStatus(status: ServerStatusParams | { health: "stopped" }) { let icon = ""; const statusBar = this.statusBar; + statusBar.show(); statusBar.tooltip = new vscode.MarkdownString("", true); statusBar.tooltip.isTrusted = true; switch (status.health) { @@ -343,6 +415,7 @@ export class Ctx { statusBar.color = undefined; statusBar.backgroundColor = undefined; statusBar.command = "rust-analyzer.stopServer"; + this.dependencies?.refresh(); break; case "warning": if (status.message) { @@ -378,12 +451,17 @@ export class Ctx { if (statusBar.tooltip.value) { statusBar.tooltip.appendText("\n\n"); } + statusBar.tooltip.appendMarkdown("\n\n[Open logs](command:rust-analyzer.openLogs)"); statusBar.tooltip.appendMarkdown( "\n\n[Reload Workspace](command:rust-analyzer.reloadWorkspace)" ); - statusBar.tooltip.appendMarkdown("\n\n[Open logs](command:rust-analyzer.openLogs)"); - statusBar.tooltip.appendMarkdown("\n\n[Restart server](command:rust-analyzer.startServer)"); - statusBar.tooltip.appendMarkdown("[Stop server](command:rust-analyzer.stopServer)"); + statusBar.tooltip.appendMarkdown( + "\n\n[Rebuild Proc Macros](command:rust-analyzer.rebuildProcMacros)" + ); + statusBar.tooltip.appendMarkdown( + "\n\n[Restart server](command:rust-analyzer.restartServer)" + ); + statusBar.tooltip.appendMarkdown("\n\n[Stop server](command:rust-analyzer.stopServer)"); if (!status.quiescent) icon = "$(sync~spin) "; statusBar.text = `${icon}rust-analyzer`; } @@ -400,4 +478,5 @@ export class Ctx { export interface Disposable { dispose(): void; } + export type Cmd = (...args: any[]) => unknown; diff --git a/src/tools/rust-analyzer/editors/code/src/debug.ts b/src/tools/rust-analyzer/editors/code/src/debug.ts index 268b70b4fbb..cffee1de6af 100644 --- a/src/tools/rust-analyzer/editors/code/src/debug.ts +++ b/src/tools/rust-analyzer/editors/code/src/debug.ts @@ -118,8 +118,8 @@ async function getDebugConfiguration( return path.normalize(p).replace(wsFolder, "${workspaceFolder" + workspaceQualifier + "}"); } - const executable = await getDebugExecutable(runnable); const env = prepareEnv(runnable, ctx.config.runnableEnv); + const executable = await getDebugExecutable(runnable, env); let sourceFileMap = debugOptions.sourceFileMap; if (sourceFileMap === "auto") { // let's try to use the default toolchain @@ -156,8 +156,11 @@ async function getDebugConfiguration( return debugConfig; } -async function getDebugExecutable(runnable: ra.Runnable): Promise<string> { - const cargo = new Cargo(runnable.args.workspaceRoot || ".", debugOutput); +async function getDebugExecutable( + runnable: ra.Runnable, + env: Record<string, string> +): Promise<string> { + const cargo = new Cargo(runnable.args.workspaceRoot || ".", debugOutput, env); const executable = await cargo.executableFromArgs(runnable.args.cargoArgs); // if we are here, there were no compilation errors. diff --git a/src/tools/rust-analyzer/editors/code/src/dependencies_provider.ts b/src/tools/rust-analyzer/editors/code/src/dependencies_provider.ts new file mode 100644 index 00000000000..8900aa9a5f3 --- /dev/null +++ b/src/tools/rust-analyzer/editors/code/src/dependencies_provider.ts @@ -0,0 +1,148 @@ +import * as vscode from "vscode"; +import * as fspath from "path"; +import * as fs from "fs"; +import { CtxInit } from "./ctx"; +import * as ra from "./lsp_ext"; +import { FetchDependencyListResult } from "./lsp_ext"; + +export class RustDependenciesProvider + implements vscode.TreeDataProvider<Dependency | DependencyFile> +{ + dependenciesMap: { [id: string]: Dependency | DependencyFile }; + ctx: CtxInit; + + constructor(ctx: CtxInit) { + this.dependenciesMap = {}; + this.ctx = ctx; + } + + private _onDidChangeTreeData: vscode.EventEmitter< + Dependency | DependencyFile | undefined | null | void + > = new vscode.EventEmitter<Dependency | undefined | null | void>(); + + readonly onDidChangeTreeData: vscode.Event< + Dependency | DependencyFile | undefined | null | void + > = this._onDidChangeTreeData.event; + + getDependency(filePath: string): Dependency | DependencyFile | undefined { + return this.dependenciesMap[filePath.toLowerCase()]; + } + + contains(filePath: string): boolean { + return filePath.toLowerCase() in this.dependenciesMap; + } + + isInitialized(): boolean { + return Object.keys(this.dependenciesMap).length !== 0; + } + + refresh(): void { + this.dependenciesMap = {}; + this._onDidChangeTreeData.fire(); + } + + getParent?( + element: Dependency | DependencyFile + ): vscode.ProviderResult<Dependency | DependencyFile> { + if (element instanceof Dependency) return undefined; + return element.parent; + } + + getTreeItem(element: Dependency | DependencyFile): vscode.TreeItem | Thenable<vscode.TreeItem> { + if (element.id! in this.dependenciesMap) return this.dependenciesMap[element.id!]; + return element; + } + + getChildren( + element?: Dependency | DependencyFile + ): vscode.ProviderResult<Dependency[] | DependencyFile[]> { + return new Promise((resolve, _reject) => { + if (!vscode.workspace.workspaceFolders) { + void vscode.window.showInformationMessage("No dependency in empty workspace"); + return Promise.resolve([]); + } + if (element) { + const files = fs.readdirSync(element.dependencyPath).map((fileName) => { + const filePath = fspath.join(element.dependencyPath, fileName); + const collapsibleState = fs.lstatSync(filePath).isDirectory() + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None; + const dep = new DependencyFile(fileName, filePath, element, collapsibleState); + this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep; + return dep; + }); + return resolve(files); + } else { + return resolve(this.getRootDependencies()); + } + }); + } + + private async getRootDependencies(): Promise<Dependency[]> { + const dependenciesResult: FetchDependencyListResult = await this.ctx.client.sendRequest( + ra.fetchDependencyList, + {} + ); + const crates = dependenciesResult.crates; + + return crates + .map((crate) => { + const dep = this.toDep(crate.name || "unknown", crate.version || "", crate.path); + this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep; + return dep; + }) + .sort((a, b) => { + return a.label.localeCompare(b.label); + }); + } + + private toDep(moduleName: string, version: string, path: string): Dependency { + return new Dependency( + moduleName, + version, + vscode.Uri.parse(path).fsPath, + vscode.TreeItemCollapsibleState.Collapsed + ); + } +} + +export class Dependency extends vscode.TreeItem { + constructor( + public readonly label: string, + private version: string, + readonly dependencyPath: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState + ) { + super(label, collapsibleState); + this.resourceUri = vscode.Uri.file(dependencyPath); + this.id = this.resourceUri.fsPath.toLowerCase(); + this.description = this.version; + if (this.version) { + this.tooltip = `${this.label}-${this.version}`; + } else { + this.tooltip = this.label; + } + } +} + +export class DependencyFile extends vscode.TreeItem { + constructor( + readonly label: string, + readonly dependencyPath: string, + readonly parent: Dependency | DependencyFile, + public readonly collapsibleState: vscode.TreeItemCollapsibleState + ) { + super(vscode.Uri.file(dependencyPath), collapsibleState); + this.id = this.resourceUri!.fsPath.toLowerCase(); + const isDir = fs.lstatSync(this.resourceUri!.fsPath).isDirectory(); + if (!isDir) { + this.command = { + command: "vscode.open", + title: "Open File", + arguments: [this.resourceUri], + }; + } + } +} + +export type DependencyId = { id: string }; diff --git a/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts b/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts index 872d7199b83..b72804e510c 100644 --- a/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts +++ b/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts @@ -10,12 +10,9 @@ export const hover = new lc.RequestType< HoverParams, (lc.Hover & { actions: CommandLinkGroup[] }) | null, void ->("textDocument/hover"); -export type HoverParams = { position: lc.Position | lc.Range } & Omit< - lc.TextDocumentPositionParams, - "position" -> & - lc.WorkDoneProgressParams; +>(lc.HoverRequest.method); +export type HoverParams = { position: lc.Position | lc.Range } & Omit<lc.HoverParams, "position">; + export type CommandLink = { /** * A tooltip for the command, when represented in the UI. @@ -43,6 +40,7 @@ export const relatedTests = new lc.RequestType<lc.TextDocumentPositionParams, Te "rust-analyzer/relatedTests" ); export const reloadWorkspace = new lc.RequestType0<null, void>("rust-analyzer/reloadWorkspace"); +export const rebuildProcMacros = new lc.RequestType0<null, void>("rust-analyzer/rebuildProcMacros"); export const runFlycheck = new lc.NotificationType<{ textDocument: lc.TextDocumentIdentifier | null; @@ -63,12 +61,47 @@ export const viewHir = new lc.RequestType<lc.TextDocumentPositionParams, string, export const viewMir = new lc.RequestType<lc.TextDocumentPositionParams, string, void>( "rust-analyzer/viewMir" ); +export const interpretFunction = new lc.RequestType<lc.TextDocumentPositionParams, string, void>( + "rust-analyzer/interpretFunction" +); export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void>( "rust-analyzer/viewItemTree" ); export type AnalyzerStatusParams = { textDocument?: lc.TextDocumentIdentifier }; +export interface FetchDependencyListParams {} + +export interface FetchDependencyListResult { + crates: { + name: string | undefined; + version: string | undefined; + path: string; + }[]; +} + +export const fetchDependencyList = new lc.RequestType< + FetchDependencyListParams, + FetchDependencyListResult, + void +>("rust-analyzer/fetchDependencyList"); + +export interface FetchDependencyGraphParams {} + +export interface FetchDependencyGraphResult { + crates: { + name: string; + version: string; + path: string; + }[]; +} + +export const fetchDependencyGraph = new lc.RequestType< + FetchDependencyGraphParams, + FetchDependencyGraphResult, + void +>("rust-analyzer/fetchDependencyGraph"); + export type ExpandMacroParams = { textDocument: lc.TextDocumentIdentifier; position: lc.Position; diff --git a/src/tools/rust-analyzer/editors/code/src/main.ts b/src/tools/rust-analyzer/editors/code/src/main.ts index d5de00561b1..be9bc9d363c 100644 --- a/src/tools/rust-analyzer/editors/code/src/main.ts +++ b/src/tools/rust-analyzer/editors/code/src/main.ts @@ -120,13 +120,11 @@ function createCommands(): Record<string, CommandFactory> { enabled: commands.onEnter, disabled: (_) => () => vscode.commands.executeCommand("default:type", { text: "\n" }), }, - reload: { + restartServer: { enabled: (ctx) => async () => { - void vscode.window.showInformationMessage("Reloading rust-analyzer..."); await ctx.restart(); }, disabled: (ctx) => async () => { - void vscode.window.showInformationMessage("Reloading rust-analyzer..."); await ctx.start(); }, }, @@ -153,6 +151,7 @@ function createCommands(): Record<string, CommandFactory> { memoryUsage: { enabled: commands.memoryUsage }, shuffleCrateGraph: { enabled: commands.shuffleCrateGraph }, reloadWorkspace: { enabled: commands.reloadWorkspace }, + rebuildProcMacros: { enabled: commands.rebuildProcMacros }, addProject: { enabled: commands.addProject }, matchingBrace: { enabled: commands.matchingBrace }, joinLines: { enabled: commands.joinLines }, @@ -160,6 +159,7 @@ function createCommands(): Record<string, CommandFactory> { syntaxTree: { enabled: commands.syntaxTree }, viewHir: { enabled: commands.viewHir }, viewMir: { enabled: commands.viewMir }, + interpretFunction: { enabled: commands.interpretFunction }, viewFileText: { enabled: commands.viewFileText }, viewItemTree: { enabled: commands.viewItemTree }, viewCrateGraph: { enabled: commands.viewCrateGraph }, @@ -190,5 +190,6 @@ function createCommands(): Record<string, CommandFactory> { showReferences: { enabled: commands.showReferences }, triggerParameterHints: { enabled: commands.triggerParameterHints }, openLogs: { enabled: commands.openLogs }, + revealDependency: { enabled: commands.revealDependency }, }; } diff --git a/src/tools/rust-analyzer/editors/code/src/run.ts b/src/tools/rust-analyzer/editors/code/src/run.ts index 35627e2fc6b..623fb102953 100644 --- a/src/tools/rust-analyzer/editors/code/src/run.ts +++ b/src/tools/rust-analyzer/editors/code/src/run.ts @@ -157,7 +157,7 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise cargoTask.presentationOptions.clear = true; // Sadly, this doesn't prevent focus stealing if the terminal is currently - // hidden, and will become revealed due to task exucution. + // hidden, and will become revealed due to task execution. cargoTask.presentationOptions.focus = false; return cargoTask; diff --git a/src/tools/rust-analyzer/editors/code/src/tasks.ts b/src/tools/rust-analyzer/editors/code/src/tasks.ts index e6239deeb21..d6509d9aa6e 100644 --- a/src/tools/rust-analyzer/editors/code/src/tasks.ts +++ b/src/tools/rust-analyzer/editors/code/src/tasks.ts @@ -128,7 +128,7 @@ export async function buildCargoTask( name, TASK_SOURCE, exec, - ["$rustc"] + ["$rustc", "$rust-panic"] ); } diff --git a/src/tools/rust-analyzer/editors/code/src/toolchain.ts b/src/tools/rust-analyzer/editors/code/src/toolchain.ts index eb70b88871e..917a1d6b099 100644 --- a/src/tools/rust-analyzer/editors/code/src/toolchain.ts +++ b/src/tools/rust-analyzer/editors/code/src/toolchain.ts @@ -18,7 +18,11 @@ export interface ArtifactSpec { } export class Cargo { - constructor(readonly rootFolder: string, readonly output: vscode.OutputChannel) {} + constructor( + readonly rootFolder: string, + readonly output: vscode.OutputChannel, + readonly env: Record<string, string> + ) {} // Made public for testing purposes static artifactSpec(args: readonly string[]): ArtifactSpec { @@ -102,6 +106,7 @@ export class Cargo { const cargo = cp.spawn(path, cargoArgs, { stdio: ["ignore", "pipe", "pipe"], cwd: this.rootFolder, + env: this.env, }); cargo.on("error", (err) => reject(new Error(`could not launch cargo: ${err}`))); diff --git a/src/tools/rust-analyzer/editors/code/src/util.ts b/src/tools/rust-analyzer/editors/code/src/util.ts index 922fbcbcf35..b6b779e2660 100644 --- a/src/tools/rust-analyzer/editors/code/src/util.ts +++ b/src/tools/rust-analyzer/editors/code/src/util.ts @@ -112,6 +112,19 @@ export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { return isRustDocument(editor.document); } +export function isDocumentInWorkspace(document: RustDocument): boolean { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return false; + } + for (const folder of workspaceFolders) { + if (document.uri.fsPath.startsWith(folder.uri.fsPath)) { + return true; + } + } + return false; +} + export function isValidExecutable(path: string): boolean { log.debug("Checking availability of a binary at", path); |
