Skip to content

Add support for selection-based result filtering in result viewer#4362

Open
asgerf wants to merge 5 commits intogithub:mainfrom
asgerf:asgerf/filter-results-to-selection
Open

Add support for selection-based result filtering in result viewer#4362
asgerf wants to merge 5 commits intogithub:mainfrom
asgerf:asgerf/filter-results-to-selection

Conversation

@asgerf
Copy link
Copy Markdown
Contributor

@asgerf asgerf commented Apr 9, 2026

Adds 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.

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 bqrs like we do for sorting, although that wouldn't work for interpreted results from SARIF files. This is the approach I took:

  • When the currently-viewed file changes, read the whole BQRS/SARIF in the extension and send the results for that file to the webview.
  • The webview then post-filters those to the current selection range (if the selection is non-empty).
  • There is no support for pagination, but the filtering will in practice reduce the number of results so they fit in the UI just fine.

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 selectedTable state had to be lifted one component up in the hierarchy (from ResultTable to ResultApp) so it is available at the point where we request file-filtered results from the extension.

@asgerf asgerf force-pushed the asgerf/filter-results-to-selection branch from 24df5f9 to 86e3e25 Compare April 9, 2026 11:22
@asgerf asgerf changed the title Adds support for selection-based result filtering in result viewer Add support for selection-based result filtering in result viewer Apr 9, 2026
@asgerf asgerf force-pushed the asgerf/filter-results-to-selection branch from 195b0ac to 30ccbf3 Compare April 9, 2026 12:58
asgerf added 2 commits April 9, 2026 17:51
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.
@asgerf asgerf force-pushed the asgerf/filter-results-to-selection branch from 30ccbf3 to 2bca2f8 Compare April 9, 2026 15:58
@asgerf asgerf marked this pull request as ready for review April 10, 2026 08:42
@asgerf asgerf requested review from a team as code owners April 10, 2026 08:42
Copilot AI review requested due to automatic review settings April 10, 2026 08:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 selectedTable state to ResultsApp and 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, offset is still computed from the current pageNumber. If the user enables filtering while on a later page, row numbering (and any offset-based logic in RawTable) will start at a non-zero index even though the view is now showing an unpaged, file-filtered set. Consider using offset = 0 when pagination is disabled / selection filtering is active.
  const offset = parsedResultSets.pageNumber * parsedResultSets.pageSize;

extensions/ql-vscode/src/local-queries/results-view.ts:1204

  • loadFileFilteredResults decodes the entire result set in one bqrsDecode call using pageSize: 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 in rawRows staying undefined, which can lead to the webview treating file-filtered results as perpetually “loading” (since no concrete empty result is returned). Consider setting rawRows to an empty array when the schema exists but has 0 rows (and/or always returning a FileFilteredResults object 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

  • rawRows is sliced to RAW_RESULTS_LIMIT before being sent to the webview. This can silently drop matching rows (including ones inside the selection range) and also prevents RawTable from 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

asgerf and others added 3 commits April 10, 2026 11:25
Otherwise the webview doesn't work when built locally
When loadFileFilteredResults could not produce results (e.g. 0-row table,
graph table, or no displayed query), it sent results: undefined which the
webview ignored, leaving it in the loading state and causing repeated
re-requests on every render.

Now always send a FileFilteredResults object so the webview marks the
request as completed. Made SetFileFilteredResultsMsg.results non-optional
to enforce this at the type level.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@cklin
Copy link
Copy Markdown
Contributor

cklin commented Apr 10, 2026

Some initial feedback:

  1. Please restructure the PR into smaller, self-contained commits so that they are easier to understand and to review. Ideally the PR would separate into different commits (a) structural changes that are obviously behavior-preserving from (b) behavior changes, with most of the change in the first category, so that I can devote more attention to the parts that need to be reviewed in more detail.

  2. Can you elaborate how you arrived at the design of performing the filtering on the (webview) client side? I would have expected this design:

    1. The client side informs the (extension) server side when the state of the filtering checkbox changes
    2. The server side applies the filtering (or not) to the results
    3. The server side informs the client side that the results have changed
    4. The client side fetches the updated results from the server side

    The client and the server probably already handle server-side result changes, so steps (iii) and (iv) can use that existing infrastructure. There would be minimal changes on the client side, and existing features (such as result pagination) would "just work" also for filtered results.

    I am sure you have reasons for doing things your way, and I want to hear them.

@cklin cklin self-assigned this Apr 10, 2026
@asgerf
Copy link
Copy Markdown
Contributor Author

asgerf commented Apr 13, 2026

Thanks for looking into this. I'll look into restructuring the commit history when I have time.

As for the way filtering work: It started out being more server-side but state management became too error-prone. There's more state to keep track of than is obvious at first, and you end up with a bunch of fields and patterns of "whenever X changes/happens remember to also do Y". I eventually refactored into the tried and true "React + stateless server" pattern which is less error-prone.

It's hard to remember exactly why it was so hard, other than the fact that there was always one more nuance to deal with, and it felt like the architecture wasn't sound. This is my best attempt at bringing up from memory what the issues were:

Selection and active editor
There are some subtleties around when the editor selection or active-editor change should update the effective selection filter.

  • Clicking a link in the results view can change the selection and/or active editor, but should not change the selection filter. (Otherwise it's hard to fully explore a set of results that you're focusing on)
  • But if the user clicks a link and then enables filtering we want the filter to apply to the currently-viewed file.
  • VSCode doesn't have an "active" editor when the webview has focus so we end up having to remember the last-active editor, yet another thing to track.

Race conditions
There is potential for race conditions between UI events and messages from the extension. The client might request a state change, but then the user quickly clicks a checkbox, then results arrive but now they are stale. If the client doesn't "own" enough state it gets very hard to validate what parts of a setState message is stale.

Pagination
Pagination for BQRS-based results still would not work without also changing codeql bqrs decode to do the file-filtering. We rely on bqrs diff for pagination, but as mentioned, we can't just filter page-by-page as the filtered results can get scattered across thousands of mostly-empty pages.

We'd also have to re-paginate the results whenever the selection changes, preferrably starting using the cached file-filtered results for efficiency. Not impossible to implement or anything, but I wouldn't say we get pagination "for free".

@cklin
Copy link
Copy Markdown
Contributor

cklin commented Apr 13, 2026

Thanks for the additional context! Sounds like server-side filtering is simpler only in theory; in that case client-side filtering makes sense.

Please @-mention me when you are done with the commit restructuring!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants