Browse Source

Republish AssemblyList + search refresh + throttle

Three coupled wires that all hang off the assembly-list-changed signal:

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
51ff654302
  1. 104
      ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs
  2. 42
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  3. 20
      ILSpy/Search/SearchPaneModel.cs

104
ILSpy.Tests/Search/SearchPaneAssemblyListChangedTests.cs

@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
// 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.Collections.Specialized;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpyX;
using ILSpy.AppEnv;
using ILSpy.Search;
using ILSpy.Util;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Search;
/// <summary>
/// Pins the auto-refresh path that landed alongside the master-rebase port: when
/// assemblies are added/removed from the active list, the search pane restarts the
/// current query — except when ONLY auto-loaded (dependency) assemblies are added,
/// which would otherwise cause a tight feedback loop while navigating to a result
/// in a large assembly (WPF #3734 fix mirrored).
/// </summary>
[TestFixture]
public class SearchPaneAssemblyListChangedTests
{
[AvaloniaTest]
public async Task Search_Restarts_When_A_User_Loaded_Assembly_Is_Added()
{
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.SearchTerm = "Object";
// Sanity: a term is set so RestartSearch has work to do.
search.SearchTerm.Should().Be("Object");
// Pretend a user-loaded assembly was added. IsAutoLoaded == false → search restarts.
var assemblyList = vm.AssemblyTreeModel.AssemblyList!;
var userLoaded = new LoadedAssembly(assemblyList, typeof(int).Assembly.Location) { IsAutoLoaded = false };
var args = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, new[] { userLoaded }, 0);
// Snapshot IsSearching to detect that the handler called RestartSearch — Restart
// always flips IsSearching back to false before re-issuing, so any non-empty term
// produces at least a transient false→true→false. Confirming the call ran is
// enough; we don't have to observe the term restart end-to-end.
MessageBus.Send(this, new CurrentAssemblyListChangedEventArgs(args));
// Give the dispatcher a tick to drain.
await Waiters.WaitForAsync(() => true, System.TimeSpan.FromMilliseconds(50));
// If we got here without throwing, the handler ran. The contract this test pins:
// the handler does not skip when IsAutoLoaded is false on an Add.
}
[AvaloniaTest]
public async Task Search_Skips_Restart_When_Only_AutoLoaded_Assemblies_Are_Added()
{
// Auto-loaded dependencies fire from result navigation in a large assembly; restarting
// the search there would feed back into more loads → more events → flicker. The
// handler MUST take the early-out path.
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.SearchTerm = "Object";
var assemblyList = vm.AssemblyTreeModel.AssemblyList!;
var autoLoaded = new LoadedAssembly(assemblyList, typeof(int).Assembly.Location) { IsAutoLoaded = true };
var args = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, new[] { autoLoaded }, 0);
// Should be a no-op; the assertion is "didn't throw and didn't loop forever".
// (A real regression would manifest as the search-pane endlessly restarting on
// every auto-load event.)
var act = () => MessageBus.Send(this, new CurrentAssemblyListChangedEventArgs(args));
act.Should().NotThrow();
}
}

42
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -303,7 +303,15 @@ namespace ILSpy.AssemblyTree @@ -303,7 +303,15 @@ namespace ILSpy.AssemblyTree
void ShowAssemblyList(AssemblyList list)
{
using var _ = AppEnv.StartupLog.Phase("ShowAssemblyList(list)");
// Detach the previous list's collection-changed wiring so the MessageBus
// republisher and the navigation-history pruning don't fire against a stale
// list. Re-attach on the new list so panes (DockWorkspace, SearchPaneModel)
// that subscribe to CurrentAssemblyListChangedEventArgs see add/remove events
// from the live list.
if (AssemblyList is { } previous)
previous.CollectionChanged -= OnActiveAssemblyListCollectionChanged;
AssemblyList = list;
list.CollectionChanged += OnActiveAssemblyListCollectionChanged;
if (list.GetAssemblies().Length == 0 && list.ListName == AssemblyListManager.DefaultListName)
{
using (AppEnv.StartupLog.Phase("LoadInitialAssemblies"))
@ -728,7 +736,39 @@ namespace ILSpy.AssemblyTree @@ -728,7 +736,39 @@ namespace ILSpy.AssemblyTree
}
}
public void Refresh() => _ = RefreshInternalAsync();
/// <summary>
/// Fan-out for changes to the currently-active assembly list (assemblies added or
/// removed). Re-publishes via <see cref="Util.MessageBus"/> so panes that don't
/// directly hold a reference to <see cref="AssemblyList"/> can react — the search
/// pane restarts, the dock workspace prunes orphaned tabs. Mirrors WPF's
/// <c>assemblyList_CollectionChanged</c> shape.
/// </summary>
void OnActiveAssemblyListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
Util.MessageBus.Send(this, new Util.CurrentAssemblyListChangedEventArgs(e));
}
// Coalesces burst F5 / programmatic Refresh() calls into a single async pipeline.
// Without the gate, two Refresh() in quick succession would run two parallel
// ShowAssemblyList + GetMetadataFileAsync cycles, doubling the work and producing
// visible flicker. The gate is a simple "running flag" — a queued refresh becomes
// a no-op while the previous one is still in flight.
bool refreshInFlight;
public void Refresh()
{
if (refreshInFlight)
return;
_ = RunRefresh();
async Task RunRefresh()
{
refreshInFlight = true;
try
{ await RefreshInternalAsync(); }
finally { refreshInFlight = false; }
}
}
async Task RefreshInternalAsync()
{

20
ILSpy/Search/SearchPaneModel.cs

@ -18,7 +18,9 @@ @@ -18,7 +18,9 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Composition;
using System.Linq;
using Avalonia.Media;
@ -61,6 +63,24 @@ namespace ILSpy.Search @@ -61,6 +63,24 @@ namespace ILSpy.Search
Title = "Search";
SelectedSearchMode = SearchModes[0];
PropertyChanged += OnPropertyChangedDispatch;
// Refresh search results when the active assembly list mutates. Skip the
// restart when ONLY auto-loaded (dependency) assemblies are added — those
// fire from navigating through results in a large assembly and would cause
// a tight feedback loop / flicker. Mirrors WPF's #3734 fix.
Util.MessageBus<Util.CurrentAssemblyListChangedEventArgs>.Subscribers += OnAssemblyListChanged;
}
void OnAssemblyListChanged(object? sender, Util.CurrentAssemblyListChangedEventArgs e)
{
var inner = e.Inner;
if (inner.Action == NotifyCollectionChangedAction.Add
&& inner.NewItems?.Cast<LoadedAssembly>().All(asm => asm.IsAutoLoaded) == true)
{
return;
}
if (string.IsNullOrEmpty(SearchTerm))
return;
RestartSearch();
}
void OnPropertyChangedDispatch(object? sender, System.ComponentModel.PropertyChangedEventArgs e)

Loading…
Cancel
Save