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
SharpTreeView is a ListBox with AutoScrollToSelectedItem left at its default
(true), so it chased the selected row whenever its index shifted: expanding an
unrelated node pushed the selection off-screen and the ListBox yanked the
viewport back to it, fighting the expand's own reveal -- the "weird scrolling".
The rule is that a user mutating a control directly should not have the app
mutate the view too; only navigation from a *different* control (search
results, code/metadata links, analyzer nodes, Back/forward) may sync the tree.
The app already does that sync explicitly through the model
(TreeSelectionBinder -> CenterNodeInView), so the ListBox's own auto-scroll is
redundant and wrong for in-tree actions. Disable it.
Assisted-by: Claude:claude-opus-4-8:Claude Code
SharpTreeView.SelectedItems is IList?, so reading .Count on the realised grid raised CS8602. Null-forgive the dereferences in the Ctrl+A and plain-click selection tests, where the grid is always realised.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Center-on-select reveals a navigated-to node centred in the viewport, but it was also yanking rows the user can already see: opening a visible row in a new tab (or any model-driven selection) pulled it to the middle. The skip cannot be decided from post-selection geometry because AutoScrollToSelectedItem first drags an off-screen row to an edge, making it look visible. Snapshot visibility in TreeSelectionBinder.SyncModelToTree before the selection changes: an already-visible primary is focused without scrolling, while a genuinely off-screen one (Back navigation, go-to-definition) is still centred.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The assembly/analyzer panes now host SharpTreeView, but ~20 headless tests
still queried the old ProDataGrid surface (DataGrid/DataGridRow/Hierarchical
Model/HierarchicalNode), so they could not exercise the live tree. Retarget
them to SharpTreeView/SharpTreeViewItem and the flattener (ItemsSource) the
control actually exposes, and centralise the row/selection lookups in
TreeNodeAssertions so the scroll assertions ride the new container type.
Driving the retargeted tests against the real control surfaced four genuine
gaps, fixed here in SharpTreeView:
- Ctrl+A selected nothing on the first press because the base ListBox only
selects all once an item inside it is focused; handle it explicitly and make
the control itself focusable so the gesture works the moment the pane gains
focus.
- Left/Right navigation focused the parent/first-child container but never
moved the selection (selection must follow the caret); add SelectAndFocus.
- The expander toggle had no stable name; name it PART_Expander so hit-target
assertions can find it, and align the test to the shipped 13px column (kept
at 13 so the +/- box centres on the connector lines).
- Tree rows stretch to content width with horizontal scroll, so a row centre
can sit past the viewport's right edge; the pointer tests now click within
the visible grid width instead of the off-screen row centre.
The UseNestedNamespaceNodes re-bind test asserted a ProDataGrid Hierarchical
Model swap that no longer exists; rewrite it to assert the live flattener
reshapes the visible rows in place when the setting toggles.
Extending a tree selection with Shift+Down then pressing Shift+Up didn't shrink the range back toward the anchor -- the dropped rows stayed selected, and in the middle of a list the anchor drifted so the range crept the wrong way. Root cause is upstream in ProDataGrid: a shift+nav key moves the grid's current row and anchor correctly, but SelectFromAnchorToCurrent calls SetRowsSelection(start,end), which only ADDS the range and never deselects rows outside it, so a shrink is a no-op. After the grid processes a shift+nav key, TreeKeyboardController prunes the selection to exactly the [anchor..current] range. It deselects out-of-range rows via the internal SetRowSelection(slot, isSelected:false, setAnchorSlot:false) rather than SelectedItems.Remove: removing an item makes the grid re-derive its current row and corrupt the anchor for the next key, whereas SetRowSelection deselects a slot in place and leaves current/anchor intact. The slot members are read reflectively and defensively (falls back to the pre-fix behaviour if a future ProDataGrid renames them). Both the assembly and analyzer trees get it via the shared controller.
Extract the standard tree keyboard gestures -- Left/Right expand-collapse + parent/child nav, numpad +/-/*, and new type-ahead incremental search -- out of AssemblyListPane into a reusable TreeKeyboardController that drives any hierarchical DataGrid. The model implements ITreeKeyboardTarget (PrimarySelectedNode + SelectNode) so each tree keeps its own select-and-reveal behaviour; expansion goes through the grid's IHierarchicalModel. Both the assembly tree and the analyzer tree now attach one, so they share consistent keyboard behaviour. Delete (unload) and Ctrl+R (analyze) stay assembly-specific in the pane.
Numpad + expands the focused node, - collapses it, and * expands it recursively. The recursive walk only descends into children whose SharpTreeNode.CanExpandRecursively is true, which is false for lazy-loading nodes -- so * stays bounded and won't try to materialise an entire assembly. Mirrors WPF SharpTreeView.
Standard tree keyboard navigation in the assembly list: Left collapses an expanded node or moves to the parent when already collapsed; Right expands a closed node or steps into its first child. Operates on the primary selection via the hierarchical model (FindNode/Expand/Collapse) and SelectNode; the parent step is gated on the parent being a visible row so it never selects the hidden root.
Pressing Delete unloaded the assembly but left the grid showing a row selected while model.SelectedItems went empty -- so a second Delete read an empty selection and no-op'd ("spamming Delete breaks"). Record the deleted node's flattened index and, once the visible tree rebuilds, re-select the node that now occupies that slot (clamped to the new end) via SelectNode, which re-syncs grid and model. Selects nothing when the list empties.
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.
The command resolves each selected assembly's references through the
per-assembly resolver, which adds the targets to the live list as
auto-loaded entries. It then finished with a full list Refresh (F5),
whose LoadList rebuilds the list from persisted state -- which never
contains on-demand auto-loaded assemblies -- so the just-resolved
dependencies were discarded the instant they were added and the command
looked like a no-op. Re-decompile the active view instead (the original
behaviour) without reloading the list. F5 still drops auto-loaded
assemblies by design; the two refresh paths must stay distinct.
Two selection-sync defects in the assembly tree. Ctrl+A only selected
the last row on the first press: SelectedItem is the last entry of
SelectedItems and every collection change re-raised it, so the grid->model
sync bounced back through SyncSelectionFromModel and assigned the singular
SelectedItem, collapsing the just-made multi-selection. The syncingSelection
guard now also covers that notification.
Clicking one row of a multi-selection left every row selected: ProDataGrid
keeps the selection on press so a row-drag can move all of them (multi-row
reorder is a real feature), but 12.0.0 has no release-side collapse for a
plain click. Mirror the usual behaviour by collapsing to the clicked row on
release when the pointer did not move far enough to be a drag.
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 PE TypeDef table includes a <Module> pseudo-type plus any user-declared
top-level types in the empty namespace. Until now AssemblyTreeNode skipped
the empty namespace when building NamespaceTreeNodes and instead hung the
global types directly off the assembly node, breaking the long-standing
tree shape (every type lives under a namespace folder).
Audit had ExitCommand listed as Missing because it grepped for a separate
ExitCommand.cs file (the WPF layout). Avalonia bundles File-menu commands
into FileCommands.cs so the audit missed it. Command was already implemented
correctly — uses IClassicDesktopStyleApplicationLifetime.Shutdown() rather
than mainWindow.Close() (cleaner since Avalonia apps can have multiple
windows).
Assisted-by: Claude:claude-opus-4-7:Claude Code
FindMemberNode's IMethod branch only checked direct MethodTreeNode children of
the type. Accessor methods (get_/set_/add_/remove_/invoke_) live as children of
their owning PropertyTreeNode / EventTreeNode, so the lookup always returned
null — and MMB on a metadata-grid accessor row silently no-opped because
OpenNodeInNewTab couldn't resolve the IMethod to a tree node.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors WPF's mechanism: ILSpyTreeNode.OnChildrenChanged calls ApplyFilterToChild
on every newly-added child while the parent IsVisible, writing the Filter result
into SharpTreeNode.IsHidden. Because SharpTreeNode.IsVisible defaults to true,
the cascade fires while a node's children are added in its constructor — so a
PropertyTreeNode's get_/set_ accessors are stamped IsHidden=true under the
default ShowApiLevel before the property even attaches to its type. The base
SharpTreeNode.ShowExpander then naturally returns false ("Children.Any(!isHidden)"
sees no visible children), no per-node override needed.
Assisted-by: Claude:claude-opus-4-7:Claude Code
PropertyTreeNode now eagerly adds Getter / Setter as MethodTreeNode children
(when present); EventTreeNode does the same for Add / Remove / Invoke
accessors. The ShowExpander => false overrides drop so the base default
(Children.Any) lights up the chevron when accessors exist.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each ContentTabPage now carries the SharpTreeNode it represents (SourceNode),
stamped at construction time so the activation event fires with it already
populated. DockWorkspace subscribes to factory.ActiveDockableChanged: when a
new tab becomes active — whether by MMB carve-out, the "Decompile to new tab"
context-menu entry, or the user clicking a different tab in the strip — the
assembly-tree selection is pulled across to that tab's SourceNode. A guard
prevents the change from cascading back into ShowSelectedNode and overwriting
MainTab's content. Existing single-tab reuse path stamps MainTab.SourceNode
on every selection so flipping back to it stays a no-op.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The new-tab path always built a DecompilerTabPageModel, so MMB / context-menu
on a metadata-table tree node forced the node into a decompiler tab and a second
metadata view couldn't coexist with the first. Move the carve-out into a single
DockWorkspace.OpenNodeInNewTab that asks node.CreateTab for custom content
(metadata tables, resource viewers, …) — wiring the same NavigateToCellRequested
/ RowActivated subscriptions AttachCustomContent applies on the reuse path —
and only falls back to spawning a fresh decompiler tab when the node has no
custom page-type. The assembly-tree MMB handler, the metadata-grid MMB handler,
and the "Decompile to new tab" context-menu entry now all delegate to this one
method.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Wires MMB on the assembly tree and the metadata grid to the existing "open in
new tab" path, matching the WPF gesture (DecompileInNewViewCommand carries
InputGestureText = "MMB"). Plain double-click reverts to reusing the active tab;
the unreliable Shift+double-click variant is dropped.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replaces the brittle root.GetVisualDescendants().OfType<T>().Single() / .First()
pattern with a new WaitForComponent<T>() extension that polls until the requested
control is in the visual tree, then returns it. Avalonia.Headless tests routinely
queried the visual tree before lazily-templated panes (DataGrid, dock content)
had materialised, surfacing as intermittent 'Sequence contains no elements'
failures across the suite.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each tree-expansion, selection change, command execution, async wait and
key/mouse injection inside an [AvaloniaTest] now carries a short imperative
comment on the line above (e.g. "// expand typeNode", "// select methodA",
"// wait for assemblies to load", "// execute aboutCmd"). The comments are
the same scaffolding the manual debug-with-screenshot workflow uses to
follow what's happening at each breakpoint, surfaced into the committed
source so the tests are readable without the breakpoint markers attached.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Adds a leading paragraph describing what each test verifies and Arrange /
Act / Assert markers (split into numbered phases for multi-step tests) so
the intent of each fixture is readable from comments alone.
Assisted-by: Claude:claude-opus-4-7:Claude Code
ProDataGrid's HierarchicalModel kept its own per-wrapper expanded state and
ignored SharpTreeNode.IsExpanded — toggling the model property had no visible
effect, and clicking the chevron in the grid didn't update the model either.
Set HierarchicalOptions.IsExpandedPropertyPath = "IsExpanded" so ProDataGrid
reads/writes the property via reflection and observes INotifyPropertyChanged
on the source. Production code stays a one-liner; the imperative
NodeExpanded/PropertyChanged plumbing first attempt would have been ~80 lines.
Test exercises the model→grid direction.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Both commands have correct implementations since the WPF main-menu port; this
locks the behaviour in. Clear: with a populated list, CanExecute is true and
Execute drains the list to zero. Remove-load-errors: drops a synthetic non-PE
file into the list, waits for HasLoadError (GetLoadResultAsync rethrows the
underlying BadImageFormatException so it isn't a usable wait probe), invokes
the command, asserts the broken entry is unloaded while every previously-valid
entry is preserved. Both confirmed red by temporarily no-op'ing Execute and
green again after restoring.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors the WPF SharpTreeView's ApplicationCommands.Delete handling:
KeyDown on the tree DataGrid scoops up every selected AssemblyTreeNode and
calls AssemblyList.Unload on each. Snapshots the list before mutating because
Unload triggers CollectionChanged which clears the model selection mid-loop.
Non-assembly selections (types, methods, namespaces, …) are ignored — Del is
specifically for top-level entries today; richer per-node Delete semantics
can come with the broader IPlatformDataObject port. Test pins the dispatch.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors WPF SaveCodeContextMenuEntry.Execute: when a single tree node is
selected, call its Save() override first (resource nodes already self-handle
the dialog + raw/ResX choice). Otherwise prompt for a path with the active
language's extension and re-decompile the node with FullDecompilation = true,
EscapeInvalidIdentifiers = true, writing the output as plain text via
ICSharpCode.Decompiler.PlainTextOutput on a background task.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Clicking an already-visible row was running ScrollIntoView+CenterRowInView via
the SelectionChanged → model PropertyChanged round-trip, yanking the viewport.
Add a clickInProgress flag separate from syncingSelection: clickInProgress is
set in OnTreeGridSelectionChanged and cleared at Background priority alongside
syncingSelection, so the model's two PropertyChanged notifications (Clear+Add
on SelectedItems) both bail out — but hyperlink / Back/Forward / search-hit
navigation, which never enters OnTreeGridSelectionChanged, still scrolls the
target into view.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Five new IResourceNodeFactory implementations route .xml/.xsd/.xslt to a shared
XmlResourceEntryNode (text + XML highlighting via SyntaxExtensionOverride on the
output), .xaml to XamlResourceEntryNode (same pattern), and .png/.gif/.bmp/.jpg /
.ico / .cur to image-rendering nodes that inline the bitmap with a Save button.
SyntaxExtensionOverride is read after Decompile to switch the editor's highlighter
without changing the active language. Cursor handling flips byte 2 of the on-disk
header so Skia decodes the .cur as an icon for preview while Save preserves the
original bytes. ResourceFactoryTests pin the dispatch — including .xsd — and the
existing assembly-tree assertion is relaxed since typed entries are no longer all
ResourceTreeNodes.
Assisted-by: Claude:claude-opus-4-7:Claude Code