Add commands for path navigation
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
25
extensions/ql-vscode/src/view/event-handler-list.ts
Normal file
25
extensions/ql-vscode/src/view/event-handler-list.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user