diff --git a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs index 5aefe64bd..2c4bf4fdd 100644 --- a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs +++ b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs @@ -625,4 +625,55 @@ public class PreviewTabPromotionTests docs.VisibleDockables!.IndexOf(frozen).Should().BeGreaterThan(0, "the frozen ex-One slides to the right of the new One"); } + + [AvaloniaTest] + public async Task Preview_Cannot_Be_Reordered_Out_Of_Slot_0() + { + // The in-strip reorder drag commits through the same-dock MoveDockable; it's overridden to + // refuse moving the One out of slot 0. Drive that commit directly (what ItemDragHelper does). + var (_, vm) = await TestHarness.BootAsync(3); + var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; + var docs = vm.DockWorkspace.Documents!; + + var typeA = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + vm.AssemblyTreeModel.SelectNode(typeA); + vm.DockWorkspace.SettleSelection(); + var theOne = factory.MainTab!; + var typeC = vm.AssemblyTreeModel.FindNode( + "System.Private.Uri", "System", "System.Uri"); + vm.DockWorkspace.OpenNodeInNewTab(typeC); + var frozen = docs.VisibleDockables!.OfType().Last(t => t.SourceNode == typeC); + docs.VisibleDockables!.IndexOf(theOne).Should().Be(0, "precondition: the One is at index 0"); + + factory.MoveDockable(docs, theOne, frozen); + + docs.VisibleDockables!.IndexOf(theOne).Should().Be(0, + "the One must stay at index 0 -- it is immovable"); + } + + [AvaloniaTest] + public async Task Frozen_Tab_Cannot_Be_Reordered_Before_The_Preview() + { + var (_, vm) = await TestHarness.BootAsync(3); + var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; + var docs = vm.DockWorkspace.Documents!; + + var typeA = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + vm.AssemblyTreeModel.SelectNode(typeA); + vm.DockWorkspace.SettleSelection(); + var theOne = factory.MainTab!; + var typeC = vm.AssemblyTreeModel.FindNode( + "System.Private.Uri", "System", "System.Uri"); + vm.DockWorkspace.OpenNodeInNewTab(typeC); + var frozen = docs.VisibleDockables!.OfType().Last(t => t.SourceNode == typeC); + + factory.MoveDockable(docs, frozen, theOne); + + docs.VisibleDockables!.IndexOf(theOne).Should().Be(0, + "the One must remain at index 0; a frozen tab cannot slip before it"); + docs.VisibleDockables!.IndexOf(frozen).Should().BeGreaterThan(0, + "the frozen tab is clamped to a position after the One"); + } } diff --git a/ILSpy/Docking/ILSpyDockFactory.cs b/ILSpy/Docking/ILSpyDockFactory.cs index fe6e16f9d..6b99b7322 100644 --- a/ILSpy/Docking/ILSpyDockFactory.cs +++ b/ILSpy/Docking/ILSpyDockFactory.cs @@ -83,6 +83,55 @@ namespace ILSpy.Docking return fresh; } + // The One is immovable at documents-dock index 0. A within-strip reorder drag commits + // through this same-dock MoveDockable (Dock's DocumentTabStripItem/ItemDragHelper does NOT + // consult IDockable.CanDrag), so overriding it is the choke point that pins the One to + // slot 0 and keeps frozen tabs to its right. + public override void MoveDockable(IDock dock, IDockable sourceDockable, IDockable targetDockable) + { + if (ReferenceEquals(dock, Documents)) + { + // Never drag the One out of slot 0. + if (ReferenceEquals(sourceDockable, MainTab)) + return; + // A frozen tab must not land at or before the One: retarget to the tab at index 1 + // so the source lands at position 1, not 0. + if (ReferenceEquals(targetDockable, MainTab)) + { + var kids = dock.VisibleDockables; + if (kids is { Count: > 1 }) + targetDockable = kids[1]; + else + return; + } + } + base.MoveDockable(dock, sourceDockable, targetDockable); + } + + // Cross-dock move: never pull the One into another document dock (only reachable if the + // document area is ever split). + public override void MoveDockable(IDock sourceDock, IDock targetDock, IDockable sourceDockable, IDockable? targetDockable) + { + if (ReferenceEquals(sourceDockable, MainTab)) + return; + base.MoveDockable(sourceDock, targetDock, sourceDockable, targetDockable); + } + + // Self-healing insurance: whatever drag path ran, re-assert the One at index 0. Idempotent + // and cheap -- guards against a future Dock version routing a reorder past MoveDockable. + public override void OnDockableMoved(IDockable? dockable) + { + base.OnDockableMoved(dockable); + if (MainTab is not { } one || Documents?.VisibleDockables is not { } kids) + return; + int idx = kids.IndexOf(one); + if (idx > 0) + { + kids.RemoveAt(idx); + kids.Insert(0, one); + } + } + public ILSpyDockFactory(ToolPaneRegistry registry) { this.panes = registry.Panes;