From 9f8e8295d0c45d512198f8785a8dc78a499b14c8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Mar 2025 12:01:29 +0800 Subject: [PATCH 01/79] Delete CopyToClipboard function in JsonRPC api --- .../Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs index 8df2ce9ed9e..de90419ee5b 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -34,11 +34,6 @@ public void ShellRun(string cmd, string filename = "cmd.exe") _api.ShellRun(cmd, filename); } - public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true) - { - _api.CopyToClipboard(text, directCopy, showDefaultNotification); - } - public void SaveAppAllSettings() { _api.SaveAppAllSettings(); From d7a29e5ee275cd2dc7ebf4ba90816a91ebf2fd2a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Mar 2025 15:10:01 +0800 Subject: [PATCH 02/79] Revert "Delete CopyToClipboard function in JsonRPC api" This reverts commit 9f8e8295d0c45d512198f8785a8dc78a499b14c8. --- .../Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs index de90419ee5b..8df2ce9ed9e 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -34,6 +34,11 @@ public void ShellRun(string cmd, string filename = "cmd.exe") _api.ShellRun(cmd, filename); } + public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true) + { + _api.CopyToClipboard(text, directCopy, showDefaultNotification); + } + public void SaveAppAllSettings() { _api.SaveAppAllSettings(); From e5ad777bd23497364d8ffe84a933eadcab03c949 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Mar 2025 16:25:42 +0800 Subject: [PATCH 03/79] Fix copy to clipboard STA thread issue --- .../NativeMethods.txt | 5 +- .../UserSettings/Settings.cs | 2 +- Flow.Launcher.Infrastructure/Win32Helper.cs | 75 +++++++++++++++++ Flow.Launcher/Languages/en.xaml | 1 + Flow.Launcher/PublicAPIInstance.cs | 80 ++++++++++++++++--- 5 files changed, 148 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index f117534a1ff..d8777ff277c 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -16,4 +16,7 @@ WM_KEYUP WM_SYSKEYDOWN WM_SYSKEYUP -EnumWindows \ No newline at end of file +EnumWindows + +OleInitialize +OleUninitialize \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 0fb6e2b3cbd..01a0da8f4e9 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -248,7 +248,7 @@ public SearchPrecisionScore QuerySearchPrecision [JsonIgnore] public ObservableCollection BuiltinShortcuts { get; set; } = new() { - new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), + new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText).Result), new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) }; diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 867fef4f509..fbcb76c233a 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -2,6 +2,9 @@ using System.Runtime.InteropServices; using System.Windows.Interop; using System.Windows; +using System.Threading.Tasks; +using System.Threading; +using Windows.Win32; namespace Flow.Launcher.Infrastructure { @@ -97,5 +100,77 @@ private static void SetWindowAccent(Window w, AccentState state) Marshal.FreeHGlobal(accentPtr); } #endregion + + #region STA Thread + + /* + Found on https://github.com/files-community/Files + */ + + public static Task StartSTATaskAsync(Action action) + { + var taskCompletionSource = new TaskCompletionSource(); + Thread thread = new(() => + { + PInvoke.OleInitialize(); + + try + { + action(); + taskCompletionSource.SetResult(); + } + catch (System.Exception) + { + taskCompletionSource.SetResult(); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return taskCompletionSource.Task; + } + + public static Task StartSTATaskAsync(Func func) + { + var taskCompletionSource = new TaskCompletionSource(); + + Thread thread = new(() => + { + PInvoke.OleInitialize(); + + try + { + taskCompletionSource.SetResult(func()); + } + catch (System.Exception) + { + taskCompletionSource.SetResult(default); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return taskCompletionSource.Task; + } + + #endregion } } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 058a51ae3fb..a460b232d7a 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -336,6 +336,7 @@ This new Action Keyword is already assigned to another plugin, please choose a different one Success Completed successfully + Failed to copy Enter the action keyword you like to use to start the plugin. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords. diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index ac22170aeeb..91c6bf018cb 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -118,37 +118,91 @@ public void ShellRun(string cmd, string filename = "cmd.exe") ShellCommand.Execute(startInfo); } - public void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true) + public async void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true) { if (string.IsNullOrEmpty(stringToCopy)) + { return; + } var isFile = File.Exists(stringToCopy); if (directCopy && (isFile || Directory.Exists(stringToCopy))) { - var paths = new StringCollection + // Sometimes the clipboard is locked and cannot be accessed, + // we need to retry a few times before giving up + var exception = await RetryActionOnSTAThreadAsync(() => + { + var paths = new StringCollection { stringToCopy }; - Clipboard.SetFileDropList(paths); - - if (showDefaultNotification) - ShowMsg( - $"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}", - GetTranslation("completedSuccessfully")); + Clipboard.SetFileDropList(paths); + }); + + if (exception == null) + { + if (showDefaultNotification) + { + ShowMsg( + $"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}", + GetTranslation("completedSuccessfully")); + } + } + else + { + LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception); + ShowMsgError(GetTranslation("failedToCopy")); + } } else { - Clipboard.SetDataObject(stringToCopy); + // Sometimes the clipboard is locked and cannot be accessed, + // we need to retry a few times before giving up + var exception = await RetryActionOnSTAThreadAsync(() => + { + // We shouold use SetText instead of SetDataObject to avoid the clipboard being locked by other applications + Clipboard.SetText(stringToCopy); + }); - if (showDefaultNotification) - ShowMsg( - $"{GetTranslation("copy")} {GetTranslation("textTitle")}", - GetTranslation("completedSuccessfully")); + if (exception == null) + { + if (showDefaultNotification) + { + ShowMsg( + $"{GetTranslation("copy")} {GetTranslation("textTitle")}", + GetTranslation("completedSuccessfully")); + } + } + else + { + LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception); + ShowMsgError(GetTranslation("failedToCopy")); + } } } + private static async Task RetryActionOnSTAThreadAsync(Action action, int retryCount = 6, int retryDelay = 150) + { + for (var i = 0; i < retryCount; i++) + { + try + { + await Win32Helper.StartSTATaskAsync(action); + break; + } + catch (Exception e) + { + if (i == retryCount - 1) + { + return e; + } + await Task.Delay(retryDelay); + } + } + return null; + } + public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible; public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; From b034f0d30801313b227b829c53040f373b5f2f96 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Fri, 21 Mar 2025 07:20:05 +0800 Subject: [PATCH 04/79] Update Flow.Launcher.Infrastructure/Win32Helper.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Flow.Launcher.Infrastructure/Win32Helper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index bfa9490fabb..28beed55865 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -370,9 +370,9 @@ public static Task StartSTATaskAsync(Func func) { taskCompletionSource.SetResult(func()); } - catch (System.Exception) + catch (System.Exception ex) { - taskCompletionSource.SetResult(default); + taskCompletionSource.SetException(ex); } finally { From c98c41976f8a5a6cd55b4e34262cac3c978b6790 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Fri, 21 Mar 2025 07:20:24 +0800 Subject: [PATCH 05/79] Update Flow.Launcher.Infrastructure/Win32Helper.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Flow.Launcher.Infrastructure/Win32Helper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 28beed55865..cf579c1d209 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -338,9 +338,9 @@ public static Task StartSTATaskAsync(Action action) action(); taskCompletionSource.SetResult(); } - catch (System.Exception) + catch (System.Exception ex) { - taskCompletionSource.SetResult(); + taskCompletionSource.SetException(ex); } finally { From e0a651c889b6705b38cecafb1f7d3090a5bfc86a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 30 Mar 2025 10:23:46 +0800 Subject: [PATCH 06/79] Add async buildin shortcut model --- .../UserSettings/CustomShortcutModel.cs | 67 +++++++++++++++---- .../UserSettings/Settings.cs | 4 +- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs index 71020369a60..725cff046f5 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs @@ -1,15 +1,15 @@ using System; using System.Text.Json.Serialization; +using System.Threading.Tasks; namespace Flow.Launcher.Infrastructure.UserSettings { + #region Base + public abstract class ShortcutBaseModel { public string Key { get; set; } - [JsonIgnore] - public Func Expand { get; set; } = () => { return ""; }; - public override bool Equals(object obj) { return obj is ShortcutBaseModel other && @@ -22,16 +22,14 @@ public override int GetHashCode() } } - public class CustomShortcutModel : ShortcutBaseModel + public class BaseCustomShortcutModel : ShortcutBaseModel { public string Value { get; set; } - [JsonConstructorAttribute] - public CustomShortcutModel(string key, string value) + public BaseCustomShortcutModel(string key, string value) { Key = key; Value = value; - Expand = () => { return Value; }; } public void Deconstruct(out string key, out string value) @@ -40,26 +38,69 @@ public void Deconstruct(out string key, out string value) value = Value; } - public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut) + public static implicit operator (string Key, string Value)(BaseCustomShortcutModel shortcut) { return (shortcut.Key, shortcut.Value); } - public static implicit operator CustomShortcutModel((string Key, string Value) shortcut) + public static implicit operator BaseCustomShortcutModel((string Key, string Value) shortcut) { - return new CustomShortcutModel(shortcut.Key, shortcut.Value); + return new BaseCustomShortcutModel(shortcut.Key, shortcut.Value); } } - public class BuiltinShortcutModel : ShortcutBaseModel + public class BaseBuiltinShortcutModel : ShortcutBaseModel { public string Description { get; set; } - public BuiltinShortcutModel(string key, string description, Func expand) + public BaseBuiltinShortcutModel(string key, string description) { Key = key; Description = description; - Expand = expand ?? (() => { return ""; }); } } + + #endregion + + #region Custom Shortcut + + public class CustomShortcutModel : BaseCustomShortcutModel + { + [JsonIgnore] + public Func Expand { get; set; } = () => { return string.Empty; }; + + [JsonConstructor] + public CustomShortcutModel(string key, string value) : base(key, value) + { + Expand = () => { return Value; }; + } + } + + #endregion + + #region Buildin Shortcut + + public class BuiltinShortcutModel : BaseBuiltinShortcutModel + { + [JsonIgnore] + public Func Expand { get; set; } = () => { return string.Empty; }; + + public BuiltinShortcutModel(string key, string description, Func expand) : base(key, description) + { + Expand = expand ?? (() => { return string.Empty; }); + } + } + + public class AsyncBuiltinShortcutModel : BaseBuiltinShortcutModel + { + [JsonIgnore] + public Func> ExpandAsync { get; set; } = () => { return Task.FromResult(string.Empty); }; + + public AsyncBuiltinShortcutModel(string key, string description, Func> expandAsync) : base(key, description) + { + ExpandAsync = expandAsync ?? (() => { return Task.FromResult(string.Empty); }); + } + } + + #endregion } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 795f23005fc..259a90b2e68 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -295,9 +295,9 @@ public bool KeepMaxResults public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection(); [JsonIgnore] - public ObservableCollection BuiltinShortcuts { get; set; } = new() + public ObservableCollection BuiltinShortcuts { get; set; } = new() { - new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText).Result), + new AsyncBuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText)), new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) }; From a49c4652dba2de8ab4010e5e24aeacddb45d0eb7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 30 Mar 2025 10:27:29 +0800 Subject: [PATCH 07/79] Add async buildin shortcuts in mainvm --- Flow.Launcher/ViewModel/MainViewModel.cs | 62 ++++++++++++++---------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 56de70b4785..cc793c14527 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1155,7 +1155,7 @@ private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = tru { _updateSource?.Cancel(); - var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); + var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); if (query == null) // shortcut expanded { @@ -1284,8 +1284,8 @@ async Task QueryTaskAsync(PluginPair plugin, bool reSelect = true) } } - private Query ConstructQuery(string queryText, IEnumerable customShortcuts, - IEnumerable builtInShortcuts) + private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, + IEnumerable builtInShortcuts) { if (string.IsNullOrWhiteSpace(queryText)) { @@ -1306,36 +1306,48 @@ private Query ConstructQuery(string queryText, IEnumerable queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand()); } - string customExpanded = queryBuilder.ToString(); + var customExpanded = queryBuilder.ToString(); + var queryChanged = false; - Application.Current.Dispatcher.Invoke(() => + foreach (var shortcut in builtInShortcuts) { - foreach (var shortcut in builtInShortcuts) + string expansion; + if (shortcut is BuiltinShortcutModel syncShortcut) { - try - { - if (customExpanded.Contains(shortcut.Key)) - { - var expansion = shortcut.Expand(); - queryBuilder.Replace(shortcut.Key, expansion); - queryBuilderTmp.Replace(shortcut.Key, expansion); - } - } - catch (Exception e) + expansion = syncShortcut.Expand(); + } + else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) + { + expansion = await asyncShortcut.ExpandAsync(); + } + else + { + continue; + } + try + { + if (customExpanded.Contains(shortcut.Key)) { - Log.Exception( - $"{nameof(MainViewModel)}.{nameof(ConstructQuery)}|Error when expanding shortcut {shortcut.Key}", - e); + queryBuilder.Replace(shortcut.Key, expansion); + queryBuilderTmp.Replace(shortcut.Key, expansion); + queryChanged = true; } } - }); + catch (Exception e) + { + App.API.LogException(nameof(MainViewModel), $"Error when expanding shortcut {shortcut.Key}", e); + } + } - // show expanded builtin shortcuts - // use private field to avoid infinite recursion - _queryText = queryBuilderTmp.ToString(); + if (queryChanged) + { + // show expanded builtin shortcuts + // use private field to avoid infinite recursion + _queryText = queryBuilderTmp.ToString(); + OnPropertyChanged(nameof(QueryText)); + } - var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); - return query; + return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); } private void RemoveOldQueryResults(Query query) From 9897213f0aa6028b73224c8ce6faf256581c23c8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 30 Mar 2025 10:30:09 +0800 Subject: [PATCH 08/79] Improve log --- Flow.Launcher/ViewModel/MainViewModel.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index cc793c14527..c01c3c427cf 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -31,6 +31,8 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable { #region Private Fields + private static string ClassName = nameof(MainViewModel); + private bool _isQueryRunning; private Query _lastQuery; private string _queryTextBeforeLeaveResults; @@ -214,7 +216,7 @@ async Task UpdateActionAsync() } if (!_disposed) - Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); + Log.Error(ClassName, "Unexpected ResultViewUpdate ends"); } void continueAction(Task t) @@ -250,7 +252,7 @@ public void RegisterResultsUpdatedEvent() if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, token))) { - Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); + Log.Error(ClassName, "Unable to add item to Result Update Queue"); } }; } @@ -885,7 +887,7 @@ public bool InternalPreviewVisible #if DEBUG throw new NotImplementedException("ResultAreaColumn should match ResultAreaColumnPreviewShown/ResultAreaColumnPreviewHidden value"); #else - Log.Error("MainViewModel", "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible"); + Log.Error(ClassName, "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible"); return false; #endif } @@ -1279,7 +1281,7 @@ async Task QueryTaskAsync(PluginPair plugin, bool reSelect = true) if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, _updateSource.Token, reSelect))) { - Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); + Log.Error(ClassName, "Unable to add item to Result Update Queue"); } } } @@ -1335,7 +1337,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable Date: Sat, 5 Apr 2025 09:53:45 +0800 Subject: [PATCH 09/79] Improve code comments Co-authored-by: Jeremy Wu --- Flow.Launcher.Infrastructure/Win32Helper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 7ecc716af83..99a1a547f63 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -327,7 +327,7 @@ internal static HWND GetWindowHandle(Window window, bool ensure = false) #region STA Thread /* - Found on https://github.com/files-community/Files + Inspired by https://github.com/files-community/Files code on STA Thread handling. */ public static Task StartSTATaskAsync(Action action) From 3fc69ab4f6eee8549aa3f05d342eaca1d138ead1 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sun, 13 Apr 2025 20:02:46 +0800 Subject: [PATCH 10/79] Fix typos Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher/PublicAPIInstance.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 1c8328ad497..c23fc8ebaf8 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -188,8 +188,7 @@ public async void CopyToClipboard(string stringToCopy, bool directCopy = false, // we need to retry a few times before giving up var exception = await RetryActionOnSTAThreadAsync(() => { - // We shouold use SetText instead of SetDataObject to avoid the clipboard being locked by other applications - Clipboard.SetText(stringToCopy); + // We should use SetText instead of SetDataObject to avoid the clipboard being locked by other applications }); if (exception == null) From 0314a9f23c4e9deca275cbe876823ea8ac1442b7 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sun, 13 Apr 2025 20:05:46 +0800 Subject: [PATCH 11/79] Add SetText back Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Flow.Launcher/PublicAPIInstance.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index c23fc8ebaf8..372c7dbb4f1 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -189,6 +189,7 @@ public async void CopyToClipboard(string stringToCopy, bool directCopy = false, var exception = await RetryActionOnSTAThreadAsync(() => { // We should use SetText instead of SetDataObject to avoid the clipboard being locked by other applications + Clipboard.SetText(stringToCopy); }); if (exception == null) From 8db8d5cb650e4ecd16576ea968ce45cf648f8c7d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 21:42:46 +0800 Subject: [PATCH 12/79] Add dispatcher back --- Flow.Launcher/ViewModel/MainViewModel.cs | 67 +++++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 257158af689..c5b88c22fe7 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1332,45 +1332,48 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable { - string expansion; - if (shortcut is BuiltinShortcutModel syncShortcut) - { - expansion = syncShortcut.Expand(); - } - else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) - { - expansion = await asyncShortcut.ExpandAsync(); - } - else - { - continue; - } - try + var queryChanged = false; + + foreach (var shortcut in builtInShortcuts) { - if (customExpanded.Contains(shortcut.Key)) + string expansion; + if (shortcut is BuiltinShortcutModel syncShortcut) + { + expansion = syncShortcut.Expand(); + } + else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) { - queryBuilder.Replace(shortcut.Key, expansion); - queryBuilderTmp.Replace(shortcut.Key, expansion); - queryChanged = true; + expansion = await asyncShortcut.ExpandAsync(); + } + else + { + continue; + } + try + { + if (customExpanded.Contains(shortcut.Key)) + { + queryBuilder.Replace(shortcut.Key, expansion); + queryBuilderTmp.Replace(shortcut.Key, expansion); + queryChanged = true; + } + } + catch (Exception e) + { + App.API.LogException(ClassName, $"Error when expanding shortcut {shortcut.Key}", e); } } - catch (Exception e) + + if (queryChanged) { - App.API.LogException(ClassName, $"Error when expanding shortcut {shortcut.Key}", e); + // show expanded builtin shortcuts + // use private field to avoid infinite recursion + _queryText = queryBuilderTmp.ToString(); + OnPropertyChanged(nameof(QueryText)); } - } - - if (queryChanged) - { - // show expanded builtin shortcuts - // use private field to avoid infinite recursion - _queryText = queryBuilderTmp.ToString(); - OnPropertyChanged(nameof(QueryText)); - } + }); return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); } From f7424609cc47f45dfb3bd3ac620c185b4d6867b1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 21:55:27 +0800 Subject: [PATCH 13/79] Add code comments --- Flow.Launcher/ViewModel/MainViewModel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c5b88c22fe7..20ea93029e0 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1317,8 +1317,8 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable x.Key.Length)) @@ -1332,7 +1332,9 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable + + // We must use dispatcher because text here will be used in TextBox + await Application.Current?.Dispatcher.InvokeAsync(async () => { var queryChanged = false; From ae7cea18892b774f7c9c2d697d02c0cc3376ea50 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 22:05:53 +0800 Subject: [PATCH 14/79] Fix COMException --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 20ea93029e0..65d16e3fbac 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1347,7 +1347,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable Date: Mon, 14 Apr 2025 22:53:07 +0800 Subject: [PATCH 15/79] Revert "Fix COMException" This reverts commit ae7cea18892b774f7c9c2d697d02c0cc3376ea50. --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 65d16e3fbac..20ea93029e0 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1347,7 +1347,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable Date: Mon, 14 Apr 2025 23:01:53 +0800 Subject: [PATCH 16/79] Do not open report window for task scheduler exception --- Flow.Launcher/App.xaml.cs | 3 +-- Flow.Launcher/Helper/ErrorReporting.cs | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 87677f58a67..411816e2ea1 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -286,9 +286,8 @@ private static void RegisterAppDomainExceptions() } /// - /// let exception throw as normal is better for Debug + /// exception will not be thrown normally, so we log it /// - [Conditional("RELEASE")] private static void RegisterTaskSchedulerUnhandledException() { TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException; diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index b1ddba7179a..6e65acad87b 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using System.Windows; using System.Windows.Threading; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Exception; @@ -35,8 +34,11 @@ public static void DispatcherUnhandledException(object sender, DispatcherUnhandl public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { //handle unobserved task exceptions - Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); + //do not open report window since it is not an unhandled exception + var logger = LogManager.GetLogger("TaskSchedulerUnobservedException"); + logger.Fatal(ExceptionFormatter.FormatExcpetion(e.Exception)); //prevent application exit, so the user can copy the prompted error info + e.SetObserved(); } public static string RuntimeInfo() From 1b412da00942fb1c02228579794bd00aa1a082cd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 08:37:13 +0800 Subject: [PATCH 17/79] Improve error reporting --- Flow.Launcher/App.xaml.cs | 5 +++-- Flow.Launcher/Helper/ErrorReporting.cs | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 411816e2ea1..dcde8fc3977 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -225,6 +225,7 @@ private void AutoStartup() } } + [Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -282,11 +283,11 @@ private void RegisterDispatcherUnhandledException() [Conditional("RELEASE")] private static void RegisterAppDomainExceptions() { - AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; + AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledException; } /// - /// exception will not be thrown normally, so we log it + /// exception will not be thrown normally, so we need to register event /// private static void RegisterTaskSchedulerUnhandledException() { diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index 6e65acad87b..99d9659e7a2 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows.Threading; using Flow.Launcher.Infrastructure; @@ -9,35 +10,33 @@ namespace Flow.Launcher.Helper; public static class ErrorReporting { - private static void Report(Exception e) + private static void Report(Exception e, [CallerMemberName] string methodName = "UnHandledException") { - var logger = LogManager.GetLogger("UnHandledException"); + var logger = LogManager.GetLogger(methodName); logger.Fatal(ExceptionFormatter.FormatExcpetion(e)); var reportWindow = new ReportWindow(e); reportWindow.Show(); } - public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e) + public static void UnhandledException(object sender, UnhandledExceptionEventArgs e) { - //handle non-ui thread exceptions + // handle non-ui thread exceptions Report((Exception)e.ExceptionObject); } public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { - //handle ui thread exceptions + // handle ui thread exceptions Report(e.Exception); - //prevent application exist, so the user can copy prompted error info + // prevent application exist, so the user can copy prompted error info e.Handled = true; } public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { - //handle unobserved task exceptions - //do not open report window since it is not an unhandled exception - var logger = LogManager.GetLogger("TaskSchedulerUnobservedException"); - logger.Fatal(ExceptionFormatter.FormatExcpetion(e.Exception)); - //prevent application exit, so the user can copy the prompted error info + // handle unobserved task exceptions on UI thread + System.Windows.Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); + // prevent application exit, so the user can copy the prompted error info e.SetObserved(); } From 370be2ab02c2dfd1ec4028bd9bb6228ad54fea63 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 08:52:43 +0800 Subject: [PATCH 18/79] Use JoinableTaskFactory instead --- .../ExternalPlugins/Environments/PythonEnvironment.cs | 2 +- .../ExternalPlugins/Environments/TypeScriptEnvironment.cs | 2 +- .../Environments/TypeScriptV2Environment.cs | 2 +- Flow.Launcher/App.xaml.cs | 2 ++ Flow.Launcher/ViewModel/MainViewModel.cs | 8 ++++---- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs index 455ee096da6..d93ab16267e 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -39,7 +39,7 @@ internal override void InstallEnvironment() // Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and // uses Python plugin they need to custom install and use v3.8.9 - JTF.Run(() => DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath)); + JTF.Run(async () => await DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs index 12965286f47..aa557951dd4 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -34,7 +34,7 @@ internal override void InstallEnvironment() { FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); - JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); + JTF.Run(async () => await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs index 6960b79c9a7..1ebe3755085 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -34,7 +34,7 @@ internal override void InstallEnvironment() { FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); - JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); + JTF.Run(async () => await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index dcde8fc3977..ea217886c88 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -21,6 +21,7 @@ using Flow.Launcher.ViewModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.Threading; namespace Flow.Launcher { @@ -29,6 +30,7 @@ public partial class App : IDisposable, ISingleInstanceApp #region Public Properties public static IPublicAPI API { get; private set; } + public static JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); #endregion diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 20ea93029e0..8ebfde4a8a4 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1160,7 +1160,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { _updateSource?.Cancel(); - var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); + var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); var plugins = PluginManager.ValidPluginsForQuery(query); @@ -1309,7 +1309,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } } - private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, + private Query ConstructQuery(string queryText, IEnumerable customShortcuts, IEnumerable builtInShortcuts) { if (string.IsNullOrWhiteSpace(queryText)) @@ -1334,7 +1334,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable + Application.Current?.Dispatcher.Invoke(() => { var queryChanged = false; @@ -1347,7 +1347,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable await asyncShortcut.ExpandAsync()); } else { From 0c592b1a66b5e06ccb7a004abe0026fd64b3bbf0 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 15 Apr 2025 08:59:46 +0800 Subject: [PATCH 19/79] Fix typos Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../UserSettings/CustomShortcutModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs index 725cff046f5..2d15b54c5be 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs @@ -78,7 +78,7 @@ public CustomShortcutModel(string key, string value) : base(key, value) #endregion - #region Buildin Shortcut + #region Builtin Shortcut public class BuiltinShortcutModel : BaseBuiltinShortcutModel { From d08deab252906d97fd164be4349610120496d044 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 11:44:38 +0800 Subject: [PATCH 20/79] Code quality --- Flow.Launcher/Helper/ErrorReporting.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index 99d9659e7a2..aa810ba651a 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using System.Windows; using System.Windows.Threading; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Exception; @@ -35,7 +36,7 @@ public static void DispatcherUnhandledException(object sender, DispatcherUnhandl public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { // handle unobserved task exceptions on UI thread - System.Windows.Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); + Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); // prevent application exit, so the user can copy the prompted error info e.SetObserved(); } From e55f79eb6c3b647802964427450e39f8e74d8771 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 11:50:59 +0800 Subject: [PATCH 21/79] Improve code comments & code quality --- .../ExternalPlugins/Environments/PythonEnvironment.cs | 2 +- .../ExternalPlugins/Environments/TypeScriptEnvironment.cs | 2 +- .../ExternalPlugins/Environments/TypeScriptV2Environment.cs | 2 +- Flow.Launcher/App.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs index d93ab16267e..455ee096da6 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -39,7 +39,7 @@ internal override void InstallEnvironment() // Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and // uses Python plugin they need to custom install and use v3.8.9 - JTF.Run(async () => await DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath)); + JTF.Run(() => DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs index aa557951dd4..12965286f47 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -34,7 +34,7 @@ internal override void InstallEnvironment() { FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); - JTF.Run(async () => await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); + JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs index 1ebe3755085..6960b79c9a7 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -34,7 +34,7 @@ internal override void InstallEnvironment() { FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); - JTF.Run(async () => await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); + JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index ea217886c88..b49c82dee2f 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -289,7 +289,7 @@ private static void RegisterAppDomainExceptions() } /// - /// exception will not be thrown normally, so we need to register event + /// let exception throw as normal for Debug and Release /// private static void RegisterTaskSchedulerUnhandledException() { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8ebfde4a8a4..56d19c035a8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1347,7 +1347,7 @@ private Query ConstructQuery(string queryText, IEnumerable } else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) { - expansion = App.JTF.Run(async () => await asyncShortcut.ExpandAsync()); + expansion = App.JTF.Run(() => asyncShortcut.ExpandAsync()); } else { From 882b7dcf6b19f3a89546c133e6d9a68b537cc7f6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 19:06:19 +0800 Subject: [PATCH 22/79] Check ui thread access when calling ui thread --- Flow.Launcher/ViewModel/MainViewModel.cs | 84 +++++++++++++----------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 56d19c035a8..375f2e756a2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1331,53 +1331,63 @@ private Query ConstructQuery(string queryText, IEnumerable queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand()); } - var customExpanded = queryBuilder.ToString(); + // Applying builtin shortcuts + BuildQuery(builtInShortcuts, queryBuilder, queryBuilderTmp); - // We must use dispatcher because text here will be used in TextBox - Application.Current?.Dispatcher.Invoke(() => + return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); + } + + // We must use dispatcher because text here will be used in TextBox + private void BuildQuery(IEnumerable builtInShortcuts, + StringBuilder queryBuilder, StringBuilder queryBuilderTmp) + { + if (!Application.Current.Dispatcher.CheckAccess()) { - var queryChanged = false; + Application.Current.Dispatcher.Invoke(() => BuildQuery(builtInShortcuts, queryBuilder, queryBuilderTmp)); + return; + } - foreach (var shortcut in builtInShortcuts) + var customExpanded = queryBuilder.ToString(); + + var queryChanged = false; + + foreach (var shortcut in builtInShortcuts) + { + string expansion; + if (shortcut is BuiltinShortcutModel syncShortcut) { - string expansion; - if (shortcut is BuiltinShortcutModel syncShortcut) - { - expansion = syncShortcut.Expand(); - } - else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) - { - expansion = App.JTF.Run(() => asyncShortcut.ExpandAsync()); - } - else - { - continue; - } - try - { - if (customExpanded.Contains(shortcut.Key)) - { - queryBuilder.Replace(shortcut.Key, expansion); - queryBuilderTmp.Replace(shortcut.Key, expansion); - queryChanged = true; - } - } - catch (Exception e) + expansion = syncShortcut.Expand(); + } + else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) + { + expansion = App.JTF.Run(() => asyncShortcut.ExpandAsync()); + } + else + { + continue; + } + try + { + if (customExpanded.Contains(shortcut.Key)) { - App.API.LogException(ClassName, $"Error when expanding shortcut {shortcut.Key}", e); + queryBuilder.Replace(shortcut.Key, expansion); + queryBuilderTmp.Replace(shortcut.Key, expansion); + queryChanged = true; } } - - if (queryChanged) + catch (Exception e) { - // show expanded builtin shortcuts - // use private field to avoid infinite recursion - _queryText = queryBuilderTmp.ToString(); - OnPropertyChanged(nameof(QueryText)); + App.API.LogException(ClassName, $"Error when expanding shortcut {shortcut.Key}", e); } - }); + } - return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); + if (queryChanged) + { + // show expanded builtin shortcuts + // use private field to avoid infinite recursion + _queryText = queryBuilderTmp.ToString(); + OnPropertyChanged(nameof(QueryText)); + } } private void RemoveOldQueryResults(Query query) From ba5a76a8e6b5e503386ed5153c65c19ea1c3adf0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 19:15:13 +0800 Subject: [PATCH 23/79] Improve performance --- Flow.Launcher/ViewModel/MainViewModel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 375f2e756a2..d11e0ad97f1 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1209,14 +1209,15 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _lastQuery = query; - if (string.IsNullOrEmpty(query.ActionKeyword)) + // 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(_ => { From 632dbeb6a9dae8e748d3aa067a867562271f35bd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 19:29:09 +0800 Subject: [PATCH 24/79] Check ui thread access when calling ui thread --- Flow.Launcher.Core/Resource/Theme.cs | 59 +++++++---- Flow.Launcher/ViewModel/MainViewModel.cs | 127 ++++++++++++++--------- 2 files changed, 114 insertions(+), 72 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 64ffec907d9..fc650a4857a 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -584,28 +584,37 @@ private void SetResizeBoarderThickness(Thickness? effectMargin) /// /// Refreshes the frame to apply the current theme settings. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD103:Call async methods when in an async method", Justification = "")] public async Task RefreshFrameAsync() { - await Application.Current.Dispatcher.InvokeAsync(() => + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return; + + // Must check access so that we will not block the UI thread which causes other issues + if (!Application.Current.Dispatcher.CheckAccess()) { - // Get the actual backdrop type and drop shadow effect settings - var (backdropType, useDropShadowEffect) = GetActualValue(); + // When application is exiting, the Application.Current will be null + await Application.Current?.Dispatcher.InvokeAsync(RefreshFrameAsync, DispatcherPriority.Render); + return; + } - // Remove OS minimizing/maximizing animation - // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, useDropShadowEffect) = GetActualValue(); - // The timing of adding the shadow effect should vary depending on whether the theme is transparent. - if (BlurEnabled) - { - AutoDropShadow(useDropShadowEffect); - } - SetBlurForWindow(_settings.Theme, backdropType); + // Remove OS minimizing/maximizing animation + // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); - if (!BlurEnabled) - { - AutoDropShadow(useDropShadowEffect); - } - }, DispatcherPriority.Render); + // The timing of adding the shadow effect should vary depending on whether the theme is transparent. + if (BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + SetBlurForWindow(_settings.Theme, backdropType); + + if (!BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } } /// @@ -613,13 +622,21 @@ await Application.Current.Dispatcher.InvokeAsync(() => /// public async Task SetBlurForWindowAsync() { - await Application.Current.Dispatcher.InvokeAsync(() => + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return; + + // Must check access so that we will not block the UI thread which causes other issues + if (!Application.Current.Dispatcher.CheckAccess()) { - // Get the actual backdrop type and drop shadow effect settings - var (backdropType, _) = GetActualValue(); + // When application is exiting, the Application.Current will be null + await Application.Current?.Dispatcher.InvokeAsync(SetBlurForWindowAsync, DispatcherPriority.Render); + return; + } + + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, _) = GetActualValue(); - SetBlurForWindow(_settings.Theme, backdropType); - }, DispatcherPriority.Render); + SetBlurForWindow(_settings.Theme, backdropType); } /// diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index d11e0ad97f1..9375de1e0e1 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -648,10 +648,10 @@ public void ChangeQueryText(string queryText, bool isReQuery = false) /// private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false) { - // Must check access so that we will not block the UI thread which cause window visibility issue + // Must check access so that we will not block the UI thread which causes window visibility issue if (!Application.Current.Dispatcher.CheckAccess()) { - await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery)); + await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryTextAsync(queryText, isReQuery)); return; } @@ -1342,6 +1342,7 @@ private Query ConstructQuery(string queryText, IEnumerable private void BuildQuery(IEnumerable builtInShortcuts, StringBuilder queryBuilder, StringBuilder queryBuilderTmp) { + // Must check access so that we will not block the UI thread which causes other issues if (!Application.Current.Dispatcher.CheckAccess()) { Application.Current.Dispatcher.Invoke(() => BuildQuery(builtInShortcuts, queryBuilder, queryBuilderTmp)); @@ -1517,32 +1518,7 @@ public bool ShouldIgnoreHotkeys() public void Show() { - // When application is exiting, the Application.Current will be null - Application.Current?.Dispatcher.Invoke(() => - { - // When application is exiting, the Application.Current will be null - if (Application.Current?.MainWindow is MainWindow mainWindow) - { - // 📌 Remove DWM Cloak (Make the window visible normally) - Win32Helper.DWMSetCloakForWindow(mainWindow, false); - - // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; - ClockPanelOpacity = opacity; - SearchIconOpacity = opacity; - - // Set clock and search icon visibility - ClockPanelVisibility = string.IsNullOrEmpty(QueryText) ? Visibility.Visible : Visibility.Collapsed; - if (PluginIconSource != null) - { - SearchIconOpacity = 0.0; - } - else - { - SearchIconVisibility = Visibility.Visible; - } - } - }, DispatcherPriority.Render); + ShowWindow(); // Update WPF properties MainWindowVisibility = Visibility.Visible; @@ -1556,6 +1532,43 @@ public void Show() } } + private void ShowWindow() + { + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return; + + // Must check access so that we will not block the UI thread which causes window visibility issue + if (!Application.Current.Dispatcher.CheckAccess()) + { + // When application is exiting, the Application.Current will be null + Application.Current?.Dispatcher.Invoke(ShowWindow, DispatcherPriority.Render); + return; + } + + // When application is exiting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + // 📌 Remove DWM Cloak (Make the window visible normally) + Win32Helper.DWMSetCloakForWindow(mainWindow, false); + + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = string.IsNullOrEmpty(QueryText) ? Visibility.Visible : Visibility.Collapsed; + if (PluginIconSource != null) + { + SearchIconOpacity = 0.0; + } + else + { + SearchIconVisibility = Visibility.Visible; + } + } + } + public async void Hide() { lastHistoryIndex = 1; @@ -1592,29 +1605,7 @@ public async void Hide() break; } - // When application is exiting, the Application.Current will be null - Application.Current?.Dispatcher.Invoke(() => - { - // When application is exiting, the Application.Current will be null - if (Application.Current?.MainWindow is MainWindow mainWindow) - { - // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; - ClockPanelOpacity = opacity; - SearchIconOpacity = opacity; - - // Set clock and search icon visibility - ClockPanelVisibility = Visibility.Hidden; - SearchIconVisibility = Visibility.Hidden; - - // Force UI update - mainWindow.ClockPanel.UpdateLayout(); - mainWindow.SearchIcon.UpdateLayout(); - - // 📌 Apply DWM Cloak (Completely hide the window) - Win32Helper.DWMSetCloakForWindow(mainWindow, true); - } - }, DispatcherPriority.Render); + HideWindow(); // Switch keyboard layout if (StartWithEnglishMode) @@ -1631,6 +1622,40 @@ public async void Hide() VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false }); } + private void HideWindow() + { + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return; + + // Must check access so that we will not block the UI thread which causes window visibility issue + if (!Application.Current.Dispatcher.CheckAccess()) + { + // When application is exiting, the Application.Current will be null + Application.Current?.Dispatcher.Invoke(HideWindow, DispatcherPriority.Render); + return; + } + + // When application is exiting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = Visibility.Hidden; + SearchIconVisibility = Visibility.Hidden; + + // Force UI update + mainWindow.ClockPanel.UpdateLayout(); + mainWindow.SearchIcon.UpdateLayout(); + + // 📌 Apply DWM Cloak (Completely hide the window) + Win32Helper.DWMSetCloakForWindow(mainWindow, true); + } + } + #pragma warning restore VSTHRD100 // Avoid async void methods /// From 8f23870e060a0adda7a2573ea640c0953de9d2a2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 19:36:07 +0800 Subject: [PATCH 25/79] Back to original style for fixing task issues --- Flow.Launcher/ViewModel/MainViewModel.cs | 39 ++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 9375de1e0e1..b88722b6efe 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -640,7 +640,31 @@ private void DecreaseMaxResult() /// Force query even when Query Text doesn't change public void ChangeQueryText(string queryText, bool isReQuery = false) { - _ = ChangeQueryTextAsync(queryText, isReQuery); + // Must check access so that we will not block the UI thread which causes window visibility issue + if (!Application.Current.Dispatcher.CheckAccess()) + { + Application.Current.Dispatcher.Invoke(() => ChangeQueryText(queryText, isReQuery)); + return; + } + + if (QueryText != queryText) + { + // Change query text first + QueryText = queryText; + // When we are changing query from codes, we should not delay the query + Query(false, isReQuery: false); + + // set to false so the subsequent set true triggers + // PropertyChanged and MoveQueryTextToEnd is called + QueryTextCursorMovedToEnd = false; + } + else if (isReQuery) + { + // When we are re-querying, we should not delay the query + Query(false, isReQuery: true); + } + + QueryTextCursorMovedToEnd = true; } /// @@ -1050,7 +1074,18 @@ private bool QueryResultsPreviewed() public void Query(bool searchDelay, bool isReQuery = false) { - _ = QueryAsync(searchDelay, isReQuery); + if (QueryResultsSelected()) + { + _ = QueryResultsAsync(searchDelay, isReQuery); + } + else if (ContextMenuSelected()) + { + QueryContextMenu(); + } + else if (HistorySelected()) + { + QueryHistory(); + } } private async Task QueryAsync(bool searchDelay, bool isReQuery = false) From 9dc9cdeea0e6fa9e9962e9658d16a86f499e746e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:02:43 +0800 Subject: [PATCH 26/79] Check token --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b88722b6efe..b3493ad51d7 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1337,6 +1337,9 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } } + if (token.IsCancellationRequested) + return; + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, token, reSelect))) { From 59908204aa4cbeba36465ec123fab3aea94de03a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:11:48 +0800 Subject: [PATCH 27/79] Set update source null --- Flow.Launcher/ViewModel/MainViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b3493ad51d7..788539591fa 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1222,6 +1222,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } _updateSource?.Dispose(); + _updateSource = null; var currentUpdateSource = new CancellationTokenSource(); _updateSource = currentUpdateSource; From 72c2d7fda6d6194003b38d09e8b5e8d0c6a01239 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:13:47 +0800 Subject: [PATCH 28/79] Wait update source cancellation before next update source --- Flow.Launcher/ViewModel/MainViewModel.cs | 111 ++++++++++++----------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 788539591fa..9acc33b5fe5 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -43,8 +43,9 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private readonly UserSelectedRecord _userSelectedRecord; private readonly TopMostRecord _topMostRecord; - private CancellationTokenSource _updateSource; + private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; + private SemaphoreSlim _updateSlim = new(1, 1); // Used to make sure one query flow running private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -1224,38 +1225,41 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _updateSource?.Dispose(); _updateSource = null; - var currentUpdateSource = new CancellationTokenSource(); - _updateSource = currentUpdateSource; - _updateToken = _updateSource.Token; + await _updateSlim.WaitAsync(); + try + { + var currentUpdateSource = new CancellationTokenSource(); + _updateSource = currentUpdateSource; + _updateToken = _updateSource.Token; - ProgressBarVisibility = Visibility.Hidden; - _isQueryRunning = true; + ProgressBarVisibility = Visibility.Hidden; + _isQueryRunning = true; - // Switch to ThreadPool thread - await TaskScheduler.Default; + // Switch to ThreadPool thread + await TaskScheduler.Default; - if (_updateSource.Token.IsCancellationRequested) - return; + if (_updateSource.Token.IsCancellationRequested) + return; - // Update the query's IsReQuery property to true if this is a re-query - query.IsReQuery = isReQuery; + // Update the query's IsReQuery property to true if this is a re-query + query.IsReQuery = isReQuery; - // handle the exclusiveness of plugin using action keyword - RemoveOldQueryResults(query); + // handle the exclusiveness of plugin using action keyword + RemoveOldQueryResults(query); - _lastQuery = query; + _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; - }*/ + // 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 (!_updateSource.Token.IsCancellationRequested && _isQueryRunning) @@ -1263,38 +1267,43 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b 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; - if (!_updateSource.Token.IsCancellationRequested) + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _isQueryRunning = false; + 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; + _updateSlim.Release(); } // Local function From 9242f8e6f5027390e31868808400c4b0c4aeea74 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:16:34 +0800 Subject: [PATCH 29/79] Valid plugin after --- Flow.Launcher/ViewModel/MainViewModel.cs | 31 ++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 9acc33b5fe5..143352e84c1 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1198,9 +1198,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); - var plugins = PluginManager.ValidPluginsForQuery(query); - - if (query == null || plugins.Count == 0) // shortcut expanded + if (query == null) // shortcut expanded { Results.Clear(); Results.Visibility = Visibility.Collapsed; @@ -1209,18 +1207,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b SearchIconVisibility = Visibility.Visible; return; } - else 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; - } _updateSource?.Dispose(); _updateSource = null; @@ -1249,6 +1235,21 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _lastQuery = query; + 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)) { From 0e70519f680bb9cd3a9229fffc8ca27bbcfc1e6f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:17:28 +0800 Subject: [PATCH 30/79] Adjust ident --- Flow.Launcher/ViewModel/MainViewModel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 143352e84c1..825ae94e942 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1261,13 +1261,13 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b }*/ _ = 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 (!_updateSource.Token.IsCancellationRequested && _isQueryRunning) { - ProgressBarVisibility = Visibility.Visible; - } - }, + // 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 (!_updateSource.Token.IsCancellationRequested && _isQueryRunning) + { + ProgressBarVisibility = Visibility.Visible; + } + }, _updateSource.Token, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); From 290750a57683a31e17a1430f7aa7c0044a3daff4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:20:44 +0800 Subject: [PATCH 31/79] Dispose update slim --- Flow.Launcher/ViewModel/MainViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 825ae94e942..742b7f298fd 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1804,6 +1804,7 @@ protected virtual void Dispose(bool disposing) { _resultsViewUpdateTask.Dispose(); } + _updateSlim?.Dispose(); _disposed = true; } } From a780719552d5aecd017225b947bb40dd560449c5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:22:05 +0800 Subject: [PATCH 32/79] Improve code comments --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 742b7f298fd..8662a6019ab 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -45,7 +45,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; - private SemaphoreSlim _updateSlim = new(1, 1); // Used to make sure one query flow running + private readonly SemaphoreSlim _updateSlim = new(1, 1); // Used to ensure one updating flow private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; From 00c496fcedbba89fba3e86a1a2114ea45d441d37 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:33:38 +0800 Subject: [PATCH 33/79] Revert "Set update source null" This reverts commit 59908204aa4cbeba36465ec123fab3aea94de03a. --- Flow.Launcher/ViewModel/MainViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8662a6019ab..1d56b6658b4 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1209,7 +1209,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } _updateSource?.Dispose(); - _updateSource = null; await _updateSlim.WaitAsync(); try From 5002449ccda06fed3cbe207ef05ba73c63122434 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:35:37 +0800 Subject: [PATCH 34/79] Use gloabl token & remove useless check --- Flow.Launcher/ViewModel/MainViewModel.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1d56b6658b4..8745b039fed 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1275,7 +1275,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTaskAsync(plugin, _updateSource.Token), + false => QueryTaskAsync(plugin), true => Task.CompletedTask }).ToArray(); @@ -1307,15 +1307,15 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } // Local function - async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) + async Task QueryTaskAsync(PluginPair plugin) { if (searchDelay) { var searchDelayTime = plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime; - await Task.Delay(searchDelayTime, token); + await Task.Delay(searchDelayTime, _updateSource.Token); - if (token.IsCancellationRequested) + if (_updateSource.Token.IsCancellationRequested) return; } @@ -1323,9 +1323,9 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) // Task.Yield will force it to run in ThreadPool await Task.Yield(); - var results = await PluginManager.QueryForPluginAsync(plugin, query, token); + var results = await PluginManager.QueryForPluginAsync(plugin, query, _updateSource.Token); - if (token.IsCancellationRequested) + if (_updateSource.Token.IsCancellationRequested) return; IReadOnlyList resultsCopy; @@ -1336,7 +1336,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results, token); + resultsCopy = DeepCloneResults(results, _updateSource.Token); } foreach (var result in resultsCopy) @@ -1347,11 +1347,8 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } } - if (token.IsCancellationRequested) - return; - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, - token, reSelect))) + _updateSource.Token, reSelect))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } From 396cd699c8db9e89a545ea274c4c35558d52d898 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:38:39 +0800 Subject: [PATCH 35/79] Do not dispose update source --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8745b039fed..44de326fc7a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1208,8 +1208,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } - _updateSource?.Dispose(); - await _updateSlim.WaitAsync(); try { From 893ec481e40f596c573de7a8aa626fca2b5f4515 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:43:42 +0800 Subject: [PATCH 36/79] Remove unused check --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 44de326fc7a..9b9635e836e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1260,7 +1260,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _ = 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 (!_updateSource.Token.IsCancellationRequested && _isQueryRunning) + if (_isQueryRunning) { ProgressBarVisibility = Visibility.Visible; } From a22306aea2f056ea6c7336bbb41391ba8c441bef Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 21:50:06 +0800 Subject: [PATCH 37/79] Use Query check instead of bool check --- Flow.Launcher/ViewModel/MainViewModel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 9b9635e836e..33d2f9b3b4a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -31,8 +31,8 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private static readonly string ClassName = nameof(MainViewModel); - private bool _isQueryRunning; private Query _lastQuery; + private Query _runningQuery; private string _queryTextBeforeLeaveResults; private readonly FlowLauncherJsonStorage _historyItemsStorage; @@ -1216,7 +1216,8 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; - _isQueryRunning = true; + + _runningQuery = query; // Switch to ThreadPool thread await TaskScheduler.Default; @@ -1260,7 +1261,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _ = 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 (_runningQuery != null && _runningQuery == query) { ProgressBarVisibility = Visibility.Visible; } @@ -1292,7 +1293,8 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // this should happen once after all queries are done so progress bar should continue // until the end of all querying - _isQueryRunning = false; + _runningQuery = null; + if (!_updateSource.Token.IsCancellationRequested) { // update to hidden if this is still the current query From 8e15e7f9345a0fc009559cd1e5c7b242248d252b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 16 Apr 2025 22:01:30 +0800 Subject: [PATCH 38/79] Change variable name --- Flow.Launcher/ViewModel/MainViewModel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 33d2f9b3b4a..c96ca099ae0 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -45,7 +45,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; - private readonly SemaphoreSlim _updateSlim = new(1, 1); // Used to ensure one updating flow + private readonly SemaphoreSlim _updateLock = new(1, 1); // Used to ensure one updating flow private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -1208,7 +1208,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } - await _updateSlim.WaitAsync(); + await _updateLock.WaitAsync(); try { var currentUpdateSource = new CancellationTokenSource(); @@ -1303,7 +1303,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } finally { - _updateSlim.Release(); + _updateLock.Release(); } // Local function @@ -1800,7 +1800,7 @@ protected virtual void Dispose(bool disposing) { _resultsViewUpdateTask.Dispose(); } - _updateSlim?.Dispose(); + _updateLock?.Dispose(); _disposed = true; } } From 67ed7a135df6a9732a00cadc1fdd12956bda1841 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 12:46:29 +0800 Subject: [PATCH 39/79] Do not use dispatcher --- Flow.Launcher/ViewModel/MainViewModel.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c96ca099ae0..30e5930dd70 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1383,17 +1383,9 @@ private Query ConstructQuery(string queryText, IEnumerable return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); } - // We must use dispatcher because text here will be used in TextBox private void BuildQuery(IEnumerable builtInShortcuts, StringBuilder queryBuilder, StringBuilder queryBuilderTmp) { - // Must check access so that we will not block the UI thread which causes other issues - if (!Application.Current.Dispatcher.CheckAccess()) - { - Application.Current.Dispatcher.Invoke(() => BuildQuery(builtInShortcuts, queryBuilder, queryBuilderTmp)); - return; - } - var customExpanded = queryBuilder.ToString(); var queryChanged = false; From 14cdf8a34a4162cd814a771894d30beb411f2b8d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 12:48:28 +0800 Subject: [PATCH 40/79] Add cancel token check --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 30e5930dd70..91546950c27 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1347,6 +1347,9 @@ async Task QueryTaskAsync(PluginPair plugin) } } + if (_updateSource.Token.IsCancellationRequested) + return; + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, _updateSource.Token, reSelect))) { From be0c1ad5162c33d7c2567c929bfd1a78364041d8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 12:58:18 +0800 Subject: [PATCH 41/79] Switch to ThreadPool thread to keep UI responsive when waiting update lock --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 91546950c27..23cb91785d3 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1208,6 +1208,9 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } + // Switch to ThreadPool thread to keep UI responsive when waiting update lock + await TaskScheduler.Default; + await _updateLock.WaitAsync(); try { From 43826cb783233ffc48dbbaf7f8e96c9406546cae Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 13:03:45 +0800 Subject: [PATCH 42/79] Add cancel token --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 23cb91785d3..4540606b49f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1211,7 +1211,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // Switch to ThreadPool thread to keep UI responsive when waiting update lock await TaskScheduler.Default; - await _updateLock.WaitAsync(); + await _updateLock.WaitAsync(CancellationToken.None); try { var currentUpdateSource = new CancellationTokenSource(); From 5441fc4266a8df0499e9a0164d7de91342b372ae Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 18:30:14 +0800 Subject: [PATCH 43/79] Fix log message issue --- Flow.Launcher/App.xaml.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index b49c82dee2f..795e76a1c3a 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -151,7 +151,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate(); API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------"); - API.LogInfo(ClassName, "Runtime info:{ErrorReporting.RuntimeInfo()}"); + API.LogInfo(ClassName, $"Runtime info:{ErrorReporting.RuntimeInfo()}"); RegisterAppDomainExceptions(); RegisterDispatcherUnhandledException(); @@ -178,8 +178,6 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => _mainWindow = new MainWindow(); - API.LogInfo(ClassName, "Dependencies Info:{ErrorReporting.DependenciesInfo()}"); - Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; From 20b5c474caeafab4db799252198ca7b8af494364 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 18:47:43 +0800 Subject: [PATCH 44/79] Fix progress bar issue when cancelling query with empty --- Flow.Launcher/ViewModel/MainViewModel.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 4540606b49f..1630647024b 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1205,6 +1205,17 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; + + // Wait last query to be canceled and hide progress bar + await _updateLock.WaitAsync(CancellationToken.None); + try + { + ProgressBarVisibility = Visibility.Hidden; + } + finally + { + _updateLock.Release(); + } return; } From 9eb7c9c969e07c5fc97f7fba4bc40ab292559fc8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 18:57:43 +0800 Subject: [PATCH 45/79] Wait last query to be canceled and then do resetting actions --- Flow.Launcher/ViewModel/MainViewModel.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1630647024b..faad898742d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1200,16 +1200,20 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (query == null) // shortcut expanded { + // Wait last query to be canceled and then do resetting actions + await _updateLock.WaitAsync(CancellationToken.None); + try + { + // Reset the results Results.Clear(); Results.Visibility = Visibility.Collapsed; + + // Reset the plugin icon PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; - // Wait last query to be canceled and hide progress bar - await _updateLock.WaitAsync(CancellationToken.None); - try - { + // Reset the progress bar ProgressBarVisibility = Visibility.Hidden; } finally From b9e04ff4ba8bd4dfd14cb463bf0359e01e3c16ba Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 18:59:32 +0800 Subject: [PATCH 46/79] Improve code comments --- Flow.Launcher/ViewModel/MainViewModel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index faad898742d..b346c8958d5 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1204,16 +1204,16 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b await _updateLock.WaitAsync(CancellationToken.None); try { - // Reset the results + // Reset results Results.Clear(); Results.Visibility = Visibility.Collapsed; - // Reset the plugin icon + // Reset plugin icon PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; - // Reset the progress bar + // Reset progress bar ProgressBarVisibility = Visibility.Hidden; } finally From 236abc8d22536a72a34f7f82055549a87863846b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 19:02:48 +0800 Subject: [PATCH 47/79] Add test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 53 +++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b346c8958d5..577f636e76a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -307,6 +307,7 @@ public void ReQuery() { if (QueryResultsSelected()) { + App.API.LogDebug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {true}"); // When we are re-querying, we should not delay the query _ = QueryResultsAsync(false, isReQuery: true); } @@ -315,6 +316,7 @@ public void ReQuery() public void ReQuery(bool reselect) { BackToQueryResults(); + App.API.LogDebug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {reselect}"); // When we are re-querying, we should not delay the query _ = QueryResultsAsync(false, isReQuery: true, reSelect: reselect); } @@ -1077,6 +1079,7 @@ public void Query(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) { + App.API.LogDebug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); _ = QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) @@ -1093,6 +1096,7 @@ private async Task QueryAsync(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) { + App.API.LogDebug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); await QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) @@ -1196,22 +1200,26 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { _updateSource?.Cancel(); + App.API.LogDebug(ClassName, $"Query construct for QueryText: {QueryText}"); + var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); if (query == null) // shortcut expanded { + App.API.LogDebug(ClassName, $"Query null for QueryText: null"); + // Wait last query to be canceled and then do resetting actions await _updateLock.WaitAsync(CancellationToken.None); try { // Reset results - Results.Clear(); - Results.Visibility = Visibility.Collapsed; + Results.Clear(); + Results.Visibility = Visibility.Collapsed; // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; // Reset progress bar ProgressBarVisibility = Visibility.Hidden; @@ -1226,6 +1234,8 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // Switch to ThreadPool thread to keep UI responsive when waiting update lock await TaskScheduler.Default; + App.API.LogDebug(ClassName, $"Wait for QueryText: {query.RawQuery}"); + await _updateLock.WaitAsync(CancellationToken.None); try { @@ -1235,13 +1245,19 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b ProgressBarVisibility = Visibility.Hidden; + App.API.LogDebug(ClassName, $"Start for QueryText: {query.RawQuery}"); + App.API.LogDebug(ClassName, $"ProgressBar: {Visibility.Hidden}"); + _runningQuery = query; // Switch to ThreadPool thread await TaskScheduler.Default; if (_updateSource.Token.IsCancellationRequested) + { + App.API.LogDebug(ClassName, $"Cancel for QueryText: {query.RawQuery}"); return; + } // Update the query's IsReQuery property to true if this is a re-query query.IsReQuery = isReQuery; @@ -1249,10 +1265,14 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // handle the exclusiveness of plugin using action keyword RemoveOldQueryResults(query); + App.API.LogDebug(ClassName, $"Remove old for QueryText: {query.RawQuery}"); + _lastQuery = query; var plugins = PluginManager.ValidPluginsForQuery(query); + App.API.LogDebug(ClassName, $"Valid {plugins.Count} plugins QueryText: {query.RawQuery}"); + if (plugins.Count == 1) { PluginIconPath = plugins.Single().Metadata.IcoPath; @@ -1278,10 +1298,14 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _ = Task.Delay(200, _updateSource.Token).ContinueWith(_ => { + App.API.LogDebug(ClassName, $"Check ProgressBar for QueryText: running: {_runningQuery?.RawQuery ?? "null"} query: {query.RawQuery}"); + // 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 (_runningQuery != null && _runningQuery == query) { ProgressBarVisibility = Visibility.Visible; + + App.API.LogDebug(ClassName, $"ProgressBar: {Visibility.Visible}"); } }, _updateSource.Token, @@ -1307,7 +1331,10 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } if (_updateSource.Token.IsCancellationRequested) + { + App.API.LogDebug(ClassName, $"Cancel for QueryText: {QueryText}"); return; + } // this should happen once after all queries are done so progress bar should continue // until the end of all querying @@ -1317,10 +1344,13 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; + + App.API.LogDebug(ClassName, $"ProgressBar: {Visibility.Hidden}"); } } finally { + App.API.LogDebug(ClassName, $"Query return for QueryText: {QueryText}"); _updateLock.Release(); } @@ -1334,7 +1364,10 @@ async Task QueryTaskAsync(PluginPair plugin) await Task.Delay(searchDelayTime, _updateSource.Token); if (_updateSource.Token.IsCancellationRequested) + { + App.API.LogDebug(ClassName, $"Cancel for QueryText: {QueryText}"); return; + } } // Since it is wrapped within a ThreadPool Thread, the synchronous context is null @@ -1344,7 +1377,10 @@ async Task QueryTaskAsync(PluginPair plugin) var results = await PluginManager.QueryForPluginAsync(plugin, query, _updateSource.Token); if (_updateSource.Token.IsCancellationRequested) + { + App.API.LogDebug(ClassName, $"Cancel for QueryText: {query.RawQuery}"); return; + } IReadOnlyList resultsCopy; if (results == null) @@ -1366,13 +1402,20 @@ async Task QueryTaskAsync(PluginPair plugin) } if (_updateSource.Token.IsCancellationRequested) + { + App.API.LogDebug(ClassName, $"Cancel for QueryText: {query.RawQuery}"); return; + } if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, _updateSource.Token, reSelect))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } + else + { + App.API.LogDebug(ClassName, $"Write updates for QueryText: {query.RawQuery}"); + } } } From 96bb07dabbaa74e7bb7592e6f8bfcf30095589fe Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 19:38:53 +0800 Subject: [PATCH 48/79] Use dispatcher for performance --- Flow.Launcher.Core/Resource/Theme.cs | 79 ++++++--------- Flow.Launcher/ViewModel/MainViewModel.cs | 122 +++++++++-------------- 2 files changed, 80 insertions(+), 121 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index fc650a4857a..2834b3ff017 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -76,7 +76,7 @@ public Theme(IPublicAPI publicAPI, Settings settings) { _api.LogError(ClassName, "Current theme resource not found. Initializing with default theme."); _oldTheme = Constant.DefaultTheme; - }; + } } #endregion @@ -126,7 +126,7 @@ public void UpdateFonts() // Load a ResourceDictionary for the specified theme. var themeName = _settings.Theme; var dict = GetThemeResourceDictionary(themeName); - + // Apply font settings to the theme resource. ApplyFontSettings(dict); UpdateResourceDictionary(dict); @@ -152,11 +152,11 @@ private void ApplyFontSettings(ResourceDictionary dict) var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); - + SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); } - + if (dict["ItemTitleStyle"] is Style resultItemStyle && dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && @@ -172,7 +172,7 @@ private void ApplyFontSettings(ResourceDictionary dict) SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); } - + if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) { @@ -197,7 +197,7 @@ private static void SetFontProperties(Style style, FontFamily fontFamily, FontSt // First, find the setters to remove and store them in a list var settersToRemove = style.Setters .OfType() - .Where(setter => + .Where(setter => setter.Property == Control.FontFamilyProperty || setter.Property == Control.FontStyleProperty || setter.Property == Control.FontWeightProperty || @@ -227,18 +227,18 @@ private static void SetFontProperties(Style style, FontFamily fontFamily, FontSt { var settersToRemove = style.Setters .OfType() - .Where(setter => + .Where(setter => setter.Property == TextBlock.FontFamilyProperty || setter.Property == TextBlock.FontStyleProperty || setter.Property == TextBlock.FontWeightProperty || setter.Property == TextBlock.FontStretchProperty) .ToList(); - + foreach (var setter in settersToRemove) { style.Setters.Remove(setter); } - + style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); @@ -421,7 +421,7 @@ public bool ChangeTheme(string theme = null) // Retrieve theme resource – always use the resource with font settings applied. var resourceDict = GetResourceDictionary(theme); - + UpdateResourceDictionary(resourceDict); _settings.Theme = theme; @@ -584,37 +584,28 @@ private void SetResizeBoarderThickness(Thickness? effectMargin) /// /// Refreshes the frame to apply the current theme settings. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD103:Call async methods when in an async method", Justification = "")] public async Task RefreshFrameAsync() { - // When application is exiting, the Application.Current will be null - if (Application.Current == null) return; - - // Must check access so that we will not block the UI thread which causes other issues - if (!Application.Current.Dispatcher.CheckAccess()) + await Application.Current.Dispatcher.InvokeAsync(() => { - // When application is exiting, the Application.Current will be null - await Application.Current?.Dispatcher.InvokeAsync(RefreshFrameAsync, DispatcherPriority.Render); - return; - } - - // Get the actual backdrop type and drop shadow effect settings - var (backdropType, useDropShadowEffect) = GetActualValue(); + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, useDropShadowEffect) = GetActualValue(); - // Remove OS minimizing/maximizing animation - // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); + // Remove OS minimizing/maximizing animation + // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); - // The timing of adding the shadow effect should vary depending on whether the theme is transparent. - if (BlurEnabled) - { - AutoDropShadow(useDropShadowEffect); - } - SetBlurForWindow(_settings.Theme, backdropType); + // The timing of adding the shadow effect should vary depending on whether the theme is transparent. + if (BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + SetBlurForWindow(_settings.Theme, backdropType); - if (!BlurEnabled) - { - AutoDropShadow(useDropShadowEffect); - } + if (!BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + }, DispatcherPriority.Render); } /// @@ -622,21 +613,13 @@ public async Task RefreshFrameAsync() /// public async Task SetBlurForWindowAsync() { - // When application is exiting, the Application.Current will be null - if (Application.Current == null) return; - - // Must check access so that we will not block the UI thread which causes other issues - if (!Application.Current.Dispatcher.CheckAccess()) + await Application.Current.Dispatcher.InvokeAsync(() => { - // When application is exiting, the Application.Current will be null - await Application.Current?.Dispatcher.InvokeAsync(SetBlurForWindowAsync, DispatcherPriority.Render); - return; - } - - // Get the actual backdrop type and drop shadow effect settings - var (backdropType, _) = GetActualValue(); + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, _) = GetActualValue(); - SetBlurForWindow(_settings.Theme, backdropType); + SetBlurForWindow(_settings.Theme, backdropType); + }, DispatcherPriority.Render); } /// diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 577f636e76a..72cb90358bf 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1619,7 +1619,32 @@ public bool ShouldIgnoreHotkeys() public void Show() { - ShowWindow(); + // When application is exiting, the Application.Current will be null + Application.Current?.Dispatcher.Invoke(() => + { + // When application is exiting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + // 📌 Remove DWM Cloak (Make the window visible normally) + Win32Helper.DWMSetCloakForWindow(mainWindow, false); + + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = string.IsNullOrEmpty(QueryText) ? Visibility.Visible : Visibility.Collapsed; + if (PluginIconSource != null) + { + SearchIconOpacity = 0.0; + } + else + { + SearchIconVisibility = Visibility.Visible; + } + } + }, DispatcherPriority.Render); // Update WPF properties MainWindowVisibility = Visibility.Visible; @@ -1633,43 +1658,6 @@ public void Show() } } - private void ShowWindow() - { - // When application is exiting, the Application.Current will be null - if (Application.Current == null) return; - - // Must check access so that we will not block the UI thread which causes window visibility issue - if (!Application.Current.Dispatcher.CheckAccess()) - { - // When application is exiting, the Application.Current will be null - Application.Current?.Dispatcher.Invoke(ShowWindow, DispatcherPriority.Render); - return; - } - - // When application is exiting, the Application.Current will be null - if (Application.Current?.MainWindow is MainWindow mainWindow) - { - // 📌 Remove DWM Cloak (Make the window visible normally) - Win32Helper.DWMSetCloakForWindow(mainWindow, false); - - // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; - ClockPanelOpacity = opacity; - SearchIconOpacity = opacity; - - // Set clock and search icon visibility - ClockPanelVisibility = string.IsNullOrEmpty(QueryText) ? Visibility.Visible : Visibility.Collapsed; - if (PluginIconSource != null) - { - SearchIconOpacity = 0.0; - } - else - { - SearchIconVisibility = Visibility.Visible; - } - } - } - public async void Hide() { lastHistoryIndex = 1; @@ -1706,7 +1694,29 @@ public async void Hide() break; } - HideWindow(); + // When application is exiting, the Application.Current will be null + Application.Current?.Dispatcher.Invoke(() => + { + // When application is exiting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = Visibility.Hidden; + SearchIconVisibility = Visibility.Hidden; + + // Force UI update + mainWindow.ClockPanel.UpdateLayout(); + mainWindow.SearchIcon.UpdateLayout(); + + // 📌 Apply DWM Cloak (Completely hide the window) + Win32Helper.DWMSetCloakForWindow(mainWindow, true); + } + }, DispatcherPriority.Render); // Switch keyboard layout if (StartWithEnglishMode) @@ -1723,40 +1733,6 @@ public async void Hide() VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false }); } - private void HideWindow() - { - // When application is exiting, the Application.Current will be null - if (Application.Current == null) return; - - // Must check access so that we will not block the UI thread which causes window visibility issue - if (!Application.Current.Dispatcher.CheckAccess()) - { - // When application is exiting, the Application.Current will be null - Application.Current?.Dispatcher.Invoke(HideWindow, DispatcherPriority.Render); - return; - } - - // When application is exiting, the Application.Current will be null - if (Application.Current?.MainWindow is MainWindow mainWindow) - { - // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; - ClockPanelOpacity = opacity; - SearchIconOpacity = opacity; - - // Set clock and search icon visibility - ClockPanelVisibility = Visibility.Hidden; - SearchIconVisibility = Visibility.Hidden; - - // Force UI update - mainWindow.ClockPanel.UpdateLayout(); - mainWindow.SearchIcon.UpdateLayout(); - - // 📌 Apply DWM Cloak (Completely hide the window) - Win32Helper.DWMSetCloakForWindow(mainWindow, true); - } - } - #pragma warning restore VSTHRD100 // Avoid async void methods /// From 56a355132ed48d56d565627875497f40d951a4cd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 19:54:18 +0800 Subject: [PATCH 49/79] Fix performance issue --- Flow.Launcher/ViewModel/MainViewModel.cs | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 72cb90358bf..8147f7c00fa 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1456,23 +1456,23 @@ private void BuildQuery(IEnumerable builtInShortcuts, foreach (var shortcut in builtInShortcuts) { - string expansion; - if (shortcut is BuiltinShortcutModel syncShortcut) - { - expansion = syncShortcut.Expand(); - } - else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) - { - expansion = App.JTF.Run(() => asyncShortcut.ExpandAsync()); - } - else - { - continue; - } try { if (customExpanded.Contains(shortcut.Key)) { + string expansion; + if (shortcut is BuiltinShortcutModel syncShortcut) + { + expansion = syncShortcut.Expand(); + } + else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) + { + expansion = App.JTF.Run(() => asyncShortcut.ExpandAsync()); + } + else + { + continue; + } queryBuilder.Replace(shortcut.Key, expansion); queryBuilderTmp.Replace(shortcut.Key, expansion); queryChanged = true; From 4ca8c60928dcd5bbbb45e206005a93c558912764 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 20:15:02 +0800 Subject: [PATCH 50/79] Add test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8147f7c00fa..7406c54ba54 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1212,6 +1212,8 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b await _updateLock.WaitAsync(CancellationToken.None); try { + App.API.LogDebug(ClassName, $"Clear for QueryText: {query.RawQuery}"); + // Reset results Results.Clear(); Results.Visibility = Visibility.Collapsed; From ab953428da62f764590fe62127ce1e96e7b8ad72 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 20:16:08 +0800 Subject: [PATCH 51/79] Improve test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7406c54ba54..f1d44296d57 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1257,7 +1257,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (_updateSource.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText: {query.RawQuery}"); + App.API.LogDebug(ClassName, $"Cancel for QueryText 1: {query.RawQuery}"); return; } @@ -1334,7 +1334,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (_updateSource.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText: {QueryText}"); + App.API.LogDebug(ClassName, $"Cancel for QueryText 2: {QueryText}"); return; } @@ -1367,7 +1367,7 @@ async Task QueryTaskAsync(PluginPair plugin) if (_updateSource.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText: {QueryText}"); + App.API.LogDebug(ClassName, $"Cancel for QueryText 3: {QueryText}"); return; } } @@ -1380,7 +1380,7 @@ async Task QueryTaskAsync(PluginPair plugin) if (_updateSource.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText: {query.RawQuery}"); + App.API.LogDebug(ClassName, $"Cancel for QueryText 4: {query.RawQuery}"); return; } @@ -1405,7 +1405,7 @@ async Task QueryTaskAsync(PluginPair plugin) if (_updateSource.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText: {query.RawQuery}"); + App.API.LogDebug(ClassName, $"Cancel for QueryText 5: {query.RawQuery}"); return; } From 186cd2c8384f58e73ce314fcda247864bbbcfb41 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 20:25:03 +0800 Subject: [PATCH 52/79] Remove useless switch --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f1d44296d57..72ed10d4b00 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1233,9 +1233,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } - // Switch to ThreadPool thread to keep UI responsive when waiting update lock - await TaskScheduler.Default; - App.API.LogDebug(ClassName, $"Wait for QueryText: {query.RawQuery}"); await _updateLock.WaitAsync(CancellationToken.None); From 4aa9a34377ccd50da3b8ebb0739f39122e4792b0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 20:27:11 +0800 Subject: [PATCH 53/79] Fix test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 72ed10d4b00..ac7ae94e6ba 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1206,13 +1206,13 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (query == null) // shortcut expanded { - App.API.LogDebug(ClassName, $"Query null for QueryText: null"); + App.API.LogDebug(ClassName, $"Query null for QueryText"); // Wait last query to be canceled and then do resetting actions await _updateLock.WaitAsync(CancellationToken.None); try { - App.API.LogDebug(ClassName, $"Clear for QueryText: {query.RawQuery}"); + App.API.LogDebug(ClassName, $"Clear for QueryText"); // Reset results Results.Clear(); From e9a08682ced2104f8f01422bc415170650132c67 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 20:48:42 +0800 Subject: [PATCH 54/79] Add running query check --- Flow.Launcher/ViewModel/MainViewModel.cs | 30 ++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ac7ae94e6ba..4f2bfb14be2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -237,7 +237,7 @@ public void RegisterResultsUpdatedEvent() var plugin = (IResultUpdated)pair.Plugin; plugin.ResultsUpdated += (s, e) => { - if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested) + if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || e.Token.IsCancellationRequested) { return; } @@ -256,8 +256,14 @@ public void RegisterResultsUpdatedEvent() } PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, - token))) + + if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || token.IsCancellationRequested) + { + return; + } + + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, + token))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } @@ -1315,7 +1321,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTaskAsync(plugin), + false => QueryTaskAsync(plugin, _updateSource.Token), true => Task.CompletedTask }).ToArray(); @@ -1354,15 +1360,15 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } // Local function - async Task QueryTaskAsync(PluginPair plugin) + async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { if (searchDelay) { var searchDelayTime = plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime; - await Task.Delay(searchDelayTime, _updateSource.Token); + await Task.Delay(searchDelayTime, token); - if (_updateSource.Token.IsCancellationRequested) + if (token.IsCancellationRequested) { App.API.LogDebug(ClassName, $"Cancel for QueryText 3: {QueryText}"); return; @@ -1373,9 +1379,9 @@ async Task QueryTaskAsync(PluginPair plugin) // Task.Yield will force it to run in ThreadPool await Task.Yield(); - var results = await PluginManager.QueryForPluginAsync(plugin, query, _updateSource.Token); + var results = await PluginManager.QueryForPluginAsync(plugin, query, token); - if (_updateSource.Token.IsCancellationRequested) + if (token.IsCancellationRequested) { App.API.LogDebug(ClassName, $"Cancel for QueryText 4: {query.RawQuery}"); return; @@ -1389,7 +1395,7 @@ async Task QueryTaskAsync(PluginPair plugin) else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results, _updateSource.Token); + resultsCopy = DeepCloneResults(results, token); } foreach (var result in resultsCopy) @@ -1400,14 +1406,14 @@ async Task QueryTaskAsync(PluginPair plugin) } } - if (_updateSource.Token.IsCancellationRequested) + if (token.IsCancellationRequested) { App.API.LogDebug(ClassName, $"Cancel for QueryText 5: {query.RawQuery}"); return; } if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, - _updateSource.Token, reSelect))) + token, reSelect))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } From 854e61d2e249b041b8a6045428145d0ae76e8973 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 20:51:54 +0800 Subject: [PATCH 55/79] Add test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 4f2bfb14be2..0737ce87e76 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -237,8 +237,11 @@ public void RegisterResultsUpdatedEvent() var plugin = (IResultUpdated)pair.Plugin; plugin.ResultsUpdated += (s, e) => { + App.API.LogDebug(ClassName, ClassName, $"Call IResultsUpdated for QueryText: {e.Query.RawQuery}"); + if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || e.Token.IsCancellationRequested) { + App.API.LogDebug(ClassName, $"Cancel for QueryText 6: {e.Query.RawQuery}"); return; } @@ -259,6 +262,7 @@ public void RegisterResultsUpdatedEvent() if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || token.IsCancellationRequested) { + App.API.LogDebug(ClassName, $"Cancel for QueryText 7: {e.Query.RawQuery}"); return; } @@ -267,6 +271,10 @@ public void RegisterResultsUpdatedEvent() { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } + else + { + App.API.LogDebug(ClassName, $"Write updates for QueryText 1: {e.Query.RawQuery}"); + } }; } } From 7961615bbe239634e984a93a71a7f0a940bb44d3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 20:53:49 +0800 Subject: [PATCH 56/79] Improve test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0737ce87e76..25c5994ce4e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -237,11 +237,11 @@ public void RegisterResultsUpdatedEvent() var plugin = (IResultUpdated)pair.Plugin; plugin.ResultsUpdated += (s, e) => { - App.API.LogDebug(ClassName, ClassName, $"Call IResultsUpdated for QueryText: {e.Query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, ClassName, $"Call IResultsUpdated for QueryText: {e.Query.RawQuery}"); if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || e.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText 6: {e.Query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 6: {e.Query.RawQuery}"); return; } @@ -262,7 +262,7 @@ public void RegisterResultsUpdatedEvent() if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText 7: {e.Query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 7: {e.Query.RawQuery}"); return; } @@ -273,7 +273,7 @@ public void RegisterResultsUpdatedEvent() } else { - App.API.LogDebug(ClassName, $"Write updates for QueryText 1: {e.Query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Write updates for QueryText 1: {e.Query.RawQuery}"); } }; } @@ -321,7 +321,7 @@ public void ReQuery() { if (QueryResultsSelected()) { - App.API.LogDebug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {true}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {true}"); // When we are re-querying, we should not delay the query _ = QueryResultsAsync(false, isReQuery: true); } @@ -330,7 +330,7 @@ public void ReQuery() public void ReQuery(bool reselect) { BackToQueryResults(); - App.API.LogDebug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {reselect}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {reselect}"); // When we are re-querying, we should not delay the query _ = QueryResultsAsync(false, isReQuery: true, reSelect: reselect); } @@ -1093,7 +1093,7 @@ public void Query(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) { - App.API.LogDebug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); _ = QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) @@ -1110,7 +1110,7 @@ private async Task QueryAsync(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) { - App.API.LogDebug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); await QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) @@ -1214,19 +1214,19 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { _updateSource?.Cancel(); - App.API.LogDebug(ClassName, $"Query construct for QueryText: {QueryText}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Query construct for QueryText: {QueryText}"); var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); if (query == null) // shortcut expanded { - App.API.LogDebug(ClassName, $"Query null for QueryText"); + Infrastructure.Logger.Log.Debug(ClassName, $"Query null for QueryText"); // Wait last query to be canceled and then do resetting actions await _updateLock.WaitAsync(CancellationToken.None); try { - App.API.LogDebug(ClassName, $"Clear for QueryText"); + Infrastructure.Logger.Log.Debug(ClassName, $"Clear for QueryText"); // Reset results Results.Clear(); @@ -1247,7 +1247,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } - App.API.LogDebug(ClassName, $"Wait for QueryText: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Wait for QueryText: {query.RawQuery}"); await _updateLock.WaitAsync(CancellationToken.None); try @@ -1258,8 +1258,8 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b ProgressBarVisibility = Visibility.Hidden; - App.API.LogDebug(ClassName, $"Start for QueryText: {query.RawQuery}"); - App.API.LogDebug(ClassName, $"ProgressBar: {Visibility.Hidden}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Start for QueryText: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"ProgressBar: {Visibility.Hidden}"); _runningQuery = query; @@ -1268,7 +1268,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (_updateSource.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText 1: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 1: {query.RawQuery}"); return; } @@ -1278,13 +1278,13 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // handle the exclusiveness of plugin using action keyword RemoveOldQueryResults(query); - App.API.LogDebug(ClassName, $"Remove old for QueryText: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Remove old for QueryText: {query.RawQuery}"); _lastQuery = query; var plugins = PluginManager.ValidPluginsForQuery(query); - App.API.LogDebug(ClassName, $"Valid {plugins.Count} plugins QueryText: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Valid {plugins.Count} plugins QueryText: {query.RawQuery}"); if (plugins.Count == 1) { @@ -1311,14 +1311,14 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _ = Task.Delay(200, _updateSource.Token).ContinueWith(_ => { - App.API.LogDebug(ClassName, $"Check ProgressBar for QueryText: running: {_runningQuery?.RawQuery ?? "null"} query: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Check ProgressBar for QueryText: running: {_runningQuery?.RawQuery ?? "null"} query: {query.RawQuery}"); // 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 (_runningQuery != null && _runningQuery == query) { ProgressBarVisibility = Visibility.Visible; - App.API.LogDebug(ClassName, $"ProgressBar: {Visibility.Visible}"); + Infrastructure.Logger.Log.Debug(ClassName, $"ProgressBar: {Visibility.Visible}"); } }, _updateSource.Token, @@ -1345,7 +1345,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (_updateSource.Token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText 2: {QueryText}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 2: {QueryText}"); return; } @@ -1358,12 +1358,12 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; - App.API.LogDebug(ClassName, $"ProgressBar: {Visibility.Hidden}"); + Infrastructure.Logger.Log.Debug(ClassName, $"ProgressBar: {Visibility.Hidden}"); } } finally { - App.API.LogDebug(ClassName, $"Query return for QueryText: {QueryText}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Query return for QueryText: {QueryText}"); _updateLock.Release(); } @@ -1378,7 +1378,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) if (token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText 3: {QueryText}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 3: {QueryText}"); return; } } @@ -1391,7 +1391,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) if (token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText 4: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 4: {query.RawQuery}"); return; } @@ -1416,7 +1416,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) if (token.IsCancellationRequested) { - App.API.LogDebug(ClassName, $"Cancel for QueryText 5: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 5: {query.RawQuery}"); return; } @@ -1427,7 +1427,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } else { - App.API.LogDebug(ClassName, $"Write updates for QueryText: {query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Write updates for QueryText: {query.RawQuery}"); } } } From e5b5ec5a9a540d6caea66c57060c20f08f9d0078 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 21:00:55 +0800 Subject: [PATCH 57/79] Improve test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 25c5994ce4e..22f3d7e406d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -237,7 +237,7 @@ public void RegisterResultsUpdatedEvent() var plugin = (IResultUpdated)pair.Plugin; plugin.ResultsUpdated += (s, e) => { - Infrastructure.Logger.Log.Debug(ClassName, ClassName, $"Call IResultsUpdated for QueryText: {e.Query.RawQuery}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Call IResultsUpdated for QueryText: {e.Query.RawQuery}"); if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || e.Token.IsCancellationRequested) { From 30a840ae88d8923c8ffcda868e6dab4cedb7aae9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 21:09:19 +0800 Subject: [PATCH 58/79] Improve test codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 22f3d7e406d..882063cfec3 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1345,7 +1345,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (_updateSource.Token.IsCancellationRequested) { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 2: {QueryText}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 2: {query.RawQuery}"); return; } @@ -1363,7 +1363,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } finally { - Infrastructure.Logger.Log.Debug(ClassName, $"Query return for QueryText: {QueryText}"); + Infrastructure.Logger.Log.Debug(ClassName, $"Query return for QueryText: {query.RawQuery}"); _updateLock.Release(); } From b918d0099ff494c591359e7ef72c11ce0f82f542 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Apr 2025 21:32:01 +0800 Subject: [PATCH 59/79] Improve display performance --- Flow.Launcher/ViewModel/MainViewModel.cs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 882063cfec3..c52f923de9c 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1222,22 +1222,29 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { Infrastructure.Logger.Log.Debug(ClassName, $"Query null for QueryText"); - // Wait last query to be canceled and then do resetting actions + // Hide and clear results fast because results are already invalid although query is still running + Results.Visibility = Visibility.Collapsed; + Results.Clear(); + + // Hide progress bar because running query is already invalid + ProgressBarVisibility = Visibility.Hidden; + + // Wait last query to be canceled and then reset UI elements await _updateLock.WaitAsync(CancellationToken.None); try { Infrastructure.Logger.Log.Debug(ClassName, $"Clear for QueryText"); - // Reset results - Results.Clear(); + // Hide and clear results again because running query may show and add some results Results.Visibility = Visibility.Collapsed; + Results.Clear(); // Reset plugin icon PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; - // Reset progress bar + // Hide progress bar again because running query may set this to visible ProgressBarVisibility = Visibility.Hidden; } finally @@ -1252,6 +1259,14 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b await _updateLock.WaitAsync(CancellationToken.None); 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.RawQuery != QueryText) + { + Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 0: {query.RawQuery}"); + return; + } + var currentUpdateSource = new CancellationTokenSource(); _updateSource = currentUpdateSource; _updateToken = _updateSource.Token; From adde4919ba0e705332f86598ca12beea63364467 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 08:05:01 +0800 Subject: [PATCH 60/79] Suppress warning --- Flow.Launcher/PublicAPIInstance.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 372c7dbb4f1..d58e9cf5320 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -69,8 +69,7 @@ public void ChangeQuery(string query, bool requery = false) _mainVM.ChangeQueryText(query, requery); } -#pragma warning disable VSTHRD100 // Avoid async void methods - + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] public async void RestartApp() { _mainVM.Hide(); @@ -89,8 +88,6 @@ public async void RestartApp() UpdateManager.RestartApp(Constant.ApplicationFileName); } -#pragma warning restore VSTHRD100 // Avoid async void methods - public void ShowMainWindow() => _mainVM.Show(); public void HideMainWindow() => _mainVM.Hide(); @@ -145,6 +142,7 @@ public void ShellRun(string cmd, string filename = "cmd.exe") ShellCommand.Execute(startInfo); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] public async void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true) { if (string.IsNullOrEmpty(stringToCopy)) From c0b792c783aa8ede77a02dab1b87c747f92423ec Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 09:44:16 +0800 Subject: [PATCH 61/79] Set running query null after update lock and update cancellation --- Flow.Launcher/ViewModel/MainViewModel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c52f923de9c..06ceeaf336a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1213,6 +1213,7 @@ private void QueryHistory() private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { _updateSource?.Cancel(); + _runningQuery = null; Infrastructure.Logger.Log.Debug(ClassName, $"Query construct for QueryText: {QueryText}"); @@ -1379,6 +1380,10 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b finally { Infrastructure.Logger.Log.Debug(ClassName, $"Query return for QueryText: {query.RawQuery}"); + // this make sures running query is null even if the query is canceled + _runningQuery = null; + + // release the lock so that other query can be executed _updateLock.Release(); } From 7669bba2e30e5a20e72492216df18a4d6eebb26d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 11:00:05 +0800 Subject: [PATCH 62/79] Inline update source --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 06ceeaf336a..ad0c433e28a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1268,8 +1268,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } - var currentUpdateSource = new CancellationTokenSource(); - _updateSource = currentUpdateSource; + _updateSource = new CancellationTokenSource(); _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; From 282f4860f56d45e5c338883cd06620c3e8cc8e13 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 20:16:22 +0800 Subject: [PATCH 63/79] Fix query input check issue --- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 13 ++++++++----- Flow.Launcher.Plugin/Query.cs | 9 ++++++++- Flow.Launcher.Test/QueryBuilderTest.cs | 6 +++--- Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 6 +++--- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 3dc7877acc2..df6a322d166 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Flow.Launcher.Plugin; @@ -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) { // replace multiple white spaces with one white space var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); @@ -21,13 +21,15 @@ public static Query Build(string text, Dictionary 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; @@ -36,10 +38,11 @@ public static Query Build(string text, Dictionary nonGlobalP return new Query () { Search = search, + Input = input, RawQuery = rawQuery, SearchTerms = searchTerms, ActionKeyword = actionKeyword }; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 913dc31ae65..5b9181004fd 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -8,7 +8,14 @@ namespace Flow.Launcher.Plugin public class Query { /// - /// Raw query, this includes action keyword if it has + /// 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 hotkeys and user custom hotkeys, and it trims the whitespace. /// We didn't recommend use this property directly. You should always use Search property. /// public string RawQuery { get; internal init; } 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 bf7a45b1d25..2f35dfb3564 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -401,7 +401,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 ad0c433e28a..0f9a75bd575 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -380,7 +380,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('\\')); @@ -1262,7 +1262,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { // 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.RawQuery != QueryText) + if (query.Input != QueryText && query.RawQuery != QueryText.Trim()) { Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 0: {query.RawQuery}"); return; @@ -1476,7 +1476,7 @@ private Query ConstructQuery(string queryText, IEnumerable // Applying builtin shortcuts BuildQuery(builtInShortcuts, queryBuilder, queryBuilderTmp); - return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); + return QueryBuilder.Build(queryText, queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); } private void BuildQuery(IEnumerable builtInShortcuts, From a143cb45016b7a87b013b4d50762d90417f7267d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 21:29:13 +0800 Subject: [PATCH 64/79] Improve code comments --- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 3 ++- Flow.Launcher.Plugin/Query.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index df6a322d166..2d9acc45b33 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -11,7 +11,8 @@ public static Query Build(string input, string text, Dictionary /// Raw query, this includes action keyword if it has. - /// It has handled buildin custom hotkeys and user custom hotkeys, and it trims the whitespace. + /// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace. /// We didn't recommend use this property directly. You should always use Search property. /// public string RawQuery { get; internal init; } From 1ffbbcabc82e148e1b18d52ee80e254d200f052c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 21:30:51 +0800 Subject: [PATCH 65/79] Remove _updateToken --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0f9a75bd575..f86a2aad454 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -44,7 +44,6 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows - private CancellationToken _updateToken; private readonly SemaphoreSlim _updateLock = new(1, 1); // Used to ensure one updating flow private ChannelWriter _resultsUpdateChannelWriter; @@ -245,7 +244,7 @@ public void RegisterResultsUpdatedEvent() return; } - var token = e.Token == default ? _updateToken : e.Token; + var token = e.Token == default ? _updateSource.Token : e.Token; // make a clone to avoid possible issue that plugin will also change the list and items when updating view model var resultsCopy = DeepCloneResults(e.Results, token); @@ -1269,7 +1268,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; From 836d09e8a8a3b28957b10fc6e880bf386c24dbe5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 23 Apr 2025 20:22:16 +0800 Subject: [PATCH 66/79] Ignore main window show when exiting --- Flow.Launcher/App.xaml.cs | 3 ++- Flow.Launcher/ViewModel/MainViewModel.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 98f4ac0c37d..f3d5e6ea3ac 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -31,6 +31,7 @@ public partial class App : IDisposable, ISingleInstanceApp public static IPublicAPI API { get; private set; } public static JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); + public static bool Exitting => _mainWindow.CanClose; #endregion @@ -39,7 +40,7 @@ public partial class App : IDisposable, ISingleInstanceApp private static readonly string ClassName = nameof(App); private static bool _disposed; - private MainWindow _mainWindow; + private static MainWindow _mainWindow; private readonly MainViewModel _mainVM; private readonly Settings _settings; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f86a2aad454..cfeec0b99de 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1649,6 +1649,9 @@ public bool ShouldIgnoreHotkeys() public void Show() { + // When application is exiting, we should not show the main window + if (App.Exitting) return; + // When application is exiting, the Application.Current will be null Application.Current?.Dispatcher.Invoke(() => { From 6bb638b79b6c5d879cf871263b2e79853128e21b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 23 Apr 2025 21:26:18 +0800 Subject: [PATCH 67/79] Fix window blink issue --- Flow.Launcher/App.xaml.cs | 3 +++ Flow.Launcher/MainWindow.xaml.cs | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index f3d5e6ea3ac..cb15c9d1cb3 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -182,6 +182,9 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; + // Initialize hotkey mapper instantly after main window is created because it will steal focus from main window + HotKeyMapper.Initialize(); + // main windows needs initialized before theme change because of blur settings Ioc.Default.GetRequiredService().ChangeTheme(); diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2f35dfb3564..1c9b73a5475 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -16,7 +16,6 @@ using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; @@ -174,9 +173,6 @@ private async void OnLoaded(object sender, RoutedEventArgs _) // Set the initial state of the QueryTextBoxCursorMovedToEnd property // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. _viewModel.QueryTextCursorMovedToEnd = false; - - // Initialize hotkey mapper after window is loaded - HotKeyMapper.Initialize(); // View model property changed event _viewModel.PropertyChanged += (o, e) => From dd467e2614c50b846c17940e5573d749267dd434 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 25 Apr 2025 08:32:15 +0800 Subject: [PATCH 68/79] Fix typo --- Flow.Launcher/App.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index cb15c9d1cb3..2bfb2aa1294 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -31,7 +31,7 @@ public partial class App : IDisposable, ISingleInstanceApp public static IPublicAPI API { get; private set; } public static JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); - public static bool Exitting => _mainWindow.CanClose; + public static bool Exiting => _mainWindow.CanClose; #endregion diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index cfeec0b99de..cdb313db3fe 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1650,7 +1650,7 @@ public bool ShouldIgnoreHotkeys() public void Show() { // When application is exiting, we should not show the main window - if (App.Exitting) return; + if (App.Exiting) return; // When application is exiting, the Application.Current will be null Application.Current?.Dispatcher.Invoke(() => From 74f274a8bd677164ccb8a29e40e8ac1c924aa917 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 25 Apr 2025 11:21:41 +0800 Subject: [PATCH 69/79] Add hotkey mapper initialization back --- Flow.Launcher/App.xaml.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 932b9065b21..9146d243455 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -200,6 +200,10 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; + // Initialize hotkey mapper instantly after main window is created because + // it will steal focus from main window which causes window hide + HotKeyMapper.Initialize(); + // Main windows needs initialized before theme change because of blur settings Ioc.Default.GetRequiredService().ChangeTheme(); From 780054035cffbf53a7d465428dca697478095999 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 25 Apr 2025 11:41:17 +0800 Subject: [PATCH 70/79] Improve code quality --- Flow.Launcher/Helper/DataWebRequestFactory.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/Helper/DataWebRequestFactory.cs b/Flow.Launcher/Helper/DataWebRequestFactory.cs index 2e72ee240fd..db198ede4e7 100644 --- a/Flow.Launcher/Helper/DataWebRequestFactory.cs +++ b/Flow.Launcher/Helper/DataWebRequestFactory.cs @@ -28,18 +28,18 @@ class DataWebResponse : WebResponse public DataWebResponse(Uri uri) { - string uriString = uri.AbsoluteUri; + var uriString = uri.AbsoluteUri; - int commaIndex = uriString.IndexOf(','); - var headers = uriString.Substring(0, commaIndex).Split(';'); + var commaIndex = uriString.IndexOf(','); + var headers = uriString[..commaIndex].Split(';'); _contentType = headers[0]; - string dataString = uriString.Substring(commaIndex + 1); + var dataString = uriString[(commaIndex + 1)..]; _data = Convert.FromBase64String(dataString); } public override string ContentType { - get { return _contentType; } + get => _contentType; set { throw new NotSupportedException(); @@ -48,7 +48,7 @@ public override string ContentType public override long ContentLength { - get { return _data.Length; } + get => _data.Length; set { throw new NotSupportedException(); From 15e67543692830ef528f7ae7cb25e54b0162f5f7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 25 Apr 2025 11:59:31 +0800 Subject: [PATCH 71/79] Fix code comments --- Flow.Launcher/PublicAPIInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index d58e9cf5320..9f578ccef95 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -40,7 +40,7 @@ public class PublicAPIInstance : IPublicAPI, IRemovable private readonly Settings _settings; private readonly MainViewModel _mainVM; - // Must use getter to access Application.Current.Resources.MergedDictionaries so earlier + // Must use getter to avoid accessing Application.Current.Resources.MergedDictionaries so earlier in theme constructor private Theme _theme; private Theme Theme => _theme ??= Ioc.Default.GetRequiredService(); From fa49c088cc7f3147b7320f399577292a1a37c83e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 25 Apr 2025 17:05:41 +0800 Subject: [PATCH 72/79] Add ConfigureAwait(false) to slightly reduce the risk of deadlocks --- Flow.Launcher/PublicAPIInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 9f578ccef95..5b8e8c9af89 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -213,7 +213,7 @@ private static async Task RetryActionOnSTAThreadAsync(Action action, { try { - await Win32Helper.StartSTATaskAsync(action); + await Win32Helper.StartSTATaskAsync(action).ConfigureAwait(false); break; } catch (Exception e) From ebc81c09963a59b24103e8d10f60461f72fc3572 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 25 Apr 2025 21:36:53 +0800 Subject: [PATCH 73/79] Improve code quality --- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 5 ++++- .../SettingsPanePluginStoreViewModel.cs | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 31580fbe80a..d4eb02a909e 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -471,8 +471,11 @@ public interface IPublicAPI public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default); /// - /// Get the plugin manifest + /// Get the plugin manifest. /// + /// + /// If Flow cannot get manifest data, this could be null + /// /// public IReadOnlyList GetPluginManifest(); diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index 68c69f8411b..372fe0dc515 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -9,10 +9,22 @@ namespace Flow.Launcher.SettingPages.ViewModels; public partial class SettingsPanePluginStoreViewModel : BaseModel { - public string FilterText { get; set; } = string.Empty; + private string filterText = string.Empty; + public string FilterText + { + get => filterText; + set + { + if (filterText != value) + { + filterText = value; + OnPropertyChanged(); + } + } + } - public IList ExternalPlugins => - App.API.GetPluginManifest()?.Select(p => new PluginStoreItemViewModel(p)) + public IList ExternalPlugins => App.API.GetPluginManifest()? + .Select(p => new PluginStoreItemViewModel(p)) .OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease) .ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated) .ThenByDescending(p => p.Category == PluginStoreItemViewModel.None) @@ -31,7 +43,7 @@ private async Task RefreshExternalPluginsAsync() public bool SatisfiesFilter(PluginStoreItemViewModel plugin) { return string.IsNullOrEmpty(FilterText) || - App.API.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() || - App.API.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet(); + App.API.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet(); } } From 593a36059e86dc4f157b779a55b7127580acb55a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 25 Apr 2025 21:42:37 +0800 Subject: [PATCH 74/79] Remove selected item binding & Use filter instead of data change --- .../SettingsPanePluginsViewModel.cs | 33 ++++++++++++------- .../Views/SettingsPanePlugins.xaml | 9 +++-- .../Views/SettingsPanePlugins.xaml.cs | 31 ++++++++++++++++- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index de7cf15c399..abb314355b4 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -86,12 +86,22 @@ public SettingsPanePluginsViewModel(Settings settings) UpdateEnumDropdownLocalizations(); } - public string FilterText { get; set; } = string.Empty; - - public PluginViewModel? SelectedPlugin { get; set; } + private string filterText = string.Empty; + public string FilterText + { + get => filterText; + set + { + if (filterText != value) + { + filterText = value; + OnPropertyChanged(); + } + } + } - private IEnumerable? _pluginViewModels; - private IEnumerable PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins + private IList? _pluginViewModels; + public IList PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins .OrderBy(plugin => plugin.Metadata.Disabled) .ThenBy(plugin => plugin.Metadata.Name) .Select(plugin => new PluginViewModel @@ -102,13 +112,12 @@ public SettingsPanePluginsViewModel(Settings settings) .Where(plugin => plugin.PluginSettingsObject != null) .ToList(); - public List FilteredPluginViewModels => PluginViewModels - .Where(v => - string.IsNullOrEmpty(FilterText) || - App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() || - App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet() - ) - .ToList(); + public bool SatisfiesFilter(PluginViewModel plugin) + { + return string.IsNullOrEmpty(FilterText) || + App.API.FuzzySearch(FilterText, plugin.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(FilterText, plugin.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet(); + } [RelayCommand] private async Task OpenHelperAsync(Button button) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml index bb742f800e0..52d77f91418 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml @@ -15,6 +15,12 @@ FocusManager.FocusedElement="{Binding ElementName=PluginFilterTextbox}" KeyDown="SettingsPanePlugins_OnKeyDown" mc:Ignorable="d"> + + + @@ -115,10 +121,9 @@ Background="{DynamicResource Color01B}" FontSize="14" ItemContainerStyle="{StaticResource PluginList}" - ItemsSource="{Binding FilteredPluginViewModels}" + ItemsSource="{Binding Source={StaticResource PluginCollectionView}}" ScrollViewer.CanContentScroll="False" ScrollViewer.HorizontalScrollBarVisibility="Disabled" - SelectedItem="{Binding SelectedPlugin}" SnapsToDevicePixels="True" Style="{DynamicResource PluginListStyle}" VirtualizingPanel.ScrollUnit="Pixel" diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs index 97574b3ce48..e9490804ab5 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs @@ -1,7 +1,10 @@ -using System.Windows.Input; +using System.ComponentModel; +using System.Windows.Data; +using System.Windows.Input; using System.Windows.Navigation; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.ViewModel; namespace Flow.Launcher.SettingPages.Views; @@ -17,12 +20,38 @@ protected override void OnNavigatedTo(NavigationEventArgs e) DataContext = _viewModel; InitializeComponent(); } + _viewModel.PropertyChanged += ViewModel_PropertyChanged; base.OnNavigatedTo(e); } + private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SettingsPanePluginsViewModel.FilterText)) + { + ((CollectionViewSource)FindResource("PluginCollectionView")).View.Refresh(); + } + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + _viewModel.PropertyChanged -= ViewModel_PropertyChanged; + base.OnNavigatingFrom(e); + } + private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e) { if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return; PluginFilterTextbox.Focus(); } + + private void PluginCollectionView_OnFilter(object sender, FilterEventArgs e) + { + if (e.Item is not PluginViewModel plugin) + { + e.Accepted = false; + return; + } + + e.Accepted = _viewModel.SatisfiesFilter(plugin); + } } From 4283ef55a29004d2ddef9f0435bd864e8664f731 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 27 Apr 2025 13:46:49 +0800 Subject: [PATCH 75/79] Enlarge update flow number --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index cdb313db3fe..fc3635381f6 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -44,7 +44,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows - private readonly SemaphoreSlim _updateLock = new(1, 1); // Used to ensure one updating flow + private readonly SemaphoreSlim _updateLock = new(1, 6); // Used to ensure up to six update flows private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; From 03477035b2238c50e85c207b6da678bc0d095b38 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 27 Apr 2025 14:21:07 +0800 Subject: [PATCH 76/79] Remove update lock --- Flow.Launcher/ViewModel/MainViewModel.cs | 40 +++++------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index fc3635381f6..d8149e9a975 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -44,7 +44,6 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows - private readonly SemaphoreSlim _updateLock = new(1, 6); // Used to ensure up to six update flows private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -1222,41 +1221,20 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { Infrastructure.Logger.Log.Debug(ClassName, $"Query null for QueryText"); - // Hide and clear results fast because results are already invalid although query is still running + // Hide and clear results again because running query may show and add some results Results.Visibility = Visibility.Collapsed; Results.Clear(); - // Hide progress bar because running query is already invalid - ProgressBarVisibility = Visibility.Hidden; + // Reset plugin icon + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; - // Wait last query to be canceled and then reset UI elements - await _updateLock.WaitAsync(CancellationToken.None); - try - { - Infrastructure.Logger.Log.Debug(ClassName, $"Clear for QueryText"); - - // Hide and clear results again because running query may show and add some results - Results.Visibility = Visibility.Collapsed; - Results.Clear(); - - // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; - - // Hide progress bar again because running query may set this to visible - ProgressBarVisibility = Visibility.Hidden; - } - finally - { - _updateLock.Release(); - } + // Hide progress bar again because running query may set this to visible + ProgressBarVisibility = Visibility.Hidden; return; } - Infrastructure.Logger.Log.Debug(ClassName, $"Wait for QueryText: {query.RawQuery}"); - - await _updateLock.WaitAsync(CancellationToken.None); try { // Check if the query has changed because query can be changed so fast that @@ -1379,9 +1357,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b Infrastructure.Logger.Log.Debug(ClassName, $"Query return for QueryText: {query.RawQuery}"); // this make sures running query is null even if the query is canceled _runningQuery = null; - - // release the lock so that other query can be executed - _updateLock.Release(); } // Local function @@ -1865,7 +1840,6 @@ protected virtual void Dispose(bool disposing) { _resultsViewUpdateTask.Dispose(); } - _updateLock?.Dispose(); _disposed = true; } } From ebaffc623f5614d44ddca18666b9a0ec974ada33 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 27 Apr 2025 14:36:07 +0800 Subject: [PATCH 77/79] Fix result update issue --- Flow.Launcher/ViewModel/MainViewModel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index d8149e9a975..92244acc5fc 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -32,7 +32,8 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private static readonly string ClassName = nameof(MainViewModel); private Query _lastQuery; - private Query _runningQuery; + private Query _runningQuery; // Used for QueryResultAsync + private Query _currentQuery; // Used for ResultsUpdated private string _queryTextBeforeLeaveResults; private readonly FlowLauncherJsonStorage _historyItemsStorage; @@ -237,7 +238,7 @@ public void RegisterResultsUpdatedEvent() { Infrastructure.Logger.Log.Debug(ClassName, $"Call IResultsUpdated for QueryText: {e.Query.RawQuery}"); - if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || e.Token.IsCancellationRequested) + if (_currentQuery == null || e.Query.RawQuery != _currentQuery.RawQuery || e.Token.IsCancellationRequested) { Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 6: {e.Query.RawQuery}"); return; @@ -258,7 +259,7 @@ public void RegisterResultsUpdatedEvent() PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); - if (_runningQuery == null || e.Query.RawQuery != _runningQuery.RawQuery || token.IsCancellationRequested) + if (_currentQuery == null || e.Query.RawQuery != _currentQuery.RawQuery || token.IsCancellationRequested) { Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 7: {e.Query.RawQuery}"); return; @@ -1253,6 +1254,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b Infrastructure.Logger.Log.Debug(ClassName, $"ProgressBar: {Visibility.Hidden}"); _runningQuery = query; + _currentQuery = query; // Switch to ThreadPool thread await TaskScheduler.Default; From 68e1aadabe443e515c1f3c0bb3a9e73a63f578a4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 30 Apr 2025 20:43:03 +0800 Subject: [PATCH 78/79] Revert logic change --- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 14 +- Flow.Launcher.Plugin/Query.cs | 10 +- Flow.Launcher.Test/QueryBuilderTest.cs | 6 +- Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 221 +++++++--------------- 5 files changed, 83 insertions(+), 170 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 2d9acc45b33..0ef3f30f5e1 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -6,13 +6,12 @@ namespace Flow.Launcher.Core.Plugin { public static class QueryBuilder { - public static Query Build(string input, string text, Dictionary nonGlobalPlugins) + public static Query Build(string text, Dictionary 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; } @@ -22,24 +21,21 @@ public static Query Build(string input, string text, Dictionary 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; } - return new Query () + 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 24f4b597c02..c3eede4c6b6 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -7,12 +7,6 @@ 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. @@ -70,10 +64,10 @@ public class Query /// [JsonIgnore] public string FirstSearch => SplitSearch(0); - + [JsonIgnore] private string _secondToEndSearch; - + /// /// strings from second search (including) to last search /// diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index 3912f26a7d3..c8ac17748da 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", "> ping google.com -n 20 -6", nonGlobalPlugins); + Query q = QueryBuilder.Build("> 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", "> ping google.com -n 20 -6", nonGlobalPlugins); + Query q = QueryBuilder.Build("> 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", "file.txt file2 file3", new Dictionary()); + Query q = QueryBuilder.Build("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 90260df533f..ae7b098a206 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -408,7 +408,7 @@ private void OnKeyDown(object sender, KeyEventArgs e) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) { var queryWithoutActionKeyword = - QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + QueryBuilder.Build(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 92244acc5fc..aa41cacbfb1 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -31,9 +31,8 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private static readonly string ClassName = nameof(MainViewModel); + private bool _isQueryRunning; private Query _lastQuery; - private Query _runningQuery; // Used for QueryResultAsync - private Query _currentQuery; // Used for ResultsUpdated private string _queryTextBeforeLeaveResults; private readonly FlowLauncherJsonStorage _historyItemsStorage; @@ -236,11 +235,8 @@ public void RegisterResultsUpdatedEvent() var plugin = (IResultUpdated)pair.Plugin; plugin.ResultsUpdated += (s, e) => { - Infrastructure.Logger.Log.Debug(ClassName, $"Call IResultsUpdated for QueryText: {e.Query.RawQuery}"); - - if (_currentQuery == null || e.Query.RawQuery != _currentQuery.RawQuery || e.Token.IsCancellationRequested) + if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested) { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 6: {e.Query.RawQuery}"); return; } @@ -259,21 +255,13 @@ public void RegisterResultsUpdatedEvent() PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); - if (_currentQuery == null || e.Query.RawQuery != _currentQuery.RawQuery || token.IsCancellationRequested) - { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 7: {e.Query.RawQuery}"); - return; - } + if (token.IsCancellationRequested) return; if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, token))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } - else - { - Infrastructure.Logger.Log.Debug(ClassName, $"Write updates for QueryText 1: {e.Query.RawQuery}"); - } }; } } @@ -320,7 +308,6 @@ public void ReQuery() { if (QueryResultsSelected()) { - Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {true}"); // When we are re-querying, we should not delay the query _ = QueryResultsAsync(false, isReQuery: true); } @@ -329,7 +316,6 @@ public void ReQuery() public void ReQuery(bool reselect) { BackToQueryResults(); - Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {false}, Is Requery: {true}, Reselect: {reselect}"); // When we are re-querying, we should not delay the query _ = QueryResultsAsync(false, isReQuery: true, reSelect: reselect); } @@ -379,7 +365,7 @@ private void LoadContextMenu() [RelayCommand] private void Backspace(object index) { - var query = QueryBuilder.Build(QueryText, QueryText.Trim(), PluginManager.NonGlobalPlugins); + var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); // GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\')); @@ -1092,7 +1078,6 @@ public void Query(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) { - Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); _ = QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) @@ -1109,7 +1094,6 @@ private async Task QueryAsync(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) { - Infrastructure.Logger.Log.Debug(ClassName, $"Search Delay: {searchDelay}, Is Requery: {isReQuery}, Reselect: {true}"); await QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) @@ -1212,16 +1196,11 @@ private void QueryHistory() private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { _updateSource?.Cancel(); - _runningQuery = null; - - Infrastructure.Logger.Log.Debug(ClassName, $"Query construct for QueryText: {QueryText}"); var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); if (query == null) // shortcut expanded { - Infrastructure.Logger.Log.Debug(ClassName, $"Query null for QueryText"); - // Hide and clear results again because running query may show and add some results Results.Visibility = Visibility.Collapsed; Results.Clear(); @@ -1236,129 +1215,89 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } - 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 && query.RawQuery != QueryText.Trim()) - { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 0: {query.RawQuery}"); - return; - } - - _updateSource = new CancellationTokenSource(); - - ProgressBarVisibility = Visibility.Hidden; - - Infrastructure.Logger.Log.Debug(ClassName, $"Start for QueryText: {query.RawQuery}"); - Infrastructure.Logger.Log.Debug(ClassName, $"ProgressBar: {Visibility.Hidden}"); - - _runningQuery = query; - _currentQuery = query; + _updateSource = new CancellationTokenSource(); - // Switch to ThreadPool thread - await TaskScheduler.Default; + ProgressBarVisibility = Visibility.Hidden; + _isQueryRunning = true; - if (_updateSource.Token.IsCancellationRequested) - { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 1: {query.RawQuery}"); - 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 (_updateSource.Token.IsCancellationRequested) return; - // handle the exclusiveness of plugin using action keyword - RemoveOldQueryResults(query); + // Update the query's IsReQuery property to true if this is a re-query + query.IsReQuery = isReQuery; - Infrastructure.Logger.Log.Debug(ClassName, $"Remove old for QueryText: {query.RawQuery}"); + // handle the exclusiveness of plugin using action keyword + RemoveOldQueryResults(query); - _lastQuery = query; + _lastQuery = query; - var plugins = PluginManager.ValidPluginsForQuery(query); + var plugins = PluginManager.ValidPluginsForQuery(query); - Infrastructure.Logger.Log.Debug(ClassName, $"Valid {plugins.Count} plugins QueryText: {query.RawQuery}"); + 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; + } - 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; + }*/ - // Do not wait for performance improvement - /*if (string.IsNullOrEmpty(query.ActionKeyword)) + _ = Task.Delay(200, _updateSource.Token).ContinueWith(_ => { - // 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(_ => + // 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) { - Infrastructure.Logger.Log.Debug(ClassName, $"Check ProgressBar for QueryText: running: {_runningQuery?.RawQuery ?? "null"} query: {query.RawQuery}"); - - // 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 (_runningQuery != null && _runningQuery == query) - { - ProgressBarVisibility = Visibility.Visible; - - Infrastructure.Logger.Log.Debug(ClassName, $"ProgressBar: {Visibility.Visible}"); - } - }, - _updateSource.Token, - TaskContinuationOptions.NotOnCanceled, - TaskScheduler.Default); - - // plugins are ICollection, meaning LINQ will get the Count and preallocate Array + ProgressBarVisibility = Visibility.Visible; + } + }, + _updateSource.Token, + TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default); - var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch - { - false => QueryTaskAsync(plugin, _updateSource.Token), - true => Task.CompletedTask - }).ToArray(); + // plugins are ICollection, meaning LINQ will get the Count and preallocate Array - 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 - } + var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + { + false => QueryTaskAsync(plugin, _updateSource.Token), + true => Task.CompletedTask + }).ToArray(); - if (_updateSource.Token.IsCancellationRequested) - { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 2: {query.RawQuery}"); - return; - } + 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 + } - // this should happen once after all queries are done so progress bar should continue - // until the end of all querying - _runningQuery = null; + if (_updateSource.Token.IsCancellationRequested) return; - if (!_updateSource.Token.IsCancellationRequested) - { - // update to hidden if this is still the current query - ProgressBarVisibility = Visibility.Hidden; + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _isQueryRunning = false; - Infrastructure.Logger.Log.Debug(ClassName, $"ProgressBar: {Visibility.Hidden}"); - } - } - finally + if (!_updateSource.Token.IsCancellationRequested) { - Infrastructure.Logger.Log.Debug(ClassName, $"Query return for QueryText: {query.RawQuery}"); - // this make sures running query is null even if the query is canceled - _runningQuery = null; + // update to hidden if this is still the current query + ProgressBarVisibility = Visibility.Hidden; } // Local function @@ -1370,11 +1309,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) await Task.Delay(searchDelayTime, token); - if (token.IsCancellationRequested) - { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 3: {QueryText}"); - return; - } + if (token.IsCancellationRequested) return; } // Since it is wrapped within a ThreadPool Thread, the synchronous context is null @@ -1383,11 +1318,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) var results = await PluginManager.QueryForPluginAsync(plugin, query, token); - if (token.IsCancellationRequested) - { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 4: {query.RawQuery}"); - return; - } + if (token.IsCancellationRequested) return; IReadOnlyList resultsCopy; if (results == null) @@ -1408,21 +1339,13 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } } - if (token.IsCancellationRequested) - { - Infrastructure.Logger.Log.Debug(ClassName, $"Cancel for QueryText 5: {query.RawQuery}"); - return; - } + if (token.IsCancellationRequested) return; if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, token, reSelect))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } - else - { - Infrastructure.Logger.Log.Debug(ClassName, $"Write updates for QueryText: {query.RawQuery}"); - } } } @@ -1451,7 +1374,7 @@ private Query ConstructQuery(string queryText, IEnumerable // Applying builtin shortcuts BuildQuery(builtInShortcuts, queryBuilder, queryBuilderTmp); - return QueryBuilder.Build(queryText, queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); + return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); } private void BuildQuery(IEnumerable builtInShortcuts, From d023ecddd87983ad89bcf17ef051ea42bc79b1c9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 2 May 2025 10:51:52 +0800 Subject: [PATCH 79/79] Make ConstructQuery async --- Flow.Launcher/App.xaml.cs | 1 - Flow.Launcher/ViewModel/MainViewModel.cs | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 9146d243455..402812a92d7 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -32,7 +32,6 @@ public partial class App : IDisposable, ISingleInstanceApp #region Public Properties public static IPublicAPI API { get; private set; } - public static JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); public static bool Exiting => _mainWindow.CanClose; #endregion diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index aa41cacbfb1..2f1ed0f5103 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1197,7 +1197,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { _updateSource?.Cancel(); - var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); + var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); if (query == null) // shortcut expanded { @@ -1349,7 +1349,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } } - private Query ConstructQuery(string queryText, IEnumerable customShortcuts, + private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, IEnumerable builtInShortcuts) { if (string.IsNullOrWhiteSpace(queryText)) @@ -1372,12 +1372,12 @@ private Query ConstructQuery(string queryText, IEnumerable } // Applying builtin shortcuts - BuildQuery(builtInShortcuts, queryBuilder, queryBuilderTmp); + await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp); return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); } - private void BuildQuery(IEnumerable builtInShortcuts, + private async Task BuildQueryAsync(IEnumerable builtInShortcuts, StringBuilder queryBuilder, StringBuilder queryBuilderTmp) { var customExpanded = queryBuilder.ToString(); @@ -1397,7 +1397,7 @@ private void BuildQuery(IEnumerable builtInShortcuts, } else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) { - expansion = App.JTF.Run(() => asyncShortcut.ExpandAsync()); + expansion = await asyncShortcut.ExpandAsync(); } else {