Add support for selection-based result filtering in result viewer#4362
Add support for selection-based result filtering in result viewer#4362asgerf wants to merge 2 commits intogithub:mainfrom
Conversation
24df5f9 to
86e3e25
Compare
195b0ac to
30ccbf3
Compare
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.
30ccbf3 to
2bca2f8
Compare
There was a problem hiding this comment.
Pull request overview
Adds a selection-based filtering mode to the results viewer. When enabled, the results view is restricted to the currently viewed file (and further restricted to the current editor selection range when the selection is non-empty), with extension ↔ webview messaging to avoid stale/incorrect results.
Changes:
- Introduces a “Filter results to current file or selection” checkbox and a dedicated empty-state message for selection-filtered views.
- Lifts
selectedTablestate toResultsAppand adds new messaging/state to request/receive file-filtered results from the extension. - Implements file/selection filtering helpers for raw rows and SARIF results, and updates UI to show filtered vs total counts and disable pagination while filtering.
Show a summary per file
| File | Description |
|---|---|
| extensions/ql-vscode/src/view/results/SelectionFilterNoResults.tsx | New empty-state UI when selection filtering yields no matches. |
| extensions/ql-vscode/src/view/results/SelectionFilterCheckbox.tsx | New checkbox control to toggle selection/file filtering. |
| extensions/ql-vscode/src/view/results/ResultTablesHeader.tsx | Adds pagination-disable mode while selection filtering is active. |
| extensions/ql-vscode/src/view/results/ResultTables.tsx | Wires checkbox state, disables pagination, computes filtered rows/results and filtered counts. |
| extensions/ql-vscode/src/view/results/ResultTable.tsx | Applies filtered rows/results to child tables and shows a filter-specific no-results message. |
| extensions/ql-vscode/src/view/results/ResultsApp.tsx | Lifts selected table state; requests/receives file-filtered results; tracks editor selection/filter state. |
| extensions/ql-vscode/src/view/results/ResultCount.tsx | Displays filtered/total result counts when filtering is active. |
| extensions/ql-vscode/src/view/results/result-table-utils.ts | Adds raw-row and SARIF filtering utilities and selection overlap logic. |
| extensions/ql-vscode/src/local-queries/results-view.ts | Sends editor selection updates; handles file-filtered results requests (BQRS decode + SARIF filtering). |
| extensions/ql-vscode/src/databases/local-databases/locations.ts | Returns revealed editor/location from jump helpers to support selection updates after navigation. |
| extensions/ql-vscode/src/common/sarif-utils.ts | Adds helpers to normalize file URIs and extract all locations from SARIF results. |
| extensions/ql-vscode/src/common/interface-types.ts | Adds shared types/messages for editor selection and file-filtered results. |
| extensions/ql-vscode/CHANGELOG.md | Documents the new selection-based filtering feature. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (4)
extensions/ql-vscode/src/view/results/ResultTables.tsx:172
- When selection filtering disables pagination,
offsetis still computed from the currentpageNumber. If the user enables filtering while on a later page, row numbering (and any offset-based logic inRawTable) will start at a non-zero index even though the view is now showing an unpaged, file-filtered set. Consider usingoffset = 0when pagination is disabled / selection filtering is active.
const offset = parsedResultSets.pageNumber * parsedResultSets.pageSize;
extensions/ql-vscode/src/local-queries/results-view.ts:1204
loadFileFilteredResultsdecodes the entire result set in onebqrsDecodecall usingpageSize: schema.rows. For large result sets this can be extremely slow/memory-heavy even if only a small subset matches the file. Consider decoding incrementally using the pagination offsets/page size (and short-circuit once you’ve collected enough matches for the UI), rather than decoding all rows at once.
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,
},
);
extensions/ql-vscode/src/local-queries/results-view.ts:1211
- The conditional
if (schema && schema.rows > 0)means tables with zero rows result inrawRowsstayingundefined, which can lead to the webview treating file-filtered results as perpetually “loading” (since no concrete empty result is returned). Consider settingrawRowsto an empty array when the schema exists but has 0 rows (and/or always returning aFileFilteredResultsobject for the requested (fileUri, selectedTable), even if both arrays are empty).
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) {
extensions/ql-vscode/src/local-queries/results-view.ts:1209
rawRowsis sliced toRAW_RESULTS_LIMITbefore being sent to the webview. This can silently drop matching rows (including ones inside the selection range) and also preventsRawTablefrom showing its existing "Too many results to show at once… omitted" message because it never sees the full length. Consider sending truncation metadata (or collecting up to the limit while still tracking omitted count) so the UI can indicate that results were truncated.
const resultSet = bqrsToResultSet(schema, chunk);
rawRows = filterRowsByFileUri(resultSet.rows, normalizedFilterUri);
if (rawRows.length > RAW_RESULTS_LIMIT) {
rawRows = rawRows.slice(0, RAW_RESULTS_LIMIT);
}
- Files reviewed: 13/13 changed files
- Comments generated: 4
| case "setFileFilteredResults": | ||
| if (msg.results != null) { | ||
| const results = msg.results; | ||
| setState((prev) => { | ||
| if ( | ||
| results.fileUri === prev.editorSelection?.fileUri && | ||
| results.selectedTable === prev.selectedTable && | ||
| prev.fileFilteredResults === undefined | ||
| ) { | ||
| return { ...prev, fileFilteredResults: results }; | ||
| } | ||
| return prev; | ||
| }); | ||
| } | ||
| break; |
There was a problem hiding this comment.
setFileFilteredResults messages with results: undefined are ignored. When the extension can’t produce file-filtered results (e.g. selected table has 0 rows, or the graph table), the UI will stay in the "loading filtered results" state and the request effect can repeatedly re-request results on every render. Handle the undefined case by marking the request as completed (e.g. store a sentinel like null/status flag, or store an empty FileFilteredResults for the requested (fileUri, selectedTable)).
| // True if file-filtered results are still loading from the extension | ||
| const isLoadingFilteredResults = | ||
| selectionFilter != null && fileFilteredResults == null; |
There was a problem hiding this comment.
isLoadingFilteredResults is derived solely from selectionFilter != null && fileFilteredResults == null. For result types that don’t support file-filtering (e.g. graph interpretation), fileFilteredResults will never be set and the UI can get stuck showing "Loading filtered results…". Consider making the loading state conditional on the current resultSet actually being filterable (RawResultSet or SARIF interpreted), or ensure a completion message is always sent/handled.
| // True if file-filtered results are still loading from the extension | |
| const isLoadingFilteredResults = | |
| selectionFilter != null && fileFilteredResults == null; | |
| const isFilterableResultSet = resultSet?.t === "RawResultSet"; | |
| // True if file-filtered results are still loading from the extension | |
| const isLoadingFilteredResults = | |
| isFilterableResultSet && | |
| selectionFilter != null && | |
| fileFilteredResults == null; |
| private computeEditorSelection(): EditorSelection | undefined { | ||
| const editor = window.activeTextEditor; | ||
| if (!editor) { | ||
| void this.logger.log("No active editor"); |
There was a problem hiding this comment.
computeEditorSelection logs "No active editor" every time it runs without an active editor. Since this can happen frequently (e.g. focusing the webview or switching tabs), this risks spamming extension logs. Consider removing this log, lowering its level, or only logging once/throttling.
This issue also appears in the following locations of the same file:
- line 1193
- line 1195
- line 1205
| void this.logger.log("No active editor"); |
Adds support for selection-based result filtering via a checkbox in the result viewer.
A tuple matches the filter if any of its cells contain an entity whose location matches the filter.
selection-filter-cg.mp4
Implementation notes
The result viewer shows results in pages and the webview only has access to its current page. But if we simply apply the filtering on a page-by-page basis, the filtered results could end being scattered across thousands of mostly-empty pages.
The ideal might have been to support for filtering in
codeql bqrslike we do for sorting, although that wouldn't work for interpreted results from SARIF files. This is the approach I took:The most complex part of the change is getting the extension <-> webview communication to work well and making sure we don't show stale results regardless of what order UI changes occur in.
The
selectedTablestate had to be lifted one component up in the hierarchy (fromResultTabletoResultApp) so it is available at the point where we request file-filtered results from the extension.