The headless UI tests synchronized with the application by pumping a fixed
number of frames (39 loops of RunJobs/Delay across 19 files) and by pressing
at a point computed once from a control's bounds. Both encode how fast the
machine that wrote the test was: on the loaded Windows Debug CI agent the
frame count comes up short and the point goes stale, which is the recurring
timeout in the tree context-menu tests and the reason each such failure was
repaired one test at a time.
Waiters.WaitForIdleAsync replaces the frame loops. It observes the actual
precondition - no dispatcher job queued at Background priority or above, no
assembly still loading in the background sweep, a frame rendered - and
requires it on two consecutive polls so a thread-pool continuation about to
post back is caught as well.
Window.ClickAsync replaces element-targeted MouseDown/MouseUp pairs. It
re-resolves the target on every poll and presses only once the window's hit
test at the click point answers with that target, reporting the point and
what was hit instead on timeout. That diagnostic exposed one vacuous test:
User_Click_On_Visible_Row_Does_Not_Recentre_Viewport clicked the centre of a
row wider than the tree viewport, which lies under the decompiler text view,
so its assertion held without the row ever being clicked. It now clamps the
point to the viewport like the other tree-row clicks.
Clicks at text positions and press-only gutter clicks stay raw; they do not
target an element.
Assisted-by: Claude:claude-fable-5:Claude Code
The dark palette is hand-authored for C# only; every other highlighting
definition -- XML, IL, Asm, and all AvaloniaEdit built-ins -- is derived by
inverting HSL lightness. HSL lightness is not perceptual luminance, so the
result depended entirely on hue: blue carries a 0.0722 luminance weight, so
plain Blue landed at 4.08:1 against the editor canvas, and an already-light
source such as Asm's #8080FF inverted downwards to 1.29:1 -- invisible.
Reported against XML resources in #3986.
Enforcing a 5.5:1 WCAG floor on the converted foreground fixes every affected
definition in the one place they all route through, which a per-language
palette would not: the AvaloniaEdit built-ins (JSON, Markdown, JS, HTML, CSS,
Python) have no palette to author. 5.5 is where the existing CSharpDark values
already sit; the 4.5 AA threshold was measured and only moves the reported blue
to 4.51. The floor is deliberately foreground-only -- forcing a span background
to contrast with the canvas would repaint Asm's #EEEEEE Registers background as
a bright block and bury the text on top of it -- and it is measured against the
surface the foreground lands on, which is that span background when the colour
declares one, so a light-on-dark span cannot be pulled apart into two colours
that no longer contrast with each other.
The same function's desaturation guard only fired when the inverted lightness
stayed below 0.75, so a dark fully saturated source (DarkMagenta) came back
light and still fully saturated -- exactly the neon the softening exists to
prevent. Only the softening becomes unconditional; the lightness lift paired
with it stays scoped to over-saturated colours, because it is not monotone
across its own 0.75 boundary and would reorder neighbouring greys.
Hyperlinks were a second, unrelated path: nothing ever set
TextView.LinkTextForegroundBrush, so the About page and every decompiler-view
link used AvaloniaEdit's registered default of pure blue, 1.94:1 on dark. They
now share a themed ILSpy.LinkForeground with the metadata table's token cells,
which take it from a style rather than a local Foreground so the selected row's
white override still wins over the accent fill.
Assisted-by: Claude:claude-opus-5[1m]: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
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
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
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 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
WPF showed a row Offset column and decoded the Kind GUID into one cell
combining heap offset, friendly name, and raw GUID; the Avalonia table
only carried the bare heap offsets. Offset follows the GetRowOffset
convention every other table uses. Kind is plain text rather than a
hex column plus tooltip so the per-column filter matches the friendly
names.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Selecting a row in the CustomDebugInformation table now previews its
Value blob beneath it, completing WPF row-details parity: state-machine
hoisted local scopes, compilation options, metadata references, and
tuple element names parse into typed sub-grid rows; source-link JSON
shows as text; everything else — unrecognized kinds, embedded source,
and malformed structured blobs — degrades to a hex dump, as in WPF.
The parser switches on the Kind GUID directly via KnownGuids; the
entry deliberately carries no decoded-kind state, leaving that to the
kind-column work that builds on this.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
WPF ILSpy used DataGrid row details in three places: the COFF and
Optional header views (flag-bit breakdown of the Characteristics /
DLL Characteristics words, permanently expanded) and the
CustomDebugInformation table (decoded Value blob, visible when
selected). The Avalonia port already carried the data — both header
nodes populate Entry.RowDetails and the column builder skips it — but
nothing displayed it.
ProDataGrid builds the row-details template once per details element
and recycles the built control across rows, swapping only the
DataContext, so a WPF-style template selector would go stale;
MetadataRowDetailsControl re-runs its content factory on every
DataContext change instead. Per-row initial visibility has no
SetDetailsVisibilityForItem equivalent and recycling resets
AreDetailsVisible, so the view re-derives it in LoadingRow. Row
activation now ignores gestures originating inside the details
presenter — selecting text in a details blob must not navigate away.
This lands the infrastructure plus the two header consumers; the
CustomDebugInformation details build on the EmbeddedSource decoding
work and follow with it. ConfigurePage on MetadataTableTreeNode is
the hook that consumer overrides.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Clicking inside the open flags filter popup could act on the
DataGridColumnHeader underneath: unhandled press/release pairs bubbled
out of the popup into the header (which treats them as a sort click
and flashes its pressed visual), and on X11 overlay popups the
light-dismiss machinery could even re-target a click's press directly
at the header while the popup stayed open.
The filter UI is now hosted in a Flyout attached to the funnel icon
instead of a hand-rolled Popup parked in the header's panel. Avalonia
positions Popup as the low-level primitive and recommends Flyout for
attached pickers: light dismiss, Escape-to-close, focus handling, and
theme-correct presenter chrome come built in (the old hard-coded white
background was also wrong in dark mode). The flyout content swallows
unhandled wheel and press/release events, since its internal popup is
still logically parented to the funnel inside the header.
A headless end-to-end regression test drives a real DataGrid with
overlay popup hosts (the X11 OverlayPopups=true configuration the app
runs with) and raw input through the funnel and every visible flyout
control, asserting the header never sorts and never receives an
unhandled press or release.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
ECMA-335 names the zero state of most mutually exclusive sub-ranges
(NotPublic, AutoLayout, Class, AnsiClass, PrivateScope, ReuseSlot, ...)
and the reflection enums declare those members, but the schema inferer
skipped zero-valued fields and synthesised a generic '(none)' entry
instead. Zero fits every mask, so the member is attributed to a group
by declaration order: ECMA-335 and the reflection enums declare each
mask directly followed by its members. Filter persistence stores
numeric values, so saved filters are unaffected by the label change.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
The schema inferer only walks the enum's declared fields, so the
ECMA-335 Forwarder bit (0x00200000, no member in
System.Reflection.TypeAttributes) was invisible to the per-column
flags filter even though the cell tooltip already shows it. An
explicit per-enum extras table appends it as an independent flag,
making forwarders filterable on the ExportedType table.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
0x00200000 is the ECMA-335 Forwarder bit (II.23.1.15), set on
ExportedType rows that forward a type to another assembly.
System.Reflection.TypeAttributes has no member for it, so the
enum-driven flag enumeration left it invisible in the Flags group.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
The reflection-driven column builder turned every public property into a
column, so each {Column}Tooltip companion (NameTooltip, FlagsTooltip,
...) rendered as its own raw column next to the column it describes. That
text is already surfaced as the sibling cell's hover tooltip by
MetadataCellTooltip, which reflects the property off the row directly, so
dropping these properties from the column set leaves the tooltips intact
and removes the duplicate columns.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Keep the headless UI tests from timing out on the slow Windows runner:
decompile a small CoreLib type (System.Object) in the folding and
token-history tests instead of a large one, and raise the shared
decompile-wait timeout so a cold first decompile (JIT plus building a
CoreLib-scale type system) finishes in time.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The metadata-grid hover infrastructure shipped but no table entry fed it,
so every cell tooltip the previous version had was silently gone: the
heap-offset/value hints on string and blob columns, the entity description
on token columns, and the per-bit breakdown on flag columns.
Reinstate all of them. FlagsTooltip renders the rich per-bit view (a
checkbox per flag plus the selected member of each mutually exclusive
sub-range) instead of a flat string; GenerateTooltip on the table base
describes what a token column points at; the remaining columns expose
their heap-offset and value hints. MetadataCellTooltip now hands a
FlagsTooltip back as a built control and stringifies everything else.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The tree node's text leads with the table's token-kind byte (e.g.
"02 TypeDef (1234)"), but the opened tab's title dropped it and showed
just "TypeDef (1234)". Lead the tab title with the same byte so the two
match.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Jumping to a token (the "Go to token" entry and token-cell clicks) set
suppressHistoryRecording while selecting the target table, so the jump
left no back-stack entry and Back skipped over it. Stop suppressing so
the destination is recorded, and carry the target row on the entry
(TreeNodeEntry.MetadataRow) so Back/Forward restores the exact token
rather than the top of the table.
Assisted-by: Claude:claude-opus-4-8:Claude Code
To audit what each UI test actually exercises, every step now snapshots the
live window to <TestFixtureName>/<TestName>_<NN>_<ShortDescription>.png: a
booted frame (emitted automatically by TestHarness.BootAsync), one after each
state-changing action, and one before each assertion. Flip ILSPY_TESTS_VISIBLE=1
to render the filmstrip; it lands under %TEMP%/ilspy-test-captures (overridable
via ILSPY_TEST_CAPTURES).
The step number and fixture/test name are derived automatically so inserting a
breakpoint never renumbers the rest. The identity is recorded up front from the
real ITest in an ITestAction hook rather than read live: NUnit's
TestContext.CurrentContext does not flow onto async continuations, so a capture
after an await would otherwise collide under the ad-hoc context. And when
rendering is off the whole call is a true no-op -- not even a dispatcher pump --
so instrumenting a test can never perturb the navigation/tab timing it asserts
on. Full headless suite stays green.
Nearly every headless UI test opened with the same four-line prologue
(resolve the shared MainWindow, show it, cast its DataContext, wait for the
assembly list), then repeated the corelib lookup, the EnsureLazyChildren +
Children.OfType<T>().Single() drill, the registry single-by-header lookups,
and the open-an-assembly-and-wait dance. The duplication made the intent of
each test hard to see and every signature tweak a suite-wide edit.
Collapse those into TestHarness (BootAsync, OpenAssemblyAsync, GetCommand,
GetEntry) and TreeNavigation extensions (FindCoreLib, GetChild<T>, Expand<T>),
then apply them across the suite. Net ~865 lines of boilerplate removed with
no change in behaviour; the full headless suite stays green.
The PE/metadata nodes had collapsed onto the generic MetadataTable glyph
during the port. Bring back the per-node icons the previous version
shipped: a Metadata glyph for the root, a Header glyph for the DOS/COFF/
Optional headers, a Heap glyph for the heaps, a table-group glyph for
"Tables", and the folder glyphs for the Data Directories container. The
four missing SVG assets are ported from history.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Language and Language-Version pickers in the main toolbar have no
effect when the active tab renders PE-header / metadata-table fields
straight from the metadata, or shows a structural compare diff —
language choice doesn't change what's drawn. Mirrors WPF's per-tab
TabPageModel.SupportsLanguageSwitching flag.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Browsing portable PDB metadata required drilling three levels deep:
Assembly → Metadata → Debug Directory → Debug Metadata (Embedded).
The PDB's tables are now exposed as a top-level MetadataTreeNode
alongside the host module's Metadata folder, so PDB browsing is one
click away. The PdbProvider already cached for decompilation is
reused — no second parse.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Label and input now share the same Panel slot inside a horizontal DockPanel —
the funnel icon docks right and exactly one of {label, input} is visible at a
time. Hovering the header (or setting a filter, or opening the popup) swaps
the column name out for the input; otherwise the label stays. Two-row stack
is gone, so the data-rows no longer shift down when an input gets revealed.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each metadata column header now shows a small funnel icon to the right of the
column name. The TextBox / dropdown trigger collapses by default and only
becomes visible when the pointer is over the header, when a filter is set, or
while the [Flags] popup is open. Active filters tint the funnel SteelBlue so
the user can see at a glance which columns are constraining the view.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Extends the double-click-to-decompile path with a new-tab variant. RaiseRowActivated
now carries an OpenInNewTab flag; DockWorkspace branches on it to either reuse the
active tab (existing tree-selection path) or spawn a fresh DecompilerTabPageModel.
The view's OnGridDoubleTapped reads KeyModifiers.Shift; a new
DecompileMetadataRowInNewTabCommand exports the same gesture as a context-menu entry.
Assisted-by: Claude:claude-opus-4-7:Claude Code
DataGrid.DoubleTapped raises a RowActivated event on the metadata
page model. The dock workspace subscribes during AttachCustomContent
and resolves the row's metadataFile + Token to an IEntity via the
type system, then selects the matching tree node — which triggers
ShowSelectedNode → CreateTab → decompiler view, the same path a
click in the assembly tree takes. Falls through silently for rows
whose token doesn't resolve to an IEntity (heap rows, AssemblyRef,
constants).
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replace the string-token flag predicate with a numeric bitmask on
ColumnFilter (FlagMask, default -1 meaning "all rows"). The dropdown
on a [Flags] column now drives the mask directly:
Assisted-by: Claude:claude-opus-4-7:Claude Code
Right-clicking an entity hyperlink in the decompiler view now offers
"Go to token" — when the click lands on a reference whose payload is
an IEntity backed by a real metadata module, the entry routes through
the dock workspace to the matching CLI metadata table at the entity's
row. The decompiler text view also gets a context-menu host so other
[ExportContextMenuEntry] contributors that target a TextView surface
become reachable here too.
Assisted-by: Claude:claude-opus-4-7:Claude Code
A column filter wrapped in `/.../` is parsed as a case-insensitive
regex; anything else stays on the existing substring path. Unparseable
patterns fall through to a literal substring match so a typo can't
take down the filter row. Compiled regexes are cached per filter
text.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The path-based restore (SessionSettings.ActiveTreeViewPath +
FindNodeByPath) already round-trips through the metadata sub-tree's
stable ToString() values — these tests pin that down: selecting
TypeDef and a debug-only Document table both save paths whose final
segments resolve back to the originating node, including the second
"Metadata: Debug Metadata (Embedded)" hop into the embedded PDB
sub-tree.
Assisted-by: Claude:claude-opus-4-7:Claude Code
SessionSettings now carries HideEmptyMetadataTables (default true,
matching prior behavior) and MetadataTablesTreeNode honors it: when
true, tables with zero rows are hidden; when false, every CLI
TableIndex is surfaced. The setting persists across launches via
ILSpy.xml. There's no options dialog yet to flip it, but the plumbing
is in place for a future settings UI to toggle.
Assisted-by: Claude:claude-opus-4-7:Claude Code
DebugDirectoryTreeNode is now a lazy folder. Expanding it walks the
debug directory and adds a nested MetadataTreeNode per
EmbeddedPortablePdb entry, so the user can browse the debug-only
metadata tables (Document, MethodDebugInformation, …) the same way
they browse the host module's tables. The grid view (entry list) is
preserved when the node itself is clicked.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The metadata page's KeyDown handler now mirrors the "Go to token"
context-menu entry: when Ctrl+G fires while focus is on a token-kind
DataGridCell (or, as a fallback, while one is hovered), dispatch the
same NavigateToCellRequested path as the hyperlink-style click. The
focus-resolution + dispatch logic lives behind an internal
TryNavigateToTokenInCell hook so tests can drive it without simulating
the keyboard event end-to-end.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Upgrade the integration filter test to drive the live DataGrid: locate
the TextBox the column builder embedded in the Name column's header,
set its Text, and assert on DataGridCollectionView.Count — the visible
row count the user actually sees. Drop the temporary verification
fixture; the regression coverage now lives alongside the predicate /
event-forwarding unit tests.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Object-initializer Filter assignment lands on the DataGridCollectionView
instance but is gone by the time the grid finishes wiring it up — the
live view ends up with Filter=null, so Refresh() returns the unfiltered
source. Construct the view first, attach to the grid, then assign Filter
so the predicate sticks. The verification oracle now asserts on the
view's Count (the actual visible-row count) rather than re-running the
predicate manually, which catches this exact regression.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Selecting a tree node was firing ShowSelectedNode twice — once via
SelectedItems.CollectionChanged and once via SelectedItem
PropertyChanged. Each call ran node.CreateTab(), producing a fresh
MetadataTablePageModel with a fresh ColumnFilters set. The DataGrid
ended up rendering the second page's columns, but the first page's
filters were what tests (and any code reading the model) saw — so
typing into a header TextBox updated a filter no one was reading.
Track the last shown nodes inside ShowSelectedNode and short-circuit
when they match.
Assisted-by: Claude:claude-opus-4-7:Claude Code