diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md
index ee40e8cf406..8801a7e2cf6 100644
--- a/extensions/ql-vscode/CHANGELOG.md
+++ b/extensions/ql-vscode/CHANGELOG.md
@@ -3,6 +3,7 @@
## [UNRELEASED]
- Remove support for CodeQL CLI versions older than 2.22.4. [#4344](https://github.com/github/vscode-codeql/pull/4344)
+- Added support for selection-based result filtering via a checkbox in the result viewer. When enabled, only results from the currently-viewed file are shown. Additionally, if the editor selection is non-empty, only results within the selection range are shown. [#4362](https://github.com/github/vscode-codeql/pull/4362)
## 1.17.7 - 5 December 2025
diff --git a/extensions/ql-vscode/src/common/interface-types.ts b/extensions/ql-vscode/src/common/interface-types.ts
index 7d18d4db93c..3939b820ebb 100644
--- a/extensions/ql-vscode/src/common/interface-types.ts
+++ b/extensions/ql-vscode/src/common/interface-types.ts
@@ -220,6 +220,60 @@ interface UntoggleShowProblemsMsg {
t: "untoggleShowProblems";
}
+export const enum SourceArchiveRelationship {
+ /** The file is in the source archive of the database the query was run on. */
+ CorrectArchive = "correct-archive",
+ /** The file is in a source archive, but for a different database. */
+ WrongArchive = "wrong-archive",
+ /** The file is not in any source archive. */
+ NotInArchive = "not-in-archive",
+}
+
+/**
+ * Information about the current editor selection, sent to the results view
+ * so it can filter results to only those overlapping the selection.
+ */
+export interface EditorSelection {
+ /** The file URI in result-compatible format. */
+ fileUri: string;
+ startLine: number;
+ endLine: number;
+ startColumn: number;
+ endColumn: number;
+ /** True if the selection is empty (just a cursor), in which case we match the whole file. */
+ isEmpty: boolean;
+ /** Describes the relationship between the current file and the query's database source archive. */
+ sourceArchiveRelationship: SourceArchiveRelationship;
+}
+
+interface SetEditorSelectionMsg {
+ t: "setEditorSelection";
+ selection: EditorSelection | undefined;
+ wasFromUserInteraction?: boolean;
+}
+
+/**
+ * Results pre-filtered by file URI, sent from the extension when the
+ * selection filter is active and the editor's file changes.
+ * This bypasses pagination so the webview can apply line-range filtering
+ * on the complete set of results for the file.
+ */
+export interface FileFilteredResults {
+ /** The file URI these results were filtered for. */
+ fileUri: string;
+ /** The result set table these results were filtered for. */
+ selectedTable: string;
+ /** Raw result rows from the current result set that reference this file. */
+ rawRows?: Row[];
+ /** SARIF results that reference this file. */
+ sarifResults?: Result[];
+}
+
+interface SetFileFilteredResultsMsg {
+ t: "setFileFilteredResults";
+ results: FileFilteredResults;
+}
+
/**
* A message sent into the results view.
*/
@@ -229,7 +283,9 @@ export type IntoResultsViewMsg =
| SetUserSettingsMsg
| ShowInterpretedPageMsg
| NavigateMsg
- | UntoggleShowProblemsMsg;
+ | UntoggleShowProblemsMsg
+ | SetEditorSelectionMsg
+ | SetFileFilteredResultsMsg;
/**
* A message sent from the results view.
@@ -241,7 +297,20 @@ export type FromResultsViewMsg =
| ChangeRawResultsSortMsg
| ChangeInterpretedResultsSortMsg
| ChangePage
- | OpenFileMsg;
+ | OpenFileMsg
+ | RequestFileFilteredResultsMsg;
+
+/**
+ * Message from the results view to request pre-filtered results for
+ * a specific (file, table) pair. The extension loads all results from
+ * the given table that reference the given file and sends them back
+ * via setFileFilteredResults.
+ */
+interface RequestFileFilteredResultsMsg {
+ t: "requestFileFilteredResults";
+ fileUri: string;
+ selectedTable: string;
+}
/**
* Message from the results view to open a source
diff --git a/extensions/ql-vscode/src/common/sarif-utils.ts b/extensions/ql-vscode/src/common/sarif-utils.ts
index 61e3d3a3807..95b60cbb0cb 100644
--- a/extensions/ql-vscode/src/common/sarif-utils.ts
+++ b/extensions/ql-vscode/src/common/sarif-utils.ts
@@ -1,4 +1,4 @@
-import type { Location, Region } from "sarif";
+import type { Location, Region, Result } from "sarif";
import type { HighlightedRegion } from "../variant-analysis/shared/analysis-result";
import type { UrlValueResolvable } from "./raw-result-types";
import { isEmptyPath } from "./bqrs-utils";
@@ -252,3 +252,52 @@ export function parseHighlightedLine(
return { plainSection1, highlightedSection, plainSection2 };
}
+
+/**
+ * Normalizes a file URI to a plain path for comparison purposes.
+ * Strips the `file:` scheme prefix and decodes URI components.
+ */
+export function normalizeFileUri(uri: string): string {
+ try {
+ const path = uri.replace(/^file:\/*/, "/");
+ return decodeURIComponent(path);
+ } catch {
+ return uri.replace(/^file:\/*/, "/");
+ }
+}
+
+interface ParsedResultLocation {
+ uri: string;
+ startLine?: number;
+ endLine?: number;
+}
+
+/**
+ * Extracts all locations from a SARIF result, including relatedLocations.
+ */
+export function getLocationsFromSarifResult(
+ result: Result,
+ sourceLocationPrefix: string,
+): ParsedResultLocation[] {
+ const sarifLocations: Location[] = [
+ ...(result.locations ?? []),
+ ...(result.relatedLocations ?? []),
+ ];
+ const parsed: ParsedResultLocation[] = [];
+ for (const loc of sarifLocations) {
+ const p = parseSarifLocation(loc, sourceLocationPrefix);
+ if ("hint" in p) {
+ continue;
+ }
+ if (p.type === "wholeFileLocation") {
+ parsed.push({ uri: p.uri });
+ } else if (p.type === "lineColumnLocation") {
+ parsed.push({
+ uri: p.uri,
+ startLine: p.startLine,
+ endLine: p.endLine,
+ });
+ }
+ }
+ return parsed;
+}
diff --git a/extensions/ql-vscode/src/common/vscode/webview-html.ts b/extensions/ql-vscode/src/common/vscode/webview-html.ts
index e1eac40024c..68b6751d012 100644
--- a/extensions/ql-vscode/src/common/vscode/webview-html.ts
+++ b/extensions/ql-vscode/src/common/vscode/webview-html.ts
@@ -80,7 +80,7 @@ export function getHtmlForWebview(
diff --git a/extensions/ql-vscode/src/databases/local-databases/locations.ts b/extensions/ql-vscode/src/databases/local-databases/locations.ts
index 55961c2a143..ca871175f1f 100644
--- a/extensions/ql-vscode/src/databases/local-databases/locations.ts
+++ b/extensions/ql-vscode/src/databases/local-databases/locations.ts
@@ -9,6 +9,7 @@ import {
window as Window,
workspace,
} from "vscode";
+import type { TextEditor } from "vscode";
import { assertNever, getErrorMessage } from "../../common/helpers-pure";
import type { Logger } from "../../common/logging";
import type { DatabaseItem } from "./database-item";
@@ -76,6 +77,12 @@ function resolveWholeFileLocation(
);
}
+/** Returned from `showLocation` and related functions, to indicate which editor and location was ultimately highlighted. */
+interface RevealedLocation {
+ editor: TextEditor;
+ location: Location;
+}
+
/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.
@@ -105,9 +112,9 @@ export async function showResolvableLocation(
loc: UrlValueResolvable,
databaseItem: DatabaseItem | undefined,
logger: Logger,
-): Promise {
+): Promise {
try {
- await showLocation(tryResolveLocation(loc, databaseItem));
+ return showLocation(tryResolveLocation(loc, databaseItem));
} catch (e) {
if (e instanceof Error && e.message.match(/File not found/)) {
void Window.showErrorMessage(
@@ -116,12 +123,15 @@ export async function showResolvableLocation(
} else {
void logger.log(`Unable to jump to location: ${getErrorMessage(e)}`);
}
+ return null;
}
}
-export async function showLocation(location?: Location) {
+export async function showLocation(
+ location?: Location,
+): Promise {
if (!location) {
- return;
+ return null;
}
const doc = await workspace.openTextDocument(location.uri);
@@ -156,6 +166,8 @@ export async function showLocation(location?: Location) {
editor.revealRange(range, TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
editor.setDecorations(shownLocationLineDecoration, [range]);
+
+ return { editor, location };
}
export async function jumpToLocation(
@@ -163,10 +175,10 @@ export async function jumpToLocation(
loc: UrlValueResolvable,
databaseManager: DatabaseManager,
logger: Logger,
-) {
+): Promise {
const databaseItem =
databaseUri !== undefined
? databaseManager.findDatabaseItem(Uri.parse(databaseUri))
: undefined;
- await showResolvableLocation(loc, databaseItem, logger);
+ return showResolvableLocation(loc, databaseItem, logger);
}
diff --git a/extensions/ql-vscode/src/local-queries/results-view.ts b/extensions/ql-vscode/src/local-queries/results-view.ts
index 55480c344f8..a5ac1b12848 100644
--- a/extensions/ql-vscode/src/local-queries/results-view.ts
+++ b/extensions/ql-vscode/src/local-queries/results-view.ts
@@ -1,5 +1,9 @@
import type { Location, Result, Run } from "sarif";
-import type { WebviewPanel, TextEditorSelectionChangeEvent } from "vscode";
+import type {
+ WebviewPanel,
+ TextEditorSelectionChangeEvent,
+ Range,
+} from "vscode";
import {
Diagnostic,
DiagnosticRelatedInformation,
@@ -18,6 +22,10 @@ import type {
DatabaseManager,
} from "../databases/local-databases";
import { DatabaseEventKind } from "../databases/local-databases";
+import {
+ decodeSourceArchiveUri,
+ zipArchiveScheme,
+} from "../common/vscode/archive-filesystem-provider";
import {
asError,
assertNever,
@@ -35,6 +43,7 @@ import type {
InterpretedResultsSortState,
RawResultsSortState,
ParsedResultSets,
+ EditorSelection,
} from "../common/interface-types";
import {
SortDirection,
@@ -42,6 +51,8 @@ import {
GRAPH_TABLE_NAME,
NavigationDirection,
getDefaultResultSetName,
+ RAW_RESULTS_LIMIT,
+ SourceArchiveRelationship,
} from "../common/interface-types";
import { extLogger } from "../common/logging/vscode";
import type { Logger } from "../common/logging";
@@ -53,6 +64,8 @@ import type {
import { interpretResultsSarif, interpretGraphResults } from "../query-results";
import type { QueryEvaluationInfo } from "../run-queries-shared";
import {
+ getLocationsFromSarifResult,
+ normalizeFileUri,
parseSarifLocation,
parseSarifPlainTextMessage,
} from "../common/sarif-utils";
@@ -73,7 +86,7 @@ import { redactableError } from "../common/errors";
import type { ResultsViewCommands } from "../common/commands";
import type { App } from "../common/app";
import type { Disposable } from "../common/disposable-object";
-import type { RawResultSet } from "../common/raw-result-types";
+import type { RawResultSet, Row } from "../common/raw-result-types";
import type { BqrsResultSetSchema } from "../common/bqrs-cli-types";
import { CachedOperation } from "../language-support/contextual/cached-operation";
@@ -197,6 +210,12 @@ export class ResultsView extends AbstractWebview<
),
);
+ this.disposableEventListeners.push(
+ window.onDidChangeActiveTextEditor(() => {
+ this.sendEditorSelectionToWebview();
+ }),
+ );
+
this.disposableEventListeners.push(
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
if (kind === DatabaseEventKind.Remove) {
@@ -277,12 +296,23 @@ export class ResultsView extends AbstractWebview<
this.onWebViewLoaded();
break;
case "viewSourceFile": {
- await jumpToLocation(
+ const jumpTarget = await jumpToLocation(
msg.databaseUri,
msg.loc,
this.databaseManager,
this.logger,
);
+ if (jumpTarget != null) {
+ // For selection-filtering purposes, we want to notify the webview that a new file is being looked at.
+ await this.postMessage({
+ t: "setEditorSelection",
+ selection: this.rangeToEditorSelection(
+ jumpTarget.location.uri,
+ jumpTarget.location.range,
+ ),
+ wasFromUserInteraction: false,
+ });
+ }
break;
}
case "toggleDiagnostics": {
@@ -333,6 +363,9 @@ export class ResultsView extends AbstractWebview<
case "openFile":
await this.openFile(msg.filePath);
break;
+ case "requestFileFilteredResults":
+ void this.loadFileFilteredResults(msg.fileUri, msg.selectedTable);
+ break;
case "telemetry":
telemetryListener?.sendUIInteraction(msg.action);
break;
@@ -573,6 +606,9 @@ export class ResultsView extends AbstractWebview<
queryName: this.labelProvider.getLabel(fullQuery),
queryPath: fullQuery.initialInfo.queryPath,
});
+
+ // Send the current editor selection so the webview can apply filtering immediately
+ this.sendEditorSelectionToWebview();
}
/**
@@ -1021,7 +1057,10 @@ export class ResultsView extends AbstractWebview<
}
private handleSelectionChange(event: TextEditorSelectionChangeEvent): void {
- if (event.kind === TextEditorSelectionChangeKind.Command) {
+ const wasFromUserInteraction =
+ event.kind !== TextEditorSelectionChangeKind.Command;
+ this.sendEditorSelectionToWebview(wasFromUserInteraction);
+ if (!wasFromUserInteraction) {
return; // Ignore selection events we caused ourselves.
}
const editor = window.activeTextEditor;
@@ -1031,6 +1070,178 @@ export class ResultsView extends AbstractWebview<
}
}
+ /**
+ * Sends the current editor selection to the webview so it can filter results.
+ * Does not send when there is no active text editor (e.g. when the webview
+ * gains focus), so the webview retains the last known selection.
+ */
+ private sendEditorSelectionToWebview(wasFromUserInteraction = false): void {
+ if (!this.isShowingPanel) {
+ return;
+ }
+ const selection = this.computeEditorSelection();
+ if (selection === undefined) {
+ return;
+ }
+ void this.postMessage({
+ t: "setEditorSelection",
+ selection,
+ wasFromUserInteraction,
+ });
+ }
+
+ /**
+ * Computes the current editor selection in a format compatible with result locations.
+ */
+ private computeEditorSelection(): EditorSelection | undefined {
+ const editor = window.activeTextEditor;
+ if (!editor) {
+ return undefined;
+ }
+
+ return this.rangeToEditorSelection(editor.document.uri, editor.selection);
+ }
+
+ private rangeToEditorSelection(uri: Uri, range: Range) {
+ const fileUri = this.getEditorFileUri(uri);
+ if (fileUri == null) {
+ return undefined;
+ }
+ return {
+ fileUri,
+ // VS Code selections are 0-based; result locations are 1-based
+ startLine: range.start.line + 1,
+ endLine: range.end.line + 1,
+ startColumn: range.start.character + 1,
+ endColumn: range.end.character + 1,
+ isEmpty: range.isEmpty,
+ sourceArchiveRelationship: this.getSourceArchiveRelationship(uri),
+ };
+ }
+
+ /**
+ * Gets a file URI from the editor that can be compared with result location URIs.
+ *
+ * Result URIs (in BQRS and SARIF) use the original source file paths.
+ * For `file:` scheme editors, the URI already matches.
+ * For source archive editors, we extract the path within the archive,
+ * which corresponds to the original source file path.
+ */
+ private getEditorFileUri(editorUri: Uri): string | undefined {
+ if (editorUri.scheme === "file") {
+ return editorUri.toString();
+ }
+ if (editorUri.scheme === zipArchiveScheme) {
+ try {
+ const { pathWithinSourceArchive } = decodeSourceArchiveUri(editorUri);
+ return `file://${pathWithinSourceArchive}`;
+ } catch {
+ return undefined;
+ }
+ }
+ return undefined;
+ }
+
+ /**
+ * Determines the relationship between the editor file and the query's database source archive.
+ */
+ private getSourceArchiveRelationship(
+ editorUri: Uri,
+ ): SourceArchiveRelationship {
+ if (editorUri.scheme !== zipArchiveScheme) {
+ return SourceArchiveRelationship.NotInArchive;
+ }
+ const dbItem = this._displayedQuery
+ ? this.databaseManager.findDatabaseItem(
+ Uri.parse(this._displayedQuery.initialInfo.databaseInfo.databaseUri),
+ )
+ : undefined;
+ if (
+ dbItem?.sourceArchive &&
+ dbItem.belongsToSourceArchiveExplorerUri(editorUri)
+ ) {
+ return SourceArchiveRelationship.CorrectArchive;
+ }
+ return SourceArchiveRelationship.WrongArchive;
+ }
+
+ /**
+ * Loads all results from the given table that reference the given file URI,
+ * and sends them to the webview. Called on demand when the webview requests
+ * pre-filtered results for a specific (file, table) pair.
+ */
+ private async loadFileFilteredResults(
+ fileUri: string,
+ selectedTable: string,
+ ): Promise {
+ const query = this._displayedQuery;
+ if (!query) {
+ void this.postMessage({
+ t: "setFileFilteredResults",
+ results: { fileUri, selectedTable },
+ });
+ return;
+ }
+
+ const normalizedFilterUri = normalizeFileUri(fileUri);
+
+ let rawRows: Row[] | undefined;
+ let sarifResults: Result[] | undefined;
+
+ // Load and filter raw BQRS results
+ try {
+ const resultSetSchemas = await this.getResultSetSchemas(
+ query.completedQuery,
+ );
+ const schema = resultSetSchemas.find((s) => s.name === selectedTable);
+
+ if (schema && schema.rows > 0) {
+ const resultsPath = query.completedQuery.getResultsPath(selectedTable);
+ const chunk = await this.cliServer.bqrsDecode(
+ resultsPath,
+ schema.name,
+ {
+ offset: schema.pagination?.offsets[0],
+ pageSize: schema.rows,
+ },
+ );
+ const resultSet = bqrsToResultSet(schema, chunk);
+ rawRows = filterRowsByFileUri(resultSet.rows, normalizedFilterUri);
+ if (rawRows.length > RAW_RESULTS_LIMIT) {
+ rawRows = rawRows.slice(0, RAW_RESULTS_LIMIT);
+ }
+ }
+ } catch (e) {
+ void this.logger.log(
+ `Error loading file-filtered raw results: ${getErrorMessage(e)}`,
+ );
+ }
+
+ // Filter SARIF results (already in memory)
+ if (this._interpretation?.data.t === "SarifInterpretationData") {
+ const allResults = this._interpretation.data.runs[0]?.results ?? [];
+ sarifResults = allResults.filter((result) => {
+ const locations = getLocationsFromSarifResult(
+ result,
+ this._interpretation!.sourceLocationPrefix,
+ );
+ return locations.some(
+ (loc) => normalizeFileUri(loc.uri) === normalizedFilterUri,
+ );
+ });
+ }
+
+ void this.postMessage({
+ t: "setFileFilteredResults",
+ results: {
+ fileUri,
+ selectedTable,
+ rawRows,
+ sarifResults,
+ },
+ });
+ }
+
dispose() {
super.dispose();
@@ -1039,3 +1250,32 @@ export class ResultsView extends AbstractWebview<
this.disposableEventListeners = [];
}
}
+
+/**
+ * Filters raw result rows to those that have at least one location
+ * referencing the given file (compared by normalized URI).
+ */
+function filterRowsByFileUri(rows: Row[], normalizedFileUri: string): Row[] {
+ return rows.filter((row) => {
+ for (const cell of row) {
+ if (cell.type !== "entity") {
+ continue;
+ }
+ const url = cell.value.url;
+ if (!url) {
+ continue;
+ }
+ let uri: string | undefined;
+ if (
+ url.type === "wholeFileLocation" ||
+ url.type === "lineColumnLocation"
+ ) {
+ uri = url.uri;
+ }
+ if (uri !== undefined && normalizeFileUri(uri) === normalizedFileUri) {
+ return true;
+ }
+ }
+ return false;
+ });
+}
diff --git a/extensions/ql-vscode/src/view/results/ResultCount.tsx b/extensions/ql-vscode/src/view/results/ResultCount.tsx
index 2311a652ad5..7f64b4f73e0 100644
--- a/extensions/ql-vscode/src/view/results/ResultCount.tsx
+++ b/extensions/ql-vscode/src/view/results/ResultCount.tsx
@@ -3,6 +3,7 @@ import { tableHeaderItemClassName } from "./result-table-utils";
interface Props {
resultSet?: ResultSet;
+ filteredCount?: number;
}
function getResultCount(resultSet: ResultSet): number {
@@ -19,10 +20,18 @@ export function ResultCount(props: Props): React.JSX.Element | null {
return null;
}
- const resultCount = getResultCount(props.resultSet);
+ const totalCount = getResultCount(props.resultSet);
+ if (props.filteredCount !== undefined) {
+ return (
+
+ {props.filteredCount} / {totalCount}{" "}
+ {totalCount === 1 ? "result" : "results"}
+
+ );
+ }
return (
- {resultCount} {resultCount === 1 ? "result" : "results"}
+ {totalCount} {totalCount === 1 ? "result" : "results"}
);
}
diff --git a/extensions/ql-vscode/src/view/results/ResultTable.tsx b/extensions/ql-vscode/src/view/results/ResultTable.tsx
index b6718dd36b8..83c64afdc32 100644
--- a/extensions/ql-vscode/src/view/results/ResultTable.tsx
+++ b/extensions/ql-vscode/src/view/results/ResultTable.tsx
@@ -4,19 +4,70 @@ import { RawTable } from "./RawTable";
import type { ResultTableProps } from "./result-table-utils";
import { AlertTableNoResults } from "./AlertTableNoResults";
import { AlertTableHeader } from "./AlertTableHeader";
+import { SelectionFilterNoResults } from "./SelectionFilterNoResults";
+import type { Row } from "../../common/raw-result-types";
+import type { Result } from "sarif";
+import type { EditorSelection } from "../../common/interface-types";
+
+interface FilteredResultTableProps extends ResultTableProps {
+ /**
+ * When selection filtering is active, these hold the pre-filtered results.
+ * `undefined` means filtering is not active for this result type.
+ */
+ filteredRawRows?: Row[];
+ filteredSarifResults?: Result[];
+ /** True if file-filtered results are still loading from the extension. */
+ isLoadingFilteredResults?: boolean;
+ selectionFilter?: EditorSelection;
+}
+
+export function ResultTable(props: FilteredResultTableProps) {
+ const {
+ resultSet,
+ userSettings,
+ selectionFilter,
+ filteredRawRows,
+ filteredSarifResults,
+ isLoadingFilteredResults,
+ } = props;
+
+ if (isLoadingFilteredResults) {
+ return Loading filtered results…;
+ }
+
+ // When filtering is active and the filtered results are empty, show a
+ // message instead of forwarding to child tables (which would misleadingly
+ // say the query returned no results).
+ if (selectionFilter) {
+ const filteredEmpty =
+ (filteredRawRows !== undefined && filteredRawRows.length === 0) ||
+ (filteredSarifResults !== undefined && filteredSarifResults.length === 0);
+ if (filteredEmpty) {
+ return (
+
+ );
+ }
+ }
-export function ResultTable(props: ResultTableProps) {
- const { resultSet, userSettings } = props;
switch (resultSet.t) {
- case "RawResultSet":
- return ;
+ case "RawResultSet": {
+ const rows = filteredRawRows ?? resultSet.resultSet.rows;
+ const filteredResultSet = {
+ ...resultSet.resultSet,
+ rows,
+ };
+ return ;
+ }
case "InterpretedResultSet": {
const data = resultSet.interpretation.data;
switch (data.t) {
case "SarifInterpretationData": {
+ const results = filteredSarifResults ?? data.runs[0].results ?? [];
return (
void;
+ selectionFilter: EditorSelection | undefined;
+ fileFilteredResults: FileFilteredResults | undefined;
+ selectionFilterEnabled: boolean;
+ onSelectionFilterEnabledChange: (value: boolean) => void;
+ problemsViewSelected: boolean;
+ onProblemsViewSelectedChange: (selected: boolean) => void;
}
const UPDATING_RESULTS_TEXT_CLASS_NAME =
@@ -101,75 +114,28 @@ export function ResultTables(props: ResultTablesProps) {
origResultsPaths,
isLoadingNewResults,
sortStates,
+ selectedTable,
+ onSelectedTableChange,
+ selectionFilter,
+ fileFilteredResults,
+ selectionFilterEnabled,
+ onSelectionFilterEnabledChange,
+ problemsViewSelected,
+ onProblemsViewSelectedChange,
} = props;
- const [selectedTable, setSelectedTable] = useState(
- parsedResultSets.selectedTable ||
- getDefaultResultSet(getResultSets(rawResultSets, interpretation)),
- );
- const [problemsViewSelected, setProblemsViewSelected] = useState(false);
-
- const handleMessage = useCallback((msg: IntoResultsViewMsg): void => {
- switch (msg.t) {
- case "untoggleShowProblems":
- setProblemsViewSelected(false);
- break;
-
- default:
- // noop
- }
- }, []);
-
- const vscodeMessageHandler = useCallback(
- (evt: MessageEvent): void => {
- // sanitize origin
- const origin = evt.origin.replace(/\n|\r/g, "");
- if (evt.origin === window.origin) {
- handleMessage(evt.data as IntoResultsViewMsg);
- } else {
- console.error(`Invalid event origin ${origin}`);
- }
- },
- [handleMessage],
- );
-
- // TODO: Duplicated from ResultsApp.tsx consider a way to
- // avoid this duplication
- useEffect(() => {
- window.addEventListener("message", vscodeMessageHandler);
-
- return () => {
- window.removeEventListener("message", vscodeMessageHandler);
- };
- }, [vscodeMessageHandler]);
-
- useEffect(() => {
- const resultSetExists =
- parsedResultSets.resultSetNames.some((v) => selectedTable === v) ||
- getResultSets(rawResultSets, interpretation).some(
- (v) => selectedTable === getResultSetName(v),
- );
-
- // If the selected result set does not exist, select the default result set.
- if (!resultSetExists) {
- setSelectedTable(
- parsedResultSets.selectedTable ||
- getDefaultResultSet(getResultSets(rawResultSets, interpretation)),
- );
- }
- }, [parsedResultSets, interpretation, rawResultSets, selectedTable]);
-
const onTableSelectionChange = useCallback(
(event: React.ChangeEvent): void => {
- const selectedTable = event.target.value;
+ const table = event.target.value;
vscode.postMessage({
t: "changePage",
pageNumber: 0,
- selectedTable,
+ selectedTable: table,
});
+ onSelectedTableChange(table);
sendTelemetry("local-results-table-selection");
},
- [],
+ [onSelectedTableChange],
);
const handleCheckboxChanged = useCallback(
@@ -178,7 +144,7 @@ export function ResultTables(props: ResultTablesProps) {
// no change
return;
}
- setProblemsViewSelected(e.target.checked);
+ onProblemsViewSelectedChange(e.target.checked);
if (e.target.checked) {
sendTelemetry("local-results-show-results-in-problems-view");
}
@@ -192,7 +158,14 @@ export function ResultTables(props: ResultTablesProps) {
});
}
},
- [database, metadata, origResultsPaths, problemsViewSelected, resultsPath],
+ [
+ database,
+ metadata,
+ onProblemsViewSelectedChange,
+ origResultsPaths,
+ problemsViewSelected,
+ resultsPath,
+ ],
);
const offset = parsedResultSets.pageNumber * parsedResultSets.pageSize;
@@ -223,15 +196,85 @@ export function ResultTables(props: ResultTablesProps) {
const resultSetName = resultSet ? getResultSetName(resultSet) : undefined;
+ // True if file-filtered results are still loading from the extension
+ const isLoadingFilteredResults =
+ selectionFilter != null && fileFilteredResults == null;
+
+ // Filter rows at line granularity (if filtering is enabled)
+ const filteredRawRows = useMemo(() => {
+ if (!selectionFilter || !resultSet || resultSet.t !== "RawResultSet") {
+ return undefined;
+ }
+ if (isLoadingFilteredResults) {
+ return undefined;
+ }
+ const sourceRows =
+ fileFilteredResults?.rawRows !== undefined
+ ? fileFilteredResults.rawRows
+ : resultSet.resultSet.rows;
+ return filterRawRows(sourceRows, selectionFilter);
+ }, [
+ selectionFilter,
+ fileFilteredResults,
+ resultSet,
+ isLoadingFilteredResults,
+ ]);
+
+ // Filter SARIF results at line granularity (if filtering is enabled)
+ const filteredSarifResults = useMemo(() => {
+ if (
+ !selectionFilter ||
+ !resultSet ||
+ resultSet.t !== "InterpretedResultSet"
+ ) {
+ return undefined;
+ }
+ const data = resultSet.interpretation.data;
+ if (data.t !== "SarifInterpretationData") {
+ return undefined;
+ }
+ if (isLoadingFilteredResults) {
+ return undefined;
+ }
+ const sourceResults =
+ fileFilteredResults?.sarifResults !== undefined
+ ? fileFilteredResults.sarifResults
+ : (data.runs[0].results ?? []);
+ return filterSarifResults(
+ sourceResults,
+ resultSet.interpretation.sourceLocationPrefix,
+ selectionFilter,
+ );
+ }, [
+ selectionFilter,
+ fileFilteredResults,
+ resultSet,
+ isLoadingFilteredResults,
+ ]);
+
+ const filteredCount = filteredRawRows?.length ?? filteredSarifResults?.length;
+
return (