Browse Source

Record a history entry when navigating to a metadata token

Jumping to a token (the "Go to token" entry and token-cell clicks) set
suppressHistoryRecording while selecting the target table, so the jump
left no back-stack entry and Back skipped over it. Stop suppressing so
the destination is recorded, and carry the target row on the entry
(TreeNodeEntry.MetadataRow) so Back/Forward restores the exact token
rather than the top of the table.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
c9f00746b1
  1. 76
      ILSpy.Tests/Metadata/GoToTokenHistoryTests.cs
  2. 25
      ILSpy/Docking/DockWorkspace.cs
  3. 7
      ILSpy/NavigationEntry.cs

76
ILSpy.Tests/Metadata/GoToTokenHistoryTests.cs

@ -0,0 +1,76 @@ @@ -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;
/// <summary>
/// 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.
/// </summary>
[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<TypeTreeNode>(
"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));
}
}

25
ILSpy/Docking/DockWorkspace.cs

@ -608,6 +608,9 @@ namespace ILSpy.Docking @@ -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 @@ -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()

7
ILSpy/NavigationEntry.cs

@ -88,6 +88,13 @@ namespace ILSpy.Navigation @@ -88,6 +88,13 @@ namespace ILSpy.Navigation
/// </summary>
public FoldingsViewState.Snapshot? Foldings { get; set; }
/// <summary>
/// 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. <c>null</c> for ordinary tree-node selections.
/// </summary>
public int? MetadataRow { get; set; }
public TreeNodeEntry(TabPageModel tab, SharpTreeNode node)
: base(tab)
{

Loading…
Cancel
Save