Skip to content

Commit b21ae3d

Browse files
committed
Add ability to filter results to the current file or selection
A new checkbox appears above the result viewer table. When checked, only the results from the currently-viewed file are shown. Additionally, if the selection range is non-empty, only results whose first line overlaps within the selection range are shown.
1 parent 0f7a747 commit b21ae3d

File tree

11 files changed

+937
-105
lines changed

11 files changed

+937
-105
lines changed

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

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,60 @@ interface UntoggleShowProblemsMsg {
220220
t: "untoggleShowProblems";
221221
}
222222

223+
export const enum SourceArchiveRelationship {
224+
/** The file is in the source archive of the database the query was run on. */
225+
CorrectArchive = "correct-archive",
226+
/** The file is in a source archive, but for a different database. */
227+
WrongArchive = "wrong-archive",
228+
/** The file is not in any source archive. */
229+
NotInArchive = "not-in-archive",
230+
}
231+
232+
/**
233+
* Information about the current editor selection, sent to the results view
234+
* so it can filter results to only those overlapping the selection.
235+
*/
236+
export interface EditorSelection {
237+
/** The file URI in result-compatible format, or undefined if no editor is active. */
238+
fileUri: string | undefined;
239+
startLine: number;
240+
endLine: number;
241+
startColumn: number;
242+
endColumn: number;
243+
/** True if the selection is empty (just a cursor), in which case we match the whole file. */
244+
isEmpty: boolean;
245+
/** Describes the relationship between the current file and the query's database source archive. */
246+
sourceArchiveRelationship: SourceArchiveRelationship;
247+
}
248+
249+
interface SetEditorSelectionMsg {
250+
t: "setEditorSelection";
251+
selection: EditorSelection | undefined;
252+
wasFromUserInteraction?: boolean;
253+
}
254+
255+
/**
256+
* Results pre-filtered by file URI, sent from the extension when the
257+
* selection filter is active and the editor's file changes.
258+
* This bypasses pagination so the webview can apply line-range filtering
259+
* on the complete set of results for the file.
260+
*/
261+
export interface FileFilteredResults {
262+
/** The file URI these results were filtered for. */
263+
fileUri: string;
264+
/** The result set table these results were filtered for. */
265+
selectedTable: string;
266+
/** Raw result rows from the current result set that reference this file. */
267+
rawRows?: Row[];
268+
/** SARIF results that reference this file. */
269+
sarifResults?: Result[];
270+
}
271+
272+
interface SetFileFilteredResultsMsg {
273+
t: "setFileFilteredResults";
274+
results: FileFilteredResults | undefined;
275+
}
276+
223277
/**
224278
* A message sent into the results view.
225279
*/
@@ -229,7 +283,9 @@ export type IntoResultsViewMsg =
229283
| SetUserSettingsMsg
230284
| ShowInterpretedPageMsg
231285
| NavigateMsg
232-
| UntoggleShowProblemsMsg;
286+
| UntoggleShowProblemsMsg
287+
| SetEditorSelectionMsg
288+
| SetFileFilteredResultsMsg;
233289

234290
/**
235291
* A message sent from the results view.
@@ -241,7 +297,31 @@ export type FromResultsViewMsg =
241297
| ChangeRawResultsSortMsg
242298
| ChangeInterpretedResultsSortMsg
243299
| ChangePage
244-
| OpenFileMsg;
300+
| OpenFileMsg
301+
| RequestSelectionUpdateMsg
302+
| RequestFileFilteredResultsMsg;
303+
304+
/**
305+
* Message from the results view to request the extension to re-send
306+
* the current editor selection and file-filtered results. Sent when
307+
* the selection filter is toggled on to ensure the filter state
308+
* reflects the actual current editor, not a stale cached value.
309+
*/
310+
interface RequestSelectionUpdateMsg {
311+
t: "requestSelectionUpdate";
312+
}
313+
314+
/**
315+
* Message from the results view to request pre-filtered results for
316+
* a specific (file, table) pair. The extension loads all results from
317+
* the given table that reference the given file and sends them back
318+
* via setFileFilteredResults.
319+
*/
320+
interface RequestFileFilteredResultsMsg {
321+
t: "requestFileFilteredResults";
322+
fileUri: string;
323+
selectedTable: string;
324+
}
245325

246326
/**
247327
* Message from the results view to open a source

extensions/ql-vscode/src/databases/local-databases/locations.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
window as Window,
1010
workspace,
1111
} from "vscode";
12+
import type { TextEditor } from "vscode";
1213
import { assertNever, getErrorMessage } from "../../common/helpers-pure";
1314
import type { Logger } from "../../common/logging";
1415
import type { DatabaseItem } from "./database-item";
@@ -76,6 +77,12 @@ function resolveWholeFileLocation(
7677
);
7778
}
7879

80+
/** Returned from `showLocation` and related functions, to indicate which editor and location was ultimately highlighted. */
81+
export interface RevealedLocation {
82+
editor: TextEditor;
83+
location: Location;
84+
}
85+
7986
/**
8087
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
8188
* can be resolved, returns `undefined`.
@@ -105,9 +112,9 @@ export async function showResolvableLocation(
105112
loc: UrlValueResolvable,
106113
databaseItem: DatabaseItem | undefined,
107114
logger: Logger,
108-
): Promise<void> {
115+
): Promise<RevealedLocation | null> {
109116
try {
110-
await showLocation(tryResolveLocation(loc, databaseItem));
117+
return showLocation(tryResolveLocation(loc, databaseItem));
111118
} catch (e) {
112119
if (e instanceof Error && e.message.match(/File not found/)) {
113120
void Window.showErrorMessage(
@@ -116,12 +123,15 @@ export async function showResolvableLocation(
116123
} else {
117124
void logger.log(`Unable to jump to location: ${getErrorMessage(e)}`);
118125
}
126+
return null;
119127
}
120128
}
121129

122-
export async function showLocation(location?: Location) {
130+
export async function showLocation(
131+
location?: Location,
132+
): Promise<RevealedLocation | null> {
123133
if (!location) {
124-
return;
134+
return null;
125135
}
126136

127137
const doc = await workspace.openTextDocument(location.uri);
@@ -156,17 +166,19 @@ export async function showLocation(location?: Location) {
156166
editor.revealRange(range, TextEditorRevealType.InCenter);
157167
editor.setDecorations(shownLocationDecoration, [range]);
158168
editor.setDecorations(shownLocationLineDecoration, [range]);
169+
170+
return { editor, location };
159171
}
160172

161173
export async function jumpToLocation(
162174
databaseUri: string | undefined,
163175
loc: UrlValueResolvable,
164176
databaseManager: DatabaseManager,
165177
logger: Logger,
166-
) {
178+
): Promise<RevealedLocation | null> {
167179
const databaseItem =
168180
databaseUri !== undefined
169181
? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
170182
: undefined;
171-
await showResolvableLocation(loc, databaseItem, logger);
183+
return showResolvableLocation(loc, databaseItem, logger);
172184
}

0 commit comments

Comments
 (0)