mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Wire the search pane to the shared ICSharpCode.ILSpyX.Search engine. Setting SearchTerm or SelectedSearchMode now cancels any in-flight run and starts a fresh one; cleared term tears the run down. Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
5 changed files with 573 additions and 1 deletions
@ -0,0 +1,126 @@ |
|||||||
|
// 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.ViewModels; |
||||||
|
using ILSpy.Views; |
||||||
|
|
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Tests.Search; |
||||||
|
|
||||||
|
[TestFixture] |
||||||
|
public class SearchPaneStreamingTests |
||||||
|
{ |
||||||
|
[AvaloniaTest] |
||||||
|
public async Task Diagnostic_Direct_Strategy_Invocation_Surfaces_Type_Results() |
||||||
|
{ |
||||||
|
// Diagnostic: drive a MemberSearchStrategy directly to confirm the shared search
|
||||||
|
// logic works for the fixture. If this fails, the bug is in the strategy / fixture;
|
||||||
|
// if it passes but the orchestrator test below fails, the bug is in our wiring.
|
||||||
|
|
||||||
|
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||||
|
window.Show(); |
||||||
|
var vm = (MainWindowViewModel)window.DataContext!; |
||||||
|
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||||
|
|
||||||
|
var coreLibName = typeof(object).Assembly.GetName().Name!; |
||||||
|
var assemblyNode = vm.AssemblyTreeModel.FindNode<global::ILSpy.TreeNodes.AssemblyTreeNode>(coreLibName); |
||||||
|
var module = await assemblyNode.LoadedAssembly.GetMetadataFileAsync(); |
||||||
|
|
||||||
|
var queue = new System.Collections.Concurrent.ConcurrentQueue<ICSharpCode.ILSpyX.Search.SearchResult>(); |
||||||
|
var language = AppComposition.Current.GetExport<global::ILSpy.Languages.LanguageService>().CurrentLanguage; |
||||||
|
var request = new ICSharpCode.ILSpyX.Search.SearchRequest { |
||||||
|
Mode = SearchMode.Type, |
||||||
|
Keywords = new[] { "Enumerable" }, |
||||||
|
SearchResultFactory = new global::ILSpy.Search.AvaloniaSearchResultFactory(language), |
||||||
|
DecompilerSettings = new ICSharpCode.Decompiler.DecompilerSettings(), |
||||||
|
FullNameSearch = false, |
||||||
|
OmitGenerics = false, |
||||||
|
InNamespace = null!, |
||||||
|
InAssembly = null!, |
||||||
|
}; |
||||||
|
var strategy = new ICSharpCode.ILSpyX.Search.MemberSearchStrategy( |
||||||
|
language, ICSharpCode.ILSpyX.ApiVisibility.All, request, queue, |
||||||
|
ICSharpCode.ILSpyX.Search.MemberSearchKind.Type); |
||||||
|
|
||||||
|
strategy.Search(module, default); |
||||||
|
queue.Count.Should().BeGreaterThan(0, |
||||||
|
"if the strategy can't find Enumerable in the fixture, the orchestrator certainly won't either"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task Typing_A_Term_Surfaces_Matching_Type_Results_From_The_Loaded_AssemblyList() |
||||||
|
{ |
||||||
|
// End-to-end: set SearchTerm to "Enumerable", let the orchestrator run a TypeAndMember
|
||||||
|
// search across the loaded fixture assemblies, and wait for at least one matching
|
||||||
|
// result to land in the Results collection.
|
||||||
|
|
||||||
|
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.Name.Contains("Enumerable", StringComparison.OrdinalIgnoreCase)), |
||||||
|
timeout: TimeSpan.FromSeconds(30)); |
||||||
|
|
||||||
|
search.Results.Should().NotBeEmpty( |
||||||
|
"the Type search strategy must have surfaced at least one match within the timeout"); |
||||||
|
search.Results.Should().Contain(r => r.Name.Contains("Enumerable", StringComparison.OrdinalIgnoreCase)); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task Clearing_The_Search_Term_Empties_The_Result_List() |
||||||
|
{ |
||||||
|
// Switching to an empty term tears down the running search and clears the visible
|
||||||
|
// results. The user shouldn't see stale rows after they've explicitly cleared the box.
|
||||||
|
|
||||||
|
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.Count > 0, timeout: TimeSpan.FromSeconds(30)); |
||||||
|
|
||||||
|
search.SearchTerm = string.Empty; |
||||||
|
await Waiters.WaitForAsync(() => search.Results.Count == 0, timeout: TimeSpan.FromSeconds(5)); |
||||||
|
search.Results.Should().BeEmpty("an empty search term must clear stale results"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,136 @@ |
|||||||
|
// 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 ICSharpCode.Decompiler; |
||||||
|
using ICSharpCode.Decompiler.Metadata; |
||||||
|
using ICSharpCode.Decompiler.Output; |
||||||
|
using ICSharpCode.Decompiler.TypeSystem; |
||||||
|
using ICSharpCode.ILSpyX.Abstractions; |
||||||
|
using ICSharpCode.ILSpyX.Search; |
||||||
|
|
||||||
|
using ILSpy.Languages; |
||||||
|
|
||||||
|
namespace ILSpy.Search |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Builds the <see cref="SearchResult"/> objects the search strategies stream into the
|
||||||
|
/// pane's result queue. Mirrors WPF's <c>SearchResultFactory</c>: fitness ranking
|
||||||
|
/// privileges short names (shorter == higher fitness == higher rank); compiler-generated
|
||||||
|
/// names (those starting with <c><</c>) get fitness 0 so they sink to the bottom.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class AvaloniaSearchResultFactory : ISearchResultFactory |
||||||
|
{ |
||||||
|
readonly Language language; |
||||||
|
|
||||||
|
public AvaloniaSearchResultFactory(Language language) |
||||||
|
{ |
||||||
|
this.language = language; |
||||||
|
} |
||||||
|
|
||||||
|
public MemberSearchResult Create(IEntity entity) |
||||||
|
{ |
||||||
|
var declaringType = entity.DeclaringTypeDefinition; |
||||||
|
return new MemberSearchResult { |
||||||
|
Member = entity, |
||||||
|
Fitness = CalculateFitness(entity), |
||||||
|
Name = GetLanguageSpecificName(entity), |
||||||
|
Location = declaringType != null |
||||||
|
? language.TypeToString(declaringType, ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.UseFullyQualifiedTypeNames) |
||||||
|
: entity.Namespace ?? string.Empty, |
||||||
|
Assembly = entity.ParentModule?.FullAssemblyName ?? string.Empty, |
||||||
|
ToolTip = entity.ParentModule?.MetadataFile?.FileName, |
||||||
|
Image = GetIcon(entity), |
||||||
|
LocationImage = declaringType != null ? Images.Images.Class : Images.Images.Namespace, |
||||||
|
AssemblyImage = Images.Images.Assembly, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public ResourceSearchResult Create(MetadataFile module, Resource resource, ITreeNode node, ITreeNode parent) |
||||||
|
{ |
||||||
|
return new ResourceSearchResult { |
||||||
|
Resource = resource, |
||||||
|
Fitness = 1.0f / Math.Max(1, resource.Name.Length), |
||||||
|
Image = Images.Images.Library, |
||||||
|
Name = resource.Name, |
||||||
|
LocationImage = Images.Images.Library, |
||||||
|
Location = (parent.Text as string) ?? string.Empty, |
||||||
|
Assembly = module.FullName, |
||||||
|
ToolTip = module.FileName, |
||||||
|
AssemblyImage = Images.Images.Assembly, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public AssemblySearchResult Create(MetadataFile module) |
||||||
|
{ |
||||||
|
return new AssemblySearchResult { |
||||||
|
Module = module, |
||||||
|
Fitness = 1.0f / Math.Max(1, module.Name.Length), |
||||||
|
Name = module.Name, |
||||||
|
Location = module.FileName, |
||||||
|
Assembly = module.FullName, |
||||||
|
ToolTip = module.FileName, |
||||||
|
Image = Images.Images.Assembly, |
||||||
|
LocationImage = Images.Images.Library, |
||||||
|
AssemblyImage = Images.Images.Assembly, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public NamespaceSearchResult Create(MetadataFile module, INamespace ns) |
||||||
|
{ |
||||||
|
var name = ns.FullName.Length == 0 ? "-" : ns.FullName; |
||||||
|
return new NamespaceSearchResult { |
||||||
|
Namespace = ns, |
||||||
|
Name = name, |
||||||
|
Fitness = 1.0f / Math.Max(1, name.Length), |
||||||
|
Location = module.Name, |
||||||
|
Assembly = module.FullName, |
||||||
|
Image = Images.Images.Namespace, |
||||||
|
LocationImage = Images.Images.Assembly, |
||||||
|
AssemblyImage = Images.Images.Assembly, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
static float CalculateFitness(IEntity member) |
||||||
|
{ |
||||||
|
var text = member.Name; |
||||||
|
if (text.StartsWith('<')) |
||||||
|
return 0; |
||||||
|
if (member.SymbolKind is SymbolKind.Constructor or SymbolKind.Destructor) |
||||||
|
text = member.DeclaringType?.Name ?? text; |
||||||
|
text = ReflectionHelper.SplitTypeParameterCountFromReflectionName(text); |
||||||
|
return 1.0f / Math.Max(1, text.Length); |
||||||
|
} |
||||||
|
|
||||||
|
string GetLanguageSpecificName(IEntity member) => member switch { |
||||||
|
ITypeDefinition t => language.TypeToString(t, ConversionFlags.None), |
||||||
|
IField or IProperty or IMethod or IEvent => language.EntityToString(member, ConversionFlags.ShowDeclaringType), |
||||||
|
_ => member.Name, |
||||||
|
}; |
||||||
|
|
||||||
|
static object GetIcon(IEntity member) => member switch { |
||||||
|
ITypeDefinition => Images.Images.Class, |
||||||
|
IField => Images.Images.Field, |
||||||
|
IProperty => Images.Images.Property, |
||||||
|
IMethod => Images.Images.Method, |
||||||
|
IEvent => Images.Images.Event, |
||||||
|
_ => Images.Images.Library, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,222 @@ |
|||||||
|
// 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.Collections.Concurrent; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Collections.ObjectModel; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Reflection; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
using Avalonia.Threading; |
||||||
|
|
||||||
|
using ICSharpCode.Decompiler; |
||||||
|
using ICSharpCode.Decompiler.Metadata; |
||||||
|
using ICSharpCode.ILSpyX; |
||||||
|
using ICSharpCode.ILSpyX.Search; |
||||||
|
|
||||||
|
using ILSpy.Languages; |
||||||
|
|
||||||
|
namespace ILSpy.Search |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Orchestrates one search across the loaded <see cref="AssemblyList"/>. Builds a
|
||||||
|
/// <see cref="SearchRequest"/>, picks the matching <see cref="AbstractSearchStrategy"/>,
|
||||||
|
/// runs it on a background task, and streams each emitted <see cref="SearchResult"/>
|
||||||
|
/// into the supplied UI-thread <see cref="ObservableCollection{T}"/> via
|
||||||
|
/// <see cref="Dispatcher.UIThread"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RunningSearch |
||||||
|
{ |
||||||
|
const int MaxResults = 1000; |
||||||
|
|
||||||
|
readonly IReadOnlyList<LoadedAssembly> assemblies; |
||||||
|
readonly SearchMode mode; |
||||||
|
readonly string searchTerm; |
||||||
|
readonly Language language; |
||||||
|
readonly ApiVisibility apiVisibility; |
||||||
|
readonly ISearchResultFactory resultFactory; |
||||||
|
readonly ObservableCollection<SearchResult> sink; |
||||||
|
readonly ConcurrentQueue<SearchResult> queue = new(); |
||||||
|
readonly CancellationTokenSource cts = new(); |
||||||
|
Task? runTask; |
||||||
|
|
||||||
|
public RunningSearch( |
||||||
|
IReadOnlyList<LoadedAssembly> assemblies, |
||||||
|
string searchTerm, |
||||||
|
SearchMode mode, |
||||||
|
Language language, |
||||||
|
ApiVisibility apiVisibility, |
||||||
|
ISearchResultFactory resultFactory, |
||||||
|
ObservableCollection<SearchResult> sink) |
||||||
|
{ |
||||||
|
this.assemblies = assemblies; |
||||||
|
this.searchTerm = searchTerm; |
||||||
|
this.mode = mode; |
||||||
|
this.language = language; |
||||||
|
this.apiVisibility = apiVisibility; |
||||||
|
this.resultFactory = resultFactory; |
||||||
|
this.sink = sink; |
||||||
|
} |
||||||
|
|
||||||
|
public bool IsCompleted => runTask is { IsCompleted: true }; |
||||||
|
|
||||||
|
public void Start() |
||||||
|
{ |
||||||
|
var token = cts.Token; |
||||||
|
// Task.Run with an async lambda wraps the inner Task so runTask only completes
|
||||||
|
// after RunSearchAsync actually finishes — without the wrapping, runTask would
|
||||||
|
// complete the moment Task.Run returned the inner Task to the caller, and the
|
||||||
|
// drain loop would exit before any results landed.
|
||||||
|
runTask = Task.Run(async () => await RunSearchAsync(token).ConfigureAwait(false), token); |
||||||
|
_ = DrainQueueAsync(token); |
||||||
|
} |
||||||
|
|
||||||
|
public void Cancel() |
||||||
|
{ |
||||||
|
cts.Cancel(); |
||||||
|
} |
||||||
|
|
||||||
|
async Task RunSearchAsync(CancellationToken ct) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var request = BuildRequest(); |
||||||
|
var strategy = GetStrategy(request); |
||||||
|
if (strategy == null) |
||||||
|
return; |
||||||
|
foreach (var assembly in assemblies) |
||||||
|
{ |
||||||
|
ct.ThrowIfCancellationRequested(); |
||||||
|
// Force the load so search hits the metadata even for assemblies the user
|
||||||
|
// hasn't expanded in the tree yet. Failed loads are already surfaced in
|
||||||
|
// the assembly tree with the AssemblyWarning icon — don't double-report
|
||||||
|
// here, just skip so the search keeps streaming results from the
|
||||||
|
// healthy assemblies.
|
||||||
|
MetadataFile? module; |
||||||
|
try |
||||||
|
{ |
||||||
|
module = await assembly.GetMetadataFileAsync().ConfigureAwait(false); |
||||||
|
} |
||||||
|
catch (Exception ex) when (IsExpectedLoadFailure(ex)) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (module == null) |
||||||
|
continue; |
||||||
|
strategy.Search(module, ct); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (OperationCanceledException) |
||||||
|
{ |
||||||
|
// Expected on user-driven term/mode change.
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static bool IsExpectedLoadFailure(Exception ex) => ex is |
||||||
|
BadImageFormatException |
||||||
|
or IOException |
||||||
|
or InvalidOperationException |
||||||
|
or UnauthorizedAccessException |
||||||
|
or NotSupportedException |
||||||
|
or ReflectionTypeLoadException; |
||||||
|
|
||||||
|
SearchRequest BuildRequest() |
||||||
|
{ |
||||||
|
// Minimal keyword parser: split on whitespace. The WPF pane supports a richer
|
||||||
|
// prefix DSL (inassembly:, t:, /regex/, =exact, ~fuzzy, …) — that lands as a
|
||||||
|
// follow-up. Plain-keyword search covers the common case end-to-end.
|
||||||
|
var keywords = (searchTerm ?? string.Empty) |
||||||
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); |
||||||
|
return new SearchRequest { |
||||||
|
Mode = mode, |
||||||
|
Keywords = keywords, |
||||||
|
SearchResultFactory = resultFactory, |
||||||
|
// MemberSearchStrategy.Search resolves a type system via
|
||||||
|
// module.GetTypeSystemWithDecompilerSettingsOrNull(request.DecompilerSettings);
|
||||||
|
// passing null short-circuits to zero results. Default settings are fine for
|
||||||
|
// search — we only need the type system to materialise, not specific
|
||||||
|
// decompiler behaviour.
|
||||||
|
DecompilerSettings = new DecompilerSettings(), |
||||||
|
FullNameSearch = false, |
||||||
|
OmitGenerics = false, |
||||||
|
// IsInNamespaceOrAssembly treats null as "no filter, accept everything";
|
||||||
|
// the EMPTY STRING would restrict to the global namespace (Namespace.Length
|
||||||
|
// == 0), which matches almost nothing. The DSL prefixes that flip these on
|
||||||
|
// (innamespace:, inassembly:) are out of scope for the minimal parser.
|
||||||
|
InNamespace = null!, |
||||||
|
InAssembly = null!, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
AbstractSearchStrategy? GetStrategy(SearchRequest request) |
||||||
|
{ |
||||||
|
if (request.Keywords.Length == 0 && request.RegEx is null) |
||||||
|
return null; |
||||||
|
return mode switch { |
||||||
|
SearchMode.TypeAndMember => new MemberSearchStrategy(language, apiVisibility, request, queue), |
||||||
|
SearchMode.Type => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Type), |
||||||
|
SearchMode.Member => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Member), |
||||||
|
SearchMode.Method => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Method), |
||||||
|
SearchMode.Field => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Field), |
||||||
|
SearchMode.Property => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Property), |
||||||
|
SearchMode.Event => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Event), |
||||||
|
SearchMode.Literal => new LiteralSearchStrategy(language, apiVisibility, request, queue), |
||||||
|
SearchMode.Token => new MetadataTokenSearchStrategy(language, apiVisibility, request, queue), |
||||||
|
SearchMode.Assembly => new AssemblySearchStrategy(request, queue, AssemblySearchKind.NameOrFileName), |
||||||
|
SearchMode.Namespace => new NamespaceSearchStrategy(request, queue), |
||||||
|
// Resource search needs ITreeNodeFactory infrastructure — deferred to a follow-up.
|
||||||
|
_ => null, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async Task DrainQueueAsync(CancellationToken ct) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
while (!ct.IsCancellationRequested) |
||||||
|
{ |
||||||
|
var batch = new List<SearchResult>(); |
||||||
|
while (queue.TryDequeue(out var result) && batch.Count < 100) |
||||||
|
batch.Add(result); |
||||||
|
if (batch.Count > 0) |
||||||
|
{ |
||||||
|
Dispatcher.UIThread.Post(() => { |
||||||
|
foreach (var r in batch) |
||||||
|
{ |
||||||
|
if (sink.Count >= MaxResults) |
||||||
|
break; |
||||||
|
sink.Add(r); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
if (runTask is { IsCompleted: true } && queue.IsEmpty) |
||||||
|
return; |
||||||
|
await Task.Delay(50, ct).ConfigureAwait(false); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (OperationCanceledException) |
||||||
|
{ |
||||||
|
// Expected when the run is replaced or cleared.
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue