Skip to content

fix(vs2026): filter now prunes non-matching subtrees deterministically#17

Merged
rcappello merged 5 commits into
mainfrom
fix/vs2026-filter-prune-descendants
Jul 2, 2026
Merged

fix(vs2026): filter now prunes non-matching subtrees deterministically#17
rcappello merged 5 commits into
mainfrom
fix/vs2026-filter-prune-descendants

Conversation

@rcappello

@rcappello rcappello commented Jul 2, 2026

Copy link
Copy Markdown
Owner

fix(vs2026): filter now prunes non-matching subtrees deterministically

Three follow-up fixes on top of the plan 001 filter, plus one draft planning
document.

Symptoms

Under an active filter (e.g. build.yml) the VS 2026 tree could show:

  • non-matching sibling templates inside a pipeline the user expanded after the
    scan (Templates 7 with 7 visible items, only 4 actually matched);
  • a Templates group still shown as empty because the match lived several
    levels deep inside a nested template chain;
  • a Scripts group visible under a matched pipeline even when no script inside
    it matched — the offending pipeline varied between runs.

Fixes (in order applied)

  1. 5850e94BuildAnalysisChildren now re-applies the active filter when a
    pipeline / template is lazily expanded after the scan, so newly materialised
    children collapse away exactly like they would if they had existed at scan
    time.
  2. 008613f — the scan records deferred group-visibility marks (Templates /
    Scripts) so the visibility of a group node can be reconstructed after it is
    materialised, and DFS-walks nested template chains to record every
    intermediate template on a path to a nested match. Intermediate templates
    surface even before the user drills into them.
  3. 57d4d51 — apply visibility on the local children list before
    ReplaceList(parent.Children, children), not after. The old order pushed
    the new children with the default IsVisibleUnderFilter=true, then fired
    PropertyChanged(false) on freshly-added items — that late property change
    was dropped by the WPF-side binding on the Remote UI client, leaving the
    Scripts group stuck visible. The initial snapshot now carries the correct
    value and no post-add property change is required.

Combined, the tree under an active filter matches the VS Code client and
stays deterministic regardless of when (or whether) the user expands each
pipeline.

Other commits

  • ee6c91e — draft plan 003 scoping the next filter improvement
    (show pipeline in context + highlight). Documentation only, no code
    change. Can be dropped from this PR if you'd rather keep the fix isolated —
    say the word and I'll rebase it out.

Files touched

  • src/vs2026/ViewModels/PipelinesViewModel.cs — filter algorithm + lazy-load
    reapply + pre-ReplaceList visibility.
  • src/vs2026/CHANGELOG.md — rolled the fixes into [0.3.1].
  • src/vs2026/source.extension.vsixmanifestIdentity/@Version 0.3.0
    0.3.1 (csproj propagates it to the assembly Version).
  • .specify/plans/003-* — draft plan (see ee6c91e).

How to verify

  1. Build + install src/vs2026/bin/Debug/net10.0-windows/PipelinesExplorer.VisualStudio.vsix.
  2. Sign in to an ADO org with pipelines that mix inline scripts, external
    scripts and nested template: references.
  3. Type build.yml in the filter box. Wait for the debounce.
  4. Expand every visible pipeline. Every Scripts group with no script whose
    basename contains build.yml should collapse away; every Templates
    group should show only the templates on a path to a match.

Reference log (log level Info, %LocalAppData%\PipelinesExplorer\logs) can
be re-instrumented locally by reverting commit 57d4d51 up to before the
final cleanup — the intermediate diagnostic logs ([FILTER] group-created,
apply group=Scripts …, group-visibility-changed) were used to prove the
race, then removed.

Release policy

Per .github/instructions/project.instructions.md, this touches user-facing
code under src/vs2026/ViewModels/. The VS 2026 client is bumped to 0.3.1
in this PR (4af7760). The VS Code client is untouched.

rcappello added 5 commits July 2, 2026 17:01
When a user expanded a pipeline or template after the filter scan had finished, LoadPipelineAnalysisAsync / LoadTemplateAnalysisAsync built fresh child nodes that defaulted to IsVisibleUnderFilter=true — leaving non-matching sibling templates and the whole Scripts group visible under matched pipelines (bug reported against 0.3.0). BuildAnalysisChildren now runs ApplyVisibilityRecursive on the parent after ReplaceList when a filter is active, so newly materialised subtrees prune correctly. InfoNodes (loading, warning, no-templates) are pinned visible so the parent's decision still surfaces its status text, but they do not on their own force a non-matching parent open. Restores VS Code parity (plan 001 section 2).
…hlight

Follow-up to plan 001. Keeps matches-only pruning as the default; adds a per-pipeline context-menu action to temporarily unprune a single pipeline's subtree, and independently adds substring highlighting in visible matched labels. No global setting: the right view depends on the moment ('find' vs 'explore'), not on a persistent preference.
…plates

Two follow-up gaps discovered after commit 5850e94 (which reapplied filter visibility on lazy-load but only covered one case).

1) Race between scan and lazy-load. MarkTemplateOrScriptMatch silently no-op'd whenever the target pipeline's Templates/Scripts group was not yet materialised (children still [Loading InfoNode]), which is the case for pipelines the user hadn't expanded before typing the filter. When the user then expanded that pipeline after the scan finished, BuildAnalysisChildren materialised the groups and ApplyVisibilityRecursive hid every non-directly-matching template — so a pipeline matched via a nested template ended up either invisible or (worse) collapsed with no expandable content. The scan now records deferred (pipeline, GroupKind) marks in _deferredGroupMarks regardless of materialisation state, and BuildAnalysisChildren applies them to newly-materialised GroupNodes before running ApplyVisibilityRecursive. Non-deterministic behaviour reported after reset+re-apply of the same filter is explained by this race and eliminated.

2) Intermediate templates with only nested matches. ScanTemplatesRecursivelyAsync only tracked a scalar anyNestedMatch — nothing recorded WHICH intermediate template on the path to the match should be surfaced. For a pipeline whose Templates group contained e.g. deployment.yml -> build.yml, only build.yml (buried inside deployment.yml) matched the term 'build.yml'; the intermediate deployment.yml basename did not, so ApplyVisibilityRecursive hid deployment.yml, leaving a 'Templates 7' group that expanded to nothing. The scan is now a proper DFS (ScanTemplateLevelAsync helper) that returns true when the subtree contains a match; every intermediate template on such a path is added to _templatesWithDescendantMatch (keyed by repo-absolute resolved path). ApplyVisibilityRecursive treats a TemplateNode whose ResolvedPath sits in that set as self-matched, so it surfaces the full path from the pipeline down to the deepest match — matching the VS Code client.

Both tracking sets are cleared in ClearFilterInternal and at the top of RunFilterScanAsync alongside _matchedNodes / _ancestorNodes.
…te UI race

Symptoms: the `Scripts` group of a matched pipeline could stay visible under an active filter even though `ApplyVisibilityRecursive` correctly set `IsVisibleUnderFilter=false` on it. Which pipeline was affected varied between runs.

Root cause: `BuildAnalysisChildren` used to push the newly-materialised children to the parent via `ReplaceList` first (initial snapshot with the default `IsVisibleUnderFilter=true`), then call `ApplyVisibilityRecursive` — firing `PropertyChanged(false)` on nodes that had just been added. The follow-up property change on a freshly-added item is dropped by the WPF-side binding on the Remote UI client, so the group remained visible.

Fix: apply visibility on the local `children` list before handing it to `ReplaceList`. The initial snapshot sent to the client already carries the correct `IsVisibleUnderFilter` for every new node — no post-add property change needed, no race.
Roll the three filter-pruning fixes on this branch (5850e94, 008613f, 57d4d51) into a 0.3.1 patch release of the VS 2026 client.

- `source.extension.vsixmanifest` Identity/@Version 0.3.0 -> 0.3.1 (csproj propagates it to the assembly Version).

- CHANGELOG: converted [Unreleased] into [0.3.1] - 2026-07-02 and added a fresh empty [Unreleased] section on top.

No code change here.
@rcappello rcappello merged commit 40d82a2 into main Jul 2, 2026
5 checks passed
@rcappello rcappello deleted the fix/vs2026-filter-prune-descendants branch July 2, 2026 16:53
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.

1 participant