diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 25a32a728d3..c316e7cc1c2 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -6,7 +6,7 @@ namespace Flow.Launcher.Core.Plugin { public static class QueryBuilder { - public static Query Build(string text, Dictionary nonGlobalPlugins) + public static Query Build(string input, string text, Dictionary nonGlobalPlugins) { // home query if (string.IsNullOrEmpty(text)) @@ -14,6 +14,7 @@ public static Query Build(string text, Dictionary nonGlobalP return new Query() { Search = string.Empty, + Input = string.Empty, RawQuery = string.Empty, SearchTerms = Array.Empty(), ActionKeyword = string.Empty, @@ -52,6 +53,7 @@ public static Query Build(string text, Dictionary nonGlobalP return new Query() { Search = search, + Input = input, RawQuery = rawQuery, SearchTerms = searchTerms, ActionKeyword = actionKeyword, diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index f50614699fd..74e5ca5c8d8 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -7,6 +7,12 @@ namespace Flow.Launcher.Plugin /// public class Query { + /// + /// Input text in query box. + /// We didn't recommend use this property directly. You should always use Search property. + /// + public string Input { get; internal init; } + /// /// Raw query, this includes action keyword if it has. /// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace. diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index c8ac17748da..3912f26a7d3 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -16,7 +16,7 @@ public void ExclusivePluginQueryTest() {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}}}} }; - Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); + Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins); ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); @@ -39,7 +39,7 @@ public void ExclusivePluginQueryIgnoreDisabledTest() {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}, Disabled = true}}} }; - Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); + Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins); ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search); ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); @@ -51,7 +51,7 @@ public void ExclusivePluginQueryIgnoreDisabledTest() [Test] public void GenericPluginQueryTest() { - Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); + Query q = QueryBuilder.Build("file.txt file2 file3", "file.txt file2 file3", new Dictionary()); ClassicAssert.AreEqual("file.txt file2 file3", q.Search); ClassicAssert.AreEqual("", q.ActionKeyword); diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index c4ed73a0d5a..da1f6bb99e5 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -472,7 +472,7 @@ private void OnKeyDown(object sender, KeyEventArgs e) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) { var queryWithoutActionKeyword = - QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 66fa706829b..3433ef6617d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -34,9 +34,10 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private static readonly string ClassName = nameof(MainViewModel); - private bool _isQueryRunning; private Query _lastQuery; private bool _previousIsHomeQuery; + private Query _progressQuery; // Used for QueryResultAsync + private Query _updateQuery; // Used for ResultsUpdated private string _queryTextBeforeLeaveResults; private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results @@ -282,7 +283,7 @@ public void RegisterResultsUpdatedEvent() var plugin = (IResultUpdated)pair.Plugin; plugin.ResultsUpdated += (s, e) => { - if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested) + if (_updateQuery == null || e.Query.Input != _updateQuery.Input || e.Token.IsCancellationRequested) { return; } @@ -440,7 +441,7 @@ private void LoadContextMenu() [RelayCommand] private void Backspace(object index) { - var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); + var query = QueryBuilder.Build(QueryText, QueryText.Trim(), PluginManager.NonGlobalPlugins); // GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\')); @@ -1357,69 +1358,73 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } - _updateSource?.Dispose(); + try + { + _updateSource?.Dispose(); - var currentUpdateSource = new CancellationTokenSource(); - _updateSource = currentUpdateSource; - var currentCancellationToken = _updateSource.Token; - _updateToken = currentCancellationToken; + var currentUpdateSource = new CancellationTokenSource(); + _updateSource = currentUpdateSource; + var currentCancellationToken = _updateSource.Token; + _updateToken = currentCancellationToken; - ProgressBarVisibility = Visibility.Hidden; - _isQueryRunning = true; + ProgressBarVisibility = Visibility.Hidden; - // Switch to ThreadPool thread - await TaskScheduler.Default; + _progressQuery = query; + _updateQuery = query; - if (currentCancellationToken.IsCancellationRequested) return; + // Switch to ThreadPool thread + await TaskScheduler.Default; - // Update the query's IsReQuery property to true if this is a re-query - query.IsReQuery = isReQuery; + if (currentCancellationToken.IsCancellationRequested) return; - ICollection plugins = Array.Empty(); - if (currentIsHomeQuery) - { - if (Settings.ShowHomePage) - { - plugins = PluginManager.ValidPluginsForHomeQuery(); - } + // Update the query's IsReQuery property to true if this is a re-query + query.IsReQuery = isReQuery; - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; - } - else - { - plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump); - - if (plugins.Count == 1) - { - PluginIconPath = plugins.Single().Metadata.IcoPath; - PluginIconSource = await App.API.LoadImageAsync(PluginIconPath); - SearchIconVisibility = Visibility.Hidden; - } - else + ICollection plugins = Array.Empty(); + if (currentIsHomeQuery) { + if (Settings.ShowHomePage) + { + plugins = PluginManager.ValidPluginsForHomeQuery(); + } + PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; } - } + else + { + plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump); - App.API.LogDebug(ClassName, $"Valid <{plugins.Count}> plugins: {string.Join(" ", plugins.Select(x => $"<{x.Metadata.Name}>"))}"); + if (plugins.Count == 1) + { + PluginIconPath = plugins.Single().Metadata.IcoPath; + PluginIconSource = await App.API.LoadImageAsync(PluginIconPath); + SearchIconVisibility = Visibility.Hidden; + } + else + { + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; + } + } - // Do not wait for performance improvement - /*if (string.IsNullOrEmpty(query.ActionKeyword)) - { - // Wait 15 millisecond for query change in global query - // if query changes, return so that it won't be calculated - await Task.Delay(15, currentCancellationToken); - if (currentCancellationToken.IsCancellationRequested) return; - }*/ + App.API.LogDebug(ClassName, $"Valid <{plugins.Count}> plugins: {string.Join(" ", plugins.Select(x => $"<{x.Metadata.Name}>"))}"); - _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => + // Do not wait for performance improvement + /*if (string.IsNullOrEmpty(query.ActionKeyword)) + { + // Wait 15 millisecond for query change in global query + // if query changes, return so that it won't be calculated + await Task.Delay(15, currentCancellationToken); + if (currentCancellationToken.IsCancellationRequested) return; + }*/ + + _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet - if (_isQueryRunning) + if (_progressQuery != null && _progressQuery.Input == query.Input) { ProgressBarVisibility = Visibility.Visible; } @@ -1428,58 +1433,65 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); - // plugins are ICollection, meaning LINQ will get the Count and preallocate Array + // plugins are ICollection, meaning LINQ will get the Count and preallocate Array - Task[] tasks; - if (currentIsHomeQuery) - { - if (ShouldClearExistingResultsForNonQuery(plugins)) + Task[] tasks; + if (currentIsHomeQuery) { - Results.Clear(); - App.API.LogDebug(ClassName, $"Existing results are cleared for non-query"); - } + if (ShouldClearExistingResultsForNonQuery(plugins)) + { + // there are no update tasks and so we can directly return + ClearResults(); + return; + } + + tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch + { + false => QueryTaskAsync(plugin, currentCancellationToken), + true => Task.CompletedTask + }).ToArray(); - tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch + // Query history results for home page firstly so it will be put on top of the results + if (Settings.ShowHistoryResultsForHomePage) + { + QueryHistoryTask(currentCancellationToken); + } + } + else { - false => QueryTaskAsync(plugin, currentCancellationToken), - true => Task.CompletedTask - }).ToArray(); + tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + { + false => QueryTaskAsync(plugin, currentCancellationToken), + true => Task.CompletedTask + }).ToArray(); + } - // Query history results for home page firstly so it will be put on top of the results - if (Settings.ShowHistoryResultsForHomePage) + try { - QueryHistoryTask(currentCancellationToken); + // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first + await Task.WhenAll(tasks); } - } - else - { - tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + catch (OperationCanceledException) { - false => QueryTaskAsync(plugin, currentCancellationToken), - true => Task.CompletedTask - }).ToArray(); - } - - try - { - // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first - await Task.WhenAll(tasks); - } - catch (OperationCanceledException) - { - // nothing to do here - } + // nothing to do here + } - if (currentCancellationToken.IsCancellationRequested) return; + if (currentCancellationToken.IsCancellationRequested) return; - // this should happen once after all queries are done so progress bar should continue - // until the end of all querying - _isQueryRunning = false; + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _progressQuery = null; - if (!currentCancellationToken.IsCancellationRequested) + if (!currentCancellationToken.IsCancellationRequested) + { + // update to hidden if this is still the current query + ProgressBarVisibility = Visibility.Hidden; + } + } + finally { - // update to hidden if this is still the current query - ProgressBarVisibility = Visibility.Hidden; + // this make sures progress query is null when this query is canceled + _progressQuery = null; } // Local function @@ -1579,7 +1591,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable ConstructQueryAsync(string queryText, IEnumerable builtInShortcuts,