From ea0b3f6abe04c9892e37e2aec9dd2e7dab577093 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 6 Jun 2026 12:19:55 +0200 Subject: [PATCH] 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 --- .../Analyzers/AnalyzeContextMenuTests.cs | 26 +++++++++++++ ILSpy/Analyzers/AnalyzeContextMenuEntry.cs | 39 ++++++++++++------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs index 8d972912a..d0d330501 100644 --- a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs +++ b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs @@ -23,12 +23,14 @@ using Avalonia.Headless.NUnit; using AwesomeAssertions; +using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpyX.TreeView; using ILSpy; using ILSpy.Analyzers; using ILSpy.AppEnv; +using ILSpy.TextView; using ILSpy.TreeNodes; using ILSpy.ViewModels; using ILSpy.Views; @@ -76,6 +78,30 @@ public class AnalyzeContextMenuTests .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() + .GetEntry(nameof(Resources.Analyze)); + + var typeNode = vm.AssemblyTreeModel.FindNode( + "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(); + 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] public async Task Executing_Analyze_Pushes_The_Selected_Entity_Into_The_Analyzer_Pane() { diff --git a/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs b/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs index 4e26c44db..adf41aaa5 100644 --- a/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs +++ b/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs @@ -53,28 +53,24 @@ namespace ILSpy.Analyzers public bool IsVisible(TextViewContext context) { - if (context.SelectedTreeNodes is not { Length: > 0 } nodes) - return false; - return nodes.All(n => n is IMemberTreeNode); + if (context.SelectedTreeNodes is { Length: > 0 } nodes) + 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) { - if (context.SelectedTreeNodes is not { Length: > 0 } nodes) - return false; - return nodes.OfType().All(n => IsAnalysable(n.Member)); + if (context.SelectedTreeNodes is { Length: > 0 } nodes) + return nodes.OfType().All(n => IsAnalysable(n.Member)); + return context.Reference?.Reference is IEntity entity && IsAnalysable(entity); } public void Execute(TextViewContext context) { - if (context.SelectedTreeNodes is not { Length: > 0 } nodes) - return; - var analysable = nodes.OfType() - .Select(n => n.Member) - .Where(IsAnalysable) - .ToList(); + var analysable = MembersToAnalyse(context); 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 // added. AnalyzerTreeViewModel.Analyze deliberately leaves dock-activation to its // caller — that's this entry's job. @@ -82,6 +78,23 @@ namespace ILSpy.Analyzers 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 MembersToAnalyse(TextViewContext context) + { + if (context.SelectedTreeNodes is { Length: > 0 } nodes) + { + return nodes.OfType() + .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 { entity }; + return new System.Collections.Generic.List(); + } + /// /// Const fields are textual literals at every use-site rather than entities the /// analyser can match against — exclude them so the entry stays disabled.