Rebuild the assembly-tree namespaces eagerly, as the WPF host did#3885
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR restores the WPF host’s eager namespace-band construction for the Avalonia assembly tree to fix missing namespaces and reduce first-expand latency when Use nested namespace structure is enabled. It also adds indexing to make namespace/type lookups O(1) at any nesting depth and updates navigation to use those lookups.
Changes:
- Eagerly builds the full namespace tree in
AssemblyTreeNodein a single pass over top-level metadata types, with indexes for namespace and type lookups. - Simplifies
NamespaceTreeNodeto a label holder (with display escaping) and removes per-namespace lazy child loading. - Updates type navigation in
TreeNodeLocatorand adds Avalonia headless tests covering nested namespace visibility and lookup/navigation behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| ILSpy/TreeNodes/NamespaceTreeNode.cs | Removes per-namespace lazy loading, escapes display label, and relies on AssemblyTreeNode for populating children. |
| ILSpy/TreeNodes/AssemblyTreeNode.cs | Eagerly constructs namespace/type nodes in one pass and adds O(1) lookup dictionaries for namespaces and top-level types. |
| ILSpy/AssemblyTree/TreeNodeLocator.cs | Switches type reference resolution to use AssemblyTreeNode.FindTypeNode for nested-namespace correctness. |
| ILSpy.Tests/AssemblyList/NestedNamespaceTreeTests.cs | Adds coverage for nested-namespace visibility and lookup/navigation behavior in Avalonia headless tests. |
| ILSpy.Tests/AssemblyList/NamespaceTreeNodeTests.cs | Adds coverage for namespace label escaping and global-namespace rendering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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
siegfriedpammer
force-pushed
the
restore-eager-namespace-tree
branch
from
July 17, 2026 15:48
ca57b41 to
de011d9
Compare
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.
Problem
With Use nested namespace structure enabled, most namespaces are missing from the assembly tree, and the first expand of a large assembly lags. This is the same bug #3879 addresses.
Both symptoms share one root cause. The Avalonia assembly-tree nodes were written from scratch as lazy scaffolding rather than ported from the WPF host, and lost the single eager namespace build the WPF design (
release/10.1) used:NamespaceTreeNodeonly ever filters asRecurse/MatchAndRecurse, so the filter cascade computes itsIsHiddenas "all children hidden" — vacuously true for an empty child collection. The lazy build attached each namespace node while it was still empty, so intermediate namespaces that hold only sub-namespaces (e.g.System.Collectionsin an assembly that only shipsSystem.Collections.Generic) latched hidden and stranded everything beneath them.TypeDefinitionstable once per namespace node (the lag, at ~17k types).Solution
Restore
release/10.1's structure:AssemblyTreeNodebuilds the entire namespace band in one pass over the module's top-level types, and populates each node before attaching it, so the empty-childrenIsHiddenlatch never fires.TypeTreeNodeholds the resolvedITypeDefinitionit renders from, so painting a cell never re-enters the settings-keyed type-system cache. Master's lazy node did, on everyText/Icon/Filterread, rebuilding an effective-settings object and taking its lock each time.ReflectionNameis what interleaves each 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. That is the WPF order; grouping all types ahead of all namespaces was a visible departure.FindNamespaceNode/FindTypeNodeO(1) and correct at any nesting depth.TreeNodeLocator.FindTypeNode(hyperlink clicks, search-result activation,JumpToType) delegates to that index instead of walking children by display name, which never matched in nested mode.NamespaceTreeNodegoes back to a dumb label holder and re-escapes its display label viaILAmbience.EscapeName.Holding resolved entities means the tree is 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.
AssemblyTreeModelreloads the loaded assemblies when the computedTypeSystemOptionsactually change (Display-only settings never do, and cost nothing), then restores the selected node from its path — which re-expands its ancestors — the wayRefreshdoes. The WPF host got this for free: its modal Options dialog rebuilt the tree on close, where the Avalonia page applies live.Two deliberate departures from a literal 10.1 copy: keep the global-namespace
-node, and keep the cachedIsPublicAPIgetter. Both index dictionaries are cleared on rebuild so a nested/flat toggle leaves no stale entries.Tests
New
NestedNamespaceTreeTests/NamespaceTreeNodeTests/TypeSystemStalenessTestscover: intermediate namespaces stay visible; nested-namespace and nested-type lookups resolve; reference navigation resolves in nested mode; a namespace's types and sub-namespaces interleave alphabetically; a language-version change re-resolves the tree against the new type system and restores the selected node; label escaping; and — as a guard — that a namespace whose types are all filtered out by ShowApiLevel is still hidden by the cascade (mutation-verified against theFilter => Matchopt-out). FullILSpy.Testssuite green.Relationship to #3879
This overlaps #3879, which fixes the same display bug by keeping the lazy design and opting namespace nodes out of the filter cascade. This PR takes the alternative the discussion there converged on — restoring the WPF eager build — which fixes the display, the lag, and nested-mode navigation together, and keeps the cascade intact. #3879's own improvements (the
DecompilefullNamefix and theName→FullNamefixes in the MSDN/scope-search entries) are not included here and would be reintegrated on top.