Browse Source

Analyzer pane polish — surface on Analyze, header icon

Two of the three analyzer follow-ups I'd logged from earlier live-testing.
Both are tiny, both close gaps that made the analyzer feature feel
half-finished:
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
63a5cea58f
  1. 113
      ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs
  2. 18
      ILSpy/Analyzers/AnalyzeContextMenuEntry.cs
  3. 8
      ILSpy/Analyzers/AnalyzerSearchTreeNode.cs
  4. 1
      ILSpy/Assets/Icons/Search.svg
  5. 1
      ILSpy/Images.cs

113
ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs

@ -145,4 +145,117 @@ public class AnalyzeContextMenuTests @@ -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<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var dockWorkspace = AppComposition.Current.GetExport<global::ILSpy.Docking.DockWorkspace>();
var analyzerVm = AppComposition.Current.GetExport<AnalyzerTreeViewModel>();
var entry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>()
.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<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.IsExpanded = true;
var methodNode = typeNode.Children.OfType<MethodTreeNode>()
.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<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var analyzerVm = AppComposition.Current.GetExport<AnalyzerTreeViewModel>();
var entry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>()
.Entries.Single(e => e.Metadata.Header == nameof(Resources.Analyze)).Value;
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.IsExpanded = true;
var methodNode = typeNode.Children.OfType<MethodTreeNode>()
.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<AnalyzerEntityTreeNode>().Last();
((object?)entityRow.Icon).Should().NotBeNull("the analysed-entity row needs an icon");
entityRow.EnsureLazyChildren();
foreach (var searchHeader in entityRow.Children.OfType<AnalyzerSearchTreeNode>())
{
((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<global::Dock.Model.Core.IDockable> 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;
}
}

18
ILSpy/Analyzers/AnalyzeContextMenuEntry.cs

@ -22,6 +22,7 @@ using System.Linq; @@ -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 @@ -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 @@ -66,12 +69,17 @@ namespace ILSpy.Analyzers
{
if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
return;
foreach (var member in nodes.OfType<IMemberTreeNode>()
var analysable = nodes.OfType<IMemberTreeNode>()
.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);
}
/// <summary>

8
ILSpy/Analyzers/AnalyzerSearchTreeNode.cs

@ -72,6 +72,14 @@ namespace ILSpy.Analyzers @@ -72,6 +72,14 @@ namespace ILSpy.Analyzers
? headerText
: headerText + " (" + Children.Count + " in " + stopwatch.ElapsedMilliseconds + " ms)";
/// <summary>
/// Semantic icon for an analyzer-search header row ("Used By", "Uses", "Exposed By",
/// etc.). Without this override the row inherits null from <see cref="SharpTreeNode"/>
/// and renders an empty icon slot next to the header text — visually mismatched with
/// the result rows underneath, which all carry entity-kind icons.
/// </summary>
public override object Icon => Images.Images.Search;
protected override void LoadChildren()
{
cancellation?.Cancel();

1
ILSpy/Assets/Icons/Search.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 5.833a5.84 5.84 0 0 1-5.833 5.833 5.688 5.688 0 0 1-2.913-.8L2.56 15.559a1.494 1.494 0 0 1-2.121.002 1.501 1.501 0 0 1 0-2.121l4.694-4.695a5.69 5.69 0 0 1-.8-2.911C4.333 2.617 6.95 0 10.167 0S16 2.617 16 5.833z" id="outline"/><path class="icon-vs-fg" d="M14 5.833a3.837 3.837 0 0 1-3.833 3.833c-2.114 0-3.833-1.72-3.833-3.833S8.053 2 10.167 2A3.838 3.838 0 0 1 14 5.833z" id="iconFg"/><path class="icon-vs-bg" d="M10.167 1a4.84 4.84 0 0 0-4.834 4.833c0 1.152.422 2.197 1.097 3.028l-5.284 5.285a.5.5 0 0 0 .708.708l5.284-5.285c.832.676 1.877 1.098 3.029 1.098 2.665 0 4.833-2.168 4.833-4.833S12.832 1 10.167 1zm0 8.667c-2.114 0-3.833-1.72-3.833-3.833S8.053 2 10.167 2C12.28 2 14 3.72 14 5.833s-1.72 3.834-3.833 3.834z" id="iconBg"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
ILSpy/Images.cs

@ -63,6 +63,7 @@ namespace ILSpy.Images @@ -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));

Loading…
Cancel
Save