Clicking a reference left AvaloniaEdit's selection-drag mode stuck, so a
subsequent mouse move (with no button held) extended a selection.
AvaloniaEdit's SelectionMouseHandler captures the pointer and enters
selection mode on press, and extends the selection on every move while that
mode is active -- it keys off the mode, not whether a button is down. Only
its bubble-phase PointerReleased handler resets the mode and releases the
capture, and that handler early-returns when the event is already handled.
The reference-click handler ran in the tunnel phase and marked the release
handled before AvaloniaEdit saw it, so AvaloniaEdit's cleanup never ran.
Move the release handler to the bubble phase so it runs after AvaloniaEdit
has reset its state. AvaloniaEdit subscribes from the TextArea constructor,
before this view attaches its handlers, so the ordering is deterministic and
navigation can stay synchronous.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Navigating to a target on startup first eagerly loads every relevant
assembly's metadata so the entity search that follows can resolve it.
That pre-load used the throwing GetMetadataFileAsync, so a restored
session that still referenced an assembly whose file had since been
deleted or moved crashed startup with an unhandled
DirectoryNotFoundException instead of simply skipping the gone entry.
Use GetMetadataFileOrNullAsync there: a missing or unreadable assembly
now resolves to null and is skipped, which the entity search already
tolerates (it uses the OrNull variant too).
Assisted-by: Claude:claude-opus-4-8:Claude Code
ILSpy showed the current location only implicitly in the tree selection and kept search in a separate docked pane. The omnibar, modelled on the Files community app and jiripolasek's EditorBar, puts an address-bar atop each decompiler document: a breadcrumb of the node (Assembly > Namespace > Type > Member) whose segments navigate and whose chevrons list child nodes, turning into a search box on typing that reuses the existing search engine. It coexists with the docked search pane and ships off by default behind the Options / Display 'Tab options' EnableOmnibar toggle, which applies live.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The first CI run on a macOS runner surfaced that these tests encoded
the Windows/Linux menu shape, while MainMenu.Attach intentionally
diverges on macOS: TranslateGesturesForMacOS rewrites Control to Meta
so shortcuts follow the Cmd-key convention, and PromoteHelpToMacAppMenu
moves the Help items into the application menu and drops the _Help
top-level. The tests now assert the macOS-correct expectations on
macOS, including that the Help content lands in the app menu rather
than vanishing.
Assisted-by: Claude:claude-fable-5:Claude Code
WPF revealed files through the shell COM API (SHOpenFolderAndSelectItems):
selecting several assemblies and choosing "Open Containing Folder" opened
one Explorer window per folder with all of that folder's files selected,
reusing a window already open at the location. The Avalonia port replaced
this with explorer.exe /select, invoked once per file, so revealing N
assemblies spawned N new windows and a single reveal never reused an
existing one.
Restore the shell-COM reveal on Windows behind the existing cross-platform
ShellHelper, grouping the selection by containing folder. macOS keeps a
single Finder "open -R" for the whole selection; Linux, lacking a portable
select-item hook, opens each distinct parent folder once instead of one
window per file. The folder grouping is a pure, unit-tested seam so the
behaviour is verified without launching the OS file manager.
The Windows COM portion is adapted from the pre-Avalonia ShellHelper; its
author's copyright notice is retained per its MIT license.
Assisted-by: Claude:claude-opus-4-8:Claude Code
LoadedAssembly caches one type system per TypeSystemOptions value, but
the consumers disagreed on which one to use: tree nodes and the
metadata navigator resolved through GetTypeSystemOrNull (a separate
default-options instance) and the search built its request with default
settings. Each module could therefore materialise several type systems,
and none of the display surfaces respected settings that change entity
shapes (nullability, tuples, extension methods, ...). WPF routed all of
these through GetTypeSystemWithCurrentOptionsOrNull.
Reintroduce that helper on top of CreateEffectiveDecompilerSettings and
use it in TypeTreeNode, AssemblyReferenceTreeNode, and the metadata
navigator; the search request now derives its settings the same way, so
all of them hit the same cached instance. NamespaceTreeNode.Decompile
enumerates through the type system matching the decompilation options
it was handed.
Assisted-by: Claude:claude-fable-5:Claude Code
The Avalonia port gave DecompilationOptions a parameterless constructor
that silently defaults to new DecompilerSettings(). Several paths picked
it up and decompiled with default settings where the WPF version used
the user's current options: tree member filtering (CSharpLanguage.
ShowMember), PDB generation, the single-file / project / solution Save
Code paths, and the DEBUG decompile-all commands.
Promote the live-snapshot logic that was private to DecompilerTabPage-
Model (settings clone + Display-option bridge + toolbar language
version) to SettingsService.CreateEffectiveDecompilerSettings and use
it at every entry point. Remove the parameterless DecompilationOptions
constructor and make SolutionWriter require settings, so reaching for
defaults is an explicit choice rather than a silent fallback - that
default is exactly what masked these regressions. Search deliberately
keeps default settings (it only needs a type system to materialise).
Assisted-by: Claude:claude-fable-5: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
Windows maps CON, PRN, AUX, NUL, COM1-9 and LPT1-9 to devices -- on many
builds even with an extension appended, so a type named Con made both
whole-project export and the save dialog fail with IOException '\\.\Con'.
CleanUpName only checked for reserved names after re-appending the file
extension, where they never match, and the save-dialog default-name
helpers did not check them at all. The escape appends the underscore to
the base name (con_.txt, not con.txt_) because device-name parsing
ignores everything after the first dot, and is applied per path segment
so reserved directory names produced by namespaces are covered too. The
ILSpy.Tests.Windows fixture verifies on a real Windows filesystem that
the escaped names are creatable.
Assisted-by: Claude:claude-fable-5:Claude Code
Reference links navigated on pointer-press, which meant a press-drag
over a link could never select its text -- the press fired navigation
and handled the event before a selection could start. Move navigation
to pointer-release and only follow the link when the pointer did not
drag past the click threshold, matching the WPF view's mouse-up
handling. A press-drag now selects link text like any other span.
Assisted-by: Claude:claude-fable-5:Claude Code
Selecting text inside the popup closed it: the selection drag captures
the pointer, so sweeping past the popup edge fired the content's
PointerExited with IsPointerOver already false. The close veto now
covers pointer-over, keyboard-focus-within (so a finished selection
survives for Ctrl+C, as the WPF tooltip did), and pointer capture held
inside the popup; only a document change force-closes.
Assisted-by: Claude:claude-fable-5:Claude Code
Doc-comment crefs in hover tooltips rendered as link-styled but inert
text, and cref resolution was never wired up. Crefs now resolve against
the current tab's assembly list and navigate through the same
NavigateToReferenceEventArgs channel the analyzer uses; href links open
via the TopLevel launcher. Avalonia inlines are not input elements, so
links are hit-testable TextBlocks embedded via InlineUIContainer.
Interactive content also requires the popup to survive the pointer
travelling onto it: the editor's exit handler now tolerates the
overlay-popup enter/exit ordering via the distance corridor, and the
popup closes when the pointer leaves its own content instead.
Assisted-by: Claude:claude-fable-5:Claude Code
The WPF app showed the disassembled IL header when hovering a member
reference in IL view, and the opcode's XML documentation when hovering
an instruction. The Avalonia port lost both: ILLanguage fell back to
the base ambience one-liner and the opcode tooltip carried only the
name and encoding. Opcode docs additionally need a modern-.NET source:
the WPF code read the .NET Framework reference-assembly docs, which do
not exist on modern .NET, so MscorlibDocumentation now falls back to
the ref pack parallel to the hosting runtime.
Assisted-by: Claude:claude-fable-5:Claude Code
When the current language is not an IDebugStepProvider (e.g. the IL
disassembler), the Debug Steps pane kept displaying the previous
language's step tree, and its commands still triggered re-decompiles
with a step limit the language ignores. Detaching now clears the tree
and the view shows a "not available" note until a step-providing
language is selected again.
Assisted-by: Claude:claude-fable-5:Claude Code
Two failure modes that both showed the user the "no" cursor with no
other feedback:
1. ResolveDropTarget returned null when e.Source had no
SharpTreeViewItem ancestor, so Explorer drops landing in the gap
beneath the last row (or onto an empty list) never reached
SharpTreeNode.InternalDrop.
2. The middle 50% of a row produces a DropPlace.Inside target on the
row's own node. Most concrete SharpTreeNodes inherit the base CanDrop
(returns false), so a literal "drop a .dll onto an assembly row"
refused even though AssemblyListTreeNode happily accepts that payload.
For (1), fall back to (Root, Children.Count, DropPlace.Inside) when no
row is hit. For (2), introduce PickAcceptingTarget which retries with
the empty-space (root) target when the initial CanDrop is false. Both
OnDragOver and OnDrop go through the same picker so the cursor and the
actual drop agree on the outcome. The retry skips when the initial
target already IS the root, so a real refusal still surfaces as "no".
The marker adorner stays hidden for empty-space drops because the
place is always Inside (Item is null for that case); the existing
DropPlace.Inside early return covers it.
DropTarget, DropPlace, and the two new helpers are internal-visible to
the ILSpy.Tests project for the unit tests that assert the fallback
chain.
Assisted-by: Claude:claude-opus-4-7[1m]:Claude Code
Frozen tabs each cache their own decompiled output, but the language
change handler only refreshed ActiveDecompilerTab — hard-wired to the
preview tab — and the display-setting refresh only reached the preview
tab's cached content model, so frozen (and floated) tabs kept showing
stale output until manually re-selected. The handler predates the
freeze feature, which introduced multiple sibling decompiler tabs
without widening it.
Both paths now iterate every decompiler tab across all document docks
(the walk CancelPendingOperationsAsync already used, extracted as
AllDecompilerTabs), and the language handler uses Redecompile instead
of the CurrentNode re-assignment hack.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Three follow-ups so the WPF-era ILSpy.xml survives a load-save-load
cycle through the Avalonia host without losing data the user expected
to keep.
Preserve unknown children verbatim. LoadFromXml only interprets a
known set of element names (KnownChildren) and stashes everything
else; SaveToXml re-emits the stash after the known section. The
AvalonDock <DockLayout> blob the WPF host writes is incompatible with
the Avalonia Dock host and we don't want to interpret it, but blank-
ing it on save would discard a WPF user's saved layout the first
time they launched the Avalonia build. The same stash future-proofs
older builds against fields added in newer ones.
Emit the legacy on-disk shape on save. WindowBounds is written as a
CSV body "L,T,W,H" (the WPF Rect TypeConverter format) instead of
Left/Top/Width/Height attributes, and ActiveTreeViewPath <Node>
values are \xNNNN-hex-escaped on write. With both directions of the
conversion in this file a file written by the Avalonia build is
still readable by an older ILSpy 10.x install, and the diff against
a pre-existing ILSpy.xml stays small during the transition.
Wire SelectedSearchMode and ActiveAutoLoadedAssembly. The WPF host
persisted both: the search-pane mode picker so the user's last
choice survives restarts, and the file path of the auto-loaded
(dependency) assembly the saved tree-path lands in so the restore
can re-open it before walking. The Avalonia code read both into
properties that nothing wrote to. Now SearchPaneModel restores
SelectedSearchMode on construction and writes back on change via
SettingsService, and AssemblyTreeModel walks the selection's
ancestor chain to find the owning AssemblyTreeNode, stores its
LoadedAssembly.FileName when IsAutoLoaded, and re-opens that file
(when it still exists) before RestoreSelectedPathAsync.
Assisted-by: Claude:claude-opus-4-7[1m]:Claude Code
The new Avalonia SessionSettings reads WindowBounds as Left/Top/Width/Height
attributes and reads ActiveTreeViewPath node values raw, but ILSpy 10.x wrote
WindowBounds as a CSV element body ("L,T,W,H", the Rect TypeConverter format)
and \xNNNN-hex-escaped every non-letter-or-digit char in each Node value (so
"TomsToolbox.Wpf" round-tripped through XML as "TomsToolbox\x002EWpf"). On
first launch against an existing ILSpy.xml that means the saved window
position resets to the default and the selected tree node never restores --
the escaped path can't match a live node's ToString().
Accept the CSV body when the WindowBounds element has no attributes, and
decode \xNNNN escapes in tree-view node values. Both fall back through the
existing ParseDouble defaults if a piece is missing, so a corrupted entry
won't crash startup.
Lock the shape in with tests against the actual section the WPF host emits,
plus a forward-compat pair confirming LoadFromXml ignores unknown children
(DockLayout, SelectedSearchMode, ActiveAutoLoadedAssembly) and SaveToXml
never echoes them back -- the AvalonDock schema doesn't translate to the
Avalonia Dock host and would otherwise persist forever as dead state.
Assisted-by: Claude:claude-opus-4-7[1m]:Claude Code
WPF showed a row Offset column and decoded the Kind GUID into one cell
combining heap offset, friendly name, and raw GUID; the Avalonia table
only carried the bare heap offsets. Offset follows the GetRowOffset
convention every other table uses. Kind is plain text rather than a
hex column plus tooltip so the per-column filter matches the friendly
names.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Selecting a row in the CustomDebugInformation table now previews its
Value blob beneath it, completing WPF row-details parity: state-machine
hoisted local scopes, compilation options, metadata references, and
tuple element names parse into typed sub-grid rows; source-link JSON
shows as text; everything else — unrecognized kinds, embedded source,
and malformed structured blobs — degrades to a hex dump, as in WPF.
The parser switches on the Kind GUID directly via KnownGuids; the
entry deliberately carries no decoded-kind state, leaving that to the
kind-column work that builds on this.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
WPF ILSpy used DataGrid row details in three places: the COFF and
Optional header views (flag-bit breakdown of the Characteristics /
DLL Characteristics words, permanently expanded) and the
CustomDebugInformation table (decoded Value blob, visible when
selected). The Avalonia port already carried the data — both header
nodes populate Entry.RowDetails and the column builder skips it — but
nothing displayed it.
ProDataGrid builds the row-details template once per details element
and recycles the built control across rows, swapping only the
DataContext, so a WPF-style template selector would go stale;
MetadataRowDetailsControl re-runs its content factory on every
DataContext change instead. Per-row initial visibility has no
SetDetailsVisibilityForItem equivalent and recycling resets
AreDetailsVisible, so the view re-derives it in LoadingRow. Row
activation now ignores gestures originating inside the details
presenter — selecting text in a details blob must not navigate away.
This lands the infrastructure plus the two header consumers; the
CustomDebugInformation details build on the EmbeddedSource decoding
work and follow with it. ConfigurePage on MetadataTableTreeNode is
the hook that consumer overrides.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Clicking inside the open flags filter popup could act on the
DataGridColumnHeader underneath: unhandled press/release pairs bubbled
out of the popup into the header (which treats them as a sort click
and flashes its pressed visual), and on X11 overlay popups the
light-dismiss machinery could even re-target a click's press directly
at the header while the popup stayed open.
The filter UI is now hosted in a Flyout attached to the funnel icon
instead of a hand-rolled Popup parked in the header's panel. Avalonia
positions Popup as the low-level primitive and recommends Flyout for
attached pickers: light dismiss, Escape-to-close, focus handling, and
theme-correct presenter chrome come built in (the old hard-coded white
background was also wrong in dark mode). The flyout content swallows
unhandled wheel and press/release events, since its internal popup is
still logically parented to the funnel inside the header.
A headless end-to-end regression test drives a real DataGrid with
overlay popup hosts (the X11 OverlayPopups=true configuration the app
runs with) and raw input through the funnel and every visible flyout
control, asserting the header never sorts and never receives an
unhandled press or release.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Opening a package from a feed previously meant downloading the .nupkg by
hand and using File > Open (#2313). The new File menu command searches any
public V3 feed (editable package-source list, persisted as an MRU in the
ILSpy settings), offers the latest 100 versions in a dropdown, and downloads
into the NuGet global packages folder so the cache is shared with every
other NuGet consumer on the machine and nothing is fetched twice. The cached
.nupkg is then opened exactly like the regular Open command. Feed access
sits behind INuGetFeedClient so the headless test suite covers search,
paging, version selection, download, cancellation, and error surfacing
without touching the network.
Assisted-by: Claude:claude-fable-5:Claude Code
ECMA-335 names the zero state of most mutually exclusive sub-ranges
(NotPublic, AutoLayout, Class, AnsiClass, PrivateScope, ReuseSlot, ...)
and the reflection enums declare those members, but the schema inferer
skipped zero-valued fields and synthesised a generic '(none)' entry
instead. Zero fits every mask, so the member is attributed to a group
by declaration order: ECMA-335 and the reflection enums declare each
mask directly followed by its members. Filter persistence stores
numeric values, so saved filters are unaffected by the label change.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
The schema inferer only walks the enum's declared fields, so the
ECMA-335 Forwarder bit (0x00200000, no member in
System.Reflection.TypeAttributes) was invisible to the per-column
flags filter even though the cell tooltip already shows it. An
explicit per-enum extras table appends it as an independent flag,
making forwarders filterable on the ExportedType table.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
0x00200000 is the ECMA-335 Forwarder bit (II.23.1.15), set on
ExportedType rows that forward a type to another assembly.
System.Reflection.TypeAttributes has no member for it, so the
enum-driven flag enumeration left it invisible in the Flags group.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Two Options tests selected the whole System.Linq.Enumerable type and then
awaited WaitForDecompiledTextAsync. That decompile takes >15 s in headless and
overran the 60 s wait on a loaded CI runner, so the suite flaked intermittently
(green in one run of a commit, red in another). The behaviours under test --
that a re-decompile setting refreshes the active tab, and that the refresh does
not steal focus from the Options tab -- fire on whatever the tab shows, so a
single small method exercises them just as well and decompiles near-instantly.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Selecting a C# version, switching to a version-less language (IL), then back to
C# left the toolbar's version ComboBox blank even though LanguageService had
restored CurrentVersion. The MVVM-toolkit setter runs OnCurrentLanguageChanged
(which assigns CurrentVersion, pushing it onto the bound SelectedItem) before it
raises PropertyChanged for CurrentLanguage (which repopulates ItemsSource from
the new language's versions). So SelectedItem is set against the previous
language's still-stale, empty list, rejected, and never re-pulled once
ItemsSource updates.
Re-assert the version combo's SelectedItem from the bound CurrentVersion after
each ItemsSource swap settles, keeping the model untouched.
Assisted-by: Claude:claude-opus-4-8:Claude Code
CaretHighlightAdornerTests waited on the wall clock (3s budget) for the
adorner's 1s real-time DispatcherTimer to remove it. Avalonia.Headless has no
simulated clock, so under a loaded CI runner the dispatcher-thread timer plus
the poll loop slipped past the tight budget and the test timed out
intermittently.
Extract the teardown the lifetime timer runs into Dismiss(); the test grabs the
live adorner and calls it directly, verifying the unregister half without any
timing dependency. Registration is still exercised through the real
OnReferenceClicked path, and production behaviour is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Toggling a display setting that affects decompiler output (folding, member or
using expansion, debug info, IL detail, indentation) routed through
ForceRefreshActiveTab, whose ShowSelectedNode re-projects the tree selection
and so activates the preview tab -- yanking the user off whatever tab they were
on (e.g. the live Options page they were editing).
Add RefreshDecompilerOutputInPlace, which re-decompiles the cached decompiler
content directly without re-projecting or activating anything, and route the
Redecompile display-setting reaction to it. F5 reload and dependency-load keep
using ForceRefreshActiveTab, where re-projecting the selection is intended.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Project/solution export (and other RunInNewTabAsync work) opened a tab with a
static title and a permanently indeterminate progress bar, even though the
whole-project decompiler already produces per-file DecompilationProgress that
was being dropped.
Wire that progress through: DecompilationOptions carries an
IProgress<DecompilationProgress> that DecompileAsProject sets on the
WholeProjectDecompiler; a new RunInNewTabAsync overload hands the work a
UI-thread-marshaled Progress<T> feeding the tab. The tab now animates the
braille spinner over its title (the operation name) and shows a determinate bar
plus the file currently being written and an "N of M" count. Solution export
reports per-assembly rather than per-file, since its assemblies decompile in
parallel and per-file reports would race. The determinate bar is pinned to 75%
of the overlay width so it doesn't jitter as the status text changes.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Debug Steps pane previously only served the ILAst language (one node per
IL transform). The C# decompiler's intermediate AST states were instead
exposed as a row of extra "C# - after TRANSFORM" dropdown languages, which
cluttered the language list and only let you jump to one fixed step at a time.
Generalise the pane to an IDebugStepProvider interface: ILAst keeps its IL
steps, and the C# language now contributes one step per AST transform, mapping
a selected step onto options.StepLimit (already honoured by CreateDecompiler).
Each language also supplies its own options object, so the writing-options
checkboxes show for ILAst and nothing for C#. The per-transform dropdown
languages are removed in favour of this.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Microsoft.DiaSymReader / .PortablePdb packages pull NETStandard.Library
1.6.1, which drags in 4.3.0 builds of System.Net.Http, System.Private.Uri
and System.Text.RegularExpressions, all carrying known advisories (NU1902/
NU1903). They are framework-provided at runtime on net10.0; pin the patched
4.3.4 / 4.3.2 / 4.3.1 via central transitive pinning so NuGet audit passes.
Enabling transitive pinning also aligns a handful of other transitive
packages to their declared central versions.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Options page is non-modal and live-apply, so -- unlike the WPF host,
which ran a full assembly-list Refresh() when its modal Options dialog
closed -- a setting that changes the decompiler/disassembler output (brace
folding, member/using expansion, debug info, IL detail, indentation) had
nothing to make it take effect; toggling it did nothing until the user
re-navigated.
Classify every DisplaySettings property in one table (DisplaySettingReactions:
editor-live / tree-text / tree-shape / re-decompile / none), grounded in the
actual consumers (ApplyDisplaySettings, GetIndentationString, the IL/mixed
languages). OnSettingsChanged dispatches on it, and a coverage test asserts
the table spans every settable property so a newly-added one can't silently
fall through.
Fixing the re-decompile case exposed that ForceRefreshActiveTab was itself a
no-op for an unchanged node -- ShowSelectedNode re-sets CurrentNodes, whose
setter dedups -- so RefreshDecompiledView (also used after dependency
resolution) never actually re-ran. Add DecompilerTabPageModel.Redecompile()
to force past the dedup and call it from ForceRefreshActiveTab.
Also drop the EnableSmoothScrolling setting: it drove TomsToolbox's
AdvancedScrollWheelBehavior in WPF, which the Avalonia port doesn't use and
never replaced, so the checkbox persisted a value nothing read.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The drag-drop / file-drop callback selected the dropped assemblies with a
raw SelectedItems.Clear() + Add() loop, which flashes a transient empty
selection. The model has SelectNodes precisely to avoid that -- it brackets
the swap so the selection-changed fan-out fires once, with the final set,
never empty. A transient empty poisons the grid-sync deferred guard, after
which the tree stops following tab activation. Route the callback through
SelectNodes; a regression test asserts no empty SelectedItem is observed
mid-drop.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "resolve assembly -> entity handle -> uncached DecompilerTypeSystem ->
ResolveEntity" sequence was copy-pasted in the code-view hover navigation
and the assembly-tree node finder. Move it onto EntityReference as a
Resolve(AssemblyList) member so both call sites just supply the list they
already have, and the uncached-per-call decision lives in one place.
Assisted-by: Claude:claude-opus-4-8: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 File-menu entry was a stub that popped a "not implemented" dialog,
while the working generation logic lived only in the assembly context-
menu entry. Lift that logic into a shared PdbGenerator and route both
entry points through it, so the menu command now generates PDBs for the
selected assemblies (enabled only when the selection holds a valid one).
This was the last NotImplementedDialog caller, so the dialog is removed.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The reflection-driven column builder turned every public property into a
column, so each {Column}Tooltip companion (NameTooltip, FlagsTooltip,
...) rendered as its own raw column next to the column it describes. That
text is already surfaced as the sibling cell's hover tooltip by
MetadataCellTooltip, which reflects the property off the row directly, so
dropping these properties from the column set leaves the tooltips intact
and removes the duplicate columns.
Assisted-by: Claude:claude-opus-4-8:Claude Code
DockWorkspace keeps the last remaining document un-closeable (CanClose
false) so the document area can't be emptied, and Dock's own close
button already honours that. The context-menu Close entry did not, so a
right-click still offered to close the final tab. Bind its IsEnabled to
the tab's CanClose, which is observable, so it tracks tabs opening and
closing.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The document tab strip's multi-row layout and the mouse-wheel gesture
that toggles it were only reachable by the wheel itself, with no
discoverable UI and no way to opt out of the gesture. Surface both as
checkboxes under a new "Tab options" group on the Display page, and add
a MouseWheelTogglesTabStripRows setting (default on) so the wheel toggle
can be turned off while keeping the persisted multi-row choice.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Clone / Rename / Delete / Select act on the selected list but were always
enabled, so clicking them with nothing selected silently no-op'd (the
handlers already guarded on SelectedListName). Bind their IsEnabled to the
list's selection via ObjectConverters.IsNotNull so they disable until a list
is picked; New / Reset / Add-preconfigured / Close stay enabled.
Assisted-by: Claude:claude-opus-4-8: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
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
The Export Project/Solution, Save Code and Compare tests decompiled whatever
the default assembly list seeds -- System.Linq, CoreLib, the Uri assembly --
so each spent ~10s purely on type count (Solution/CreateSolution/SaveCode/
DecompileAssembly ~10s, CompareView ~4.5s).
Add FixtureAssembly, which emits a minimal two-type assembly via
PersistedAssemblyBuilder, and point these tests at it. They still exercise the
full project/solution writer, save-to-file and compare-tree pipelines, but
each now runs well under a second (e.g. Solution_Mode 10.8s -> 0.3s,
CreateSolution 10.8s -> 0.2s, ShowIdentical 4.5s -> 0.3s).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Pdb2Xml command (ILSpy) and the PDB round-trip tests (Decompiler.Tests)
reference Microsoft.DiaSymReader*, previously gated on the build host being
Windows. That made dotnet restore resolve a different graph on Windows than
on Linux -- the packages (and their transitive tail: DiaSymReader.PortablePdb,
the legacy NETCore.Platforms/Win32 packages) appear only on Windows, and
DiaSymReader.Native flips between Direct and CentralTransitive. So a checkout
could not be developed across OSes without the committed packages.lock.json
churning on every Windows restore.
Drop the OS gate (keep Debug-only) so the restored graph, and the committed
lock, are identical on every OS. The consuming code is still gated by DEBUG
and WINDOWS, so on non-Windows the packages are restored but never compiled
in; the native asset only resolves for win-* RIDs.
The "Verify package contents" step (which checks the committed *.filelist
snapshots still match the built VSIX/MSI contents) had been excluding
packages.lock.json from its git diff to tolerate that per-OS churn. With the
locks now identical across OSes the carve-out is unnecessary, so it goes back
to a plain git diff --exit-code.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Replace the Windows-only .bat helpers (clean / debugbuild / releasebuild /
restore / updatedeps and BuildTools/format) with cross-platform pwsh
scripts at the repo root: restore.ps1, build.ps1 (-Configuration), clean.ps1,
updatedeps.ps1 and BuildTools/format.ps1, alongside the existing publish.ps1.
Enable a packages.lock.json for every project by hoisting
RestorePackagesWithLockFile into the root Directory.Build.props (the four
core libraries set it individually before) and commit the generated locks,
so restores are repeatable and CI can cache packages off them.
Cache the NuGet packages folder in the three setup-dotnet workflows
(build-ilspy, build-frontends, codeql-analysis), keyed on the lock files
per the setup-dotnet caching guidance.
Scope the Debug "Verify package contents" check to the *.filelist outputs
it actually generates. A project's packages.lock.json is keyed only by
(framework, RID), with no host-OS axis, so a lock produced on Linux
legitimately differs from one produced on Windows whenever an OS-conditional
PackageReference applies (Debug+Windows pulls Microsoft.DiaSymReader*). The
Windows restore then rewrites those locks; that churn must not fail a step
whose job is to police the VSIX/MSI file lists.
Also drop the dead ILSpy.BamlDecompiler publish line from
publishlocaldev.ps1, mirroring the earlier publish.ps1 fix.
Assisted-by: Claude:claude-opus-4-8:Claude Code