aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorveetaha <[email protected]>2020-03-31 14:05:42 +0100
committerveetaha <[email protected]>2020-03-31 14:20:59 +0100
commit09a760e52e20dcd79d902b05065934615cc4d56b (patch)
treedcc97feaab96dcd25fc7712d38d6fd1cd189b113
parent6f0d8db529478ce41b429f06708fa600a97c2151 (diff)
vscode: add syntax tree inspection hovers and highlights
-rw-r--r--editors/code/src/commands/syntax_tree.ts169
-rw-r--r--editors/code/src/util.ts4
2 files changed, 118 insertions, 55 deletions
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
index 2e08e8f11..21ecf2661 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,13 @@ 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 ctx.pushCleanup(new AstInspector);
14 vscode.workspace.registerTextDocumentContentProvider( 16 ctx.pushCleanup(tdcp);
15 'rust-analyzer', 17 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
16 tdcp,
17 ),
18 );
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 18
39 return async () => { 19 return async () => {
40 const editor = vscode.window.activeTextEditor; 20 const editor = vscode.window.activeTextEditor;
41 const rangeEnabled = !!(editor && !editor.selection.isEmpty); 21 const rangeEnabled = !!editor && !editor.selection.isEmpty;
42 22
43 const uri = rangeEnabled 23 const uri = rangeEnabled
44 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) 24 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
@@ -48,45 +28,128 @@ export function syntaxTree(ctx: Ctx): Cmd {
48 28
49 tdcp.eventEmitter.fire(uri); 29 tdcp.eventEmitter.fire(uri);
50 30
51 return vscode.window.showTextDocument( 31 void await vscode.window.showTextDocument(document, {
52 document, 32 viewColumn: vscode.ViewColumn.Two,
53 vscode.ViewColumn.Two, 33 preserveFocus: true
54 true, 34 });
55 );
56 }; 35 };
57} 36}
58 37
59// We need to order this after LS updates, but there's no API for that. 38class TextDocumentContentProvider implements vscode.TextDocumentContentProvider, Disposable {
60// Hence, good old setTimeout. 39 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
61function afterLs(f: () => void) { 40 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
62 setTimeout(f, 10); 41 private readonly disposables: Disposable[] = [];
63}
64
65
66class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
67 uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
68 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
69 42
70 constructor(private readonly ctx: Ctx) { 43 constructor(private readonly ctx: Ctx) {
44 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
45 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables);
46 }
47 dispose() {
48 this.disposables.forEach(d => d.dispose());
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, Disposable {
86 private static readonly astDecorationType = vscode.window.createTextEditorDecorationType({
87 fontStyle: "normal",
88 border: "#ffffff 1px solid",
89 });
90 private rustEditor: undefined | RustEditor;
91 private readonly disposables: Disposable[] = [];
92
93 constructor() {
94 this.disposables.push(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
95 vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables);
96 vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
97 }
98 dispose() {
99 this.setRustEditor(undefined);
100 this.disposables.forEach(d => d.dispose());
101 }
102
103 private onDidCloseTextDocument(doc: vscode.TextDocument) {
104 if (!!this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
105 this.setRustEditor(undefined);
106 }
107 }
108
109 private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
110 if (editors.every(suspect => suspect.document.uri.scheme !== AST_FILE_SCHEME)) {
111 this.setRustEditor(undefined);
112 return;
113 }
114 this.setRustEditor(editors.find(isRustEditor));
115 }
116
117 private setRustEditor(newRustEditor: undefined | RustEditor) {
118 if (newRustEditor !== this.rustEditor) {
119 this.rustEditor?.setDecorations(AstInspector.astDecorationType, []);
120 }
121 this.rustEditor = newRustEditor;
122 }
123
124 provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
125 if (!this.rustEditor) return;
126
127 const astTextLine = doc.lineAt(hoverPosition.line);
128
129 const rustTextRange = this.parseRustTextRange(this.rustEditor.document, astTextLine.text);
130 if (!rustTextRange) return;
131
132 this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]);
133
134 const rustSourceCode = this.rustEditor.document.getText(rustTextRange);
135 const astTextRange = this.findAstRange(astTextLine);
136
137 return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astTextRange);
138 }
139
140 private findAstRange(astLine: vscode.TextLine) {
141 const lineOffset = astLine.range.start;
142 const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
143 const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
144 return new vscode.Range(begin, end);
145 }
146
147 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
148 const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine);
149 if (!parsedRange) return;
150
151 const [, begin, end] = parsedRange.map(off => doc.positionAt(+off));
152
153 return new vscode.Range(begin, end);
154 }
155}
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 978a31751..6f91f81d6 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -65,12 +65,12 @@ export async function sendRequestWithRetry<TParam, TRet>(
65 throw 'unreachable'; 65 throw 'unreachable';
66} 66}
67 67
68function sleep(ms: number) { 68export function sleep(ms: number) {
69 return new Promise(resolve => setTimeout(resolve, ms)); 69 return new Promise(resolve => setTimeout(resolve, ms));
70} 70}
71 71
72export type RustDocument = vscode.TextDocument & { languageId: "rust" }; 72export type RustDocument = vscode.TextDocument & { languageId: "rust" };
73export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; 73export type RustEditor = vscode.TextEditor & { document: RustDocument };
74 74
75export function isRustDocument(document: vscode.TextDocument): document is RustDocument { 75export function isRustDocument(document: vscode.TextDocument): document is RustDocument {
76 return document.languageId === 'rust' 76 return document.languageId === 'rust'