Skip to content

fix: guard Quick Search "Open in Search View" against unmaterialized tree element (fixes #326646)#326652

Open
vs-code-engineering[bot] wants to merge 2 commits into
mainfrom
fix/quick-search-tree-element-not-found-f09610e5ab4fb24d
Open

fix: guard Quick Search "Open in Search View" against unmaterialized tree element (fixes #326646)#326652
vs-code-engineering[bot] wants to merge 2 commits into
mainfrom
fix/quick-search-tree-element-not-found-f09610e5ab4fb24d

Conversation

@vs-code-engineering

@vs-code-engineering vs-code-engineering Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

TreeError [SearchView] Tree element not found is thrown when a user moves a Quick Search (Go to File / text search quick access) result into the full Search view via the "Open in Search View" / "More" / "See More Files" affordances. TextSearchQuickAccess.moveToSearchViewlet transfers its search model to the Search view and then calls viewer.setSelection([currentElem]) on the view's WorkbenchCompressibleAsyncDataTree. Because that tree lazy-loads its nodes, currentElem (often a deep Match under a not-yet-expanded file, or a file match beyond the visible limit) may not be materialized in the tree yet, so setSelection throws. The impact is an unhandled error surfaced to telemetry and a failed "open in search view" gesture.

Fixes #326646
Recommended reviewer: @osortega

Culprit Commit

Field Value
Commit 88056f2
Author @andreamah
PR #228886
Message Use WorkbenchCompressibleAsyncDataTree for lazy loading AI results (#228886)
Why This commit converted the Search view tree from a fully-materialized sync tree to a lazy-loading async data tree. moveToSearchViewlet kept calling setSelection/reveal with an element reference that a sync tree always contained but the async tree only contains once its parent chain has been loaded, so the pre-existing call became able to throw Tree element not found.

This is a long-standing mechanism (the async-tree conversion predates the shipped build by well over a year) surfaced as a recent telemetry spike, so it is effectively pre-existing; 88056f2 is named because it is the change that made the setSelection call unsafe. The fix is independent of exact culprit attribution.

Code Flow

sequenceDiagram
    participant User as Quick Search pick
    participant QA as TextSearchQuickAccess.moveToSearchViewlet
    participant View as SearchView.replaceSearchModel
    participant Tree as CompressibleAsyncDataTree
    participant Model as CompressedObjectTreeModel

    User->>QA: accept "Open in Search View" / "More"
    QA->>View: replaceSearchModel(model) (queues async refresh)
    Note over View: setInput + queued updateChildren;<br/>deep nodes not yet materialized
    QA->>Tree: setSelection([currentElem])
    Tree->>Model: map element -> node (getNode/getCompressedNode)
    Note over Model: ⚠️ element not in nodes map
    Note over Model: 💥 TreeError [SearchView] Tree element not found
Loading

Affected Files

File Role Evidence
src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts crash site L347 throw new TreeError(this.user, 'Tree element not found: ...') (from stack)
src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts root cause L219-L226: viewer.setSelection([currentElem]) called on a lazy async tree with an element (matches[limit] L256 / deep element L291) that may not be materialized
src/vs/base/browser/ui/tree/asyncDataTree.ts precondition L839-L850 hasNode() reports whether an element has been materialized in the async tree

Repro Steps

  1. Open a folder with many text matches for a common term.
  2. Open Quick Search (text search quick access) and type a term producing many results, enough to show the "More" / "See More Files" separators.
  3. Trigger "Open in Search View" / "More" on a match whose file match is not expanded/loaded (or a file match beyond the shown limit).
  4. Intermittently, the transferred Search view tree has not yet materialized that node, so setSelection throws TreeError [SearchView] Tree element not found. Timing-dependent: larger result sets and collapsed files increase the likelihood.

How the Fix Works

Chosen approachtextSearchQuickAccess.tsmoveToSearchViewlet(): guard the setFocus/setSelection/reveal block on viewer?.hasNode(currentElem). When the element has not been materialized in the async tree, fall back to focusing the search widget — the exact behavior the existing else branch already provides for the "no element" case. This checks a documented precondition of the async-data-tree API before invoking it, at the site that produces the invalid call (fix at the caller that makes the unsupported call, not at the shared tree crash site). It reuses the same hasNode materialization check already used elsewhere in this view (RefreshTreeController.refreshTree, searchView.ts L2769), so it introduces no new contract and no try/catch that would hide errors from telemetry.

Lifecycle pattern: async-init race (stale/unmaterialized cached reference) — the consumer holds a RenderableMatch that the freshly-input async tree has not yet loaded.
Producer site: src/vs/workbench/contrib/search/browser/searchView.ts:469replaceSearchModel calls setInput and only queues (does not synchronously complete) the tree refresh, so deep nodes are absent when control returns to the caller.
Why this fix: after this change, textSearchQuickAccess.ts:220 only calls setSelection/reveal when hasNode confirms the element exists, so CompressedObjectTreeModel.getCompressedNode can no longer receive an absent element from this path.

Alternatives considered: awaiting a full recursive tree materialization/expandTo inside replaceSearchModel before returning — rejected because it would force-expand collapsed file matches (changing UX) and broaden the change well beyond the failing path for a gesture whose graceful degradation (focus the widget) already exists.

Recommended Owner

@osortega — current Search View owner per microsoft/vscode-engineering triage/working-areas.md (the culprit-commit author's change is effectively pre-existing; area owner selected as the live, authoritative owner of Search View UI / quick search).

Generated by errors-fix · 2.6K AIC · ⌖ 35.1 AIC · ⊞ 71K ·


errors-fix-driver — cycle 1

Trigger: cron_check_failed · Head: d910a70fea1 (d910a70)

Item Action
Review on textSearchQuickAccess.ts:224 — optional chaining doesn't narrow viewer (in scope) Fixed in d910a70 (replied + resolved)
Review on textSearchQuickAccess.ts:227 — same narrowing concern (in scope) Fixed in d910a70 (replied + resolved)
CI: Linux / Electron (prior SHA) Superseded by new push; fresh run pending

Push: yes — d910a70 · Copilot rerequested: ok

Ready gate: ci pending after push → not marking ready this cycle

Generated by errors-fix-driver · 770.3 AIC · ⌖ 34.2 AIC · ⊞ 72.9K ·

…tree element (fixes #326646)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 20, 2026 15:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot could not run the full agentic suite for this review because it was automatically requested on a bot-authored pull request. Request a review from Copilot under Reviewers to retry with the full agentic suite. Improved support for bot-authored pull requests is coming soon.

Adds a defensive guard in Quick Access text search selection logic to avoid errors when the async tree hasn’t materialized the target element yet.

Changes:

  • Add an explanatory comment describing the “Tree element not found” failure mode in the async tree.
  • Guard focusing/revealing a match with hasNode to avoid acting on non-materialized elements.

Comment on lines 224 to 227
if (currentElem && viewer?.hasNode(currentElem)) {
viewer.setFocus([currentElem], getSelectionKeyboardEvent());
viewer.setSelection([currentElem], getSelectionKeyboardEvent());
viewer.reveal(currentElem);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d910a70: replaced the optional-chaining condition with an explicit currentElem && viewer && viewer.hasNode(currentElem) guard so viewer is narrowed to non-undefined before setFocus/setSelection/reveal.

Generated by errors-fix-driver · 770.3 AIC · ⌖ 34.2 AIC · ⊞ 72.9K

@vs-code-engineering
vs-code-engineering Bot marked this pull request as ready for review July 20, 2026 15:59
@vs-code-engineering
vs-code-engineering Bot enabled auto-merge (squash) July 20, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment on lines 219 to 227
const viewer: WorkbenchCompressibleAsyncDataTree<ISearchResult, RenderableMatch> | undefined = viewlet?.getControl();
if (currentElem) {
// The async data tree may not have materialized `currentElem` yet (e.g. its parent
// file match has not been expanded/loaded), in which case selecting or revealing it
// throws `TreeError [SearchView] Tree element not found`. Guard on `hasNode` before
// touching the tree and fall back to focusing the search widget otherwise.
if (currentElem && viewer?.hasNode(currentElem)) {
viewer.setFocus([currentElem], getSelectionKeyboardEvent());
viewer.setSelection([currentElem], getSelectionKeyboardEvent());
viewer.reveal(currentElem);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d910a70: the condition now uses an explicit currentElem && viewer && viewer.hasNode(currentElem) check, narrowing viewer to a non-undefined type inside the block before the tree method calls.

Generated by errors-fix-driver · 770.3 AIC · ⌖ 34.2 AIC · ⊞ 72.9K

…ow viewer before tree calls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vs-code-engineering

Copy link
Copy Markdown
Contributor Author

Commit pushed: 4cb4e41

Generated by errors-fix-driver · 770.3 AIC · ⌖ 34.2 AIC · ⊞ 72.9K

@vs-code-engineering
vs-code-engineering Bot requested a review from Copilot July 20, 2026 16:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment on lines +220 to +224
// The async data tree may not have materialized `currentElem` yet (e.g. its parent
// file match has not been expanded/loaded), in which case selecting or revealing it
// throws `TreeError [SearchView] Tree element not found`. Guard on `hasNode` before
// touching the tree and fall back to focusing the search widget otherwise.
if (currentElem && viewer && viewer.hasNode(currentElem)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The explicit fallback is already present locally: the else branch of this same if calls viewlet.searchAndReplaceWidget.focus(). The guard change (currentElem && viewer && viewer.hasNode(currentElem)) simply routes the "node not yet materialized" case into that existing else, so the unmaterialized-node path now degrades to focusing the search widget — exactly what the comment describes. No rewording or additional else handling is needed.

Generated by errors-fix-driver · 476.2 AIC · ⌖ 24.2 AIC · ⊞ 72.9K

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Error] unhandlederror-TreeError [SearchView] Tree element not found: [object Object]

2 participants