Added two new headless tests in MainWindowTests:
- Back_Navigation_Restores_Previously_Selected_Node — selects two methods on
Enumerable, executes DockWorkspace.NavigateBackCommand, asserts the original
node is reselected, decompiled text reverts, and the row is centred.
- Opening_Assembly_Adds_It_To_The_Tree — drives the production OpenCommand via
MainMenuCommandRegistry (matched by Resources._Open header), confirms the
new LoadedAssembly appears, the AssemblyTreeNode is reachable by short name,
and SharpTreeNode.IsSelected is set on the freshly opened node.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Restructure the Avalonia AssemblyTreeModel so its task layout matches the WPF
host, both for readability when cross-referencing the two trees during the
port and so future WPF changes can be ported mechanically. New surface (all
mirrored from ILSpy/AssemblyTree/AssemblyTreeModel.cs):
Assisted-by: Claude:claude-opus-4-7:Claude Code
CenterRowInView calls DataGrid.ScrollIntoView and then reads the row's bounds
to compute the offset that puts it on the viewport mid-line. ScrollIntoView
only updates ScrollViewer.Offset though; the new DataGridRow shows up on the
next layout pass. When the target row was outside the current viewport (e.g.
back-navigation across distant selections), the lookup ran before that pass
and returned null, so the centring step bailed and the offset stayed where it
was. Force the layout pass with TreeGrid.UpdateLayout() so the row is realised
synchronously.
Assisted-by: Claude:claude-opus-4-7:Claude Code
DecompileAsEnumerableTest exercises the full path: open mscorlib, navigate
to Enumerable.AsEnumerable, render the decompilation, verify text and
verify the row centred in the assembly tree. Writing it surfaced two
pre-existing bugs the manual smoke-test missed:
File / View / Window / Help built from MEF main-menu commands plus a static
View submenu (API visibility, Theme = Light/Dark via Application.RequestedThemeVariant,
UI Language) bound to LanguageSettings on SessionSettings. Window menu also lists
the existing tool-pane toggles. Commands without ported deps (Options, About,
Updates, GAC, manage lists, save/PDB) surface a "not yet ported" stub dialog.
Open uses Avalonia StorageProvider; CloseAllDocs/ResetLayout call into DockWorkspace.
InputGestureText on the export attribute is parsed into MenuItem.HotKey, giving
Open Ctrl+O / Save Ctrl+S / Reload F5 as window-scoped accelerators.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The renamed Avalonia csproj was referencing the localized strings file
via a sibling-project relative path (..\ILSpy\Properties\Resources.resx)
that resolved into the WPF host's tree. The host is gone now, so the
reference is dangling.
Subscribes to AvaloniaEdit's PointerHoverEvent / PointerHoverStoppedEvent
on the inner TextView and resolves the segment under the live pointer at
hover-fire time. Same pattern WPF ILSpy uses on AvalonEdit's MouseHover.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The decompiler ships a number of Debug.Assert calls that fire on real-world
but unusual IL (e.g. method-body shapes the constructor-initializer transform
doesn't expect). Without a custom listener those terminate the process via
the default trace listener's UI, making any non-trivial decompile crash the
Avalonia host.
Assisted-by: Claude:claude-opus-4-7:Claude Code
DockWorkspace now keeps a NavigationHistory<SharpTreeNode> and pushes the
selected tree node on each SelectedItem change (rapid <0.5s consecutive
selections collapse so tree refreshes don't pollute the back stack). Back/
Forward themselves suppress recording while they reassign SelectedItem.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Adds Language.GetRichTextTooltip and a CSharpLanguage override that
runs the entity through CSharpHighlightingTokenWriter to produce a
syntax-coloured signature, mirroring the WPF host. The decompiler view
now consumes Avalonia's built-in ToolTip system (PointerHover →
ToolTip.Tip), rendering RichText as Run inlines on a SelectableTextBlock.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Phase 2.2 of the references/tooltips work — clicking a local reference
(parameter, variable, label, ...) highlights every occurrence in the
current document with a colored background. Cross-document references
clear any existing marks before navigating.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Phase 2.1 of the references/tooltips work — click navigation only, no
hover tooltips and no local-reference highlighting yet.
Assisted-by: Claude:claude-opus-4-7:Claude Code
A cancelled decompile used to leave the tab title pointing at the new
node while the editor still showed the previous node's output. Move
Title and SyntaxExtension into the same UI-thread post that publishes
the rendered text, foldings, and highlighting model — so all five land
together when the decompile succeeds, and stay at the previous values
on cancel.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Cancellation flips activeCts.Cancel() but doesn't interrupt the
Task.Run — CSharpDecompiler must reach the next ThrowIfCancellationRequested
before the action returns. The previous finally-less version returned
early on cts.IsCancellationRequested, leaving IsDecompiling = true and
the adorner stuck forever.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors ILSpy/TextView/DecompilerTextView.xaml's wait-adorner shape: a
translucent overlay above the editor with a centred "Decompiling…"
title, an indeterminate ProgressBar, and a Cancel button. Visibility is
driven by a new IsDecompiling flag on DecompilerTabPageModel, set
around the Task.Run that runs the decompile. The Cancel button is
wired to a [RelayCommand] CancelDecompilation that triggers the
existing CancellationTokenSource.
Assisted-by: Claude:claude-opus-4-7:Claude Code
AvaloniaEditTextOutput now records MarkFoldStart/MarkFoldEnd ranges
into a NewFolding list (skipping single-line ones the way the WPF host
does), and DecompilerTextView installs an AvaloniaEdit FoldingManager
on the TextArea each time a fresh document is applied. Definitions
emit foldings unconditionally; users will get the all-braces flavour
once we expose DecompilerSettings.FoldBraces from the display options.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Wires up CSharpHighlightingTokenWriter so the decompiler view paints
keywords / types / members / parameters in distinct colours, identical
to the WPF host. Supporting changes:
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors the WPF host's light theme, which binds the text background to
SystemColors.InfoColorKey (~#FFFFE1) — the familiar pale-yellow tooltip
colour. Avalonia's SimpleTheme doesn't surface the same key, so set it
literally on the TextEditor.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Selecting a node in the assembly tree now opens (or refocuses) a
DecompilerTabPageModel document tab and renders the C# decompilation in
an AvaloniaEdit TextEditor with XSHD-driven syntax highlighting.
Assisted-by: Claude:claude-opus-4-7:Claude Code
GlobalExceptionHandler used to write to Debug.WriteLine and disappear,
which made debugging release-mode crashes painful. Now route the report
through a small modal Window that shows the type and message in a bold
header plus the full stack trace in a scrollable read-only TextBox.
Marshals to the UI thread when needed and parents itself on the main
window when one is visible.
Assisted-by: Claude:claude-opus-4-7:Claude Code
SessionSettings gains an ActiveTreeViewPath (string array) saved every
time AssemblyTreeModel.SelectedItem changes and consulted on the next
launch when the assembly list root is built. To make the path stable
across language switches, each tree node overrides ToString to return a
language-independent identifier — file path for assemblies, ITypeDefinition.
ReflectionName for types, and IL-formatted entity strings for members,
matching the WPF host's scheme.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors the layered drawing scheme from ILSpy/Images/Images.cs in the
WPF host: a custom IImage composes the base symbol with an
accessibility overlay (private / protected / internal / …), shrinking
the base to 80% to make room when present, and stamps Static or
Extension badges on top. Type / method / field / property / event tree
nodes now derive their icon from accessibility, IsStatic, and
IsExtensionMethod via Images.GetIcon. Overlay SVGs copied over from
the WPF Images folder.
Assisted-by: Claude:claude-opus-4-7:Claude Code
ProDataGrid measures the first child of a freshly expanded parent against
a too-narrow cell width on its initial pass; with a StackPanel that
clipped the TextBlock to nothing (and the only fix was selecting and
deselecting the row to force a remeasure). A Grid with the label in the
star column derives its width from the cell directly and sidesteps the
measurement quirk.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Restyles ProDataGrid's hierarchical presenter so the assembly tree feels
like the WPF SharpTreeView:
- 9x9 +/- expander box with the gradient fill and Path glyph from
ILSpy/Controls/TreeView/SharpTreeView.xaml, hidden via opacity on
leaves so column widths stay aligned
- LinesRenderer ported as TreeLines, drawn behind the indent/expander
via an override of the presenter's ControlTemplate
- cell focus rectangle, gray pointer-over highlight, and dark text on
selected rows all suppressed
Assisted-by: Claude:claude-opus-4-7:Claude Code
The Fluent theme combined with the Inter font made the app feel like a
modern UWP/WinUI surface, not the WPF/native Windows look the project
wants. Switching to Avalonia's Simple theme (and Dock's matching
DockSimpleTheme + DataGrid Simple.v2.xaml) gives flatter chrome with
denser default control padding. Dropping WithInterFont() lets Avalonia
fall back to the OS font (Segoe UI on Windows).
Assisted-by: Claude:claude-opus-4-7:Claude Code
Hooks DataGrid.DoubleTapped on the assembly tree pane: walks the visual
tree from the event source up to the row whose DataContext is the
HierarchicalNode wrapper, and calls IHierarchicalModel.Toggle on it
unless it's a leaf. Leaves (members with no children) ignore the
double-click.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Adds the Language abstraction so tree-node labels render through the
active output language instead of raw metadata names.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each AssemblyTreeNode now kicks off GetMetadataFileOrNullAsync() in
its constructor instead of waiting for the user to expand the node:
Assisted-by: Claude:claude-opus-4-7:Claude Code
New SessionSettings (ObservableObject + ISettingsSection) saved under
<SessionSettings> in ILSpy.xml: ActiveAssemblyList, WindowState, and
WindowBounds (Left/Top/Width/Height attributes).
Assisted-by: Claude:claude-opus-4-7:Claude Code
- Hold AssemblyListManager on AssemblyTreeModel and mirror its
AssemblyLists collection so the toolbar combo can bind to it.
- New ActiveListName ([ObservableProperty]); OnActiveListNameChanged
loads the named list via the manager and rebuilds Root.
- The "seed initial assemblies if empty" fallback now only triggers
for the (Default) list, so switching to an empty user list doesn't
auto-populate it.
- MainToolBar AssemblyListComboBox bound to AssemblyLists /
ActiveListName (TwoWay).
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replaces the temporary 3-column Grid placeholder in MainWindow with a
Dock.Avalonia DockControl driven by an ILSpyDockFactory and orchestrated
by a [Shared] DockWorkspace exported via MEF.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Adds the assembly tree pane (DataGrid-based, ProDataGrid hierarchical
rows), the SharpTreeNode-derived node type skeletons it consumes, the
empty MainToolBar placeholder, the ILSpySettings file-path provider
wiring in App.axaml.cs, and the AssemblyTreeModel.Initialize() hook in
the MainWindow code-behind.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Lifted out of "Assembly tree pane scaffolding" so the toolbar control
exists at the point MainWindow.axaml references it. The control body
is empty for now; later commits flesh it out with the actual buttons.
Strip the WPF ILSpy/, ILSpy.Tests/, and ILSpy.BamlDecompiler/ projects
and their solution entries so this commit can serve as the base for
replaying the rewritten Avalonia history on top. The BAML plugin needed
UseWpf + AvalonEdit and had a ProjectReference to ILSpy/ILSpy.csproj;
once both are gone it cannot build and would block bisect for every
intermediate commit. The Avalonia main app will absorb its three exports
(factory, file handler, entry node) in the later "BAML to XAML
decompiler integration" commit.
Decompiler.Tests targeted net11.0-windows only because
Microsoft.DiaSymReader.Native is Win-only and Tester.cs hardcoded ilasm.exe
/ ildasm.exe filenames. Both are narrow problems: gate the Native package
to Windows, replace the ".exe" suffix with an OS-aware helper, and drop
the -windows TFM. The Microsoft.NETCore.IL{,D}Asm metapackages' RID graph
already ships ilasm/ildasm for linux + osx via per-RID runtime sub-packages
when RuntimeIdentifier resolves dynamically, so the tool just lands in the
test output dir with the right suffix on every host.
Windows-only test fixtures (RoundtripAssembly + the RunWithTestRunner,
SignAssembly, FindMSBuild call sites) gate via Assert.Ignore +
[Platform("Win")] so they skip on non-Windows instead of failing. CA1416
joins NoWarn -- the analyzer can't see the runtime guards, and the test
inputs (Console.CapsLock, Registry access) are decompilation targets, not
infrastructure.
BuildTools/pre-commit learns to find dotnet-format on non-Windows (XDG
basedir + no .exe suffix) so the hook can run wherever the build now does.
Assisted-by: Claude:claude-opus-4-7: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.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the cluster-1/3/4 fixes converged every caller on the same matching
shape (match the slot/kind/type of a reference ILVariable, then check the
init value), the `Predicate<StLoc>` parameter was just a hole through which
each caller restated that logic verbatim. Fold the slot/kind/type check
into the helper and have callers pass just the reference variable and a
value matcher.
The multi-handler matcher only recognized a switch-instruction dispatch — but
when a try-catch has just two handlers (or a handful with non-consecutive K
values), Roslyn emits an if-chain instead:
if (num == K_1) br case_K_1; br nextBlock
; nextBlock { if (num == K_2) br case_K_2; <leave outer | br end> }
Add a parallel matcher that walks the if-chain and collects (K, case-block)
pairs the same way MatchSwitchDispatch does, plus the terminating leave/branch
as the default exit. Call it as a fallback when the switch matcher rejects.
Also clone the default-exit before re-adding it to the continuation block —
in the if-chain shape it's a child of a *different* block (a later step in
the chain), not the now-cleared switch instruction, so the in-place re-add
relied on the switch's release cascade and didn't generalize.
Closes Cluster 2 from #3745.
The flag-based early-return rewriter was tied to one specific lowered shape:
the try body's flag-setter had to be exactly `stloc flag(K); leave try`, the
post-try check had to be a `br checkBlock` (not an inline `IfInstruction`), and
the early path had to be a direct Leave or a forward to a one-instruction
leave-block whose target was the function body. None of those hold for
`try { try { return X; } finally { await ... } } finally { await ... }`:
- The inner flag-setter has a leading capture-forwarding store
(`stloc capture(X); stloc innerFlag(K); leave inner-try`).
- The inner check-block's early path branches to a multi-instruction helper
that sets the *outer* flag and leaves the outer try, instead of being a
direct return.
- SplitVariables hands out a separate ILVariable for the pre-init flag store
when the in-handler store is in a disjoint dataflow region.
Rebuild the matcher around the idea of a "template" — the chain of stores
the early path performs before its terminating Leave. Each flag-setter then
becomes its own prefix stores + a clone of the template, which collapses the
inner-then-outer flag chain in two passes (inner first, outer second, because
descendant order visits the inner TryFinally first). Also extend the
flag-setter scan to walk the whole try-block's descendants — after the inner
rewrite, the inner's spliced flag-setter lives inside the inner-try container
but still leaves outwards to the outer try, so it's an outer flag-setter from
the outer's perspective.
Add a `RUNTIMEASYNC` preprocessor symbol (defined when `EnableRuntimeAsync`
is set) and gate the new return-from-try-finally fixtures on it — the
state-machine async pipeline doesn't recover this shape, so it would expand
the same source into the `int result; try { ...; result = X; } finally { ... }
return result;` verbose form and the Async (state-machine) pretty test would
regress.
Closes Cluster 1 (1.1, 1.3) from #3745. Cluster 1.2 (void `return;` at the
end of a try-finally body) and 1.4 (break/continue across a try-finally) are
left for a follow-up: both round-trip semantically equivalently but the AST
emitter drops a trailing void `return;` and the break/continue lowering uses
a switch dispatch that the current single-K matcher can't recognize.
`try { throw new ...(); } finally { await ... }` lowers to a try whose only
exit is the throw (handled by the synthetic catch). The existing matcher
required at least one outward Branch to the continuation, which is too strict
— a throw-only try body produces zero outward branches but is still a valid
lowered shape. Two follow-on fixes were also needed:
- The pre-init's ILVariable diverges from the in-handler store after
SplitVariables when the try body has no path that reaches the dispatch's
load without going through the catch; match the flag init by slot/kind/type
instead of identity (same workaround the multi-handler matcher uses).
- With a throw-only try body the new TryFinally has unreachable endpoint,
so appending the no-exception successor after it would put a non-final
unreachable-endpoint instruction in the parent block. Skip the append in
that case — the parent block's endpoint is already correctly unreachable.
Closes Cluster 4 from #3745.
The single-handler try-catch matcher was tied to the top-level shape: it
required the try-catch be the last instruction in its parent block, that the
post-catch "no exception" path be a direct Leave that exits the function, and
that the flag-init's ILVariable be identical to the in-handler flag store.
None of those hold for an inner try-catch sitting inside an outer try-finally
where both await — the inner is followed by a `br continuation`, the no-exception
path leaves the outer try-block (not the function), and SplitVariables hands
out a separate ILVariable for the pre-init store.
Drop the "must be last instruction" gate, accept Leave-to-any-ancestor and
cross-container Branch as the no-exception exit (extracted into a new
`IsContainerExit` helper), and match the flag-init by slot/kind/type the same
way the multi-handler matcher already does.
Closes Cluster 3 from #3745.