From 9a1749c1b23637b4fe40c378d45630dd81a81e79 Mon Sep 17 00:00:00 2001 From: Gs-itisitcat <66198929+Gs-itisitcat@users.noreply.github.com> Date: Wed, 22 May 2024 19:39:00 +0900 Subject: [PATCH 1/3] Create DirectorySearcherBase and move some properties from ISearcher --- .../Searcher/DirectorySearcherBase.cs | 56 ++++++++++++++++++ .../Searcher/ISearcher.cs | 10 ---- .../NonRecursiveRepositorySearcher.cs | 58 ++++-------------- .../Searcher/RecursiveRepositorySearcher.cs | 59 ++++--------------- 4 files changed, 79 insertions(+), 104 deletions(-) create mode 100644 local-repository-listing/Searcher/DirectorySearcherBase.cs diff --git a/local-repository-listing/Searcher/DirectorySearcherBase.cs b/local-repository-listing/Searcher/DirectorySearcherBase.cs new file mode 100644 index 0000000..d7152b4 --- /dev/null +++ b/local-repository-listing/Searcher/DirectorySearcherBase.cs @@ -0,0 +1,56 @@ + +using Microsoft.Extensions.FileSystemGlobbing; + +namespace LocalRepositoryListing.Searcher; + +public abstract class DirectorySearcherBase : ISearcher +{ + protected static readonly string _rootSearchPattern = "*"; + protected static readonly string _searchPattern = ".git"; + /// + /// Gets the root directories to search in. + /// + public IReadOnlyCollection RootDirectories { get; init; } + + /// + /// Gets the paths to exclude from the search. + /// + public IReadOnlyCollection ExcludePaths { get; init; } + + /// + /// Gets the directory names to exclude from the search. + /// + public IReadOnlyCollection ExcludeNames { get; init; } + + private readonly Matcher _nameMatcher = new(); + + public DirectorySearcherBase(IList rootDirectories, IList excludePaths, IList excludeNames) + { + RootDirectories = rootDirectories.AsReadOnly(); + ExcludePaths = excludePaths.AsReadOnly(); + ExcludeNames = excludeNames.AsReadOnly(); + _nameMatcher.AddIncludePatterns(excludeNames); + } + + protected abstract EnumerationOptions EnumerationOptions { get; } + + /// + /// Determines if the specified directory matches the exclusion criteria. + /// + /// The object representing the directory to check. + /// true if the directory matches the exclusion criteria; otherwise, false. + protected bool IsMatchExclude(DirectoryInfo directoryInfo) + { + return directoryInfo.FullName + .Split(Path.DirectorySeparatorChar) + .Where(p => !string.IsNullOrEmpty(p)) + .Any(p => _nameMatcher.Match(p).HasMatches) + || ExcludePaths + .Any(p => directoryInfo.FullName + .Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .Contains(p) + ); + } + + public abstract ParallelQuery Search(CancellationToken cancellationToken); +} diff --git a/local-repository-listing/Searcher/ISearcher.cs b/local-repository-listing/Searcher/ISearcher.cs index 0840f85..8befc92 100644 --- a/local-repository-listing/Searcher/ISearcher.cs +++ b/local-repository-listing/Searcher/ISearcher.cs @@ -4,16 +4,6 @@ /// public interface ISearcher { - /// - /// Gets the root directories to search in. - /// - public IReadOnlyCollection RootDirectories { get; } - - /// - /// Gets the paths to exclude from the search. - /// - public IReadOnlyCollection ExcludePaths { get; } - /// /// Gets the directory names to exclude from the search. /// diff --git a/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs b/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs index fd8a63d..c3e2ef3 100644 --- a/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs +++ b/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs @@ -1,17 +1,17 @@ -using Microsoft.Extensions.FileSystemGlobbing; - -namespace LocalRepositoryListing.Searcher; +namespace LocalRepositoryListing.Searcher; /// /// Represents a class that searches for local repositories within specified root directories. /// -public class NonRecursiveRepositorySearcher : ISearcher +/// +/// Initializes a new instance of the class. +/// +/// The root directories to search in. +/// The paths to exclude from the search. +/// The names to exclude from the search. +public class NonRecursiveRepositorySearcher(IList rootDirectories, IList excludePaths, IList excludeNames) : DirectorySearcherBase(rootDirectories, excludePaths, excludeNames), ISearcher { - public IReadOnlyCollection RootDirectories { get; init; } - public IReadOnlyCollection ExcludePaths { get; init; } - public IReadOnlyCollection ExcludeNames { get; init; } - - private static readonly EnumerationOptions _enumerationOptions = new() + protected override EnumerationOptions EnumerationOptions { get; } = new() { RecurseSubdirectories = false, IgnoreInaccessible = true, @@ -30,48 +30,14 @@ public class NonRecursiveRepositorySearcher : ISearcher /// An array of root directories to search for repositories. public NonRecursiveRepositorySearcher(IList rootDirectories) : this(rootDirectories, [], []) { } - /// - /// Initializes a new instance of the class. - /// - /// The root directories to search in. - /// The paths to exclude from the search. - /// The names to exclude from the search. - public NonRecursiveRepositorySearcher(IList rootDirectories, IList excludePaths, IList excludeNames) - { - RootDirectories = rootDirectories.AsReadOnly(); - ExcludePaths = excludePaths.AsReadOnly(); - ExcludeNames = excludeNames.AsReadOnly(); - _nameMatcher.AddIncludePatterns(excludeNames); - } - - private readonly Matcher _nameMatcher = new(); - - /// - /// Determines if the specified directory matches the exclusion criteria. - /// - /// The object representing the directory to check. - /// true if the directory matches the exclusion criteria; otherwise, false. - private bool IsMatchExclude(DirectoryInfo directoryInfo) - { - return directoryInfo.FullName - .Split(Path.DirectorySeparatorChar) - .Where(p => !string.IsNullOrEmpty(p)) - .Any(p => _nameMatcher.Match(p).HasMatches) - || ExcludePaths - .Any(p => directoryInfo.FullName - .Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) - .Contains(p) - ); - } - - public ParallelQuery Search(CancellationToken cancellationToken) + public override ParallelQuery Search(CancellationToken cancellationToken) { return RootDirectories .AsParallel() .WithCancellation(cancellationToken) - .SelectMany(d => Directory.EnumerateDirectories(d, "*", _enumerationOptions)) + .SelectMany(d => Directory.EnumerateDirectories(d, _rootSearchPattern, EnumerationOptions)) .Select(d => new DirectoryInfo(d)) - .Where(d => d.GetDirectories(".git", SearchOption.TopDirectoryOnly).Any()) + .Where(d => d.GetDirectories(_searchPattern, SearchOption.TopDirectoryOnly).Length != 0) .Where(d => !IsMatchExclude(d)); } } diff --git a/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs b/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs index 08a2543..43a5da3 100644 --- a/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs +++ b/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs @@ -1,19 +1,16 @@ -using Microsoft.Extensions.FileSystemGlobbing; -using System.Collections.ObjectModel; - -namespace LocalRepositoryListing.Searcher; +namespace LocalRepositoryListing.Searcher; /// /// Represents a class that searches for local repositories within specified root directories. /// -public class RecursiveRepositorySearcher : ISearcher +/// +/// Initializes a new instance of the class. +/// +/// The root directories to search in. +/// The paths to exclude from the search. +/// The names to exclude from the search. +public class RecursiveRepositorySearcher(IList rootDirectories, IList excludePaths, IList excludeNames) : DirectorySearcherBase(rootDirectories, excludePaths, excludeNames), ISearcher { - private static readonly string _rootSearchPattern = "*"; - private static readonly string _searchPattern = ".git"; - public IReadOnlyCollection RootDirectories { get; init; } - public IReadOnlyCollection ExcludePaths { get; init; } - public IReadOnlyCollection ExcludeNames { get; init; } - private static readonly EnumerationOptions _rootEnumerationOptions = new() { RecurseSubdirectories = false, @@ -27,7 +24,7 @@ public class RecursiveRepositorySearcher : ISearcher | FileAttributes.ReparsePoint, }; - private static readonly EnumerationOptions _enumerationOptions = new() + protected override EnumerationOptions EnumerationOptions { get; } = new() { RecurseSubdirectories = true, IgnoreInaccessible = true, @@ -40,52 +37,18 @@ public class RecursiveRepositorySearcher : ISearcher | FileAttributes.ReparsePoint, }; - private readonly Matcher _nameMatcher = new(); - - /// - /// Determines if the specified directory matches the exclusion criteria. - /// - /// The object representing the directory to check. - /// true if the directory matches the exclusion criteria; otherwise, false. - private bool IsMatchExclude(DirectoryInfo directoryInfo) - { - return directoryInfo.FullName - .Split(Path.DirectorySeparatorChar) - .Where(p => !string.IsNullOrEmpty(p)) - .Any(p => _nameMatcher.Match(p).HasMatches) - || ExcludePaths - .Any(p => directoryInfo.FullName - .Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) - .Contains(p) - ); - } - /// /// Initializes a new instance of the RepositorySearcher class with the specified root directories. /// /// An array of root directories to search for repositories. public RecursiveRepositorySearcher(IList rootDirectories) : this(rootDirectories, [], []) { } - /// - /// Initializes a new instance of the class. - /// - /// The root directories to search in. - /// The paths to exclude from the search. - /// The names to exclude from the search. - public RecursiveRepositorySearcher(IList rootDirectories, IList excludePaths, IList excludeNames) - { - RootDirectories = rootDirectories.AsReadOnly(); - ExcludePaths = excludePaths.AsReadOnly(); - ExcludeNames = excludeNames.AsReadOnly(); - _nameMatcher.AddIncludePatterns(excludeNames); - } - /// /// Initializes a new instance of the RepositorySearcher class with the logical drives as the root directories. /// public RecursiveRepositorySearcher() : this(Environment.GetLogicalDrives()) { } - public ParallelQuery Search(CancellationToken cancellationToken) + public override ParallelQuery Search(CancellationToken cancellationToken) { return RootDirectories .AsParallel() @@ -94,7 +57,7 @@ public ParallelQuery Search(CancellationToken cancellationToken) .AsParallel() // Somehow faster with this additional AsParallel() .WithCancellation(cancellationToken) .Where(d => !IsMatchExclude(new DirectoryInfo(d))) - .SelectMany(d => Directory.EnumerateDirectories(d, _searchPattern, _enumerationOptions)) + .SelectMany(d => Directory.EnumerateDirectories(d, _searchPattern, EnumerationOptions)) .Select(d => Directory.GetParent(d)) .Where(d => d != null) .Select(d => d ?? throw new InvalidOperationException("Directory.GetParent returns null")) From 878ff68fa3ae5b119fa8b7361950f36f0d67ac69 Mon Sep 17 00:00:00 2001 From: Gs-itisitcat <66198929+Gs-itisitcat@users.noreply.github.com> Date: Thu, 23 May 2024 00:29:17 +0900 Subject: [PATCH 2/3] Use R3 for handling search result --- .../ListLocalReposCommand.cs | 14 ++-- .../ResultLister/ConsoleOutputLister.cs | 58 +++++++++++++ .../FZFLister.cs} | 13 ++- .../ResultLister/FuzzyFinderListerBase.cs | 72 ++++++++++++++++ .../ResultLister/IResultLister.cs | 11 +++ .../ResultProcessor/ConsoleOutputProcessor.cs | 60 -------------- .../FuzzyFinderProcessorBase.cs | 82 ------------------- .../ResultProcessor/ISearchResultProcessor.cs | 12 --- .../Searcher/DirectorySearcherBase.cs | 18 +++- .../Searcher/ISearcher.cs | 13 ++- .../NonRecursiveRepositorySearcher.cs | 25 ++++-- .../Searcher/RecursiveRepositorySearcher.cs | 30 ++++--- .../local-repository-listing.csproj | 3 +- 13 files changed, 218 insertions(+), 193 deletions(-) create mode 100644 local-repository-listing/ResultLister/ConsoleOutputLister.cs rename local-repository-listing/{ResultProcessor/FZFProcessor.cs => ResultLister/FZFLister.cs} (67%) create mode 100644 local-repository-listing/ResultLister/FuzzyFinderListerBase.cs create mode 100644 local-repository-listing/ResultLister/IResultLister.cs delete mode 100644 local-repository-listing/ResultProcessor/ConsoleOutputProcessor.cs delete mode 100644 local-repository-listing/ResultProcessor/FuzzyFinderProcessorBase.cs delete mode 100644 local-repository-listing/ResultProcessor/ISearchResultProcessor.cs diff --git a/local-repository-listing/ListLocalReposCommand.cs b/local-repository-listing/ListLocalReposCommand.cs index 0ca16e0..31fab28 100644 --- a/local-repository-listing/ListLocalReposCommand.cs +++ b/local-repository-listing/ListLocalReposCommand.cs @@ -35,7 +35,7 @@ You can use glob patterns. /// Arguments to pass to the fuzzy finder process. /// The result of the command execution. [RootCommand] - public int Lepol( + public async ValueTask Lepol( [Option(0, ArgumentDescription)] string arg = "", [Option("r", RootDescription)] string root = "", [Option("l", ListOnlyDescription)] bool listOnly = false, @@ -52,15 +52,11 @@ public int Lepol( ? new NonRecursiveRepositorySearcher(rootDirectories, excludePaths ?? [], excludeNames ?? []) : new RecursiveRepositorySearcher(rootDirectories, excludePaths ?? [], excludeNames ?? []); - ISearchResultProcessor processor = listOnly - ? new ConsoleOutputProcessor(arg) - : new FZFProcessor(arg, fuzzyFinderArgs ?? []); + IResultLister listable = listOnly + ? new ConsoleOutputLister(searcher, arg) + : new FZFLister(searcher, arg, fuzzyFinderArgs ?? []); - // Pass the cancellation token source of search cancellation token to the fuzzy finder process - // to cancel the search when the fuzzy finder process is terminated. - var cts = new CancellationTokenSource(); - Context.CancellationToken.Register(cts.Cancel); - return processor.ProcessSearchResult(searcher.Search(cts.Token), cts); + return await listable.ExecuteListing(Context.CancellationToken); } } diff --git a/local-repository-listing/ResultLister/ConsoleOutputLister.cs b/local-repository-listing/ResultLister/ConsoleOutputLister.cs new file mode 100644 index 0000000..69a6985 --- /dev/null +++ b/local-repository-listing/ResultLister/ConsoleOutputLister.cs @@ -0,0 +1,58 @@ +using LocalRepositoryListing.Searcher; +using R3; +namespace LocalRepositoryListing.ResultProcessor; + +/// +/// Initializes a new instance of the class with the specified search pattern. +/// +/// The object representing the searcher. +/// The search pattern to match against the full names of the directories. +public class ConsoleOutputLister(ISearcher searcher, string searchPattern) : IResultLister +{ + /// + /// The search pattern to match against the full names of the directories. + /// + private readonly string _searchPattern = searchPattern; + private readonly ISearcher _searcher = searcher; + + public async ValueTask ExecuteListing(CancellationToken cancellationToken) + { + using var searchSubscription = _searcher.SearchResults.Subscribe(d => + { + var fullName = d.FullName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + if (string.IsNullOrEmpty(fullName) || (!string.IsNullOrEmpty(_searchPattern) && !fullName.Contains(_searchPattern))) + { + return; + } + + Console.WriteLine(fullName); + }); + + cancellationToken.Register(searchSubscription.Dispose); + + try + { + var searchTask = _searcher.Search(cancellationToken); + + while ( + !cancellationToken.IsCancellationRequested + && !searchTask.IsCompleted + && !searchTask.IsFaulted + && !searchTask.IsCanceled + ) + { + await Task.Delay(100, cancellationToken); + } + } + catch (OperationCanceledException) + { + searchSubscription.Dispose(); + Console.Error.WriteLine("Search was cancelled."); + return 1; + } + searchSubscription.Dispose(); + + return 0; + } +} diff --git a/local-repository-listing/ResultProcessor/FZFProcessor.cs b/local-repository-listing/ResultLister/FZFLister.cs similarity index 67% rename from local-repository-listing/ResultProcessor/FZFProcessor.cs rename to local-repository-listing/ResultLister/FZFLister.cs index 3a1887e..357c5a4 100644 --- a/local-repository-listing/ResultProcessor/FZFProcessor.cs +++ b/local-repository-listing/ResultLister/FZFLister.cs @@ -1,6 +1,13 @@ -namespace LocalRepositoryListing.ResultProcessor; +using LocalRepositoryListing.Searcher; -public class FZFProcessor(string? searchPattern, string[] args) : FuzzyFinderProcessorBase(arguments: [ +namespace LocalRepositoryListing.ResultProcessor; + +/// +/// Represents the processor for the FZF fuzzy finder. +/// +/// +/// +public class FZFLister(ISearcher searcher, string? searchPattern, string[] args) : FuzzyFinderListerBase(searcher, arguments: [ "--ansi", "--header", "\"Select a git repository\"", @@ -18,7 +25,7 @@ public class FZFProcessor(string? searchPattern, string[] args) : FuzzyFinderPro "--query", $"{(string.IsNullOrWhiteSpace(searchPattern) ? "\"\"" : searchPattern)}", ..args - ]) + ]), IResultLister { private static readonly string _fuzzyFinderName = "fzf"; public override string FuzzyFinderName => _fuzzyFinderName; diff --git a/local-repository-listing/ResultLister/FuzzyFinderListerBase.cs b/local-repository-listing/ResultLister/FuzzyFinderListerBase.cs new file mode 100644 index 0000000..dc1774a --- /dev/null +++ b/local-repository-listing/ResultLister/FuzzyFinderListerBase.cs @@ -0,0 +1,72 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using LocalRepositoryListing.Searcher; +using R3; + +namespace LocalRepositoryListing.ResultProcessor; + +/// +/// Represents the base class for a fuzzy finder process. +/// +public abstract class FuzzyFinderListerBase : IResultLister +{ + /// + /// Gets the name of the fuzzy finder. + /// + public abstract string FuzzyFinderName { get; } + + /// + /// Gets the arguments for the processor. + /// + public ReadOnlyCollection Arguments => _arguments.AsReadOnly(); + private readonly string[] _arguments = []; + + private readonly ProcessStartInfo _processStartInfo; + private readonly ISearcher _searcher; + + /// + /// Initializes a new instance of the class. + /// + /// The object representing the searcher. + /// The arguments for the processor. + public FuzzyFinderListerBase(ISearcher searcher, string[] arguments) + { + _arguments = arguments; + _processStartInfo = new ProcessStartInfo(FuzzyFinderName) + { + // UseShellExecute is set to false to start the child process without using a shell + UseShellExecute = false, + RedirectStandardInput = true, + // For Non-ASCII characters + StandardInputEncoding = System.Text.Encoding.UTF8, + Arguments = string.Join(" ", arguments), + }; + + _searcher = searcher; + } + + public ValueTask ExecuteListing(CancellationToken cancellationToken) + { + using var process = Process.Start(_processStartInfo); + if (process == null) + { + Console.Error.WriteLine($"Failed to start {FuzzyFinderName}"); + return ValueTask.FromResult(1); + } + + using var input = TextWriter.Synchronized(process.StandardInput); + if (input == null) + { + Console.Error.WriteLine($"Failed to get StandardInput of {FuzzyFinderName}"); + return ValueTask.FromResult(1); + } + + using var searchSubscription = _searcher.SearchResults.Subscribe(d => input.WriteLine(d.FullName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + + _ = _searcher.Search(cancellationToken); + + process.WaitForExit(); + + return ValueTask.FromResult(process.ExitCode); + } +} diff --git a/local-repository-listing/ResultLister/IResultLister.cs b/local-repository-listing/ResultLister/IResultLister.cs new file mode 100644 index 0000000..68e1fe4 --- /dev/null +++ b/local-repository-listing/ResultLister/IResultLister.cs @@ -0,0 +1,11 @@ +namespace LocalRepositoryListing.ResultProcessor; + +public interface IResultLister +{ + /// + /// Executes the listing operation. + /// + /// The cancellation token. + /// A representing the asynchronous operation, yielding the result of the listing operation. + public ValueTask ExecuteListing(CancellationToken cancellationToken); +} diff --git a/local-repository-listing/ResultProcessor/ConsoleOutputProcessor.cs b/local-repository-listing/ResultProcessor/ConsoleOutputProcessor.cs deleted file mode 100644 index c2ad94e..0000000 --- a/local-repository-listing/ResultProcessor/ConsoleOutputProcessor.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace LocalRepositoryListing.ResultProcessor; - -/// -/// Initializes a new instance of the class with the specified search pattern. -/// -/// The search pattern to match against the full names of the directories. -public class ConsoleOutputProcessor(string searchPattern) : ISearchResultProcessor -{ - /// - /// The search pattern to match against the full names of the directories. - /// - private readonly string _searchPattern = searchPattern; - - - /// - /// Processes the search result by printing the full names of the directories to the console. - /// - /// The search result containing the directories to be processed. - /// The cancellation token source used to cancel the search. - /// 0 if the search result was processed successfully, 1 if the search was cancelled. - public int ProcessSearchResult(ParallelQuery searchResult, CancellationTokenSource searchCancellationTokenSource) - { - try - { - var searchTask = Task.Run(() => - searchResult - .Select(d => d.FullName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) - .Where(d => d.Contains(_searchPattern, StringComparison.OrdinalIgnoreCase)) - .ForAll(d => - Console.WriteLine(d) - ), searchCancellationTokenSource.Token - ); - - // For immediate cancellation. - // This is necessary because the searchTask is not cancelled immediately - // when the searchCancellationTokenSource is cancelled by pressing Ctrl+C. - while ( - !searchCancellationTokenSource.Token.IsCancellationRequested - && !searchTask.IsCompleted - && !searchTask.IsFaulted - && !searchTask.IsCanceled - ) - { - Task.Delay(100).Wait(); - } - } - catch (OperationCanceledException) - { - Console.Error.WriteLine("Search was cancelled"); - return 1; - } - - if (searchCancellationTokenSource.Token.IsCancellationRequested) - { - Console.Error.WriteLine("Search was cancelled"); - return 1; - } - return 0; - } -} diff --git a/local-repository-listing/ResultProcessor/FuzzyFinderProcessorBase.cs b/local-repository-listing/ResultProcessor/FuzzyFinderProcessorBase.cs deleted file mode 100644 index 4066139..0000000 --- a/local-repository-listing/ResultProcessor/FuzzyFinderProcessorBase.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections.ObjectModel; -using System.Diagnostics; - -namespace LocalRepositoryListing.ResultProcessor; - -/// -/// Represents the base class for a fuzzy finder process. -/// -public abstract class FuzzyFinderProcessorBase : ISearchResultProcessor -{ - /// - /// Gets the name of the fuzzy finder. - /// - public abstract string FuzzyFinderName { get; } - - /// - /// Gets the arguments for the processor. - /// - public ReadOnlyCollection Arguments => _arguments.AsReadOnly(); - private readonly string[] _arguments = []; - - private ProcessStartInfo _processStartInfo; - - /// - /// Initializes a new instance of the class. - /// - public FuzzyFinderProcessorBase(string[] arguments) - { - _arguments = arguments; - _processStartInfo = new ProcessStartInfo(FuzzyFinderName) - { - // UseShellExecute is set to false to start the child process without using a shell - UseShellExecute = false, - RedirectStandardInput = true, - // For Non-ASCII characters - StandardInputEncoding = System.Text.Encoding.UTF8, - Arguments = string.Join(" ", arguments), - }; - } - - /// - /// Processes the search result by starting the fuzzy finder process, writing the found repositories to its standard input, - /// and waiting for the process to exit. If selection or cancellation occurs in fuzzy finder, the repository search is interrupted. - /// - /// The parallel query of DirectoryInfo objects representing the searched repositories. - /// The CancellationTokenSource used to cancel the repository search. - /// The exit code of the fuzzy finder process. - public int ProcessSearchResult(ParallelQuery searched, CancellationTokenSource searchCancellationTokenSource) - { - using var process = Process.Start(_processStartInfo); - if (process == null) - { - Console.Error.WriteLine($"Failed to start {FuzzyFinderName}"); - return 1; - } - - // Get the standard input of the redirected fuzzy finder process - // For thread safety, use a synchronized wrapper around the StandardInput stream - using var input = TextWriter.Synchronized(process.StandardInput); - - if (input == null) - { - Console.Error.WriteLine($"Failed to get StandardInput of {FuzzyFinderName}"); - return 1; - } - - // Write the found repositories to the standard input while fuzzy finder is running - _ = Task.Run(() => - searched - .Select(d => d.FullName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) - .ForAll(d => - input.WriteLine(d) - ), searchCancellationTokenSource.Token); - - // WaitForExitAsync() causes OperationCanceledException and prevents fuzzy finder from starting for some reason - process.WaitForExit(); - // If selection or cancellation occurs in fuzzy finder, interrupt the repository search even if it is not finished - searchCancellationTokenSource.Cancel(); - - return process.ExitCode; - } -} diff --git a/local-repository-listing/ResultProcessor/ISearchResultProcessor.cs b/local-repository-listing/ResultProcessor/ISearchResultProcessor.cs deleted file mode 100644 index 1a203b8..0000000 --- a/local-repository-listing/ResultProcessor/ISearchResultProcessor.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace LocalRepositoryListing.ResultProcessor; - -public interface ISearchResultProcessor -{ - /// - /// Processes the search result. - /// - /// The search result. - /// The cancellation token source for the search operation. - /// Exit code of the search result processor. - public int ProcessSearchResult(ParallelQuery searchResult, CancellationTokenSource searchCancellationTokenSource); -} diff --git a/local-repository-listing/Searcher/DirectorySearcherBase.cs b/local-repository-listing/Searcher/DirectorySearcherBase.cs index d7152b4..895d958 100644 --- a/local-repository-listing/Searcher/DirectorySearcherBase.cs +++ b/local-repository-listing/Searcher/DirectorySearcherBase.cs @@ -1,12 +1,17 @@  using Microsoft.Extensions.FileSystemGlobbing; +using R3; namespace LocalRepositoryListing.Searcher; public abstract class DirectorySearcherBase : ISearcher { + protected readonly Subject _searchResults = new(); protected static readonly string _rootSearchPattern = "*"; protected static readonly string _searchPattern = ".git"; + + public Observable SearchResults { get; } + /// /// Gets the root directories to search in. /// @@ -24,14 +29,25 @@ public abstract class DirectorySearcherBase : ISearcher private readonly Matcher _nameMatcher = new(); + /// + /// Initializes a new instance of the class with the specified root directories, paths to exclude, and names to exclude. + /// + /// The root directories to search in. + /// The repository paths to exclude from the search. + /// The repository names to exclude from the search. public DirectorySearcherBase(IList rootDirectories, IList excludePaths, IList excludeNames) { RootDirectories = rootDirectories.AsReadOnly(); ExcludePaths = excludePaths.AsReadOnly(); ExcludeNames = excludeNames.AsReadOnly(); _nameMatcher.AddIncludePatterns(excludeNames); + SearchResults = _searchResults; } + + /// + /// Gets the enumeration options for the search. + /// protected abstract EnumerationOptions EnumerationOptions { get; } /// @@ -52,5 +68,5 @@ protected bool IsMatchExclude(DirectoryInfo directoryInfo) ); } - public abstract ParallelQuery Search(CancellationToken cancellationToken); + public abstract Task Search(CancellationToken cancellationToken); } diff --git a/local-repository-listing/Searcher/ISearcher.cs b/local-repository-listing/Searcher/ISearcher.cs index 8befc92..003fdc4 100644 --- a/local-repository-listing/Searcher/ISearcher.cs +++ b/local-repository-listing/Searcher/ISearcher.cs @@ -1,9 +1,16 @@ -namespace LocalRepositoryListing.Searcher; +using R3; +namespace LocalRepositoryListing.Searcher; /// /// Represents a searcher that can search for repositories based on specified criteria. /// public interface ISearcher { + + /// + /// Gets the observable collection of search results. + /// + public Observable SearchResults { get; } + /// /// Gets the directory names to exclude from the search. /// @@ -13,6 +20,6 @@ public interface ISearcher /// Searches for repositories based on the specified criteria. /// /// A cancellation token to cancel the search operation. - /// A parallel query of objects representing the search results. - public ParallelQuery Search(CancellationToken cancellationToken); + /// A task representing the asynchronous operation. + public Task Search(CancellationToken cancellationToken); } diff --git a/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs b/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs index c3e2ef3..ba69824 100644 --- a/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs +++ b/local-repository-listing/Searcher/NonRecursiveRepositorySearcher.cs @@ -1,4 +1,6 @@ -namespace LocalRepositoryListing.Searcher; +using R3; + +namespace LocalRepositoryListing.Searcher; /// /// Represents a class that searches for local repositories within specified root directories. @@ -30,14 +32,19 @@ public class NonRecursiveRepositorySearcher(IList rootDirectories, IList /// An array of root directories to search for repositories. public NonRecursiveRepositorySearcher(IList rootDirectories) : this(rootDirectories, [], []) { } - public override ParallelQuery Search(CancellationToken cancellationToken) + public override Task Search(CancellationToken cancellationToken) { - return RootDirectories - .AsParallel() - .WithCancellation(cancellationToken) - .SelectMany(d => Directory.EnumerateDirectories(d, _rootSearchPattern, EnumerationOptions)) - .Select(d => new DirectoryInfo(d)) - .Where(d => d.GetDirectories(_searchPattern, SearchOption.TopDirectoryOnly).Length != 0) - .Where(d => !IsMatchExclude(d)); + // Somehow cancelled or exited faster by wrapping in Task.Run + return Task.Run(() => + RootDirectories + .AsParallel() + .WithCancellation(cancellationToken) + .SelectMany(d => Directory.EnumerateDirectories(d, _rootSearchPattern, EnumerationOptions)) + .Select(d => new DirectoryInfo(d)) + .Where(d => d.GetDirectories(_searchPattern, SearchOption.TopDirectoryOnly).Length != 0) + .Where(d => !IsMatchExclude(d)) + .ForAll(_searchResults.OnNext) + , cancellationToken); + } } diff --git a/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs b/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs index 43a5da3..8e80578 100644 --- a/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs +++ b/local-repository-listing/Searcher/RecursiveRepositorySearcher.cs @@ -48,19 +48,23 @@ public RecursiveRepositorySearcher(IList rootDirectories) : this(rootDir /// public RecursiveRepositorySearcher() : this(Environment.GetLogicalDrives()) { } - public override ParallelQuery Search(CancellationToken cancellationToken) + public override Task Search(CancellationToken cancellationToken) { - return RootDirectories - .AsParallel() - .WithCancellation(cancellationToken) - .SelectMany(d => Directory.EnumerateDirectories(d, _rootSearchPattern, _rootEnumerationOptions)) - .AsParallel() // Somehow faster with this additional AsParallel() - .WithCancellation(cancellationToken) - .Where(d => !IsMatchExclude(new DirectoryInfo(d))) - .SelectMany(d => Directory.EnumerateDirectories(d, _searchPattern, EnumerationOptions)) - .Select(d => Directory.GetParent(d)) - .Where(d => d != null) - .Select(d => d ?? throw new InvalidOperationException("Directory.GetParent returns null")) - .Where(d => !IsMatchExclude(d)); + // Somehow Cancelled faster by wrapping in Task.Run + return Task.Run(() => + RootDirectories + .AsParallel() + .WithCancellation(cancellationToken) + .SelectMany(d => Directory.EnumerateDirectories(d, _rootSearchPattern, _rootEnumerationOptions)) + .AsParallel() // Somehow faster with this additional AsParallel() + .WithCancellation(cancellationToken) + .Where(d => !IsMatchExclude(new DirectoryInfo(d))) + .SelectMany(d => Directory.EnumerateDirectories(d, _searchPattern, EnumerationOptions)) + .Select(d => Directory.GetParent(d)) + .Where(d => d != null) + .Select(d => d ?? throw new InvalidOperationException("Directory.GetParent returns null")) + .Where(d => !IsMatchExclude(d)) + .ForAll(_searchResults.OnNext) + , cancellationToken); } } diff --git a/local-repository-listing/local-repository-listing.csproj b/local-repository-listing/local-repository-listing.csproj index 5f1362a..242d15d 100644 --- a/local-repository-listing/local-repository-listing.csproj +++ b/local-repository-listing/local-repository-listing.csproj @@ -9,13 +9,14 @@ lepol 1.0.1 true - true + true embedded + From d23a7e419e30ea828e63cfb4dd1c04a2a48702b1 Mon Sep 17 00:00:00 2001 From: Gs-itisitcat <66198929+Gs-itisitcat@users.noreply.github.com> Date: Thu, 23 May 2024 00:30:15 +0900 Subject: [PATCH 3/3] Update version to 1.1.0 --- local-repository-listing/local-repository-listing.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-repository-listing/local-repository-listing.csproj b/local-repository-listing/local-repository-listing.csproj index 242d15d..4e1ca3f 100644 --- a/local-repository-listing/local-repository-listing.csproj +++ b/local-repository-listing/local-repository-listing.csproj @@ -7,7 +7,7 @@ enable enable lepol - 1.0.1 + 1.1.0 true true embedded