mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
SearchPaneModel gains an Activate(SearchResult) method that resolves result.Reference through AssemblyTreeModel.FindTreeNode and moves the assembly-tree selection there — exactly the WPF NavigateToReferenceEventArgs flow, minus the message-bus indirection. SearchPane.axaml.cs wires DoubleTapped + Enter on the results ListBox to call Activate. Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
7 changed files with 390 additions and 13 deletions
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
// 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.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.ILSpy.Properties; |
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using ILSpy; |
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Search; |
||||
using ILSpy.TreeNodes; |
||||
using ILSpy.ViewModels; |
||||
using ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Search; |
||||
|
||||
[TestFixture] |
||||
public class ScopeSearchToTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Scope_Search_To_Assembly_Prepends_InAssembly_To_The_Search_Term() |
||||
{ |
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||
|
||||
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>(); |
||||
var entry = registry.Entries |
||||
.Single(e => e.Metadata.Header == nameof(Resources.ScopeSearchToThisAssembly)) |
||||
.Value; |
||||
|
||||
var asm = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>("System.Linq"); |
||||
var search = AppComposition.Current.GetExport<SearchPaneModel>(); |
||||
search.SearchTerm = string.Empty; |
||||
|
||||
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { asm } }) |
||||
.Should().BeTrue("the entry must surface on AssemblyTreeNode selections"); |
||||
entry.Execute(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { asm } }); |
||||
|
||||
search.SearchTerm.Should().Contain("inassembly:", |
||||
"Execute must inject the inassembly: prefix into the live search term"); |
||||
search.SearchTerm.Should().Contain("System.Linq"); |
||||
|
||||
search.SearchTerm = string.Empty; |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Scope_Search_To_Namespace_Prepends_InNamespace_To_The_Search_Term() |
||||
{ |
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||
|
||||
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>(); |
||||
var entry = registry.Entries |
||||
.Single(e => e.Metadata.Header == nameof(Resources.ScopeSearchToThisNamespace)) |
||||
.Value; |
||||
|
||||
var ns = vm.AssemblyTreeModel.FindNode<NamespaceTreeNode>("System.Linq", "System.Linq"); |
||||
var search = AppComposition.Current.GetExport<SearchPaneModel>(); |
||||
search.SearchTerm = string.Empty; |
||||
|
||||
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { ns } }) |
||||
.Should().BeTrue("the entry must surface on NamespaceTreeNode selections"); |
||||
entry.Execute(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { ns } }); |
||||
|
||||
search.SearchTerm.Should().Contain("innamespace:"); |
||||
search.SearchTerm.Should().Contain("System.Linq"); |
||||
|
||||
search.SearchTerm = string.Empty; |
||||
} |
||||
} |
||||
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
// 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; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.ILSpyX.Search; |
||||
|
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Search; |
||||
using ILSpy.TreeNodes; |
||||
using ILSpy.ViewModels; |
||||
using ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Search; |
||||
|
||||
[TestFixture] |
||||
public class SearchResultNavigationTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Activating_A_Member_Search_Result_Selects_That_Member_In_The_Assembly_Tree() |
||||
{ |
||||
// End-to-end: run a search, take the first MemberSearchResult, ask the search pane
|
||||
// to activate it, and confirm the assembly tree's SelectedItem becomes the matching
|
||||
// tree node.
|
||||
|
||||
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||
window.Show(); |
||||
var vm = (MainWindowViewModel)window.DataContext!; |
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||
|
||||
var search = AppComposition.Current.GetExport<SearchPaneModel>(); |
||||
search.Results.Clear(); |
||||
search.SelectedSearchMode = search.SearchModes.First(m => m.Mode == SearchMode.Type); |
||||
search.SearchTerm = "Enumerable"; |
||||
await Waiters.WaitForAsync(() => search.Results.Any(r => r is MemberSearchResult), |
||||
timeout: TimeSpan.FromSeconds(30)); |
||||
|
||||
var hit = (MemberSearchResult)search.Results.First(r => r is MemberSearchResult); |
||||
search.Activate(hit); |
||||
|
||||
await Waiters.WaitForAsync(() => vm.AssemblyTreeModel.SelectedItem is TypeTreeNode); |
||||
((object?)vm.AssemblyTreeModel.SelectedItem).Should().NotBeNull( |
||||
"Activate must move the assembly-tree selection to the result's underlying entity"); |
||||
(vm.AssemblyTreeModel.SelectedItem is TypeTreeNode).Should().BeTrue( |
||||
"the type-mode search returned a TypeDefinition; the assembly tree must land on the matching TypeTreeNode"); |
||||
|
||||
// Tidy up so subsequent tests don't see leftover term.
|
||||
search.SearchTerm = string.Empty; |
||||
} |
||||
} |
||||
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
// 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.Composition; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.ILSpy.Properties; |
||||
|
||||
using ILSpy.TreeNodes; |
||||
|
||||
namespace ILSpy.Search |
||||
{ |
||||
/// <summary>
|
||||
/// Right-click an <see cref="AssemblyTreeNode"/> → "Search within this assembly".
|
||||
/// Prepends <c>inassembly:<short-name></c> to the live search term so subsequent
|
||||
/// matches are restricted to that assembly. The minimal parser in
|
||||
/// <see cref="RunningSearch"/> recognises the prefix and feeds it into the
|
||||
/// <c>SearchRequest.InAssembly</c> filter the ILSpyX strategies consult.
|
||||
/// </summary>
|
||||
[ExportContextMenuEntry(Header = nameof(Resources.ScopeSearchToThisAssembly), Order = 1100)] |
||||
[Shared] |
||||
public sealed class ScopeSearchToAssemblyContextMenuEntry : IContextMenuEntry |
||||
{ |
||||
readonly SearchPaneModel searchPane; |
||||
|
||||
[ImportingConstructor] |
||||
public ScopeSearchToAssemblyContextMenuEntry(SearchPaneModel searchPane) |
||||
{ |
||||
this.searchPane = searchPane; |
||||
} |
||||
|
||||
public bool IsVisible(TextViewContext context) |
||||
{ |
||||
if (context.SelectedTreeNodes is not { Length: > 0 } nodes) |
||||
return false; |
||||
return nodes.All(n => n is AssemblyTreeNode); |
||||
} |
||||
|
||||
public bool IsEnabled(TextViewContext context) => IsVisible(context); |
||||
|
||||
public void Execute(TextViewContext context) |
||||
{ |
||||
if (context.SelectedTreeNodes is not { Length: > 0 } nodes) |
||||
return; |
||||
var asm = nodes.OfType<AssemblyTreeNode>().FirstOrDefault(); |
||||
if (asm == null) |
||||
return; |
||||
var name = asm.LoadedAssembly.ShortName; |
||||
searchPane.SearchTerm = MergeScopePrefix(searchPane.SearchTerm, "inassembly", name); |
||||
} |
||||
|
||||
internal static string MergeScopePrefix(string current, string prefix, string value) |
||||
{ |
||||
// Strip any existing same-prefix token so consecutive scope clicks REPLACE rather
|
||||
// than stack. The value may need quoting if it contains spaces — assembly short
|
||||
// names typically don't, but quote anyway for safety.
|
||||
var tokens = (current ?? string.Empty) |
||||
.Split(' ', System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries) |
||||
.Where(t => !t.StartsWith(prefix + ":", System.StringComparison.OrdinalIgnoreCase)) |
||||
.ToList(); |
||||
var quoted = value.Contains(' ') ? "\"" + value + "\"" : value; |
||||
tokens.Insert(0, prefix + ":" + quoted); |
||||
return string.Join(' ', tokens); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
// 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.Composition; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.ILSpy.Properties; |
||||
|
||||
using ILSpy.TreeNodes; |
||||
|
||||
namespace ILSpy.Search |
||||
{ |
||||
/// <summary>
|
||||
/// Right-click a <see cref="NamespaceTreeNode"/> → "Search within this namespace".
|
||||
/// Prepends <c>innamespace:<namespace></c> to the live search term so subsequent
|
||||
/// matches are restricted to that namespace. Same parser/filter path as
|
||||
/// <see cref="ScopeSearchToAssemblyContextMenuEntry"/>.
|
||||
/// </summary>
|
||||
[ExportContextMenuEntry(Header = nameof(Resources.ScopeSearchToThisNamespace), Order = 1101)] |
||||
[Shared] |
||||
public sealed class ScopeSearchToNamespaceContextMenuEntry : IContextMenuEntry |
||||
{ |
||||
readonly SearchPaneModel searchPane; |
||||
|
||||
[ImportingConstructor] |
||||
public ScopeSearchToNamespaceContextMenuEntry(SearchPaneModel searchPane) |
||||
{ |
||||
this.searchPane = searchPane; |
||||
} |
||||
|
||||
public bool IsVisible(TextViewContext context) |
||||
{ |
||||
if (context.SelectedTreeNodes is not { Length: > 0 } nodes) |
||||
return false; |
||||
return nodes.All(n => n is NamespaceTreeNode); |
||||
} |
||||
|
||||
public bool IsEnabled(TextViewContext context) => IsVisible(context); |
||||
|
||||
public void Execute(TextViewContext context) |
||||
{ |
||||
if (context.SelectedTreeNodes is not { Length: > 0 } nodes) |
||||
return; |
||||
var ns = nodes.OfType<NamespaceTreeNode>().FirstOrDefault(); |
||||
if (ns == null || string.IsNullOrEmpty(ns.Name)) |
||||
return; |
||||
searchPane.SearchTerm = ScopeSearchToAssemblyContextMenuEntry.MergeScopePrefix( |
||||
searchPane.SearchTerm, "innamespace", ns.Name); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue