Skip to content

Preserve Debug Steps tree state across filter sessions - #3919

Merged
siegfriedpammer merged 3 commits into
masterfrom
debugsteps-filter-tree-state
Jul 28, 2026
Merged

Preserve Debug Steps tree state across filter sessions#3919
siegfriedpammer merged 3 commits into
masterfrom
debugsteps-filter-tree-state

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

The Debug Steps pane's filter box destroyed the tree state it filtered: typing the first character expanded every row (wiping the expansion the user had built up), and clearing the filter collapsed the whole tree, leaving the still-selected step buried and off screen.

Root cause

The XAML drove every row's IsExpanded from the single IsFiltering flag through a TreeViewItem style setter. All user expansion gestures (expander arrow, header double-tap, arrow keys) write the property via SetCurrentValue, and a current value is discarded whenever the active style binding produces a new value - so each flip of IsFiltering overwrote every row's manual state. Expansion lived nowhere else: Stepper.Node has no UI state, and the containers were the only carrier.

Fix

  • Each step is wrapped in a StepNodeViewModel carrying IsVisible and a two-way-bound IsExpanded, making row state authoritative in the view-model instead of dying with the containers.
  • A filter session is transient: expansion is snapshotted when the filter becomes non-empty, non-matches are hidden and only the paths to matches are expanded while typing, and clearing the filter restores the snapshot, re-expands the selected step's ancestors, and centers the selected row in the viewport.
  • Wrapping is skipped for reference-equal step lists, because step replays re-report the same run and a rebuild would wipe the state mid-navigation.
  • DebugStepFilterConverter is gone; the matching logic moved into the pane model where it is unit-testable.

Tests

Four new headless view-level tests in DebugStepsFilterStateTests pin the behavior (expansion round-trip, match revealing, selection reveal, centering); they simulate gestures with SetCurrentValue, matching what real TreeViewItem gestures do. Full ILSpy.Tests suite: 1073 tests, 0 failures.

This PR was authored by an AI agent (Claude Code) operated by @siegfriedpammer.

🤖 Generated with Claude Code

The filter drove every row's IsExpanded from the single IsFiltering flag
via a TreeViewItem style setter. Expansion lived nowhere else: user
gestures (expander arrow, double-tap, arrow keys) all write the property
with SetCurrentValue, which the style binding overwrites on every flip
of the flag. Starting a filter therefore destroyed the expansion state
the user had built up, and clearing it collapsed the whole tree,
burying the still-selected row under collapsed groups.

Row state (visibility + expansion) now lives on a StepNodeViewModel
wrapper per step, two-way bound from the style, so gestures persist in
the view-model. A filter session snapshots expansion on entry, hides
non-matches and opens only the paths to matches while typing, and on
exit restores the snapshot and re-expands the selected step's ancestors
so the selection stays visible. Wrapping is skipped for reference-equal
step lists because step replays re-report the same run and a rebuild
would wipe the state mid-navigation.

Assisted-by: Claude:claude-fable-5:Claude Code

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

Improves the Debug Steps tool pane (debug-only Avalonia UI) so filtering the step tree no longer destroys user-driven expansion state, and ensures the currently selected step is revealed/centered after filter changes.

Changes:

  • Moves per-row UI state (visibility + expansion) into a new StepNodeViewModel wrapper so filtering and user gestures update stable view-model state rather than transient TreeViewItem containers.
  • Reworks DebugStepsPaneModel filtering to snapshot/restore expansion across filter sessions, expand paths to matches, and request selection centering after filter updates.
  • Updates the view and tests accordingly, removing the old XAML multi-binding converter and adding headless tests that pin the new behavior.

Reviewed changes

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

Show a summary per file
File Description
ILSpy/Views/DebugSteps.axaml.cs Adds view-side attach/detach wiring and logic to center the selected tree row after filter-driven expansion changes.
ILSpy/Views/DebugSteps.axaml Switches TreeView row bindings to view-model-owned IsVisible / two-way IsExpanded, removing the prior converter-based filtering.
ILSpy/Views/DebugStepFilterConverter.cs Removes the old IMultiValueConverter-based filtering implementation.
ILSpy/ViewModels/StepNodeViewModel.cs Introduces a wrapper view-model that carries per-row UI state and parent/child relationships.
ILSpy/ViewModels/DebugStepsPaneModel.cs Implements filter-session snapshot/restore and match-path expansion; adds a view notification for selection centering.
ILSpy.Tests/Views/DebugStepsTests.cs Updates existing tests to use the wrapper tree and pane-side filtering behavior.
ILSpy.Tests/Views/DebugStepsFilterStateTests.cs Adds new headless tests covering filter round-trips, match visibility/expansion, selection preservation, and centering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ILSpy/Views/DebugSteps.axaml.cs
Filtering and un-filtering the Debug Steps tree re-arranges rows around
the selection: matches surface under newly expanded groups and the
restore step re-opens the selected step's ancestors. Without scrolling,
the selected row regularly ended up outside the viewport even though it
was expanded into view. The ViewModel now raises a reveal request after
each filter pass while a visible step is selected; the View answers it
by centering the row, because scrolling needs containers and the
ScrollViewer. The measurement anchors on the row header since an
expanded group's container spans its whole subtree, and it runs at
Loaded dispatcher priority so freshly expanded containers have valid
geometry.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer force-pushed the debugsteps-filter-tree-state branch from 2b40970 to e5cb1b6 Compare July 27, 2026 16:45
The explicit 15-second budget timed out on a loaded Windows Release CI
runner (a different test of this fixture on each attempt, while the
sibling workflow run passed the same head). The waits are
completion-bounded - the predicate observes the end of an async
re-decompile - so the 60-second Waiters default costs nothing when
healthy and only converts runner slowness from a red run into a longer
green one.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer merged commit 89800af into master Jul 28, 2026
13 checks passed
@siegfriedpammer
siegfriedpammer deleted the debugsteps-filter-tree-state branch July 28, 2026 14:16
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.

2 participants