diff --git a/docs/dev/README.md b/docs/dev/README.md index 8d7e18010927..f230dc1db1a4 100644 --- a/docs/dev/README.md +++ b/docs/dev/README.md @@ -155,6 +155,16 @@ There's also two VS Code commands which might be of interest: * `Rust Analyzer: Syntax Tree` shows syntax tree of the current file/selection. + You can hover over syntax nodes in the opened text file to see the appropriate + rust code that it refers to and the rust editor will also highlight the proper + text range. + + If you press Ctrl (i.e. trigger goto definition) in the inspected + Rust source file the syntax tree read-only editor should scroll to and select the + appropriate syntax node token. + + ![demo](https://user-images.githubusercontent.com/36276403/78225773-6636a480-74d3-11ea-9d9f-1c9d42da03b0.png) + # Profiling We have a built-in hierarchical profiler, you can enable it by using `RA_PROFILE` env-var: diff --git a/docs/user/features.md b/docs/user/features.md index 8aeec2e81efb..56d2969fd43e 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -81,12 +81,6 @@ Join selected lines into one, smartly fixing up whitespace and trailing commas. Shows the parse tree of the current file. It exists mostly for debugging rust-analyzer itself. -You can hover over syntax nodes in the opened text file to see the appropriate -rust code that it refers to and the rust editor will also highlight the proper -text range. - -demo - #### Expand Macro Recursively Shows the full macro expansion of the macro at current cursor. diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index 8d71cb39e315..b7a397414ebd 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -82,8 +82,8 @@ class TextDocumentContentProvider implements vscode.TextDocumentContentProvider // FIXME: consider implementing this via the Tree View API? // https://code.visualstudio.com/api/extension-guides/tree-view -class AstInspector implements vscode.HoverProvider, Disposable { - private static readonly astDecorationType = vscode.window.createTextEditorDecorationType({ +class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable { + private readonly astDecorationType = vscode.window.createTextEditorDecorationType({ borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'), borderStyle: "solid", borderWidth: "2px", @@ -91,9 +91,32 @@ class AstInspector implements vscode.HoverProvider, Disposable { }); private rustEditor: undefined | RustEditor; + // Lazy rust token range -> syntax tree file range. + private readonly rust2Ast = new Lazy(() => { + const astEditor = this.findAstTextEditor(); + if (!this.rustEditor || !astEditor) return undefined; + + const buf: [vscode.Range, vscode.Range][] = []; + for (let i = 0; i < astEditor.document.lineCount; ++i) { + const astLine = astEditor.document.lineAt(i); + + // Heuristically look for nodes with quoted text (which are token nodes) + const isTokenNode = astLine.text.lastIndexOf('"') >= 0; + if (!isTokenNode) continue; + + const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text); + if (!rustRange) continue; + + buf.push([rustRange, this.findAstNodeRange(astLine)]); + } + return buf; + }); + constructor(ctx: Ctx) { ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this)); + ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this)); vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions); + vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions); vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions); ctx.pushCleanup(this); @@ -102,6 +125,12 @@ class AstInspector implements vscode.HoverProvider, Disposable { this.setRustEditor(undefined); } + private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { + if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) { + this.rust2Ast.reset(); + } + } + private onDidCloseTextDocument(doc: vscode.TextDocument) { if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) { this.setRustEditor(undefined); @@ -109,38 +138,67 @@ class AstInspector implements vscode.HoverProvider, Disposable { } private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) { - if (editors.every(suspect => suspect.document.uri.scheme !== AST_FILE_SCHEME)) { + if (!this.findAstTextEditor()) { this.setRustEditor(undefined); return; } this.setRustEditor(editors.find(isRustEditor)); } + private findAstTextEditor(): undefined | vscode.TextEditor { + return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === AST_FILE_SCHEME); + } + private setRustEditor(newRustEditor: undefined | RustEditor) { - if (newRustEditor !== this.rustEditor) { - this.rustEditor?.setDecorations(AstInspector.astDecorationType, []); + if (this.rustEditor && this.rustEditor !== newRustEditor) { + this.rustEditor.setDecorations(this.astDecorationType, []); + this.rust2Ast.reset(); } this.rustEditor = newRustEditor; } + // additional positional params are omitted + provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult { + if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return; + + const astEditor = this.findAstTextEditor(); + if (!astEditor) return; + + const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos)); + if (!rust2AstRanges) return; + + const [rustFileRange, astFileRange] = rust2AstRanges; + + astEditor.revealRange(astFileRange); + astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end); + + return [{ + targetRange: astFileRange, + targetUri: astEditor.document.uri, + originSelectionRange: rustFileRange, + targetSelectionRange: astFileRange, + }]; + } + + // additional positional params are omitted provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult { if (!this.rustEditor) return; - const astTextLine = doc.lineAt(hoverPosition.line); + const astFileLine = doc.lineAt(hoverPosition.line); - const rustTextRange = this.parseRustTextRange(this.rustEditor.document, astTextLine.text); - if (!rustTextRange) return; + const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text); + if (!rustFileRange) return; - this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]); - this.rustEditor.revealRange(rustTextRange); + this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]); + this.rustEditor.revealRange(rustFileRange); - const rustSourceCode = this.rustEditor.document.getText(rustTextRange); - const astTextRange = this.findAstRange(astTextLine); + const rustSourceCode = this.rustEditor.document.getText(rustFileRange); + const astFileRange = this.findAstNodeRange(astFileLine); - return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astTextRange); + return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange); } - private findAstRange(astLine: vscode.TextLine) { + private findAstNodeRange(astLine: vscode.TextLine) { const lineOffset = astLine.range.start; const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex); const end = lineOffset.translate(undefined, astLine.text.trimEnd().length); @@ -156,3 +214,17 @@ class AstInspector implements vscode.HoverProvider, Disposable { return new vscode.Range(begin, end); } } + +class Lazy { + val: undefined | T; + + constructor(private readonly compute: () => undefined | T) { } + + get() { + return this.val ?? (this.val = this.compute()); + } + + reset() { + this.val = undefined; + } +}