diff options
Diffstat (limited to 'editors/code/src/ast_inspector.ts')
-rw-r--r-- | editors/code/src/ast_inspector.ts | 185 |
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 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import { Ctx, Disposable } from './ctx'; | ||
4 | import { 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 | ||
8 | export 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 | |||
173 | class 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 | } | ||