diff --git a/ILSpy.Tests/Search/ShowSearchCommandTests.cs b/ILSpy.Tests/Search/ShowSearchCommandTests.cs new file mode 100644 index 000000000..2f16e16fe --- /dev/null +++ b/ILSpy.Tests/Search/ShowSearchCommandTests.cs @@ -0,0 +1,62 @@ +// 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.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ILSpy.AppEnv; +using ILSpy.Docking; +using ILSpy.Search; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Search; + +[TestFixture] +public class ShowSearchCommandTests +{ + [AvaloniaTest] + public async Task ShowSearchCommand_Activates_The_Search_Pane() + { + // Ctrl+Shift+F / Ctrl+E in the live app are wired to this command via + // MainWindow.KeyBindings. Here we drive the command directly and verify the + // activation reaches the SearchPaneModel. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var dockWorkspace = AppComposition.Current.GetExport(); + var search = AppComposition.Current.GetExport(); + + dockWorkspace.ShowSearchCommand.CanExecute(null).Should().BeTrue(); + dockWorkspace.ShowSearchCommand.Execute(null); + + // The dock factory's ActiveDockable should now be the search pane. We can't + // assert against the factory's deep state easily (IDockable identity), so check + // the pane's IsActive flag, which Dock.Avalonia flips during SetActiveDockable. + search.IsActive.Should().BeTrue( + "ShowSearchCommand must bring the search pane to the active tab in its dock group"); + } +} diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index afb766626..b5bd33fed 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -71,6 +71,12 @@ namespace ILSpy.Docking public IRelayCommand NavigateForwardCommand { get; } public IRelayCommand NavigateToHistoryCommand { get; } + /// + /// Brings the search tool pane to focus. Wired to Ctrl+Shift+F and Ctrl+E on the + /// main window. Idempotent — re-firing on an already-active pane is a no-op. + /// + public IRelayCommand ShowSearchCommand { get; } + // Read-only history snapshots for the Back/Forward split-button dropdowns; oldest-first. // The UI reverses these for newest-first display. public IReadOnlyList BackHistory => history.BackEntries; @@ -93,6 +99,8 @@ namespace ILSpy.Docking NavigateForwardCommand = new RelayCommand(NavigateForward, () => history.CanNavigateForward); NavigateToHistoryCommand = new RelayCommand(NavigateToHistory, entry => entry != null && (history.BackEntries.Contains(entry) || history.ForwardEntries.Contains(entry))); + ShowSearchCommand = new RelayCommand( + () => ShowToolPane(ILSpy.Search.SearchPaneModel.PaneContentId)); using (ILSpy.AppEnv.StartupLog.Phase("ILSpyDockFactory ctor + CreateLayout")) { factory = new ILSpyDockFactory(toolPaneRegistry); @@ -555,6 +563,41 @@ namespace ILSpy.Docking /// so can pick it up and pull the tree's /// selection across — setting it after-the-fact misses the activation event. /// + /// + /// Brings the tool pane with the given to focus. Walks + /// every in the layout via and + /// matches on . Silently no-ops when the pane isn't in the + /// layout — e.g. it was closed via its X button and hasn't been re-added. + /// + public void ShowToolPane(string contentId) + { + foreach (var dockable in GetAllDockables(Layout)) + { + if (dockable is ToolPaneModel toolPane && toolPane.Id == contentId) + { + factory.SetActiveDockable(toolPane); + if (toolPane.Owner is IDock owner) + factory.SetFocusedDockable(owner, toolPane); + return; + } + } + } + + static System.Collections.Generic.IEnumerable GetAllDockables(IDockable? root) + { + if (root == null) + yield break; + yield return root; + if (root is IDock dock && dock.VisibleDockables != null) + { + foreach (var child in dock.VisibleDockables) + { + foreach (var descendant in GetAllDockables(child)) + yield return descendant; + } + } + } + public ContentTabPage OpenNewTab(object content, SharpTreeNode? sourceNode = null) { var tab = new ContentTabPage { Content = content, SourceNode = sourceNode }; diff --git a/ILSpy/Views/MainWindow.axaml b/ILSpy/Views/MainWindow.axaml index ffdada095..06e1ebf8e 100644 --- a/ILSpy/Views/MainWindow.axaml +++ b/ILSpy/Views/MainWindow.axaml @@ -23,6 +23,10 @@ Avalonia 12 yet, so we wire keyboard only for now. --> + + +