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+ ctx . pushCleanup ( new AstInspector ) ;
16+ ctx . pushCleanup ( tdcp ) ;
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,128 @@ 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-
66- class TextDocumentContentProvider implements vscode . TextDocumentContentProvider {
67- uri = vscode . Uri . parse ( 'rust-analyzer://syntaxtree' ) ;
68- eventEmitter = new vscode . EventEmitter < vscode . Uri > ( ) ;
38+ class TextDocumentContentProvider implements vscode . TextDocumentContentProvider , Disposable {
39+ readonly uri = vscode . Uri . parse ( 'rust-analyzer://syntaxtree' ) ;
40+ readonly eventEmitter = new vscode . EventEmitter < vscode . Uri > ( ) ;
41+ private readonly disposables : Disposable [ ] = [ ] ;
6942
7043 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 ( ) ) ;
7149 }
7250
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 '' ;
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 '' ;
7767
7868 // 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 )
69+ const range = uri . query === 'range=true' && ! rustEditor . selection . isEmpty
70+ ? this . ctx . client . code2ProtocolConverter . asRange ( rustEditor . selection )
8171 : null ;
8272
83- return client . sendRequest ( ra . syntaxTree , {
84- textDocument : { uri : editor . document . uri . toString ( ) } ,
85- range,
86- } ) ;
73+ const params = { textDocument : { uri : rustEditor . document . uri . toString ( ) } , range, } ;
74+ return this . ctx . client . sendRequest ( ra . syntaxTree , params , ct ) ;
8775 }
8876
8977 get onDidChange ( ) : vscode . Event < vscode . Uri > {
9078 return this . eventEmitter . event ;
9179 }
9280}
81+
82+
83+ // FIXME: consider implementing this via the Tree View API?
84+ // https://code.visualstudio.com/api/extension-guides/tree-view
85+ class 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+ }
0 commit comments