Skip to content

Commit 7a2edfb

Browse files
committed
Add commands for path navigation
1 parent c0ffb7e commit 7a2edfb

File tree

7 files changed

+103
-9
lines changed

7 files changed

+103
-9
lines changed

extensions/ql-vscode/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,14 @@
170170
{
171171
"command": "codeQLQueryHistory.itemClicked",
172172
"title": "Query History Item"
173+
},
174+
{
175+
"command": "codeQLQueryResults.nextPathStep",
176+
"title": "CodeQL: Show Next Step on Path"
177+
},
178+
{
179+
"command": "codeQLQueryResults.previousPathStep",
180+
"title": "CodeQL: Show Previous Step on Path"
173181
}
174182
],
175183
"menus": {

extensions/ql-vscode/src/interface-types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ export interface SetStateMsg {
6565
shouldKeepOldResultsWhileRendering: boolean;
6666
};
6767

68-
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg;
68+
/** Advance to the next or previous path no in the path viewer */
69+
export interface NavigatePathMsg {
70+
t: 'navigatePath',
71+
72+
/** 1 for next, -1 for previous */
73+
direction: number;
74+
}
75+
76+
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
6977

7078
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;
7179

extensions/ql-vscode/src/interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ export class InterfaceManager extends DisposableObject {
9999
super();
100100
this.push(this._diagnosticCollection);
101101
this.push(vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)));
102+
this.push(vscode.commands.registerCommand('codeQLQueryResults.nextPathStep', this.navigatePathStep.bind(this, 1)));
103+
this.push(vscode.commands.registerCommand('codeQLQueryResults.previousPathStep', this.navigatePathStep.bind(this, -1)));
104+
}
105+
106+
navigatePathStep(direction: number) {
107+
this.postMessage({ t: "navigatePath", direction });
102108
}
103109

104110
// Returns the webview panel, creating it if it doesn't already

extensions/ql-vscode/src/view/alert-table.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import * as Sarif from 'sarif';
44
import * as Keys from '../result-keys';
55
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
66
import * as octicons from './octicons';
7-
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe } from './result-table-utils';
8-
import { PathTableResultSet } from './results';
7+
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation } from './result-table-utils';
8+
import { PathTableResultSet, onNavigation, NavigationEvent } from './results';
99

1010
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
1111
export interface PathTableState {
@@ -75,6 +75,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
7575
constructor(props: PathTableProps) {
7676
super(props);
7777
this.state = { expanded: {}, selectedPathNode: undefined };
78+
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
7879
}
7980

8081
/**
@@ -290,6 +291,37 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
290291
<tbody>{rows}</tbody>
291292
</table>;
292293
}
294+
295+
private handleNavigationEvent(event: NavigationEvent) {
296+
this.setState(prevState => {
297+
let { selectedPathNode } = prevState;
298+
if (selectedPathNode === undefined) return prevState;
299+
300+
let path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
301+
if (path === undefined) return prevState;
302+
303+
let nextIndex = selectedPathNode.pathNodeIndex + event.direction;
304+
if (nextIndex < 0 || nextIndex >= path.locations.length) return prevState;
305+
306+
let sarifLoc = path.locations[nextIndex].location;
307+
if (sarifLoc === undefined) return prevState;
308+
309+
let loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
310+
if (loc.t === 'NoLocation') return prevState;
311+
312+
jumpToLocation(loc, this.props.databaseUri);
313+
let newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
314+
return { ...prevState, selectedPathNode: newSelection };
315+
});
316+
}
317+
318+
componentDidMount() {
319+
onNavigation.addListener(this.handleNavigationEvent);
320+
}
321+
322+
componentWillUnmount() {
323+
onNavigation.removeListener(this.handleNavigationEvent);
324+
}
293325
}
294326

295327
function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type EventHandler<T> = (event: T) => void;
2+
3+
/**
4+
* A set of listeners for events of type `T`.
5+
*/
6+
export class EventHandlers<T> {
7+
private handlers: EventHandler<T>[] = [];
8+
9+
public addListener(handler: EventHandler<T>) {
10+
this.handlers.push(handler);
11+
}
12+
13+
public removeListener(handler: EventHandler<T>) {
14+
let index = this.handlers.indexOf(handler);
15+
if (index !== -1) {
16+
this.handlers.splice(index, 1);
17+
}
18+
}
19+
20+
public fire(event: T) {
21+
for (let handler of this.handlers) {
22+
handler(event);
23+
}
24+
}
25+
}

extensions/ql-vscode/src/view/result-table-utils.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,21 @@ export function jumpToLocationHandler(
2424
callback?: () => void
2525
): (e: React.MouseEvent) => void {
2626
return (e) => {
27-
vscode.postMessage({
28-
t: 'viewSourceFile',
29-
loc,
30-
databaseUri
31-
});
27+
jumpToLocation(loc, databaseUri);
3228
e.preventDefault();
3329
e.stopPropagation();
3430
if (callback) callback();
3531
};
3632
}
3733

34+
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string) {
35+
vscode.postMessage({
36+
t: 'viewSourceFile',
37+
loc,
38+
databaseUri
39+
});
40+
}
41+
3842
/**
3943
* Render a location as a link which when clicked displays the original location.
4044
*/

extensions/ql-vscode/src/view/results.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import * as Rdom from 'react-dom';
33
import * as bqrs from 'semmle-bqrs';
44
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
55
import { assertNever } from '../helpers-pure';
6-
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState } from '../interface-types';
6+
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState, NavigatePathMsg } from '../interface-types';
77
import { ResultTables } from './result-tables';
8+
import { EventHandlers as EventHandlerList } from './event-handler-list';
89

910
/**
1011
* results.tsx
@@ -156,6 +157,13 @@ interface ResultsViewState {
156157
isExpectingResultsUpdate: boolean;
157158
}
158159

160+
export type NavigationEvent = NavigatePathMsg;
161+
162+
/**
163+
* Event handlers to be notified of navigation events coming from outside the webview.
164+
*/
165+
export const onNavigation = new EventHandlerList<NavigationEvent>();
166+
159167
/**
160168
* A minimal state container for displaying results.
161169
*/
@@ -192,6 +200,9 @@ class App extends React.Component<{}, ResultsViewState> {
192200
isExpectingResultsUpdate: true
193201
});
194202
break;
203+
case 'navigatePath':
204+
onNavigation.fire(msg);
205+
break;
195206
default:
196207
assertNever(msg);
197208
}

0 commit comments

Comments
 (0)