diff --git a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs index 738732d..8b03d67 100644 --- a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs @@ -1,4 +1,5 @@ -using FileTime.App.Core.ViewModels; +using Avalonia.Input; +using FileTime.App.Core.ViewModels; using FileTime.App.FuzzyPanel; namespace FileTime.App.CommandPalette.ViewModels; @@ -7,4 +8,5 @@ public interface ICommandPaletteViewModel : IFuzzyPanelViewModel ShowWindow { get; } void Close(); + Task HandleKeyUp(KeyEventArgs keyEventArgs); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs index bd04bdb..31af0fb 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs @@ -25,6 +25,7 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel logger) + : base((a, b) => a.Identifier == b.Identifier) { _commandPaletteService = commandPaletteService; _identifiableUserCommandService = identifiableUserCommandService; @@ -105,6 +106,13 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel HandleKeyUp(KeyEventArgs keyEventArgs) + { + if (keyEventArgs.Handled) return false; + if (keyEventArgs.Key == Key.Enter) { if (SelectedItem is null) return false; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Exceptions/ItemNotFoundException.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Exceptions/ItemNotFoundException.cs new file mode 100644 index 0000000..ad650d9 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Exceptions/ItemNotFoundException.cs @@ -0,0 +1,32 @@ +using FileTime.Core.Models; + +namespace FileTime.App.Core.Exceptions; + +public enum ItemNotFoundExceptionType +{ + Raw, + FullName, + NativePath +} +public class ItemNotFoundException : Exception +{ + public string Path { get; } + public ItemNotFoundExceptionType Type { get; } = ItemNotFoundExceptionType.Raw; + + public ItemNotFoundException(string path) + { + Path = path; + } + + public ItemNotFoundException(FullName path) + { + Path = path.Path; + Type = ItemNotFoundExceptionType.FullName; + } + + public ItemNotFoundException(NativePath path) + { + Path = path.Path; + Type = ItemNotFoundExceptionType.NativePath; + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs index 8d04b40..316683b 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs @@ -1,6 +1,7 @@ using System.Reactive.Subjects; using FileTime.App.Core.Models; using FileTime.Core.Interactions; +using FileTime.Core.Models; namespace FileTime.App.Core.Interactions; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IItemNameConverterService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IItemNameConverterService.cs index fac1901..8346706 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IItemNameConverterService.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IItemNameConverterService.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using FileTime.Core.Models; namespace FileTime.App.Core.Services; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/SearchCommand.cs b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/SearchCommand.cs index 1ea18fc..6567fcd 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/SearchCommand.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/SearchCommand.cs @@ -21,10 +21,14 @@ public class SearchCommand : IUserCommand public sealed class IdentifiableSearchCommand : SearchCommand, IIdentifiableUserCommand { public const string SearchByNameContainsCommandName = "search_name_contains"; + public const string SearchByRegexCommandName = "search_name_regex"; public static readonly IdentifiableSearchCommand SearchByNameContains = new(null, SearchType.NameContains, SearchByNameContainsCommandName, "Search by name"); + public static readonly IdentifiableSearchCommand SearchByRegex = + new(null, SearchType.NameRegex, SearchByRegexCommandName, "Search by name (Regex)"); + private IdentifiableSearchCommand( string? searchText, SearchType searchType, diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs index cd4b7a9..82f4030 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs @@ -10,7 +10,7 @@ public interface IAppState ReadOnlyObservableCollection Tabs { get; } IObservable SelectedTab { get; } IObservable SearchText { get; } - IObservable ViewMode { get; } + IDeclarativeProperty ViewMode { get; } DeclarativeProperty RapidTravelText { get; } ITabViewModel? CurrentSelectedTab { get; } ITimelineViewModel TimelineViewModel { get; } @@ -18,6 +18,6 @@ public interface IAppState void AddTab(ITabViewModel tabViewModel); void RemoveTab(ITabViewModel tabViewModel); void SetSearchText(string? searchText); - void SwitchViewMode(ViewMode newViewMode); + Task SwitchViewModeAsync(ViewMode newViewMode); void SetSelectedTab(ITabViewModel tabToSelect); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs index 01e274c..7c1315b 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs @@ -1,5 +1,4 @@ using DeclarativeProperty; -using FileTime.App.Core.Models; using FileTime.App.Core.Models.Enums; using FileTime.Core.Models; using InitableService; diff --git a/src/AppCommon/FileTime.App.Core/Services/ItemNameConverterService.cs b/src/AppCommon/FileTime.App.Core/Services/ItemNameConverterService.cs index c966825..3ffedc0 100644 --- a/src/AppCommon/FileTime.App.Core/Services/ItemNameConverterService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/ItemNameConverterService.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using FileTime.Core.Models; namespace FileTime.App.Core.Services; @@ -13,7 +14,7 @@ public class ItemNameConverterService : IItemNameConverterService { var nameLeft = name; - while (nameLeft.ToLower().IndexOf(searchText, StringComparison.Ordinal) is int rapidTextStart && rapidTextStart != -1) + while (nameLeft.ToLower().IndexOf(searchText, StringComparison.Ordinal) is var rapidTextStart && rapidTextStart != -1) { var before = rapidTextStart > 0 ? nameLeft.Substring(0, rapidTextStart) : null; var rapidTravel = nameLeft.Substring(rapidTextStart, searchText.Length); diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs index 37f6fc1..054774c 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs @@ -310,13 +310,13 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase private Task EnterRapidTravel() { - _appState.SwitchViewMode(ViewMode.RapidTravel); + _appState.SwitchViewModeAsync(ViewMode.RapidTravel); return Task.CompletedTask; } private Task ExitRapidTravel() { - _appState.SwitchViewMode(ViewMode.Default); + _appState.SwitchViewModeAsync(ViewMode.Default); return Task.CompletedTask; } diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs index 25154c6..f0a4c4d 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ToolUserCommandHandlerService.cs @@ -106,10 +106,10 @@ public class ToolUserCommandHandlerService : UserCommandHandlerServiceBase //TODO proper error message if (string.IsNullOrWhiteSpace(searchQuery)) return; - var searchMatcher = searchCommand.SearchType switch + ISearchMatcher searchMatcher = searchCommand.SearchType switch { SearchType.NameContains => new NameContainsMatcher(_itemNameConverterService, searchQuery), - //SearchType.NameRegex => new NameRegexMatcher(searchQuery), + SearchType.NameRegex => new RegexMatcher(searchQuery), _ => throw new ArgumentOutOfRangeException() }; diff --git a/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs b/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs index 2f18609..b6745cb 100644 --- a/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs +++ b/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs @@ -54,6 +54,7 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler AddUserCommand(SortItemsCommand.OrderByDateCommand); AddUserCommand(SortItemsCommand.OrderByDateDescCommand); AddUserCommand(IdentifiableSearchCommand.SearchByNameContains); + AddUserCommand(IdentifiableSearchCommand.SearchByRegex); AddUserCommand(SwitchToTabCommand.SwitchToLastTab); AddUserCommand(SwitchToTabCommand.SwitchToTab1); AddUserCommand(SwitchToTabCommand.SwitchToTab2); diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs index d4b3398..a949951 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs @@ -16,10 +16,10 @@ public abstract partial class AppStateBase : IAppState { private readonly BehaviorSubject _searchText = new(null); private readonly BehaviorSubject _selectedTab = new(null); - private readonly BehaviorSubject _viewMode = new(Models.Enums.ViewMode.Default); + private readonly DeclarativeProperty _viewMode = new(Models.Enums.ViewMode.Default); private readonly SourceList _tabs = new(); - public IObservable ViewMode { get; private set; } + public IDeclarativeProperty ViewMode { get; private set; } public ReadOnlyObservableCollection Tabs { get; private set; } public IObservable SearchText { get; private set; } @@ -31,7 +31,7 @@ public abstract partial class AppStateBase : IAppState partial void OnInitialize() { RapidTravelText = new(""); - ViewMode = _viewMode.AsObservable(); + ViewMode = _viewMode; var tabsObservable = _tabs.Connect(); @@ -70,10 +70,7 @@ public abstract partial class AppStateBase : IAppState public void SetSearchText(string? searchText) => _searchText.OnNext(searchText); - public void SwitchViewMode(ViewMode newViewMode) - { - _viewMode.OnNext(newViewMode); - } + public async Task SwitchViewModeAsync(ViewMode newViewMode) => await _viewMode.SetValue(newViewMode); public void SetSelectedTab(ITabViewModel tabToSelect) => _selectedTab.OnNext(tabToSelect); diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs index 1d37c43..ecbec6f 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs @@ -15,8 +15,6 @@ public partial class ContainerSizeContainerViewModel : ItemViewModel, IContainer { } - public void Init(IContainer item, ITabViewModel parentTab, ItemViewModelType itemViewModelType) - { - Init((IItem)item, parentTab, itemViewModelType); - } + public void Init(IContainer item, ITabViewModel parentTab, ItemViewModelType itemViewModelType) + => Init((IItem)item, parentTab, itemViewModelType); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs index efc917a..a37852c 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs @@ -17,8 +17,6 @@ public partial class ElementViewModel : ItemViewModel, IElementViewModel { } - public void Init(IElement item, ITabViewModel parentTab, ItemViewModelType itemViewModelType) - { - Init((IItem)item, parentTab, itemViewModelType); - } + public void Init(IElement item, ITabViewModel parentTab, ItemViewModelType itemViewModelType) + => Init((IItem)item, parentTab, itemViewModelType); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs index cb77547..77a4a23 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemPreview/ElementPreviewViewModel.cs @@ -96,6 +96,7 @@ public partial class ElementPreviewViewModel : IItemPreviewViewModel, IAsyncInit var encodingsWithPartialResult = binaryCharacter.Where(e => !string.IsNullOrWhiteSpace(e.Value.PartialResult)).ToList(); if (encodingsWithPartialResult.Count > 0) { + stringBuilder.AppendLine(); stringBuilder.AppendLine("The following partial texts could be read by encodings:"); foreach (var binaryByEncoding in encodingsWithPartialResult) { diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs index 12e3010..f3b0973 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs @@ -2,9 +2,9 @@ using System.ComponentModel; using System.Reactive.Linq; using DeclarativeProperty; using DynamicData; -using FileTime.App.Core.Models; using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Services; +using FileTime.Core.Behaviors; using FileTime.Core.Helper; using FileTime.Core.Models; using MoreLinq; @@ -37,7 +37,10 @@ public abstract partial class ItemViewModel : IItemViewModel public IDeclarativeProperty>? DisplayName { get; private set; } - public void Init(IItem item, ITabViewModel parentTab, ItemViewModelType itemViewModelType) + public void Init( + IItem item, + ITabViewModel parentTab, + ItemViewModelType itemViewModelType) { _parentTab = parentTab; @@ -51,7 +54,12 @@ public abstract partial class ItemViewModel : IItemViewModel var displayName = itemViewModelType switch { - ItemViewModelType.Main => _appState.RapidTravelText.Map(s => (IReadOnlyList) _itemNameConverterService.GetDisplayName(item.DisplayName, s)), + ItemViewModelType.Main => _appState.RapidTravelText.Map(async (s, _) => + _appState.ViewMode.Value != Models.Enums.ViewMode.RapidTravel + && _appState.CurrentSelectedTab?.CurrentLocation.Value?.Provider is IItemNameConverterProvider nameConverterProvider + ? (IReadOnlyList) await nameConverterProvider.GetItemNamePartsAsync(item) + : _itemNameConverterService.GetDisplayName(item.DisplayName, s) + ), _ => new DeclarativeProperty>(new List {new(item.DisplayName)}), }; diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs index fcc5878..2a0a5d5 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs @@ -1,3 +1,4 @@ +using Avalonia.Input; using FileTime.App.Core.ViewModels; using FileTime.App.FuzzyPanel; @@ -7,4 +8,5 @@ public interface IFrequencyNavigationViewModel : IFuzzyPanelViewModel, I { IObservable ShowWindow { get; } void Close(); + Task HandleKeyUp(KeyEventArgs keyEventArgs); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs b/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs index da7111a..03405e1 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs @@ -30,11 +30,9 @@ public class FrequencyNavigationViewModel : FuzzyPanelViewModel, IFreque public void Close() => _frequencyNavigationService.CloseNavigationWindow(); - public override async Task HandleKeyDown(KeyEventArgs keyEventArgs) + public async Task HandleKeyUp(KeyEventArgs keyEventArgs) { - var handled = await base.HandleKeyDown(keyEventArgs); - - if (handled) return true; + if (keyEventArgs.Handled) return false; if (keyEventArgs.Key == Key.Enter) { @@ -49,6 +47,23 @@ public class FrequencyNavigationViewModel : FuzzyPanelViewModel, IFreque return false; } + public override async Task HandleKeyDown(KeyEventArgs keyEventArgs) + { + if (keyEventArgs.Handled) return false; + var handled = await base.HandleKeyDown(keyEventArgs); + + if (handled) return true; + + if (keyEventArgs.Key == Key.Escape) + { + keyEventArgs.Handled = true; + Close(); + return true; + } + + return false; + } + public override void UpdateFilteredMatches() => FilteredMatches = new List(_frequencyNavigationService.GetMatchingContainers(SearchText)); diff --git a/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs b/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs index e80f447..2375ed6 100644 --- a/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs +++ b/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs @@ -6,12 +6,18 @@ namespace FileTime.App.FuzzyPanel; public abstract partial class FuzzyPanelViewModel : IFuzzyPanelViewModel where TItem : class { + private readonly Func _itemEquality; private string _searchText = String.Empty; [Notify(set: Setter.Protected)] private IObservable _showWindow; [Notify(set: Setter.Protected)] private List _filteredMatches; [Notify(set: Setter.Protected)] private TItem? _selectedItem; + protected FuzzyPanelViewModel(Func? itemEquality = null) + { + _itemEquality = itemEquality ?? ((a, b) => a == b); + } + public string SearchText { get => _searchText; @@ -42,7 +48,9 @@ public abstract partial class FuzzyPanelViewModel : IFuzzyPanelViewModel< { if (keyEventArgs.Key == Key.Down) { - var nextItem = FilteredMatches.SkipWhile(i => i != SelectedItem).Skip(1).FirstOrDefault(); + var nextItem = SelectedItem is null + ? FilteredMatches.FirstOrDefault() + : FilteredMatches.SkipWhile(i => !_itemEquality(i, SelectedItem)).Skip(1).FirstOrDefault(); if (nextItem is not null) { @@ -54,7 +62,9 @@ public abstract partial class FuzzyPanelViewModel : IFuzzyPanelViewModel< } else if (keyEventArgs.Key == Key.Up) { - var previousItem = FilteredMatches.TakeWhile(i => i != SelectedItem).LastOrDefault(); + var previousItem = SelectedItem is null + ? FilteredMatches.LastOrDefault() + : FilteredMatches.TakeWhile(i => !_itemEquality(i, SelectedItem)).LastOrDefault(); if (previousItem is not null) { diff --git a/src/AppCommon/FileTime.App.Search.Abstractions/ISearchTask.cs b/src/AppCommon/FileTime.App.Search.Abstractions/ISearchTask.cs index 66cebef..9dad367 100644 --- a/src/AppCommon/FileTime.App.Search.Abstractions/ISearchTask.cs +++ b/src/AppCommon/FileTime.App.Search.Abstractions/ISearchTask.cs @@ -5,5 +5,6 @@ namespace FileTime.App.Search; public interface ISearchTask { IContainer SearchContainer { get; } + IReadOnlyDictionary RealFullNames { get; } Task StartAsync(); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/RegexMatcher.cs b/src/AppCommon/FileTime.App.Search/RegexMatcher.cs new file mode 100644 index 0000000..ab48ee3 --- /dev/null +++ b/src/AppCommon/FileTime.App.Search/RegexMatcher.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; +using FileTime.App.Core.Models; +using FileTime.Core.Models; + +namespace FileTime.App.Search; + +public class RegexMatcher : ISearchMatcher +{ + private readonly Regex _regex; + + public RegexMatcher(string pattern) + { + _regex = new Regex(pattern); + } + + public Task IsItemMatchAsync(IItem item) + => Task.FromResult(_regex.IsMatch(item.DisplayName)); + + public List GetDisplayName(IItem item) + { + var displayName = item.DisplayName; + + var match = _regex.Match(item.DisplayName); + var splitPoints = new List(match.Groups.Count * 2); + if (match.Groups.Count == 0) + { + return new List + { + new(displayName) + }; + } + + var areEvensSpecial = match.Groups[0].Index == 0; + var isSpecialMatchNumber = areEvensSpecial ? 0 : 1; + + foreach (Group group in match.Groups) + { + splitPoints.Add(group.Index); + splitPoints.Add(group.Index + group.Value.Length); + } + + if (splitPoints[0] != 0) + splitPoints.Insert(0, 0); + + var itemNameParts = new List(); + for (var i = 0; i < splitPoints.Count; i++) + { + var index = splitPoints[i]; + var nextIndex = i == splitPoints.Count - 1 + ? displayName.Length + : splitPoints[i + 1]; + + if (nextIndex == index) continue; + + var text = displayName.Substring(index, nextIndex - index); + itemNameParts.Add(new ItemNamePart(text, i % 2 == isSpecialMatchNumber)); + } + + return itemNameParts; + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs index 69b9f46..aa8a66d 100644 --- a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs +++ b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs @@ -1,3 +1,5 @@ +using FileTime.App.Core.Exceptions; +using FileTime.Core.Behaviors; using FileTime.Core.ContentAccess; using FileTime.Core.Enums; using FileTime.Core.Models; @@ -5,7 +7,7 @@ using FileTime.Core.Timeline; namespace FileTime.App.Search; -public class SearchContentProvider : ContentProviderBase, ISearchContentProvider +public class SearchContentProvider : ContentProviderBase, ISearchContentProvider, IItemNameConverterProvider { private readonly ITimelessContentProvider _timelessContentProvider; private readonly List _searchTasks = new(); @@ -17,6 +19,40 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider _timelessContentProvider = timelessContentProvider; } + public override async Task GetItemByFullNameAsync( + FullName fullName, + PointInTime pointInTime, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = null + ) + { + if (fullName.Path == ContentProviderName) + return this; + + if (_searchTasks + .FirstOrDefault(t => t.SearchContainer.FullName == fullName) is { } searchTask) + { + return searchTask.SearchContainer; + } + + if (_searchTasks.FirstOrDefault(t => t.RealFullNames.ContainsKey(fullName)) is { } searchTask2) + { + var realFullName = searchTask2.RealFullNames[fullName]; + var item = await _timelessContentProvider.GetItemByFullNameAsync( + realFullName, + pointInTime, + forceResolve, + forceResolvePathType, + itemInitializationSettings + ); + item = item.WithParent(new AbsolutePath(_timelessContentProvider, searchTask2.SearchContainer)); + return item; + } + + throw new ItemNotFoundException(fullName); + } + public override Task GetItemByNativePathAsync( NativePath nativePath, PointInTime pointInTime, @@ -63,7 +99,7 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider { var searchTask = _searchTasks.FirstOrDefault(t => t.SearchContainer.FullName == searchFullName); if (searchTask is null) return; - + _searchTasks.Remove(searchTask); var searchItem = Items.FirstOrDefault(c => c.Path == searchTask.SearchContainer.FullName); if (searchItem is not null) @@ -71,4 +107,23 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider Items.Remove(searchItem); } } + + public async Task> GetItemNamePartsAsync(IItem item) + { + var currentItem = item; + SearchTask? searchTask = null; + + while (searchTask is null && currentItem is not null) + { + searchTask = currentItem.GetExtension()?.SearchTask; + + currentItem = currentItem.Parent is null + ? null + : await currentItem.Parent.ResolveAsync(itemInitializationSettings: new ItemInitializationSettings {SkipChildInitialization = true}); + } + + if (searchTask is null) return new List {new(item.DisplayName)}; + + return searchTask.Matcher.GetDisplayName(item); + } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/SearchExtension.cs b/src/AppCommon/FileTime.App.Search/SearchExtension.cs new file mode 100644 index 0000000..e05e997 --- /dev/null +++ b/src/AppCommon/FileTime.App.Search/SearchExtension.cs @@ -0,0 +1,3 @@ +namespace FileTime.App.Search; + +public record SearchExtension(SearchTask SearchTask); \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Search/SearchTask.cs b/src/AppCommon/FileTime.App.Search/SearchTask.cs index c787712..2bf2e0d 100644 --- a/src/AppCommon/FileTime.App.Search/SearchTask.cs +++ b/src/AppCommon/FileTime.App.Search/SearchTask.cs @@ -1,6 +1,4 @@ using System.Collections.ObjectModel; -using DynamicData; -using FileTime.Core.ContentAccess; using FileTime.Core.Enums; using FileTime.Core.Models; using FileTime.Core.Timeline; @@ -18,8 +16,11 @@ public class SearchTask : ISearchTask private readonly SemaphoreSlim _searchingLock = new(1, 1); private bool _isSearching; private static int _searchId = 1; + private readonly Dictionary _realFullNames = new(); + public IReadOnlyDictionary RealFullNames { get; } public IContainer SearchContainer => _container; + public ISearchMatcher Matcher => _matcher; public SearchTask( IContainer baseContainer, @@ -33,6 +34,12 @@ public class SearchTask : ISearchTask _baseContainer = baseContainer; _timelessContentProvider = timelessContentProvider; _matcher = matcher; + RealFullNames = _realFullNames.AsReadOnly(); + + var extensions = new ExtensionCollection + { + new SearchExtension(this) + }; _container = new Container( baseContainer.Name, baseContainer.DisplayName, @@ -49,7 +56,7 @@ public class SearchTask : ISearchTask false, PointInTime.Present, _exceptions, - new ReadOnlyExtensionCollection(new ExtensionCollection()), + new ReadOnlyExtensionCollection(extensions), _items ); } @@ -89,14 +96,17 @@ public class SearchTask : ISearchTask foreach (var itemPath in items) { - var item = await itemPath.ResolveAsync( - itemInitializationSettings: new ItemInitializationSettings - { - Parent = new AbsolutePath(_timelessContentProvider, _container) - }); + var item = await itemPath.ResolveAsync(); if (await _matcher.IsItemMatchAsync(item)) { - _items.Add(itemPath); + var childName = _container.FullName.GetChild(itemPath.Path.GetName()); + _realFullNames.Add(childName, itemPath.Path); + _items.Add(new AbsolutePath( + _timelessContentProvider, + PointInTime.Present, + childName, + AbsolutePathType.Container + )); } if (item is IContainer childContainer) diff --git a/src/Core/FileTime.Core.Abstraction/Behaviors/IItemNameConverterProvider.cs b/src/Core/FileTime.Core.Abstraction/Behaviors/IItemNameConverterProvider.cs new file mode 100644 index 0000000..e3a1f6b --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Behaviors/IItemNameConverterProvider.cs @@ -0,0 +1,8 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.Behaviors; + +public interface IItemNameConverterProvider +{ + Task> GetItemNamePartsAsync(IItem item); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/IItem.cs b/src/Core/FileTime.Core.Abstraction/Models/IItem.cs index 5f768c0..6ac211a 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/IItem.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/IItem.cs @@ -26,4 +26,6 @@ public interface IItem ReadOnlyExtensionCollection Extensions { get; } T? GetExtension() => (T?)Extensions.FirstOrDefault(i => i is T); + + IItem WithParent(AbsolutePath parent); } diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/ItemNamePart.cs b/src/Core/FileTime.Core.Abstraction/Models/ItemNamePart.cs similarity index 64% rename from src/AppCommon/FileTime.App.Core.Abstraction/Models/ItemNamePart.cs rename to src/Core/FileTime.Core.Abstraction/Models/ItemNamePart.cs index d04c1f6..77ec3e7 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Models/ItemNamePart.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/ItemNamePart.cs @@ -1,3 +1,3 @@ -namespace FileTime.App.Core.Models; +namespace FileTime.Core.Models; public record ItemNamePart(string Text, bool IsSpecial = false); \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs index 4a6e6a3..b1b457c 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs @@ -90,4 +90,5 @@ public abstract class ContentProviderBase : IContentProvider public abstract bool CanHandlePath(NativePath path); public bool CanHandlePath(FullName path) => CanHandlePath(GetNativePath(path)); + public IItem WithParent(AbsolutePath parent) => this; } \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/RootContentProvider.cs b/src/Core/FileTime.Core.ContentAccess/RootContentProvider.cs index 821e8cb..da43b24 100644 --- a/src/Core/FileTime.Core.ContentAccess/RootContentProvider.cs +++ b/src/Core/FileTime.Core.ContentAccess/RootContentProvider.cs @@ -84,4 +84,5 @@ public class RootContentProvider : IRootContentProvider public bool CanHandlePath(NativePath path) => throw new NotImplementedException(); public bool CanHandlePath(FullName path) => throw new NotImplementedException(); + public IItem WithParent(AbsolutePath parent) => this; } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Models/Container.cs b/src/Core/FileTime.Core.Models/Container.cs index f4b3c48..465ffc6 100644 --- a/src/Core/FileTime.Core.Models/Container.cs +++ b/src/Core/FileTime.Core.Models/Container.cs @@ -45,15 +45,19 @@ public record Container( _isLoading.OnNext(true); IsLoaded = false; } + public void StopLoading() { _isLoading.OnNext(false); IsLoaded = true; } + public void CancelLoading() { _loadingCancellationTokenSource.Cancel(); _isLoading.OnNext(false); IsLoaded = true; } + + public IItem WithParent(AbsolutePath parent) => this with {Parent = parent}; } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Models/Element.cs b/src/Core/FileTime.Core.Models/Element.cs index 5b65a33..f2854eb 100644 --- a/src/Core/FileTime.Core.Models/Element.cs +++ b/src/Core/FileTime.Core.Models/Element.cs @@ -24,4 +24,6 @@ public record Element( ReadOnlyExtensionCollection Extensions) : IElement { public AbsolutePathType Type => AbsolutePathType.Element; + + public IItem WithParent(AbsolutePath parent) => this with { Parent = parent }; } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/NamePartShrinkerConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/NamePartShrinkerConverter.cs index d6585a9..3c12e05 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/NamePartShrinkerConverter.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/NamePartShrinkerConverter.cs @@ -2,6 +2,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; using FileTime.App.Core.Models; +using FileTime.Core.Models; using FileTime.GuiApp.ViewModels; namespace FileTime.GuiApp.Converters; diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml index 37cad10..2ff8b49 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml @@ -4,18 +4,18 @@ mc:Ignorable="d" x:Class="FileTime.GuiApp.Views.CommandPalette" x:CompileBindings="True" - xmlns:vm="clr-namespace:FileTime.App.CommandPalette.ViewModels;assembly=FileTime.App.CommandPalette.Abstractions" xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="clr-namespace:FileTime.App.CommandPalette.ViewModels;assembly=FileTime.App.CommandPalette.Abstractions" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + - + - + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs index 84409f9..bfd7118 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs @@ -26,6 +26,7 @@ public partial class CommandPalette : UserControl private void Search_OnKeyDown(object? sender, KeyEventArgs e) { + if (e.Handled) return; if (DataContext is not ICommandPaletteViewModel viewModel) return; if (e.Key == Key.Escape) @@ -38,4 +39,11 @@ public partial class CommandPalette : UserControl viewModel.HandleKeyDown(e); } } + + private void Search_OnKeyUp(object? sender, KeyEventArgs e) + { + if (e.Handled) return; + if (DataContext is not ICommandPaletteViewModel viewModel) return; + viewModel.HandleKeyUp(e); + } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml index 16297d9..c8377fd 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml @@ -17,6 +17,7 @@ _inputViewModel = inputViewModel ); }; - + _logger?.LogInformation( $"{nameof(MainWindow)} opened, starting {nameof(MainWindowViewModel)} initialization..."); ViewModel = DI.ServiceProvider.GetRequiredService(); @@ -111,7 +111,7 @@ public partial class MainWindow : Window, IUiAccessor && sender is StyledElement control) { FullName? path = null; - if (control.DataContext is IHaveFullPath {Path: { }} hasFullPath) + if (control.DataContext is IHaveFullPath { Path: { } } hasFullPath) { path = hasFullPath.Path; }