Skip to content

Improve update logic & Fix update logic issue & Input for Query #3502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
34c3cda
Improve update logic
Jack251970 May 2, 2025
381334f
Improve variable names
Jack251970 May 2, 2025
1381748
Merge branch 'dev' into improve_update
Jack251970 May 3, 2025
a2a4b5a
Merge branch 'dev' into improve_update
Jack251970 May 5, 2025
77b8749
Revert error change
Jack251970 May 5, 2025
07bcd5d
Merge branch 'dev' into improve_update
Jack251970 May 5, 2025
cf3fc6a
Fix build issue & Adjust indent
Jack251970 May 5, 2025
51714d5
Add Input for home query
Jack251970 May 6, 2025
ece9b96
Merge branch 'dev' into improve_update
Jack251970 May 7, 2025
8c48cbe
Use currentCancellationToken instead
Jack251970 May 7, 2025
4c5b5fb
Improve code quality
Jack251970 May 7, 2025
f599359
Improve code quality
Jack251970 May 7, 2025
16dc921
Merge branch 'dev' into improve_update
Jack251970 May 10, 2025
8450e68
Remove async
Jack251970 May 10, 2025
a47d8fe
Merge branch 'dev' into improve_update
Jack251970 May 10, 2025
bde7463
Fix build issue
Jack251970 May 10, 2025
23a2f88
Merge branch 'dev' into improve_update
Jack251970 May 14, 2025
8670461
Merge branch 'dev' into improve_update
Jack251970 May 19, 2025
7c12956
Clear results when there are no update tasks
Jack251970 May 19, 2025
a1df6a1
Merge branch 'dev' into improve_update
Jack251970 Jun 3, 2025
3bf6008
Update code comments
Jack251970 Jun 3, 2025
3786130
Merge branch 'dev' into improve_update
Jack251970 Jun 28, 2025
84e0193
Merge branch 'dev' into improve_update
Jack251970 Jul 5, 2025
2229db0
Merge branch 'dev' into improve_update
Jack251970 Jul 14, 2025
9136b19
Merge branch 'dev' into improve_update
Jack251970 Jul 19, 2025
81429d7
Merge branch 'dev' into improve_update
Jack251970 Jul 20, 2025
27ea2e4
Improve code quality
Jack251970 Jul 20, 2025
1c0c9e7
Await cancel async
Jack251970 Jul 21, 2025
5493e5c
Fix null exception
Jack251970 Jul 21, 2025
a545a40
Merge branch 'dev' into improve_update
Jack251970 Jul 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Flow.Launcher.Core/Plugin/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ namespace Flow.Launcher.Core.Plugin
{
public static class QueryBuilder
{
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins)
public static Query Build(string input, string text, Dictionary<string, PluginPair> nonGlobalPlugins)
{
// replace multiple white spaces with one white space
var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
if (terms.Length == 0)
{ // nothing was typed
{
// nothing was typed
return null;
}

Expand All @@ -21,13 +22,15 @@ public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalP
string[] searchTerms;

if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled)
{ // use non global plugin for query
{
// use non global plugin for query
actionKeyword = possibleActionKeyword;
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
searchTerms = terms[1..];
}
else
{ // non action keyword
{
// non action keyword
actionKeyword = string.Empty;
search = rawQuery.TrimStart();
searchTerms = terms;
Expand All @@ -36,6 +39,7 @@ public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalP
return new Query()
{
Search = search,
Input = input,
RawQuery = rawQuery,
SearchTerms = searchTerms,
ActionKeyword = actionKeyword
Expand Down
6 changes: 6 additions & 0 deletions Flow.Launcher.Plugin/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ namespace Flow.Launcher.Plugin
/// </summary>
public class Query
{
/// <summary>
/// Input text in query box.
/// We didn't recommend use this property directly. You should always use Search property.
/// </summary>
public string Input { get; internal init; }

/// <summary>
/// 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.
Expand Down
6 changes: 3 additions & 3 deletions Flow.Launcher.Test/QueryBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public void ExclusivePluginQueryTest()
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}}}}
};

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.");
Expand All @@ -39,7 +39,7 @@ public void ExclusivePluginQueryIgnoreDisabledTest()
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}, 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.");
Expand All @@ -51,7 +51,7 @@ public void ExclusivePluginQueryIgnoreDisabledTest()
[Test]
public void GenericPluginQueryTest()
{
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary<string, PluginPair>());
Query q = QueryBuilder.Build("file.txt file2 file3", "file.txt file2 file3", new Dictionary<string, PluginPair>());

ClassicAssert.AreEqual("file.txt file2 file3", q.Search);
ClassicAssert.AreEqual("", q.ActionKeyword);
Expand Down
2 changes: 1 addition & 1 deletion Flow.Launcher/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,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))
{
Expand Down
158 changes: 87 additions & 71 deletions Flow.Launcher/ViewModel/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable

private static readonly string ClassName = nameof(MainViewModel);

private bool _isQueryRunning;
private Query _lastQuery;
private Query _progressQuery; // Used for QueryResultAsync
private Query _updateQuery; // Used for ResultsUpdated
private string _queryTextBeforeLeaveResults;
private string _ignoredQueryText = null;

Expand Down Expand Up @@ -236,7 +237,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.RawQuery != _updateQuery.RawQuery || e.Token.IsCancellationRequested)
{
return;
}
Expand All @@ -256,9 +257,12 @@ public void RegisterResultsUpdatedEvent()

PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);

if (token.IsCancellationRequested) return;
if (_updateQuery == null || e.Query.RawQuery != _updateQuery.RawQuery || token.IsCancellationRequested)
{
return;
}

if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
token)))
{
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
Expand Down Expand Up @@ -366,7 +370,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('\\'));
Expand Down Expand Up @@ -790,7 +794,7 @@ private ResultsViewModel SelectedResults

public Visibility ProgressBarVisibility { get; set; }
public Visibility MainWindowVisibility { get; set; }

// This is to be used for determining the visibility status of the main window instead of MainWindowVisibility
// because it is more accurate and reliable representation than using Visibility as a condition check
public bool MainWindowVisibilityStatus { get; set; } = true;
Expand Down Expand Up @@ -1067,7 +1071,7 @@ private bool CanExternalPreviewSelectedResult(out string path)
path = QueryResultsPreviewed() ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty;
return !string.IsNullOrEmpty(path);
}

private bool QueryResultsPreviewed()
{
var previewed = PreviewSelectedItem == Results.SelectedItem;
Expand Down Expand Up @@ -1214,6 +1218,7 @@ private void QueryHistory()
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
{
_updateSource?.Cancel();
_progressQuery = null;

var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts);

Expand All @@ -1233,89 +1238,103 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b
return;
}

_updateSource = new CancellationTokenSource();
try
{
// Check if the query has changed because query can be changed so fast that
// token of the query between two queries has not been created yet
if (query.Input != QueryText) return;

ProgressBarVisibility = Visibility.Hidden;
_isQueryRunning = true;
_updateSource = new CancellationTokenSource();

// Switch to ThreadPool thread
await TaskScheduler.Default;
ProgressBarVisibility = Visibility.Hidden;

if (_updateSource.Token.IsCancellationRequested) return;
_progressQuery = query;
_updateQuery = query;

// Update the query's IsReQuery property to true if this is a re-query
query.IsReQuery = isReQuery;
// Switch to ThreadPool thread
await TaskScheduler.Default;

// handle the exclusiveness of plugin using action keyword
RemoveOldQueryResults(query);
if (_updateSource.Token.IsCancellationRequested) return;

_lastQuery = query;
// Update the query's IsReQuery property to true if this is a re-query
query.IsReQuery = isReQuery;

var plugins = PluginManager.ValidPluginsForQuery(query);
// handle the exclusiveness of plugin using action keyword
RemoveOldQueryResults(query);

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;
}
_lastQuery = query;

// 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, _updateSource.Token);
if (_updateSource.Token.IsCancellationRequested)
return;
}*/
var plugins = PluginManager.ValidPluginsForQuery(query);

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, _updateSource.Token);
if (_updateSource.Token.IsCancellationRequested)
return;
}*/

_ = Task.Delay(200, _updateSource.Token).ContinueWith(_ =>
_ = Task.Delay(200, _updateSource.Token).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 == query)
{
ProgressBarVisibility = Visibility.Visible;
}
},
_updateSource.Token,
TaskContinuationOptions.NotOnCanceled,
TaskScheduler.Default);
_updateSource.Token,
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

var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
{
false => QueryTaskAsync(plugin, _updateSource.Token),
true => Task.CompletedTask
}).ToArray();
var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
{
false => QueryTaskAsync(plugin, _updateSource.Token),
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
}
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
}

if (_updateSource.Token.IsCancellationRequested) return;
if (_updateSource.Token.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 (!_updateSource.Token.IsCancellationRequested)
if (!_updateSource.Token.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 running query is null even if the query is canceled
_progressQuery = null;
}

// Local function
Expand Down Expand Up @@ -1392,7 +1411,7 @@ private async Task<Query> ConstructQueryAsync(string queryText, IEnumerable<Cust
// Applying builtin shortcuts
await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp);

return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
return QueryBuilder.Build(QueryText, queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
}

private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts,
Expand Down Expand Up @@ -1570,9 +1589,6 @@ public bool ShouldIgnoreHotkeys()

public void Show()
{
// When application is exiting, we should not show the main window
if (App.Exiting) return;

// When application is exiting, the Application.Current will be null
Application.Current?.Dispatcher.Invoke(() =>
{
Expand Down
Loading