Add commands for path navigation

This commit is contained in:
Asger F
2019-11-21 15:04:29 +00:00
parent c0ffb7eaf1
commit 7a2edfbbf9
7 changed files with 103 additions and 9 deletions

View File

@@ -170,6 +170,14 @@
{
"command": "codeQLQueryHistory.itemClicked",
"title": "Query History Item"
},
{
"command": "codeQLQueryResults.nextPathStep",
"title": "CodeQL: Show Next Step on Path"
},
{
"command": "codeQLQueryResults.previousPathStep",
"title": "CodeQL: Show Previous Step on Path"
}
],
"menus": {

View File

@@ -65,7 +65,15 @@ export interface SetStateMsg {
shouldKeepOldResultsWhileRendering: boolean;
};
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg;
/** Advance to the next or previous path no in the path viewer */
export interface NavigatePathMsg {
t: 'navigatePath',
/** 1 for next, -1 for previous */
direction: number;
}
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;

View File

@@ -99,6 +99,12 @@ export class InterfaceManager extends DisposableObject {
super();
this.push(this._diagnosticCollection);
this.push(vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.nextPathStep', this.navigatePathStep.bind(this, 1)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.previousPathStep', this.navigatePathStep.bind(this, -1)));
}
navigatePathStep(direction: number) {
this.postMessage({ t: "navigatePath", direction });
}
// Returns the webview panel, creating it if it doesn't already

View File

@@ -4,8 +4,8 @@ import * as Sarif from 'sarif';
import * as Keys from '../result-keys';
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe } from './result-table-utils';
import { PathTableResultSet } from './results';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent } from './results';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
@@ -75,6 +75,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
constructor(props: PathTableProps) {
super(props);
this.state = { expanded: {}, selectedPathNode: undefined };
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
}
/**
@@ -290,6 +291,37 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
<tbody>{rows}</tbody>
</table>;
}
private handleNavigationEvent(event: NavigationEvent) {
this.setState(prevState => {
let { selectedPathNode } = prevState;
if (selectedPathNode === undefined) return prevState;
let path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
if (path === undefined) return prevState;
let nextIndex = selectedPathNode.pathNodeIndex + event.direction;
if (nextIndex < 0 || nextIndex >= path.locations.length) return prevState;
let sarifLoc = path.locations[nextIndex].location;
if (sarifLoc === undefined) return prevState;
let loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
if (loc.t === 'NoLocation') return prevState;
jumpToLocation(loc, this.props.databaseUri);
let newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
return { ...prevState, selectedPathNode: newSelection };
});
}
componentDidMount() {
onNavigation.addListener(this.handleNavigationEvent);
}
componentWillUnmount() {
onNavigation.removeListener(this.handleNavigationEvent);
}
}
function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {

View File

@@ -0,0 +1,25 @@
export type EventHandler<T> = (event: T) => void;
/**
* A set of listeners for events of type `T`.
*/
export class EventHandlers<T> {
private handlers: EventHandler<T>[] = [];
public addListener(handler: EventHandler<T>) {
this.handlers.push(handler);
}
public removeListener(handler: EventHandler<T>) {
let index = this.handlers.indexOf(handler);
if (index !== -1) {
this.handlers.splice(index, 1);
}
}
public fire(event: T) {
for (let handler of this.handlers) {
handler(event);
}
}
}

View File

@@ -24,17 +24,21 @@ export function jumpToLocationHandler(
callback?: () => void
): (e: React.MouseEvent) => void {
return (e) => {
vscode.postMessage({
t: 'viewSourceFile',
loc,
databaseUri
});
jumpToLocation(loc, databaseUri);
e.preventDefault();
e.stopPropagation();
if (callback) callback();
};
}
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string) {
vscode.postMessage({
t: 'viewSourceFile',
loc,
databaseUri
});
}
/**
* Render a location as a link which when clicked displays the original location.
*/

View File

@@ -3,8 +3,9 @@ import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs';
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState } from '../interface-types';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState, NavigatePathMsg } from '../interface-types';
import { ResultTables } from './result-tables';
import { EventHandlers as EventHandlerList } from './event-handler-list';
/**
* results.tsx
@@ -156,6 +157,13 @@ interface ResultsViewState {
isExpectingResultsUpdate: boolean;
}
export type NavigationEvent = NavigatePathMsg;
/**
* Event handlers to be notified of navigation events coming from outside the webview.
*/
export const onNavigation = new EventHandlerList<NavigationEvent>();
/**
* A minimal state container for displaying results.
*/
@@ -192,6 +200,9 @@ class App extends React.Component<{}, ResultsViewState> {
isExpectingResultsUpdate: true
});
break;
case 'navigatePath':
onNavigation.fire(msg);
break;
default:
assertNever(msg);
}