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 all 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
4 changes: 3 additions & 1 deletion Flow.Launcher.Core/Plugin/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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)
{
// home query
if (string.IsNullOrEmpty(text))
{
return new Query()
{
Search = string.Empty,
Input = string.Empty,
RawQuery = string.Empty,
SearchTerms = Array.Empty<string>(),
ActionKeyword = string.Empty,
Expand Down Expand Up @@ -52,6 +53,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
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using NUnit.Framework;
using NUnit.Framework.Legacy;

Check warning on line 3 in Flow.Launcher.Test/QueryBuilderTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;

Expand All @@ -16,16 +16,16 @@
{">", 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);

Check warning on line 19 in Flow.Launcher.Test/QueryBuilderTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`google` is not a recognized word. (unrecognized-spelling)

ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);

Check warning on line 21 in Flow.Launcher.Test/QueryBuilderTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`google` is not a recognized word. (unrecognized-spelling)
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");

Check warning on line 22 in Flow.Launcher.Test/QueryBuilderTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`google` is not a recognized word. (unrecognized-spelling)
ClassicAssert.AreEqual(">", q.ActionKeyword);

ClassicAssert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match.");

ClassicAssert.AreEqual("ping", q.FirstSearch);
ClassicAssert.AreEqual("google.com", q.SecondSearch);

Check warning on line 28 in Flow.Launcher.Test/QueryBuilderTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`google` is not a recognized word. (unrecognized-spelling)
ClassicAssert.AreEqual("-n", q.ThirdSearch);

ClassicAssert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
Expand All @@ -39,7 +39,7 @@
{">", 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 @@
[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 @@ -113,7 +113,7 @@
{
var handle = Win32Helper.GetWindowHandle(this, true);
_hwndSource = HwndSource.FromHwnd(handle);
_hwndSource.AddHook(WndProc);

Check warning on line 116 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this);
}
Expand Down Expand Up @@ -371,7 +371,7 @@
{
try
{
_hwndSource.RemoveHook(WndProc);

Check warning on line 374 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
}
catch (Exception)
{
Expand Down Expand Up @@ -474,7 +474,7 @@
&& 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 Expand Up @@ -584,9 +584,9 @@

#endregion

#region Window WndProc

Check warning on line 587 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)

Check warning on line 589 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
{
switch (msg)
{
Expand Down Expand Up @@ -773,7 +773,7 @@
Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")",
Icon = openIcon
};
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };

Check warning on line 776 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`gamemode` is not a recognized word. (unrecognized-spelling)
var gamemode = new MenuItem
{
Header = App.API.GetTranslation("GameMode"),
Expand Down Expand Up @@ -820,7 +820,7 @@

public void UpdatePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910

Check failure on line 823 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`work around` matches a line_forbidden.patterns entry: `\bwork[- ]arounds?\b`. (forbidden-pattern)
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
Expand All @@ -844,7 +844,7 @@

private void InitializePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910

Check failure on line 847 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`work around` matches a line_forbidden.patterns entry: `\bwork[- ]arounds?\b`. (forbidden-pattern)
InitializePositionInner();
InitializePositionInner();
return;
Expand Down
215 changes: 120 additions & 95 deletions Flow.Launcher/ViewModel/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,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

Expand Down Expand Up @@ -281,7 +282,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 Down Expand Up @@ -309,7 +310,10 @@ 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;
}

App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>");

Expand Down Expand Up @@ -439,7 +443,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 @@ -1328,7 +1332,11 @@ private static List<Result> GetHistoryItems(IEnumerable<HistoryItem> historyItem

private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
{
_updateSource?.Cancel();
if (_updateSource != null)
{
await _updateSource.CancelAsync();
}
_progressQuery = null;

App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>");

Expand All @@ -1340,8 +1348,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b
return;
}

App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>");

var currentIsHomeQuery = query.IsHomeQuery;
var currentIsDialogJump = _isDialogJump;

Expand All @@ -1352,69 +1358,78 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b
return;
}

_updateSource?.Dispose();
try
{
// Check if the input text matches the query text
if (query.Input != QueryText) return;

var currentUpdateSource = new CancellationTokenSource();
_updateSource = currentUpdateSource;
var currentCancellationToken = _updateSource.Token;
_updateToken = currentCancellationToken;
App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>");

ProgressBarVisibility = Visibility.Hidden;
_isQueryRunning = true;
_updateSource?.Dispose();

// Switch to ThreadPool thread
await TaskScheduler.Default;
var currentUpdateSource = new CancellationTokenSource();
_updateSource = currentUpdateSource;
var currentCancellationToken = _updateSource.Token;
_updateToken = currentCancellationToken;

if (currentCancellationToken.IsCancellationRequested) return;
ProgressBarVisibility = Visibility.Hidden;

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

ICollection<PluginPair> plugins = Array.Empty<PluginPair>();
if (currentIsHomeQuery)
{
if (Settings.ShowHomePage)
{
plugins = PluginManager.ValidPluginsForHomeQuery();
}
// Switch to ThreadPool thread
await TaskScheduler.Default;

PluginIconPath = null;
PluginIconSource = null;
SearchIconVisibility = Visibility.Visible;
}
else
{
plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump);
if (currentCancellationToken.IsCancellationRequested) return;

if (plugins.Count == 1)
{
PluginIconPath = plugins.Single().Metadata.IcoPath;
PluginIconSource = await App.API.LoadImageAsync(PluginIconPath);
SearchIconVisibility = Visibility.Hidden;
}
else
// Update the query's IsReQuery property to true if this is a re-query
query.IsReQuery = isReQuery;

ICollection<PluginPair> plugins = Array.Empty<PluginPair>();
if (currentIsHomeQuery)
{
if (Settings.ShowHomePage)
{
plugins = PluginManager.ValidPluginsForHomeQuery();
}

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
{
PluginIconPath = null;
PluginIconSource = null;
SearchIconVisibility = Visibility.Visible;
}
}

App.API.LogDebug(ClassName, $"Valid <{plugins.Count}> plugins: {string.Join(" ", plugins.Select(x => $"<{x.Metadata.Name}>"))}");
App.API.LogDebug(ClassName, $"Valid <{plugins.Count}> plugins: {string.Join(" ", plugins.Select(x => $"<{x.Metadata.Name}>"))}");

// 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;
}*/
// 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(_ =>
_ = 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 == query)
{
ProgressBarVisibility = Visibility.Visible;
}
Expand All @@ -1423,58 +1438,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))
{
// No update tasks and just return
ClearResults();
return;
}

tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch
tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled 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)
{
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 running query is null even if the query is canceled
_progressQuery = null;
}

// Local function
Expand Down Expand Up @@ -1574,7 +1596,7 @@ private async Task<Query> ConstructQueryAsync(string queryText, IEnumerable<Cust
{
if (string.IsNullOrWhiteSpace(queryText))
{
return QueryBuilder.Build(string.Empty, PluginManager.NonGlobalPlugins);
return QueryBuilder.Build(string.Empty, string.Empty, PluginManager.NonGlobalPlugins);
}

var queryBuilder = new StringBuilder(queryText);
Expand All @@ -1594,7 +1616,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 @@ -1907,7 +1929,10 @@ public async Task SetupDialogJumpAsync(nint handle)
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// Cancel the previous Dialog Jump task
_dialogJumpSource?.Cancel();
if (_dialogJumpSource != null)
{
await _dialogJumpSource.CancelAsync();
}

// Create a new cancellation token source
_dialogJumpSource = new CancellationTokenSource();
Expand Down
Loading