]> git.lizzy.rs Git - rust.git/blob - editors/code/src/commands/syntax_tree.ts
vscode: add support for light themes and color customization for syntax tree highlights
[rust.git] / editors / code / src / commands / syntax_tree.ts
1 import * as vscode from 'vscode';
2 import * as ra from '../rust-analyzer-api';
3
4 import { Ctx, Cmd, Disposable } from '../ctx';
5 import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
6
7 const 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`
12 export 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
19     return async () => {
20         const editor = vscode.window.activeTextEditor;
21         const rangeEnabled = !!editor && !editor.selection.isEmpty;
22
23         const uri = rangeEnabled
24             ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
25             : tdcp.uri;
26
27         const document = await vscode.workspace.openTextDocument(uri);
28
29         tdcp.eventEmitter.fire(uri);
30
31         void await vscode.window.showTextDocument(document, {
32             viewColumn: vscode.ViewColumn.Two,
33             preserveFocus: true
34         });
35     };
36 }
37
38 class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
39     readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
40     readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
41
42
43     constructor(private readonly ctx: Ctx) {
44         vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
45         vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
46     }
47
48     private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
49         if (isRustDocument(event.document)) {
50             // We need to order this after language server updates, but there's no API for that.
51             // Hence, good old sleep().
52             void sleep(10).then(() => this.eventEmitter.fire(this.uri));
53         }
54     }
55     private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
56         if (editor && isRustEditor(editor)) {
57             this.eventEmitter.fire(this.uri);
58         }
59     }
60
61     provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
62         const rustEditor = this.ctx.activeRustEditor;
63         if (!rustEditor) return '';
64
65         // When the range based query is enabled we take the range of the selection
66         const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
67             ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
68             : null;
69
70         const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
71         return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
72     }
73
74     get onDidChange(): vscode.Event<vscode.Uri> {
75         return this.eventEmitter.event;
76     }
77 }
78
79
80 // FIXME: consider implementing this via the Tree View API?
81 // https://code.visualstudio.com/api/extension-guides/tree-view
82 class AstInspector implements vscode.HoverProvider, Disposable {
83     private static readonly astDecorationType = vscode.window.createTextEditorDecorationType({
84         borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
85         borderStyle: "solid",
86         borderWidth: "2px",
87
88     });
89     private rustEditor: undefined | RustEditor;
90
91     constructor(ctx: Ctx) {
92         ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
93         vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
94         vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
95
96         ctx.pushCleanup(this);
97     }
98     dispose() {
99         this.setRustEditor(undefined);
100     }
101
102     private onDidCloseTextDocument(doc: vscode.TextDocument) {
103         if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
104             this.setRustEditor(undefined);
105         }
106     }
107
108     private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
109         if (editors.every(suspect => suspect.document.uri.scheme !== AST_FILE_SCHEME)) {
110             this.setRustEditor(undefined);
111             return;
112         }
113         this.setRustEditor(editors.find(isRustEditor));
114     }
115
116     private setRustEditor(newRustEditor: undefined | RustEditor) {
117         if (newRustEditor !== this.rustEditor) {
118             this.rustEditor?.setDecorations(AstInspector.astDecorationType, []);
119         }
120         this.rustEditor = newRustEditor;
121     }
122
123     provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
124         if (!this.rustEditor) return;
125
126         const astTextLine = doc.lineAt(hoverPosition.line);
127
128         const rustTextRange = this.parseRustTextRange(this.rustEditor.document, astTextLine.text);
129         if (!rustTextRange) return;
130
131         this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]);
132         this.rustEditor.revealRange(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.slice(1).map(off => doc.positionAt(+off));
152
153         return new vscode.Range(begin, end);
154     }
155 }