The blob was tracked 100644, so every fresh checkout (and any branch
switch in a worktree hosting the shared .git/hooks symlink target)
produces a non-executable hook script, which git then silently skips
with only an 'ignoredHook' hint. With core.fileMode=false on this
clone a plain chmod is git-invisible; the mode has to be fixed in the
index.
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 #Strings/#US/#Blob nodes each open-coded the same offset-chained
walk, whose subtlety (handle 0 is a real entry despite being the nil
handle) is now documented once in WalkHeap. The TypeDef and
ExportedType entries built byte-identical TypeAttributes flag-group
tooltips; FlagsTooltip.ForTypeAttributes is the shared factory.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
The pane carried four hand-rolled try/catch export lookups that
predate AppComposition.TryGetExport and were missed when the other
call sites were converted.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
All six entity nodes repeated the same registry walk in LoadChildren;
AddAnalyzerChildren gives analyzer registration a single home, with
subclasses adding their entity-specific rows (accessors, backing
fields) before calling it.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
All six member-level Decompile* entry points wired the same assembly
resolver and debug info onto a fresh disassembler before dispatching;
the module-aware CreateDisassembler overload now owns that setup.
Assisted-by: Claude:claude-fable-5[1m]:Claude Code
Two Options tests selected the whole System.Linq.Enumerable type and then
awaited WaitForDecompiledTextAsync. That decompile takes >15 s in headless and
overran the 60 s wait on a loaded CI runner, so the suite flaked intermittently
(green in one run of a commit, red in another). The behaviours under test --
that a re-decompile setting refreshes the active tab, and that the refresh does
not steal focus from the Options tab -- fire on whatever the tab shows, so a
single small method exercises them just as well and decompiles near-instantly.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Selecting a C# version, switching to a version-less language (IL), then back to
C# left the toolbar's version ComboBox blank even though LanguageService had
restored CurrentVersion. The MVVM-toolkit setter runs OnCurrentLanguageChanged
(which assigns CurrentVersion, pushing it onto the bound SelectedItem) before it
raises PropertyChanged for CurrentLanguage (which repopulates ItemsSource from
the new language's versions). So SelectedItem is set against the previous
language's still-stale, empty list, rejected, and never re-pulled once
ItemsSource updates.
Re-assert the version combo's SelectedItem from the bound CurrentVersion after
each ItemsSource swap settles, keeping the model untouched.
Assisted-by: Claude:claude-opus-4-8:Claude Code
CaretHighlightAdornerTests waited on the wall clock (3s budget) for the
adorner's 1s real-time DispatcherTimer to remove it. Avalonia.Headless has no
simulated clock, so under a loaded CI runner the dispatcher-thread timer plus
the poll loop slipped past the tight budget and the test timed out
intermittently.
Extract the teardown the lifetime timer runs into Dismiss(); the test grabs the
live adorner and calls it directly, verifying the unregister half without any
timing dependency. Registration is still exercised through the real
OnReferenceClicked path, and production behaviour is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
These three project settings were present in the WPF build but lost in the
move to Avalonia. RollForward=major matters most: the default publish is
framework-dependent (publish.ps1 uses --no-self-contained) and the app targets
net10.0, so without it a machine that only has a newer major runtime installed
fails to start the app. DynamicAdaptationMode (DATAS) lets server GC shrink its
heap count to the live load for a smaller idle working set, and Debug-only
CheckForOverflowUnderflow traps arithmetic wrap during development.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Generate-PDB and assembly-node decompile failures reported only the exception
Message, which is too thin to act on: the runtime-async PDB-generation crash
surfaces as a bare "Object reference not set to an instance of an object" with
no indication of where it came from. Append the whole exception (type, message,
stack trace, inner exceptions) inside a default-collapsed "Exception details"
fold so the report stays scannable while the trace is one click away.
The fold span is counted in WriteLine() calls rather than embedded newline
characters, so the trace is emitted one line per WriteLine() -- a single
Write() of the multi-line string would collapse to one counted line and be
dropped as a single-line fold.
Also fixes the PDB tab title showing a literal "{0}": the GeneratingPortablePDB
resource is a format string whose placeholder was never filled.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Debug.Assert failures were routed straight to the single-button unhandled-
exception dialog and deduplicated by call site, so the only available action
was effectively "ignore". Restore the developer dialog the WPF host offered:
Throw raises the assertion as an exception, Debug breaks into the debugger,
Ignore continues, and Ignore All suppresses that call site for the session.
Asserts fire on the background decompiler thread, so the dialog marshals to the
UI thread and blocks the caller on a nested dispatcher frame until a choice is
made, matching how the WPF version stayed responsive.
Separately, cancelling a long-running operation tab left it blank; it now shows
"Operation was cancelled." so the frozen tab explains why it stopped.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Toggling a display setting that affects decompiler output (folding, member or
using expansion, debug info, IL detail, indentation) routed through
ForceRefreshActiveTab, whose ShowSelectedNode re-projects the tree selection
and so activates the preview tab -- yanking the user off whatever tab they were
on (e.g. the live Options page they were editing).
Add RefreshDecompilerOutputInPlace, which re-decompiles the cached decompiler
content directly without re-projecting or activating anything, and route the
Redecompile display-setting reaction to it. F5 reload and dependency-load keep
using ForceRefreshActiveTab, where re-projecting the selection is intended.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Project/solution export (and other RunInNewTabAsync work) opened a tab with a
static title and a permanently indeterminate progress bar, even though the
whole-project decompiler already produces per-file DecompilationProgress that
was being dropped.
Wire that progress through: DecompilationOptions carries an
IProgress<DecompilationProgress> that DecompileAsProject sets on the
WholeProjectDecompiler; a new RunInNewTabAsync overload hands the work a
UI-thread-marshaled Progress<T> feeding the tab. The tab now animates the
braille spinner over its title (the operation name) and shows a determinate bar
plus the file currently being written and an "N of M" count. Solution export
reports per-assembly rather than per-file, since its assemblies decompile in
parallel and per-file reports would race. The determinate bar is pinned to 75%
of the overlay width so it doesn't jitter as the status text changes.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The README still described ILSpy as a WPF app. Reflect the current reality:
the desktop UI is cross-platform Avalonia (Windows/Linux/macOS), the solution
filters are ILSpy.Desktop.slnf / ILSpy.XPlat.slnf (no ILSpy.Wpf.slnf), Unix/Mac
can build and run the UI, and the build requires the .NET 11 SDK. In CLAUDE.md,
the net472 add-ins / installer / Windows-bound tests are Windows-only frontends
and extra tests, not a legacy porting backlog.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Make the repo-root pwsh scripts the mandated path for restore/build/update-deps/
format/clean/publish, and explain why: the bare dotnet commands prune the
packages.lock.json files (the recurring spurious lock-file diff). Add two code
conventions -- comments must read without any knowledge of the session that
wrote them, and edits made outside the session must not be reverted without
confirmation. Drop the "Framework-specific gotchas" section, which only covered
a few narrow UI control quirks.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Debug Steps pane previously only served the ILAst language (one node per
IL transform). The C# decompiler's intermediate AST states were instead
exposed as a row of extra "C# - after TRANSFORM" dropdown languages, which
cluttered the language list and only let you jump to one fixed step at a time.
Generalise the pane to an IDebugStepProvider interface: ILAst keeps its IL
steps, and the C# language now contributes one step per AST transform, mapping
a selected step onto options.StepLimit (already honoured by CreateDecompiler).
Each language also supplies its own options object, so the writing-options
checkboxes show for ILAst and nothing for C#. The per-transform dropdown
languages are removed in favour of this.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ILSpy targets net10.0, not net10.0-windows; the VS add-in build paths, the
installer output dir, the local-dev publish script, and the VS Code launch
config still referenced the old net10.0-windows layout. Align them with the
actual TFM, matching publish.ps1 and the build workflow.
Assisted-by: Claude:claude-opus-4-8:Claude Code
In pwsh the bare '-xr!*.pdb' 7z argument is split into two tokens, '-xr!*'
(which excludes everything) and a bogus '.pdb' source, so the Release
binaries zip added 0 files and the step failed. Quote the exclusion so it
reaches 7z as one token, and pin the job's run shell to pwsh via defaults
so no step silently inherits a different default shell.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ICSharpCode.Decompiler / ILSpyX / BamlDecompiler set RestoreLockedMode but
declared no RuntimeIdentifiers, so their lock files were RID-agnostic. A
RID-specific 'dotnet publish -r <rid>' of ILSpy (which declares all four
distribution RIDs) propagated the RID to these locked project references and
failed restore with NU1004 unless --no-restore or --force-evaluate was used.
Declare the same win-x64;win-arm64;linux-x64;osx-arm64 set on the three
libraries and regenerate the locks so a locked-mode publish works for every
platform out of the box.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Pull in the submodule's new root Directory.Build.props so the decompiler
test fixtures aren't subject to this repo's TreatWarningsAsErrors=true.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A clean build currently has zero warnings; enforce that so new ones can't
accumulate unnoticed. Set TreatWarningsAsErrors in the root
Directory.Build.props rather than per project. ICSharpCode.ILSpyCmd keeps
its existing TreatWarningsAsErrors=false (project-level wins over the
imported default), and the three legacy net472 projects (the VS add-ins
and the installer builder) opt out explicitly since they only build on
Windows and aren't covered by the cross-platform CI that gates this.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Microsoft.DiaSymReader / .PortablePdb packages pull NETStandard.Library
1.6.1, which drags in 4.3.0 builds of System.Net.Http, System.Private.Uri
and System.Text.RegularExpressions, all carrying known advisories (NU1902/
NU1903). They are framework-provided at runtime on net10.0; pin the patched
4.3.4 / 4.3.2 / 4.3.1 via central transitive pinning so NuGet audit passes.
Enabling transitive pinning also aligns a handful of other transitive
packages to their declared central versions.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The 23 typed table-row viewmodels each inlined the same
MetadataOffset + GetTableMetadataOffset + GetTableRowSize * (rid - 1)
arithmetic, and the four heap tree nodes each repeated an identical
lazy-cache / "{name} Heap (count)" header / CreateTab shell that differed
only in the heap name and how rows are walked. A single mistyped
TableIndex or a divergent cache shell would have been invisible across
that many copies.
Add MetadataTableTreeNode.GetRowOffset (with a MetadataReader+offset
overload for the one entry that lacks a MetadataFile) and a generic
MetadataHeapTreeNode<TEntry> base carrying the cache, header, and tab,
leaving subclasses to supply only the name and row materialiser.
Assisted-by: Claude:claude-opus-4-8:Claude Code
DockWorkspace carried the reflection-heavy metadata-grid resolution: read a
clicked token cell, resolve a double-clicked row to its tree node, and find
the metadata-table node a token points at -- with the "read the row's
metadataFile private field + int token" reflection block copy-pasted between
the cell-click and row-activation paths. As the WPF host kept this metadata
logic out of the main workspace, lift it into a MetadataNavigator service.
DockWorkspace keeps the dock-side actions (select, scroll-to-row, history
stamping, open-in-new-tab) and now delegates resolution; the public
NavigateToToken / TryResolveRowToTreeNode surface is unchanged, so the
external callers (ShowInMetadata, DecompileMetadataRowInNewTab) don't move.
The duplicated reflection collapses into one TryReadRow.
Assisted-by: Claude:claude-opus-4-8:Claude Code
AssemblyTreeModel carried a ~180-line node-finding subsystem -- resolve a
reference (entity / namespace / resource / assembly / metadata file) or a
saved path to its tree node -- that has nothing to do with the class's
selection and list-lifecycle responsibilities. Lift the implementation into
a TreeNodeLocator, with the locator taking the tree root and assembly list
as parameters so it stays stateless.
The model keeps its public FindTreeNode / FindNodeByPath / GetPathForNode as
thin delegators (they're called from DockWorkspace, SearchPaneModel,
AssemblyTreeNode and tests), so no external call site changes -- the god
class just sheds the implementation.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ApplyDocument was a 116-line god-method and the constructor a 112-line wall
of unrelated wiring. Both are linear sequences with no shared control flow,
so split them into intention-named steps:
ApplyDocument (now ~40 lines) orchestrates RebuildFoldings,
RestoreOrResetViewState and SwapCustomElementGenerators; the constructor
(now ~30 lines) calls SetupElementGenerators / SetupBackgroundRenderers /
SetupHoverHandlers / SetupContextMenuAndHyperlinks / SetupRichPopup /
SetupZoomAndCopy. Order is preserved exactly. Pure refactor, no behaviour
change.
The five renderer/generator/popup fields lose `readonly` since they are now
assigned in the ctor-only Setup helpers rather than the ctor body; they are
still single-assignment in practice.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The C# 13 'allows ref struct' anti-constraint is emitted as a single
PrimitiveType token (TypeSystemAstBuilder), so it reached WritePrimitiveType
rather than WriteKeyword and fell through uncoloured. Add it to the
value-type keyword group alongside the other constraint, 'unmanaged'.
Assisted-by: Claude:claude-opus-4-8:Claude Code
WriteKeyword coloured every modifier the decompiler emits except
'required' (C# 11 required members), which fell through and rendered in
the default text colour. Add it to the modifiers group.
Assisted-by: Claude:claude-opus-4-8:Claude Code
WriteKeyword/WritePrimitiveType/WriteIdentifier/WritePrimitiveValue each
ended with the same begin-span / base-write / end-span guard. Replace it
with `using (Colored(color)) base.WriteX(...)`, backed by an allocation-free
ref-struct scope so the per-token hot path takes no closure.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Options page is non-modal and live-apply, so -- unlike the WPF host,
which ran a full assembly-list Refresh() when its modal Options dialog
closed -- a setting that changes the decompiler/disassembler output (brace
folding, member/using expansion, debug info, IL detail, indentation) had
nothing to make it take effect; toggling it did nothing until the user
re-navigated.
Classify every DisplaySettings property in one table (DisplaySettingReactions:
editor-live / tree-text / tree-shape / re-decompile / none), grounded in the
actual consumers (ApplyDisplaySettings, GetIndentationString, the IL/mixed
languages). OnSettingsChanged dispatches on it, and a coverage test asserts
the table spans every settable property so a newly-added one can't silently
fall through.
Fixing the re-decompile case exposed that ForceRefreshActiveTab was itself a
no-op for an unchanged node -- ShowSelectedNode re-sets CurrentNodes, whose
setter dedups -- so RefreshDecompiledView (also used after dependency
resolution) never actually re-ran. Add DecompilerTabPageModel.Redecompile()
to force past the dedup and call it from ForceRefreshActiveTab.
Also drop the EnableSmoothScrolling setting: it drove TomsToolbox's
AdvancedScrollWheelBehavior in WPF, which the Avalonia port doesn't use and
never replaced, so the checkbox persisted a value nothing read.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The UseNestedNamespaceNodes and HideEmptyMetadataTables handlers both
force a materialised node to re-create its children (clear, re-arm
LazyLoading, EnsureLazyChildren), guarded on it not already being lazy.
That's a general tree-node operation -- a soft, in-place rebuild that
re-runs LoadChildren without reloading the assembly -- so it belongs on
SharpTreeNode next to the lazy contract it builds on, not as a private
helper in the model. Both call sites now use node.ReloadChildren().
Assisted-by: Claude:claude-opus-4-8:Claude Code
The drag-drop / file-drop callback selected the dropped assemblies with a
raw SelectedItems.Clear() + Add() loop, which flashes a transient empty
selection. The model has SelectNodes precisely to avoid that -- it brackets
the swap so the selection-changed fan-out fires once, with the final set,
never empty. A transient empty poisons the grid-sync deferred guard, after
which the tree stops following tab activation. Route the callback through
SelectNodes; a regression test asserts no empty SelectedItem is observed
mid-drop.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The eight DecompileX overloads each opened with the same five lines:
resolve the module, build the decompiler, emit both reference warnings and
the assembly-name comment. Fold that into BeginDecompile(IEntity, ...),
which returns the decompiler; each overload then adds only its own (varying)
type-header comment. The three DecompileExtension overloads, identical bar
the comment target, now share a DecompileExtensionCore.
Assisted-by: Claude:claude-opus-4-8:Claude Code
DecompileAsync and ShowText both pushed the same eight properties (syntax
extension, highlighting model + spans, foldings, references, definition
lookup, UI elements, text) onto the page. Fold them into ApplyOutput,
which reads the collateral straight off the now-immutable output and takes
the syntax extension and text as parameters -- so the decompile path keeps
computing GetText off the UI thread and supplies it, while ShowText passes
its own. Drops the six hand-captured locals in the decompile apply step.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "resolve assembly -> entity handle -> uncached DecompilerTypeSystem ->
ResolveEntity" sequence was copy-pasted in the code-view hover navigation
and the assembly-tree node finder. Move it onto EntityReference as a
Resolve(AssemblyList) member so both call sites just supply the list they
already have, and the uncached-per-call decision lives in one place.
Assisted-by: Claude:claude-opus-4-8:Claude Code
OpenNewTab's summary (and its sourceNode param doc) sat above ShowToolPane,
and FreezeCurrentTab's summary sat above SettleSelection, so two members
were undocumented while two carried a second, wrong summary. Move each back
to its member and fix the dead <see cref="OnActiveDockableChanged"/> (the
event is OnDocumentsPropertyChanged).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Six command outputs ended with the same AddButton(OpenExplorer ...) +
WriteLine() tail. Add AddOpenFolderButton / AddRevealFileButton on
ISmartTextOutput and route each site through them. This also drops the
no-op `isFile ? path : path` in the package-extraction output, which now
picks the reveal vs open helper directly.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "walk up to the enclosing AssemblyTreeNode" helper was byte-identical
in the open-terminal and open-containing-folder commands. Lift it to a
static on AssemblyTreeNode and route both through it.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ApplyDocument and the theme-change rebuild both removed the active
RichTextColorizer, nulled the reference, and added a new one. Pull that
into SetColorizer(RichTextModel?) -- a null model just clears it -- so the
two callers only supply the model (the per-document one, or the one rebuilt
from the captured spans on a palette change).
Assisted-by: Claude:claude-opus-4-8:Claude Code
SetActiveDockable + SetFocusedDockable were always called together at five
sites across the factory and the workspace. Fold the pair into one
factory method so the invariant (a surfaced dockable is also focused) is
expressed once; the per-site "add to the dock first" step, which varies
(unconditional / only-if-absent / none), stays at the call site.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The SortResX target only set its command on Windows (powershell), so the
resx-sorting build step was a silent no-op on Linux/macOS even though
pwsh (PowerShell 7+) is available there -- mirror the PowerShell cmdlet
project's powershell/pwsh split. The included Resources.resx change is
that step catching up one entry (ExportProjectSolution) that had drifted
out of order while the target was disabled. The script is EOL-preserving,
so this stays LF with no cross-OS churn.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The two were identical bar the skip predicate (the home tab vs the kept
tab); both snapshot the visible documents and route each through CloseTab.
Extract the shared loop as CloseTabsWhere(predicate).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The WPF host attached a registry-driven context menu to the search list
(ContextMenuProvider.Add) and showed a tooltip on every result column;
the Avalonia port carried neither. Right-clicking a result now opens the
same menu the trees use -- the selected result's entity is handed to the
entries via TextViewContext.Reference, so Analyze and the scope-search
entries light up -- and the Location/Assembly cells regain their
full-text tooltips (the Name cell already showed the file path per
Fix#1263).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The TryGetExport consolidation missed five more copies of the
GetExport-or-null idiom in the assembly-list pane and analyzer tree view
(TryGetLanguageSettings / TryGetContextMenuRegistry /
TryGetAnalyzerTreeViewModel plus an inline OpenNodeInNewTab guard).
Route them through AppComposition.TryGetExport like the rest.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The classic-desktop lifetime cast for the dialog owner, the clipboard and
application shutdown was repeated across the file commands, file pickers,
project export, the PDB selector, the taskbar service and the analyzer
copy entries. Replace those copies with a single UiContext exposing
MainWindow, Clipboard and Shutdown.
Assisted-by: Claude:claude-opus-4-8:Claude Code