11import * as vscode from 'vscode' ;
22import * as ra from '../rust-analyzer-api' ;
33
4- import { Ctx , Cmd } from '../ctx' ;
5- import { isRustDocument } from '../util' ;
4+ import { Ctx , Cmd , Disposable } from '../ctx' ;
5+ import { isRustDocument , RustEditor , isRustEditor , sleep } from '../util' ;
6+
7+ const AST_FILE_SCHEME = "rust-analyzer" ;
68
79// Opens the virtual file that will show the syntax tree
810//
911// The contents of the file come from the `TextDocumentContentProvider`
1012export function syntaxTree ( ctx : Ctx ) : Cmd {
1113 const tdcp = new TextDocumentContentProvider ( ctx ) ;
1214
13- ctx . pushCleanup (
14- vscode . workspace . registerTextDocumentContentProvider (
15- 'rust-analyzer' ,
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- ) ;
15+ void new AstInspector ( ctx ) ;
16+
17+ ctx . pushCleanup ( vscode . workspace . registerTextDocumentContentProvider ( AST_FILE_SCHEME , tdcp ) ) ;
3818
3919 return async ( ) => {
4020 const editor = vscode . window . activeTextEditor ;
41- const rangeEnabled = ! ! ( editor && ! editor . selection . isEmpty ) ;
21+ const rangeEnabled = ! ! editor && ! editor . selection . isEmpty ;
4222
4323 const uri = rangeEnabled
4424 ? vscode . Uri . parse ( `${ tdcp . uri . toString ( ) } ?range=true` )
@@ -48,45 +28,126 @@ export function syntaxTree(ctx: Ctx): Cmd {
4828
4929 tdcp . eventEmitter . fire ( uri ) ;
5030
51- return vscode . window . showTextDocument (
52- document ,
53- vscode . ViewColumn . Two ,
54- true ,
55- ) ;
31+ void await vscode . window . showTextDocument ( document , {
32+ viewColumn : vscode . ViewColumn . Two ,
33+ preserveFocus : true
34+ } ) ;
5635 } ;
5736}
5837
59- // We need to order this after LS updates, but there's no API for that.
60- // Hence, good old setTimeout.
61- function afterLs ( f : ( ) => void ) {
62- setTimeout ( f , 10 ) ;
63- }
64-
65-
6638class TextDocumentContentProvider implements vscode . TextDocumentContentProvider {
67- uri = vscode . Uri . parse ( 'rust-analyzer://syntaxtree' ) ;
68- eventEmitter = new vscode . EventEmitter < vscode . Uri > ( ) ;
39+ readonly uri = vscode . Uri . parse ( 'rust-analyzer://syntaxtree' ) ;
40+ readonly eventEmitter = new vscode . EventEmitter < vscode . Uri > ( ) ;
41+
6942
7043 constructor ( private readonly ctx : Ctx ) {
44+ vscode . workspace . onDidChangeTextDocument ( this . onDidChangeTextDocument , this , ctx . subscriptions ) ;
45+ vscode . window . onDidChangeActiveTextEditor ( this . onDidChangeActiveTextEditor , this , ctx . subscriptions ) ;
7146 }
7247
73- provideTextDocumentContent ( uri : vscode . Uri ) : vscode . ProviderResult < string > {
74- const editor = vscode . window . activeTextEditor ;
75- const client = this . ctx . client ;
76- if ( ! editor || ! client ) return '' ;
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 '' ;
7764
7865 // When the range based query is enabled we take the range of the selection
79- const range = uri . query === 'range=true' && ! editor . selection . isEmpty
80- ? client . code2ProtocolConverter . asRange ( editor . selection )
66+ const range = uri . query === 'range=true' && ! rustEditor . selection . isEmpty
67+ ? this . ctx . client . code2ProtocolConverter . asRange ( rustEditor . selection )
8168 : null ;
8269
83- return client . sendRequest ( ra . syntaxTree , {
84- textDocument : { uri : editor . document . uri . toString ( ) } ,
85- range,
86- } ) ;
70+ const params = { textDocument : { uri : rustEditor . document . uri . toString ( ) } , range, } ;
71+ return this . ctx . client . sendRequest ( ra . syntaxTree , params , ct ) ;
8772 }
8873
8974 get onDidChange ( ) : vscode . Event < vscode . Uri > {
9075 return this . eventEmitter . event ;
9176 }
9277}
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+ fontStyle : "normal" ,
85+ border : "#ffffff 1px solid" ,
86+ } ) ;
87+ private rustEditor : undefined | RustEditor ;
88+
89+ constructor ( ctx : Ctx ) {
90+ ctx . pushCleanup ( vscode . languages . registerHoverProvider ( { scheme : AST_FILE_SCHEME } , this ) ) ;
91+ vscode . workspace . onDidCloseTextDocument ( this . onDidCloseTextDocument , this , ctx . subscriptions ) ;
92+ vscode . window . onDidChangeVisibleTextEditors ( this . onDidChangeVisibleTextEditors , this , ctx . subscriptions ) ;
93+
94+ ctx . pushCleanup ( this ) ;
95+ }
96+ dispose ( ) {
97+ this . setRustEditor ( undefined ) ;
98+ }
99+
100+ private onDidCloseTextDocument ( doc : vscode . TextDocument ) {
101+ if ( this . rustEditor && doc . uri . toString ( ) === this . rustEditor . document . uri . toString ( ) ) {
102+ this . setRustEditor ( undefined ) ;
103+ }
104+ }
105+
106+ private onDidChangeVisibleTextEditors ( editors : vscode . TextEditor [ ] ) {
107+ if ( editors . every ( suspect => suspect . document . uri . scheme !== AST_FILE_SCHEME ) ) {
108+ this . setRustEditor ( undefined ) ;
109+ return ;
110+ }
111+ this . setRustEditor ( editors . find ( isRustEditor ) ) ;
112+ }
113+
114+ private setRustEditor ( newRustEditor : undefined | RustEditor ) {
115+ if ( newRustEditor !== this . rustEditor ) {
116+ this . rustEditor ?. setDecorations ( AstInspector . astDecorationType , [ ] ) ;
117+ }
118+ this . rustEditor = newRustEditor ;
119+ }
120+
121+ provideHover ( doc : vscode . TextDocument , hoverPosition : vscode . Position ) : vscode . ProviderResult < vscode . Hover > {
122+ if ( ! this . rustEditor ) return ;
123+
124+ const astTextLine = doc . lineAt ( hoverPosition . line ) ;
125+
126+ const rustTextRange = this . parseRustTextRange ( this . rustEditor . document , astTextLine . text ) ;
127+ if ( ! rustTextRange ) return ;
128+
129+ this . rustEditor . setDecorations ( AstInspector . astDecorationType , [ rustTextRange ] ) ;
130+ this . rustEditor . revealRange ( rustTextRange ) ;
131+
132+ const rustSourceCode = this . rustEditor . document . getText ( rustTextRange ) ;
133+ const astTextRange = this . findAstRange ( astTextLine ) ;
134+
135+ return new vscode . Hover ( [ "```rust\n" + rustSourceCode + "\n```" ] , astTextRange ) ;
136+ }
137+
138+ private findAstRange ( astLine : vscode . TextLine ) {
139+ const lineOffset = astLine . range . start ;
140+ const begin = lineOffset . translate ( undefined , astLine . firstNonWhitespaceCharacterIndex ) ;
141+ const end = lineOffset . translate ( undefined , astLine . text . trimEnd ( ) . length ) ;
142+ return new vscode . Range ( begin , end ) ;
143+ }
144+
145+ private parseRustTextRange ( doc : vscode . TextDocument , astLine : string ) : undefined | vscode . Range {
146+ const parsedRange = / \[ ( \d + ) ; ( \d + ) \) / . exec ( astLine ) ;
147+ if ( ! parsedRange ) return ;
148+
149+ const [ begin , end ] = parsedRange . slice ( 1 ) . map ( off => doc . positionAt ( + off ) ) ;
150+
151+ return new vscode . Range ( begin , end ) ;
152+ }
153+ }
0 commit comments