diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index bd6223ef3..d9f1a73eb 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -1332,4 +1332,124 @@ public class AssemblyTreeTests ((MethodTreeNode)resolved!).MethodDefinition.MetadataToken .Should().Be(getter.MetadataToken); } + + [AvaloniaTest] + public async Task Type_Tree_Node_Exposes_BaseTypes_Subtree_Listing_Object_For_System_Exception() + { + // Each type node should surface a "Base Types" sub-tree listing its base classes / + // interfaces, mirroring the WPF tree. System.Exception extends only System.Object so we + // expect a single BaseTypesEntryNode whose label ends in "Object". + + // Arrange — boot, wait for assemblies, drill into System.Exception. + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLibName, "System", "System.Exception"); + typeNode.IsExpanded = true; + + // Assert — BaseTypes child exists, expanding it yields entries, Object is among them. + var baseTypes = typeNode.Children.OfType().Single(); + baseTypes.IsExpanded = true; + baseTypes.Children.OfType().Should().NotBeEmpty(); + baseTypes.Children.OfType() + .Should().Contain(e => e.Text.ToString()!.EndsWith("Object"), + "System.Exception's base-type chain must include Object"); + } + + [AvaloniaTest] + public async Task BaseTypesEntryNode_Activate_Navigates_To_The_Base_Type() + { + // Activating a BaseTypesEntryNode jumps the assembly-tree selection to the underlying + // type node — same gesture the WPF tree uses to walk an inheritance chain. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLibName, "System", "System.Exception"); + typeNode.IsExpanded = true; + var baseTypes = typeNode.Children.OfType().Single(); + baseTypes.IsExpanded = true; + var objectEntry = baseTypes.Children.OfType() + .First(e => e.Text.ToString()!.EndsWith("Object")); + + // Act — activate via the stub routed-args (mirrors a double-click). + var args = new StubRoutedEventArgs(); + objectEntry.ActivateItem(args); + + // Assert — selection moved off System.Exception and onto a TypeTreeNode for Object. + // Cast through object to bypass SharpTreeNodeAssertions' restricted member set. + ((object?)vm.AssemblyTreeModel.SelectedItem).Should().BeOfType(); + ((object?)vm.AssemblyTreeModel.SelectedItem).Should().NotBeSameAs(typeNode); + ((TypeTreeNode)vm.AssemblyTreeModel.SelectedItem!).Text.ToString() + .Should().Be("Object"); + } + + [AvaloniaTest] + public async Task Type_Tree_Node_Exposes_DerivedTypes_Subtree_For_Non_Sealed_Class() + { + // Non-sealed types get a "Derived Types" sub-tree listing classes that extend them. We + // scan the assembly list synchronously — for the small fixture loaded in tests this + // yields a few hits per common base (e.g. SystemException is derived from Exception). + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLibName, "System", "System.Exception"); + typeNode.IsExpanded = true; + + var derived = typeNode.Children.OfType().Single(); + derived.IsExpanded = true; + derived.Children.OfType().Should().NotBeEmpty( + "the loaded assembly list contains several Exception subclasses (e.g. SystemException, ArgumentException)"); + } + + [AvaloniaTest] + public async Task Sealed_Class_Has_No_DerivedTypes_Node() + { + // Sealed types can't be derived from, so the DerivedTypes sub-tree must not appear. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var stringNode = vm.AssemblyTreeModel.FindNode( + coreLibName, "System", "System.String"); + stringNode.IsExpanded = true; + + stringNode.Children.OfType() + .Should().BeEmpty("System.String is sealed"); + } + + [AvaloniaTest] + public async Task Object_Has_No_BaseTypes_Node() + { + // System.Object has no base types, so the BaseTypes sub-tree must not appear. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var objectNode = vm.AssemblyTreeModel.FindNode( + coreLibName, "System", "System.Object"); + objectNode.IsExpanded = true; + + objectNode.Children.OfType() + .Should().BeEmpty("System.Object has no base types"); + } } diff --git a/ILSpy.Tests/Navigation/NavigationTests.cs b/ILSpy.Tests/Navigation/NavigationTests.cs index a9b0c38ab..247118911 100644 --- a/ILSpy.Tests/Navigation/NavigationTests.cs +++ b/ILSpy.Tests/Navigation/NavigationTests.cs @@ -135,10 +135,14 @@ public class NavigationTests // Assert 1 — newest-first ordering: index 0 is the immediate previous selection // (methodB), index 1 is the one before that (methodA). Each menu item carries a - // TreeNodeEntry wrapping the original tree node. + // TreeNodeEntry wrapping the original tree node, and the header reads the richer + // NavigationText (mirrors WPF — disambiguates "Empty" from other "Empty" methods by + // prefixing the declaring type). var items = flyout.Items.OfType().ToList(); - ((string)items[0].Header!).Should().Be((string)methodB.Text); - ((string)items[1].Header!).Should().Be((string)methodA.Text); + ((string)items[0].Header!).Should().Be((string)methodB.NavigationText); + ((string)items[1].Header!).Should().Be((string)methodA.NavigationText); + ((string)items[0].Header!).Should().Contain("Enumerable", + "NavigationText must include the declaring type"); items[1].CommandParameter.Should().BeOfType(); var entry = (global::ILSpy.Navigation.TreeNodeEntry)items[1].CommandParameter!; ReferenceEquals(entry.Node, methodA).Should().BeTrue(); @@ -151,4 +155,52 @@ public class NavigationTests // so Forward becomes available. vm.DockWorkspace.NavigateForwardCommand.CanExecute(null).Should().BeTrue(); } + + [AvaloniaTest] + public async Task Back_History_Dropdown_Uses_NavigationText_For_Generic_Tree_Nodes() + { + // Bare Text on grouping nodes ("Base Types", "Derived Types", "References") reads + // ambiguously in the back-history dropdown — the user can't tell which type's bases + // they're looking at. ILSpyTreeNode.NavigationText carries the richer disambiguated + // form ("Base Types (System.Exception)"), and TreeNodeEntry.DisplayText must use it + // when the node implements ILSpyTreeNode. + + // Arrange — boot, wait for assemblies, expand System.Exception so we can select its + // BaseTypesTreeNode child. + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLibName, "System", "System.Exception"); + typeNode.IsExpanded = true; + var baseTypes = typeNode.Children.OfType().Single(); + + // Act — select the grouping node, wait for decompile, navigate elsewhere so the + // grouping lands on the back stack. + vm.AssemblyTreeModel.SelectNode(baseTypes); + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + await Task.Delay(600); + vm.AssemblyTreeModel.SelectNode(typeNode); + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + // Open the back-history flyout and read the entry header. + var backSplit = window.GetVisualDescendants().OfType() + .Single(sb => sb.Name == "BackSplitButton"); + var flyout = (MenuFlyout)backSplit.Flyout!; + flyout.ShowAt(backSplit); + await Waiters.WaitForAsync(() => flyout.Items.OfType().Any()); + + // Assert — the entry shows the richer "Base Types (System.Exception)" form, NOT the + // bare "Base Types" Text. + var items = flyout.Items.OfType().ToList(); + var baseTypesEntry = items.First(i => + ((string)i.Header!).StartsWith((string)baseTypes.Text)); + ((string)baseTypesEntry.Header!).Should().Contain("Exception", + "the dropdown header must disambiguate generic grouping nodes via NavigationText"); + ((string)baseTypesEntry.Header!).Should().NotBe((string)baseTypes.Text, + "falling back to bare Text would reproduce the WPF parity gap"); + } } diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index 3dd777cea..1eaa0e09e 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -492,6 +492,23 @@ namespace ILSpy.AssemblyTree SelectedItem = node; } + /// + /// Resolves to the matching in the + /// loaded assembly list and selects it. Returns false when the type's parent module is + /// not loaded (or the namespace / nested-type chain can't be walked) — used by the + /// Base/Derived Types entry nodes to jump along an inheritance chain. + /// + public bool JumpToType(ITypeDefinition? type) + { + if (type == null || assemblyListTreeNode == null) + return false; + var node = FindTypeNode(assemblyListTreeNode, type); + if (node == null) + return false; + SelectNode(node); + return true; + } + public void OpenFiles(string[] fileNames, bool focusNode = true) { ArgumentNullException.ThrowIfNull(fileNames); diff --git a/ILSpy/Assets/Icons/SubTypes.svg b/ILSpy/Assets/Icons/SubTypes.svg new file mode 100644 index 000000000..fbe0b6840 --- /dev/null +++ b/ILSpy/Assets/Icons/SubTypes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/SuperTypes.svg b/ILSpy/Assets/Icons/SuperTypes.svg new file mode 100644 index 000000000..201af86b7 --- /dev/null +++ b/ILSpy/Assets/Icons/SuperTypes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs index 803d3d646..4a5d7439d 100644 --- a/ILSpy/Images.cs +++ b/ILSpy/Images.cs @@ -78,6 +78,10 @@ namespace ILSpy.Images public static readonly IImage ShowPrivateInternal = LoadSvg(nameof(ShowPrivateInternal)); public static readonly IImage ShowAll = LoadSvg(nameof(ShowAll)); + // Type-relation tree nodes (Base Types / Derived Types). + public static readonly IImage SuperTypes = LoadSvg(nameof(SuperTypes)); + public static readonly IImage SubTypes = LoadSvg(nameof(SubTypes)); + // Containers public static readonly IImage Namespace = LoadSvg(nameof(Namespace)); public static readonly IImage ReferenceFolder = LoadSvg(nameof(ReferenceFolder)); diff --git a/ILSpy/LanguageSettings.cs b/ILSpy/LanguageSettings.cs index 2799bc267..84a644110 100644 --- a/ILSpy/LanguageSettings.cs +++ b/ILSpy/LanguageSettings.cs @@ -85,5 +85,15 @@ namespace ILSpy [ObservableProperty] string? languageVersionId; + + // SearchTerm is currently a placeholder mirroring WPF — the Avalonia search pane isn't + // ported yet, but several tree-node Filter overrides reference SearchTermMatches when + // porting the WPF logic verbatim. Returning string.Empty + always-true SearchTermMatches + // means the Filter cascade collapses to "match if visibility passes", same as no-term-set + // in WPF. When the search pane lands, swap these for a real backing field bound to the + // pane's text box. + public string SearchTerm => string.Empty; + + public bool SearchTermMatches(string value) => true; } } diff --git a/ILSpy/Languages/Language.cs b/ILSpy/Languages/Language.cs index 2a278f7eb..3f0547d1c 100644 --- a/ILSpy/Languages/Language.cs +++ b/ILSpy/Languages/Language.cs @@ -18,17 +18,20 @@ using System; using System.Collections.Generic; +using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using AvaloniaEdit.Highlighting; using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Disassembler; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.Abstractions; namespace ILSpy.Languages { @@ -37,12 +40,49 @@ namespace ILSpy.Languages /// Subclasses override formatting for their target syntax. Implementations must be /// thread-safe. /// - public abstract class Language + public abstract class Language : ILanguage { public abstract string Name { get; } public abstract string FileExtension { get; } + /// + /// Versions selectable for this language (e.g. C# 1.0 → C# 14). Default is empty; + /// reports whether any are available so toolbar UI + /// can hide the version picker for languages that don't differentiate. + /// + public virtual IReadOnlyList LanguageVersions => System.Array.Empty(); + + public bool HasLanguageVersions => LanguageVersions.Count > 0; + + /// + /// Token-to-source-line mapping for navigation. Default returns a no-op CodeMappingInfo — + /// language subclasses with full decompilation override and produce a real one. + /// + public virtual CodeMappingInfo GetCodeMappingInfo(MetadataFile module, EntityHandle member) + => new(module, (TypeDefinitionHandle)member); + + /// + /// Stable name string for an entity reachable only by metadata token. Falls back to the + /// language's path so subclasses don't have to repeat the + /// formatting rules — overrideable when a language wants a tokenless name (e.g. C# uses a + /// disassembler-friendly form). + /// + public virtual string GetEntityName(MetadataFile module, EntityHandle handle, bool fullName, bool omitGenerics) + { + ArgumentNullException.ThrowIfNull(module); + var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver()); + var entity = typeSystem.MainModule.ResolveEntity(handle); + if (entity == null) + return string.Empty; + var flags = ConversionFlags.ShowParameterList | ConversionFlags.ShowParameterModifiers; + if (fullName) + flags |= ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames; + if (!omitGenerics) + flags |= ConversionFlags.ShowTypeParameterList; + return entity is IType type ? TypeToString(type, flags) : EntityToString((IEntity)entity, flags); + } + public virtual string TypeToString(IType type, ConversionFlags conversionFlags = ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames) { ArgumentNullException.ThrowIfNull(type); diff --git a/ILSpy/NavigationEntry.cs b/ILSpy/NavigationEntry.cs index 9d464e00a..36de107ad 100644 --- a/ILSpy/NavigationEntry.cs +++ b/ILSpy/NavigationEntry.cs @@ -23,6 +23,7 @@ using Avalonia.Media; using ICSharpCode.ILSpyX.TreeView; +using ILSpy.TreeNodes; using ILSpy.ViewModels; namespace ILSpy.Navigation @@ -68,7 +69,10 @@ namespace ILSpy.Navigation Node = node ?? throw new ArgumentNullException(nameof(node)); } - public override object DisplayText => Node.Text ?? string.Empty; + // ILSpyTreeNode exposes a richer NavigationText for nodes whose generic Text ("Base + // Types", "Derived Types", "References", …) reads ambiguously without context. Plain + // SharpTreeNode entries fall back to Text. + public override object DisplayText => (Node is ILSpyTreeNode ilspy ? ilspy.NavigationText : Node.Text) ?? string.Empty; public override IImage? DisplayIcon => Node.Icon as IImage; diff --git a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs index 4a33c3f0c..01457ad18 100644 --- a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs @@ -60,6 +60,8 @@ namespace ILSpy.TreeNodes public override object Text => ILAmbience.EscapeName(reference.Name); + public override object? NavigationText => $"{Text} ({ICSharpCode.ILSpy.Properties.Resources.References})"; + public override object Icon { get { if (state == LoadState.Unloaded) diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 4cd792d5c..4beea52af 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -277,6 +277,14 @@ namespace ILSpy.TreeNodes Children.Add(new TypeTreeNode(t, module)); } + public override FilterResult Filter(LanguageSettings settings) + { + if (settings.SearchTermMatches(LoadedAssembly.ShortName)) + return FilterResult.Match; + else + return FilterResult.Recurse; + } + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { void HandleException(Exception ex, string message) diff --git a/ILSpy/TreeNodes/BaseTypesEntryNode.cs b/ILSpy/TreeNodes/BaseTypesEntryNode.cs new file mode 100644 index 000000000..7b8de63a7 --- /dev/null +++ b/ILSpy/TreeNodes/BaseTypesEntryNode.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Output; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX.TreeView; +using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions; + +using ILSpy.AppEnv; +using ILSpy.AssemblyTree; +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// Single base-class / interface entry under a . Activating + /// the entry navigates the assembly-tree selection to the corresponding . + /// + public sealed class BaseTypesEntryNode : ILSpyTreeNode, IMemberTreeNode + { + readonly ITypeDefinition type; + + public BaseTypesEntryNode(ITypeDefinition type) + { + this.type = type; + } + + public override object Text => Language.TypeToString(type, ConversionFlags.None); + + public override object? NavigationText => $"{Text} ({ICSharpCode.ILSpy.Properties.Resources.BaseTypes})"; + + public override object Icon => type.Kind == TypeKind.Interface + ? Images.Images.Interface + : Images.Images.Class; + + public override void ActivateItem(IPlatformRoutedEventArgs e) + { + e.Handled = ActivateItem(this, type); + } + + /// + /// Shared activate helper used by and + /// . Mirrors WPF's matching helper. The + /// parameter is unused on Avalonia (navigation routes through + /// the MEF-resolved instead of walking up the tree to + /// the assembly-list node), but the signature stays in lock-step with WPF so any future + /// caller that needs the originating node has it on hand. + /// + internal static bool ActivateItem(SharpTreeNode node, ITypeDefinition? def) + { + if (def == null) + return false; + var atm = AppComposition.Current.GetExport(); + return atm.JumpToType(def); + } + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + language.WriteCommentLine(output, language.TypeToString(type, ConversionFlags.None)); + } + + IEntity IMemberTreeNode.Member => type; + } +} diff --git a/ILSpy/TreeNodes/BaseTypesTreeNode.cs b/ILSpy/TreeNodes/BaseTypesTreeNode.cs new file mode 100644 index 000000000..7da1ecbee --- /dev/null +++ b/ILSpy/TreeNodes/BaseTypesTreeNode.cs @@ -0,0 +1,85 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Linq; +using System.Reflection.Metadata; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.TreeView; + +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// Lists the base types of a class — the inheritance chain plus implemented interfaces, in + /// type-itself-first → most-distant-ancestor order. Lazy: children are only resolved when + /// the user expands the node. + /// + public sealed class BaseTypesTreeNode : ILSpyTreeNode + { + readonly MetadataFile module; + readonly ITypeDefinition type; + + public BaseTypesTreeNode(MetadataFile module, ITypeDefinition type) + { + this.module = module; + this.type = type; + LazyLoading = true; + } + + public override object Text => ICSharpCode.ILSpy.Properties.Resources.BaseTypes; + + public override object? NavigationText => $"{Text} ({Language.TypeToString(type)})"; + + public override object Icon => Images.Images.SuperTypes; + + protected override void LoadChildren() + { + AddBaseTypes(Children, module, type); + } + + internal static void AddBaseTypes(SharpTreeNodeCollection children, MetadataFile module, ITypeDefinition typeDefinition) + { + // Re-resolve the type with an Uncached type system so we get a fresh inheritance + // chain (some Decompiler-flag toggles affect how interfaces fold in). Mirrors WPF. + var handle = (TypeDefinitionHandle)typeDefinition.MetadataToken; + var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver(), + TypeSystemOptions.Default | TypeSystemOptions.Uncached); + if (typeSystem.MainModule.ResolveEntity(handle) is not ITypeDefinition t) + return; + // GetAllBaseTypeDefinitions returns [furthest-ancestor, ..., direct-base, self]; we + // want everything except self, in walk-up order — so reverse and skip(1). + foreach (var td in t.GetAllBaseTypeDefinitions().Reverse().Skip(1)) + { + if (t.Kind != TypeKind.Interface || t.Kind == td.Kind) + children.Add(new BaseTypesEntryNode(td)); + } + } + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + EnsureLazyChildren(); + foreach (var child in Children.OfType()) + child.Decompile(language, output, options); + } + } +} diff --git a/ILSpy/TreeNodes/DerivedTypesEntryNode.cs b/ILSpy/TreeNodes/DerivedTypesEntryNode.cs new file mode 100644 index 000000000..b482d84e9 --- /dev/null +++ b/ILSpy/TreeNodes/DerivedTypesEntryNode.cs @@ -0,0 +1,97 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Output; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.Abstractions; +using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions; + +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// Single derived-type entry under a . Activating jumps + /// to the corresponding . Itself lazy-recursive — expanding a + /// derived-type entry walks one more level, surfacing types derived from it. + /// + public sealed class DerivedTypesEntryNode : ILSpyTreeNode, IMemberTreeNode + { + readonly AssemblyList list; + readonly ITypeDefinition type; + + public DerivedTypesEntryNode(AssemblyList list, ITypeDefinition type) + { + this.list = list; + this.type = type; + LazyLoading = true; + } + + public override bool ShowExpander => !type.IsSealed && base.ShowExpander; + + public override object Text => Language.TypeToString(type, ConversionFlags.None); + + public override object? NavigationText => $"{Text} ({ICSharpCode.ILSpy.Properties.Resources.DerivedTypes})"; + + public override object Icon => type.Kind == TypeKind.Interface + ? Images.Images.Interface + : Images.Images.Class; + + protected override void LoadChildren() + { + foreach (var entry in DerivedTypesTreeNode.FindDerivedTypes(list, LanguageService.CurrentLanguage, type, CancellationToken.None)) + Children.Add(entry); + } + + public override bool IsPublicAPI => type.Accessibility switch { + Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal => true, + _ => false, + }; + + /// + /// Mirrors WPF's filter — drop non-public entries under PublicOnly visibility, otherwise + /// recurse so the user can drill into derived chains. The WPF overload also reads + /// SearchTermMatches (a helper that's not yet in + /// the Avalonia port) to surface only entries whose name matches the active search term; + /// reinstate that branch when the search infrastructure lands. + /// + public override FilterResult Filter(LanguageSettings settings) + { + if (settings.ShowApiLevel == ApiVisibility.PublicOnly && !IsPublicAPI) + return FilterResult.Hidden; + return FilterResult.Recurse; + } + + public override void ActivateItem(IPlatformRoutedEventArgs e) + { + e.Handled = BaseTypesEntryNode.ActivateItem(this, type); + } + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + language.WriteCommentLine(output, language.TypeToString(type, ConversionFlags.None)); + } + + IEntity IMemberTreeNode.Member => type; + } +} diff --git a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs new file mode 100644 index 000000000..fe4f5a423 --- /dev/null +++ b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.Analyzers; + +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// Lists the derived types of a class — every loaded type whose direct base list contains + /// our target. Lazy: the assembly-list scan only runs when the user expands the node. The + /// scan is synchronous; small assembly lists complete in milliseconds. A future task can + /// switch to a background-loaded variant when this proves slow under real workloads. + /// + public sealed class DerivedTypesTreeNode : ILSpyTreeNode + { + readonly AssemblyList list; + readonly ITypeDefinition type; + + public DerivedTypesTreeNode(AssemblyList list, ITypeDefinition type) + { + this.list = list; + this.type = type; + LazyLoading = true; + } + + public override object Text => ICSharpCode.ILSpy.Properties.Resources.DerivedTypes; + + public override object? NavigationText => $"{Text} ({Language.TypeToString(type)})"; + + public override object Icon => Images.Images.SubTypes; + + protected override void LoadChildren() + { + foreach (var entry in FindDerivedTypes(list, LanguageService.CurrentLanguage, type, CancellationToken.None)) + Children.Add(entry); + } + + internal static IEnumerable FindDerivedTypes(AssemblyList list, Language language, ITypeDefinition type, CancellationToken cancellationToken) + { + var context = new AnalyzerContext { + CancellationToken = cancellationToken, + Language = language, + AssemblyList = list, + }; + var scope = context.GetScopeOf(type); + + foreach (var td in scope.GetTypesInScope(cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var baseType in td.DirectBaseTypes) + { + if (baseType.FullName == type.FullName && baseType.TypeParameterCount == type.TypeParameterCount) + { + yield return new DerivedTypesEntryNode(list, td); + break; + } + } + } + } + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + EnsureLazyChildren(); + foreach (var child in Children) + { + if (child is ILSpyTreeNode node) + node.Decompile(language, output, options); + } + } + } +} diff --git a/ILSpy/TreeNodes/EventTreeNode.cs b/ILSpy/TreeNodes/EventTreeNode.cs index be55600ef..1597b0a7a 100644 --- a/ILSpy/TreeNodes/EventTreeNode.cs +++ b/ILSpy/TreeNodes/EventTreeNode.cs @@ -46,6 +46,9 @@ namespace ILSpy.TreeNodes } public override object Text => Language.EntityToString(EventDefinition, ConversionFlags.None); + + public override object NavigationText => Language.EntityToString(EventDefinition, ConversionFlags.ShowDeclaringType); + public override object Icon => Images.Images.GetIcon(Images.Images.Event, Images.Images.GetOverlay(EventDefinition.Accessibility), EventDefinition.IsStatic); @@ -61,9 +64,10 @@ namespace ILSpy.TreeNodes { if (settings.ShowApiLevel == ApiVisibility.PublicOnly && !IsPublicAPI) return FilterResult.Hidden; - if (settings.ShowApiLevel == ApiVisibility.All || Language.ShowMember(EventDefinition)) + if (settings.SearchTermMatches(EventDefinition.Name) && (settings.ShowApiLevel == ApiVisibility.All || LanguageService.CurrentLanguage.ShowMember(EventDefinition))) return FilterResult.Match; - return FilterResult.Hidden; + else + return FilterResult.Hidden; } public override string ToString() => "Event " + EventDefinition.Name; diff --git a/ILSpy/TreeNodes/FieldTreeNode.cs b/ILSpy/TreeNodes/FieldTreeNode.cs index 718aaa15a..ec0aaa4c1 100644 --- a/ILSpy/TreeNodes/FieldTreeNode.cs +++ b/ILSpy/TreeNodes/FieldTreeNode.cs @@ -40,6 +40,9 @@ namespace ILSpy.TreeNodes } public override object Text => Language.EntityToString(FieldDefinition, ConversionFlags.None); + + public override object NavigationText => Language.EntityToString(FieldDefinition, ConversionFlags.ShowDeclaringType); + public override object Icon => Images.Images.GetIcon(Images.Images.Field, Images.Images.GetOverlay(FieldDefinition.Accessibility), FieldDefinition.IsStatic); public override bool ShowExpander => false; @@ -56,9 +59,10 @@ namespace ILSpy.TreeNodes { if (settings.ShowApiLevel == ApiVisibility.PublicOnly && !IsPublicAPI) return FilterResult.Hidden; - if (settings.ShowApiLevel == ApiVisibility.All || Language.ShowMember(FieldDefinition)) + if (settings.SearchTermMatches(FieldDefinition.Name) && (settings.ShowApiLevel == ApiVisibility.All || LanguageService.CurrentLanguage.ShowMember(FieldDefinition))) return FilterResult.Match; - return FilterResult.Hidden; + else + return FilterResult.Hidden; } public override string ToString() => "Field " + FieldDefinition.Name; diff --git a/ILSpy/TreeNodes/FilterResult.cs b/ILSpy/TreeNodes/FilterResult.cs index 7ae4277a4..e0d7ec23b 100644 --- a/ILSpy/TreeNodes/FilterResult.cs +++ b/ILSpy/TreeNodes/FilterResult.cs @@ -21,14 +21,26 @@ namespace ILSpy.TreeNodes /// /// Outcome of an evaluation against the active /// . Drives whether the assembly tree shows the node, hides - /// it outright, or shows it only if any descendant matches. + /// it outright, or shows it only if any descendant matches. Layout mirrors WPF's + /// FilterResult for 1:1 portability of overrides. /// public enum FilterResult { - /// The node passes the filter and should be displayed. + /// Shows the node (and resets the search term for child nodes). Match, - /// The node itself should be hidden, but its descendants must still be evaluated. + /// + /// Hides the node only if all children are hidden (and resets the search term for + /// child nodes). Treated identically to by the cascade — the + /// "reset search term" distinction is documented for forward-compat with the WPF + /// behavior contract. + /// + MatchAndRecurse, + + /// + /// Hides the node only if all children are hidden (doesn't reset the search term + /// for child nodes). + /// Recurse, /// The node and all its descendants are hidden. diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs index 9d56ff340..8dfa63f1f 100644 --- a/ILSpy/TreeNodes/ILSpyTreeNode.cs +++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs @@ -162,6 +162,7 @@ namespace ILSpy.TreeNodes child.IsHidden = false; break; case FilterResult.Recurse: + case FilterResult.MatchAndRecurse: child.EnsureChildrenFiltered(); child.IsHidden = child.Children.All(c => c.IsHidden); break; diff --git a/ILSpy/TreeNodes/MemberReferenceTreeNode.cs b/ILSpy/TreeNodes/MemberReferenceTreeNode.cs index 2e85a4279..6db860b9a 100644 --- a/ILSpy/TreeNodes/MemberReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/MemberReferenceTreeNode.cs @@ -52,6 +52,8 @@ namespace ILSpy.TreeNodes public override object Text => Signature; + public override object NavigationText => $"{Text} ({ICSharpCode.ILSpy.Properties.Resources.ReferencedTypes})"; + public override object Icon => r.MemberReferenceKind switch { MemberReferenceKind.Method => Images.Images.MethodReference, MemberReferenceKind.Field => Images.Images.FieldReference, diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs index c3a0c7c79..14d1b37f4 100644 --- a/ILSpy/TreeNodes/MethodTreeNode.cs +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -41,6 +41,8 @@ namespace ILSpy.TreeNodes public override object Text => Language.EntityToString(MethodDefinition, ConversionFlags.None); + public override object NavigationText => Language.EntityToString(MethodDefinition, ConversionFlags.ShowDeclaringType); + public override object Icon { get { var baseImage = MethodDefinition.IsConstructor ? Images.Images.Constructor : @@ -67,9 +69,13 @@ namespace ILSpy.TreeNodes { if (settings.ShowApiLevel == ApiVisibility.PublicOnly && !IsPublicAPI) return FilterResult.Hidden; - if (settings.ShowApiLevel == ApiVisibility.All || Language.ShowMember(MethodDefinition)) + // WPF additionally hides extension-block implementation methods when the + // ExtensionMembers decompiler setting is on; tracked as `methodtreenode-extension-gate` + // in the parity TODO and reinstated when the DecompilerSettings UI lands. + if (settings.SearchTermMatches(MethodDefinition.Name) && (settings.ShowApiLevel == ApiVisibility.All || LanguageService.CurrentLanguage.ShowMember(MethodDefinition))) return FilterResult.Match; - return FilterResult.Hidden; + else + return FilterResult.Hidden; } // Stable identity for SessionSettings.ActiveTreeViewPath; format must round-trip diff --git a/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs b/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs index ecdabf114..a22334855 100644 --- a/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/ModuleReferenceTreeNode.cs @@ -70,6 +70,8 @@ namespace ILSpy.TreeNodes public override object Text => moduleName; + public override object? NavigationText => $"{Text} ({ICSharpCode.ILSpy.Properties.Resources.References})"; + public override object Icon => Images.Images.Library; public override void ActivateItem(IPlatformRoutedEventArgs e) diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index 0b860472a..5d4aa5ce3 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -98,9 +98,10 @@ namespace ILSpy.TreeNodes public override FilterResult Filter(LanguageSettings settings) { - if (settings.ShowApiLevel == ApiVisibility.PublicOnly && !IsPublicAPI) - return FilterResult.Hidden; - return FilterResult.Match; + if (settings.SearchTermMatches(name)) + return FilterResult.MatchAndRecurse; + else + return FilterResult.Recurse; } } } diff --git a/ILSpy/TreeNodes/PropertyTreeNode.cs b/ILSpy/TreeNodes/PropertyTreeNode.cs index dd36a769f..32927c7e2 100644 --- a/ILSpy/TreeNodes/PropertyTreeNode.cs +++ b/ILSpy/TreeNodes/PropertyTreeNode.cs @@ -44,6 +44,9 @@ namespace ILSpy.TreeNodes } public override object Text => Language.EntityToString(PropertyDefinition, ConversionFlags.None); + + public override object NavigationText => Language.EntityToString(PropertyDefinition, ConversionFlags.ShowDeclaringType); + public override object Icon => Images.Images.GetIcon(Images.Images.Property, Images.Images.GetOverlay(PropertyDefinition.Accessibility), PropertyDefinition.IsStatic); @@ -59,9 +62,10 @@ namespace ILSpy.TreeNodes { if (settings.ShowApiLevel == ApiVisibility.PublicOnly && !IsPublicAPI) return FilterResult.Hidden; - if (settings.ShowApiLevel == ApiVisibility.All || Language.ShowMember(PropertyDefinition)) + if (settings.SearchTermMatches(PropertyDefinition.Name) && (settings.ShowApiLevel == ApiVisibility.All || LanguageService.CurrentLanguage.ShowMember(PropertyDefinition))) return FilterResult.Match; - return FilterResult.Hidden; + else + return FilterResult.Hidden; } public override string ToString() diff --git a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs index 251f85e9e..5f7904f9c 100644 --- a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs +++ b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs @@ -46,6 +46,8 @@ namespace ILSpy.TreeNodes public override object Text => Resources.References; + public override object? NavigationText => $"{Text} ({module.Name})"; + public override object Icon => Images.Images.ReferenceFolder; protected override void LoadChildren() diff --git a/ILSpy/TreeNodes/ResourceListTreeNode.cs b/ILSpy/TreeNodes/ResourceListTreeNode.cs index 7d70afa32..744367491 100644 --- a/ILSpy/TreeNodes/ResourceListTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceListTreeNode.cs @@ -44,6 +44,8 @@ namespace ILSpy.TreeNodes public override object Text => Resources._Resources; + public override object? NavigationText => $"{Text} ({module.Name})"; + public override object Icon => IsExpanded ? Images.Images.FolderOpen : Images.Images.FolderClosed; protected override void OnExpanding() @@ -64,6 +66,18 @@ namespace ILSpy.TreeNodes Children.Add(ResourceTreeNode.Create(r)); } + public override FilterResult Filter(LanguageSettings settings) + { + // WPF's variant short-circuits on string.IsNullOrEmpty(SearchTerm) — when no term + // is set the folder reads as a plain match, otherwise the tree walks into resources + // to surface only matching ones. We use the same shape so a future search pane + // behaves identically. + if (string.IsNullOrEmpty(settings.SearchTerm)) + return FilterResult.MatchAndRecurse; + else + return FilterResult.Recurse; + } + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { Dispatcher.UIThread.Invoke(EnsureLazyChildren); diff --git a/ILSpy/TreeNodes/TypeReferenceTreeNode.cs b/ILSpy/TreeNodes/TypeReferenceTreeNode.cs index 120cf4806..79e1958a0 100644 --- a/ILSpy/TreeNodes/TypeReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/TypeReferenceTreeNode.cs @@ -47,6 +47,8 @@ namespace ILSpy.TreeNodes public override object Text => Language.TypeToString(resolvedType, ConversionFlags.None); + public override object NavigationText => $"{Text} ({ICSharpCode.ILSpy.Properties.Resources.ReferencedTypes})"; + public override object Icon => Images.Images.TypeReference; protected override void LoadChildren() diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs index 59d341e4d..b29bc8c7b 100644 --- a/ILSpy/TreeNodes/TypeTreeNode.cs +++ b/ILSpy/TreeNodes/TypeTreeNode.cs @@ -27,6 +27,8 @@ using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; using ILSpy; +using ILSpy.AppEnv; +using ILSpy.AssemblyTree; using ILSpy.Languages; namespace ILSpy.TreeNodes @@ -99,9 +101,17 @@ namespace ILSpy.TreeNodes var typeDef = ResolveTypeDefinition(); if (typeDef == null) return FilterResult.Match; - if (settings.ShowApiLevel == ApiVisibility.All || Language.ShowMember(typeDef)) - return FilterResult.Match; - return FilterResult.Hidden; + if (settings.SearchTermMatches(typeDef.Name)) + { + if (settings.ShowApiLevel == ApiVisibility.All || LanguageService.CurrentLanguage.ShowMember(typeDef)) + return FilterResult.Match; + else + return FilterResult.Hidden; + } + else + { + return FilterResult.Recurse; + } } // Stable identity for SessionSettings.ActiveTreeViewPath. ReflectionName is @@ -112,6 +122,18 @@ namespace ILSpy.TreeNodes return typeDef?.ReflectionName ?? module.Metadata.GetString(module.Metadata.GetTypeDefinition(handle).Name); } + // Sealed / static / value-type / enum / delegate cannot be the base of another class, + // so a DerivedTypes child would always show up empty. Suppress it for those kinds. + static bool CanHaveDerivedTypes(ITypeDefinition typeDef) + { + if (typeDef.IsSealed) + return false; + return typeDef.Kind switch { + TypeKind.Class or TypeKind.Interface => true, + _ => false, + }; + } + ITypeDefinition? ResolveTypeDefinition() { var typeSystem = module.GetTypeSystemOrNull(); @@ -126,6 +148,20 @@ namespace ILSpy.TreeNodes if (typeDef == null) return; + // Inheritance-relation siblings come first so they sit above the type's own members. + // BaseTypes is skipped for System.Object (no upstream chain) and for value types' + // implicit System.ValueType base when there's nothing else to show — the AddBaseTypes + // pass produces an empty set, which collapses the node. + if (typeDef.DirectBaseTypes.Any()) + Children.Add(new BaseTypesTreeNode(module, typeDef)); + + // DerivedTypes is meaningful only for non-sealed reference types (and abstract + // classes / interfaces). Sealed classes can't be derived from; static classes are + // implicitly sealed. + var assemblyList = AppComposition.Current.GetExport().AssemblyList; + if (assemblyList != null && CanHaveDerivedTypes(typeDef)) + Children.Add(new DerivedTypesTreeNode(assemblyList, typeDef)); + foreach (var nestedType in typeDef.NestedTypes .OrderBy(t => t.Name, NaturalStringComparer.Instance)) {