The headless UI tests synchronized with the application by pumping a fixed
number of frames (39 loops of RunJobs/Delay across 19 files) and by pressing
at a point computed once from a control's bounds. Both encode how fast the
machine that wrote the test was: on the loaded Windows Debug CI agent the
frame count comes up short and the point goes stale, which is the recurring
timeout in the tree context-menu tests and the reason each such failure was
repaired one test at a time.
Waiters.WaitForIdleAsync replaces the frame loops. It observes the actual
precondition - no dispatcher job queued at Background priority or above, no
assembly still loading in the background sweep, a frame rendered - and
requires it on two consecutive polls so a thread-pool continuation about to
post back is caught as well.
Window.ClickAsync replaces element-targeted MouseDown/MouseUp pairs. It
re-resolves the target on every poll and presses only once the window's hit
test at the click point answers with that target, reporting the point and
what was hit instead on timeout. That diagnostic exposed one vacuous test:
User_Click_On_Visible_Row_Does_Not_Recentre_Viewport clicked the centre of a
row wider than the tree viewport, which lies under the decompiler text view,
so its assertion held without the row ever being clicked. It now clamps the
point to the viewport like the other tree-row clicks.
Clicks at text positions and press-only gutter clicks stay raw; they do not
target an element.
Assisted-by: Claude:claude-fable-5:Claude Code
The search icon tests only exercised the type and field delegations; the
method, property and event arms and the namespace LocationImage fallback
for top-level types were untested.
Assisted-by: Claude:claude-fable-5:Claude Code
The WPF frontend uses a type-only overlay mapper that shows protected
internal types with the plain protected badge, while members get the
combined protected-internal badge; the Avalonia frontend ran both through
the shared Images.GetOverlay and so badged types differently. Restore the
type-only mapping in TypeTreeNode.GetIcon, which now also covers search
results and every other caller of the helper.
Assisted-by: Claude:claude-fable-5:Claude Code
The search result factory built icons from the bare base images, bypassing
Images.GetIcon, so search results lost the private/internal/protected and
static mini-overlays (and flattened interfaces, structs, enums and delegates
to the class icon; constructors, operators and indexers to the plain member
icons). Delegate to the tree nodes' GetIcon helpers instead, as the WPF
frontend's SearchResultFactory did, so search icons match the assembly tree
by construction. TypeTreeNode and EventTreeNode get the same static GetIcon
extraction the other member tree nodes already had.
Assisted-by: Claude:claude-fable-5:Claude Code
ProgressBar.IsIndeterminate defaults to false, so the "nothing is running
yet" assertion would also pass against a pane whose DataContext is not
the resolved SearchPaneModel, and the failure would only surface one
line later, blamed on the binding direction rather than on the missing
DataContext.
Assisted-by: Claude:claude-fable-5:Claude Code
The search pane's progress bar was permanently indeterminate and merely
hidden when idle, and the decompiler view's bar defaults to
indeterminate mode whether or not a decompilation is running. The
indeterminate indicator is an infinite keyframe animation that keeps
running - and keeps the control's whole visual tree alive through the
render clock - for as long as the pseudo-class is set, hidden or not.
Both bars now go indeterminate only for the duration of the work.
Assisted-by: Claude:claude-fable-5:Claude Code
These explain the code by pointing at the front-end that used to implement it.
That front-end is no longer in the tree, so the referent a reader would go
looking for does not exist: "Mirrors WPF's RefreshDecompiledView() call" names
a method nobody can open. In almost every case the sentence beside it already
carried the reason, and the reference was an appendix.
Comments citing a live platform difference are left alone, because there the
comparison is the reason rather than a memory: Avalonia genuinely has no
global RequerySuggested signal, which is why SimpleCommand exists at all.
One had gone stale rather than merely redundant. DerivedTypesEntryNode
described consulting the active search term as a missing feature to reinstate,
but SearchTermMatches is deliberately a no-op so the assembly tree stays
independent of the search pane; the comment now says so.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A comment that justifies behaviour by pointing at the WPF front-end means
nothing to someone reading the file cold: the reason is either already stated
beside it or is not stated anywhere. Each of these now names the constraint
itself - why navigation waits for pointer-release, why a signature block wraps,
why the tree filter ignores the search term.
Comments citing an external product's documented behaviour as the source of a
rule are left alone; there the reference is the reason, not a memory of how the
code arrived.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The global:: prefixes existed because the test project's namespace
ICSharpCode.ILSpy.Tests used to shadow the app's old top-level ILSpy
namespace. With the UI code back under ICSharpCode.ILSpy there is
nothing left to shadow, so plain fully qualified names resolve fine.
Assisted-by: Claude:claude-fable-5:Claude Code
The Avalonia port had placed the UI app in an ILSpy.* namespace tree,
while the csproj RootNamespace and every prior release (through 10.1)
use ICSharpCode.ILSpy.*. Restoring the historical namespace reduces the
public API diff against release/10.1 for plugin authors and removes the
shadowing that forced global:: qualifiers in the test project. The
Images class and AccessOverlayIcon enum move back into the root
namespace (as in 10.1), since an ICSharpCode.ILSpy.Images namespace
would shadow the Images class for all code inside ICSharpCode.ILSpy.
Assisted-by: Claude:claude-fable-5:Claude Code
The WPF host attached a registry-driven context menu to the search list
(ContextMenuProvider.Add) and showed a tooltip on every result column;
the Avalonia port carried neither. Right-clicking a result now opens the
same menu the trees use -- the selected result's entity is handed to the
entries via TextViewContext.Reference, so Analyze and the scope-search
entries light up -- and the Location/Assembly cells regain their
full-text tooltips (the Name cell already showed the file path per
Fix#1263).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The search panel rendered hits in a bare ListBox with no headers, so the only
ordering was the fixed fitness/name streaming order chosen at search time --
the user could not re-sort by Name, Location or Assembly. Replace the ListBox
with the app's standard sortable Avalonia DataGrid (mirroring OpenFromGacDialog
/ MetadataTablePage): three template columns that keep the per-field icons,
each with a SortMemberPath so clicking a header sorts by Name / Location /
Assembly. No initial column sort is applied, so the default fitness ranking is
preserved until the user clicks a header.
The code-behind is unchanged: ListBox and DataGrid share the
SelectingItemsControl surface, so the existing selection / activation /
keyboard / middle-click handlers port as-is (verified by the existing search
niceties tests now running against the DataGrid).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Restores the result-navigation gestures the pane shipped with before the
port: Ctrl+T/M/S jump the picker to Type/Member/Constant, Down/Up arrow
hand focus between the search box and the result list, and Ctrl+Enter or a
middle-click open a result in a new document tab instead of reusing the
active one. New-tab activation routes through the existing
OpenNodeInNewTab so it matches the tree's open-in-new-tab behaviour.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Search, Analyzer and Debug Steps cluttered the default layout. They now opt out via ExportToolPane.IsVisibleByDefault = false (which BuildToolDock finally honours), so a fresh launch shows just the assembly tree. Each pane keeps its home alignment and is materialised there on demand by ShowToolPane, so opening Search / Analyze surfaces it in the same place as before.
To audit what each UI test actually exercises, every step now snapshots the
live window to <TestFixtureName>/<TestName>_<NN>_<ShortDescription>.png: a
booted frame (emitted automatically by TestHarness.BootAsync), one after each
state-changing action, and one before each assertion. Flip ILSPY_TESTS_VISIBLE=1
to render the filmstrip; it lands under %TEMP%/ilspy-test-captures (overridable
via ILSPY_TEST_CAPTURES).
The step number and fixture/test name are derived automatically so inserting a
breakpoint never renumbers the rest. The identity is recorded up front from the
real ITest in an ITestAction hook rather than read live: NUnit's
TestContext.CurrentContext does not flow onto async continuations, so a capture
after an await would otherwise collide under the ad-hoc context. And when
rendering is off the whole call is a true no-op -- not even a dispatcher pump --
so instrumenting a test can never perturb the navigation/tab timing it asserts
on. Full headless suite stays green.
Nearly every headless UI test opened with the same four-line prologue
(resolve the shared MainWindow, show it, cast its DataContext, wait for the
assembly list), then repeated the corelib lookup, the EnsureLazyChildren +
Children.OfType<T>().Single() drill, the registry single-by-header lookups,
and the open-an-assembly-and-wait dance. The duplication made the intent of
each test hard to see and every signature tweak a suite-wide edit.
Collapse those into TestHarness (BootAsync, OpenAssemblyAsync, GetCommand,
GetEntry) and TreeNavigation extensions (FindCoreLib, GetChild<T>, Expand<T>),
then apply them across the suite. Net ~865 lines of boilerplate removed with
no change in behaviour; the full headless suite stays green.
DisplaySettings.SortResults was persisted but unused — RunningSearch hardcoded
ComparerByFitness. Now SearchPaneModel.RestartSearch reads the setting at
start-of-search and passes either ComparerByFitness (default) or ComparerByName
into RunningSearch. Capturing at start matches WPF — mid-run toggles only
affect the next search.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The user reported that expanding an enum (System.DayOfWeek) with a search
term active showed only Base/Derived nodes — every FieldTreeNode child
hidden. This is NOT a recent regression. It traces back to commit
45461ddde, which made LanguageSettings.SearchTermMatches honour the
SearchTerm and wired SearchPaneModel to push its term into
LanguageSettings on every keystroke.
The pane's watermark advertises a prefix DSL that the parser didn't
implement — only inassembly: and innamespace: were honoured. Typing
"t:String" treated the whole thing as a literal keyword and matched
nothing useful.
Ctrl+E / Ctrl+Shift+F previously activated the pane but left
keyboard focus wherever it was — the user still had to click the
TextBox before typing. Add a FocusRequested event on SearchPaneModel
that the view subscribes to and pushes Focus() on the SearchInput
TextBox through Dispatcher.UIThread.Post (a tick lets the freshly-
active pane surface in the layout so .Focus() actually takes).
Assisted-by: Claude:claude-opus-4-7:Claude Code
SearchPaneModel gains an Activate(SearchResult) method that resolves
result.Reference through AssemblyTreeModel.FindTreeNode and moves the
assembly-tree selection there — exactly the WPF NavigateToReferenceEventArgs
flow, minus the message-bus indirection. SearchPane.axaml.cs wires
DoubleTapped + Enter on the results ListBox to call Activate.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Flip LanguageSettings.SearchTerm from the always-empty placeholder
to a real ObservableProperty backing field, and replace the
always-true SearchTermMatches stub with a case-insensitive substring
contains. Empty term still matches everything (no filter active).
Assisted-by: Claude:claude-opus-4-7:Claude Code
Wire the search pane to the shared ICSharpCode.ILSpyX.Search engine.
Setting SearchTerm or SelectedSearchMode now cancels any in-flight
run and starts a fresh one; cleared term tears the run down.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replace the placeholder TextBlock with the real layout: a TextBox at
the top bound two-way to SearchPaneModel.SearchTerm (UpdateSourceTrigger
PropertyChanged so the orchestrator reacts on every keystroke once it
lands), a ComboBox bound to SearchModes / SelectedSearchMode, and a
ListBox that renders each SearchResult as three trimmed columns
(Name / Location / Assembly).
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replace the SearchPaneModel stub with the real shape that the pane's
view, the background streaming orchestrator, and the LanguageSettings
filter cascade will all bind against.
Assisted-by: Claude:claude-opus-4-7:Claude Code