Browse Source

Sort results setting reaches the search pipeline

DisplaySettings.SortResults was persisted but unused — RunningSearch hardcoded
ComparerByFitness. Now SearchPaneModel.RestartSearch reads the setting at
start-of-search and passes either ComparerByFitness (default) or ComparerByName
into RunningSearch. Capturing at start matches WPF — mid-run toggles only
affect the next search.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
58f5ac24e2
  1. 91
      ILSpy.Tests/Search/SearchResultSortOrderTests.cs
  2. 17
      ILSpy/Search/RunningSearch.cs
  3. 9
      ILSpy/Search/SearchPaneModel.cs

91
ILSpy.Tests/Search/SearchResultSortOrderTests.cs

@ -0,0 +1,91 @@
// 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;
using ILSpy.AppEnv;
using ILSpy.Search;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Search;
[TestFixture]
public class SearchResultSortOrderTests
{
[AvaloniaTest]
public async Task When_SortResults_Is_False_Results_Are_Ordered_By_Name_Not_Fitness()
{
// The "Sort results by fitness" checkbox in Display Settings must reach the search
// pipeline. Default is true (rank by Fitness desc); flipping to false must rank by
// Name asc (StringComparer.Ordinal). Mirrors WPF's SearchPane.xaml.cs:288-290
// which captures the comparer at start-of-search based on DisplaySettings.SortResults.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var settings = AppComposition.Current.GetExport<SettingsService>();
var originalSortResults = settings.DisplaySettings.SortResults;
try
{
settings.DisplaySettings.SortResults = false;
var search = AppComposition.Current.GetExport<SearchPaneModel>();
search.SearchTerm = string.Empty;
search.SelectedSearchMode = search.SearchModes.First(m => m.Mode == SearchMode.Type);
search.SearchTerm = "Enumerable";
await Waiters.WaitForAsync(
() => !search.IsSearching && search.Results.Count >= 2,
timeout: TimeSpan.FromSeconds(30));
// Drop assembly/namespace results — they share a unit Fitness with all peers in
// their bucket, so any tie-break is ambiguous between fitness- and name-sort.
// Member results are where Fitness varies (1/Name.Length), so their order is
// what distinguishes the two comparers.
var names = search.Results.OfType<MemberSearchResult>()
.Select(r => r.Name)
.ToList();
names.Should().HaveCountGreaterThan(1,
"need at least two member hits to compare orderings");
names.Should().BeInAscendingOrder(StringComparer.Ordinal,
"with SortResults=false the pane must rank by Name ordinal-asc, not by Fitness");
search.SearchTerm = string.Empty;
}
finally
{
settings.DisplaySettings.SortResults = originalSortResults;
}
}
}

17
ILSpy/Search/RunningSearch.cs

@ -59,6 +59,7 @@ namespace ILSpy.Search
readonly ApiVisibility apiVisibility; readonly ApiVisibility apiVisibility;
readonly ISearchResultFactory resultFactory; readonly ISearchResultFactory resultFactory;
readonly ObservableCollection<SearchResult> sink; readonly ObservableCollection<SearchResult> sink;
readonly IComparer<SearchResult> sortComparer;
readonly ConcurrentQueue<SearchResult> queue = new(); readonly ConcurrentQueue<SearchResult> queue = new();
readonly CancellationTokenSource cts = new(); readonly CancellationTokenSource cts = new();
DispatcherTimer? drainTimer; DispatcherTimer? drainTimer;
@ -74,7 +75,8 @@ namespace ILSpy.Search
Language language, Language language,
ApiVisibility apiVisibility, ApiVisibility apiVisibility,
ISearchResultFactory resultFactory, ISearchResultFactory resultFactory,
ObservableCollection<SearchResult> sink) ObservableCollection<SearchResult> sink,
IComparer<SearchResult> sortComparer)
{ {
this.assemblies = assemblies; this.assemblies = assemblies;
this.searchTerm = searchTerm; this.searchTerm = searchTerm;
@ -83,6 +85,7 @@ namespace ILSpy.Search
this.apiVisibility = apiVisibility; this.apiVisibility = apiVisibility;
this.resultFactory = resultFactory; this.resultFactory = resultFactory;
this.sink = sink; this.sink = sink;
this.sortComparer = sortComparer;
} }
public bool IsCompleted => runTask is { IsCompleted: true }; public bool IsCompleted => runTask is { IsCompleted: true };
@ -201,12 +204,12 @@ namespace ILSpy.Search
{ {
if (!queue.TryDequeue(out var result)) if (!queue.TryDequeue(out var result))
break; break;
// Sorted insert: results land in fitness order so the most relevant hit // Sorted insert against the run's captured comparer. The Display-Settings
// rises to the top while later, less relevant matches are still streaming // "Sort results by fitness" checkbox picks between ComparerByFitness (the
// in. ObservableCollection<T> implements IList<T>, so the InsertSorted // default — shorter names rise to the top) and ComparerByName (ordinal asc).
// extension binary-searches and Inserts at the right index — O(log n) // ObservableCollection<T> implements IList<T>, so the InsertSorted extension
// compare + O(n) shift. // binary-searches and Inserts at the right index — O(log n) compare + O(n) shift.
sink.InsertSorted(result, SearchResult.ComparerByFitness); sink.InsertSorted(result, sortComparer);
} }
if (sink.Count >= MaxResults) if (sink.Count >= MaxResults)

9
ILSpy/Search/SearchPaneModel.cs

@ -196,6 +196,12 @@ namespace ILSpy.Search
?? ApiVisibility.PublicOnly; ?? ApiVisibility.PublicOnly;
var factory = new AvaloniaSearchResultFactory(language); var factory = new AvaloniaSearchResultFactory(language);
// Capture the comparer at start-of-search (matches WPF SearchPane.xaml.cs:288).
// Toggling "Sort results by fitness" mid-run won't reshuffle results already on
// screen — it only takes effect on the next search.
var sortComparer = (TryGetSettings()?.DisplaySettings.SortResults ?? true)
? SearchResult.ComparerByFitness
: SearchResult.ComparerByName;
var run = new RunningSearch( var run = new RunningSearch(
assemblyList.GetAssemblies(), assemblyList.GetAssemblies(),
term, term,
@ -203,7 +209,8 @@ namespace ILSpy.Search
language, language,
apiVisibility, apiVisibility,
factory, factory,
Results); Results,
sortComparer);
run.Completed += OnRunCompleted; run.Completed += OnRunCompleted;
currentSearch = run; currentSearch = run;
IsSearching = true; IsSearching = true;

Loading…
Cancel
Save