Avalonia 12 Port and Removal of the WPF UI#3755
Merged
Merged
Conversation
Assisted-by: Claude:claude-opus-4-7:Claude Code Assisted-by: Claude:claude-opus-4-7:Claude Code
Assisted-by: Claude:claude-opus-4-7:Claude Code Assisted-by: Claude:claude-opus-4-7:Claude Code
Cherry-picked from master commits 4c8e606 + 4c347ef. Refresh (F5) on a selection inside a lazy-loaded resource tree (e.g. a .baml entry inside an embedded .resources file) was collapsing the selection back to the resources folder because FindNodeByPath ran before the assembly's metadata-file finished loading and the resource children hadn't materialised yet. Assisted-by: Claude:claude-opus-4-7:Claude Code
Three coupled wires that all hang off the assembly-list-changed signal: Assisted-by: Claude:claude-opus-4-7:Claude Code
1) post-refresh-redecompile: AssemblyTreeModel.RefreshInternalAsync now calls DockWorkspace.ForceRefreshActiveTab() after SelectNode. F5 on the same assembly list doesn't rebuild the tree → FindNodeByPath returns the same tree-node reference → SelectedItem setter early-outs → ShowSelectedNode's lastShownNodes dedup short-circuits, leaving stale decompiled text. The force path resets lastShownNodes and re-runs the decompile pipeline. Mirrors WPF's RefreshDecompiledView() call. Assisted-by: Claude:claude-opus-4-7:Claude Code
Persists the live dock layout to ILSpy.Layout.json next to ILSpy.xml on MainWindow.OnClosing; loads it on DockWorkspace ctor; falls back to factory.CreateLayout when the file is absent or fails to deserialize. WPF stays XML in ILSpy.xml — this is Avalonia-side only. Assisted-by: Claude:claude-opus-4-7:Claude Code
Two missing toolbar buttons (Manage Assembly Lists, Show Search) and an incorrect button order; the layout now mirrors WPF MainToolBar.xaml + InitToolbar exactly. Also fixes an InitializeButtons no-op under Avalonia.Headless (Loaded never fired; switched to AttachedToVisualTree). Assisted-by: Claude:claude-opus-4-7:Claude Code
Imports the three SVG assets that were missing from `ILSpy.Avalonia/Assets/Icons/` and registers them in `Images/Images.cs`, so the MEF-injected toolbar buttons for "Reload all assemblies", "Sort assembly list by name", and "Collapse all tree nodes" now render their icons instead of falling back to the tooltip text (`MainToolBar.axaml.cs:226` `ResolveIcon` returned null for these). Adds a regression test asserting every `[ExportToolbarCommand]`'s `ToolbarIcon` resolves to a real `IImage` field on the registry.
LMB double-click is WPF's expand/collapse gesture, already wired through `OnTreeGridDoubleTapped`. An earlier patch also added it as a third "open in new tab" gesture in `OnTreeGridPointerPressed`, so users got BOTH effects on every double-click (the row toggled *and* a new tab opened). Restore parity by removing the `isDoubleClick` branch from `OnTreeGridPointerPressed`; MMB single-click and Ctrl+LMB single-click remain the two new-tab gestures. Regression test in `HeadlessMmbPointerTests`.
The user-reported "Debug Steps pane is empty after clicking Show Steps" bug was a View-lifecycle issue: `DebugStepsPaneModel` declares `IsVisibleByDefault = false`, so the `DebugSteps` UserControl was realised by the dock factory only when its tab was first activated. By then `BlockILLanguage.DecompileMethod` had already fired `OnStepperUpdated` into the void — no subscriber existed yet.
The three editor-command tests (Copy_Entry_Reflects, Copy_Execute, SelectAll_Execute) selected `System.Linq.Enumerable` (the whole type, 300+ LINQ methods) and waited on `view.Editor.Document.TextLength > 0` — a 15s `WaitForAsync` poll. In suite context this was sloppy: it could latch on stale editor content from a previous test, and in isolation it occasionally raced past 15s on a slow system, producing the intermittent `failed: 1` we saw earlier. Assisted-by: Claude:claude-opus-4-7:Claude Code
Without per-dockable view resolution the dock chrome caches one view per slot and reuses it for every dockable that lands there, so a second pane sharing an Alignment (Analyzer + Debug Steps both Bottom) renders the first pane's content under the second pane's tab. Each dockable instead owns its view (IDockableViewOwner) and DockableViewRecycling hands that instance back keyed by dockable identity -- there is no app-lifetime global view cache to leak every transient document tab's view subtree. Resolution runs through the single application-wide ViewLocator (its dispatch map is introduced here rather than in the later DataTemplate cleanup) so the dock never re-resolves an owned view through the template machinery on a cache hit. Assisted-by: Claude:claude-opus-4-8:Claude Code
Replaces the reflection-driven Dock.Serializer.SystemTextJson._options mutation with a from-scratch JsonSerializerOptions in ILSpyDockJson.cs. Mirrors Newtonsoft's ListContractResolver semantics — polymorphism on the six IDockable interfaces, IList<T> -> ObservableCollection<T> substitution at deserialise time, writable-only property persistence, the standard ignore set (ICommand, [IgnoreDataMember]) — and keeps the existing back-reference / Task-shape strip plus the singleton-dockable CreateObject hook. Assisted-by: Claude:claude-opus-4-7:Claude Code
Generalises the existing internal StartupLog into a public AppLog with named categories. The Startup category (default-on) keeps the existing [startup +Nms] format and Mark/Phase API, so all 44 call sites get a mechanical StartupLog. -> AppLog. rename without behavioural change. Assisted-by: Claude:claude-opus-4-7:Claude Code
Sweep-up commit. The dotnet-format pre-commit hook keeps re-ordering these usings on every other commit; landing them once stops the hook from grumbling at unrelated diffs going forward. Assisted-by: Claude:claude-opus-4-7:Claude Code
Ports the WPF assemblyList_CollectionChanged history-prune from ILSpy/AssemblyTree/AssemblyTreeModel.cs to the Avalonia DockWorkspace.OnAssemblyListChanged handler. NavigationHistory<T> already exposed the RemoveAll(Predicate<T>) primitive; the caller wiring was the missing piece. Without it, a tree-row click after removing an assembly walked through NavigationEntry.DisplayText on a stale TreeNodeEntry, which hit MemberReferenceTreeNode.Signature -> Language.EntityToString -> ILAmbience.ConvertSymbol and NRE'd on a now-null ParentModule. Assisted-by: Claude:claude-opus-4-7:Claude Code
DockWorkspace previously subscribed to IFactory.ActiveDockableChanged hoping it would fire on tab-strip clicks. It doesn't — Dock's FactoryBase only raises that event from InitActiveDockable (layout structural init at startup/load). User-driven tab clicks set dock.ActiveDockable = X directly on the dock-model, which raises the model's own INotifyPropertyChanged but bypasses the factory event. Assisted-by: Claude:claude-opus-4-7:Claude Code
Stepper.Step's break-at-step-limit relies on Debugger.Break(), which is a silent no-op when no debugger is attached. The wiring (command -> RestartDecompileWithStepLimit -> DecompilationOptions.IsDebug -> context.Stepper.IsDebug -> Stepper.Step) was all correct, but the common-case "user runs from a terminal" left the gesture invisible. Assisted-by: Claude:claude-opus-4-7:Claude Code
UpdateLastDocumentCanClose was counting only factory.Documents.VisibleDockables to decide CanClose. After the user drags a tab out into a side-by-side split, Dock creates a second IDocumentDock that holds the dragged tab. factory.Documents still points at the ORIGINAL dock (now with one tab), so the handler updates CanClose=false on its remaining tab — but the new sibling dock's tab keeps the CanClose=true it had before the drag. Result: one tab closable, the other not, even though the user has two documents open. Assisted-by: Claude:claude-opus-4-7:Claude Code
The default ApiVisibility=PublicOnly setting filters search results through CheckVisibility, which rejects private entities. Compiler- generated names — async/iterator state machines (<Method>d__N), display classes (<>c__DisplayClass), anonymous-method closures (<>c) — are all private, so they were unfindable even when the user typed the exact full name. Assisted-by: Claude:claude-opus-4-7:Claude Code
Language.BracketSearcher defaulted to DefaultBracketSearcher (no-op); only CSharpLanguage overrode it. Switching the active language to ILAst or IL left caret-on-bracket inert. Assisted-by: Claude:claude-opus-4-7:Claude Code
Build the layout and call InitLayout up front in DockWorkspace instead of letting DockControl run it post-template-apply, and populate the locators explicitly: tool panes map their id to the [Shared] singleton pane, the Documents dock and main tab get entries for navigate-by-id, and HostWindowLocator is registered so panes can float into their own windows. Doing it before the chrome attaches keeps the layout's owner/factory wiring ahead of the first content realisation. Also drops the now-redundant per-VM DataTemplate blocks from App.axaml -- the single ViewLocator already resolves them -- and the unused MainWindowViewModel.DockFactory property and DockControl init flags. Assisted-by: Claude:claude-opus-4-8:Claude Code
Two halves to the fix: Assisted-by: Claude:claude-opus-4-7:Claude Code
The "Compare..." right-click flow opened a tab that came up blank because two stacked bugs masked each other. Assisted-by: Claude:claude-opus-4-7:Claude Code
User-reported: opening multiple document tabs and restarting brings them all back broken except the first one. Assisted-by: Claude:claude-opus-4-7:Claude Code
User-reported: Ctrl+LMB on an assembly-tree row both toggled the row in the multi-selection AND opened a new decompiler tab. ProDataGrid's Extended-selection mode treats Ctrl+LMB as a toggle, and OnTreeGridPointerPressed also treated it as "open in new tab", so every Ctrl+click fired both effects regardless of the e.Handled=true on our handler — selection lives in the DataGrid template's own pre-bubble pipeline. Assisted-by: Claude:claude-opus-4-7:Claude Code
WPF parity: under DEBUG, the language dropdown gains one extra entry per C# AST transform — "C# - no transforms", "C# - after FirstTransformName", … "C# - after LastTransformName" — so a developer can pick a step and see the decompiler's mid-pipeline AST output. Assisted-by: Claude:claude-opus-4-7:Claude Code
User-reported: editor hover tooltips were not wide enough to show full method signatures. Root cause was a three-way layout interaction — DocumentationRenderer.AddSignatureBlock created the signature SelectableTextBlock with TextWrapping.NoWrap, the outer Border capped MaxWidth at 600px, and the wrapping ScrollViewer disabled horizontal scrolling. So anything past 600px was simply clipped, with no way to reveal the rest. Assisted-by: Claude:claude-opus-4-7:Claude Code
WPF's Open-from-GAC dialog uses a ListView with five SortableGridViewColumns (Reference Name / Version / Culture / Public Key Token / Location), each click-to-sort, with the initial sort by name ascending. The Avalonia port had reduced this to a one-column ListBox showing the raw ToString() — all field-level information mashed into a single TextBlock, no per-field sort, no Location column, no localised strings. Assisted-by: Claude:claude-opus-4-7:Claude Code
Adds TabPageMenuItem (mirrors ToolPaneMenuItem) and a live ObservableCollection on DockWorkspace kept in sync with factory.Documents.VisibleDockables. MainMenu appends a separator + radio-style MenuItem per tab, with Header bound to Title and IsChecked bound to IsActive. WPF parity for the previously-skipped tabs section of the Window menu. Assisted-by: Claude:claude-opus-4-7: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 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
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 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 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 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
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
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
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
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
This was
linked to
issues
Jun 10, 2026
|
Great |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NOTE: The below summary/overview of the changes in this PR was generated by an LLM and reviewed by me.
Please report any issues or missing features you might encounter.
NOTE: Currently, our CI only provides pre-built artifacts for Windows.
Migrate the ILSpy desktop UI to cross-platform Avalonia 12
Summary
running on Windows, Linux, and macOS.
ICSharpCode.Decompiler,ICSharpCode.ILSpyX) was already cross-platformand is carried over largely unchanged; the work here is the UI host, its supporting
infrastructure, and the build/CI changes needed to ship a cross-platform app.
masteris fully merged in (0 commitsbehind). Forked at
e0954474a; opens with "Remove WPF ILSpy host (prep for Avalonia rewrite)".Tech stack
ProDataGrid hierarchical grids, Avalonia.Xaml.Behaviors.
Microsoft.Extensions.DependencyInjection+System.CompositionMEF, with a small bridge.preview SDK.
WPF -> Avalonia: what changed
The headline change is the UI host itself: the WPF
ILSpy.exe(AvalonDock, WPFSharpTreeView,FlowDocumenttooltips, WPF theming) is replaced by an Avalonia 12 app (Dock library, AvaloniaEdit,Avalonia-native tree + tooltips). The decompiler core is unchanged, so the comparison below is about
how the app surface stacks up against the WPF original (
master).At parity
The day-to-day feature surface is ported and working: the assembly tree and all node types,
drag-and-drop and assembly-list management, the decompiler languages (C#, IL, "IL with C#") with
C#-version selection, hyperlink navigation, hover tooltips, code folding, in-document search,
copy-with-syntax-highlighting (HTML clipboard), the output-length guard, Search (all modes, mode
hotkeys, middle-click open-in-new-tab), Analyze, the Metadata explorer, navigation history
(back/forward), Save Code, Generate Portable PDB, Open-from-GAC, command-line handling, plugin
loading, and the per-node / text-view context menus.
Where Avalonia is ahead of WPF
[Flags]value popups, persisted filterstate, token hyperlinks with
Ctrl+Ggo-to-token, and copy-cell / copy-row-as-TSV.progress tabs (determinate bar showing the current file + remaining count); WPF folded export
into "Save Code".
IDebugStepProvider), replacing the old per-transform dropdown "languages"; theILAst/BlockILviews are now visible in Release.Ctrl+0/+/-), folding chords (Ctrl+M/Ctrl+Shift+M),and menu gesture hints.
Known gaps vs WPF
those variants are not ported.
cannot be previewed and discarded as a unit.
crefresolution dropped — hover tooltips show the rawcreftext instead of aresolved, navigable entity link (cross-assembly resolution is wired to
null).association registration.
rather than the right-clicked line; no
Pdb2XmlDEBUG context entry).Build & CI (supporting the above)
System.Net.Http/System.Private.Uri/System.Text.RegularExpressions4.3.0 trio from the DiaSymReader chain).dotnet publish -r <rid>works in locked mode on any host.net10.0publish-path correction). VS add-ins and WiX installer remain Windows-only
net472.Issues addressed
Testing
ILSpy.Tests-- headless Avalonia NUnit coverage(
TestHarness.BootAsync,Waiters) across docking, tree, metadata, search, options reactions,and export.
Notes / follow-ups
net472/net11.0-windowsand build on Windows only.Screenshots (past versions, might not reflect the current state 100%)