aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/ast_inspector.ts
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/ast_inspector.ts')
-rw-r--r--editors/code/src/ast_inspector.ts185
1 files changed, 185 insertions, 0 deletions
diff --git a/editors/code/src/ast_inspector.ts b/editors/code/src/ast_inspector.ts
new file mode 100644
index 000000000..4fdd167bd
--- /dev/null
+++ b/editors/code/src/ast_inspector.ts
@@ -0,0 +1,185 @@
1import * as vscode from 'vscode';
2
3import { Ctx, Disposable } from './ctx';
4import { RustEditor, isRustEditor } from './util';
5
6// FIXME: consider implementing this via the Tree View API?
7// https://code.visualstudio.com/api/extension-guides/tree-view
8export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
9 private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
10 borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
11 borderStyle: "solid",
12 borderWidth: "2px",
13 });
14 private rustEditor: undefined | RustEditor;
15
16 // Lazy rust token range -> syntax tree file range.
17 private readonly rust2Ast = new Lazy(() => {
18 const astEditor = this.findAstTextEditor();
19 if (!this.rustEditor || !astEditor) return undefined;
20
21 const buf: [vscode.Range, vscode.Range][] = [];
22 for (let i = 0; i < astEditor.document.lineCount; ++i) {
23 const astLine = astEditor.document.lineAt(i);
24
25 // Heuristically look for nodes with quoted text (which are token nodes)
26 const isTokenNode = astLine.text.lastIndexOf('"') >= 0;
27 if (!isTokenNode) continue;
28
29 const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text);
30 if (!rustRange) continue;
31
32 buf.push([rustRange, this.findAstNodeRange(astLine)]);
33 }
34 return buf;
35 });
36
37 constructor(ctx: Ctx) {
38 ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: 'rust-analyzer' }, this));
39 ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
40 vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
41 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
42 vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
43
44 ctx.pushCleanup(this);
45 }
46 dispose() {
47 this.setRustEditor(undefined);
48 }
49
50 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
51 if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) {
52 this.rust2Ast.reset();
53 }
54 }
55
56 private onDidCloseTextDocument(doc: vscode.TextDocument) {
57 if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
58 this.setRustEditor(undefined);
59 }
60 }
61
62 private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
63 if (!this.findAstTextEditor()) {
64 this.setRustEditor(undefined);
65 return;
66 }
67 this.setRustEditor(editors.find(isRustEditor));
68 }
69
70 private findAstTextEditor(): undefined | vscode.TextEditor {
71 return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === 'rust-analyzer');
72 }
73
74 private setRustEditor(newRustEditor: undefined | RustEditor) {
75 if (this.rustEditor && this.rustEditor !== newRustEditor) {
76 this.rustEditor.setDecorations(this.astDecorationType, []);
77 this.rust2Ast.reset();
78 }
79 this.rustEditor = newRustEditor;
80 }
81
82 // additional positional params are omitted
83 provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult<vscode.DefinitionLink[]> {
84 if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return;
85
86 const astEditor = this.findAstTextEditor();
87 if (!astEditor) return;
88
89 const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos));
90 if (!rust2AstRanges) return;
91
92 const [rustFileRange, astFileRange] = rust2AstRanges;
93
94 astEditor.revealRange(astFileRange);
95 astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end);
96
97 return [{
98 targetRange: astFileRange,
99 targetUri: astEditor.document.uri,
100 originSelectionRange: rustFileRange,
101 targetSelectionRange: astFileRange,
102 }];
103 }
104
105 // additional positional params are omitted
106 provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
107 if (!this.rustEditor) return;
108
109 const astFileLine = doc.lineAt(hoverPosition.line);
110
111 const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
112 if (!rustFileRange) return;
113
114 this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
115 this.rustEditor.revealRange(rustFileRange);
116
117 const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
118 const astFileRange = this.findAstNodeRange(astFileLine);
119
120 return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
121 }
122
123 private findAstNodeRange(astLine: vscode.TextLine): vscode.Range {
124 const lineOffset = astLine.range.start;
125 const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
126 const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
127 return new vscode.Range(begin, end);
128 }
129
130 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
131 const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
132 if (!parsedRange) return;
133
134 const [begin, end] = parsedRange
135 .slice(1)
136 .map(off => this.positionAt(doc, +off));
137
138 return new vscode.Range(begin, end);
139 }
140
141 // Memoize the last value, otherwise the CPU is at 100% single core
142 // with quadratic lookups when we build rust2Ast cache
143 cache?: { doc: vscode.TextDocument; offset: number; line: number };
144
145 positionAt(doc: vscode.TextDocument, targetOffset: number): vscode.Position {
146 if (doc.eol === vscode.EndOfLine.LF) {
147 return doc.positionAt(targetOffset);
148 }
149
150 // Dirty workaround for crlf line endings
151 // We are still in this prehistoric era of carriage returns here...
152
153 let line = 0;
154 let offset = 0;
155
156 const cache = this.cache;
157 if (cache?.doc === doc && cache.offset <= targetOffset) {
158 ({ line, offset } = cache);
159 }
160
161 while (true) {
162 const lineLenWithLf = doc.lineAt(line).text.length + 1;
163 if (offset + lineLenWithLf > targetOffset) {
164 this.cache = { doc, offset, line };
165 return doc.positionAt(targetOffset + line);
166 }
167 offset += lineLenWithLf;
168 line += 1;
169 }
170 }
171}
172
173class Lazy<T> {
174 val: undefined | T;
175
176 constructor(private readonly compute: () => undefined | T) { }
177
178 get() {
179 return this.val ?? (this.val = this.compute());
180 }
181
182 reset() {
183 this.val = undefined;
184 }
185}