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) {