diff --git a/ILSpy.Tests/Metadata/GoToTokenHistoryTests.cs b/ILSpy.Tests/Metadata/GoToTokenHistoryTests.cs
new file mode 100644
index 000000000..ae62b33d2
--- /dev/null
+++ b/ILSpy.Tests/Metadata/GoToTokenHistoryTests.cs
@@ -0,0 +1,76 @@
+// 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.Reflection.Metadata.Ecma335;
+using System.Threading.Tasks;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ICSharpCode.Decompiler.Metadata;
+
+using ILSpy.Metadata;
+using ILSpy.TreeNodes;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Metadata;
+
+///
+/// Navigating to a metadata token (the "Go to token" / token-cell path) must push a back/forward
+/// history entry, so Back returns the user to where they were before the jump.
+///
+[TestFixture]
+public class GoToTokenHistoryTests
+{
+ [AvaloniaTest]
+ public async Task Navigating_To_A_Token_Records_A_History_Entry_And_Back_Returns()
+ {
+ var (_, vm) = await TestHarness.BootAsync();
+ var ws = vm.DockWorkspace;
+
+ // Start somewhere concrete: a decompiled type. This becomes the entry Back should return to.
+ var startNode = vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ Assert.That(startNode, Is.Not.Null);
+ vm.AssemblyTreeModel.SelectNode(startNode);
+ await ws.WaitForDecompiledTextAsync();
+
+ int backBefore = ws.BackHistory.Count;
+
+ // Jump to a metadata token (a CoreLib TypeDef). This lands on the metadata TypeDef table —
+ // a different node than the starting type, so it must record a new history entry.
+ var coreLib = vm.AssemblyTreeModel.FindCoreLib();
+ var metadataFile = coreLib.LoadedAssembly.GetMetadataFileOrNull();
+ Assert.That(metadataFile, Is.Not.Null);
+ ws.NavigateToToken(new MetadataTokenReference(metadataFile!, MetadataTokens.EntityHandle(0x02000002)));
+
+ await Waiters.WaitForAsync(
+ () => vm.AssemblyTreeModel.SelectedItem is MetadataTableTreeNode);
+
+ ws.BackHistory.Count.Should().BeGreaterThan(backBefore,
+ "go-to-token must record a history entry so Back returns to the previous location");
+ ws.NavigateBackCommand.CanExecute(null).Should().BeTrue();
+
+ // Back returns to the starting type.
+ ws.NavigateBackCommand.Execute(null);
+ await Waiters.WaitForAsync(
+ () => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, startNode));
+ }
+}
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index fdb6f1f73..566b2411c 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -608,6 +608,9 @@ namespace ILSpy.Docking
treeNode.Foldings);
}
assemblyTreeModel.SelectedItem = treeNode.Node;
+ // Restore the metadata-table row for entries created by "Go to token".
+ if (treeNode.MetadataRow is { } metadataRow && factory.MainTab?.Content is MetadataTablePageModel pm)
+ pm.ScrollToRow = metadataRow;
}
// StaticPageEntry: just reactivating the tab is enough — its content was
// preserved because IsStaticContent kept tree-node selections from targeting it.
@@ -996,20 +999,20 @@ namespace ILSpy.Docking
if (tableNode == null)
return;
- suppressHistoryRecording = true;
- try
- {
- assemblyTreeModel.SelectedItem = tableNode;
- }
- finally
- {
- suppressHistoryRecording = false;
- }
+ // Let the selection record a history entry so Back returns the user here from wherever
+ // the token jump originated (a code view or another metadata table).
+ assemblyTreeModel.SelectedItem = tableNode;
// ShowSelectedNode just settled MainTab.Content to the table's MetadataTablePageModel.
- // Set ScrollToRow to the handle's row number (1-based → 0-based).
+ // Set ScrollToRow to the handle's row number (1-based → 0-based), and stamp that row onto
+ // the just-recorded history entry so Back/Forward restores the exact token, not the table top.
if (factory.MainTab?.Content is MetadataTablePageModel pm)
- pm.ScrollToRow = MetadataTokens.GetRowNumber((EntityHandle)reference.Handle) - 1;
+ {
+ int row = MetadataTokens.GetRowNumber((EntityHandle)reference.Handle) - 1;
+ pm.ScrollToRow = row;
+ if (history.Current is TreeNodeEntry current && ReferenceEquals(current.Node, tableNode))
+ current.MetadataRow = row;
+ }
}
DecompilerTabPageModel CreateDecompilerContent()
diff --git a/ILSpy/NavigationEntry.cs b/ILSpy/NavigationEntry.cs
index 9874f0136..e19b4634d 100644
--- a/ILSpy/NavigationEntry.cs
+++ b/ILSpy/NavigationEntry.cs
@@ -88,6 +88,13 @@ namespace ILSpy.Navigation
///
public FoldingsViewState.Snapshot? Foldings { get; set; }
+ ///
+ /// For a metadata-table node reached via "Go to token", the 0-based row the grid was
+ /// scrolled to. Restored on Back/Forward so returning to this entry lands on the exact
+ /// token, not just the top of the table. null for ordinary tree-node selections.
+ ///
+ public int? MetadataRow { get; set; }
+
public TreeNodeEntry(TabPageModel tab, SharpTreeNode node)
: base(tab)
{