In nested-namespace mode a NamespaceTreeNode's display label is only its
last segment ("Generic"), while the full dotted path
("System.Collections.Generic") is what identifies the namespace in
metadata and to the docs site. Three call sites read the label where they
need the full path, so in nested mode each targets the wrong namespace:
decompiling a namespace node queries an empty one and titles the output
after the last segment; the MSDN URL points at the wrong page; and
scope-search-to-namespace scopes to the wrong name.
These are ds5678's fixes from #3879, reintegrated on top of the eager
namespace rebuild. #3879's other Name -> FullName corrections, in
AssemblyTreeNode.FindNamespaceNode and TreeNodeLocator, are already
covered here by the full-namespace-name and type-handle indexes, so only
the Decompile and search-entry cases carry over.
Assisted-by: Claude:claude-opus-4-8:Claude Code
With "Use nested namespace structure" enabled, most namespaces never
appeared in the tree, and the first expand of a large assembly lagged.
Both come from the same regression: the Avalonia assembly-tree nodes
were written from scratch as lazy scaffolding, not ported from the WPF
design, and lost the single eager build the WPF host used.
A NamespaceTreeNode filters as Recurse/MatchAndRecurse, so the filter
cascade computes its IsHidden as "all children hidden" -- vacuously true
for an empty child set. The lazy build attached each namespace node
while it was still empty, latching intermediate namespaces (those that
hold only sub-namespaces, e.g. System.Collections) hidden and stranding
everything beneath them. The cascade also force-loads every namespace
node's children anyway, so the per-node laziness avoided no work: it
rescanned the whole TypeDefinitions table once per namespace node.
Restore release/10.1's structure: AssemblyTreeNode builds the entire
namespace band in one pass over the module's top-level types, populates
each node before attaching it, and keeps two indexes -- full namespace
name -> node and type handle -> node -- so FindNamespaceNode/FindTypeNode
are O(1) and correct at any nesting depth. TreeNodeLocator.FindTypeNode
(hyperlink clicks, search activation, JumpToType) delegates to that
index instead of walking children by display name, which never matched
in nested mode. NamespaceTreeNode goes back to a dumb label holder and
re-escapes its display label via ILAmbience.EscapeName.
The band is built from the module's type system, like 10.1's, not from
raw metadata: each TypeTreeNode holds the resolved ITypeDefinition it
renders from, so painting a cell no longer re-enters the settings-keyed
type-system cache the way master's lazy node did on every Text/Icon/
Filter read -- each of which rebuilt an effective-settings object and
took its lock. Ordering the pass by full ReflectionName is also what
interleaves a namespace's types and its sub-namespaces into one
alphabetical run (a sub-namespace attaches when its first descendant
type is reached, landing at its own alphabetical slot among the sibling
types); grouping all types ahead of all namespaces was a visible
departure from the WPF order.
Two deliberate departures from a literal 10.1 copy: keep the global-
namespace "-" node, and keep the cached IsPublicAPI getter. Both index
dictionaries are cleared on rebuild so a nested/flat toggle leaves no
stale entries.
Holding resolved entities means the tree has to be rebuilt when a
setting changes the type system. Only one compilation is cached per
module, keyed on the effective decompiler settings, so a language-
version or decompiler-option change drops it and would otherwise leave
every node pointing at a discarded compilation -- stale labels, icons
and filters, and the C# 14 extension-block nodes shown against the wrong
version. AssemblyTreeModel reloads the loaded assemblies when the
computed TypeSystemOptions actually change (Display-only settings never
do, and cost nothing), then restores the selected node from its path --
which re-expands its ancestors on the way to revealing it -- the way
Refresh does. The WPF host got this for free: its modal Options dialog
rebuilt the tree on close, where the Avalonia page applies live.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Solution export reported once per assembly, when that assembly finished.
Nothing was reported before the first one did, so the tab sat on the
indeterminate spinner it starts with for most of the run and then jumped
straight to the end -- exporting two assemblies showed a spinner, 1 of 2,
done. A project that bailed out before decompiling never reported at all,
stranding the bar short of the end for the rest of the export.
Sum the per-project file counts instead: each parallel worker feeds its own
counts into a shared map and the bar reports their total. WholeProjectDecompiler
carries its whole file count on every report, so the total is known from a
project's first written file rather than its last -- measured on two real
assemblies, the bar turns determinate after 245ms instead of 15s, and moves
through 978 files rather than 2 assemblies. Each project closes its share out
in a finally, so bailing out or cancelling still lets the bar reach the end.
The denominator grows over the first second as projects discover their file
counts. The alternative -- enumerating every project's types up front -- delays
the export itself to make the bar look better, which is the wrong trade.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two gaps in the export paths, both visible from the same selection.
A selection holding an assembly that failed to load was turned away by
TryGetExportableAssemblies, so Ctrl+S fell through to the single-node save
and quietly wrote just the focused assembly -- the rest of the selection
vanished with no report. The predicate now only insists that something in
the selection loaded, and the exporter skips what it cannot decompile and
names it in the status report. That is also what the dialog always assumed:
its "not a valid assembly" row badge was unreachable, because no selection
containing one could get that far.
The dialog asks for an output folder and derived the .sln name from it,
while Save Code lets the user name the file. Now the dialog offers the name
too, in solution mode, defaulting (via the placeholder) to the folder-
derived name the exporter would pick anyway.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Save Code on several assemblies had its own copy of the export flow: its
own selection matcher, its own frozen-tab runner, and a hard-coded
"Exporting solution" tab title -- so the same operation read differently
depending on whether it was started from Save Code or Export Project,
which titles the tab after the assemblies. Route it through ProjectExport
like the single-assembly path already is, leaving one runner and one
matcher behind every flow that decompiles whole assemblies to disk.
Save Code keeps letting the user name the .sln (the Export Project dialog
only asks for a folder and derives the name from it), so the export
options now carry an optional solution file name; unset means the old
folder-derived name.
Assisted-by: Claude:claude-opus-4-8:Claude Code
File -> Save Code decompiled a whole assembly on a bare Task.Run: no
progress bar, no way to cancel, and (for a .csproj) diverging from both
normal decompilation and the dedicated Export Project command, which
already report progress. Route the assembly-save paths through the shared
UI instead:
- The .csproj export reuses the Export Project machinery (ProjectExporter
in a frozen, determinate-progress tab), so a large assembly reports
per-file progress and can be cancelled while the tree stays browsable.
- The single-file save runs behind the same RunWithCancellation overlay
that normal decompilation uses.
Both entry points into the project export compute the tab title in one
place, titling it after the assemblies being exported (their tree-node
labels, joined the way a multi-node decompile tab is) so the tab reads
the same whether reached via Save Code or Export Project.
Assisted-by: Claude:claude-opus-4-8:Claude Code
LocalFunctionMethod.MemberDefinition returned the instance itself, so the
declaration of a generic local function (whose base method is an identity
specialization) and its use sites (whose base methods carry the use-site
substitutions) never compared equal, and click-highlighting could not group
them. Returning a wrapper around the unspecialized base method lets the
token writer record the same definition object for the declaration and all
use sites. Method-group references to local functions additionally need
their own lookup, because GetSymbol() does not surface a
MethodGroupResolveResult.
The token-writer tests now keep the PEFile alive for the duration of each
test: recorded references are type-system entities that lazily read from
the PE image, and formatting one in an assertion message after disposal
crashed with an AccessViolationException.
Assisted-by: Claude:claude-fable-5:Claude Code
Textual assembly resources (JSON, XML, Markdown, plain text, ...) rendered
as an opaque byte count with only a Save button. Detect text vs binary from
the payload (size cap, BOM-aware decoding, strict UTF-8, rejecting control
characters) and pick a highlighting extension by resource-name extension,
falling back to content sniffing (angle-bracket for XML/HTML, an actual
JsonDocument parse for JSON) when the extension is unknown. Text renders as
the view's whole content with the matching highlighting; binary keeps the
byte-count-plus-Save presentation.
Container resources unpack their entries as raw byte arrays. Route those
through the same IResourceNodeFactory pipeline as top-level resources so a
nested .baml gets the BAML view, an image its viewer, and so on, instead of
the generic byte node; the .resources and !AvaloniaResources views also list
their entries by name. Register built-in AvaloniaEdit definitions (JSON,
Markdown, ...) with the theme manager on lookup so they follow the dark
theme like the bundled ones.
Assisted-by: Claude:claude-fable-5:Claude Code
Doc-comment folds were created with defaultCollapsed hardcoded to true,
so /// blocks always started collapsed. The new Display option is
bridged into DecompilerSettings like the other expand flags and drives
the fold's default state.
Detecting the last line of a doc-comment block requires sibling
navigation between trivia nodes, so trivia now carry parent, sibling
list, and index state. That state lives on the Trivia subclass to keep
AstNode's size unchanged, and the tree API treats trivia as a separate
navigation space: Next/PrevSibling and Remove work within the owning
list, Slot is null, ReplaceWith throws instead of aliasing the trivia
index into the parent's child slots, GetNextNode/GetPrevNode stop at
the list boundary, Clone and CopyAnnotationsFrom deep-copy and reparent
trivia, and CheckInvariant validates the trivia lists.
Assisted-by: Claude:claude-fable-5:Claude Code
AvaloniaEdit 12's TextView.InvalidateLayer only invalidates the TextView's
measure and never re-renders the per-layer child visuals, so the caret layer
repaints only on caret blinks (focused editor) or scroll changes. During
debug-step navigation focus stays in the Debug Steps panel, so the one scroll
repaint painted an early animation frame and nothing ever erased it: the frame
stayed composited as a caret-sized black line fixed in the viewport. The
adorner now invalidates the layer visuals itself on start, every animation
frame, and dismissal, and keeps its rectangles in document coordinates so the
deferred centering scroll cannot strand the highlight at a stale viewport
position.
Assisted-by: Claude:claude-fable-5:Claude Code
Avalonia apps pack every resource (compiled XAML, image/SVG assets, ...)
behind a single !AvaloniaResources manifest blob, which until now fell
through to the generic resource node and could not be browsed. Parse the
blob's index and expose each packed file as its own entry, mirroring how
.resources files are unpacked, so individual files can be viewed and
saved. The reader is bounds-checked against crafted offsets/sizes in the
same defensive spirit as the recent .rsrc parsing guards.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The step recorder is shared by C# AST and ILAst replay, so keep the public infrastructure out of IL.Transforms and pass language-specific node navigation through neutral node metadata.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Normal decompiles do not consume node ranges, so keep AvaloniaEdit node tracking opt-in and enable it only for step-limited replay output.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
The replay tests only checked DebugStepHighlight was non-null, which the
ancestor fallback satisfies unconditionally -- a regression widening every
highlight to the enclosing method would have passed. Assert instead that the
range lies in bounds, does not span the whole document, and (unless it is a
zero-length removal caret) covers non-whitespace rendered code.
Assisted-by: Claude:claude-opus-4-8:Claude Code
NodeLookup.AddNode indexed every annotation of every rendered node by reference
identity, but the debug-step highlighter only ever looks up the DebugStepMarker;
the rest were dead keys, and a shared annotation (ResolveResult and friends,
copied across nodes) would resolve to whichever node rendered last. Make
DebugStepMarker public and bridge only it -- behaviour-preserving for resolution
while dropping the per-annotation dictionary churn on every rendered node.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The step tree can run to hundreds of entries per decompile, so finding a
specific mutation means scrolling. Add a filter box in the pane's top-right
corner: a row survives when its description -- or any descendant's -- contains
the text (case-insensitive), keeping the path to every match, and the tree
auto-expands while filtering so matches nested under transform groups stay
visible. Implemented as an item-visibility converter over the existing TreeView
rather than switching to SharpTreeView, which would change the Steps contract
and rewrite the pane's tests for no functional gain here.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A step that removes a node has nothing left to highlight in the resulting text,
so range resolution fell back to the enclosing block and flooded it. Record the
changed node's surviving neighbours as seam anchors (captured before the
mutation) and split a step's candidates into precise / seam / ancestor tiers:
when neither the node nor its marker resolves, place a zero-length caret at the
gap -- the successor's start, else the predecessor's end -- and only fall back
to the enclosing block when no neighbour survives. A zero-length highlight is
rendered as a caret (positioned, pulsed, centered) with no background mark.
Assisted-by: Claude:claude-opus-4-8:Claude Code
MarkNodeStart and BeginSpan captured builder.Length to anchor a node range
or highlight span, but indentation is written lazily on the first token of a
line. A node or span opened at the start of an indented line therefore
recorded its start before the leading tabs, so the debug-step highlight (and
any span) extended back across the indentation to column 0.
Flush the pending indent in both before capturing the offset, matching the
WPF AvalonEditTextOutput.BeginSpan the Avalonia port derived from. The emitted
text is unchanged -- the indent is written either way, in the same place; only
the recorded start moves to the first real character.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Debug Steps "show state before/after" replay re-decompiles the active
tab; the headless UI tests inferred completion by polling IsDecompiling +
Text, which can return on stale state or hit the 60s wait deadline when
that shared signal races under CI load (the observed intermittent CI
timeout). RestartDecompileWithStepLimit now returns the decompile Task so
the replay completion can be awaited deterministically; the tests await it
instead of polling. The production IsDecompiling reset is unchanged -- the
last decompile's finally always resets it; the race was only in the test's
completion inference.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The C# debug-steps view highlights and centers the exact AST node a
transform changed; the ILAst view already had the step tree and
replay-at-step but produced no highlight. Bring it to parity.
IL rendering has no token-writer seam like the C# output visitor, so
per-instruction text spans are recorded by bracketing
ILInstruction.WriteTo via a new INodeTrackingOutput. The dominant
inst.ReplaceWith(newInst) transform pattern detaches the instruction
passed to Step, so ILTransformContext gains EndStep to record the
produced instruction; Stepper additionally records the position's
ancestor chain as fallback candidates before the step-limit throw, so
the "show state before" view -- which halts at the selected step --
still resolves to a surviving ancestor (ultimately the ILFunction).
The highlight-range resolver is shared with the C# language.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Record AST transform groups and mutation steps through the C# pipeline, replay selected steps with the stepper, and carry modified-node ranges through output so the Debug Steps pane can highlight the selected mutation without replacing its full step tree.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
ILSpy resolves an assembly's references against the target framework it
detects from the TargetFrameworkAttribute. When that attribute is missing,
wrong, or the user wants to force a different framework, there was no way to
hint the correct one, so references could resolve against the wrong runtime
pack or framework directory.
A LoadedAssembly can now carry a TargetFrameworkIdOverride that short-circuits
detection (it is the single value every LoadedAssembly-based resolution path
reads), is persisted in the assembly-list XML, and is carried across a reload
so a runtime change re-resolves against the new framework. The "Set Target
Framework" context-menu entry edits it through a dialog with a free-form text
box and an always-visible list of common monikers to pick from (the app forces
overlay popups, so a dropdown would be clamped inside the small dialog); input
is validated and converted from the short TFM users know (net48) to the long
FrameworkName form the resolver consumes (.NETFramework,Version=v4.8) via
NuGet. The direct DetectTargetFrameworkId callers in the decompiler core
(project export, language version) intentionally keep reading the real
attribute; only reference resolution is overridden.
Resurrects a 2020 prototype (branch tfmoverride) re-implemented against the
current ILSpyX/Avalonia code, whose surrounding structures no longer matched.
Assisted-by: Claude:claude-opus-4-8:Claude Code
- Extract MatchesBookmark for the token + assembly identity that tied a bookmark
to a member; GetLineForBookmark and DocumentContainsBookmarkToken each spelled
it out (four copies), so a change to the match rule had to be made everywhere.
- Add a FoldingSection overload to FoldingsViewState.Restore and route the
bookmark folding restore through it, dropping a duplicated checksum + match loop.
- When two bookmarks resolve to the same gutter line, draw the enabled glyph so
the line never reads as disabled while an active bookmark sits on it.
- Drop an out-of-band reference in a test comment.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two costs on the gutter's hot path:
The margin rebuilt its document-line -> glyph map on every Render, resolving
each bookmark by scanning the document's reference segments. A scroll, a hover,
or the 600 ms post-navigation pulse repaints many times a second, so this
rescanned the references per bookmark per frame. Compute the map once and reuse
it, clearing the cache only when the document or the bookmark set changes (the
model's DebugInfo/References are set before the text, so the document-change
hook sees them current).
CanToggleBookmarkAtLine, called for every gutter line the pointer moves over,
built a full bookmark including a captured editor view state -- a foldings
snapshot, scroll offsets, and a tree-path walk -- only to test it for null. It
now does an anchor-only check; the view state is captured only when a bookmark
is actually created.
Assisted-by: Claude:claude-opus-4-8:Claude Code
After scrolling to a navigated bookmark, the deferred apply restored the
caret/scroll captured when the bookmark was created, which overwrote the
centering that had just been computed for the resolved line. A bookmark
re-anchors by token / IL offset, so a decompiler-setting change that reflows
the C# moves it to a different line than the one saved in its view state; the
stale offset then scrolled that line back off-screen, leaving only the
highlight playing where it could not be seen.
Restore just the captured foldings now -- and before centering, since
collapsing or expanding shifts where lines sit -- and let the centered,
re-resolved line be the final caret and scroll position.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Every save reloaded the JSON sidecar into fresh instances and then replaced the
whole observable collection with Clear()+Add(). That dropped and re-added every
bookmark on each toggle or edit: the bookmarks pane keeps its selection by
reference, so it was cleared on every change (leaving the toolbar acting on a
stale or null selection), the grid's scroll reset, and a checkbox/name edit made
from the grid re-entered the DataGrid mid-edit while its ItemsSource was torn
down and rebuilt.
Reconcile in place instead: keep the existing instance for any anchor still
present (refreshing only its editable Name/Enabled), remove anchors that are
gone, and append new ones. A name/enabled edit leaves the membership unchanged,
so it now touches the collection not at all; structural changes touch only the
affected rows. Instance identity is preserved, so the pane's selection and the
gutter's references survive.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two fixes to the gutter's hover preview, the faint glyph shown on a hovered
bookmarkable line:
A removal click left the pointer hovering the line it had just cleared, so the
hover preview redrew a glyph there immediately and the click looked like a
no-op. The just-removed line's preview is now suppressed until the pointer
leaves that line; a jitter that stays on the same line keeps it hidden, and a
fresh hover after leaving shows it again.
The preview also never actually faded: the glyph is an SvgImage, which paints
through a custom Skia draw operation that ignores DrawingContext.PushOpacity,
so it always rendered fully opaque. The glyph is now rasterized once into a
bitmap, which DrawImage does composite at the pushed opacity.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The text area paints the I-beam across its whole surface, so the bookmark
icon gutter, line numbers, and folding margin inherited it by walking up the
visual tree. Those margins are click targets, not text, so the I-beam read
wrong over them. Force the normal arrow there, re-applying as the line-number
and folding margins come and go with their display settings.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Import read the chosen file through the tolerant sidecar-load path, which maps a
missing, empty, or unparseable file all to an empty list. Replace mode then
cleared the saved list and wrote the empty result back, so picking a corrupt or
non-bookmark JSON file for a Replace import silently destroyed the user's
bookmarks.
Import now reads through TryReadFrom, which distinguishes a valid (possibly
empty) file from a parse/read failure. A failure leaves the live and saved lists
untouched and returns false so the pane can report it; a valid empty file still
legitimately clears the list in Replace mode. The normal sidecar load keeps its
tolerant behavior.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Activating a bookmark only positioned the view when a decompiler tab was
already the active content: the pending bookmark was set on
DockWorkspace.ActiveDecompilerTab, which is null while a metadata table, the
Options page, or the About page is showing. Navigating from there selected and
decompiled the node but opened at the top with no line highlight.
The pending bookmark now travels to the decompiler model that ShowSelectedNode
routes the selection to, and is consumed in the document-apply step alongside
the view-state restore -- mirroring PendingViewState -- rather than reacting to
a property change. The earlier property-change path fired synchronously during
selection, scrolled, then a deferred document apply reset the caret to the top
with nothing left to re-apply. For a node that is already decompiled (re-shown
after an interlude, where no apply step runs) a ScrollToBookmark callback on the
model scrolls the live view directly, mirroring NavigateBookmarkInFile.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Bookmarks now capture and restore the requested decompiler view state, support rendered-line fallback anchors, expose hover and pane location affordances, route bookmark context-menu actions through the clicked offset, and update the JSON sidecar under a mutex so concurrent instances do not overwrite unrelated bookmark edits.
The plan file records the completed checklist and the remaining manual smoke-test gap.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
There was no way to mark and return to interesting spots in decompiled
code. This adds a flat (no folders, no labels) bookmark list: toggle on a
line from the context menu, the gutter, or Ctrl+B; see an icon in a new
left-margin gutter that honours a disabled state; and manage the list in a
dockable pane that auto-registers in the Window menu.
Bookmarks anchor by metadata token, never by a raw line number, so they
survive re-decompilation and decompiler-setting changes that reflow the C#
text: a definition line anchors to its token, while a line inside a method
body anchors to the method token plus an IL offset. Recovering an IL offset
needs the decompiler's sequence points, which the normal C# output did not
carry, so they are captured once at the WriteCode chokepoint and stored as a
per-document line/offset map. The map is also what places gutter icons and
scrolls navigation to the exact line.
The list persists to an ILSpy.Bookmarks.json sidecar next to ILSpy.xml; the
path logic is extracted into AppEnv/ConfigurationFiles so the dock layout
sidecar shares it. Navigating to a bookmark loads its assembly from disk if
it dropped out of the list (and only then offers to remove a bookmark whose
file is gone), then centres the line and plays a brief line flash plus a
gutter-icon pulse. Disabled bookmarks stay visible but are skipped by the
next/previous actions.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The IL view renders a macro opcode like ldarg.0 as the mnemonic (an opcode
reference) followed by the index written as a separate clickable local or
parameter reference. AvaloniaEditTextOutput's omitSuffix path used
TrimEnd('.') to drop the index, but the name ends in the index digit, not a
dot, so it was a no-op: the full "ldarg.0" was written and the index appended
again, producing "ldarg.00" (and likewise ldloc.NN / stloc.NN).
PlainTextOutput and the WPF AvalonEditTextOutput strip everything after the
last dot; the Avalonia port regressed this. Restore that behavior. Only the UI
was affected -- ilspycmd and the disassembler tests use PlainTextOutput.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A standalone portable PDB (or raw metadata stream) loads as a metadata-only
MetadataFile, but the tree node was left empty: LoadChildren early-returned for
any kind other than PortableExecutable/WebCIL, so the node showed an expander
over no children. This regressed in the WPF -> Avalonia port, which dropped the
old default switch arm; release/10.1 still populated such nodes with the
metadata table and heap nodes directly. Restore that behavior.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "Toggle folding" context-menu entry folded the block at the caret, so
right-clicking a fold on a different line than the caret toggled the wrong
block. Record the document offset under the pointer when the text-view menu
opens and expose it on TextViewContext as TextLocation; the folding entry acts
on that offset (falling back to the caret only when no click position was
recorded). The pointer handler now records the offset before the reference
lookup, so a document with foldings but no clickable references still gets a
position.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Analyze panel showed each signature as a flat string, so the type names
that carry the key information were hard to spot in long Used-By / Uses lists.
Render analyzer entity rows as syntax-highlighted rich text with the type-name
spans emboldened (issue #2164).
Replace Language.GetRichTextTooltip(entity) with a general
GetRichText(entity, conversionFlags, boldTypeNames): the hover tooltip is one
caller, the analyzer pane another. C# colours the signature via
CSharpHighlightingTokenWriter and bolds the type-name colour spans; IL renders
its disassembled header (its signature form), already lexically highlighted;
the base produces plain text. The IL method/type/field header is semantically
the same artifact as the C# signature, so it shares the one method.
Rendering is opt-in: nodes that want rich labels implement IRichTextNode, and
the single shared SharpTreeView cell renders its runs via the new RichNodeText
attached behaviour, falling back to the plain Text otherwise. SharpTreeNode.Text
stays the authoritative plain string, so search, copy and keyboard navigation
are unchanged; analyzer entity nodes derive Text from the same RichText so the
two never diverge.
Fixes#2164
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "Open from NuGet feed" chooser rendered every result with the default
NuGet logo, even though the feed search already returns each package's own
IconUrl - it was fetched into NuGetPackageInfo and then ignored. The list
looked like a wall of identical icons, unlike the NuGet gallery.
NuGetPackageInfo is an immutable feed DTO bound straight into the ListBox, so
it can't raise PropertyChanged for an icon that arrives later. Rather than
bolt mutability and an Avalonia image type onto that DTO, introduce a thin
per-row view model (NuGetPackageViewModel) that owns an observable Icon
defaulting to the logo and swaps in the real icon once downloaded, plus an
INuGetIconLoader service that fetches and decodes off the UI thread. Icons
are decoded to 64px (rendered at 32) and cached by URL as the in-flight Task
so each URL downloads once and concurrent requests share it; the loader is
held for the command's lifetime so the cache survives reopening the dialog.
A per-search CancellationTokenSource stops a stale result set's downloads
from painting onto rows recycled by the next search. Any failure - non-http
URL, network error, decode error - collapses to null so a dead icon URL just
keeps the default.
Also disable horizontal scrolling on the results list. With it enabled
(the default), rows measured at unbounded width, so the star column stopped
filling the viewport and selecting a row brought the overflow into view -
the list jumped sideways and grew a scrollbar. The row layout is meant to
fit the width (the description wraps/trims), so horizontal scrolling was
never wanted.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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