diff --git a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs index f5bede0be..3ee946fae 100644 --- a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs @@ -145,4 +145,117 @@ public class AnalyzeContextMenuTests analyzerVm.Root.Children.Count.Should().Be(beforeCount + 1, "Ctrl+R with a type selected must analyse it"); } + + [AvaloniaTest] + public async Task Execute_Surfaces_The_Analyzer_Pane_So_Hidden_Panes_Become_Visible() + { + // Logged follow-up from earlier live-testing: analysing an entity adds it to the + // pane's Root.Children, but if the pane is hidden the user doesn't see anything + // happen. Execute should call DockWorkspace.ShowToolPane so the pane surfaces. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var dockWorkspace = AppComposition.Current.GetExport(); + var analyzerVm = AppComposition.Current.GetExport(); + var entry = AppComposition.Current.GetExport() + .Entries.Single(e => e.Metadata.Header == nameof(Resources.Analyze)).Value; + + // Pick a method that won't already be in the analyzer pane. + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.IsExpanded = true; + var methodNode = typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "Count"); + + // Walk the dock layout to find the analyzer pane's current visibility state. + var analyzerPane = FindAnalyzerPane(dockWorkspace); + Assert.That(analyzerPane, Is.Not.Null, "the analyzer pane must be in the dock layout"); + var owningDock = analyzerPane!.Owner as global::Dock.Model.Core.IDock; + Assert.That(owningDock, Is.Not.Null, "the analyzer pane must have a dock parent"); + + // Pick any sibling dockable and force it to be active first, so the analyzer pane + // starts in the inactive state — that's the regression we're testing. + var sibling = owningDock!.VisibleDockables? + .FirstOrDefault(d => !ReferenceEquals(d, analyzerPane)); + if (sibling != null) + { + owningDock.ActiveDockable = sibling; + ((object?)owningDock.ActiveDockable).Should().NotBeSameAs(analyzerPane, + "baseline: sibling-active must really make the analyzer pane inactive"); + } + + entry.Execute(new TextViewContext { + SelectedTreeNodes = new ICSharpCode.ILSpyX.TreeView.SharpTreeNode[] { methodNode } + }); + + // After Execute, the active dockable in the analyzer pane's owning dock should be + // the analyzer pane itself — that's what ShowToolPane does. + ((object?)owningDock.ActiveDockable).Should().BeSameAs(analyzerPane, + "Execute must call ShowToolPane so the analyzer pane surfaces"); + } + + [AvaloniaTest] + public async Task Every_Analyzer_Tree_Row_Surfaces_A_Non_Null_Icon() + { + // Logged follow-up from earlier live-testing: analyzer rows render without icons. + // The Analyzed{Type,Method,Field,Property,Event,Accessor,Module}TreeNode types all + // have Icon overrides returning real IImages. The "Used By" / "Uses" search-result + // header rows (AnalyzerSearchTreeNode), however, didn't override Icon and inherited + // null from SharpTreeNode — so the row's Image element rendered empty even though + // the leaf result rows below had perfectly good icons. Every materialised node in + // the analyzer tree must report a non-null Icon for the cell template to render. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var analyzerVm = AppComposition.Current.GetExport(); + var entry = AppComposition.Current.GetExport() + .Entries.Single(e => e.Metadata.Header == nameof(Resources.Analyze)).Value; + + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.IsExpanded = true; + var methodNode = typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "Empty"); + + entry.Execute(new TextViewContext { + SelectedTreeNodes = new ICSharpCode.ILSpyX.TreeView.SharpTreeNode[] { methodNode } + }); + + // Walk every materialised node under the analyzer root and assert each has Icon. + var entityRow = analyzerVm.Root.Children.OfType().Last(); + ((object?)entityRow.Icon).Should().NotBeNull("the analysed-entity row needs an icon"); + entityRow.EnsureLazyChildren(); + foreach (var searchHeader in entityRow.Children.OfType()) + { + ((object?)searchHeader.Icon).Should().NotBeNull( + $"'{searchHeader.AnalyzerHeader}' analyzer-search row needs an icon — was the regression"); + } + } + + static AnalyzerTreeViewModel? FindAnalyzerPane(global::ILSpy.Docking.DockWorkspace dockWorkspace) + { + foreach (var dockable in WalkDockables(dockWorkspace.Layout)) + { + if (dockable is AnalyzerTreeViewModel apm) + return apm; + } + return null; + } + + static System.Collections.Generic.IEnumerable WalkDockables(global::Dock.Model.Core.IDockable? root) + { + if (root == null) + yield break; + yield return root; + if (root is global::Dock.Model.Core.IDock dock && dock.VisibleDockables != null) + foreach (var child in dock.VisibleDockables) + foreach (var descendant in WalkDockables(child)) + yield return descendant; + } } diff --git a/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs b/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs index eb872ae74..bbe373326 100644 --- a/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs +++ b/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs @@ -22,6 +22,7 @@ using System.Linq; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpy.Properties; +using ILSpy.Docking; using ILSpy.TreeNodes; namespace ILSpy.Analyzers @@ -41,11 +42,13 @@ namespace ILSpy.Analyzers public sealed class AnalyzeContextMenuEntry : IContextMenuEntry { readonly AnalyzerTreeViewModel analyzerTreeViewModel; + readonly DockWorkspace dockWorkspace; [ImportingConstructor] - public AnalyzeContextMenuEntry(AnalyzerTreeViewModel analyzerTreeViewModel) + public AnalyzeContextMenuEntry(AnalyzerTreeViewModel analyzerTreeViewModel, DockWorkspace dockWorkspace) { this.analyzerTreeViewModel = analyzerTreeViewModel; + this.dockWorkspace = dockWorkspace; } public bool IsVisible(TextViewContext context) @@ -66,12 +69,17 @@ namespace ILSpy.Analyzers { if (context.SelectedTreeNodes is not { Length: > 0 } nodes) return; - foreach (var member in nodes.OfType() + var analysable = nodes.OfType() .Select(n => n.Member) - .Where(IsAnalysable)) - { + .Where(IsAnalysable) + .ToList(); + foreach (var member in analysable) analyzerTreeViewModel.Analyze(member!); - } + // Bring the analyzer pane to the front so the user can see the entity they just + // added. AnalyzerTreeViewModel.Analyze deliberately leaves dock-activation to its + // caller — that's this entry's job. + if (analysable.Count > 0) + dockWorkspace.ShowToolPane(AnalyzerTreeViewModel.PaneContentId); } /// diff --git a/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs b/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs index b6ea5a284..f4c3d1360 100644 --- a/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs +++ b/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs @@ -72,6 +72,14 @@ namespace ILSpy.Analyzers ? headerText : headerText + " (" + Children.Count + " in " + stopwatch.ElapsedMilliseconds + " ms)"; + /// + /// Semantic icon for an analyzer-search header row ("Used By", "Uses", "Exposed By", + /// etc.). Without this override the row inherits null from + /// and renders an empty icon slot next to the header text — visually mismatched with + /// the result rows underneath, which all carry entity-kind icons. + /// + public override object Icon => Images.Images.Search; + protected override void LoadChildren() { cancellation?.Cancel(); diff --git a/ILSpy/Assets/Icons/Search.svg b/ILSpy/Assets/Icons/Search.svg new file mode 100644 index 000000000..1b80bf68c --- /dev/null +++ b/ILSpy/Assets/Icons/Search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs index e7ffb5f83..9cd64be71 100644 --- a/ILSpy/Images.cs +++ b/ILSpy/Images.cs @@ -63,6 +63,7 @@ namespace ILSpy.Images public static readonly IImage AssemblyWarning = LoadSvg(nameof(AssemblyWarning)); public static readonly IImage Warning = LoadSvg(nameof(Warning)); public static readonly IImage FindAssembly = LoadSvg(nameof(FindAssembly)); + public static readonly IImage Search = LoadSvg(nameof(Search)); public static readonly IImage Library = LoadSvg(nameof(Library)); public static readonly IImage NuGet = LoadPng(nameof(NuGet)); public static readonly IImage MetadataFile = LoadSvg(nameof(MetadataFile));