aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/commands
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/commands')
-rw-r--r--editors/code/src/commands/runnables.ts4
-rw-r--r--editors/code/src/commands/syntax_tree.ts242
2 files changed, 194 insertions, 52 deletions
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
index 357155163..2635a1440 100644
--- a/editors/code/src/commands/runnables.ts
+++ b/editors/code/src/commands/runnables.ts
@@ -66,6 +66,10 @@ export function debugSingle(ctx: Ctx): Cmd {
66 return async (config: ra.Runnable) => { 66 return async (config: ra.Runnable) => {
67 const editor = ctx.activeRustEditor; 67 const editor = ctx.activeRustEditor;
68 if (!editor) return; 68 if (!editor) return;
69 if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) {
70 vscode.window.showErrorMessage("Install `vadimcn.vscode-lldb` extension for debugging");
71 return;
72 }
69 73
70 const debugConfig = { 74 const debugConfig = {
71 type: "lldb", 75 type: "lldb",
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
index 2e08e8f11..b7a397414 100644
--- a/editors/code/src/commands/syntax_tree.ts
+++ b/editors/code/src/commands/syntax_tree.ts
@@ -1,8 +1,10 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api'; 2import * as ra from '../rust-analyzer-api';
3 3
4import { Ctx, Cmd } from '../ctx'; 4import { Ctx, Cmd, Disposable } from '../ctx';
5import { isRustDocument } from '../util'; 5import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
6
7const AST_FILE_SCHEME = "rust-analyzer";
6 8
7// Opens the virtual file that will show the syntax tree 9// Opens the virtual file that will show the syntax tree
8// 10//
@@ -10,35 +12,16 @@ import { isRustDocument } from '../util';
10export function syntaxTree(ctx: Ctx): Cmd { 12export function syntaxTree(ctx: Ctx): Cmd {
11 const tdcp = new TextDocumentContentProvider(ctx); 13 const tdcp = new TextDocumentContentProvider(ctx);
12 14
13 ctx.pushCleanup( 15 void new AstInspector(ctx);
14 vscode.workspace.registerTextDocumentContentProvider( 16
15 'rust-analyzer', 17 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
16 tdcp, 18 ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
17 ), 19 brackets: [["[", ")"]],
18 ); 20 }));
19
20 vscode.workspace.onDidChangeTextDocument(
21 (event: vscode.TextDocumentChangeEvent) => {
22 const doc = event.document;
23 if (!isRustDocument(doc)) return;
24 afterLs(() => tdcp.eventEmitter.fire(tdcp.uri));
25 },
26 null,
27 ctx.subscriptions,
28 );
29
30 vscode.window.onDidChangeActiveTextEditor(
31 (editor: vscode.TextEditor | undefined) => {
32 if (!editor || !isRustDocument(editor.document)) return;
33 tdcp.eventEmitter.fire(tdcp.uri);
34 },
35 null,
36 ctx.subscriptions,
37 );
38 21
39 return async () => { 22 return async () => {
40 const editor = vscode.window.activeTextEditor; 23 const editor = vscode.window.activeTextEditor;
41 const rangeEnabled = !!(editor && !editor.selection.isEmpty); 24 const rangeEnabled = !!editor && !editor.selection.isEmpty;
42 25
43 const uri = rangeEnabled 26 const uri = rangeEnabled
44 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) 27 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
@@ -48,45 +31,200 @@ export function syntaxTree(ctx: Ctx): Cmd {
48 31
49 tdcp.eventEmitter.fire(uri); 32 tdcp.eventEmitter.fire(uri);
50 33
51 return vscode.window.showTextDocument( 34 void await vscode.window.showTextDocument(document, {
52 document, 35 viewColumn: vscode.ViewColumn.Two,
53 vscode.ViewColumn.Two, 36 preserveFocus: true
54 true, 37 });
55 );
56 }; 38 };
57} 39}
58 40
59// We need to order this after LS updates, but there's no API for that.
60// Hence, good old setTimeout.
61function afterLs(f: () => void) {
62 setTimeout(f, 10);
63}
64
65
66class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { 41class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
67 uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); 42 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
68 eventEmitter = new vscode.EventEmitter<vscode.Uri>(); 43 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
44
69 45
70 constructor(private readonly ctx: Ctx) { 46 constructor(private readonly ctx: Ctx) {
47 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
48 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
71 } 49 }
72 50
73 provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> { 51 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
74 const editor = vscode.window.activeTextEditor; 52 if (isRustDocument(event.document)) {
75 const client = this.ctx.client; 53 // We need to order this after language server updates, but there's no API for that.
76 if (!editor || !client) return ''; 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 '';
77 67
78 // When the range based query is enabled we take the range of the selection 68 // When the range based query is enabled we take the range of the selection
79 const range = uri.query === 'range=true' && !editor.selection.isEmpty 69 const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
80 ? client.code2ProtocolConverter.asRange(editor.selection) 70 ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
81 : null; 71 : null;
82 72
83 return client.sendRequest(ra.syntaxTree, { 73 const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
84 textDocument: { uri: editor.document.uri.toString() }, 74 return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
85 range,
86 });
87 } 75 }
88 76
89 get onDidChange(): vscode.Event<vscode.Uri> { 77 get onDidChange(): vscode.Event<vscode.Uri> {
90 return this.eventEmitter.event; 78 return this.eventEmitter.event;
91 } 79 }
92} 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) {
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.slice(1).map(off => doc.positionAt(+off));
213
214 return new vscode.Range(begin, end);
215 }
216}
217
218class 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}