TestPlugin was still WPF (net10.0-windows, UseWpf, System.Windows,
TomsToolbox, the old ICSharpCode.ILSpy namespace), so it could not load
into the cross-platform app. Re-target it to net10.0 and map each
extension point onto the port's contracts: the custom Language, About-page
addition, context-menu entry, main-menu/toolbar command, and an Avalonia
options page whose view the ViewLocator resolves by naming convention. The
command needs [ImportingConstructor] for System.Composition to satisfy its
service dependency. Reference it build-only from the tests (no DLL copied
into the test output) so a composition test can load it without the
headless app auto-loading the plugin into every other test.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A plugin whose assembly won't load, or an exported command whose
constructor can't be satisfied, previously either blanked the app into the
startup error window or threw mid-build and took the whole menu/toolbar
down with it. Collect such failures in a CompositionErrors sink instead,
skip the offending part, keep running, and show the errors in a document
tab via an ITextOutput once the window is up. Add a Composition log
category for opt-in file logging.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Rename Avalonia.Xaml.Behaviors to its successor Xaml.Behaviors.Avalonia
(12.0.0.1), and bump Dock (12.0.0.2), Svg.Controls.Skia.Avalonia,
AvaloniaUI.DiagnosticsSupport, CliWrap, coverlet.MTP, and NUnit to their
latest releases.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The document tab strip can now flow tabs onto multiple rows or stay a
single scrolling row; the mode is toggled by the mouse wheel over the
strip and persisted in SessionSettings. In single-line mode an overflow
dropdown at the strip's trailing edge lists every open document for quick
switching. Popups render in the window overlay layer (OverlayPopups) so
the dropdown's menu doesn't open as a native X11 child window that loses
focus and self-dismisses before it becomes visible.
Assisted-by: Claude:claude-opus-4-8:Claude Code
#3753: when many documents are open, the tab strip scrolls a single row,
hiding most tabs behind the overflow scroller.
Dock's DocumentTabStrip hardcodes a horizontal StackPanel inside a
PART_ScrollViewer in its theme template and ignores an ItemsPanel set via
a Style, so a MultiRowTabStripBehavior sets the WrapPanel ItemsPanel
directly and disables the scroll-viewer's horizontal scrolling, giving
the WrapPanel a bounded width so tabs wrap onto new rows. Enabled on the
document strip (not tool tabs) from App.axaml.
Drag-reorder across rows is not yet validated (Dock's drop-position logic
assumes a single horizontal strip), so this stays an opt-in prototype for
now rather than a closing fix for the issue.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Project/solution export, PDB generation, Create Diagram and Extract
Package all ran via RunWithCancellation on the active preview tab --
whose cancellation token tree-node navigation cancels. So selecting any
node while one of these ran killed it: you couldn't browse while an
export was in progress.
Add DockWorkspace.RunInNewTabAsync, which opens a new frozen document
tab (its own decompile/cancellation scope, never the navigation target),
runs the work there, and shows the report in it. Route the long-running
command sites through it. A frozen tab is immune to the selection-driven
re-decompile, so navigation can no longer cancel the work -- and the
report opens in its own tab instead of replacing the current view.
The single-node Save Code path intentionally stays on the active tab
(matches the prior version and is quick).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Right-click Save Code on a node without a Save() override (type,
namespace, member) did nothing: the context-menu entry only called
node.Save() and stopped when it returned false, unlike Ctrl+S which
falls through to a generic decompile-to-file save. The prior version
always had this fallback in both paths; the port dropped it from the
context entry.
Extract the shared "Save() else decompile the node to a single file"
flow into SaveCodeHelper and route both Ctrl+S and the context-menu
entry through it, targeting the clicked node. As before, the fallback
runs in the active decompiler tab with its progress/cancel overlay and
then shows a "decompilation complete" breadcrumb with an Open-folder
button; the default file name derives from the node's text.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Window > Debug Steps did nothing. Two bugs in ToolPaneMenuItem:
the getter treated a pane with a null Owner (one hidden by default and
never placed in the layout) as visible, so the menu showed it checked
and toggling tried to hide it; and the show branch used
factory.RestoreDockable, which only un-hides a previously-shown pane and
is a no-op for one that was never in the layout.
Report visibility from real layout membership, and show via
ShowToolPane, which materialises the pane and (re)creates its home dock.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The freeze work pointed Dock's DocumentContextMenu (which it shows on a
document-tab right-click, overriding the standard ContextMenu) at a menu
holding only "Freeze tab". That hid the Close / Close all but this /
Close all entries defined in App.axaml, and on a frozen tab — where the
Freeze item is hidden — the whole menu came up empty.
Build the full menu (Close / Close all but this / Close all, plus the
preview-only Freeze entry) in PreviewTabContextMenuBehavior and drop the
now-superseded App.axaml ContextMenu setter, so there is a single source
for the document-tab menu.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Selecting a list in the dialog only set SessionSettings.ActiveAssemblyList
directly, which merely persists the choice for the next launch — it never
reloaded the tree, so the active list appeared unchanged. Route selection
through AssemblyTreeModel.ActiveListName instead, whose setter reloads the
list (OnActiveListNameChanged -> ShowAssemblyList) and persists the choice.
Also make double-clicking an entry a shortcut for the Select button (both
go through the same activation path).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The metadata grid's double-tap handler activated grid.SelectedItem, so
rapidly clicking a column-header sort button registered as a double
click and navigated to the selected row. Resolve the row from the
double-tapped element instead and activate only when the double-click
actually landed on a data row.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The command always built a decompiler tab and force-decompiled the
node, so a metadata-table (or resource) node opened an empty code tab
instead of its grid. Route a single node through OpenNodeInNewTab, which
honours the node's CreateTab() custom content and falls back to a
decompiler tab for ordinary nodes; multi-node selections keep the
decompile-the-union behaviour.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The tree node's text leads with the table's token-kind byte (e.g.
"02 TypeDef (1234)"), but the opened tab's title dropped it and showed
just "TypeDef (1234)". Lead the tab title with the same byte so the two
match.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Jumping to a token (the "Go to token" entry and token-cell clicks) set
suppressHistoryRecording while selecting the target table, so the jump
left no back-stack entry and Back skipped over it. Stop suppressing so
the destination is recorded, and carry the target row on the entry
(TreeNodeEntry.MetadataRow) so Back/Forward restores the exact token
rather than the top of the table.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Commands and context-menu entries kick off async work as `_ = SomeAsync()`,
discarding the task. When one faulted, the failure only surfaced via
TaskScheduler.UnobservedTaskException after the GC finalized the dropped
task -- late, on the finalizer thread, and disconnected from the gesture
that caused it (a click would appear to do nothing, then a mysterious
crash report arrived seconds later).
Add a HandleExceptions() extension that observes such a task on the UI
context and routes any fault to the existing GlobalExceptionHandler at
the point of failure (treating cancellation as a normal outcome), and
route the user-invoked command/entry sites through it. The global
unhandled-exception net already existed; this just makes these failures
prompt and attributable instead of deferred to GC.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The quick Save Code path exports a project (one assembly) or a solution
(several) straight through a file picker, with no way to tune the
output. Add a dedicated, discoverable "Export Project/Solution..." entry
(File menu + assembly context menu, alongside Save Code) that opens a
configuration dialog: pick the output folder, preview the projects that
will be written (with invalid / duplicate-name / PDB-eligible badges),
toggle project-format and decompiler options, optionally sign with a
strong-name key, and optionally emit a portable PDB per assembly --
defaulting source-embedding off since the project's .cs are on disk.
The export runs on a clone of the live settings (never the persisted
instance) behind the tab's cancellable progress UI and reports into the
active decompiler tab. StrongNameKeyFile now flows through
DecompilationOptions into the project writer, which had no reachable
setter before. The engine (ProjectExporter) and the preview computation
are split out from the window so they are headless-testable.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The portable-PDB writer carried its knobs (no-logo, pdb id, progress,
progress title) as a growing list of optional WritePdb parameters.
Turn the type into a configured instance whose options are properties,
and add EmbedSourceFiles (default true): when a PDB is generated next
to a project export whose .cs are already on disk, embedding the source
again is redundant, so the caller can turn it off. The per-document
checksum/hash is computed either way, so documents still resolve.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The single-assembly project save was already wired, but selecting
multiple assemblies and choosing Save Code only ever saved one of
them: SolutionWriter had never been ported, so there was no path that
emitted a .sln with one decompiled project per assembly.
Port SolutionWriter UI-agnostically and route both Save Code surfaces
(the context-menu entry and the Ctrl+S command) through a shared
SolutionExport helper that recognises a multi-assembly selection,
prompts for the .sln path, runs the export behind the tab's cancellable
progress UI, and reports the result into the active decompiler tab
(matching how the other long-running commands surface their output).
Unlike the prior app, the project-export path writes the .csproj to
disk itself and only returns a breadcrumb through the text output, so
each project's on-disk file is located after decompiling rather than
captured from the output stream.
Assisted-by: Claude:claude-opus-4-8:Claude Code
These PackageVersion entries had no PackageReference anywhere in the tree -- WPF-era leftovers (TomsToolbox.Wpf.*, Microsoft.Xaml.Behaviors.Wpf, AvalonEdit, Dirkster.AvalonDock, DataGridExtensions), unused theme/diagnostics variants (Avalonia/Dock Fluent themes, Avalonia.Diagnostics, Fonts.Inter), the VSTest SDK (the suite runs on Microsoft.Testing.Platform), and bare System.* versions that pin nothing under central package management. Restore leaves every packages.lock.json unchanged, confirming none were in use.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Finishes the decompiler text-view context menu: the remaining tree-only entries now also act on a right-clicked symbol (IEntity). Decompile to new tab opens the entity's definition in a fresh tab -- which required honouring the previously-dead InNewTabPage flag on NavigateToReferenceEventArgs (OnNavigateToReference now opens the resolved node via OpenNodeInNewTab). Scope search to assembly/namespace read the entity's ParentModule.AssemblyName / Namespace, matching the existing inassembly:/innamespace: filters.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The decompiler text-view context menu was missing the navigation and folding entries the previous version had. Add Decompile (go to definition) on a right-clicked symbol -- it navigates the tree to the entity's definition via the existing NavigateToReference bus message -- and Toggle folding / Toggle all folding, which reuse the editor's existing Ctrl+M logic (extracted into public DecompilerTextView methods). The folding entries only show when the document has foldings.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Avalonia has no global ICommand re-query signal like WPF's CommandManager.RequerySuggested, so state-dependent main-menu commands were frozen at their startup CanExecute: a NativeMenuItem only re-reads CanExecute when the command raises CanExecuteChanged, and nothing did. The assembly list is empty when the menu is built, so Clear assembly list was disabled forever; Save and Remove-assemblies-with-load-errors had the same latent bug. Add a weak global CommandManager (held weakly so menu/toolbar items aren't pinned) that SimpleCommand routes CanExecuteChanged through -- exactly as WPF's SimpleCommand routed it -- and invalidate it at the central state-change points: tree selection, the active assembly list, and background-load-sweep completion (so load errors surface). One signal, every platform: NativeMenuItem maps CanExecute onto IsEnabled, which the macOS Cocoa, Linux DBus, and in-window menu exporters all track.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The decompiler text-view context menu only read SelectedTreeNodes, so Analyze never appeared when right-clicking a reference in the code. Read the resolved entity from context.Reference too (the same seam Show-in-Metadata uses), so right-clicking a type/member in the decompiled output offers Analyze and pushes it into the analyzer pane.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Right-clicking a document tab opened an empty menu. Add Close, Close all but this, and Close all, bound to commands on ContentTabPage that delegate to DockWorkspace. Close all now keeps the persistent preview/home tab (the One) rather than leaving whichever tab Dock's last-dockable veto happened to spare, so the app always retains a main tab to project the next selection onto.
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
TreeKeyboardController (and its ITreeKeyboardTarget interface) drove keyboard gestures on the old ProDataGrid trees, including a reflection workaround for the grid's shift-range selection that only added and never shrank a range. Both trees are SharpTreeView (ListBox) now -- which handles those gestures natively in SharpTreeView.OnKeyDown and gets anchor extend/shrink for free -- so nothing constructs the controller. Remove it and the now-vestigial interface implementations on the two tree view-models.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two implemented tree gestures had no coverage (flagged by a gesture audit): middle mouse button on a row opens it in a new document tab, and Ctrl+R analyzes the selected member into the analyzer pane.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Analyze dropped the entity's node into the tree collapsed, so the user had to expand it every time to see its analyzers (Used By, Uses, ...). Expand the freshly-added node on creation; re-analyzing an existing entity returns the same node without re-expanding it.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The startup welcome page renders the same About content in the reusable main tab. Help > About opened a second, static About tab beside it, so the user saw the page twice. Track the welcome content and, while it is still the live main-tab content, have Help > About activate it rather than open the singleton. Falls through to the singleton once a tree-node selection has replaced the welcome page.
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 Simple theme drew keyboard focus as a dotted, fill-less adorner that vanished outright once a context menu took focus, and right-click versus keyboard invocation marked the target inconsistently. Render focus as a selection-coloured fill with a dotted darker-blue border via a FocusAdornerTemplate, and reuse the same visual as a ContextTargetVisual template part for the right-click target. A keyboard-invoked menu (Shift+F10 / Apps) now marks the focused row and restores its focus and adorner on close, since Avalonia's ContextMenu never restores focus itself.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ScheduleBackgroundLoadSweep waited for TreeReady and then an extra unconditional
5s before populating sibling-assembly icons -- an artificial wall-clock delay on
top of the TreeReady gate. Fire the sweep as soon as the tree is on screen; the
TreeReady gate already keeps it off slow startups and the SemaphoreSlim(4)
throttle still bounds the load storm.
Reopening_About_Reuses_The_Same_Tab matched any tab whose Title is "About", but
two tabs can carry that title: the singleton menu-About (IsStaticContent: true,
opened via OpenSingletonTab) and the transient boot welcome page (ShowWelcome,
IsStaticContent: false, a non-static main-tab page shown when nothing is
restored). The welcome page's presence races with boot, so when it lingered the
test's .Single(IsAbout) saw two tabs and threw "Sequence contains more than one
matching element" -- a ~50% order-dependent flake.
The singleton is what the test means to verify, so match it precisely
(IsStaticContent: true). OpenSingletonTab/CloseDockable reuse was always sound;
this was purely an imprecise test predicate. Deterministic now (5/5 full-suite
runs green; was ~50%).
NodesInserted/NodesRemoved reported a multi-node change (a node plus its visible
descendants -- a contiguous run) as a sequence of single-item Add/Remove events,
but the underlying tree updates Count by the WHOLE run before they fire. A
consumer that reconciles the change one item at a time and re-indexes the source
mid-sequence then reads against a Count that has already dropped by the full run:
Avalonia's SelectionModel does exactly this (it re-reads SelectedItems while
handling each Remove), indexing past the end and throwing ArgumentOutOfRangeException
out of TreeFlattener's indexer whenever a selection was live during a bulk reshape
(e.g. toggling UseNestedNamespaceNodes). It only surfaced under full-suite timing,
which is why it read as flaky.
Raise one ranged event for the whole run instead, so the notification matches the
tree's state in a single step and Count/indices stay in agreement. The indexer
keeps throwing on genuine out-of-range access. Avalonia 12.4's virtualization and
selection handle ranged Add/Remove (every expand/collapse exercises it); full
headless suite is green.
A test that triggers a decompile spawns a Task.Run plus dispatcher continuations
and rarely awaits them; the per-test ResetAppState closed windows and drained the
dispatcher but never cancelled the background work, so a continuation could land
during the next test and read the swapped-in AppComposition.Current -- the
dominant source of order-dependent flakiness (a different test tipped each run).
Give DecompilerTabPageModel an internal CancelPendingAsync (store the in-flight
decompile task, cancel its CTS, return it) and DockWorkspace a
CancelPendingOperationsAsync that fans that across every document tab -- both
legitimate shutdown hygiene too. ResetAppState.AfterTest now cancels and pumps
the dispatcher until the work unwinds before the next test boots.
This stops the random rotation of the failing test (the victims are now stable
and diagnosable); two order-dependent failures remain (About-tab duplication and
a nested-namespace tree race) that need their own fixes -- they still pass in
isolation.
DecompilerTextView subscribed to the process-lived ThemeManager.Current.
ThemeChanged in its constructor and never unsubscribed, so every view (one per
decompiler tab) stayed rooted by the singleton for the lifetime of the process
-- a slow leak that, in the headless suite, accumulates views across every test
and adds GC pressure that tips timing-marginal tests. Bind the subscription to
the visual-tree lifetime instead (subscribe on attach, drop on detach); since
Dock hides and re-shows tab content, this also re-subscribes on re-attach, which
the sibling DecompilerTextEditor's subscribe-in-ctor variant does not.
Also quiet MenuIconWiringProbe: it dumped every menu leaf via TestContext.Out on
every run. Collect the leaves instead and name the icon-bearing ones only in the
assertion's failure message, so a passing run is silent.
The assembly-tree SharpTreeView migration (0995d32df, "WIP: tests pending")
left two bits of debt that surfaced as seven failing tests:
- It dropped the old AssemblyListPane.CenterRowInView, so a model-driven
selection only scrolled the row to the nearest viewport edge. Reinstate
centring in SharpTreeView.ScrollIntoNodeView via CenterNodeInView: bring the
row on screen, then offset so it sits at the vertical centre. It skips the
move only when the row is already roughly centred (not merely visible at an
edge), because the ListBox's AutoScrollToSelectedItem drags the selected row
to an edge first and a reveal should still pull it to the centre -- while a
click on an already-centred row doesn't twitch.
- DecompileInNewViewTests still queried the removed ProDataGrid surface
(DataGrid / DataGridRow / HierarchicalNode). Retarget to SharpTreeView /
SharpTreeViewItem and the node-valued SelectedItem, and clamp the right-click
X to the visible grid width (tree rows stretch to content width, so a row
centre can sit off-screen).
Full suite: the seven deterministic failures are gone; the only remaining two
are the pre-existing group-ordering-flaky pair (both pass in isolation).
On Linux desktops where the com.canonical.AppMenu.Registrar name is absent or
blinks in and out (e.g. KDE Plasma, where kappmenu owns it only transiently),
Avalonia's DBusMenuExporter fired an unguarded UnregisterWindowAsync on dispose.
The faulted fire-and-forget task went unobserved and was rethrown by the
finalizer as org.freedesktop.DBus.Error.ServiceUnknown ("The name is not
activatable"), surfacing through the global exception dialog. 12.0.4 observes
that task (AvaloniaUI/Avalonia#21344, fixing #17616); verified the ContinueWith
guard is present in the resolved Avalonia.FreeDesktop binary.
No lock-file churn: only the cross-platform decompiler libraries pin lock files
and none reference Avalonia. Full headless suite shows no new failures.
Sorting rebuilds every top-level assembly node (AssemblyList.Sort clears the
collection and re-adds it, so the tree tears down and recreates all nodes). That
dropped the selection entirely and snapped the list back to the top, so the user
watched the tree reshuffle out from under them. Capture the selected assemblies
before the sort and re-select them after, so the view settles on the same items
(the selection binder keeps the primary in view) instead of jumping to the top.
The Linux DBus failure (org.freedesktop.DBus.Error.ServiceUnknown) arrives as an
unobserved AggregateException rethrown by the finalizer thread, so it is
temporally divorced from the mouse/keyboard gesture that made Avalonia issue the
DBus call (AT-SPI, portals, clipboard, IME). To pin the trigger, add an
ILSPY_LOG=DBUSDEBUG category: InputDiagnostics logs every pointer/key/focus event
into a rolling ring buffer, and the unobserved-task handler dumps that trail next
to the fully unwrapped exception. The unwrapper flattens the aggregate, walks
each inner chain, and reflects DBusException.ErrorName/ErrorMessage (Tmds.DBus is
a transitive dependency we can't reference directly) -- detail the bare
ToString() buries. The exception dialog and clipboard report carry the unwrapped
chain too, so the DBus error name shows even without the category enabled.
A model selection set before the SharpTreeView is attached to the visual tree
(e.g. AnalyzerTreeViewModel.Analyze selects its node, then the pane is shown)
was lost: the ListBox isn't an initialised ItemsControl yet, so the binder's
SelectedItems add silently dropped. Nothing re-applied it, so the analyzed row
showed unselected and keyboard gestures had no current node.
Making SharpTreeView focusable (for first-press Ctrl+A) unmasked this: with the
tree itself taking focus instead of delegating to the selected row's container,
keyboard handlers fell back to SelectedItem, which was empty. Re-sync on the
tree's Loaded event so the selection sticks once the control is ready.
ILSpyTreeNode cached the SettingsService (and LanguageService) in a static
field that was never invalidated. In the headless suite the MEF container is
rebuilt per test, so from the second test onward the filter cascade
(ApplyFilterToChild / OnIsVisibleChanged) read a previous test's disposed
SettingsService and its stale ShowApiLevel. A test that set ShowApiLevel=All
on its own fresh settings then expanded a large type still had ~a third of the
members marked IsHidden against the stale level, so the flattener dropped them
and the rows never realised -- surfacing as an order-dependent timeout
(NonPublic_Member_Rows_Render and the in-place refilter test).
GetSuffixString already resolves the service fresh for exactly this reason;
apply the same to CurrentLanguageSettings and LanguageService and drop the dead
caches. A warm GetExport is a dictionary lookup, and in production (one
container for the app lifetime) behaviour is unchanged.
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.
WPF-parity reusable drag-drop: SharpTreeView now owns the drag gesture, the Before/Inside/After hit-testing, and the insert-marker (an AdornerLayer line), and delegates the actual drop to the target SharpTreeNode.CanDrop/Drop via thin Avalonia IPlatform adapters (AvaloniaDataObject / AvaloniaPlatformDragEventArgs). The drag start uses node.CanDrag + node.Copy for the payload but is orchestrated by the control (Avalonia's DnD is async/pointer-based, unlike WPF's synchronous IPlatformDragDrop).
AssemblyTreeNode gains CanDrag + Copy (drag the assemblies' file paths); AssemblyListTreeNode gains CanDrop/Drop that opens (dedupes) + Moves to the drop index -- so reorder and external file-drop unify. Post-drop selection is a view concern, delegated back to the pane via SelectAssembliesAfterDrop. AssemblyListPane sheds all its drag/file-drop code. Reorder + file-drop tests now drive the node contract directly.
Both tree panes had near-identical model<->tree selection sync; that's now one reusable TreeSelectionBinder(tree, model.SelectedItems) wiring both directions (tree->model mirror + model->tree reveal/focus, with the off-list guard) -- the analyzer and assembly panes just construct and dispose one. Adds the drag-reorder drop indicator: an accent line at the target row's Before/After edge, positioned during a reorder DragOver.
Ports assembly reorder onto the ListBox-based tree using Avalonia's DragDrop pipeline: a left-drag off a top-level assembly row starts DragDrop.DoDragDropAsync carrying a marker DataFormat (the dragged set is held in a field -- it's an internal move), DragOver/Drop dispatch reorder vs. file-drop on that marker, and the reorder itself goes through AssemblyList.Move via the new CanReorder/ReorderAssemblies methods (same validation as the old AssemblyRowDropHandler: top-level non-package assemblies only, never Inside/onto-self). The reorder tests now drive CanReorder/ReorderAssemblies directly instead of ProDataGrid's RowDropHandler, and are no longer [Ignore]d (4 passing).
Not yet ported: the drag insert-marker line (drop feedback is the Move cursor for now).
Replaces the ProDataGrid hierarchical tree in AssemblyListPane with the new ListBox-based SharpTreeView: Tree.Root binds to the model root, selection mirrors at the SharpTreeNode level (deleting the ~186 LOC HierarchicalModel sync + the TreeKeyboardController reflection workaround), the Thunderbird context-target / MMB-new-tab / Delete / Ctrl+R paths port over, file-drop is preserved, and the API-level filter re-applies in place via ILSpyTreeNode.RefreshRealizedFilter (the model self-filters into IsHidden; the flattener drops hidden nodes).
Styling to match the classic tree: flush list (no ListBox padding/border), exact WPF +/- expander box, 20px rows, gray dotted connector lines wired through the existing TreeLines control with an 18.5px indent step so a child's +/- box sits under the parent's icon and the line passes through it.
KNOWN GAP: assembly drag-reorder is not yet ported (AssemblyTreeDragReorderTests [Ignore]d, task #19) and ~20+ assembly headless tests still query the old DataGrid and need retargeting to SharpTreeView (task #20). Production builds green and the app runs; the test suite is red on those un-retargeted tests.
Groundwork for migrating the assembly tree onto SharpTreeView: the shared item theme now binds the autoloaded / non-public foreground classes (dimmed unless the row is selected), and ILSpyTreeNode gains RefreshRealizedFilter to re-apply the API-level filter to realised nodes in place when ShowApiLevel toggles -- the TreeFlattener then drops anything newly hidden, replacing ProDataGrid's ChildrenSelector filter.
Replaces the ProDataGrid + HierarchicalModel bridge and the ~57 LOC HierarchicalNode selection-sync in AnalyzerTreeView with the new control: the view-model's Root binds straight to SharpTreeView.Root and selection mirrors at the SharpTreeNode level (no FindNode/unwrap). Double-tap navigation now flows through SharpTreeView.OnDoubleTapped (node.ActivateItem first, expand only if unhandled), so the analyzer entity rows still navigate. TextViewContext.TreeGrid is widened from DataGrid? to Control? so both the (not-yet-migrated) assembly DataGrid and the SharpTreeView satisfy it. All 150 analyzer tests pass.
OnTextInput drives incremental prefix search over the visible (flattened) nodes with a 1s idle reset -- a fresh keystroke advances past the current row, a growing prefix refines forward. A regression test confirms the payoff of the ListBox base: SelectionMode.Multiple extends AND shrinks a shift-range from the anchor (Shift+Down x2 then Shift+Up -> A,B), so the migrated control needs none of the ProDataGrid reflection workaround.