Review of #3989 pointed out that guarding registration on the theme-aware
marker conflates "XSHD opts out", "already themed" and "already registered",
and leans on two non-contractual AvaloniaEdit details (the delay-load
wrapper's Properties forwarding and its materialize-on-touch behaviour).
Keying the pristine-colour snapshots by colour instance (ConditionalWeakTable)
instead of by definition makes the in-place theming idempotent no matter how
many definition identities expose the colours, so correctness no longer
depends on registration order or the marker; the guard remains only to honour
the XSHD opt-out and to skip redundant list entries. ApplyHighlightingColors
is now private so nothing can set the marker outside a registration.
Also from review: the new tests move to a uniquely named fixture (the old
name collided with Themes/ThemeAwareHighlightingColorizerTests), gain
coverage of the Light/Dark switch path after a dark startup, and the cache
characterization asserts the reconverted content instead of relying on inert
theme switches.
Assisted-by: Claude:claude-fable-5:Claude Code
With dark preselected, the first document of a session rendered with a
double-converted (washed-out) palette; resources showed it across every
token, C# only on tokens outside the hand-authored dark palette. The
same definition was registered with the theme manager under two
identities: HighlightingManager hands out a delay-loaded wrapper whose
members forward to the inner definition that HighlightingService.Load
registers during materialization. Registering the wrapper afterwards
snapshotted the shared colours AFTER the inner registration had already
darkened them, so the snapshot's "light originals" were dark values and
the rewrite darkened them a second time. In-session theme switches were
unaffected because the first touch happens in Light, where both
snapshots are pristine -- which is why the bug only appeared when dark
was already active at first touch.
Skip registration when the definition is already theme-aware: reading
the marker forces the wrapper to materialize, so the check observes the
inner registration. This also stops the remap from clobbering
definitions whose XSHD opts out via ILSpy.IsThemeAware.
Assisted-by: Claude:claude-fable-5:Claude Code
ThemeManager and ThemeAwareHighlightingColorizer split dark mode between
them: the manager darkens a registered definition's named colours in
place, the colorizer per-paint-remaps colours of unregistered
definitions. Running both on one definition converts every colour twice
and washes the palette out. The colorizer captured IsThemeAware once in
its constructor, so a definition registered after the colorizer was
created would be double-converted from then on. Today every colorizer
is created after registration (HighlightingService registers inside
GetByExtension/Load before returning), but that is a calling
convention, not an invariant; reading the flag per paint removes the
ordering dependency.
The colorizer's dark-conversion cache needs no matching flush: its keys
use HighlightingColor's content-based equality, so recolouring a source
colour in place changes its hash and the lookup misses instead of
serving a conversion of the old values. A characterization test pins
that, so an equality-semantics change in AvaloniaEdit shows up as a red
test rather than as stale colours.
Assisted-by: Claude:claude-fable-5:Claude Code
These explain the code by pointing at the front-end that used to implement it.
That front-end is no longer in the tree, so the referent a reader would go
looking for does not exist: "Mirrors WPF's RefreshDecompiledView() call" names
a method nobody can open. In almost every case the sentence beside it already
carried the reason, and the reference was an appendix.
Comments citing a live platform difference are left alone, because there the
comparison is the reason rather than a memory: Avalonia genuinely has no
global RequerySuggested signal, which is why SimpleCommand exists at all.
One had gone stale rather than merely redundant. DerivedTypesEntryNode
described consulting the active search term as a missing feature to reinstate,
but SearchTermMatches is deliberately a no-op so the assembly tree stays
independent of the search pane; the comment now says so.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A comment that justifies behaviour by pointing at the WPF front-end means
nothing to someone reading the file cold: the reason is either already stated
beside it or is not stated anywhere. Each of these now names the constraint
itself - why navigation waits for pointer-release, why a signature block wraps,
why the tree filter ignores the search term.
Comments citing an external product's documented behaviour as the source of a
rule are left alone; there the reference is the reason, not a memory of how the
code arrived.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Zoom was stored as the font size itself, so the zoom overlay had no way to
tell a Ctrl+Wheel zoom from a font size picked in the options dialog: any
size other than the hard-coded default made the overlay appear, and the
percentage was measured against that default rather than the user's font.
A separate multiplier restores the split the setting always implied - the
options dialog moves the base size, zoom scales it - so 100% means "the
font you configured", whatever that is.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Five fixtures covered the "Use nested namespace structure" setting, four of
them running the same toggle at a different layer: the model shape, the same
toggle awaited live, and the same toggle again asserting it reached the
SharpTreeView's rows. Each paid its own boot for a scenario that is one story
end to end, and together they were the second-largest block of time in the
suite after the process-list scroll loops. One test now walks the whole path
once, carrying every assertion the four had, including the nesting depth only
the first checked.
The comparison view's model-is-bound test is dropped: the test after it
renders rows out of that model, which cannot happen unless it is bound, and
it opened two fixture assemblies to prove it.
The expander hitbox test asserted the toggle measures 13x16 and its glyph 9x9,
then clicked 14px down to prove the area below the glyph is live. The click
proves the geometry; the measurements only restate it, and would fail on a
font-metric change that broke nothing. Its layout-settling loop slept 200ms
unconditionally, which is a race that usually wins - it now waits for the
condition it needs.
Assisted-by: Claude:claude-opus-5:Claude Code
These fixtures were written while porting to Avalonia, as an author's own
verification step rather than as coverage: reflection asserting that a type
derives from its base and that a property has the type it is declared with;
literals (MinHeight 29, Padding 3, MaxWidth 900) copied out of the .axaml
beside them; a property override asserted only so pane descendants stay
reachable from tests. None of them can fail except when someone deliberately
edits the line they mirror, and then they fail as a chore.
StartupPerfTests keeps its two [Explicit] benchmarks, which print per-phase
timings worth reading. The third was a wall-clock assertion (8 CoreLib copies
must settle in under 15s) that ran in CI, where a shared runner decides the
verdict; as [Explicit] it would be strictly dominated by the 200-assembly
benchmark it was derived from, so it goes.
Two fixtures are trimmed rather than deleted, because their kernel is real:
XmlDocLoader's ref-pack fallback has no other test in the repo, and the
MenuIcon metadata rasterisation was dropped once during the port already.
Both now assert that without booting MainWindow to reach it.
This is worth about two seconds - it buys reviewers less to read, not CI
less to do.
Assisted-by: Claude:claude-opus-5:Claude Code
* Set v11 RTM
* Update features in README.md
* Remove the two 900-iteration process-list scroll tests
* Keep Svg.Controls.Skia.Avalonia at 12.0.0.13
* 10.0.11 and Roslyn for net11p7
* Fix module-scan test failing when PowerShell's NGen images are stale
* Opt Pack NuGets out of the MSBuild server to fix SBOM generation
An export's ITextOutput goes nowhere: ProjectExporter and SolutionWriter both
hand the language a throwaway PlainTextOutput and build their own status
report, so a language writing failures into that output is invisible. The
failures travel on DecompilationOptions instead and are rendered by the caller
that owns the report - each one with its full exception in a collapsed fold,
which is what makes a bug report actionable.
Drive-by: WriteExceptionDetails split the exception text without trimming, so
for exceptions rendering a trailing newline the fold reached one line past the
last frame and swallowed the line behind it; and the tab's own decompilation-
failure path had regressed to dumping a raw stack trace instead of using that
helper.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The bookmark navigation tests asserted the one-shot line highlight by
polling the text view's renderer collection, but the adorner
self-dismisses after an ~800 ms lifetime driven by a DispatcherTimer.
On a loaded CI runner (the desktop job runs the UI and decompiler test
suites concurrently) the dispatcher can stall long enough that the
adorner registers and is dismissed again before the test's next
predicate check, so the wait misses the entire play and burns its full
60 s timeout; raising the timeout cannot help with that. Record the
last played line on DecompilerTextView as persistent evidence of the
one-shot highlight and assert that instead - it also pins the highlight
to the expected line, which the presence check never did.
Assisted-by: Claude:claude-fable-5:Claude Code
The tab-opening test only verifies document tab and selection wiring. Using a tiny in-assembly fixture avoids cold framework decompilation work in CI and lowers the chance of unrelated timeout noise.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Right-clicking inside the details editor inherited the metadata grid's
cell-oriented context menu, whose Copy entries are enabled only for a
hovered DataGridCell -- inside the details area there is none, so the
menu showed permanently disabled entries and selected text could not be
copied by mouse. The editor now carries the decompiler view's editor
menu shape: Copy (rich HTML copy with plain fallback, enabled while a
selection exists) and Select All. Enablement is decided in
ContextMenu.Opening, which only the context-request gesture raises;
the test therefore raises ContextRequested instead of calling Open().
Assisted-by: Claude:claude-fable-5:Claude Code
A one-line payload (short hex dump, tiny source-link document) rendered
as a squeezed strip barely taller than the row itself, which does not
read as an expandable details area. Floor the editor at 100px so the
details region stays visually recognisable regardless of payload size.
Assisted-by: Claude:claude-fable-5:Claude Code
The text-blob editor in the metadata row-details area hardcoded its font
and lacked the text view's flat selection highlight, so embedded source
looked different from the decompiled code right next to it and ignored
the user's font choice in the Options page. The decompiler-view look
(user-selected font applied live while attached, themed background,
square-cornered translucent selection) now lives in DecompilerTextEditor
itself, giving every surface hosting the editor the same appearance by
construction; DecompilerTextView and BuildTextBlob drop their now
redundant per-site styling.
Assisted-by: Claude:claude-fable-5:Claude Code
The CustomDebugInformation details area rendered decoded text payloads
(embedded source, source-link JSON, hex dumps) in a plain TextBox, which
shows code without any highlighting and materializes the whole formatted
text up front, so large embedded-source documents were expensive. Text
payloads now travel as a TextBlobDetail tagged with a file extension --
".json" for source link, the parent document's extension for embedded
source, none for hex -- and render in the theme-aware AvaloniaEdit
editor, which colours them via the existing highlighting registry and
virtualizes long documents. The editor's ThemeChanged subscription moves
from the constructor to OnAttachedToVisualTree so it stays paired with
the detach-time unsubscribe now that editors can leave and re-enter the
visual tree inside recycled row-details containers.
Assisted-by: Claude:claude-fable-5:Claude Code
The details content was pinned left and the text blob capped at 800px,
leaving dead space to the right of embedded-source text and the
flags/typed sub-grids. Let all three detail shapes stretch and give the
last sub-grid column the leftover width so the details area fills its
host row.
Assisted-by: Claude:claude-fable-5:Claude Code
ProDataGrid derives its scroll extent from the current scroll offset, so an
offset that is briefly too large inflates the extent, which permits a larger
offset again. A trackpad reaches that state within a few hundred sub-row
events: instrumented in the running app, a ~1500px list reported 8735px and
kept growing, the thumb collapsed to its minimum, and the end of the list ran
away from the user. A second defect slid the rows sideways by up to 10px - the
star-sized column is measured against the width including the vertical scroll
bar, leaving the grid convinced it has a scroll bar's worth of content to
reach.
Disabling the horizontal scroll bar, giving each grid its own
DefaultRowHeightEstimator, pinning RowHeight, forcing the vertical scroll bar
visible, and disabling scrolling on the template's inner ScrollViewer each
removed a symptom at most; none can break a loop that runs through the scroll
offset, and 12.0.4 is the newest release. A ListBox's virtualizing panel keeps
the extent a function of the items alone. The price is laying out the columns
here - so the header row and the item template have to be kept in step - and
losing sortable, resizable headers.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A collection that fails once the session has been granted - most plausibly
the target exiting before the stop command reaches it - left the task copying
the trace connection behind. That connection is torn down on the way out, the
copy faults, nobody awaits it, and the finalizer hands it to
TaskScheduler.UnobservedTaskException, which this app reports as a crash: a
second report of a failure the dialog's error bar had already explained
correctly, minutes later and detached from the gesture that caused it. The
CollectTracing2 fallback walks the same path, so one dying target produced two
of them.
The teardown order is the substance of the fix. The session connection has to
go first, because after the failure nothing else will ever end the read the
copy is parked on; the drain second, so that its own failure is observed
rather than abandoned; and the half-copied trace last, so nothing is still
writing into it when it is dropped.
The regression test pins the invariant rather than the symptom, because the
symptom is transport-specific: a Windows named pipe reports an aborted
overlapped read as cancellation, and a cancelled task is never unobserved, so
only the unix transport can produce the crash at all. What holds everywhere is
that no drain may still be running once the failure path is done with it.
Assisted-by: Claude:claude-opus-5:Claude Code
A refused unix socket raises SocketException, which derives from
Win32Exception rather than IOException, so it escaped the filter meant to
skip one unreachable process and failed the whole concurrent enumeration
instead: a machine where any .NET process exits between the port scan and
the connect showed an empty list. The classification is now a named
predicate covering both transports.
Two dialog defects shared a shape - state left behind by a query nobody is
waiting for any more. Rebuilding the bound collection on every filter
keystroke made the grid drop its selection and write that null back,
discarding the assemblies of a process the new filter still matched; and the
branch taken when nothing is selected cleared no loading flag, while the
query it superseded was no longer allowed to, so the progress bar animated
over an empty pane. Relatedly, the two-second command budget expired into a
process listed with null metadata, which made a slow machine look like a
runtime that answered with nothing; it is longer now, and expiry names the
process it gave up on, since the only thing that ever reaches the far end of
that budget is a runtime which will never answer.
The test gaps are closed the same way: a real dynamic assembly pins the
in-memory classification, the managed-only assertion is stated as the
property instead of a list of native names to exclude, and the .NET
Framework path gets live tests. Those showed that a desktop CLR process
mostly reports NGen native images rather than the IL assemblies behind them,
which is now recorded as the fidelity gap it is.
Assisted-by: Claude:claude-opus-5:Claude Code
With a modern .NET app the executable in the process list is a native
apphost that carries no IL, so the path a user can see is precisely the
one a decompiler cannot open - ILSpy itself is an example. Ask the
runtime instead: since .NET Core 3.0 every CoreCLR process serves a
diagnostics endpoint that names its managed entry assembly and, via an
EventPipe rundown, every assembly it has loaded, including ones behind a
single-file bundle or with no file at all. The endpoint answers the same
way on Windows, Linux and macOS and needs no privileges beyond same-user,
which also makes it the only workable route on macOS, where native
process introspection is gated by SIP.
The protocol and the nettrace container it returns are implemented here
rather than taken from Microsoft.Diagnostics.NETCore.Client, so the
feature costs no new package reference; the reader is scoped to loader
rundown events and steps over everything else by size. The rundown asks
for the loader keyword alone: the runtime's default set also collects the
JIT and IL-to-native-map rundown, which in a long-running process buries
the module list and overruns the session buffer, costing the very events
the dialog needs.
Windows additionally lists .NET Framework processes, which predate the
endpoint and are read from their OS module list instead.
Assisted-by: Claude:claude-fable-5:Claude Code
FindEntityInRelevantAssemblies now uses the metadata-level FindEntity
instead of parsing the ID string into type-system references and
resolving them per assembly. Two behaviors of the old path need
explicit handling because FindEntity only searches the modules it is
handed: reference assemblies are skipped so the search prefers an
assembly with a usable definition, and a member whose declaring type is
present only as a type forwarder is looked up in the assembly the
forwarder points to, which the assembly resolver may load on demand
(the old path got this through DecompilerTypeSystem resolution).
Assisted-by: Claude:claude-fable-5:Claude Code
The TypeDef table view derived FieldList/MethodList from the computed
member ranges (TypeDefinition.GetFields/GetMethods), and the MethodDef
view derived ParamList the same way; their FirstOrDefault is a nil
handle for an empty range, so memberless types and parameterless
methods displayed row 0. The stored column value is never 0: it is the
running list position, i.e. the next row's first member or one past the
member table's end. Read the raw columns instead, relative to the row
end so the widths of the preceding string-heap, blob and coded-index
columns need not be re-derived, and sized by the table the column
actually indexes (the FieldPtr/MethodPtr/ParamPtr indirection when
present). EventMap and PropertyMap already read their list columns from
the raw rows. Tooltips explain the empty-list-start semantics; rows
with members behave as before.
Assisted-by: Claude:claude-fable-5:Claude Code
Toggle folding picked the innermost fold containing the offset, but a
member's logical region is fragmented: the body fold starts at the
opening brace, the XML documentation has an independent fold, and the
header line belongs to neither, so toggling there collapsed the whole
enclosing type. Visual Studio's source editor keeps documentation
regions independent, but its metadata-as-source view treats the
member's leading trivia as one hideable unit; for a read-only
decompiler view the grouped behavior is the intuitive one.
The writer now records where an entity declaration begins, and the
definition's fold carries that logical start. Toggling targets the fold
whose logical region innermost-contains the offset, so the header line
targets the member rather than the type, and leading documentation
folds follow the member fold's new state. Inside the documentation the
doc fold itself is the innermost region and still toggles alone.
Toggle all folding now follows Visual Studio's Toggle All Outlining
parity: a mixed state expands everything, a uniform state flips.
Assisted-by: Claude:claude-fable-5:Claude Code
Clicking a local variable highlights its occurrences, but clicking a
member or type always navigated away, so there was no way to see all
uses of a member within the current view. With the new display setting
enabled (off by default, as discussed in the issue), a single click on
a member or type reference paints every occurrence in the view using
the local-reference highlight infrastructure, and Ctrl+Click performs
the navigation. Opcode references keep navigating on plain click.
Matching occurrences are compared by definition token and module
rather than by Equals, because a use site carries a specialized member
instance while the declaration carries the definition; this also fixes
the analyzer-driven highlight for specialized members. Unresolved
entity references from the IL and metadata views are compared
structurally to avoid building a type system per click.
The hand cursor promises navigation, so it is shown only when a click
would actually navigate: cursor queries factor in the setting and the
Ctrl state, and Ctrl presses/releases while hovering a reference
repaint the cursor via top-level key handlers, since keyboard focus is
usually elsewhere while hovering.
Assisted-by: Claude:claude-fable-5:Claude Code
The explicit 15-second budget timed out on a loaded Windows Release CI
runner (a different test of this fixture on each attempt, while the
sibling workflow run passed the same head). The waits are
completion-bounded - the predicate observes the end of an async
re-decompile - so the 60-second Waiters default costs nothing when
healthy and only converts runner slowness from a red run into a longer
green one.
Assisted-by: Claude:claude-fable-5:Claude Code
Filtering and un-filtering the Debug Steps tree re-arranges rows around
the selection: matches surface under newly expanded groups and the
restore step re-opens the selected step's ancestors. Without scrolling,
the selected row regularly ended up outside the viewport even though it
was expanded into view. The ViewModel now raises a reveal request after
each filter pass while a visible step is selected; the View answers it
by centering the row, because scrolling needs containers and the
ScrollViewer. The measurement anchors on the row header since an
expanded group's container spans its whole subtree, and it runs at
Loaded dispatcher priority so freshly expanded containers have valid
geometry.
Assisted-by: Claude:claude-fable-5:Claude Code
The filter drove every row's IsExpanded from the single IsFiltering flag
via a TreeViewItem style setter. Expansion lived nowhere else: user
gestures (expander arrow, double-tap, arrow keys) all write the property
with SetCurrentValue, which the style binding overwrites on every flip
of the flag. Starting a filter therefore destroyed the expansion state
the user had built up, and clearing it collapsed the whole tree,
burying the still-selected row under collapsed groups.
Row state (visibility + expansion) now lives on a StepNodeViewModel
wrapper per step, two-way bound from the style, so gestures persist in
the view-model. A filter session snapshots expansion on entry, hides
non-matches and opens only the paths to matches while typing, and on
exit restores the snapshot and re-expands the selected step's ancestors
so the selection stays visible. Wrapping is skipped for reference-equal
step lists because step replays re-report the same run and a rebuild
would wipe the state mid-navigation.
Assisted-by: Claude:claude-fable-5:Claude Code
The override modifier already navigates to the overridden member and
constructor initializers link this/base to the invoked constructor, but
the primary expressions carried no reference at all. Matching IDE
go-to-definition behavior, 'this' now references the current type and
'base' the base type; both directions are added together deliberately,
linking only 'base' would make the two keywords behave inconsistently.
Assisted-by: Claude:claude-fable-5:Claude Code
The analyzer panel could only walk downward (Overridden By, Implemented
By); finding what a member overrides required manually climbing base
types. The upward interface direction already exists as the Implements
analysis, so this adds the missing base-class direction as its dual.
The analyzer walks the override chain member by member via
InheritanceHelper.GetBaseMember instead of collecting all
signature-equal base members, so it stops at 'new virtual' shadow
boundaries, which hide any further base members from overriding. No
assembly-scope scan is needed; the walk is a direct type-system lookup.
Assisted-by: Claude:claude-fable-5:Claude Code
The Avalonia port dropped ILSpy's single-instance feature, leaving three
inert surfaces behind: the --newinstance / --noactivate switches, and the
"Allow multiple instances" option were parsed and persisted but never read,
and every launch started a new process.
The former WPF implementation also encoded the executable location in the
mutex name, so two ILSpy builds at different paths never shared an instance.
That broke the Windows "Open with ILSpy" shell command: it launches a fixed
executable and would not reuse a running instance started from elsewhere.
Reimplement it with portable primitives (named Mutex + named pipes, no
P/Invoke): the first launch for a user takes the mutex and listens; a later
launch forwards its arguments over the pipe and exits. The namespace is
derived from machine + user only -- never the location -- so any launcher
reuses the running instance. --instanceid is a runtime reuse filter matched
against the running instance's actual executable identity (via
Environment.ProcessPath, single-file-bundle safe), not part of the mutex
name, so it never re-introduces the location partitioning it replaces. The
VS add-in passes its bundled exe path as --instanceid to prefer its own
build while still sharing with a plain launch of that same executable.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A dynamic index access (a[b]) gave its IndexerExpression a
DynamicInvocationResolveResult with no symbol, so the brackets carried no
tooltip. Synthesize an indexer (FakeProperty, IsIndexer) on the target
type with the index parameters typed from the callsite delegate, and
attach it. Route it hover-only by detecting a DynamicInvocationResolveResult
directly on the node - which also covers an invoke-member's own
parentheses, so those stop producing a dead navigation link too.
Assisted-by: Claude:claude-fable-5:Claude Code
Lock the hover content the dynamic-tooltip work produces. Ambience-level
cases (CSharpAmbienceTests) pin that SpecialType.Dynamic renders as
"dynamic" and that a synthetic dynamic method renders its return and
per-argument types - including the full hover form, confirming the
unnamed synthetic parameters collapse to their types with no dangling
name. An end-to-end case (HoverOnlyReferenceTests) decompiles a dynamic
call and renders the symbol GetSymbol hands back, exercising the actual
synthesis (argument typing from the callsite delegate), not a hand-built
stand-in.
Assisted-by: Claude:claude-fable-5:Claude Code
References had two behaviors encoded in one bool: navigable links, and
"local" references that are non-navigable but highlight all occurrences
on click. Synthesized dynamic members were routed through the latter, so
they picked up the occurrence highlight even though each use is a
distinct synthetic member with nothing to group.
Model the three modes explicitly with a ReferenceMode enum on
ReferenceSegment (Link / LocalHighlight / HoverOnly). Dynamic members,
the dynamic constructor and the dynamic keyword now use HoverOnly: they
still show a hover tooltip (BuildHoverContent resolves any IEntity
reference regardless of mode) but get the arrow cursor, do not navigate,
and do not highlight. Local variables keep LocalHighlight. Plumbed via a
new isHoverOnly flag on ITextOutput.WriteLocalReference.
Assisted-by: Claude:claude-fable-5:Claude Code
TypeSystemInitStop now reports the size of the final reference set passed
to Init() instead of the raw resolve count, which included same-name
duplicates that the version dedup later drops. The schema smoke tests
asserted exact global event counts, but the providers are process-wide
and the decompiler fixtures run in parallel, so unrelated decompilations
could inflate the counts; every string payload now carries a unique
marker (and the string-less snapshot events a sentinel count) that the
assertions filter on.
Assisted-by: Claude:claude-fable-5:Claude Code
ILSpyX had no instrumentation, yet most UI-visible latency bottoms out
here: lazy assembly loads, the first-resolve cascade that metadata-loads
every assembly in a list snapshot, per-module search strategy runs,
analyzer scope scans over all assemblies and their references, bundle/zip
entry extraction, and PDB loading. The provider mirrors the
ICSharpCode.Decompiler design: Start/Stop pairs, keyword gating, and
IsEnabled() guards at every call site; per-entry package extraction is
Verbose because of its volume.
AbstractSearchStrategy.Search is now a non-virtual template method that
wraps the span around a new protected SearchCore, so derived strategies
cannot bypass the instrumentation.
Assisted-by: Claude:claude-fable-5:Claude Code
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