The debug-step highlight centred with a logical (line-number * line-height)
calculation posted at default dispatcher priority. That ignores collapsed
foldings above the target, races the layout of a just-applied document
(silently no-oping when the viewport is not yet measured), and does not recheck
the document, so a newer decompile could scroll the wrong content. The symptom
was unreliable centring, most visible in the ILAst view whose nested block
output is fold-dense and larger.
Route the highlight through the existing CenterLineInView helper that bookmark
navigation already uses: it centres via GetVisualTopByDocumentLine (fold-aware),
runs at Background priority so the document finishes measuring first, and
rechecks the document identity.
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
Place the selected debug-step node in the middle of the editor after replay so navigation lands with enough surrounding context instead of just barely scrolling the mark into view.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
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
BookmarkManager carried a byte-for-byte copy of ILSpySettings' private
MutexProtector, both guarding read-modify-write of a config sidecar in the same
settings directory so parallel ILSpy instances fold their edits in turn instead
of clobbering each other. Promote it to a single public type in
ICSharpCode.ILSpyX.Settings so the locking semantics live in one place.
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
- Route the bookmarks pane's double-click and next/previous navigation through
HandleExceptions() rather than discarding the Task with `_ =`, so an exception
raised during navigation reaches the global handler instead of vanishing.
- Drop the unused parameterless NextDefaultName() overload.
- Rename the misspelled Boomark.Disable.svg asset (and its loader string) to
Bookmark.Disable.svg.
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
Tidies up the bookmarks feature in response to review feedback:
- BookmarkMargin subscribed to the shared BookmarkManager.Changed event
in its constructor and never unsubscribed, so the singleton kept a
closed tab's margin (and its pulse timer) alive. The subscription now
follows the margin's time in the visual tree.
- LineHighlightAdorner.DisplayLineHighlight always added a fresh renderer
with its own timers; navigating repeatedly within the highlight
lifetime stacked them. Existing highlights are now dismissed first.
- ApplyOutput always built a new DecompiledDebugInfo, contradicting the
property's documented contract (null for non-C#, the Empty sentinel
when C# yielded no methods). It now honors that contract.
- The throwaway StringWriter used for sequence-point capture is disposed.
The MemberName doc comment still described a stale-token guard that was
deliberately removed: a token can resolve to a compiler-generated member
(e.g. a local function) whose name differs from the stored display name,
and a name check would wrongly reject a valid navigation. Comment fixed
to match the actual token-only navigation.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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
A field, auto-property, or event initializer is written once at its
declaration, but in IL it runs in every instance constructor that does not
chain to this(...) (and static initializers run in the static constructor).
The decompiler lifts the initializer from a single constructor, so its
breakpoint was emitted only there and the other constructors had none.
Two causes are addressed:
- The lift discarded the initializer's copies in the other constructors.
They are now kept on MemberInitializerInOtherConstructorsAnnotation and
replayed by SequencePointBuilder, mapping the same source location onto
each constructor's IL.
- PortablePdbWriter only emitted methods that DebugInfoGenerator discovered
through declaration syntax, so a constructor whose declaration is omitted
from the output (implicit default ctor, implicit static ctor, primary
ctor) dropped its generated points. Those functions are now emitted by
walking the sequence-point map directly.
PdbGen fixtures cover single, multiple, this()-chained, implicit, static,
primary-constructor, and field-like event initializers, pinning the
reconstructed breakpoint map against the C# compiler's.
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
ReadWin32Resources walked the PE resource directory tree with raw native pointer
arithmetic over attacker-controlled offsets, counts and sizes, with no bounds
checks, no recursion-depth limit and no cycle detection. The root section pointer
came from GetSectionData, whose length was read and then discarded, leaving every
dereference unbounded.
A crafted assembly could therefore turn merely opening it (the Save as project
feature reads these resources unconditionally) into an uncatchable process kill or
an out-of-bounds native read: a subdirectory entry pointing back at itself recursed
until the stack overflowed; an inflated entry count walked off the section end; and
a data entry whose Size was up to 4 GB made Buffer.MemoryCopy read far past the
section, faulting on an unmapped page or copying adjacent process memory into the
byte[] later written to app.ico/app.manifest on disk. None of this is containable,
since a StackOverflowException cannot be caught and the repo has no corrupted-state
exception handling. This is the sibling of the bundle signature fix in a154a7bbb.
Carry the section length alongside the root pointer and bounds-check every offset,
entry count, name-string length and data Size against it, cap recursion depth and
track visited directory offsets to break cycles. A hostile or truncated file now
yields a bounded, partial tree instead of a crash; well-formed resources parse
exactly as before. The parser no longer needs the whole PEReader, only a delegate
that resolves a data RVA to a bounded pointer, which is the seam the new tests drive
over a pinned buffer.
Assisted-by: Claude:claude-opus-4-8:Claude Code
SingleFileBundle.IsBundle scans for the 32-byte bundle signature and reads the
8-byte header offset stored immediately before it. That offset only exists in a
genuine bundle, where the signature sits near the end of the file. The scan
started at the first byte, so a crafted file with the signature at offset 0..7
made it read before the start of the buffer. On the production path that buffer
is a page-aligned memory-mapped view, so the read faults on the preceding
unmapped page with an AccessViolationException -- a corrupted-state exception
that bypasses the loader's catch and terminates the process merely from opening
the file. The bundle probe runs on every opened file, so this needs no user
action beyond open.
Skip the backward read unless the match is at least sizeof(long) bytes into the
buffer. While in the same file, bound ReadManifest's FileCount against the bytes
that remain before it pre-sizes the entry array, so a crafted manifest can no
longer request a multi-gigabyte allocation.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ILFunction.IsAsync is derived from the method signature, so .NET 11
runtime-async methods (MethodImplAsync bit, no MoveNext state machine)
report IsAsync without AsyncAwaitDecompiler ever populating
AsyncDebugInfo. Its Awaits then stays an uninitialized ImmutableArray,
and PortablePdbWriter threw an NRE building the MethodSteppingInformation
blob from that default struct. Runtime-async methods have no yield/resume
offsets to record, so guard on Awaits.IsDefault and omit the blob,
matching the C# compiler, which emits no stepping information for them
either. A genuinely zero-await classic state machine keeps an
initialized empty Awaits and is unaffected.
Assisted-by: Claude:claude-opus-4-8:Claude Code
DebugInfoGenerator asserted that every local variable's decompiled type
is equivalent to the metadata local-signature type at its slot. That
holds at IL-read time but not afterwards: variable splitting gives one
slot several typed variables, pinned-region locals are modeled as
pointers (int* vs int& pinned), and generic-context type-parameter
identity differs. The assertion therefore aborted PDB generation (Debug
builds) for ordinary inputs such as any method with a fixed statement.
The type is never written to the PDB - only the slot index and name are -
so the mismatch cannot affect the debugging experience. The slot index,
the only emitted value, is correct by construction: it is the IL
ldloc/stloc operand, sourced from the signature slot when the variable is
created and copied verbatim by SplitVariables; it is never reassigned.
Keep only the index-bound check and document why the type is not verified.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Decompiler warnings (ILFunction.Warnings, e.g. the DetectPinnedRegions
block-duplication notice) are surfaced as an EmptyStatement carrying only
a comment. VisitEmptyStatement prints no semicolon for it, and
EmptyStatement derives its StartLocation/EndLocation from its Location
field, which is only set when that semicolon token is written. The
statement was therefore left without a text location, and
SequencePointBuilder then asserted on the empty start location while
generating PDB sequence points - aborting PDB generation for any assembly
whose decompilation emits such a warning (e.g. System.Net.Requests).
Point the empty statement at the comment it carries (already printed by
the time the node ends, so its location is known), falling back to the
collapsed end-of-last-token position. Every printed node then has a
location, so the SequencePointBuilder invariant holds without
special-casing, and the statement lines up with the text it represents.
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 pretty DynamicTests exercised arithmetic and relational binary operators on
dynamic operands, but never the bitwise and shift operators (& | ^ << >>). Add a
BitwiseAndShiftBinaryOperators case so this operator family is pinned by a
round-trip test alongside the others.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The pretty DynamicTests only exercised += -= *= /= on a dynamic target, leaving
%= &= |= ^= <<= >>= unverified even though VisitDynamicCompoundAssign and
GetAssignmentOperatorTypeFromExpressionType already map them. Add them so the
full set of dynamic compound-assignment operators is pinned by a round-trip
test.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The correctness DynamicTests only ran binary + - * / on a dynamic operand, so
no test recompiled and executed decompiled output for any unary operator. That
gap is why ~ on a dynamic value (issue #3820) shipped uncompilable output
undetected: a correctness case round-trips the decompilation through the
compiler, so it fails the moment the decompiler emits something that does not
recompile.
Add ~, -, +, and ! cases. They pin the runtime semantics of the dynamic unary
path and would have caught the OnesComplement regression directly.
Assisted-by: Claude:claude-opus-4-8:Claude Code
VisitDynamicUnaryOperatorInstruction handled every dynamic unary operator
except ExpressionType.OnesComplement, so ~x on a dynamic operand fell through
to the unsupported-opcode error expression and produced uncompilable output
(an incomplete cast that fails to parse). Map it to the bitwise-complement
operator, like the sibling unary cases.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
NullPropagationTransform rewrote `c != null ? c.AccessChain : default` to
`c?.AccessChain ?? default` whenever the access-chain result was a non-nullable
value type. For a by-ref-like type (a ref struct such as Span<T>) that form does
not compile: a ref struct cannot be wrapped in Nullable<T> (CS8978). Exclude
by-ref-like return types from the null-coalescing rewrite.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
The PDB sequence-point tests were missing real while-loop input, and their residual comparison treated breakpoint locations as an unordered multiset. Add coverage for while/do-while fixtures and compare residuals in sequence order so stepping-order changes are pinned.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
Extends the breakpoint-map comparison to hidden sequence points, anchoring
each hidden point to the visible point it follows so the descriptor stays
independent of the IL offsets the decompiler reconstructs. Adds PdbGen cases
spanning try/catch/finally, switch, async/await, yield, loops, LINQ, pattern
matching and more, pinning the known residuals where the decompiler folds a
compiler-hidden branch into an adjacent point.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The PdbGen tests compared the reconstructed PDB to the C# compiler's
byte-for-byte, so any non-trivial method failed on reconstructed IL
ranges, hidden sequence points and local scopes - none of which the
decompiler can reproduce exactly. That left four of seven fixtures
[Ignore]d and the suite with almost no coverage.
Compare only what a debugging user actually feels: the visible (non-hidden)
breakpoint map, parsed straight from the sequence-point blobs and keyed by
method-definition row (shared between the PDB and the PE it describes). IL
offsets, hidden points, local scopes and the embedded source are dropped.
The compiler's own PDB is the oracle, so the tests assert correct debugging
behavior rather than the decompiler's past output. Methods where the
decompiler legitimately diverges pin an auto-derived residual snapshot, the
same accept-the-diff workflow as the pretty tests; a separate oracle-free
check rejects duplicate or overlapping sequence points.
Un-ignores ForLoopTests, LambdaCapturing and Members (its source is
regenerated to match the decompiler's per-type output, collapsing ~50 lines
of indentation-induced coordinate noise to two genuine differences).
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 analyzer tree set ShowLines="False" while the assembly tree uses "True", so
the Analyze panel was missing the connector lines the rest of the app shows. The
two panes already share the same SharpTreeView control and templates, and the
line geometry is driven entirely by the generic node Level/IsLast state, so the
analyzer hierarchy renders them correctly; the False was an unconsidered default
carried in when the pane was migrated to SharpTreeView. Align it with "True".
Assisted-by: Claude:claude-opus-4-8:Claude Code
The analyzer signature colours depend on the active theme, and the formatting
on the active language, but the highlighted RichText was built once and cached.
Toggling the theme or switching the decompiler language therefore left existing
analyzer results showing the old colours / syntax.
Key the node's RichText cache on the (theme, language) pair so it rebuilds when
either changes, and have the rich-text cell re-render rich rows on both
ThemeManager.ThemeChanged and the language settings' PropertyChanged (cleaned up
on Node change and on detach, like the existing subscriptions).
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 right-click menu in the decompiled code read in a near-random order. The
shared entry registry orders entries by a single Order value and only uses
Category to place separators, so a few defects scrambled the result: the
"Decompile" reference entry had Order=10, jumping above Copy/Select all; the
"Decompile to new panel" entry sat in a misspelt "Navigate" category (vs the
"Navigation" the other entries use), adding a stray separator; and it tied
Copy at Order 100 while "Go to token" tied Analyze at 200, interleaving
categories on the discovery-order tiebreak.
Pull the three navigation entries into a contiguous 150-170 band so they form
one block between the Editor (100s) and Analyze (200s) bands, fixing the
spelling so they share a category. No provider change: the existing by-hundreds
category layout is left intact, so the tree / search / metadata menus that share
the registry stay coherent.
Assisted-by: Claude:claude-opus-4-8:Claude Code
F# emits tail calls pervasively, but the 'tail.' prefix was dropped
entirely at the C# output stage, so the information never reached the
decompiled text. Render it inline as '/*tail.*/' before the call,
mirroring the existing 'constrained.' prefix comment in CallBuilder.
Fix#3817
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Simple theme sizes a menu row to its 16px icon plus margins (~24px), tighter
than the classic WPF menu. Raise MinHeight so the rows have comparable vertical
breathing room, and centre the icon, label and input-gesture text in the taller
row (the theme template leaves their VerticalAlignment unset, so a style reaches
them) instead of letting them sit at the top.
Assisted-by: Claude:claude-opus-4-8:Claude Code
* Migrate ILSpy.AddIn.VS2022 to SDK-style VSIX project
* Retire the legacy VS2017/2019 ILSpy add-in
* Build the VS extension via dotnet build on the slnx
* Inline ILSpy.AddIn.Shared into ILSpy.AddIn.VS2022
* Restore the VS add-in menus broken by the SDK-style migration
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
The project listed 201 explicit <Compile Include> and 119 <None Include> items
with EnableDefaultItems=false. Many test-case sources had been marked <None>
only because the C# compiler available when they were written could not build
them; the current Roslyn compiles them fine. The hand-maintained lists had also
drifted: five committed fixtures (Operators.cs, Issue3751.cs, three ILPretty
.cs) were on disk with passing tests but in no item list.
Switch to the SDK default **/*.cs glob and exclude only the sources that still
cannot be compiled into the test assembly, determined empirically by building
with everything included and removing what failed:
- IL-pretty inputs that are not valid standalone C# and *.Expected.cs golden
outputs that reuse type names (compile errors / duplicate definitions).
- MetadataAttributes.cs, which applies assembly/module attributes that break
NUnit test discovery (zero tests found) when compiled into the assembly.
The excluded sources stay <None> for IDE visibility; the harness compiles them
from disk at test time regardless. This compiles 23 more fixtures than before
while keeping every previously-compiled file. Default None globbing stays off so
the non-C# inputs (.il/.vb/.fs) remain the authoritative list.
Operators.cs, now part of the build, is normalized to the repo's tab indentation
by the format hook (a CS110 block used spaces). Verified: the full suite still
reports 2257 passed, 0 failed, 35 skipped.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The project listed all 576 source files as explicit <Compile Include> with
EnableDefaultItems=false, so every new file had to be added by hand. Switch to
the SDK's default **/*.cs glob and keep only the exclusions that are actually
needed:
- Properties/DecompilerVersionInfo.cs is generated before build and absent when
the glob is evaluated on a clean checkout, so it is still included explicitly
(with a paired Remove to avoid a duplicate once a prior build produced it).
- DecompilerVersionInfo.template.cs is a placeholder template, never compiled.
Also delete the hand-written DecompilerAstNodeAttribute.cs: the source generator
emits this attribute (RegisterPostInitializationOutput), so the on-disk copy was
stale dead code -- never compiled, and parameterless where real usages pass
[DecompilerAstNode(hasPatternPlaceholder: true)]. Removing it drops what would
otherwise be a third exclusion.
Default None globbing stays off so the explicit None entries remain the
authoritative list. The evaluated Compile set is identical to the previous 576
files; verified by a clean build with the version file both present and absent.
Assisted-by: Claude:claude-opus-4-8:Claude Code