Improve and fix query result update logic issue & provide access to exact query typed by user (#3502)

This commit is contained in:
Jack Ye 2025-11-26 18:15:12 +08:00 committed by GitHub
parent 38c0fae064
commit 48f67b1886
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 201 additions and 166 deletions

View file

@ -100,11 +100,11 @@ namespace Flow.Launcher.Core.Plugin
RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API)); RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API));
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((rawQuery, response) => RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((trimmedQuery, response) =>
{ {
var results = ParseResults(response); var results = ParseResults(response);
ResultsUpdated?.Invoke(this, ResultsUpdated?.Invoke(this,
new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results }); new ResultUpdatedEventArgs { Query = new Query() { TrimmedQuery = trimmedQuery }, Results = results });
})); }));
RPC.SynchronizationContext = null; RPC.SynchronizationContext = null;
RPC.StartListening(); RPC.StartListening();

View file

@ -401,7 +401,7 @@ namespace Flow.Launcher.Core.Plugin
{ {
Title = Localize.pluginStillInitializing(metadata.Name), Title = Localize.pluginStillInitializing(metadata.Name),
SubTitle = Localize.pluginStillInitializingSubtitle(), SubTitle = Localize.pluginStillInitializingSubtitle(),
AutoCompleteText = query.RawQuery, AutoCompleteText = query.TrimmedQuery,
IcoPath = metadata.IcoPath, IcoPath = metadata.IcoPath,
PluginDirectory = metadata.PluginDirectory, PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword, ActionKeywordAssigned = query.ActionKeyword,
@ -443,7 +443,7 @@ namespace Flow.Launcher.Core.Plugin
{ {
Title = Localize.pluginFailedToRespond(metadata.Name), Title = Localize.pluginFailedToRespond(metadata.Name),
SubTitle = Localize.pluginFailedToRespondSubtitle(), SubTitle = Localize.pluginFailedToRespondSubtitle(),
AutoCompleteText = query.RawQuery, AutoCompleteText = query.TrimmedQuery,
IcoPath = Constant.ErrorIcon, IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory, PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword, ActionKeywordAssigned = query.ActionKeyword,
@ -467,7 +467,7 @@ namespace Flow.Launcher.Core.Plugin
{ {
Title = Localize.pluginStillInitializing(metadata.Name), Title = Localize.pluginStillInitializing(metadata.Name),
SubTitle = Localize.pluginStillInitializingSubtitle(), SubTitle = Localize.pluginStillInitializingSubtitle(),
AutoCompleteText = query.RawQuery, AutoCompleteText = query.TrimmedQuery,
IcoPath = metadata.IcoPath, IcoPath = metadata.IcoPath,
PluginDirectory = metadata.PluginDirectory, PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword, ActionKeywordAssigned = query.ActionKeyword,

View file

@ -6,15 +6,16 @@ namespace Flow.Launcher.Core.Plugin
{ {
public static class QueryBuilder public static class QueryBuilder
{ {
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins) public static Query Build(string originalQuery, string trimmedQuery, Dictionary<string, PluginPair> nonGlobalPlugins)
{ {
// home query // home query
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(trimmedQuery))
{ {
return new Query() return new Query()
{ {
Search = string.Empty, Search = string.Empty,
RawQuery = string.Empty, OriginalQuery = string.Empty,
TrimmedQuery = string.Empty,
SearchTerms = Array.Empty<string>(), SearchTerms = Array.Empty<string>(),
ActionKeyword = string.Empty, ActionKeyword = string.Empty,
IsHomeQuery = true IsHomeQuery = true
@ -22,14 +23,13 @@ namespace Flow.Launcher.Core.Plugin
} }
// replace multiple white spaces with one white space // replace multiple white spaces with one white space
var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); var terms = trimmedQuery.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
if (terms.Length == 0) if (terms.Length == 0)
{ {
// nothing was typed // nothing was typed
return null; return null;
} }
var rawQuery = text;
string actionKeyword, search; string actionKeyword, search;
string possibleActionKeyword = terms[0]; string possibleActionKeyword = terms[0];
string[] searchTerms; string[] searchTerms;
@ -38,21 +38,22 @@ namespace Flow.Launcher.Core.Plugin
{ {
// use non global plugin for query // use non global plugin for query
actionKeyword = possibleActionKeyword; actionKeyword = possibleActionKeyword;
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty; search = terms.Length > 1 ? trimmedQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
searchTerms = terms[1..]; searchTerms = terms[1..];
} }
else else
{ {
// non action keyword // non action keyword
actionKeyword = string.Empty; actionKeyword = string.Empty;
search = rawQuery.TrimStart(); search = trimmedQuery.TrimStart();
searchTerms = terms; searchTerms = terms;
} }
return new Query() return new Query()
{ {
Search = search, Search = search,
RawQuery = rawQuery, OriginalQuery = originalQuery,
TrimmedQuery = trimmedQuery,
SearchTerms = searchTerms, SearchTerms = searchTerms,
ActionKeyword = actionKeyword, ActionKeyword = actionKeyword,
IsHomeQuery = false IsHomeQuery = false

View file

@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
@ -8,11 +9,29 @@ namespace Flow.Launcher.Plugin
public class Query public class Query
{ {
/// <summary> /// <summary>
/// Raw query, this includes action keyword if it has. /// Original query, exactly how the user has typed into the search box.
/// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace. /// We don't recommend using this property directly. You should always use Search property.
/// We didn't recommend use this property directly. You should always use Search property.
/// </summary> /// </summary>
public string RawQuery { get; internal init; } public string OriginalQuery { get; internal init; }
/// <summary>
/// Raw query, this includes action keyword if it has.
/// It has handled built-in custom query hotkeys and built-in shortcuts, and it trims the whitespace.
/// We don't recommend using this property directly. You should always use Search property.
/// </summary>
[Obsolete("RawQuery is renamed to TrimmedQuery. This property will be removed. Update the code to use TrimmedQuery instead.")]
public string RawQuery {
get => TrimmedQuery;
internal init { TrimmedQuery = value; }
}
/// <summary>
/// Original query but with trimmed whitespace. Includes action keyword.
/// It has handled built-in custom query hotkeys and build-in shortcuts.
/// If you need the exact original query from the search box, use OriginalQuery property instead.
/// We don't recommend using this property directly. You should always use Search property.
/// </summary>
public string TrimmedQuery { get; internal init; }
/// <summary> /// <summary>
/// Determines whether the query was forced to execute again. /// Determines whether the query was forced to execute again.
@ -28,7 +47,7 @@ namespace Flow.Launcher.Plugin
/// <summary> /// <summary>
/// Search part of a query. /// Search part of a query.
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery. /// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as TrimmedQuery.
/// Since we allow user to switch a exclusive plugin to generic plugin, /// Since we allow user to switch a exclusive plugin to generic plugin,
/// so this property will always give you the "real" query part of the query /// so this property will always give you the "real" query part of the query
/// </summary> /// </summary>
@ -103,6 +122,6 @@ namespace Flow.Launcher.Plugin
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => RawQuery; public override string ToString() => TrimmedQuery;
} }
} }

View file

@ -16,9 +16,9 @@ namespace Flow.Launcher.Test
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}}}} {">", 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.TrimmedQuery);
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
ClassicAssert.AreEqual(">", q.ActionKeyword); ClassicAssert.AreEqual(">", q.ActionKeyword);
@ -39,10 +39,10 @@ namespace Flow.Launcher.Test
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}, Disabled = true}}} {">", 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("> ping google.com -n 20 -6", q.Search);
ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); ClassicAssert.AreEqual(q.Search, q.TrimmedQuery, "TrimmedQuery should be equal to Search.");
ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
@ -51,7 +51,7 @@ namespace Flow.Launcher.Test
[Test] [Test]
public void GenericPluginQueryTest() 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("file.txt file2 file3", q.Search);
ClassicAssert.AreEqual("", q.ActionKeyword); ClassicAssert.AreEqual("", q.ActionKeyword);

View file

@ -16,11 +16,11 @@ public static class ResultHelper
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey); return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
} }
public static async Task<Result?> PopulateResultsAsync(string pluginId, string rawQuery, string title, string subTitle, string recordKey) public static async Task<Result?> PopulateResultsAsync(string pluginId, string trimmedQuery, string title, string subTitle, string recordKey)
{ {
var plugin = PluginManager.GetPluginForId(pluginId); var plugin = PluginManager.GetPluginForId(pluginId);
if (plugin == null) return null; if (plugin == null) return null;
var query = QueryBuilder.Build(rawQuery, PluginManager.GetNonGlobalPlugins()); var query = QueryBuilder.Build(trimmedQuery, trimmedQuery, PluginManager.GetNonGlobalPlugins());
if (query == null) return null; if (query == null) return null;
try try
{ {

View file

@ -477,7 +477,7 @@ namespace Flow.Launcher
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{ {
var queryWithoutActionKeyword = var queryWithoutActionKeyword =
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search; QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
{ {

View file

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
@ -9,7 +10,7 @@ namespace Flow.Launcher
{ {
public partial class ResultListBox public partial class ResultListBox
{ {
protected object _lock = new object(); protected Lock _lock = new();
private Point _lastpos; private Point _lastpos;
private ListBoxItem curItem = null; private ListBoxItem curItem = null;
public ResultListBox() public ResultListBox()
@ -88,12 +89,11 @@ namespace Flow.Launcher
} }
} }
private Point _start;
private Point start; private string _path;
private string path; private string _trimmedQuery;
private string query;
// this method is called by the UI thread, which is single threaded, so we can be sloppy with locking // this method is called by the UI thread, which is single threaded, so we can be sloppy with locking
private bool isDragging; private bool _isDragging;
private void ResultList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void ResultList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{ {
@ -104,53 +104,55 @@ namespace Flow.Launcher
Result: Result:
{ {
CopyText: { } copyText, CopyText: { } copyText,
OriginQuery.RawQuery: { } rawQuery OriginQuery.TrimmedQuery: { } trimmedQuery
} }
} }
}) return; }) return;
path = copyText; _path = copyText;
query = rawQuery; _trimmedQuery = trimmedQuery;
start = e.GetPosition(null); _start = e.GetPosition(null);
isDragging = true; _isDragging = true;
} }
private void ResultList_MouseMove(object sender, MouseEventArgs e) private void ResultList_MouseMove(object sender, MouseEventArgs e)
{ {
if (e.LeftButton != MouseButtonState.Pressed || !isDragging) if (e.LeftButton != MouseButtonState.Pressed || !_isDragging)
{ {
start = default; _start = default;
path = string.Empty; _path = string.Empty;
query = string.Empty; _trimmedQuery = string.Empty;
isDragging = false; _isDragging = false;
return; return;
} }
if (!File.Exists(path) && !Directory.Exists(path)) if (!File.Exists(_path) && !Directory.Exists(_path))
return; return;
Point mousePosition = e.GetPosition(null); Point mousePosition = e.GetPosition(null);
Vector diff = this.start - mousePosition; Vector diff = _start - mousePosition;
if (Math.Abs(diff.X) < SystemParameters.MinimumHorizontalDragDistance if (Math.Abs(diff.X) < SystemParameters.MinimumHorizontalDragDistance
|| Math.Abs(diff.Y) < SystemParameters.MinimumVerticalDragDistance) || Math.Abs(diff.Y) < SystemParameters.MinimumVerticalDragDistance)
return; return;
isDragging = false; _isDragging = false;
App.API.HideMainWindow(); App.API.HideMainWindow();
var data = new DataObject(DataFormats.FileDrop, new[] var data = new DataObject(DataFormats.FileDrop, new[]
{ {
path _path
}); });
// Reassigning query to a new variable because for some reason // Reassigning query to a new variable because for some reason
// after DragDrop.DoDragDrop call, 'query' loses its content, i.e. becomes empty string // after DragDrop.DoDragDrop call, 'query' loses its content, i.e. becomes empty string
var rawQuery = query; var trimmedQuery = _trimmedQuery;
var effect = DragDrop.DoDragDrop((DependencyObject)sender, data, DragDropEffects.Move | DragDropEffects.Copy); var effect = DragDrop.DoDragDrop((DependencyObject)sender, data, DragDropEffects.Move | DragDropEffects.Copy);
if (effect == DragDropEffects.Move) if (effect == DragDropEffects.Move)
App.API.ChangeQuery(rawQuery, true); App.API.ChangeQuery(trimmedQuery, true);
} }
private void ResultListBox_OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e) private void ResultListBox_OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{ {
if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result }) if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result })
@ -158,6 +160,7 @@ namespace Flow.Launcher
RightClickResultCommand?.Execute(result.Result); RightClickResultCommand?.Execute(result.Result);
} }
private void ResultListBox_OnPreviewMouseUp(object sender, MouseButtonEventArgs e) private void ResultListBox_OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{ {
if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result }) if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result })

View file

@ -19,13 +19,13 @@ public class LastOpenedHistoryItem
return Title == r.Title return Title == r.Title
&& SubTitle == r.SubTitle && SubTitle == r.SubTitle
&& PluginID == r.PluginID && PluginID == r.PluginID
&& Query == r.OriginQuery.RawQuery; && Query == r.OriginQuery.TrimmedQuery;
} }
else else
{ {
return RecordKey == r.RecordKey return RecordKey == r.RecordKey
&& PluginID == r.PluginID && PluginID == r.PluginID
&& Query == r.OriginQuery.RawQuery; && Query == r.OriginQuery.TrimmedQuery;
} }
} }
} }

View file

@ -35,7 +35,7 @@ namespace Flow.Launcher.Storage
public void Add(Result result) public void Add(Result result)
{ {
if (string.IsNullOrEmpty(result.OriginQuery.RawQuery)) return; if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return;
if (string.IsNullOrEmpty(result.PluginID)) return; if (string.IsNullOrEmpty(result.PluginID)) return;
// Maintain the max history limit // Maintain the max history limit
@ -57,7 +57,7 @@ namespace Flow.Launcher.Storage
Title = result.Title, Title = result.Title,
SubTitle = result.SubTitle, SubTitle = result.SubTitle,
PluginID = result.PluginID, PluginID = result.PluginID,
Query = result.OriginQuery.RawQuery, Query = result.OriginQuery.TrimmedQuery,
RecordKey = result.RecordKey, RecordKey = result.RecordKey,
ExecutedDateTime = DateTime.Now ExecutedDateTime = DateTime.Now
}); });

View file

@ -113,7 +113,7 @@ namespace Flow.Launcher.Storage
internal bool IsTopMost(Result result) internal bool IsTopMost(Result result)
{ {
if (records.IsEmpty || !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) if (records.IsEmpty || !records.TryGetValue(result.OriginQuery.TrimmedQuery, out var value))
{ {
return false; return false;
} }
@ -124,7 +124,7 @@ namespace Flow.Launcher.Storage
internal void Remove(Result result) internal void Remove(Result result)
{ {
records.Remove(result.OriginQuery.RawQuery, out _); records.Remove(result.OriginQuery.TrimmedQuery, out _);
} }
internal void AddOrUpdate(Result result) internal void AddOrUpdate(Result result)
@ -136,7 +136,7 @@ namespace Flow.Launcher.Storage
SubTitle = result.SubTitle, SubTitle = result.SubTitle,
RecordKey = result.RecordKey RecordKey = result.RecordKey
}; };
records.AddOrUpdate(result.OriginQuery.RawQuery, record, (key, oldValue) => record); records.AddOrUpdate(result.OriginQuery.TrimmedQuery, record, (key, oldValue) => record);
} }
} }
@ -154,7 +154,7 @@ namespace Flow.Launcher.Storage
// origin query is null when user select the context menu item directly of one item from query list // origin query is null when user select the context menu item directly of one item from query list
// in this case, we do not need to check if the result is top most // in this case, we do not need to check if the result is top most
if (records.IsEmpty || result.OriginQuery == null || if (records.IsEmpty || result.OriginQuery == null ||
!records.TryGetValue(result.OriginQuery.RawQuery, out var value)) !records.TryGetValue(result.OriginQuery.TrimmedQuery, out var value))
{ {
return false; return false;
} }
@ -168,7 +168,7 @@ namespace Flow.Launcher.Storage
// origin query is null when user select the context menu item directly of one item from query list // origin query is null when user select the context menu item directly of one item from query list
// in this case, we do not need to check if the result is top most // in this case, we do not need to check if the result is top most
if (records.IsEmpty || result.OriginQuery == null || if (records.IsEmpty || result.OriginQuery == null ||
!records.TryGetValue(result.OriginQuery.RawQuery, out var value)) !records.TryGetValue(result.OriginQuery.TrimmedQuery, out var value))
{ {
return -1; return -1;
} }
@ -194,7 +194,7 @@ namespace Flow.Launcher.Storage
// origin query is null when user select the context menu item directly of one item from query list // origin query is null when user select the context menu item directly of one item from query list
// in this case, we do not need to remove the record // in this case, we do not need to remove the record
if (result.OriginQuery == null || if (result.OriginQuery == null ||
!records.TryGetValue(result.OriginQuery.RawQuery, out var value)) !records.TryGetValue(result.OriginQuery.TrimmedQuery, out var value))
{ {
return; return;
} }
@ -204,12 +204,12 @@ namespace Flow.Launcher.Storage
if (queue.IsEmpty) if (queue.IsEmpty)
{ {
// if the queue is empty, remove the queue from the dictionary // if the queue is empty, remove the queue from the dictionary
records.TryRemove(result.OriginQuery.RawQuery, out _); records.TryRemove(result.OriginQuery.TrimmedQuery, out _);
} }
else else
{ {
// change the queue in the dictionary // change the queue in the dictionary
records[result.OriginQuery.RawQuery] = queue; records[result.OriginQuery.TrimmedQuery] = queue;
} }
} }
@ -229,19 +229,19 @@ namespace Flow.Launcher.Storage
SubTitle = result.SubTitle, SubTitle = result.SubTitle,
RecordKey = result.RecordKey RecordKey = result.RecordKey
}; };
if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value)) if (!records.TryGetValue(result.OriginQuery.TrimmedQuery, out var value))
{ {
// create a new queue if it does not exist // create a new queue if it does not exist
value = new ConcurrentQueue<Record>(); value = new ConcurrentQueue<Record>();
value.Enqueue(record); value.Enqueue(record);
records.TryAdd(result.OriginQuery.RawQuery, value); records.TryAdd(result.OriginQuery.TrimmedQuery, value);
} }
else else
{ {
// add or update the record in the queue // add or update the record in the queue
var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates
queue.Enqueue(record); queue.Enqueue(record);
records[result.OriginQuery.RawQuery] = queue; records[result.OriginQuery.TrimmedQuery] = queue;
} }
} }
} }

View file

@ -35,9 +35,10 @@ namespace Flow.Launcher.ViewModel
private static readonly string ClassName = nameof(MainViewModel); private static readonly string ClassName = nameof(MainViewModel);
private bool _isQueryRunning;
private Query _lastQuery; private Query _lastQuery;
private bool _previousIsHomeQuery; private bool _previousIsHomeQuery;
private Query _progressQuery; // Used for QueryResultAsync
private Query _updateQuery; // Used for ResultsUpdated
private string _queryTextBeforeLeaveResults; private string _queryTextBeforeLeaveResults;
private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results
@ -283,7 +284,7 @@ namespace Flow.Launcher.ViewModel
plugin.ResultsUpdated += (s, e) => plugin.ResultsUpdated += (s, e) =>
{ {
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested) if (_updateQuery == null || e.Query.OriginalQuery != _updateQuery.OriginalQuery || e.Token.IsCancellationRequested)
{ {
return; return;
} }
@ -442,7 +443,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void Backspace(object index) private void Backspace(object index)
{ {
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.GetNonGlobalPlugins()); var query = QueryBuilder.Build(QueryText, QueryText.Trim(), PluginManager.GetNonGlobalPlugins());
// GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string // GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string
var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\')); var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\'));
@ -1327,7 +1328,7 @@ namespace Flow.Launcher.ViewModel
Title = Localize.executeQuery(h.Query), Title = Localize.executeQuery(h.Query),
SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
IcoPath = Constant.HistoryIcon, IcoPath = Constant.HistoryIcon,
OriginQuery = new Query { RawQuery = h.Query }, OriginQuery = new Query { TrimmedQuery = h.Query },
Action = _ => Action = _ =>
{ {
App.API.BackToQueryResults(); App.API.BackToQueryResults();
@ -1350,7 +1351,7 @@ namespace Flow.Launcher.ViewModel
h.Title, h.Title,
SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
IcoPath = Constant.HistoryIcon, IcoPath = Constant.HistoryIcon,
OriginQuery = new Query { RawQuery = h.Query }, OriginQuery = new Query { TrimmedQuery = h.Query },
AsyncAction = async c => AsyncAction = async c =>
{ {
var reflectResult = await ResultHelper.PopulateResultsAsync(h); var reflectResult = await ResultHelper.PopulateResultsAsync(h);
@ -1391,7 +1392,7 @@ namespace Flow.Launcher.ViewModel
return; return;
} }
App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>"); App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and TrimmedQuery <{query.TrimmedQuery}>");
var currentIsHomeQuery = query.IsHomeQuery; var currentIsHomeQuery = query.IsHomeQuery;
var currentIsDialogJump = _isDialogJump; var currentIsDialogJump = _isDialogJump;
@ -1403,69 +1404,73 @@ namespace Flow.Launcher.ViewModel
return; return;
} }
_updateSource?.Dispose(); try
var currentUpdateSource = new CancellationTokenSource();
_updateSource = currentUpdateSource;
var currentCancellationToken = _updateSource.Token;
_updateToken = currentCancellationToken;
ProgressBarVisibility = Visibility.Hidden;
_isQueryRunning = true;
// Switch to ThreadPool thread
await TaskScheduler.Default;
if (currentCancellationToken.IsCancellationRequested) return;
// 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) _updateSource?.Dispose();
{
plugins = PluginManager.ValidPluginsForHomeQuery();
}
PluginIconPath = null; var currentUpdateSource = new CancellationTokenSource();
PluginIconSource = null; _updateSource = currentUpdateSource;
SearchIconVisibility = Visibility.Visible; var currentCancellationToken = _updateSource.Token;
} _updateToken = currentCancellationToken;
else
{
plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump);
if (plugins.Count == 1) ProgressBarVisibility = Visibility.Hidden;
{
PluginIconPath = plugins.Single().Metadata.IcoPath; _progressQuery = query;
PluginIconSource = await App.API.LoadImageAsync(PluginIconPath); _updateQuery = query;
SearchIconVisibility = Visibility.Hidden;
} // Switch to ThreadPool thread
else await TaskScheduler.Default;
if (currentCancellationToken.IsCancellationRequested) return;
// 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; PluginIconPath = null;
PluginIconSource = null; PluginIconSource = null;
SearchIconVisibility = Visibility.Visible; 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 App.API.LogDebug(ClassName, $"Valid <{plugins.Count}> plugins: {string.Join(" ", plugins.Select(x => $"<{x.Metadata.Name}>"))}");
/*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(_ => // 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 // 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.OriginalQuery == query.OriginalQuery)
{ {
ProgressBarVisibility = Visibility.Visible; ProgressBarVisibility = Visibility.Visible;
} }
@ -1474,58 +1479,65 @@ namespace Flow.Launcher.ViewModel
TaskContinuationOptions.NotOnCanceled, TaskContinuationOptions.NotOnCanceled,
TaskScheduler.Default); 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; Task[] tasks;
if (currentIsHomeQuery) if (currentIsHomeQuery)
{
if (ShouldClearExistingResultsForNonQuery(plugins))
{ {
Results.Clear(); if (ShouldClearExistingResultsForNonQuery(plugins))
App.API.LogDebug(ClassName, $"Existing results are cleared for non-query"); {
// 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();
// Query history results for home page firstly so it will be put on top of the results
if (Settings.ShowHistoryResultsForHomePage)
{
QueryHistoryTask(currentCancellationToken);
}
}
else
{
tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
{
false => QueryTaskAsync(plugin, currentCancellationToken),
true => Task.CompletedTask
}).ToArray();
} }
tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch try
{ {
false => QueryTaskAsync(plugin, currentCancellationToken), // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first
true => Task.CompletedTask await Task.WhenAll(tasks);
}).ToArray(); }
catch (OperationCanceledException)
{
// nothing to do here
}
// Query history results for home page firstly so it will be put on top of the results if (currentCancellationToken.IsCancellationRequested) return;
if (Settings.ShowHistoryResultsForHomePage)
// 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)
{ {
QueryHistoryTask(currentCancellationToken); // update to hidden if this is still the current query
ProgressBarVisibility = Visibility.Hidden;
} }
} }
else finally
{ {
tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch // this make sures progress query is null when this query is canceled
{ _progressQuery = null;
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
}
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;
if (!currentCancellationToken.IsCancellationRequested)
{
// update to hidden if this is still the current query
ProgressBarVisibility = Visibility.Hidden;
} }
// Local function // Local function
@ -1625,7 +1637,7 @@ namespace Flow.Launcher.ViewModel
{ {
if (string.IsNullOrWhiteSpace(queryText)) if (string.IsNullOrWhiteSpace(queryText))
{ {
return QueryBuilder.Build(string.Empty, PluginManager.GetNonGlobalPlugins()); return QueryBuilder.Build(string.Empty, string.Empty, PluginManager.GetNonGlobalPlugins());
} }
var queryBuilder = new StringBuilder(queryText); var queryBuilder = new StringBuilder(queryText);
@ -1645,7 +1657,7 @@ namespace Flow.Launcher.ViewModel
// Applying builtin shortcuts // Applying builtin shortcuts
await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp); await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp);
return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.GetNonGlobalPlugins()); return QueryBuilder.Build(queryText, queryBuilder.ToString().Trim(), PluginManager.GetNonGlobalPlugins());
} }
private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts, private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts,

View file

@ -172,7 +172,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Action = c => Action = c =>
{ {
Settings.EnableEverythingContentSearch = true; Settings.EnableEverythingContentSearch = true;
Context.API.ChangeQuery(query.RawQuery, true); Context.API.ChangeQuery(query.TrimmedQuery, true);
return false; return false;
} }
} }