Search by regex, modal Enter fixes
This commit is contained in:
61
src/AppCommon/FileTime.App.Search/RegexMatcher.cs
Normal file
61
src/AppCommon/FileTime.App.Search/RegexMatcher.cs
Normal file
@@ -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<bool> IsItemMatchAsync(IItem item)
|
||||
=> Task.FromResult(_regex.IsMatch(item.DisplayName));
|
||||
|
||||
public List<ItemNamePart> GetDisplayName(IItem item)
|
||||
{
|
||||
var displayName = item.DisplayName;
|
||||
|
||||
var match = _regex.Match(item.DisplayName);
|
||||
var splitPoints = new List<int>(match.Groups.Count * 2);
|
||||
if (match.Groups.Count == 0)
|
||||
{
|
||||
return new List<ItemNamePart>
|
||||
{
|
||||
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<ItemNamePart>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<SearchTask> _searchTasks = new();
|
||||
@@ -17,6 +19,40 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider
|
||||
_timelessContentProvider = timelessContentProvider;
|
||||
}
|
||||
|
||||
public override async Task<IItem> 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<IItem> 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<IEnumerable<ItemNamePart>> GetItemNamePartsAsync(IItem item)
|
||||
{
|
||||
var currentItem = item;
|
||||
SearchTask? searchTask = null;
|
||||
|
||||
while (searchTask is null && currentItem is not null)
|
||||
{
|
||||
searchTask = currentItem.GetExtension<SearchExtension>()?.SearchTask;
|
||||
|
||||
currentItem = currentItem.Parent is null
|
||||
? null
|
||||
: await currentItem.Parent.ResolveAsync(itemInitializationSettings: new ItemInitializationSettings {SkipChildInitialization = true});
|
||||
}
|
||||
|
||||
if (searchTask is null) return new List<ItemNamePart> {new(item.DisplayName)};
|
||||
|
||||
return searchTask.Matcher.GetDisplayName(item);
|
||||
}
|
||||
}
|
||||
3
src/AppCommon/FileTime.App.Search/SearchExtension.cs
Normal file
3
src/AppCommon/FileTime.App.Search/SearchExtension.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace FileTime.App.Search;
|
||||
|
||||
public record SearchExtension(SearchTask SearchTask);
|
||||
@@ -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<FullName, FullName> _realFullNames = new();
|
||||
public IReadOnlyDictionary<FullName, FullName> 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)
|
||||
|
||||
Reference in New Issue
Block a user