Skip to content

Commit 732eb83

Browse files
committed
Select the appropriate node in the AST viewer when the editor text selection changes
When a user clicks in an editor that whose source tree is currently being displayed in the ast viewer, the viewer selection will stay in sync with the editor selection.
1 parent 7e5d592 commit 732eb83

11 files changed

Lines changed: 17695 additions & 36 deletions

File tree

extensions/ql-vscode/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,6 @@
704704
"zip-a-folder": "~0.0.12"
705705
},
706706
"devDependencies": {
707-
"@types/semver": "~7.2.0",
708707
"@types/chai": "^4.1.7",
709708
"@types/chai-as-promised": "~7.1.2",
710709
"@types/child-process-promise": "^2.2.1",
@@ -714,7 +713,7 @@
714713
"@types/google-protobuf": "^3.2.7",
715714
"@types/gulp": "^4.0.6",
716715
"@types/gulp-sourcemaps": "0.0.32",
717-
"@types/js-yaml": "~3.12.2",
716+
"@types/js-yaml": "^3.12.5",
718717
"@types/jszip": "~3.1.6",
719718
"@types/mocha": "~8.0.3",
720719
"@types/node": "^12.0.8",
@@ -723,6 +722,7 @@
723722
"@types/react": "^16.8.17",
724723
"@types/react-dom": "^16.8.4",
725724
"@types/sarif": "~2.1.2",
725+
"@types/semver": "~7.2.0",
726726
"@types/sinon": "~7.5.2",
727727
"@types/sinon-chai": "~3.2.3",
728728
"@types/through2": "^2.0.36",
Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,81 @@
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

318
import { DatabaseItem } from './databases';
419
import { UrlValue, BqrsId } from './bqrs-cli-types';
5-
import fileRangeFromURI from './contextual/fileRangeFromURI';
620
import { showLocation } from './interface-utils';
721
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './bqrs-utils';
822

923
export 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

1833
export 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

8598
export 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
}

extensions/ql-vscode/src/contextual/astBuilder.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CodeQLCliServer } from '../cli';
33
import { DecodedBqrsChunk, BqrsId, EntityValue } from '../bqrs-cli-types';
44
import { DatabaseItem } from '../databases';
55
import { AstItem, RootAstItem } from '../astViewer';
6+
import fileRangeFromURI from './fileRangeFromURI';
67

78
/**
89
* A class that wraps a tree of QL results from a query that
@@ -87,6 +88,7 @@ export default class AstBuilder {
8788
id,
8889
label: entity.label,
8990
location: entity.url,
91+
fileLocation: fileRangeFromURI(entity.url, this.db),
9092
children: [] as AstItem[],
9193
order: Number.MAX_SAFE_INTEGER
9294
};
@@ -95,8 +97,8 @@ export default class AstBuilder {
9597
const parent = idToItem.get(childToParent.has(id) ? childToParent.get(id)! : -1);
9698

9799
if (parent) {
98-
const astItem = item as AstItem;
99-
astItem.parent = parent;
100+
const astItem = item as unknown as AstItem;
101+
(astItem).parent = parent;
100102
parent.children.push(astItem);
101103
}
102104
const children = parentToChildren.has(id) ? parentToChildren.get(id)! : [];

extensions/ql-vscode/src/contextual/fileRangeFromURI.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { UrlValue, LineColumnLocation } from '../bqrs-cli-types';
44
import { DatabaseItem } from '../databases';
55

66

7-
export default function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): vscode.Location | undefined {
8-
if (typeof uri === 'string') {
7+
export default function fileRangeFromURI(uri: UrlValue | undefined, db: DatabaseItem): vscode.Location | undefined {
8+
if (!uri || typeof uri === 'string') {
99
return undefined;
1010
} else if ('startOffset' in uri) {
1111
return undefined;

extensions/ql-vscode/src/contextual/templateProvider.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as vscode from 'vscode';
2-
import * as path from 'path';
32

43
import { decodeSourceArchiveUri, zipArchiveScheme } from '../archive-filesystem-provider';
54
import { CodeQLCliServer } from '../cli';
@@ -118,7 +117,7 @@ export class TemplatePrintAstProvider {
118117
return new AstBuilder(
119118
queryResults, this.cli,
120119
this.dbm.findDatabaseItem(vscode.Uri.parse(queryResults.database.databaseUri!))!,
121-
path.basename(document.fileName)
120+
document.fileName
122121
);
123122
}
124123

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ async function activateWithInstalledDistribution(
549549
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
550550
);
551551

552-
const astViewer = new AstViewer();
552+
const astViewer = new AstViewer(ctx);
553553
ctx.subscriptions.push(commands.registerCommand('codeQL.viewAst', async () => {
554554
const ast = await new TemplatePrintAstProvider(cliServer, qs, dbm)
555555
.provideAst(window.activeTextEditor?.document);

extensions/ql-vscode/src/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemeP
262262
});
263263
for (const { packDir, packName } of packs) {
264264
if (packDir !== undefined) {
265-
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
265+
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')) as { dbscheme: string };
266266
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
267267
return packName;
268268
}

0 commit comments

Comments
 (0)