Search WIP

This commit is contained in:
2023-02-26 20:42:25 +01:00
parent 3cd3926ed6
commit 64d7634b1b
22 changed files with 403 additions and 33 deletions

View File

@@ -8,9 +8,9 @@ public sealed class PasteCommand : IIdentifiableUserCommand
public const string PasteOverwriteCommandName = "paste_overwrite";
public const string PasteSkipCommandName = "paste_skip";
public static PasteCommand Merge { get; } = new PasteCommand(PasteMode.Merge, PasteMergeCommandName);
public static PasteCommand Overwrite { get; } = new PasteCommand(PasteMode.Overwrite, PasteOverwriteCommandName);
public static PasteCommand Skip { get; } = new PasteCommand(PasteMode.Skip, PasteSkipCommandName);
public static readonly PasteCommand Merge = new(PasteMode.Merge, PasteMergeCommandName);
public static readonly PasteCommand Overwrite = new(PasteMode.Overwrite, PasteOverwriteCommandName);
public static readonly PasteCommand Skip = new(PasteMode.Skip, PasteSkipCommandName);
public PasteMode PasteMode { get; }

View File

@@ -0,0 +1,38 @@
namespace FileTime.App.Core.UserCommand;
public enum SearchType
{
NameContains,
NameRegex
}
public class SearchCommand : IUserCommand
{
public string? SearchText { get; }
public SearchType SearchType { get; }
public SearchCommand(string? searchText, SearchType searchType)
{
SearchText = searchText;
SearchType = searchType;
}
}
public class IdentifiableSearchCommand : SearchCommand, IIdentifiableUserCommand
{
public const string SearchByNameContainsCommandName = "search_name_contains";
public static readonly IdentifiableSearchCommand SearchByNameContains =
new(null, SearchType.NameContains, SearchByNameContainsCommandName);
public IdentifiableSearchCommand(
string? searchText,
SearchType searchType,
string commandId)
: base(searchText, searchType)
{
UserCommandID = commandId;
}
public string UserCommandID { get; }
}

View File

@@ -26,6 +26,7 @@
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Core\FileTime.Core.Command\FileTime.Core.Command.csproj" />
<ProjectReference Include="..\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
<ProjectReference Include="..\FileTime.App.Search\FileTime.App.Search.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,8 @@
using System.Diagnostics;
using FileTime.App.Core.UserCommand;
using FileTime.App.Core.ViewModels;
using FileTime.App.Search;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
namespace FileTime.App.Core.Services.UserCommandHandler;
@@ -8,12 +10,23 @@ namespace FileTime.App.Core.Services.UserCommandHandler;
public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
{
private readonly ISystemClipboardService _systemClipboardService;
private readonly IUserCommunicationService _userCommunicationService;
private readonly ISearchManager _searchManager;
private readonly IItemNameConverterService _itemNameConverterService;
private IContainer? _currentLocation;
private IItemViewModel? _currentSelectedItem;
public ToolUserCommandHandlerService(IAppState appState, ISystemClipboardService systemClipboardService) : base(appState)
public ToolUserCommandHandlerService(
IAppState appState,
ISystemClipboardService systemClipboardService,
IUserCommunicationService userCommunicationService,
ISearchManager searchManager,
IItemNameConverterService itemNameConverterService) : base(appState)
{
_systemClipboardService = systemClipboardService;
_userCommunicationService = userCommunicationService;
_searchManager = searchManager;
_itemNameConverterService = itemNameConverterService;
SaveCurrentLocation(l => _currentLocation = l);
SaveCurrentSelectedItem(i => _currentSelectedItem = i);
@@ -21,9 +34,45 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
{
new TypeUserCommandHandler<OpenInDefaultFileExplorerCommand>(OpenInDefaultFileExplorer),
new TypeUserCommandHandler<CopyNativePathCommand>(CopyNativePath),
new TypeUserCommandHandler<SearchCommand>(Search),
});
}
private async Task Search(SearchCommand searchCommand)
{
if(_currentLocation is null) return;
var searchQuery = searchCommand.SearchText;
if (string.IsNullOrEmpty(searchQuery))
{
var title = searchCommand.SearchType switch
{
SearchType.NameContains => "Search by Name",
SearchType.NameRegex => "Search by Name (Regex)",
_ => throw new ArgumentOutOfRangeException()
};
var containerNameInput = new TextInputElement(title);
await _userCommunicationService.ReadInputs(containerNameInput);
if (containerNameInput.Value is not null)
{
searchQuery = containerNameInput.Value;
}
}
//TODO proper error message
if(string.IsNullOrWhiteSpace(searchQuery)) return;
var searchMatcher = searchCommand.SearchType switch
{
SearchType.NameContains => new NameContainsMatcher(_itemNameConverterService, searchQuery),
//SearchType.NameRegex => new NameRegexMatcher(searchQuery),
_ => throw new ArgumentOutOfRangeException()
};
await _searchManager.StartSearchAsync(searchMatcher, _currentLocation);
}
private async Task CopyNativePath()
{
if (_currentSelectedItem?.BaseItem?.NativePath is null) return;

View File

@@ -2,7 +2,7 @@ using FileTime.App.Core.UserCommand;
namespace FileTime.App.Core.Services.UserCommandHandler;
public class TypeUserCommandHandler<T> : IUserCommandHandler
public class TypeUserCommandHandler<T> : IUserCommandHandler where T : IUserCommand
{
private readonly Func<T, Task> _handler;

View File

@@ -41,6 +41,7 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
AddUserCommand(PauseCommandSchedulerCommand.Instance);
AddUserCommand(RefreshCommand.Instance);
AddUserCommand(StartCommandSchedulerCommand.Instance);
AddUserCommand(IdentifiableSearchCommand.SearchByNameContains);
AddUserCommand(SwitchToTabCommand.SwitchToLastTab);
AddUserCommand(SwitchToTabCommand.SwitchToTab1);
AddUserCommand(SwitchToTabCommand.SwitchToTab2);

View File

@@ -19,7 +19,7 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
private DateTime _lastSave = DateTime.Now;
private readonly ILogger<FrequencyNavigationService> _logger;
private readonly IModalService _modalService;
private readonly SemaphoreSlim _saveLock = new(1);
private readonly SemaphoreSlim _saveLock = new(1, 1);
private Dictionary<string, ContainerFrequencyData> _containerScores = new();
private readonly BehaviorSubject<bool> _showWindow = new(false);
private readonly string _dbPath;

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FileTime.App.Search</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
using FileTime.Core.ContentAccess;
namespace FileTime.App.Search;
public interface ISearchContentProvider : IContentProvider
{
}

View File

@@ -0,0 +1,8 @@
using FileTime.Core.Models;
namespace FileTime.App.Search;
public interface ISearchManager
{
Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn);
}

View File

@@ -0,0 +1,10 @@
using FileTime.App.Core.Models;
using FileTime.Core.Models;
namespace FileTime.App.Search;
public interface ISearchMatcher
{
Task<bool> IsItemMatchAsync(IItem item);
List<ItemNamePart> GetDisplayName(IItem item);
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" />
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.App.Search.Abstractions\FileTime.App.Search.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
using FileTime.App.Core.Models;
using FileTime.App.Core.Services;
using FileTime.Core.Models;
namespace FileTime.App.Search;
public class NameContainsMatcher : ISearchMatcher
{
private readonly IItemNameConverterService _itemNameConverterService;
private readonly string _searchText;
public NameContainsMatcher(IItemNameConverterService itemNameConverterService, string searchText)
{
_itemNameConverterService = itemNameConverterService;
_searchText = searchText;
}
public Task<bool> IsItemMatchAsync(IItem item) => Task.FromResult(item.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase));
public List<ItemNamePart> GetDisplayName(IItem item) => _itemNameConverterService.GetDisplayName(item.DisplayName, _searchText);
}

View File

@@ -0,0 +1,44 @@
using DynamicData;
using FileTime.Core.ContentAccess;
using FileTime.Core.Enums;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.App.Search;
public class SearchContentProvider : ISearchContentProvider
{
public string Name { get; }
public string DisplayName { get; }
public FullName? FullName { get; }
public NativePath? NativePath { get; }
public AbsolutePath? Parent { get; }
public bool IsHidden { get; }
public bool IsExists { get; }
public DateTime? CreatedAt { get; }
public SupportsDelete CanDelete { get; }
public bool CanRename { get; }
public IContentProvider Provider { get; }
public string? Attributes { get; }
public AbsolutePathType Type { get; }
public PointInTime PointInTime { get; }
public IObservable<IChangeSet<Exception>> Exceptions { get; }
public ReadOnlyExtensionCollection Extensions { get; }
public IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> Items { get; }
public IObservable<bool> IsLoading { get; }
public bool AllowRecursiveDeletion { get; }
public Task OnEnter() => throw new NotImplementedException();
public bool SupportsContentStreams { get; }
public Task<IItem> GetItemByFullNameAsync(FullName fullName, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) => throw new NotImplementedException();
public Task<IItem> GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) => throw new NotImplementedException();
public NativePath GetNativePath(FullName fullName) => throw new NotImplementedException();
public Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public bool CanHandlePath(NativePath path) => throw new NotImplementedException();
public bool CanHandlePath(FullName path) => throw new NotImplementedException();
}

View File

@@ -0,0 +1,27 @@
using FileTime.Core.Models;
namespace FileTime.App.Search;
public class SearchManager : ISearchManager
{
private readonly ISearchContentProvider _searchContainerProvider;
private readonly List<SearchTask> _searchTasks = new();
public SearchManager(ISearchContentProvider searchContainerProvider)
{
_searchContainerProvider = searchContainerProvider;
}
public async Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn)
{
var searchTask = new SearchTask(
searchIn,
_searchContainerProvider,
matcher
);
_searchTasks.Add(searchTask);
await searchTask.StartAsync();
}
}

View File

@@ -0,0 +1,101 @@
using System.Reactive.Linq;
using DynamicData;
using FileTime.Core.ContentAccess;
using FileTime.Core.Enums;
using FileTime.Core.Extensions;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.App.Search;
public class SearchTask
{
private readonly IContainer _baseContainer;
private readonly ISearchMatcher _matcher;
private readonly Container _container;
private readonly SourceList<Exception> _exceptions = new();
private readonly SourceCache<AbsolutePath, string> _items = new(p => p.Path.Path);
private readonly SemaphoreSlim _searchingLock = new(1, 1);
private bool _isSearching;
public SearchTask(
IContainer baseContainer,
IContentProvider contentProvider,
ISearchMatcher matcher
)
{
_baseContainer = baseContainer;
_matcher = matcher;
_container = new Container(
baseContainer.Name,
baseContainer.DisplayName,
new FullName(""),
new NativePath(""),
null,
false,
true,
null,
SupportsDelete.False,
false,
null,
contentProvider,
false,
PointInTime.Present,
_exceptions.Connect(),
new ReadOnlyExtensionCollection(new ExtensionCollection()),
Observable.Return(_items.Connect())
);
}
public async Task StartAsync()
{
await _searchingLock.WaitAsync();
if (_isSearching) return;
_isSearching = true;
_searchingLock.Release();
Task.Run(BootstrapSearch);
async Task BootstrapSearch()
{
try
{
_container.IsLoading.OnNext(true);
await TraverseTree(_baseContainer);
}
finally
{
_container.IsLoading.OnNext(false);
await _searchingLock.WaitAsync();
_isSearching = false;
_searchingLock.Release();
}
}
}
private async Task TraverseTree(IContainer container)
{
var items = (await container.Items.GetItemsAsync())?.ToList();
if (items is null) return;
var childContainers = new List<IContainer>();
foreach (var itemPath in items)
{
var item = await itemPath.ResolveAsync();
if (await _matcher.IsItemMatchAsync(item))
{
_items.AddOrUpdate(itemPath);
}
if (item is IContainer childContainer)
childContainers.Add(childContainer);
}
foreach (var childContainer in childContainers)
{
await TraverseTree(childContainer);
}
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.App.Search;
public static class Startup
{
public static IServiceCollection AddSearch(this IServiceCollection services)
{
services.AddSingleton<ISearchContentProvider, SearchContentProvider>();
services.AddSingleton<ISearchManager, SearchManager>();
return services;
}
}