Search WIP

This commit is contained in:
2023-02-27 08:41:19 +01:00
parent 64d7634b1b
commit a01c3a69b4
16 changed files with 128 additions and 100 deletions

View File

@@ -4,6 +4,7 @@ using FileTime.App.Core.ViewModels;
using FileTime.App.Search;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.App.Core.Services.UserCommandHandler;
@@ -13,6 +14,8 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
private readonly IUserCommunicationService _userCommunicationService;
private readonly ISearchManager _searchManager;
private readonly IItemNameConverterService _itemNameConverterService;
private readonly ITimelessContentProvider _timelessContentProvider;
private readonly IUserCommandHandlerService _userCommandHandlerService;
private IContainer? _currentLocation;
private IItemViewModel? _currentSelectedItem;
@@ -21,12 +24,16 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
ISystemClipboardService systemClipboardService,
IUserCommunicationService userCommunicationService,
ISearchManager searchManager,
IItemNameConverterService itemNameConverterService) : base(appState)
IItemNameConverterService itemNameConverterService,
ITimelessContentProvider timelessContentProvider,
IUserCommandHandlerService userCommandHandlerService) : base(appState)
{
_systemClipboardService = systemClipboardService;
_userCommunicationService = userCommunicationService;
_searchManager = searchManager;
_itemNameConverterService = itemNameConverterService;
_timelessContentProvider = timelessContentProvider;
_userCommandHandlerService = userCommandHandlerService;
SaveCurrentLocation(l => _currentLocation = l);
SaveCurrentSelectedItem(i => _currentSelectedItem = i);
@@ -40,8 +47,8 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
private async Task Search(SearchCommand searchCommand)
{
if(_currentLocation is null) return;
if (_currentLocation is null) return;
var searchQuery = searchCommand.SearchText;
if (string.IsNullOrEmpty(searchQuery))
{
@@ -59,10 +66,10 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
searchQuery = containerNameInput.Value;
}
}
//TODO proper error message
if(string.IsNullOrWhiteSpace(searchQuery)) return;
if (string.IsNullOrWhiteSpace(searchQuery)) return;
var searchMatcher = searchCommand.SearchType switch
{
SearchType.NameContains => new NameContainsMatcher(_itemNameConverterService, searchQuery),
@@ -70,7 +77,9 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase
_ => throw new ArgumentOutOfRangeException()
};
await _searchManager.StartSearchAsync(searchMatcher, _currentLocation);
var searchTask = await _searchManager.StartSearchAsync(searchMatcher, _currentLocation);
var openContainerCommand = new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, searchTask.SearchContainer));
await _userCommandHandlerService.HandleCommandAsync(openContainerCommand);
}
private async Task CopyNativePath()

View File

@@ -134,7 +134,6 @@ public partial class TabViewModel : ITabViewModel
.OfType<IContainerViewModel>()
.Where(c => c?.Container is not null)
.Select(c => c.Container!.Items)
.Switch()
.Select(i =>
i
?.TransformAsync(MapItem)
@@ -165,7 +164,6 @@ public partial class TabViewModel : ITabViewModel
.Select(p => Observable.FromAsync(async () => (IContainer)await p!.ResolveAsync()))
.Switch()
.Select(p => p.Items)
.Switch()
.Select(items =>
items
?.TransformAsync(MapItem)

View File

@@ -4,5 +4,6 @@ namespace FileTime.App.Search;
public interface ISearchManager
{
Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn);
Task<ISearchTask> StartSearchAsync(ISearchMatcher matcher, IContainer searchIn);
IReadOnlyList<ISearchTask> SearchTasks { get; }
}

View File

@@ -0,0 +1,9 @@
using FileTime.Core.Models;
namespace FileTime.App.Search;
public interface ISearchTask
{
IContainer SearchContainer { get; }
Task StartAsync();
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Core\FileTime.Core.ContentAccess\FileTime.Core.ContentAccess.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" />

View File

@@ -1,4 +1,3 @@
using DynamicData;
using FileTime.Core.ContentAccess;
using FileTime.Core.Enums;
using FileTime.Core.Models;
@@ -6,39 +5,34 @@ using FileTime.Core.Timeline;
namespace FileTime.App.Search;
public class SearchContentProvider : ISearchContentProvider
public class SearchContentProvider : ContentProviderBase, 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();
private readonly ISearchManager _searchManager;
public const string ContentProviderName = "search";
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 SearchContentProvider(ISearchManager searchManager) : base(ContentProviderName)
{
_searchManager = searchManager;
}
public Task<IItem> GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) => throw new NotImplementedException();
public override Task<IItem> GetItemByNativePathAsync(
NativePath nativePath,
PointInTime pointInTime,
bool forceResolve = false,
AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown,
ItemInitializationSettings itemInitializationSettings = default
) =>
Task.FromResult((IItem)_searchManager.SearchTasks
.First(searchTask => searchTask.SearchContainer.NativePath == nativePath).SearchContainer);
public NativePath GetNativePath(FullName fullName) => throw new NotImplementedException();
public override NativePath GetNativePath(FullName fullName) => new(fullName.Path);
public Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public override Task<byte[]?> GetContentAsync(
IElement element,
int? maxLength = null,
CancellationToken cancellationToken = default
)
=> Task.FromResult(null as byte[]);
public bool CanHandlePath(NativePath path) => throw new NotImplementedException();
public bool CanHandlePath(FullName path) => throw new NotImplementedException();
public override bool CanHandlePath(NativePath path) => path.Path.StartsWith(ContentProviderName);
}

View File

@@ -1,19 +1,25 @@
using FileTime.Core.Models;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.App.Search;
public class SearchManager : ISearchManager
{
private readonly ISearchContentProvider _searchContainerProvider;
private readonly IServiceProvider _serviceProvider;
private ISearchContentProvider? _searchContainerProvider;
private readonly List<SearchTask> _searchTasks = new();
public IReadOnlyList<ISearchTask> SearchTasks { get; }
public SearchManager(ISearchContentProvider searchContainerProvider)
public SearchManager(IServiceProvider serviceProvider)
{
_searchContainerProvider = searchContainerProvider;
_serviceProvider = serviceProvider;
SearchTasks = _searchTasks.AsReadOnly();
}
public async Task StartSearchAsync(ISearchMatcher matcher, IContainer searchIn)
public async Task<ISearchTask> StartSearchAsync(ISearchMatcher matcher, IContainer searchIn)
{
_searchContainerProvider ??= _serviceProvider.GetRequiredService<ISearchContentProvider>();
var searchTask = new SearchTask(
searchIn,
_searchContainerProvider,
@@ -23,5 +29,7 @@ public class SearchManager : ISearchManager
_searchTasks.Add(searchTask);
await searchTask.StartAsync();
return searchTask;
}
}

View File

@@ -8,7 +8,7 @@ using FileTime.Core.Timeline;
namespace FileTime.App.Search;
public class SearchTask
public class SearchTask : ISearchTask
{
private readonly IContainer _baseContainer;
private readonly ISearchMatcher _matcher;
@@ -17,6 +17,9 @@ public class SearchTask
private readonly SourceCache<AbsolutePath, string> _items = new(p => p.Path.Path);
private readonly SemaphoreSlim _searchingLock = new(1, 1);
private bool _isSearching;
private static int _searchId = 1;
public IContainer SearchContainer => _container;
public SearchTask(
IContainer baseContainer,
@@ -24,13 +27,14 @@ public class SearchTask
ISearchMatcher matcher
)
{
var randomId = $"{SearchContentProvider.ContentProviderName}/{_searchId++}_{baseContainer.Name}";
_baseContainer = baseContainer;
_matcher = matcher;
_container = new Container(
baseContainer.Name,
baseContainer.DisplayName,
new FullName(""),
new NativePath(""),
new FullName(randomId),
new NativePath(randomId),
null,
false,
true,
@@ -43,7 +47,7 @@ public class SearchTask
PointInTime.Present,
_exceptions.Connect(),
new ReadOnlyExtensionCollection(new ExtensionCollection()),
Observable.Return(_items.Connect())
_items.Connect().StartWithEmpty()
);
}

View File

@@ -1,4 +1,6 @@
using FileTime.Core.ContentAccess;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace FileTime.App.Search;
@@ -6,8 +8,9 @@ public static class Startup
{
public static IServiceCollection AddSearch(this IServiceCollection services)
{
services.AddSingleton<ISearchContentProvider, SearchContentProvider>();
services.AddSingleton<ISearchManager, SearchManager>();
services.TryAddSingleton<ISearchContentProvider, SearchContentProvider>();
services.AddSingleton<IContentProvider>(sp => sp.GetRequiredService<ISearchContentProvider>());
services.TryAddSingleton<ISearchManager, SearchManager>();
return services;
}

View File

@@ -8,8 +8,10 @@ public static class DynamicDataExtensions
{
private class DisposableContext<TParam, TTaskResult>
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly Func<TParam, TTaskResult> _transformResult;
private readonly TaskCompletionSource<TTaskResult?> _taskCompletionSource;
private bool _isFinished;
public IDisposable? Disposable { get; set; }
public DisposableContext(Func<TParam, TTaskResult> transformResult,
@@ -22,6 +24,7 @@ public static class DynamicDataExtensions
public void OnNext(TParam param)
{
if (IsFinished()) return;
Disposable?.Dispose();
var result = _transformResult(param);
_taskCompletionSource.SetResult(result);
@@ -29,15 +32,27 @@ public static class DynamicDataExtensions
public void OnError(Exception ex)
{
if (IsFinished()) return;
Disposable?.Dispose();
_taskCompletionSource.SetException(ex);
}
public void OnCompleted()
{
if (IsFinished()) return;
Disposable?.Dispose();
_taskCompletionSource.SetResult(default);
}
private bool IsFinished()
{
_semaphore.Wait();
var finished = _isFinished;
_isFinished = true;
_semaphore.Release();
return finished;
}
}
public static async Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
@@ -46,12 +61,12 @@ public static class DynamicDataExtensions
.Select(s =>
s is null
? new SourceList<AbsolutePath>().Connect().StartWithEmpty().ToCollection()
: s.ToCollection())
: s.StartWithEmpty().ToCollection())
.Switch());
public static async Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
this IObservable<IChangeSet<AbsolutePath, string>> stream)
=> await GetItemsAsync(stream.ToCollection());
=> await GetItemsAsync(stream.StartWithEmpty().ToCollection());
public static Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
this IObservable<IReadOnlyCollection<AbsolutePath>> stream)

View File

@@ -4,7 +4,7 @@ namespace FileTime.Core.Models;
public interface IContainer : IItem
{
IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> Items { get; }
IObservable<IChangeSet<AbsolutePath, string>> Items { get; }
IObservable<bool> IsLoading { get; }
bool AllowRecursiveDeletion { get; }
}

View File

@@ -10,11 +10,12 @@ namespace FileTime.Core.ContentAccess;
public abstract class ContentProviderBase : IContentProvider
{
private readonly ReadOnlyExtensionCollection _extensions;
private readonly IObservable<IChangeSet<AbsolutePath, string>> _items;
protected BehaviorSubject<IObservable<IChangeSet<AbsolutePath, string>>?> Items { get; } = new(null);
protected SourceCache<AbsolutePath, string> Items { get; } = new(p => p.Path.Path);
protected ExtensionCollection Extensions { get; }
IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> IContainer.Items => Items;
IObservable<IChangeSet<AbsolutePath, string>> IContainer.Items => _items;
public string Name { get; }
@@ -59,6 +60,7 @@ public abstract class ContentProviderBase : IContentProvider
FullName = FullName.CreateSafe(name);
Extensions = new ExtensionCollection();
_extensions = Extensions.AsReadOnly();
_items = Items.Connect().StartWithEmpty();
}
public virtual Task OnEnter() => Task.CompletedTask;

View File

@@ -24,7 +24,7 @@ public record Container(
PointInTime PointInTime,
IObservable<IChangeSet<Exception>> Exceptions,
ReadOnlyExtensionCollection Extensions,
IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> Items) : IContainer
IObservable<IChangeSet<AbsolutePath, string>> Items) : IContainer
{
private readonly CancellationTokenSource _loadingCancellationTokenSource = new();
public CancellationToken LoadingCancellationToken => _loadingCancellationTokenSource.Token;

View File

@@ -52,8 +52,7 @@ public class Tab : ITab
CurrentLocation
.Where(c => c is not null)
.Select(c => c!.Items)
.Switch()
.Select(items => items?.TransformAsync(MapItem)),
.Select(items => items.TransformAsync(MapItem)),
_itemFilters.Connect().StartWithEmpty().ToCollection(),
(items, filters) =>
items

View File

@@ -14,45 +14,35 @@ namespace FileTime.GuiApp.Services;
public class RootDriveInfoService : IStartupHandler
{
private readonly SourceList<DriveInfo> _rootDrives = new();
private readonly List<DriveInfo> _rootDrives = new();
public RootDriveInfoService(
IGuiAppState guiAppState,
ILocalContentProvider localContentProvider,
ITimelessContentProvider timelessContentProvider)
ILocalContentProvider localContentProvider)
{
InitRootDrives();
var localContentProviderAsList = new SourceCache<AbsolutePath, string>(i => i.Path.Path);
localContentProviderAsList.AddOrUpdate(new AbsolutePath(timelessContentProvider, localContentProvider));
var localContentProviderStream = localContentProviderAsList.Connect();
var rootDriveInfos = Observable.CombineLatest(
localContentProvider.Items,
_rootDrives.Connect().StartWithEmpty().ToCollection(),
(items, drives) =>
var rootDriveInfos = localContentProvider.Items.Transform(
i =>
{
var rootDrive = _rootDrives.FirstOrDefault(d =>
{
return items is null
? Observable.Empty<IChangeSet<(AbsolutePath Path, DriveInfo? Drive), string>>()
: items!
.Or(new[] { localContentProviderStream })
.Transform(i => (Path: i, Drive: drives.FirstOrDefault(d =>
{
var containerPath = localContentProvider.GetNativePath(i.Path).Path;
var drivePath = d.Name.TrimEnd(Path.DirectorySeparatorChar);
return containerPath == drivePath
|| (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" &&
d.Name == "/");
})))
.Filter(t => t.Drive is not null);
}
)
.Switch()
.TransformAsync(async t => (Item: await t.Path.ResolveAsyncSafe(), Drive: t.Drive!))
.Filter(t => t.Item is IContainer)
.Transform(t => (Container: (IContainer)t.Item!, t.Drive))
.Transform(t => new RootDriveInfo(t.Drive, t.Container))
.Sort(SortExpressionComparer<RootDriveInfo>.Ascending(d => d.Name));
var containerPath = localContentProvider.GetNativePath(i.Path).Path;
var drivePath = d.Name.TrimEnd(Path.DirectorySeparatorChar);
return containerPath == drivePath
|| (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" &&
d.Name == "/");
});
return (Path: i, Drive: rootDrive);
}
)
.Filter(t => t.Drive is not null)
.TransformAsync(async t => (Item: await t.Path.ResolveAsyncSafe(), Drive: t.Drive!))
.Filter(t => t.Item is IContainer)
.Transform(t => (Container: (IContainer) t.Item!, t.Drive))
.Transform(t => new RootDriveInfo(t.Drive, t.Container))
.Sort(SortExpressionComparer<RootDriveInfo>.Ascending(d => d.Name));
guiAppState.RootDriveInfos = rootDriveInfos.ToBindedCollection();
@@ -68,6 +58,7 @@ public class RootDriveInfoService : IStartupHandler
&& d.DriveFormat != "tracefs"
&& !d.RootDirectory.FullName.StartsWith("/snap/"));
_rootDrives.Clear();
_rootDrives.AddRange(drives);
}
}

View File

@@ -1,5 +1,4 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.InteropServices;
using DynamicData;
using FileTime.Core.ContentAccess;
@@ -13,7 +12,6 @@ namespace FileTime.Providers.Local;
public sealed partial class LocalContentProvider : ContentProviderBase, ILocalContentProvider
{
private readonly ITimelessContentProvider _timelessContentProvider;
private readonly SourceCache<AbsolutePath, string> _rootDirectories = new(i => i.Path.Path);
private readonly bool _isCaseInsensitive;
public LocalContentProvider(ITimelessContentProvider timelessContentProvider) : base("local")
@@ -24,8 +22,6 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
SupportsContentStreams = true;
RefreshRootDirectories();
Items.OnNext(_rootDirectories.Connect());
}
public override Task OnEnter()
@@ -41,7 +37,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
? new DirectoryInfo("/").GetDirectories()
: Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d));
_rootDirectories.Edit(actions =>
Items.Edit(actions =>
{
actions.Clear();
actions.AddOrUpdate(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));
@@ -50,7 +46,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
public override bool CanHandlePath(NativePath path)
{
var rootDrive = _rootDirectories
var rootDrive = Items
.Items
.FirstOrDefault(r =>
path.Path.StartsWith(
@@ -173,7 +169,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
pointInTime,
exceptions.Connect(),
new ExtensionCollection().AsReadOnly(),
Observable.Return<IObservable<IChangeSet<AbsolutePath, string>>?>(null)
new SourceCache<AbsolutePath, string>(a => a.Path.Path).Connect()
);
}
@@ -228,9 +224,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
pointInTime,
exceptions.Connect(),
new ExtensionCollection().AsReadOnly(),
//Observable.FromAsync(async () => await Task.Run(InitChildrenHelper)
//Observable.Return(InitChildren())
Observable.Return(children.Connect())
children.Connect().StartWithEmpty()
);
Task.Run(() => LoadChildren(container, directoryInfo, children, pointInTime, exceptions));