Browse Source

Offer Analyze on a right-clicked symbol in the decompiled code

The decompiler text-view context menu only read SelectedTreeNodes, so Analyze never appeared when right-clicking a reference in the code. Read the resolved entity from context.Reference too (the same seam Show-in-Metadata uses), so right-clicking a type/member in the decompiled output offers Analyze and pushes it into the analyzer pane.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
ea0b3f6abe
  1. 26
      ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs
  2. 35
      ILSpy/Analyzers/AnalyzeContextMenuEntry.cs

26
ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs

@ -23,12 +23,14 @@ using Avalonia.Headless.NUnit;
using AwesomeAssertions; using AwesomeAssertions;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpyX.TreeView; using ICSharpCode.ILSpyX.TreeView;
using ILSpy; using ILSpy;
using ILSpy.Analyzers; using ILSpy.Analyzers;
using ILSpy.AppEnv; using ILSpy.AppEnv;
using ILSpy.TextView;
using ILSpy.TreeNodes; using ILSpy.TreeNodes;
using ILSpy.ViewModels; using ILSpy.ViewModels;
using ILSpy.Views; using ILSpy.Views;
@ -76,6 +78,30 @@ public class AnalyzeContextMenuTests
.Should().BeFalse("an empty selection must hide the entry"); .Should().BeFalse("an empty selection must hide the entry");
} }
[AvaloniaTest]
public async Task Analyze_Is_Visible_And_Works_For_A_Clicked_Code_Reference()
{
// Right-clicking a resolved symbol (IEntity) in the decompiled code -- not a tree node --
// must also surface Analyze and push that entity into the analyzer pane.
var (_, vm) = await TestHarness.BootAsync();
var entry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>()
.GetEntry(nameof(Resources.Analyze));
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
var entity = (IEntity)typeNode.Member!;
var context = new TextViewContext { Reference = new ReferenceSegment { Reference = entity } };
entry.IsVisible(context).Should().BeTrue("a clicked code reference to an entity must surface Analyze");
entry.IsEnabled(context).Should().BeTrue();
var analyzerVm = AppComposition.Current.GetExport<AnalyzerTreeViewModel>();
var before = analyzerVm.Root.Children.Count;
entry.Execute(context);
analyzerVm.Root.Children.Count.Should().Be(before + 1,
"analyzing a clicked code reference must add it to the analyzer pane");
}
[AvaloniaTest] [AvaloniaTest]
public async Task Executing_Analyze_Pushes_The_Selected_Entity_Into_The_Analyzer_Pane() public async Task Executing_Analyze_Pushes_The_Selected_Entity_Into_The_Analyzer_Pane()
{ {

35
ILSpy/Analyzers/AnalyzeContextMenuEntry.cs

@ -53,28 +53,24 @@ namespace ILSpy.Analyzers
public bool IsVisible(TextViewContext context) public bool IsVisible(TextViewContext context)
{ {
if (context.SelectedTreeNodes is not { Length: > 0 } nodes) if (context.SelectedTreeNodes is { Length: > 0 } nodes)
return false;
return nodes.All(n => n is IMemberTreeNode); return nodes.All(n => n is IMemberTreeNode);
// Right-clicking a resolved symbol in the decompiled code: the reference carries the entity.
return context.Reference?.Reference is IEntity;
} }
public bool IsEnabled(TextViewContext context) public bool IsEnabled(TextViewContext context)
{ {
if (context.SelectedTreeNodes is not { Length: > 0 } nodes) if (context.SelectedTreeNodes is { Length: > 0 } nodes)
return false;
return nodes.OfType<IMemberTreeNode>().All(n => IsAnalysable(n.Member)); return nodes.OfType<IMemberTreeNode>().All(n => IsAnalysable(n.Member));
return context.Reference?.Reference is IEntity entity && IsAnalysable(entity);
} }
public void Execute(TextViewContext context) public void Execute(TextViewContext context)
{ {
if (context.SelectedTreeNodes is not { Length: > 0 } nodes) var analysable = MembersToAnalyse(context);
return;
var analysable = nodes.OfType<IMemberTreeNode>()
.Select(n => n.Member)
.Where(IsAnalysable)
.ToList();
foreach (var member in analysable) foreach (var member in analysable)
analyzerTreeViewModel.Analyze(member!); analyzerTreeViewModel.Analyze(member);
// Bring the analyzer pane to the front so the user can see the entity they just // 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 // added. AnalyzerTreeViewModel.Analyze deliberately leaves dock-activation to its
// caller — that's this entry's job. // caller — that's this entry's job.
@ -82,6 +78,23 @@ namespace ILSpy.Analyzers
dockWorkspace.ShowToolPane(AnalyzerTreeViewModel.PaneContentId); dockWorkspace.ShowToolPane(AnalyzerTreeViewModel.PaneContentId);
} }
// The analysable entities for this invocation: a tree-node selection (assembly/analyzer tree),
// or the single resolved entity under a right-clicked code reference (decompiler text view).
static System.Collections.Generic.List<IEntity> MembersToAnalyse(TextViewContext context)
{
if (context.SelectedTreeNodes is { Length: > 0 } nodes)
{
return nodes.OfType<IMemberTreeNode>()
.Select(n => n.Member)
.Where(IsAnalysable)
.Select(m => m!)
.ToList();
}
if (context.Reference?.Reference is IEntity entity && IsAnalysable(entity))
return new System.Collections.Generic.List<IEntity> { entity };
return new System.Collections.Generic.List<IEntity>();
}
/// <summary> /// <summary>
/// Const fields are textual literals at every use-site rather than entities the /// Const fields are textual literals at every use-site rather than entities the
/// analyser can match against — exclude them so the entry stays disabled. /// analyser can match against — exclude them so the entry stays disabled.

Loading…
Cancel
Save