diff --git a/ILSpy.Tests/AssemblyTree/NavigateToReferenceAssemblyTests.cs b/ILSpy.Tests/AssemblyTree/NavigateToReferenceAssemblyTests.cs
new file mode 100644
index 000000000..0b9fb4fcb
--- /dev/null
+++ b/ILSpy.Tests/AssemblyTree/NavigateToReferenceAssemblyTests.cs
@@ -0,0 +1,96 @@
+// Copyright (c) 2026 Siegfried Pammer
+//
+// 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.IO;
+using System.Threading.Tasks;
+
+using AwesomeAssertions;
+
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.ILSpy.AssemblyTree;
+using ICSharpCode.ILSpyX;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.AssemblyTree;
+
+///
+/// A navigation target that only a reference assembly declares. The VS add-in hands ILSpy the
+/// assemblies a project references, which for a framework-targeting project are the targeting
+/// pack's reference assemblies, so refusing to look in them leaves the target unresolved and
+/// the jump silently does nothing (issue #2093).
+///
+[TestFixture]
+public class NavigateToReferenceAssemblyTests
+{
+ // The member is looked up on this fixture itself: it is public, so the reference assembly
+ // declares it too, and both assemblies are next to the test at run time.
+ public static int TargetMember(string text) => text.Length;
+
+ const string TargetId = "M:ICSharpCode.ILSpy.Tests.AssemblyTree.NavigateToReferenceAssemblyTests.TargetMember(System.String)";
+
+ static string ImplementationPath => typeof(NavigateToReferenceAssemblyTests).Assembly.Location;
+
+ static string ReferencePath => Path.Combine(
+ Path.GetDirectoryName(ImplementationPath)!, "ReferenceAssemblyFixture",
+ Path.GetFileName(ImplementationPath));
+
+ [Test]
+ public async Task The_Fixture_Really_Is_A_Reference_Assembly()
+ {
+ File.Exists(ReferencePath).Should().BeTrue(
+ "the build copies this project's reference assembly next to the tests");
+ var list = new AssemblyList();
+ var reference = list.OpenAssembly(ReferencePath);
+ var file = await reference.GetMetadataFileOrNullAsync();
+ file.Should().NotBeNull();
+ file!.IsReferenceAssembly().Should().BeTrue("otherwise the tests below prove nothing");
+ }
+
+ [Test]
+ public async Task A_Member_Only_A_Reference_Assembly_Declares_Still_Resolves()
+ {
+ var list = new AssemblyList();
+ var reference = list.OpenAssembly(ReferencePath);
+ await reference.GetMetadataFileOrNullAsync();
+
+ var entity = AssemblyTreeModel.FindEntityInRelevantAssemblies(TargetId, new[] { reference });
+
+ entity.Should().NotBeNull("a reference assembly is the only place the member can be found");
+ entity!.Name.Should().Be(nameof(TargetMember));
+ }
+
+ [Test]
+ public async Task An_Implementation_Assembly_Wins_Over_A_Reference_Assembly()
+ {
+ var list = new AssemblyList();
+ var reference = list.OpenAssembly(ReferencePath);
+ var implementation = list.OpenAssembly(ImplementationPath);
+ await reference.GetMetadataFileOrNullAsync();
+ await implementation.GetMetadataFileOrNullAsync();
+
+ // The reference assembly comes first, so a plain "first hit wins" search would answer
+ // with it; only a search that prefers real definitions picks the implementation.
+ var entity = AssemblyTreeModel.FindEntityInRelevantAssemblies(
+ TargetId, new[] { reference, implementation });
+
+ entity.Should().NotBeNull();
+ entity!.ParentModule!.MetadataFile!.FileName.Should().Be(ImplementationPath,
+ "a definition with a body is more useful than a signature-only one");
+ }
+}
diff --git a/ILSpy.Tests/Commands/CommandLineArgumentsTests.cs b/ILSpy.Tests/Commands/CommandLineArgumentsTests.cs
index 336935b52..7cde3c34e 100644
--- a/ILSpy.Tests/Commands/CommandLineArgumentsTests.cs
+++ b/ILSpy.Tests/Commands/CommandLineArgumentsTests.cs
@@ -18,6 +18,8 @@
using System;
using System.IO;
+using System.Linq;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
@@ -26,6 +28,9 @@ using AwesomeAssertions;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Languages;
+using ICSharpCode.ILSpy.Metadata;
+using ICSharpCode.ILSpy.Metadata.CorTables;
+using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.TreeNodes;
using ICSharpCode.ILSpy.ViewModels;
using ICSharpCode.ILSpy.Views;
@@ -61,6 +66,79 @@ public class CommandLineArgumentsTests
languageService.CurrentLanguage.Name.Should().Be("IL");
}
+ [AvaloniaTest]
+ public async Task NavigateTo_An_Id_That_Names_Nothing_Reports_What_Was_Searched()
+ {
+ // An ID that resolves to nothing used to leave the tree untouched and say nothing, so a
+ // jump that silently did not happen looked like a jump to the wrong place. The target
+ // and the assemblies that were searched are written to the pane the jump would have
+ // filled (issue #2093).
+
+ // Arrange - boot, and decompile something so the main tab really is a decompiler tab.
+ // Whether one is active at startup is a race, and the report lands elsewhere when it
+ // is not; the sibling test below covers that case.
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
+ var typeNode = vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ vm.AssemblyTreeModel.SelectNode(typeNode);
+ await vm.DockWorkspace.WaitForDecompiledTextAsync();
+ vm.DockWorkspace.ActiveDecompilerTab.Should().NotBeNull("the report's preferred sink must exist");
+
+ var args = CommandLineArguments.Create(new[] { "--navigateto", "M:No.Such.Type.NoSuchMember" });
+
+ // Act - apply the args.
+ await vm.AssemblyTreeModel.HandleCommandLineArgumentsAsync(args);
+
+ // Assert - the pane the jump would have filled names the target that was not found.
+ var tab = vm.DockWorkspace.ActiveDecompilerTab;
+ tab.Should().NotBeNull("the report goes to the decompiler pane");
+ tab!.Text.Should().Contain("M:No.Such.Type.NoSuchMember");
+ }
+
+ [AvaloniaTest]
+ public async Task NavigateTo_Reports_An_Unresolved_Id_Even_Without_An_Active_Decompiler_Tab()
+ {
+ // DockWorkspace.ShowText writes to the active decompiler tab and silently does nothing
+ // when the active content is something else, which is a real state at startup and
+ // whenever a metadata table is in front. A report that can go missing is no better than
+ // the silence it replaces.
+
+ // Arrange - boot and put a metadata table in front, so there is no decompiler tab.
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
+ var typeDefNode = vm.AssemblyTreeModel.FindCoreLib()
+ .GetChild()
+ .GetChild()
+ .GetChild();
+ vm.AssemblyTreeModel.SelectNode(typeDefNode);
+ await vm.DockWorkspace.WaitForMetadataTabAsync();
+ vm.DockWorkspace.ActiveDecompilerTab.Should().BeNull("the metadata table must be in front");
+
+ var args = CommandLineArguments.Create(new[] { "--navigateto", "M:No.Such.Type.NoSuchMember" });
+
+ // Act - apply the args.
+ await vm.AssemblyTreeModel.HandleCommandLineArgumentsAsync(args);
+
+ // Assert - the report opened a tab of its own rather than vanishing. ActiveDecompilerTab
+ // only ever names the main tab's content, which the metadata table still occupies, so
+ // the new tab is looked for among the open documents.
+ await Waiters.WaitForAsync(() => ReportTabs(vm).Any());
+ ReportTabs(vm).Single().Text.Should().Contain("M:No.Such.Type.NoSuchMember");
+
+ static IEnumerable ReportTabs(MainWindowViewModel vm)
+ => vm.DockWorkspace.Documents?.VisibleDockables?
+ .OfType()
+ .Select(t => t.Content)
+ .OfType()
+ .Where(t => t.Title == "Navigation")
+ ?? [];
+ }
+
[AvaloniaTest]
public async Task NavigateTo_Type_Arg_Selects_The_Matching_Type_Node()
{
diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj
index 018389097..fdd5cf9d8 100644
--- a/ILSpy.Tests/ILSpy.Tests.csproj
+++ b/ILSpy.Tests/ILSpy.Tests.csproj
@@ -27,6 +27,20 @@
+
+
+ true
+
+
+
+
+
+
+