diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 57747c8b24d..0710babe368 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -24,7 +24,7 @@ private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync return new List(); } - private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString) + private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token) { return new List { diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 1b8dd602dbc..feab3a7513d 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -139,39 +139,9 @@ public void KeepResultsExcept(PluginMetadata metadata) /// public void AddResults(List newRawResults, string resultId) { - lock (_collectionLock) - { - var newResults = NewResults(newRawResults, resultId); - - // https://social.msdn.microsoft.com/Forums/vstudio/en-US/5ff71969-f183-4744-909d-50f7cd414954/binding-a-tabcontrols-selectedindex-not-working?forum=wpf - // fix selected index flow - var updateTask = Task.Run(() => - { - // update UI in one run, so it can avoid UI flickering - - Results.Update(newResults); - if (Results.Any()) - SelectedItem = Results[0]; - }); - if (!updateTask.Wait(300)) - { - updateTask.Dispose(); - throw new TimeoutException("Update result use too much time."); - } + var newResults = NewResults(newRawResults, resultId); - } - - if (Visbility != Visibility.Visible && Results.Count > 0) - { - Margin = new Thickness { Top = 8 }; - SelectedIndex = 0; - Visbility = Visibility.Visible; - } - else - { - Margin = new Thickness { Top = 0 }; - Visbility = Visibility.Collapsed; - } + UpdateResults(newResults); } /// /// To avoid deadlock, this method should not called from main thread @@ -179,12 +149,18 @@ public void AddResults(List newRawResults, string resultId) public void AddResults(IEnumerable resultsForUpdates, CancellationToken token) { var newResults = NewResults(resultsForUpdates); + if (token.IsCancellationRequested) return; + + UpdateResults(newResults, token); + } + + private void UpdateResults(List newResults, CancellationToken token = default) + { lock (_collectionLock) { // update UI in one run, so it can avoid UI flickering - Results.Update(newResults, token); if (Results.Any()) SelectedItem = Results[0]; @@ -202,7 +178,6 @@ public void AddResults(IEnumerable resultsForUpdates, Cancella Visbility = Visibility.Collapsed; break; } - } private List NewResults(List newRawResults, string resultId) @@ -212,10 +187,10 @@ private List NewResults(List newRawResults, string resu var results = Results as IEnumerable; - var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)).ToList(); + var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); return results.Where(r => r.Result.PluginID != resultId) - .Concat(results.Intersect(newResults).Union(newResults)) + .Concat(newResults) .OrderByDescending(r => r.Result.Score) .ToList(); } @@ -228,8 +203,7 @@ private List NewResults(IEnumerable resultsFo var results = Results as IEnumerable; return results.Where(r => r != null && !resultsForUpdates.Any(u => u.Metadata.ID == r.Result.PluginID)) - .Concat( - resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) + .Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) .OrderByDescending(rv => rv.Result.Score) .ToList(); } @@ -266,49 +240,50 @@ private static void FormattedTextPropertyChanged(DependencyObject d, DependencyP } #endregion - public class ResultCollection : ObservableCollection + public class ResultCollection : List, INotifyCollectionChanged { private long editTime = 0; - private bool _suppressNotifying = false; - private CancellationToken _token; - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + public event NotifyCollectionChangedEventHandler CollectionChanged; + + + protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - if (!_suppressNotifying) - { - base.OnCollectionChanged(e); - } + CollectionChanged?.Invoke(this, e); } - public void BulkAddRange(IEnumerable resultViews) + public void BulkAddAll(List resultViews) { - // suppress notifying before adding all element - _suppressNotifying = true; - foreach (var item in resultViews) - { - Add(item); - } - _suppressNotifying = false; - // manually update event - // wpf use directx / double buffered already, so just reset all won't cause ui flickering + AddRange(resultViews); + + // can return because the list will be cleared next time updated, which include a reset event if (_token.IsCancellationRequested) return; + + // manually update event + // wpf use directx / double buffered already, so just reset all won't cause ui flickering OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - public void AddRange(IEnumerable Items) + private void AddAll(List Items) { - foreach (var item in Items) + for (int i = 0; i < Items.Count; i++) { + var item = Items[i]; if (_token.IsCancellationRequested) return; Add(item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); } } - public void RemoveAll() + public void RemoveAll(int Capacity = 512) { - ClearItems(); + Clear(); + if (this.Capacity > 8000 && Capacity < this.Capacity) + this.Capacity = Capacity; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// @@ -323,15 +298,19 @@ public void Update(List newItems, CancellationToken token = def if (editTime < 10 || newItems.Count < 30) { - if (Count != 0) ClearItems(); - AddRange(newItems); + if (Count != 0) RemoveAll(newItems.Count); + AddAll(newItems); editTime++; return; } else { Clear(); - BulkAddRange(newItems); + BulkAddAll(newItems); + if (Capacity > 8000 && newItems.Count < 3000) + { + Capacity = newItems.Count; + } editTime++; } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs index 88d7d6927c1..5124f6fb324 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo { @@ -16,14 +17,18 @@ public DirectoryInfoSearch(PluginInitContext context) resultManager = new ResultManager(context); } - internal List TopLevelDirectorySearch(Query query, string search) + internal List TopLevelDirectorySearch(Query query, string search, CancellationToken token) { var criteria = ConstructSearchCriteria(search); - if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator)) - return DirectorySearch(SearchOption.AllDirectories, query, search, criteria); + if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > + search.LastIndexOf(Constants.DirectorySeperator)) + return DirectorySearch(new EnumerationOptions + { + RecurseSubdirectories = true + }, query, search, criteria, token); - return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria); + return DirectorySearch(new EnumerationOptions(), query, search, criteria, token); // null will be passed as default } public string ConstructSearchCriteria(string search) @@ -45,7 +50,8 @@ public string ConstructSearchCriteria(string search) return incompleteName; } - private List DirectorySearch(SearchOption searchOption, Query query, string search, string searchCriteria) + private List DirectorySearch(EnumerationOptions enumerationOption, Query query, string search, + string searchCriteria, CancellationToken token) { var results = new List(); @@ -58,38 +64,38 @@ private List DirectorySearch(SearchOption searchOption, Query query, str { var directoryInfo = new System.IO.DirectoryInfo(path); - foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, searchOption)) + foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption)) { - if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; - if (fileSystemInfo is System.IO.DirectoryInfo) { - folderList.Add(resultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, fileSystemInfo.FullName, query, true, false)); + folderList.Add(resultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, + fileSystemInfo.FullName, query, true, false)); } else { fileList.Add(resultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false)); } + + token.ThrowIfCancellationRequested(); } } catch (Exception e) { - if (e is UnauthorizedAccessException || e is ArgumentException) - { - results.Add(new Result { Title = e.Message, Score = 501 }); + if (!(e is ArgumentException)) + throw e; + + results.Add(new Result {Title = e.Message, Score = 501}); - return results; - } + return results; #if DEBUG // Please investigate and handle error from DirectoryInfo search - throw e; #else Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e); -#endif +#endif } - // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. + // Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList(); } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs index 8bd19956eab..6f0020ac956 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs @@ -6,25 +6,31 @@ namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks { public class QuickFolderAccess { - internal List FolderListMatched(Query query, List folderLinks, PluginInitContext context) + private readonly ResultManager resultManager; + + public QuickFolderAccess(PluginInitContext context) + { + resultManager = new ResultManager(context); + } + + internal List FolderListMatched(Query query, List folderLinks) { if (string.IsNullOrEmpty(query.Search)) return new List(); string search = query.Search.ToLower(); - - var queriedFolderLinks = folderLinks.Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)); + + var queriedFolderLinks = + folderLinks.Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)); return queriedFolderLinks.Select(item => - new ResultManager(context) - .CreateFolderResult(item.Nickname, item.Path, item.Path, query)) - .ToList(); + resultManager.CreateFolderResult(item.Nickname, item.Path, item.Path, query)) + .ToList(); } - internal List FolderListAll(Query query, List folderLinks, PluginInitContext context) + internal List FolderListAll(Query query, List folderLinks) => folderLinks - .Select(item => - new ResultManager(context).CreateFolderResult(item.Nickname, item.Path, item.Path, query)) + .Select(item => resultManager.CreateFolderResult(item.Nickname, item.Path, item.Path, query)) .ToList(); } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 912c5f42896..14aefeb1984 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -16,7 +16,7 @@ public class SearchManager private readonly IndexSearch indexSearch; - private readonly QuickFolderAccess quickFolderAccess = new QuickFolderAccess(); + private readonly QuickFolderAccess quickFolderAccess; private readonly ResultManager resultManager; @@ -28,6 +28,7 @@ public SearchManager(Settings settings, PluginInitContext context) indexSearch = new IndexSearch(context); resultManager = new ResultManager(context); this.settings = settings; + quickFolderAccess = new QuickFolderAccess(context); } internal async Task> SearchAsync(Query query, CancellationToken token) @@ -40,12 +41,10 @@ internal async Task> SearchAsync(Query query, CancellationToken tok return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false); // This allows the user to type the assigned action keyword and only see the list of quick folder links - if (settings.QuickFolderAccessLinks.Count > 0 - && query.ActionKeyword == settings.SearchActionKeyword - && string.IsNullOrEmpty(query.Search)) - return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks, context); + if (string.IsNullOrEmpty(query.Search)) + return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks); - var quickFolderLinks = quickFolderAccess.FolderListMatched(query, settings.QuickFolderAccessLinks, context); + var quickFolderLinks = quickFolderAccess.FolderListMatched(query, settings.QuickFolderAccessLinks); if (quickFolderLinks.Count > 0) results.AddRange(quickFolderLinks); @@ -78,15 +77,18 @@ internal async Task> SearchAsync(Query query, CancellationToken tok results.Add(resultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch)); - if (token.IsCancellationRequested) - return null; + token.ThrowIfCancellationRequested(); - results.AddRange(await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync, - DirectoryInfoClassSearch, - useIndexSearch, - query, - locationPath, - token).ConfigureAwait(false)); + var directoryResult = await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync, + DirectoryInfoClassSearch, + useIndexSearch, + query, + locationPath, + token).ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); + + results.AddRange(directoryResult); return results; } @@ -110,23 +112,23 @@ public bool IsFileContentSearch(string actionKeyword) return actionKeyword == settings.FileContentSearchActionKeyword; } - private List DirectoryInfoClassSearch(Query query, string querySearch) + private List DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token) { var directoryInfoSearch = new DirectoryInfoSearch(context); - return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch); + return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch, token); } public async Task> TopLevelDirectorySearchBehaviourAsync( Func>> windowsIndexSearch, - Func> directoryInfoClassSearch, + Func> directoryInfoClassSearch, bool useIndexSearch, Query query, string querySearchString, CancellationToken token) { if (!useIndexSearch) - return directoryInfoClassSearch(query, querySearchString); + return directoryInfoClassSearch(query, querySearchString, token); return await windowsIndexSearch(query, querySearchString, token); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index 76fd36bb5d9..1e92d2254ec 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -7,7 +7,7 @@ "Name": "Explorer", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Author": "Jeremy Wu", - "Version": "1.4.1", + "Version": "1.5.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",