aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/commands/syntax_tree.ts
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/commands/syntax_tree.ts')
-rw-r--r--editors/code/src/commands/syntax_tree.ts263
1 files changed, 0 insertions, 263 deletions
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
deleted file mode 100644
index a5446c327..000000000
--- a/editors/code/src/commands/syntax_tree.ts
+++ /dev/null
@@ -1,263 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api';
3
4import { Ctx, Cmd, Disposable } from '../ctx';
5import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
6
7const AST_FILE_SCHEME = "rust-analyzer";
8
9// Opens the virtual file that will show the syntax tree
10//
11// The contents of the file come from the `TextDocumentContentProvider`
12export function syntaxTree(ctx: Ctx): Cmd {
13 const tdcp = new TextDocumentContentProvider(ctx);
14
15 void new AstInspector(ctx);
16
17 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
18 ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
19 brackets: [["[", ")"]],
20 }));
21
22 return async () => {
23 const editor = vscode.window.activeTextEditor;
24 const rangeEnabled = !!editor && !editor.selection.isEmpty;
25
26 const uri = rangeEnabled
27 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
28 : tdcp.uri;
29
30 const document = await vscode.workspace.openTextDocument(uri);
31
32 tdcp.eventEmitter.fire(uri);
33
34 void await vscode.window.showTextDocument(document, {
35 viewColumn: vscode.ViewColumn.Two,
36 preserveFocus: true
37 });
38 };
39}
40
41class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
42 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
43 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
44
45
46 constructor(private readonly ctx: Ctx) {
47 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
48 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
49 }
50
51 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
52 if (isRustDocument(event.document)) {
53 // We need to order this after language server updates, but there's no API for that.
54 // Hence, good old sleep().
55 void sleep(10).then(() => this.eventEmitter.fire(this.uri));
56 }
57 }
58 private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
59 if (editor && isRustEditor(editor)) {
60 this.eventEmitter.fire(this.uri);
61 }
62 }
63
64 provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
65 const rustEditor = this.ctx.activeRustEditor;
66 if (!rustEditor) return '';
67
68 // When the range based query is enabled we take the range of the selection
69 const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
70 ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
71 : null;
72
73 const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
74 return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
75 }
76
77 get onDidChange(): vscode.Event<vscode.Uri> {
78 return this.eventEmitter.event;
79 }
80}
81
82
83// FIXME: consider implementing this via the Tree View API?
84// https://code.visualstudio.com/api/extension-guides/tree-view
85class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
86 private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
87 borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
88 borderStyle: "solid",
89 borderWidth: "2px",
90
91 });
92 private rustEditor: undefined | RustEditor;
93
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
115 constructor(ctx: Ctx) {
116 ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
117 ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
118 vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
119 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
120 vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
121
122 ctx.pushCleanup(this);
123 }
124 dispose() {
125 this.setRustEditor(undefined);
126 }
127
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
134 private onDidCloseTextDocument(doc: vscode.TextDocument) {
135 if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
136 this.setRustEditor(undefined);
137 }
138 }
139
140 private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
141 if (!this.findAstTextEditor()) {
142 this.setRustEditor(undefined);
143 return;
144 }
145 this.setRustEditor(editors.find(isRustEditor));
146 }
147
148 private findAstTextEditor(): undefined | vscode.TextEditor {
149 return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === AST_FILE_SCHEME);
150 }
151
152 private setRustEditor(newRustEditor: undefined | RustEditor) {
153 if (this.rustEditor && this.rustEditor !== newRustEditor) {
154 this.rustEditor.setDecorations(this.astDecorationType, []);
155 this.rust2Ast.reset();
156 }
157 this.rustEditor = newRustEditor;
158 }
159
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
184 provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
185 if (!this.rustEditor) return;
186
187 const astFileLine = doc.lineAt(hoverPosition.line);
188
189 const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
190 if (!rustFileRange) return;
191
192 this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
193 this.rustEditor.revealRange(rustFileRange);
194
195 const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
196 const astFileRange = this.findAstNodeRange(astFileLine);
197
198 return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
199 }
200
201 private findAstNodeRange(astLine: vscode.TextLine): vscode.Range {
202 const lineOffset = astLine.range.start;
203 const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
204 const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
205 return new vscode.Range(begin, end);
206 }
207
208 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
209 const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
210 if (!parsedRange) return;
211
212 const [begin, end] = parsedRange
213 .slice(1)
214 .map(off => this.positionAt(doc, +off));
215
216 return new vscode.Range(begin, end);
217 }
218
219 // Memoize the last value, otherwise the CPU is at 100% single core
220 // with quadratic lookups when we build rust2Ast cache
221 cache?: { doc: vscode.TextDocument; offset: number; line: number };
222
223 positionAt(doc: vscode.TextDocument, targetOffset: number): vscode.Position {
224 if (doc.eol === vscode.EndOfLine.LF) {
225 return doc.positionAt(targetOffset);
226 }
227
228 // Dirty workaround for crlf line endings
229 // We are still in this prehistoric era of carriage returns here...
230
231 let line = 0;
232 let offset = 0;
233
234 const cache = this.cache;
235 if (cache?.doc === doc && cache.offset <= targetOffset) {
236 ({ line, offset } = cache);
237 }
238
239 while (true) {
240 const lineLenWithLf = doc.lineAt(line).text.length + 1;
241 if (offset + lineLenWithLf > targetOffset) {
242 this.cache = { doc, offset, line };
243 return doc.positionAt(targetOffset + line);
244 }
245 offset += lineLenWithLf;
246 line += 1;
247 }
248 }
249}
250
251class Lazy<T> {
252 val: undefined | T;
253
254 constructor(private readonly compute: () => undefined | T) { }
255
256 get() {
257 return this.val ?? (this.val = this.compute());
258 }
259
260 reset() {
261 this.val = undefined;
262 }
263}