1- import * as vscode from 'vscode' ;
1+ import {
2+ window ,
3+ ExtensionContext ,
4+ TreeDataProvider ,
5+ EventEmitter ,
6+ commands ,
7+ Event ,
8+ ProviderResult ,
9+ TreeItemCollapsibleState ,
10+ TreeItem ,
11+ TreeView ,
12+ TextEditorSelectionChangeEvent ,
13+ Location ,
14+ Range
15+ } from 'vscode' ;
16+ import * as path from 'path' ;
217
318import { DatabaseItem } from './databases' ;
419import { UrlValue , BqrsId } from './bqrs-cli-types' ;
5- import fileRangeFromURI from './contextual/fileRangeFromURI' ;
620import { showLocation } from './interface-utils' ;
721import { isStringLoc , isWholeFileLoc , isLineColumnLoc } from './bqrs-utils' ;
822
923export interface AstItem {
1024 id : BqrsId ;
1125 label ?: string ;
1226 location ?: UrlValue ;
27+ fileLocation ?: Location ;
1328 parent : AstItem | RootAstItem ;
1429 children : AstItem [ ] ;
1530 order : number ;
1631}
1732
1833export type RootAstItem = Omit < AstItem , 'parent' > ;
1934
20- class AstViewerDataProvider implements vscode . TreeDataProvider < AstItem | RootAstItem > {
35+ class AstViewerDataProvider implements TreeDataProvider < AstItem | RootAstItem > {
2136
2237 public roots : RootAstItem [ ] = [ ] ;
2338 public db : DatabaseItem | undefined ;
2439
2540 private _onDidChangeTreeData =
26- new vscode . EventEmitter < AstItem | undefined > ( ) ;
27- readonly onDidChangeTreeData : vscode . Event < AstItem | undefined > =
41+ new EventEmitter < AstItem | undefined > ( ) ;
42+ readonly onDidChangeTreeData : Event < AstItem | undefined > =
2843 this . _onDidChangeTreeData . event ;
2944
3045 constructor ( ) {
31- vscode . commands . registerCommand ( 'codeQLAstViewer.gotoCode' ,
32- async ( location : UrlValue , db : DatabaseItem ) => {
33- if ( location ) {
34- await showLocation ( fileRangeFromURI ( location , db ) ) ;
35- }
46+ commands . registerCommand ( 'codeQLAstViewer.gotoCode' ,
47+ async ( item : AstItem ) => {
48+ await showLocation ( item . fileLocation ) ;
3649 } ) ;
3750 }
3851
3952 refresh ( ) : void {
4053 this . _onDidChangeTreeData . fire ( ) ;
4154 }
42- getChildren ( item ?: AstItem ) : vscode . ProviderResult < ( AstItem | RootAstItem ) [ ] > {
55+ getChildren ( item ?: AstItem ) : ProviderResult < ( AstItem | RootAstItem ) [ ] > {
4356 const children = item ? item . children : this . roots ;
4457 return children . sort ( ( c1 , c2 ) => ( c1 . order - c2 . order ) ) ;
4558 }
4659
47- getParent ( item : AstItem ) : vscode . ProviderResult < AstItem > {
60+ getParent ( item : AstItem ) : ProviderResult < AstItem > {
4861 return item . parent as AstItem ;
4962 }
5063
51- getTreeItem ( item : AstItem ) : vscode . TreeItem {
64+ getTreeItem ( item : AstItem ) : TreeItem {
5265 const line = this . extractLineInfo ( item ?. location ) ;
5366
5467 const state = item . children . length
55- ? vscode . TreeItemCollapsibleState . Collapsed
56- : vscode . TreeItemCollapsibleState . None ;
57- const treeItem = new vscode . TreeItem ( item . label || '' , state ) ;
68+ ? TreeItemCollapsibleState . Collapsed
69+ : TreeItemCollapsibleState . None ;
70+ const treeItem = new TreeItem ( item . label || '' , state ) ;
5871 treeItem . description = line ? `Line ${ line } ` : '' ;
5972 treeItem . id = String ( item . id ) ;
6073 treeItem . tooltip = `${ treeItem . description } ${ treeItem . label } ` ;
6174 treeItem . command = {
6275 command : 'codeQLAstViewer.gotoCode' ,
6376 title : 'Go To Code' ,
6477 tooltip : `Go To ${ item . location } ` ,
65- arguments : [ item . location , this . db ]
78+ arguments : [ item ]
6679 } ;
6780 return treeItem ;
6881 }
@@ -83,33 +96,83 @@ class AstViewerDataProvider implements vscode.TreeDataProvider<AstItem | RootAst
8396}
8497
8598export class AstViewer {
86- private treeView : vscode . TreeView < AstItem | RootAstItem > ;
99+ private treeView : TreeView < AstItem | RootAstItem > ;
87100 private treeDataProvider : AstViewerDataProvider ;
101+ private currentFile : string | undefined ;
88102
89- constructor ( ) {
103+ constructor ( ctx : ExtensionContext ) {
90104 this . treeDataProvider = new AstViewerDataProvider ( ) ;
91- this . treeView = vscode . window . createTreeView ( 'codeQLAstViewer' , {
105+ this . treeView = window . createTreeView ( 'codeQLAstViewer' , {
92106 treeDataProvider : this . treeDataProvider ,
93107 showCollapseAll : true
94108 } ) ;
95109
96- vscode . commands . registerCommand ( 'codeQLAstViewer.clear' , ( ) => {
110+ commands . registerCommand ( 'codeQLAstViewer.clear' , ( ) => {
97111 this . clear ( ) ;
98112 } ) ;
113+
114+ ctx . subscriptions . push ( window . onDidChangeTextEditorSelection ( this . updateTreeSelection , this ) ) ;
99115 }
100116
101117 updateRoots ( roots : RootAstItem [ ] , db : DatabaseItem , fileName : string ) {
102118 this . treeDataProvider . roots = roots ;
103119 this . treeDataProvider . db = db ;
104120 this . treeDataProvider . refresh ( ) ;
105- this . treeView . message = `AST for ${ fileName } ` ;
106- this . treeView . reveal ( roots [ 0 ] , { focus : true } ) ;
121+ this . treeView . message = `AST for ${ path . basename ( fileName ) } ` ;
122+ this . treeView . reveal ( roots [ 0 ] , { focus : false } ) ;
123+ this . currentFile = fileName ;
124+ }
125+
126+ private updateTreeSelection ( e : TextEditorSelectionChangeEvent ) {
127+ function isInside ( selectedRange : Range , astRange ?: Range ) : boolean {
128+ return ! ! astRange ?. contains ( selectedRange ) ;
129+ }
130+
131+ // Recursively iterate all children until we find the node with the smallest
132+ // range that contains the selection.
133+ // Some nodes do not have a location, but their children might, so must
134+ // recurse though location-less AST nodes to see if children are correct.
135+ function findBest ( selectedRange : Range , items ?: RootAstItem [ ] ) : RootAstItem | undefined {
136+ if ( ! items || ! items . length ) {
137+ return ;
138+ }
139+ for ( const item of items ) {
140+ let candidate : RootAstItem | undefined = undefined ;
141+ if ( isInside ( selectedRange , item . fileLocation ?. range ) ) {
142+ candidate = item ;
143+ }
144+ // always iterate through children since the location of an AST node in code QL does not
145+ // always cover the complete text of the node.
146+ candidate = findBest ( selectedRange , item . children ) || candidate ;
147+ if ( candidate ) {
148+ return candidate ;
149+ }
150+ }
151+ return ;
152+ }
153+
154+ if (
155+ this . treeView . visible &&
156+ e . textEditor . document . uri . fsPath === this . currentFile &&
157+ e . selections . length === 1
158+ ) {
159+ const selection = e . selections [ 0 ] ;
160+ const range = selection . anchor . isBefore ( selection . active )
161+ ? new Range ( selection . anchor , selection . active )
162+ : new Range ( selection . active , selection . anchor ) ;
163+
164+ const targetItem = findBest ( range , this . treeDataProvider . roots ) ;
165+ if ( targetItem ) {
166+ this . treeView . reveal ( targetItem ) ;
167+ }
168+ }
107169 }
108170
109171 private clear ( ) {
110172 this . treeDataProvider . roots = [ ] ;
111173 this . treeDataProvider . db = undefined ;
112174 this . treeDataProvider . refresh ( ) ;
113175 this . treeView . message = undefined ;
176+ this . currentFile = undefined ;
114177 }
115178}
0 commit comments