Skip to content

Commit 9ee96dc

Browse files
bors[bot]Veetaha
andauthored
Merge #3816
3816: vscode: add goto ast node definition from rust source code r=Veetaha a=Veetaha By holding the `Ctrl` key you can now goto-definition of the appropriate syntax token in the syntax tree read-only editor. But actually going to the definition is not very convenient, since it opens the new editor, you'd rather just hold the `Ctrl` and look at the syntax tree because it is automatically scrolled to the proper node and the node itself is enclosed with text selection. Unfortunately, the algorithm is very simple (because we don't do any elaborate parsing of the syntax tree text received from the server), but it is enough to debug not very large source files. I tested the performance and in a bad case (rust source file with 5K lines of code) it takes `1.3` seconds to build the `rust -> ast` mapping index (lazily once on the first goto definition request) and each lookup in this worst-case is approx `20-120` ms. I think this is good enough. In the simple case where the file is < 100 lines of code, it is instant. One peculiarity that I've noticed is that vscode doesn't trigger the goto-definition provider when the user triggers it on some punctuation characters (i.e. it doesn't underline them and invoke te goto-definition provider), but if you explicitly click `Ctrl+LMB` it will only then invoke the provider and navigate to the definition in a new editor. I think this is fine ;D ![rust2ast](https://user-images.githubusercontent.com/36276403/78198718-24d1d500-7492-11ea-91f6-2687cedf26ee.gif) Related: #3682 Co-authored-by: veetaha <[email protected]>
2 parents 98f7842 + dd5e4d4 commit 9ee96dc

File tree

3 files changed

+96
-20
lines changed

3 files changed

+96
-20
lines changed

docs/dev/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,16 @@ There's also two VS Code commands which might be of interest:
155155

156156
* `Rust Analyzer: Syntax Tree` shows syntax tree of the current file/selection.
157157

158+
You can hover over syntax nodes in the opened text file to see the appropriate
159+
rust code that it refers to and the rust editor will also highlight the proper
160+
text range.
161+
162+
If you press <kbd>Ctrl</kbd> (i.e. trigger goto definition) in the inspected
163+
Rust source file the syntax tree read-only editor should scroll to and select the
164+
appropriate syntax node token.
165+
166+
![demo](https://user-images.githubusercontent.com/36276403/78225773-6636a480-74d3-11ea-9d9f-1c9d42da03b0.png)
167+
158168
# Profiling
159169

160170
We have a built-in hierarchical profiler, you can enable it by using `RA_PROFILE` env-var:

docs/user/features.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,6 @@ Join selected lines into one, smartly fixing up whitespace and trailing commas.
8181
Shows the parse tree of the current file. It exists mostly for debugging
8282
rust-analyzer itself.
8383

84-
You can hover over syntax nodes in the opened text file to see the appropriate
85-
rust code that it refers to and the rust editor will also highlight the proper
86-
text range.
87-
88-
<img src="https://user-images.githubusercontent.com/36276403/78043783-7425e180-737c-11ea-8653-b02b773c5aa1.png" alt="demo" height="200px" >
89-
9084
#### Expand Macro Recursively
9185

9286
Shows the full macro expansion of the macro at current cursor.

editors/code/src/commands/syntax_tree.ts

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,41 @@ class TextDocumentContentProvider implements vscode.TextDocumentContentProvider
8282

8383
// FIXME: consider implementing this via the Tree View API?
8484
// https://code.visualstudio.com/api/extension-guides/tree-view
85-
class AstInspector implements vscode.HoverProvider, Disposable {
86-
private static readonly astDecorationType = vscode.window.createTextEditorDecorationType({
85+
class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
86+
private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
8787
borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
8888
borderStyle: "solid",
8989
borderWidth: "2px",
9090

9191
});
9292
private rustEditor: undefined | RustEditor;
9393

94+
// Lazy rust token range -> syntax tree file range.
95+
private readonly rust2Ast = new Lazy(() => {
96+
const astEditor = this.findAstTextEditor();
97+
if (!this.rustEditor || !astEditor) return undefined;
98+
99+
const buf: [vscode.Range, vscode.Range][] = [];
100+
for (let i = 0; i < astEditor.document.lineCount; ++i) {
101+
const astLine = astEditor.document.lineAt(i);
102+
103+
// Heuristically look for nodes with quoted text (which are token nodes)
104+
const isTokenNode = astLine.text.lastIndexOf('"') >= 0;
105+
if (!isTokenNode) continue;
106+
107+
const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text);
108+
if (!rustRange) continue;
109+
110+
buf.push([rustRange, this.findAstNodeRange(astLine)]);
111+
}
112+
return buf;
113+
});
114+
94115
constructor(ctx: Ctx) {
95116
ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
117+
ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
96118
vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
119+
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
97120
vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
98121

99122
ctx.pushCleanup(this);
@@ -102,45 +125,80 @@ class AstInspector implements vscode.HoverProvider, Disposable {
102125
this.setRustEditor(undefined);
103126
}
104127

128+
private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
129+
if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) {
130+
this.rust2Ast.reset();
131+
}
132+
}
133+
105134
private onDidCloseTextDocument(doc: vscode.TextDocument) {
106135
if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
107136
this.setRustEditor(undefined);
108137
}
109138
}
110139

111140
private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
112-
if (editors.every(suspect => suspect.document.uri.scheme !== AST_FILE_SCHEME)) {
141+
if (!this.findAstTextEditor()) {
113142
this.setRustEditor(undefined);
114143
return;
115144
}
116145
this.setRustEditor(editors.find(isRustEditor));
117146
}
118147

148+
private findAstTextEditor(): undefined | vscode.TextEditor {
149+
return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === AST_FILE_SCHEME);
150+
}
151+
119152
private setRustEditor(newRustEditor: undefined | RustEditor) {
120-
if (newRustEditor !== this.rustEditor) {
121-
this.rustEditor?.setDecorations(AstInspector.astDecorationType, []);
153+
if (this.rustEditor && this.rustEditor !== newRustEditor) {
154+
this.rustEditor.setDecorations(this.astDecorationType, []);
155+
this.rust2Ast.reset();
122156
}
123157
this.rustEditor = newRustEditor;
124158
}
125159

160+
// additional positional params are omitted
161+
provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult<vscode.DefinitionLink[]> {
162+
if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return;
163+
164+
const astEditor = this.findAstTextEditor();
165+
if (!astEditor) return;
166+
167+
const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos));
168+
if (!rust2AstRanges) return;
169+
170+
const [rustFileRange, astFileRange] = rust2AstRanges;
171+
172+
astEditor.revealRange(astFileRange);
173+
astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end);
174+
175+
return [{
176+
targetRange: astFileRange,
177+
targetUri: astEditor.document.uri,
178+
originSelectionRange: rustFileRange,
179+
targetSelectionRange: astFileRange,
180+
}];
181+
}
182+
183+
// additional positional params are omitted
126184
provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
127185
if (!this.rustEditor) return;
128186

129-
const astTextLine = doc.lineAt(hoverPosition.line);
187+
const astFileLine = doc.lineAt(hoverPosition.line);
130188

131-
const rustTextRange = this.parseRustTextRange(this.rustEditor.document, astTextLine.text);
132-
if (!rustTextRange) return;
189+
const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
190+
if (!rustFileRange) return;
133191

134-
this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]);
135-
this.rustEditor.revealRange(rustTextRange);
192+
this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
193+
this.rustEditor.revealRange(rustFileRange);
136194

137-
const rustSourceCode = this.rustEditor.document.getText(rustTextRange);
138-
const astTextRange = this.findAstRange(astTextLine);
195+
const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
196+
const astFileRange = this.findAstNodeRange(astFileLine);
139197

140-
return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astTextRange);
198+
return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
141199
}
142200

143-
private findAstRange(astLine: vscode.TextLine) {
201+
private findAstNodeRange(astLine: vscode.TextLine) {
144202
const lineOffset = astLine.range.start;
145203
const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
146204
const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
@@ -156,3 +214,17 @@ class AstInspector implements vscode.HoverProvider, Disposable {
156214
return new vscode.Range(begin, end);
157215
}
158216
}
217+
218+
class Lazy<T> {
219+
val: undefined | T;
220+
221+
constructor(private readonly compute: () => undefined | T) { }
222+
223+
get() {
224+
return this.val ?? (this.val = this.compute());
225+
}
226+
227+
reset() {
228+
this.val = undefined;
229+
}
230+
}

0 commit comments

Comments
 (0)