Skip to content

Commit 09b30fe

Browse files
authored
Merge pull request #1568 from asgerf/asgerf/navigate-alerts
Add commands for navigation of alerts
2 parents a3fafc8 + b480f8f commit 09b30fe

File tree

12 files changed

+391
-125
lines changed

12 files changed

+391
-125
lines changed

extensions/ql-vscode/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [UNRELEASED]
44

5+
- Add commands for navigating up, down, left, or right in the result viewer. Previously there were only commands for moving up and down the currently-selected path. We suggest binding keyboard shortcuts to these commands, for navigating the result viewer using the keyboard. [#1568](https://github.com/github/vscode-codeql/pull/1568)
6+
57
## 1.7.2 - 14 October 2022
68

79
- Fix a bug where results created in older versions were thought to be unsuccessful. [#1605](https://github.com/github/vscode-codeql/pull/1605)

extensions/ql-vscode/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ When the results are ready, they're displayed in the CodeQL Query Results view.
9999

100100
If there are any problems running a query, a notification is displayed in the bottom right corner of the application. In addition to the error message, the notification includes details of how to fix the problem.
101101

102+
### Keyboad navigation
103+
104+
If you wish to navigate the query results from your keyboard, you can bind shortcuts to the **CodeQL: Navigate Up/Down/Left/Right in Result Viewer** commands.
105+
102106
## What next?
103107

104108
For more information about the CodeQL extension, [see the documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/). Otherwise, you could:

extensions/ql-vscode/package.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -602,12 +602,20 @@
602602
"title": "Copy Repository List"
603603
},
604604
{
605-
"command": "codeQLQueryResults.nextPathStep",
606-
"title": "CodeQL: Show Next Step on Path"
605+
"command": "codeQLQueryResults.down",
606+
"title": "CodeQL: Navigate Down in Local Result Viewer"
607607
},
608608
{
609-
"command": "codeQLQueryResults.previousPathStep",
610-
"title": "CodeQL: Show Previous Step on Path"
609+
"command": "codeQLQueryResults.up",
610+
"title": "CodeQL: Navigate Up in Local Result Viewer"
611+
},
612+
{
613+
"command": "codeQLQueryResults.right",
614+
"title": "CodeQL: Navigate Right in Local Result Viewer"
615+
},
616+
{
617+
"command": "codeQLQueryResults.left",
618+
"title": "CodeQL: Navigate Left in Local Result Viewer"
611619
},
612620
{
613621
"command": "codeQL.restartQueryServer",

extensions/ql-vscode/src/interface.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
ALERTS_TABLE_NAME,
2828
GRAPH_TABLE_NAME,
2929
RawResultsSortState,
30+
NavigationDirection,
3031
} from './pure/interface-types';
3132
import { Logger } from './logging';
3233
import { commandRunner } from './commandRunner';
@@ -141,19 +142,24 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
141142
this.handleSelectionChange.bind(this)
142143
)
143144
);
144-
void logger.log('Registering path-step navigation commands.');
145-
this.push(
146-
commandRunner(
147-
'codeQLQueryResults.nextPathStep',
148-
this.navigatePathStep.bind(this, 1)
149-
)
150-
);
151-
this.push(
152-
commandRunner(
153-
'codeQLQueryResults.previousPathStep',
154-
this.navigatePathStep.bind(this, -1)
155-
)
156-
);
145+
const navigationCommands = {
146+
'codeQLQueryResults.up': NavigationDirection.up,
147+
'codeQLQueryResults.down': NavigationDirection.down,
148+
'codeQLQueryResults.left': NavigationDirection.left,
149+
'codeQLQueryResults.right': NavigationDirection.right,
150+
// For backwards compatibility with keybindings set using an earlier version of the extension.
151+
'codeQLQueryResults.nextPathStep': NavigationDirection.down,
152+
'codeQLQueryResults.previousPathStep': NavigationDirection.up,
153+
};
154+
void logger.log('Registering result view navigation commands.');
155+
for (const [commandId, direction] of Object.entries(navigationCommands)) {
156+
this.push(
157+
commandRunner(
158+
commandId,
159+
this.navigateResultView.bind(this, direction)
160+
)
161+
);
162+
}
157163

158164
this.push(
159165
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
@@ -169,8 +175,13 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
169175
);
170176
}
171177

172-
async navigatePathStep(direction: number): Promise<void> {
173-
await this.postMessage({ t: 'navigatePath', direction });
178+
async navigateResultView(direction: NavigationDirection): Promise<void> {
179+
if (!this.panel?.visible) {
180+
return;
181+
}
182+
// Reveal the panel now as the subsequent call to 'Window.showTextEditor' in 'showLocation' may destroy the webview otherwise.
183+
this.panel.reveal();
184+
await this.postMessage({ t: 'navigate', direction });
174185
}
175186

176187
protected getPanelConfig(): WebviewPanelConfig {

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,17 @@ export interface ShowInterpretedPageMsg {
145145
queryPath: string;
146146
}
147147

148-
/** Advance to the next or previous path no in the path viewer */
149-
export interface NavigatePathMsg {
150-
t: 'navigatePath';
148+
export const enum NavigationDirection {
149+
up = 'up',
150+
down = 'down',
151+
left = 'left',
152+
right = 'right',
153+
}
151154

152-
/** 1 for next, -1 for previous */
153-
direction: number;
155+
/** Move up, down, left, or right in the result viewer. */
156+
export interface NavigateMsg {
157+
t: 'navigate';
158+
direction: NavigationDirection;
154159
}
155160

156161
/**
@@ -168,7 +173,7 @@ export type IntoResultsViewMsg =
168173
| ResultsUpdatingMsg
169174
| SetStateMsg
170175
| ShowInterpretedPageMsg
171-
| NavigatePathMsg
176+
| NavigateMsg
172177
| UntoggleShowProblemsMsg;
173178

174179
/**

extensions/ql-vscode/src/pure/result-keys.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,52 @@
11
import * as sarif from 'sarif';
22

3+
/**
4+
* Identifies a result, a path, or one of the nodes on a path.
5+
*/
6+
interface ResultKeyBase {
7+
resultIndex: number;
8+
pathIndex?: number;
9+
pathNodeIndex?: number;
10+
}
11+
312
/**
413
* Identifies one of the results in a result set by its index in the result list.
514
*/
6-
export interface Result {
15+
export interface Result extends ResultKeyBase {
716
resultIndex: number;
17+
pathIndex?: undefined;
18+
pathNodeIndex?: undefined;
819
}
920

1021
/**
1122
* Identifies one of the paths associated with a result.
1223
*/
13-
export interface Path extends Result {
24+
export interface Path extends ResultKeyBase {
1425
pathIndex: number;
26+
pathNodeIndex?: undefined;
1527
}
1628

1729
/**
1830
* Identifies one of the nodes in a path.
1931
*/
20-
export interface PathNode extends Path {
32+
export interface PathNode extends ResultKeyBase {
33+
pathIndex: number;
2134
pathNodeIndex: number;
2235
}
2336

24-
/** Alias for `undefined` but more readable in some cases */
25-
export const none: PathNode | undefined = undefined;
37+
export type ResultKey = Result | Path | PathNode;
2638

2739
/**
2840
* Looks up a specific result in a result set.
2941
*/
30-
export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefined {
31-
if (sarif.runs.length === 0) return undefined;
32-
if (sarif.runs[0].results === undefined) return undefined;
33-
const results = sarif.runs[0].results;
34-
return results[key.resultIndex];
42+
export function getResult(sarif: sarif.Log, key: Result | Path | PathNode): sarif.Result | undefined {
43+
return sarif.runs[0]?.results?.[key.resultIndex];
3544
}
3645

3746
/**
3847
* Looks up a specific path in a result set.
3948
*/
40-
export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefined {
49+
export function getPath(sarif: sarif.Log, key: Path | PathNode): sarif.ThreadFlow | undefined {
4150
const result = getResult(sarif, key);
4251
if (result === undefined) return undefined;
4352
let index = -1;
@@ -58,22 +67,13 @@ export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefin
5867
export function getPathNode(sarif: sarif.Log, key: PathNode): sarif.Location | undefined {
5968
const path = getPath(sarif, key);
6069
if (path === undefined) return undefined;
61-
return path.locations[key.pathNodeIndex];
62-
}
63-
64-
/**
65-
* Returns true if the two keys are both `undefined` or contain the same set of indices.
66-
*/
67-
export function equals(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
68-
if (key1 === key2) return true;
69-
if (key1 === undefined || key2 === undefined) return false;
70-
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
70+
return path.locations[key.pathNodeIndex]?.location;
7171
}
7272

7373
/**
7474
* Returns true if the two keys contain the same set of indices and neither are `undefined`.
7575
*/
76-
export function equalsNotUndefined(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
76+
export function equalsNotUndefined(key1: Partial<PathNode> | undefined, key2: Partial<PathNode> | undefined): boolean {
7777
if (key1 === undefined || key2 === undefined) return false;
7878
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
7979
}
@@ -93,3 +93,11 @@ export function getAllPaths(result: sarif.Result): sarif.ThreadFlow[] {
9393
}
9494
return paths;
9595
}
96+
97+
/**
98+
* Creates a unique string representation of the given key, suitable for use
99+
* as the key in a map or set.
100+
*/
101+
export function keyToString(key: ResultKey) {
102+
return key.resultIndex + '-' + (key.pathIndex ?? '') + '-' + (key.pathNodeIndex ?? '');
103+
}
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
import * as React from 'react';
22
import { ResultRow } from '../../pure/bqrs-cli-types';
3-
import { zebraStripe } from './result-table-utils';
3+
import { selectedRowClassName, zebraStripe } from './result-table-utils';
44
import RawTableValue from './RawTableValue';
5+
import { ScrollIntoViewHelper } from './scroll-into-view-helper';
56

67
interface Props {
78
rowIndex: number;
89
row: ResultRow;
910
databaseUri: string;
1011
className?: string;
12+
selectedColumn?: number;
13+
onSelected?: (row: number, column: number) => void;
14+
scroller?: ScrollIntoViewHelper;
1115
}
1216

1317
export default function RawTableRow(props: Props) {
1418
return (
1519
<tr key={props.rowIndex} {...zebraStripe(props.rowIndex, props.className || '')}>
1620
<td key={-1}>{props.rowIndex + 1}</td>
1721

18-
{props.row.map((value, columnIndex) => (
19-
<td key={columnIndex}>
20-
<RawTableValue
21-
value={value}
22-
databaseUri={props.databaseUri}
23-
/>
24-
</td>
25-
))}
22+
{props.row.map((value, columnIndex) => {
23+
const isSelected = props.selectedColumn === columnIndex;
24+
return (
25+
<td ref={props.scroller?.ref(isSelected)} key={columnIndex} {...isSelected ? { className: selectedRowClassName } : {}}>
26+
<RawTableValue
27+
value={value}
28+
databaseUri={props.databaseUri}
29+
onSelected={() => props.onSelected?.(props.rowIndex, columnIndex)}
30+
/>
31+
</td>
32+
);
33+
})}
2634
</tr>
2735
);
2836
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CellValue } from '../../pure/bqrs-cli-types';
66
interface Props {
77
value: CellValue;
88
databaseUri: string;
9+
onSelected?: () => void;
910
}
1011

1112
export default function RawTableValue(props: Props): JSX.Element {
@@ -18,5 +19,5 @@ export default function RawTableValue(props: Props): JSX.Element {
1819
return <span>{renderLocation(undefined, rawValue.toString())}</span>;
1920
}
2021

21-
return renderLocation(rawValue.url, rawValue.label, props.databaseUri);
22+
return renderLocation(rawValue.url, rawValue.label, props.databaseUri, undefined, props.onSelected);
2223
}

0 commit comments

Comments
 (0)