diff --git a/ILSpy.Tests/ContextMenus/ReferenceScopeAndNewTabTests.cs b/ILSpy.Tests/ContextMenus/ReferenceScopeAndNewTabTests.cs
new file mode 100644
index 000000000..4fe363bd1
--- /dev/null
+++ b/ILSpy.Tests/ContextMenus/ReferenceScopeAndNewTabTests.cs
@@ -0,0 +1,110 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// 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 Avalonia.Threading;
+
+using AwesomeAssertions;
+
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpy.Properties;
+
+using ILSpy;
+using ILSpy.AppEnv;
+using ILSpy.Search;
+using ILSpy.TextView;
+using ILSpy.TreeNodes;
+using ILSpy.ViewModels;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+///
+/// The remaining decompiler text-view entries that act on a clicked symbol (IEntity): "Decompile to
+/// new tab", and the two "Scope search to ..." entries.
+///
+[TestFixture]
+public class ReferenceScopeAndNewTabTests
+{
+ static TextViewContext RefContext(IEntity entity)
+ => new() { Reference = new ReferenceSegment { Reference = entity } };
+
+ [AvaloniaTest]
+ public async Task Decompile_To_New_Tab_On_A_Reference_Opens_A_New_Tab()
+ {
+ var (_, vm) = await TestHarness.BootAsync(3);
+ // Two entries share the DecompileToNewPanel header (tree + metadata-row); pick the tree/code one.
+ var entry = AppComposition.Current.GetExport().Entries
+ .First(e => e.Value.GetType().Name == "DecompileInNewViewCommand").Value;
+
+ var typeNode = vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ var entity = (IEntity)typeNode.Member!;
+
+ entry.IsVisible(RefContext(entity)).Should().BeTrue("a clicked entity must surface Decompile to new tab");
+
+ int before = vm.DockWorkspace.Documents!.VisibleDockables!.OfType().Count();
+ entry.Execute(RefContext(entity));
+ for (int i = 0; i < 8; i++)
+ {
+ Dispatcher.UIThread.RunJobs();
+ await Task.Delay(20);
+ }
+
+ vm.DockWorkspace.Documents!.VisibleDockables!.OfType().Count()
+ .Should().BeGreaterThan(before, "Decompile to new tab on a code reference must open a new document tab");
+ }
+
+ [AvaloniaTest]
+ public async Task Scope_Search_To_Assembly_On_A_Reference_Sets_The_inassembly_Filter()
+ {
+ var (_, vm) = await TestHarness.BootAsync(3);
+ var entry = AppComposition.Current.GetExport()
+ .GetEntry(nameof(Resources.ScopeSearchToThisAssembly));
+ var entity = (IEntity)vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable").Member!;
+
+ entry.IsVisible(RefContext(entity)).Should().BeTrue();
+ entry.Execute(RefContext(entity));
+
+ var search = AppComposition.Current.GetExport();
+ search.SearchTerm.Should().Contain("inassembly:").And.Contain("System.Linq",
+ "scoping to the reference's assembly must prepend inassembly:");
+ }
+
+ [AvaloniaTest]
+ public async Task Scope_Search_To_Namespace_On_A_Reference_Sets_The_innamespace_Filter()
+ {
+ var (_, vm) = await TestHarness.BootAsync(3);
+ var entry = AppComposition.Current.GetExport()
+ .GetEntry(nameof(Resources.ScopeSearchToThisNamespace));
+ var entity = (IEntity)vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable").Member!;
+
+ entry.IsVisible(RefContext(entity)).Should().BeTrue();
+ entry.Execute(RefContext(entity));
+
+ var search = AppComposition.Current.GetExport();
+ search.SearchTerm.Should().Contain("innamespace:").And.Contain("System.Linq",
+ "scoping to the reference's namespace must prepend innamespace:");
+ }
+}
diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
index 2e4e30a67..01c49974e 100644
--- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -179,7 +179,16 @@ namespace ILSpy.AssemblyTree
var resolved = FindTreeNode(entity);
if (resolved == null)
return;
- SelectedItem = resolved;
+ if (e.InNewTabPage)
+ {
+ // Open the definition in a fresh carve-out tab instead of replacing the current view
+ // (e.g. "Decompile to new tab" on a symbol in the code).
+ TryGetExport()?.OpenNodeInNewTab(resolved);
+ }
+ else
+ {
+ SelectedItem = resolved;
+ }
// Source is the originally-analysed entity (set by AnalyzerEntityTreeNode.ActivateItem).
// Push it onto the active decompiler tab's HighlightedReference so the editor view
// paints local-reference marks on every match once the new Text lands.
diff --git a/ILSpy/Commands/DecompileInNewViewCommand.cs b/ILSpy/Commands/DecompileInNewViewCommand.cs
index 1c5e29e32..5b1d62e2d 100644
--- a/ILSpy/Commands/DecompileInNewViewCommand.cs
+++ b/ILSpy/Commands/DecompileInNewViewCommand.cs
@@ -54,26 +54,34 @@ namespace ILSpy.Commands
}
public bool IsVisible(TextViewContext context)
- => SelectedNodes(context).Any();
+ => SelectedNodes(context).Any() || ReferencedEntity(context) is not null;
- public bool IsEnabled(TextViewContext context)
- => SelectedNodes(context).Any();
+ public bool IsEnabled(TextViewContext context) => IsVisible(context);
public void Execute(TextViewContext context)
{
var nodes = SelectedNodes(context).ToArray();
- if (nodes.Length == 0)
+ if (nodes.Length > 0)
+ {
+ var content = new DecompilerTabPageModel { Language = languageService.CurrentLanguage };
+ // Single-node selection carries a SourceNode so the tab/tree stay in lockstep
+ // when the user flips tabs. Multi-node selections leave SourceNode null —
+ // no single tree row represents the union.
+ dockWorkspace.OpenNewTab(content, sourceNode: nodes.Length == 1 ? nodes[0] : null);
+ content.CurrentNodes = nodes;
return;
- var content = new DecompilerTabPageModel { Language = languageService.CurrentLanguage };
- // Single-node selection carries a SourceNode so the tab/tree stay in lockstep
- // when the user flips tabs. Multi-node selections leave SourceNode null —
- // no single tree row represents the union.
- dockWorkspace.OpenNewTab(content, sourceNode: nodes.Length == 1 ? nodes[0] : null);
- content.CurrentNodes = nodes;
+ }
+ // Right-click on a symbol in the decompiled code: open its definition in a new tab. The
+ // navigate handler resolves the entity to its tree node and opens it via OpenNodeInNewTab.
+ if (ReferencedEntity(context) is { } entity)
+ Util.MessageBus.Send(this, new Util.NavigateToReferenceEventArgs(entity, inNewTabPage: true));
}
static System.Collections.Generic.IEnumerable SelectedNodes(TextViewContext context)
=> context.SelectedTreeNodes?.OfType()
?? System.Linq.Enumerable.Empty();
+
+ static ICSharpCode.Decompiler.TypeSystem.IEntity? ReferencedEntity(TextViewContext context)
+ => context.Reference?.Reference as ICSharpCode.Decompiler.TypeSystem.IEntity;
}
}
diff --git a/ILSpy/Search/ScopeSearchToAssemblyContextMenuEntry.cs b/ILSpy/Search/ScopeSearchToAssemblyContextMenuEntry.cs
index 46569de15..e7175b588 100644
--- a/ILSpy/Search/ScopeSearchToAssemblyContextMenuEntry.cs
+++ b/ILSpy/Search/ScopeSearchToAssemblyContextMenuEntry.cs
@@ -19,6 +19,7 @@
using System.Composition;
using System.Linq;
+using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.Properties;
using ILSpy.TreeNodes;
@@ -44,26 +45,26 @@ namespace ILSpy.Search
this.searchPane = searchPane;
}
- public bool IsVisible(TextViewContext context)
- {
- if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
- return false;
- return nodes.All(n => n is AssemblyTreeNode);
- }
+ public bool IsVisible(TextViewContext context) => AssemblyName(context) is not null;
public bool IsEnabled(TextViewContext context) => IsVisible(context);
public void Execute(TextViewContext context)
{
- if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
- return;
- var asm = nodes.OfType().FirstOrDefault();
- if (asm == null)
+ if (AssemblyName(context) is not { } name)
return;
- var name = asm.LoadedAssembly.ShortName;
searchPane.SearchTerm = MergeScopePrefix(searchPane.SearchTerm, "inassembly", name);
}
+ // The assembly to scope to: a selected AssemblyTreeNode in the tree, or the assembly that owns
+ // the symbol under a right-clicked code reference.
+ static string? AssemblyName(TextViewContext context)
+ {
+ if (context.SelectedTreeNodes is { Length: > 0 } nodes && nodes.All(n => n is AssemblyTreeNode))
+ return nodes.OfType().FirstOrDefault()?.LoadedAssembly.ShortName;
+ return (context.Reference?.Reference as IEntity)?.ParentModule?.AssemblyName;
+ }
+
internal static string MergeScopePrefix(string current, string prefix, string value)
{
// Strip any existing same-prefix token so consecutive scope clicks REPLACE rather
diff --git a/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs b/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs
index c79c2eb9e..cee4d9232 100644
--- a/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs
+++ b/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs
@@ -19,6 +19,7 @@
using System.Composition;
using System.Linq;
+using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.Properties;
using ILSpy.TreeNodes;
@@ -43,24 +44,24 @@ namespace ILSpy.Search
this.searchPane = searchPane;
}
- public bool IsVisible(TextViewContext context)
- {
- if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
- return false;
- return nodes.All(n => n is NamespaceTreeNode);
- }
+ public bool IsVisible(TextViewContext context) => !string.IsNullOrEmpty(Namespace(context));
public bool IsEnabled(TextViewContext context) => IsVisible(context);
public void Execute(TextViewContext context)
{
- if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
- return;
- var ns = nodes.OfType().FirstOrDefault();
- if (ns == null || string.IsNullOrEmpty(ns.Name))
- return;
- searchPane.SearchTerm = ScopeSearchToAssemblyContextMenuEntry.MergeScopePrefix(
- searchPane.SearchTerm, "innamespace", ns.Name);
+ if (Namespace(context) is { Length: > 0 } ns)
+ searchPane.SearchTerm = ScopeSearchToAssemblyContextMenuEntry.MergeScopePrefix(
+ searchPane.SearchTerm, "innamespace", ns);
+ }
+
+ // The namespace to scope to: a selected NamespaceTreeNode in the tree, or the namespace of the
+ // symbol under a right-clicked code reference. The empty (global) namespace doesn't scope.
+ static string? Namespace(TextViewContext context)
+ {
+ if (context.SelectedTreeNodes is { Length: > 0 } nodes && nodes.All(n => n is NamespaceTreeNode))
+ return nodes.OfType().FirstOrDefault()?.Name;
+ return (context.Reference?.Reference as IEntity)?.Namespace;
}
}
}