mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
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 Codepull/3879/head
8 changed files with 604 additions and 136 deletions
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// 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.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using ICSharpCode.ILSpy.AssemblyTree; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
[TestFixture] |
||||
public class NamespaceTreeNodeTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Text_Escapes_Characters_That_Cannot_Be_Displayed() |
||||
{ |
||||
// Namespace names come straight out of the metadata string heap, which permits whitespace
|
||||
// and control characters that would corrupt the tree row if rendered raw. The label is
|
||||
// escaped for display; Name and FullName stay raw because they are the lookup keys used
|
||||
// against metadata.
|
||||
|
||||
var (_, vm) = await TestHarness.BootAsync(3); |
||||
var module = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>("System.Linq") |
||||
.LoadedAssembly.GetMetadataFileOrNull()!; |
||||
|
||||
var node = new NamespaceTreeNode("Weird\tNamespace", module); |
||||
|
||||
Assert.That(node.Text.ToString(), Is.EqualTo("Weird\\u0009Namespace")); |
||||
Assert.That(node.Name, Is.EqualTo("Weird\tNamespace"), "Name stays raw -- it is a lookup key"); |
||||
Assert.That(node.FullName, Is.EqualTo("Weird\tNamespace"), "FullName stays raw -- it is a lookup key"); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Text_Renders_The_Global_Namespace_As_Dash() |
||||
{ |
||||
var (_, vm) = await TestHarness.BootAsync(3); |
||||
var module = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>("System.Linq") |
||||
.LoadedAssembly.GetMetadataFileOrNull()!; |
||||
|
||||
var node = new NamespaceTreeNode(string.Empty, module); |
||||
|
||||
Assert.That(node.Text.ToString(), Is.EqualTo("-")); |
||||
} |
||||
} |
||||
@ -0,0 +1,268 @@
@@ -0,0 +1,268 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// 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.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX; |
||||
|
||||
using ICSharpCode.ILSpy; |
||||
using ICSharpCode.ILSpy.AppEnv; |
||||
using ICSharpCode.ILSpy.AssemblyTree; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
/// <summary>
|
||||
/// Nested-namespace mode ("Use nested namespace structure"): the tree must expose every namespace,
|
||||
/// and the namespace/type lookup primitives must resolve at any nesting depth.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class NestedNamespaceTreeTests |
||||
{ |
||||
/// <summary>
|
||||
/// Expands an assembly with nested-namespace mode on. The assembly node is expanded first so it
|
||||
/// is a realised, visible row: the filter cascade only runs for children of a visible parent, and
|
||||
/// that cascade is what computes <see cref="ICSharpCode.ILSpyX.TreeView.SharpTreeNode.IsHidden"/>.
|
||||
/// </summary>
|
||||
static async Task<(AssemblyTreeModel Model, AssemblyTreeNode Assembly)> BootNestedAsync(string assemblyName) |
||||
{ |
||||
var (window, vm) = await TestHarness.BootAsync(3); |
||||
var pane = await window.WaitForComponent<AssemblyListPane>(); |
||||
await pane.WaitForComponent<ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView>(); |
||||
|
||||
AppComposition.Current.GetExport<SettingsService>().DisplaySettings.UseNestedNamespaceNodes = true; |
||||
|
||||
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(assemblyName); |
||||
assemblyNode.IsExpanded = true; |
||||
assemblyNode.EnsureLazyChildren(); |
||||
return (vm.AssemblyTreeModel, assemblyNode); |
||||
} |
||||
|
||||
static void ResetNestedMode() |
||||
=> AppComposition.Current.GetExport<SettingsService>().DisplaySettings.UseNestedNamespaceNodes = false; |
||||
|
||||
static IEnumerable<NamespaceTreeNode> DescendantNamespaces(NamespaceTreeNode node) |
||||
{ |
||||
yield return node; |
||||
foreach (var child in node.Children.OfType<NamespaceTreeNode>()) |
||||
{ |
||||
foreach (var descendant in DescendantNamespaces(child)) |
||||
yield return descendant; |
||||
} |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Nested_Mode_Does_Not_Hide_Namespaces_That_Hold_Only_Child_Namespaces() |
||||
{ |
||||
// A namespace that contains no types of its own but does contain sub-namespaces -- e.g.
|
||||
// "System.Collections" in an assembly that only ships "System.Collections.Generic" types --
|
||||
// is a pure intermediate node. It must still be shown: hiding it makes every namespace
|
||||
// underneath it unreachable in the tree, which is what "not all namespaces are showing"
|
||||
// looks like to the user.
|
||||
|
||||
try |
||||
{ |
||||
var (_, assemblyNode) = await BootNestedAsync("System.Linq"); |
||||
|
||||
var intermediates = assemblyNode.Children.OfType<NamespaceTreeNode>() |
||||
.SelectMany(DescendantNamespaces) |
||||
.Where(ns => ns.Children.OfType<NamespaceTreeNode>().Any()) |
||||
.ToList(); |
||||
|
||||
Assert.That(intermediates, Is.Not.Empty, |
||||
"the fixture assembly must contain at least one namespace with sub-namespaces, " |
||||
+ "otherwise this test asserts nothing"); |
||||
|
||||
var hidden = intermediates.Where(ns => ns.IsHidden).Select(ns => ns.FullName).ToList(); |
||||
Assert.That(hidden, Is.Empty, |
||||
"namespaces holding sub-namespaces must stay visible; hiding them strands every " |
||||
+ "namespace below them"); |
||||
} |
||||
finally |
||||
{ |
||||
ResetNestedMode(); |
||||
} |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Nested_Mode_Interleaves_Types_And_Sub_Namespaces_Alphabetically() |
||||
{ |
||||
// A namespace holding both types and sub-namespaces lists them as one alphabetical
|
||||
// sequence -- "Collections" (namespace) sits between "Buffers" and "Console" (types) --
|
||||
// rather than grouping all types ahead of all namespaces. The band is built in a single
|
||||
// pass over the types ordered by full name, and a sub-namespace node is attached when its
|
||||
// first descendant is reached, which lands it at its own alphabetical position.
|
||||
|
||||
try |
||||
{ |
||||
var (_, assemblyNode) = await BootNestedAsync(TreeNavigation.CoreLibName); |
||||
|
||||
var system = assemblyNode.Children.OfType<NamespaceTreeNode>() |
||||
.SelectMany(DescendantNamespaces) |
||||
.Single(ns => ns.FullName == "System"); |
||||
|
||||
// Without both kinds present there is no interleaving to observe and the ordering
|
||||
// assertion below would hold vacuously.
|
||||
Assert.That(system.Children.OfType<TypeTreeNode>(), Is.Not.Empty, |
||||
"the fixture's System namespace must declare types of its own"); |
||||
Assert.That(system.Children.OfType<NamespaceTreeNode>(), Is.Not.Empty, |
||||
"the fixture's System namespace must contain sub-namespaces"); |
||||
|
||||
// ToString() is the ordering key on both node kinds: ReflectionName for a type,
|
||||
// the full dotted path for a namespace.
|
||||
var keys = system.Children.Select(child => child.ToString()).ToList(); |
||||
Assert.That(keys, Is.Ordered.Using(NaturalStringComparer.Instance), |
||||
"types and sub-namespaces share one alphabetical sequence; grouping the types " |
||||
+ "ahead of the namespaces breaks the ordering the WPF host had"); |
||||
} |
||||
finally |
||||
{ |
||||
ResetNestedMode(); |
||||
} |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task FindNamespaceNode_Resolves_A_Nested_Namespace() |
||||
{ |
||||
// FindNamespaceNode is the lookup primitive behind "--navigateto N:..." and namespace
|
||||
// navigation. It takes a full dotted name and must resolve it at any depth -- in nested mode
|
||||
// the node for "System.Collections.Generic" is three levels down, not an assembly child.
|
||||
|
||||
try |
||||
{ |
||||
var (_, assemblyNode) = await BootNestedAsync("System.Linq"); |
||||
|
||||
var ns = assemblyNode.FindNamespaceNode("System.Collections.Generic"); |
||||
|
||||
Assert.That(ns, Is.Not.Null, |
||||
"a full dotted namespace name must resolve in nested mode, where the matching node " |
||||
+ "is a descendant rather than a direct child of the assembly node"); |
||||
Assert.That(ns!.FullName, Is.EqualTo("System.Collections.Generic")); |
||||
Assert.That(ns.Name, Is.EqualTo("Generic"), |
||||
"in nested mode the display label is the last segment only"); |
||||
} |
||||
finally |
||||
{ |
||||
ResetNestedMode(); |
||||
} |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task FindTypeNode_Resolves_A_Type_In_A_Nested_Namespace() |
||||
{ |
||||
// FindTypeNode backs hyperlink clicks, search-result activation and JumpToType. A type whose
|
||||
// namespace has more than one segment must resolve in nested mode too.
|
||||
|
||||
try |
||||
{ |
||||
var (_, assemblyNode) = await BootNestedAsync("System.Linq"); |
||||
|
||||
var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; |
||||
var typeSystem = (MetadataModule)module.GetTypeSystemOrNull()!.MainModule; |
||||
var nestedType = typeSystem.TopLevelTypeDefinitions |
||||
.First(t => t.Namespace.Contains('.') && t.Namespace != "System.Linq"); |
||||
|
||||
var node = assemblyNode.FindTypeNode(nestedType); |
||||
|
||||
Assert.That(node, Is.Not.Null, |
||||
$"the tree node for '{nestedType.ReflectionName}' must resolve in nested mode"); |
||||
Assert.That(node!.Handle, |
||||
Is.EqualTo((System.Reflection.Metadata.TypeDefinitionHandle)nestedType.MetadataToken)); |
||||
} |
||||
finally |
||||
{ |
||||
ResetNestedMode(); |
||||
} |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task FindTreeNode_Resolves_A_Type_Reference_In_A_Nested_Namespace() |
||||
{ |
||||
// The reference-to-node lookup behind hyperlink clicks in the decompiler view, search-result
|
||||
// activation and JumpToType. It goes through TreeNodeLocator rather than calling
|
||||
// AssemblyTreeNode.FindTypeNode directly, so it needs its own coverage in nested mode.
|
||||
|
||||
try |
||||
{ |
||||
var (model, assemblyNode) = await BootNestedAsync("System.Linq"); |
||||
|
||||
var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; |
||||
var typeSystem = (MetadataModule)module.GetTypeSystemOrNull()!.MainModule; |
||||
var nestedType = typeSystem.TopLevelTypeDefinitions |
||||
.First(t => t.Namespace.Contains('.') && t.Namespace != "System.Linq"); |
||||
|
||||
var node = model.FindTreeNode(nestedType); |
||||
|
||||
Assert.That(node, Is.InstanceOf<TypeTreeNode>(), |
||||
$"a reference to '{nestedType.ReflectionName}' must resolve to its tree node in nested mode"); |
||||
Assert.That(((TypeTreeNode)node!).Handle, |
||||
Is.EqualTo((System.Reflection.Metadata.TypeDefinitionHandle)nestedType.MetadataToken)); |
||||
} |
||||
finally |
||||
{ |
||||
ResetNestedMode(); |
||||
} |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Nested_Namespace_Is_Hidden_When_All_Of_Its_Types_Are_Filtered_Out() |
||||
{ |
||||
// Nested namespace nodes stay subject to the filter cascade: a namespace whose types are all
|
||||
// filtered out by ShowApiLevel must disappear along with them, rather than linger as an empty
|
||||
// row. Opting namespace nodes out of the cascade (by reporting FilterResult.Match) would make
|
||||
// the display bug go away too, but at the cost of this behaviour -- hence the coverage.
|
||||
|
||||
var settings = AppComposition.Current.GetExport<SettingsService>().SessionSettings.LanguageSettings; |
||||
var original = settings.ShowApiLevel; |
||||
try |
||||
{ |
||||
var (_, assemblyNode) = await BootNestedAsync("System.Linq"); |
||||
|
||||
// FxResources.* holds only internal resource-string types, so PublicOnly empties it.
|
||||
var fxResources = assemblyNode.Children.OfType<NamespaceTreeNode>() |
||||
.SingleOrDefault(ns => ns.FullName == "FxResources"); |
||||
Assert.That(fxResources, Is.Not.Null, |
||||
"the fixture assembly must expose the non-public FxResources namespace in nested mode"); |
||||
Assert.That(fxResources!.IsHidden, Is.False, "it is visible while every API level is shown"); |
||||
|
||||
settings.ShowApiLevel = ApiVisibility.PublicOnly; |
||||
assemblyNode.RefreshRealizedFilter(); |
||||
|
||||
Assert.That(fxResources.IsHidden, Is.True, |
||||
"a namespace left with no visible types must be hidden by the filter cascade"); |
||||
|
||||
settings.ShowApiLevel = ApiVisibility.All; |
||||
assemblyNode.RefreshRealizedFilter(); |
||||
|
||||
Assert.That(fxResources.IsHidden, Is.False, |
||||
"and it must come back when the API level is widened again"); |
||||
} |
||||
finally |
||||
{ |
||||
settings.ShowApiLevel = original; |
||||
ResetNestedMode(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// 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.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
using ICSharpCode.ILSpy.AppEnv; |
||||
using ICSharpCode.ILSpy.AssemblyTree; |
||||
using ICSharpCode.ILSpy.Languages; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
/// <summary>
|
||||
/// The tree resolves each type once, when its assembly's namespace band is built, and holds the
|
||||
/// resulting entity. Those entities belong to one compilation, and only one is cached per module --
|
||||
/// keyed on the effective decompiler settings. Anything that changes those settings drops the cached
|
||||
/// compilation, so the tree has to re-resolve or it is left holding entities from a compilation that
|
||||
/// no longer exists.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class TypeSystemStalenessTests |
||||
{ |
||||
static TypeTreeNode FirstTypeNode(AssemblyTreeNode assembly) |
||||
=> assembly.Children.OfType<NamespaceTreeNode>() |
||||
.SelectMany(ns => ns.Children) |
||||
.OfType<TypeTreeNode>() |
||||
.First(); |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Changing_The_Language_Version_Re_Resolves_The_Tree_Against_The_New_Type_System() |
||||
{ |
||||
var (_, vm) = await TestHarness.BootAsync(3); |
||||
var languageService = AppComposition.Current.GetExport<LanguageService>(); |
||||
var originalVersion = languageService.CurrentVersion; |
||||
|
||||
try |
||||
{ |
||||
var assembly = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(TreeNavigation.CoreLibName); |
||||
assembly.IsExpanded = true; |
||||
assembly.EnsureLazyChildren(); |
||||
|
||||
var before = FirstTypeNode(assembly).TypeDefinition.Compilation; |
||||
|
||||
// C# 1 switches off the language features that map onto TypeSystemOptions (dynamic,
|
||||
// tuples, nullable annotations, ...), so the module's cached compilation is rebuilt.
|
||||
var oldest = languageService.CurrentLanguage.LanguageVersions.First(); |
||||
Assert.That(oldest, Is.Not.EqualTo(originalVersion), |
||||
"the fixture language must offer a version other than the active one, or this test " |
||||
+ "changes nothing"); |
||||
languageService.CurrentVersion = oldest; |
||||
|
||||
var after = FirstTypeNode(assembly).TypeDefinition.Compilation; |
||||
Assert.That(after, Is.Not.SameAs(before), |
||||
"the tree must re-resolve its entities after the language version changes the effective " |
||||
+ "decompiler settings; holding the old compilation's entities leaves every label, icon " |
||||
+ "and filter rendering against a type system the rest of the app has already dropped"); |
||||
} |
||||
finally |
||||
{ |
||||
languageService.CurrentVersion = originalVersion; |
||||
} |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Rebuilding_After_A_Language_Version_Change_Restores_The_Selected_Node() |
||||
{ |
||||
var (_, vm) = await TestHarness.BootAsync(3); |
||||
var languageService = AppComposition.Current.GetExport<LanguageService>(); |
||||
var originalVersion = languageService.CurrentVersion; |
||||
|
||||
try |
||||
{ |
||||
var model = vm.AssemblyTreeModel; |
||||
var assembly = model.FindNode<AssemblyTreeNode>(TreeNavigation.CoreLibName); |
||||
assembly.IsExpanded = true; |
||||
assembly.EnsureLazyChildren(); |
||||
|
||||
var selected = FirstTypeNode(assembly); |
||||
model.SelectedItem = selected; |
||||
var pathBefore = AssemblyTreeModel.GetPathForNode(selected); |
||||
|
||||
languageService.CurrentVersion = languageService.CurrentLanguage.LanguageVersions.First(); |
||||
|
||||
// The rebuild replaces the node objects, so identity cannot survive -- the path must.
|
||||
Assert.That(model.SelectedItem, Is.Not.Null, |
||||
"the rebuild must not drop the selection: the node the user was looking at has to come back"); |
||||
Assert.That(AssemblyTreeModel.GetPathForNode(model.SelectedItem), Is.EqualTo(pathBefore), |
||||
"the same node, identified by path, must be selected again after the tree is rebuilt"); |
||||
Assert.That(model.SelectedItem, Is.Not.SameAs(selected), |
||||
"and it must be the freshly resolved node, not the one holding the dropped compilation's entity"); |
||||
} |
||||
finally |
||||
{ |
||||
languageService.CurrentVersion = originalVersion; |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue