Switching to a document tab is supposed to pull the tree selection over to
what that tab shows, but it didn't reliably -- the gap was masked until the
right-click change stopped moving the selection on its own. Two problems:
The SelectedItem setter replaced the collection with Clear()+Add(), and the
transient empty step made the grid sync defer its completion flag, which then
suppressed the sync for the real new value -- so the tree visual stopped
following tab activation. Route every selection replacement through a single
batched SelectNodes() that fires the selection-changed fan-out once, with the
final set, so consumers never observe the transient empty (or a transient
multi, which would break metadata-tab reuse).
And a tab decompiled from several nodes carries no single SourceNode, so
activating it restored nothing. Restore the tab's full node set from
CurrentNodes (one or many), and mirror a multi-selection into the grid so every
restored node is highlighted, not just the primary.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Right-clicking a tree row to reach its context menu used to move the real
selection there first, because ProDataGrid selects the row on press. With the
preview document bound to the selection, that meant 'Decompile to new tab' on
node B (while viewing A) jumped the preview to B before the command ran, so you
ended up with B twice instead of the intended A + B. Middle-click avoided it,
but not every mouse has a usable one.
Capture the right-clicked row in ContextRequested (which fires even when a
previous menu's light-dismiss popup swallows the press) and swallow the
right-press so the grid never reselects: the menu now acts on the clicked row
as a Thunderbird-style context target while the selection -- and the document
-- stay put. The targeted row gets a faint focus-box highlight, cleared when
the menu closes (guarded so a stale menu's close can't wipe a newer target).
Also adds TestHarness.ClickItem to collapse the repeated
Items.OfType<MenuItem>().Single(...).RaiseEvent(...) menu-click dance.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Cocoa menu exporter samples Application.Current's NativeMenu once at
startup and never watches the property afterwards, so the previous code's
NativeMenu.SetMenu(Application.Current, freshMenu) was never observed: About
and Check for Updates simply went missing from the app-named menu. Declare an
empty NativeMenu in App.axaml so it exists before that sampling, then insert
the Help items into that same instance at startup, which fires the exporter's
re-export. Inert on Windows / Linux.
Assisted-by: Claude:claude-opus-4-8:Claude Code
This fixture was missed by both the helper-extraction and visual-breakpoint
sweeps, so it still carried 16 copies of the boot prologue and no captures.
Route it through TestHarness.BootAsync / TreeNavigation and add a Step after
each decompile, matching every other headless fixture.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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 menu builder hard-set each NativeMenuItem's IsEnabled from static
export metadata (always true), overriding the state that assigning Command
would otherwise derive from the command's CanExecute. The inline
NativeMenuBar on Windows/Linux re-derives from the command and greys out
OS-gated items (e.g. Open from GAC, which is Windows-only), but the macOS
native menu reads NativeMenuItem.IsEnabled directly -- so those items
stayed enabled there even though invoking them is a no-op. Drop the
explicit IsEnabled so every command-backed item follows its CanExecute on
all platforms.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The banner used one amber colour for every state. Show green when ILSpy
is up to date and amber when an update is available, driven by a new
UpdatePanelViewModel.UpdateAvailable bound to a style class on the border.
The message text also inherited the dark theme's near-white foreground,
which was unreadable on the always-light banner; pin it to a dark colour
since the banner stays light in both themes.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Closing and reopening Options or About built a fresh ContentTabPage (and a
fresh owned view) each time, losing the selected options page and
re-rendering the About output. Retain these static-content tabs by key on
DockWorkspace and re-add the same instance on reopen, so the dockable --
and the view it owns via IDockableViewOwner -- persists for the session.
Embedded About resource pages (license, third-party notices) are
singletons too.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Pin the DockableViewRecycling contract: one view per dockable identity,
distinct views for distinct ContentTabPages, owners bypass the global
fallback cache, an already-parented view re-parents without throwing, and
the active document tab renders its own view (the slot-sharing guard).
Assisted-by: Claude:claude-opus-4-8:Claude Code
ContentTabPageView hosted every content type in one pre-realized Panel
and toggled visibility, so each tab carried a hidden copy of every inner
view. Enabling CacheDocumentTabContent (so the editor's scroll and the
Options TabControl selection survive a tab switch) kept multiple tabs
realized, which then put several OptionsPageView / CompareView instances
in the tree at once. Host the active Content in a single ContentControl
resolved by the ViewLocator instead, so each tab realizes only the view
its content needs.
OptionsPageModel needs an explicit ViewLocator entry: it is an
ObservableObject, not a TabPageModel/ViewModelBase, and its name doesn't
fit the *ViewModel -> *View convention the fallback relies on.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Capture the editor's caret, scroll offset, and expanded foldings on
demand when a navigation record is written -- the model exposes a
CaptureViewState delegate the view sets and DockWorkspace invokes --
instead of pushing every caret/scroll event onto the view-model. The
push mistook AvaloniaEdit's programmatic caret-to-end on text replace for
a user move and recorded it, poisoning the captured position. Back /
Forward restores through a single PendingViewState channel the editor
consumes once the document Text lands.
AvaloniaEdit 12.0.0's ScrollToVerticalOffset is a no-op, so the editor's
ScrollViewer.Offset is set directly (TODO: drop once AvaloniaEdit #594
ships and the package is bumped).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Without these, CSharpLanguage fell through to the base Language: code mapping
covered only the declaring type (so compiler-generated members didn't resolve
to their source method/part), and entity names came out IL-styled (arity
suffixes, escaped identifiers) instead of C# (generics as <T>, nested types
joined with '.'). Restore both overrides -- GetCodeMappingInfo delegates to
CSharpDecompiler.GetCodeMappingInfo and GetEntityName builds C# names via the
ported ToCSharpString helper -- matching the WPF app.
Assisted-by: Claude:claude-opus-4-8:Claude Code
CovariantReturns (C# 9), InlineArrays (C# 12) and ExtensionMembers (C# 14)
are exposed in the decompiler options UI via [Description("DecompilerSettings.X")]
but had no entry in Resources.resx, so each showed its raw resource key instead
of a label. Add the three strings. AggressiveScalarReplacementOfAggregates is
left out deliberately: it is [Browsable(false)] outside DEBUG, so users never
see it.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two first-run behaviours that the previous version had and the port had
lost. When a launch has nothing to restore (no saved tree path, no
command-line target), greet the user with the About page in the main
tab instead of an empty "(no selection)" view; the greeting is fired
after the tree is ready and only takes the main tab when it's still
empty and active, so it can't steal activation from a tab the user
opened meanwhile (e.g. Compare...). And seed the default list with the
.NET framework ILSpy itself runs on - every managed assembly in the
running runtime's shared-framework directory. Headless tests opt out of
that seed (re-opening ~150 assemblies per test is ~10x slower) and fall
back to the minimal trio, leaving their expectations unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The dialog lost the entry point for building a preconfigured list when
the view-model wasn't ported; the backing AssemblyListManager.
CreateDefaultList was already present and cross-platform. Re-add the
button and a flyout of the generated lists. The three GAC-based
framework lists resolve nothing without a GAC, so they're only offered
when a GAC directory exists (i.e. on Windows); the per-installed-runtime
entries work everywhere.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The PE/metadata nodes had collapsed onto the generic MetadataTable glyph
during the port. Bring back the per-node icons the previous version
shipped: a Metadata glyph for the root, a Header glyph for the DOS/COFF/
Optional headers, a Heap glyph for the heaps, a table-group glyph for
"Tables", and the folder glyphs for the Data Directories container. The
four missing SVG assets are ported from history.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The +/- expander was a 13x13 toggle whose visible glyph (a 9x9 box) was
also the only hit-testable surface, so the real tap target was barely
9px. Grow the toggle to 16x16 but keep its laid-out width at 13 via a
negative right margin, because TreeLines hardcodes a 13px expander
column with the glyph centred at +8.5 and would otherwise misalign. A
transparent wrapper fills the 16x16 so the whole area receives input;
the visible glyph is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Dock recycles a single DecompilerTextView instance across every
document tab via App.axaml's ControlRecyclingKey, so flipping
between tabs reaches OnDataContextChanged with the FoldingManager
still holding the previous tab's collapsed / expanded state. There
was no capture step on the recycled-view path: only Back / Forward
navigation set PendingFoldings before ApplyDocument ran. Plain tab
clicks therefore landed in ApplyDocument with PendingFoldings null
and every folding rebuilt at its default-open state.
Track the previously bound model in a `boundModel` field. When
DataContextChanged fires:
- Detach the outgoing model's PropertyChanged handler (also fixes a
latent multi-attach leak the old code left in place).
- Snapshot the current FoldingManager's state into
outgoing.LastKnownFoldings so it can be restored on a return
visit.
Then in ApplyDocument fall through PendingFoldings to
LastKnownFoldings so the recycled-tab path lands on the same
restore code Back / Forward already used. PendingFoldings still wins
when both are populated -- explicit history navigation always
overrides the recycled snapshot.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Compare opens a structural side-by-side diff of two assemblies --
it's an analytical operation, not navigation between existing
documents. With Compare in Navigate it sat next to "Decompile to
new tab", which is genuinely navigational; users reading the menu
top-to-bottom had to mentally split the group.
Move it into Analyze (matching the resource string already used by
"Search Microsoft Docs") and place it at Order 220 so it sits
immediately above Search in the rendered menu. The two Analyze
entries now form a single coherent group of "tell me more about
this code" actions, and the Navigate group is left to actions that
actually navigate.
Assisted-by: Claude:claude-opus-4-7:Claude Code
When the C# 14 extension members tree feature is on, the marker
methods inside a static extension container surface as children of
the dedicated ExtensionTreeNode rather than as plain methods on the
outer class. Clicking one was running DecompileMethod, which renders
the lowered static method body, not the source-faithful
`extension(T) { ... }` block the user expects.
Match the WPF behaviour: when MethodTreeNode.Parent is an
ExtensionTreeNode and the active language is C#, dispatch to
CSharpLanguage.DecompileExtension(IMethod) so the whole declaration
emits as it was written. Other languages and non-extension methods
keep the existing DecompileMethod path.
Assisted-by: Claude:claude-opus-4-7:Claude Code
ResourceStringTable is added to the decompiler text view via
SmartTextOutput.AddUIElement, so the host stretches it to the full
document width. With Value column set to Width="*" and no MaxWidth,
a single very long resource entry pulled the entire table out past
the right edge of any reasonable reading column -- on a 27-inch
display the table could end up 1800px wide for a 40-character
header.
Pin the root grid to MaxWidth=900 and align it left so the table
keeps the same column as the rest of the document. Add column
MaxWidth caps (Name 320, Value 640) so a pathological entry on
either column can't push past the grid edge either.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The avalonia branch only carried the base set of tree-node icons.
FieldReadOnly, EnumValue, Indexer, VirtualMethod, ExtensionMethod and
PInvokeMethod were already SVGs in master:ILSpy/Images/ but never
copied over -- so e.g. readonly fields shared the same glyph as
mutable instance fields, and indexers were indistinguishable from
plain properties.
Bring across the seven SVGs (those six plus Copy for the Copy-
fully-qualified-name context-menu entry), register them on
Images.cs, and extend the icon dispatch in each tree node to mirror
WPF's logic:
FieldTreeNode EnumValue / Literal (const) / FieldReadOnly / Field
PropertyTreeNode Indexer / Property, with the C# 14 Extension overlay
when the accessor is an extension member
MethodTreeNode Operator / extension Method / Constructor / PInvoke
(extern + DllImport, no body) / VirtualMethod /
Method
PInvoke detection uses the documented (!HasBody && DllImport) pair
so managed wrappers that re-apply the attribute via forwarding don't
inherit the native-call glyph.
Also wire the Copy icon onto the "Copy fully qualified name" entry
so the menu row matches its WPF counterpart.
Assisted-by: Claude:claude-opus-4-7:Claude Code
WPF's CSharpLanguage doesn't override DecompileNamespace -- clicking
a namespace falls through to the base Language.DecompileNamespace,
which writes just the namespace name as a `// Foo.Bar` comment.
The avalonia port's override decompiled every top-level type in the
namespace, which on a large assembly turned a single click into a
multi-second blocking decompile of hundreds of types.
Remove the override so clicking a namespace lands the same cheap
comment-only output the WPF app shows. Users who do want the full
namespace decompiled can still get it via right-click > Save Code,
which routes through a separate path that constructs the project
explicitly.
ILLanguage's override is intentionally preserved -- IL mode in WPF
does disassemble the namespace; we match that.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Build on the editor / toolbar scaffolding so the rest of the visible
surface area swings with the active theme variant.
Add brush pairs for the surfaces that previously inherited Simple's
Light-only defaults or hardcoded fills:
WindowBackground / WindowForeground -- the canvas behind everything
PaneBackground -- the assembly tree DataGrid
ChromeBorder -- the hairline borders in the
options pages
MenuBackground / MenuForeground -- inline menu bar, dropdowns,
context menus, flyouts
InputBackground / InputForeground / -- TextBox, ComboBox, ListBox
InputBorder used across the Options
dialogs and side panes
SecondaryForeground -- the dimmed location / assembly
labels in search results
TreeAutoloadedForeground -- SteelBlue (autoloaded marker)
TreeNonPublicForeground -- gray (non-public API marker)
Wire them in via Style selectors against Window, DataGrid, Menu,
MenuItem, ContextMenu, MenuFlyoutPresenter, TextBox, ComboBox /
ComboBoxItem, and ListBox / ListBoxItem in App.axaml, plus the three
remaining hardcoded BorderBrushes in OptionsPageView, DisplaySettings
panel and the Foreground="Gray" instances in SearchPane.
Three deliberate gaps remain on Dark:
- Dock chrome (tab strip, splitters) -- needs Dock-specific theme
brush keys which are a separate vendor surface.
- Button / CheckBox / RadioButton fills -- Simple's defaults are
legible against the new dark canvas; revisit if they look out of
place in actual use.
- The warning banner (UpdatePanel) keeps its yellow palette in both
variants because the colour is semantic, not chrome.
Dark palette continues the VS-style line:
window #252526 pane #1E1E1E chrome border #3F3F46
menu #2D2D30 menu fg #DCDCDC
input bg #3C3C3C input border #555555
Assisted-by: Claude:claude-opus-4-7:Claude Code
Simple theme ships no Dark resource dictionary, so flipping
RequestedThemeVariant alone is a no-op against any surface whose
brushes are hardcoded or pulled from a Simple key. We can't swap
themes (Simple is the deliberate visual identity), but we can layer
our own ThemeDictionaries on top so the surfaces ILSpy owns directly
respond to View > Theme.
Introduce a single block of Light / Dark brush pairs under
Application.Resources.ThemeDictionaries (App.axaml), then route the
hardcoded brushes in the editor view, the toolbar, the zoom overlay,
and tooltips through DynamicResource lookups against those keys.
Surfaces wired:
- Decompiler text view background and wait-adorner overlay.
- Main toolbar background, bottom border, separator hairlines,
combobox border, and the disabled-button content fills.
- Zoom-buttons overlay background and border.
- Tooltips (background, foreground, border) via a Style selector
since Simple paints them via hardcoded setters that don't expose
a brush key.
Simple-themed chrome (menu, tree, scrollbars, status bar, dock
splitters) remains Light in both variants. That's partial dark mode
by design -- covering it requires either swapping to Fluent (which
costs the Simple visual identity, and a separate test showed the
chrome regressing under Fluent) or redeclaring every Simple resource
key per variant, which is a much larger effort. Follow-ups will
extend coverage one surface at a time.
Light values match what each file shipped (no visible diff in the
default theme). Dark values follow the VS-style palette:
editor #1E1E1E
toolbar #2D2D30 border #3F3F46
tooltip #252526 border #3F3F46 foreground #DCDCDC
zoom overlay #E01E1E1E with translucent gray border
Assisted-by: Claude:claude-opus-4-7:Claude Code
FieldTreeNode used the base Field icon for every kind of field. The
WPF version distinguished EnumValue and Literal so a reader could
tell at a glance whether a member was a backing field, a const, or
an enum case.
Mirror the master logic (master:ILSpy/TreeNodes/FieldTreeNode.cs:60),
but with two scope cuts:
- Use Images.Enum for enum values. We don't have a dedicated
EnumValue.svg, and the type-level Enum glyph reads correctly in
context (an enum case sits inside an Enum node).
- Skip the FieldReadOnly branch entirely -- no asset exists yet and
inventing one isn't in scope for this commit. Readonly fields keep
falling through to the base Field icon, the same as before.
Detection rule for an enum value: DeclaringType.Kind is Enum AND
ReturnType.Kind is Enum. The second clause is what excludes the
synthesised int32 value__ backing field that every enum carries --
that field's return type is Int32, not the enum itself.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Matches the WPF behaviour at master:ILSpy/AssemblyTree/AssemblyTreeModel.cs:440
where DEBUG builds get "ILSpy {DecompilerVersionInfo.FullVersion}" so the
developer can tell from a glance which build the running window came from
(especially handy when the avalonia-preview tag floats forward and several
windows are open at once). Release builds keep the bare "ILSpy" title.
The title is bound from MainWindowViewModel.Title (axaml: Title="{Binding
Title}"), so threading the version through the VM is the natural seam --
no axaml change and the bound value can later be made notifying if we
want to mix in the active assembly-list name.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The Simple theme's SimpleMenuScrollViewer template wraps menu items
in a ScrollViewer whose Top / Bottom docked RepeatButtons are the
classic Win9x scroll chevrons. Their IsVisible binding feeds through
MenuScrollingVisibilityConverter which does an AreClose check on
Extent vs Viewport. At any non-100% DPI scale (125%, 150%, ...) the
layout-rounding pass leaves a small gap so AreClose returns false
and the converter declares "content overflows," then both chevrons
render even when the menu only has two items and obviously fits.
Bug tracked upstream as AvaloniaUI/Avalonia#9501; the partial fix in
PR #11916 (ScrollContentPresenter rounding) is already in 12.0.3 but
doesn't widen the converter's tolerance, so the symptom persists.
Hide both chevrons unconditionally inside any menu's scroll viewer.
Safe because: (a) the popup positioner already clamps menu height to
the available screen area, and (b) ILSpy's menus are short enough
that real overflow doesn't occur on any sane display. Height = 0
collapses the DockPanel slot the chevrons would otherwise reserve
before the visibility binding propagates.
Two selectors (MenuFlyoutPresenter and ContextMenu) cover both
managed-menu paths; the inline NativeMenuBar on Linux / Windows
routes through the same SimpleMenuScrollViewer template, so the
top-level menus pick this up too.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Two assembly-tree right-click entries existed in master but were
dropped in the avalonia port:
* Load Dependencies (master/AssemblyTreeNode.cs:602). Resolves every
AssemblyReference on the selected assemblies through their resolver
in parallel; turns unresolved-type placeholders in subsequent
decompiles into real linked nodes. Same IsLoadedAsValidAssembly gate
master used; placed inside AssemblyTreeNode.cs as a nested type to
match the Remove / Reload pattern. Category = Dependencies, Order =
700 -- sits between Edit (600-699) and the trailing null group
(Reload 900, Remove 910).
* Open Command Line Here (master/AssemblyTreeNode.cs:734). Walks the
selection up to the enclosing AssemblyTreeNode and opens a terminal
cd'd to its containing folder. Master's GlobalUtils.OpenTerminalAt
was Windows-only (cmd.exe); ported cross-platform: cmd.exe on
Windows, `open -a Terminal` on macOS, $TERMINAL / xdg standard list
fallback on Linux. Lives in Commands/ as its own file, mirroring
OpenContainingFolderContextMenuEntry. Category = Shell, Order = 510
(just after OpenContainingFolder).
Also: DecompileToNewPanel was missing master's "MMB" InputGestureText
annotation (the middle-mouse-button affordance is what users see in
the menu). Add it to the existing attribute.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Three friction points:
* ShowInMetadataContextMenuEntry used the literal string "Metadata"
for Category, breaking the codebase's nameof(Resources.X) convention
and missing the chance to share a category with the navigation-style
entries we just established in the tree menu. Move to "Navigation",
Order = 200 (matching the tree-side ShowInMetadata sibling).
* DecompileMetadataRowInNewTabCommand had the same literal "Metadata"
category plus Order = 110, which collided with the editor Copy/Select
band (also 100/110). Move to "Navigation" Order = 100 so it parallels
the tree-side Decompile-to-new-panel entry.
* ShowCFGContextMenuEntry had no Category and no Order, so it floated
to the bottom of the implicit null group in the DEBUG-builds menu.
Give it Category = "Diagnostics" + Order = 9000 to match the pattern
we use elsewhere for DEBUG-only entries.
Adding a Copy.svg asset for the Copy / Select editor entries deferred
to a separate commit -- it's an asset import, not a metadata change.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Fourteen entries surfaced when right-clicking nodes in the assembly
tree, but six were in the implicit null category, four shared a
single overloaded "Save" category with three different operations
(save code, create diagram, extract package), and seven had Order =
double.MaxValue ties so their relative position was MEF discovery
order rather than declared. Several entries lacked icons even though
matching IImage fields already existed in Images.cs.
Apply the same non-overlapping range-per-category shape used for the
File and View menus, with 100-wide bands so each entry has a stable
slot and room for future siblings:
Navigate 100-199 Decompile to new panel (ViewCode), Compare...
Analyze 200-299 Analyze, ScopeSearchToAssembly (FindAssembly),
ScopeSearchToNamespace (Namespace), SearchMSDN (Search)
Save 300-399 Save Code, Create Diagram, Extract package entry
(FolderOpen), Extract all entries (FolderOpen)
Debug 400-499 Select PDB (ProgramDebugDatabase),
Generate Portable PDB (ProgramDebugDatabase)
Shell 500-599 Open containing folder (FolderOpen)
Edit 600-699 Copy fully qualified name
(null) 900+ Reload (Refresh, 900), Remove (Delete, 910)
Extract Package entries previously borrowed the Save icon, which
read semantically wrong -- a folder-open glyph is closer to what
"extract to a directory" actually does. Decompile-to-new-panel moves
from Analyze to Navigate because surfacing the same content in a new
tab is a navigation primitive, not an analysis one. The Reload /
Remove pair stays in the null group at the bottom of the menu --
they're list-mutation operations that conceptually anchor the menu
end, matching where they currently render.
Assisted-by: Claude:claude-opus-4-7:Claude Code
SortAssemblyListCommand and CollapseAllCommand both left ToolbarOrder
at its default of 0, so their relative position on the toolbar fell
out of MEF discovery order. Make it explicit: Sort = 0, Collapse = 1.
Assisted-by: Claude:claude-opus-4-7:Claude Code
macOS convention puts About / Check for Updates under the bold app-
named menu next to the Apple logo, not under a separate top-level Help
menu. Add a PromoteHelpToMacAppMenu pass that runs only on macOS:
1. Find the _Help submenu in topLevelByTag (populated by
AppendRegistryCommands when it sees the Help-targeted MEF commands).
2. Move its children into a new NativeMenu and SetMenu it on
Application.Current. Avalonia's Cocoa exporter watches
NativeMenu.MenuProperty on the Application as the channel for the
bold app menu, and our items get prepended above its auto-inserted
Services / Hide / Quit block.
3. Remove _Help from the window menu so the items don't appear twice.
On Windows / Linux the gate skips and the _Help top-level renders
inline as before.
The CheckForUpdatesCommand / AboutCommand attributes still declare
ParentMenuID = _Help -- they remain Help-categorised in metadata, just
re-routed at attachment time on macOS. No special Cocoa metadata
needed; the exporter picks up plain NativeMenuItems.
Assisted-by: Claude:claude-opus-4-7:Claude Code
CheckUpdates and About both lacked MenuCategory -- they fell into the
empty default category. Add MenuCategory = "Help" so the two entries
form a declared group (separators would appear if any other category
joined Help later), and renumber CheckUpdates from the arbitrary 5000
to 0 so its position relative to future siblings is meaningful.
About keeps the 99999 sentinel that matches File > Exit's convention
for "stays pinned to the bottom no matter what's inserted above."
No icons added: ILSpy/Images.cs has no Help / About / Info / Update /
Question field, so wiring would need new SVG assets -- out of scope.
Assisted-by: Claude:claude-opus-4-7:Claude Code
CloseAllDocuments, ResetLayout, and PinCurrentTab all had no
MenuCategory and no MenuOrder, so their relative order fell out of
whatever sequence the C# reflection enumerator returned for their
attributes -- not stable across refactors or partial-class layouts.
Group them into a single MenuCategory = "Window" with the three
explicit MenuOrder slots, sequenced non-destructive first then
destructive-reversible then destructive-nuke:
0 Pin current tab (toggles state; reversible by Unpin)
1 Close all documents (closes tabs; reopenable via tree click)
2 Reset layout (rebuilds the dock root; loses pin / tool-pane positions)
The tool-pane toggles (Search, Assemblies, Analyzer, Debug Steps)
and the open-document tab radios already sit below this block via
AppendWindowDynamicContent + AppendTabSection -- separators between
them stay where they were.
Assisted-by: Claude:claude-opus-4-7:Claude Code
DecompilerVersionInfo.template.cs is the source-of-truth for the
runtime version constants -- update-assemblyinfo.ps1 rewrites the
generated DecompilerVersionInfo.cs from it on every build, picking
up the live git revision and commit hash for the $INSERT...$ slots.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The MEF block of the View menu used MenuOrder = 100/101 for Back/
Forward but no MenuOrder on Sort assembly list / Collapse all tree
nodes, so the latter two implicitly defaulted to 0 and ended up
appearing before Back/Forward despite their numerically larger
explicit order. The intent reads better the other way around:
Back/Forward are high-frequency keyboard-driven actions (Alt+Left/
Right) and belong adjacent to the show-radios block, while Sort and
Collapse are tree-operation bulk actions that belong further down.
Use the same non-overlapping MenuOrder ranges that File now follows:
Navigation: 0 .. 9 (Back, Forward)
Tree: 10 .. 19 (Sort, Collapse)
Options: 999 (Options)
Sort and Collapse moved from MenuCategory = "View" to "Tree" because
the category identifier should describe the operand -- both act on
the assembly tree, neither is a generic "view" action.
Assisted-by: Claude:claude-opus-4-7:Claude Code
FileCommands.cs already declared MenuIcon = "Images/AssemblyListGAC"
on the Open-from-GAC main-menu command, but the reflection lookup
in Images.cs found no field of that name so the menu item rendered
without an icon. Port the SVG asset from master/ILSpy/Images/ into
Assets/Icons/ and add the static IImage registration so the metadata
flow completes.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The File menu's MenuCategory metadata was inherited from the WPF tree
and showed two friction points: the four DEBUG diagnostics
(Decompile All / Disassemble All / Decompile 100x / Dump PDB) lived
in the Open category, so they appeared right after Reload with no
visual separator -- the menu read as if Decompile All was a variant
of Open; and the assembly-list mutators (Remove with load errors,
Clear assembly list, Manage assembly lists) were split across two
categories (Remove and Open) even though they all operate on the
same object.
Reshape into five categories with non-overlapping MenuOrder ranges
so category order is deterministic instead of falling out of MEF
discovery order:
Open (Open, GAC, Reload) MenuOrder 0 .. 9
AssemblyList (Manage, Remove, Clear) MenuOrder 10 .. 19
Save (Save Code, Generate PDB) MenuOrder 20 .. 29
Debug (DEBUG -- ...) MenuOrder 30 .. 39
Exit (Exit) MenuOrder 99999
The "AssemblyList" category replaces the previous "Remove" identifier
(carrying Remove-with-load-errors + Clear) since both members and the
newly-relocated Manage entry are list-of-assemblies operations rather
than pure removal. The category value is a string literal because it
is only ever used as a grouping key by ContextMenuProvider /
MainMenu.AppendRegistryCommands -- never displayed.
Stride-10 within each category gives room to insert later items
without renumbering. The earlier 2.5 / 2.55 / 2.6 / 2.7 fractional
scheme had no category-boundary contract: the existing 2.6 tie
between Remove-with-load-errors and Clear meant their order between
each other depended on whatever order reflection enumerated the
fields in. Generate portable PDB previously had no MenuOrder at all
so its position relative to Save Code was MEF-implementation defined.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Master's manual ILSpy/Properties/AssemblyInfo.cs was dropped during
the Avalonia port; the SDK's auto-generated AssemblyInfo took over
with default-zero versions. As a result the built ILSpy.dll's
assembly metadata read 1.0.0.0 even though DecompilerVersionInfo
(the static class generated from the .template by the build pipeline)
had the right values. Anything that queried Assembly.GetName().Version
or AssemblyInformationalVersionAttribute -- update checks, crash
report context, the title-bar version line we still need to wire --
got the SDK default, not the real revision.
Port the cross-platform-safe slice of master's AssemblyInfo.cs:
Title/Description/Company/Product/Copyright, ComVisible(false), the
two version attributes pointing at DecompilerVersionInfo,
NeutralResourcesLanguage, InternalsVisibleTo("ILSpy.Tests"), and the
CA2243 suppression. Deliberately skipped: [SupportedOSPlatform(
"Windows7.0")] and [TargetPlatform("Windows10.0")] (would re-make the
assembly Windows-only at metadata level and emit CA1416 noise for
cross-platform code paths), [InternalsVisibleTo("ILSpy.AddIn")] (the
AddIn project isn't in the avalonia build path), and the WPF-only
Properties/WPFAssemblyInfo.cs ThemeInfo (System.Windows-typed).
The single-line ILSpy/AssemblyInfo.cs stub that only carried the
ILSpy.Tests InternalsVisibleTo gets folded into the new file and
removed, matching master's layout.
GenerateAssemblyInfo=false in the csproj is the required companion --
without it the SDK emits a second AssemblyVersion attribute and
compilation fails with CS0579.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Both menu builders previously ignored the MenuIcon/Icon metadata on
ExportMainMenuCommand and ExportContextMenuEntry: the strings were
preserved through the WPF -> Avalonia port (~12 main-menu commands
declare MenuIcon, plus a handful of context-menu entries), but the
NativeMenuItem and MenuItem instances they fed into had no icon
assignment. Avalonia's toolbar already had a private path-to-image
reflection resolver; this lifts it into Images.cs as a shared helper
so the two menu sites can consume it too.
NativeMenuItem.Icon is Bitmap-typed (macOS NSImage is the platform
target and has no vector form), so the main-menu path rasterises
SVG icons through RenderTargetBitmap at attach-time -- one-time cost
per menu item at boot. MenuItem.Icon accepts any Control, so the
context-menu path keeps the IImage live inside an <Image> control
and preserves vector rendering.
SaveCodeContextMenuEntry and AssemblyTreeNode.ReloadAssembly were
missing Icon metadata even though their main-menu equivalents
declared MenuIcon. Adding Icon = "Images/Save" and "Images/Refresh"
respectively, matching the toolbar/menu sibling. Other context-menu
entries without Icon metadata are intentionally textless: most are
node-type-specific actions (Remove, Copy results, Search MSDN) where
no main-menu sibling exists to inherit an icon mapping from.
The App.axaml separator style selector "MenuItem Separator" only
matched separators nested inside a templated MenuItem (the main-menu
submenu case). ContextMenuProvider adds Separator directly to
ContextMenu.Items between Category groups, where there's no MenuItem
ancestor for the selector to match -- so context-menu separators
rendered with the Simple theme's default near-invisible style.
Extending to "MenuItem Separator, ContextMenu Separator" covers both
surfaces; toolbar and docking-chrome separators stay scoped out.
MenuIconWiringProbe walks the materialised NativeMenu tree post-build
and asserts File > Open has its Icon populated -- regression net for
the metadata-flow path.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The trailing HeaderedContentControl in the Display panel (Other -> Sort
results by fitness) sat 15 px below the ScrollViewer.Extent's reported
height: the outer StackPanel's measure under-counted that last child's
inner content, so MaxYOffset never grew enough to scroll the checkbox
into view. The Reset-to-defaults border below the ScrollViewer covered
the gap visually, making the checkbox look obscured.
ScrollViewer.Padding alone can't compensate because Avalonia 12's
Simple-theme ScrollContentPresenter collapses Padding on the axis the
scrollbar lives on -- the horizontal 6 px stuck, the vertical 6 px was
dropped. Wrapping the content in a Border with explicit Padding makes
the padding part of the StackPanel's measured extent, and the last
child becomes reachable. Applied to both panels that use a top-level
StackPanel under a ScrollViewer; Misc is unaffected (no ScrollViewer,
two checkboxes never overflow).
The new OptionsPageScrollReachTests.Last_Item_In_Display_Panel_Is_-
Reachable_At_Max_Scroll opens the Options tab, scrolls the Display
panel to ScrollViewer.Extent.Height, and asserts the "Sort results by
fitness" CheckBox's rendered bottom sits at or above the Reset border's
top in window-coordinate space.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Avalonia's Window.Icon doesn't reach the macOS Dock, so the app needs a
proper .app bundle: an ILSpy.icns produced from the existing artwork, an
Info.plist with CFBundleIconFile=ILSpy and CFBundleIdentifier
net.sharpdevelop.ilspy, and a BuildMacAppBundle target that runs
AfterTargets="Publish" for any osx-* RuntimeIdentifier. The target lays
out the standard Contents/MacOS + Contents/Resources structure under
bin/<config>/<tfm>/<rid>/ILSpy.app/ and chmods the apphost executable
so macOS LaunchServices will accept it.
The Avalonia resource glob ('Assets/**') is told to skip the bundle
inputs since they're consumed by the OS loader rather than at runtime
via avares://. The lock files pick up RID-specific runtime asset entries
that NuGet adds when an osx RID is in the project's RuntimeIdentifiers.
The slnf name was a leftover from when ILSpy.csproj was WPF; its
contents are now the Avalonia desktop projects. Drop the dead
ILSpy.BamlDecompiler.csproj ref (the directory no longer holds a
project after the engine moved to ICSharpCode.BamlDecompiler) and the
out-of-scope items (TestPlugin, ILSpy.BamlDecompiler.Tests) -- they're
in neither ILSpy.sln nor any active build flow, so the slnf filter
surfacing them in IDE listings was just noise.
Assisted-by: Claude:claude-opus-4-7:Claude Code
If anything thrown by App.OnFrameworkInitializationCompleted propagated
past it -- a CompositionFailedException from an unresolvable plugin
dependency, say -- the process died before the dispatcher pump started,
so the AppDomain.UnhandledException dialog wired by GlobalExceptionHandler
never had a chance to surface. The user saw a silent exit; the only
trace was a stderr stack on terminal launches and nothing at all when
clicking the icon. Wrap the critical resolution paths so anything that
goes wrong lands in StartupExceptions, and on any failure swap the
MainWindow for a StartupErrorWindow that displays the captured text.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The MainMenu UserControl previously built a regular Avalonia Menu of
MenuItems, which on macOS would render inline in the window instead
of in the system menu bar -- not what Mac users expect. Avalonia's
NativeMenu + NativeMenuBar is the cross-platform abstraction: on
macOS the menu is projected into the system bar, on Windows / Linux
NativeMenuBar's presenter renders the same items inline. The MEF
registry, theme submenu, tool-pane toggles, and dynamic tab list all
flow through unchanged; only the leaf widget type swaps from MenuItem
to NativeMenuItem. KeyModifiers.Control is translated to Meta on
macOS so the system menu bar shows Cmd glyphs instead of Ctrl.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Previous rule mandated "Avalonia: <lowercase phrase>" subjects, which
encouraged restating the area at the cost of subject-line budget. The
prefix has been stripped from all 336 branch commits; update the rule
so future commits match.
Assisted-by: Claude:claude-opus-4-7:Claude Code