Skip to content

Rebuild the assembly-tree namespaces eagerly, as the WPF host did#3885

Merged
siegfriedpammer merged 1 commit into
masterfrom
restore-eager-namespace-tree
Jul 17, 2026
Merged

Rebuild the assembly-tree namespaces eagerly, as the WPF host did#3885
siegfriedpammer merged 1 commit into
masterfrom
restore-eager-namespace-tree

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Jul 16, 2026

Copy link
Copy Markdown
Member

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:

  • A NamespaceTreeNode only ever filters as Recurse/MatchAndRecurse, so the filter cascade computes its IsHidden as "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.Collections in an assembly that only ships System.Collections.Generic) latched hidden and stranded everything beneath them.
  • The cascade force-loads every namespace node's children regardless, so the per-node laziness avoided no work — it rescanned the whole TypeDefinitions table once per namespace node (the lag, at ~17k types).

Solution

Restore release/10.1's structure:

  • AssemblyTreeNode builds the entire namespace band in one pass over the module's top-level types, and populates each node before attaching it, so the empty-children IsHidden latch never fires.
  • The band is built from the module's type system, like 10.1 — each TypeTreeNode holds the resolved ITypeDefinition it renders from, so painting a cell never re-enters the settings-keyed type-system cache. Master's lazy node did, on every Text / Icon / Filter read, rebuilding an effective-settings object and taking its lock each time.
  • Ordering the pass by full ReflectionName is 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.
  • Two indexes — full namespace name → node, and type handle → node — keep FindNamespaceNode / FindTypeNode O(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.
  • NamespaceTreeNode goes back to a dumb label holder and re-escapes its display label via ILAmbience.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. 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 — 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.

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.

Tests

New NestedNamespaceTreeTests / NamespaceTreeNodeTests / TypeSystemStalenessTests cover: 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 the Filter => Match opt-out). Full ILSpy.Tests suite 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 Decompile fullName fix and the NameFullName fixes in the MSDN/scope-search entries) are not included here and would be reintegrated on top.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AssemblyTreeNode in a single pass over top-level metadata types, with indexes for namespace and type lookups.
  • Simplifies NamespaceTreeNode to a label holder (with display escaping) and removes per-namespace lazy child loading.
  • Updates type navigation in TreeNodeLocator and 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.

Comment thread ILSpy/TreeNodes/NamespaceTreeNode.cs
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
siegfriedpammer force-pushed the restore-eager-namespace-tree branch from ca57b41 to de011d9 Compare July 17, 2026 15:48
@siegfriedpammer
siegfriedpammer merged commit 640b968 into master Jul 17, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants