From 39b2869cae7a1ba7ce1402b2be52825b91859607 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 16:33:39 +0800 Subject: [PATCH 01/17] Deprecate ActionKeywordRegistered, update API docs Mark ActionKeywordRegistered as obsolete and always return false, reflecting support for multiple plugins per action keyword. Update IPublicAPI docs to clarify ActionKeywordAssigned is for legacy compatibility. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 8 ++++---- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index b808e2a7f..70a9c9c0f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -721,12 +721,12 @@ namespace Flow.Launcher.Core.Plugin #region Plugin Action Keyword + [Obsolete("This method is only used for old Flow compatibility.")] public static bool ActionKeywordRegistered(string actionKeyword) { - // this method is only checking for action keywords (defined as not '*') registration - // hence the actionKeyword != Query.GlobalPluginWildcardSign logic - return actionKeyword != Query.GlobalPluginWildcardSign - && _nonGlobalPlugins.ContainsKey(actionKeyword); + // Since now we support to assign one action keyword to multiple plugins, + // this check is unnecessary, so we will just return false here to ensure compatibility for old plugins. + return false; } /// diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 93844159f..b15aa844e 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -293,6 +293,10 @@ namespace Flow.Launcher.Plugin /// /// The actionkeyword for checking /// True if the actionkeyword is already assigned, False otherwise + /// + /// Flow now supports to one action keyword to multiple plugins, + /// so this method is only used for old Flow compatibility. + /// bool ActionKeywordAssigned(string actionKeyword); /// From f05d31a1c134ce5291c5503b98064803b35d86b6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 16:43:55 +0800 Subject: [PATCH 02/17] Add ActionKeywordAssigned method and new using directives Expanded using directives for .NET collections and diagnostics. Added ActionKeywordAssigned to PublicAPIInstance, using obsolete PluginManager.ActionKeywordRegistered with warning suppression. --- Flow.Launcher/PublicAPIInstance.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 55737151a..a1b9cd614 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; @@ -267,7 +267,9 @@ namespace Flow.Launcher public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword); +#pragma warning disable CS0618 // Type or member is obsolete public bool ActionKeywordAssigned(string actionKeyword) => PluginManager.ActionKeywordRegistered(actionKeyword); +#pragma warning restore CS0618 // Type or member is obsolete public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword); From b8bc7fa82db6efb89c0865a9cb6809b4982fdd8f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 16:54:03 +0800 Subject: [PATCH 03/17] Support multiple plugins per action keyword Refactor non-global plugin storage to allow multiple plugins to share the same action keyword by using ConcurrentDictionary>. Update all relevant methods to handle lists of plugins, ensure thread safety, and adjust the QueryBuilder logic accordingly. This change improves extensibility and flexibility in plugin management. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 65 +++++++++++++++++----- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 6 +- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 70a9c9c0f..e837843f4 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -29,7 +29,7 @@ namespace Flow.Launcher.Core.Plugin private static readonly ConcurrentDictionary _allInitializedPlugins = []; private static readonly ConcurrentDictionary _initFailedPlugins = []; private static readonly ConcurrentDictionary _globalPlugins = []; - private static readonly ConcurrentDictionary _nonGlobalPlugins = []; + private static readonly ConcurrentDictionary> _nonGlobalPlugins = []; private static PluginsSettings Settings; private static readonly ConcurrentBag ModifiedPlugins = []; @@ -332,7 +332,19 @@ namespace Flow.Launcher.Core.Plugin _globalPlugins.TryAdd(pair.Metadata.ID, pair); break; default: - _nonGlobalPlugins.TryAdd(actionKeyword, pair); + _nonGlobalPlugins.AddOrUpdate(actionKeyword, + _ => [pair], + (_, existing) => + { + lock (existing) + { + if (!existing.Contains(pair)) + { + existing.Add(pair); + } + } + return existing; + }); break; } } @@ -368,7 +380,7 @@ namespace Flow.Launcher.Core.Plugin if (query is null) return Array.Empty(); - if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) + if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugins)) { if (dialogJump) return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; @@ -376,13 +388,11 @@ namespace Flow.Launcher.Core.Plugin return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))]; } - if (dialogJump && plugin.Plugin is not IAsyncDialogJump) - return Array.Empty(); + var validPlugins = plugins.Where(p => !PluginModified(p.Metadata.ID)); + if (dialogJump) + validPlugins = validPlugins.Where(p => p.Plugin is IAsyncDialogJump); - if (PluginModified(plugin.Metadata.ID)) - return Array.Empty(); - - return [plugin]; + return [.. validPlugins]; } public static ICollection ValidPluginsForHomeQuery() @@ -576,7 +586,7 @@ namespace Flow.Launcher.Core.Plugin return [.. _globalPlugins.Values]; } - public static Dictionary GetNonGlobalPlugins() + public static Dictionary> GetNonGlobalPlugins() { return _nonGlobalPlugins.ToDictionary(); } @@ -736,13 +746,27 @@ namespace Flow.Launcher.Core.Plugin public static void AddActionKeyword(string id, string newActionKeyword) { var plugin = GetPluginForId(id); + if (plugin == null) return; + if (newActionKeyword == Query.GlobalPluginWildcardSign) { _globalPlugins.TryAdd(id, plugin); } else { - _nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin); + _nonGlobalPlugins.AddOrUpdate(newActionKeyword, + _ => [plugin], + (_, existing) => + { + lock (existing) + { + if (!existing.Contains(plugin)) + { + existing.Add(plugin); + } + } + return existing; + }); } // Update action keywords and action keyword in plugin metadata @@ -764,6 +788,8 @@ namespace Flow.Launcher.Core.Plugin public static void RemoveActionKeyword(string id, string oldActionkeyword) { var plugin = GetPluginForId(id); + if (plugin == null) return; + if (oldActionkeyword == Query.GlobalPluginWildcardSign && // Plugins may have multiple ActionKeywords that are global, eg. WebSearch plugin.Metadata.ActionKeywords @@ -774,7 +800,13 @@ namespace Flow.Launcher.Core.Plugin if (oldActionkeyword != Query.GlobalPluginWildcardSign) { - _nonGlobalPlugins.TryRemove(oldActionkeyword, out _); + if (_nonGlobalPlugins.TryGetValue(oldActionkeyword, out var plugins)) + { + lock (plugins) + { + plugins.RemoveAll(p => p.Metadata.ID == id); + } + } } // Update action keywords and action keyword in plugin metadata @@ -1032,10 +1064,13 @@ namespace Flow.Launcher.Core.Plugin { _globalPlugins.TryRemove(plugin.ID, out var _); } - var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); - foreach (var key in keysToRemove) + var entriesToUpdate = _nonGlobalPlugins.ToList(); + foreach (var entry in entriesToUpdate) { - _nonGlobalPlugins.TryRemove(key, out var _); + lock (entry.Value) + { + entry.Value.RemoveAll(p => p.Metadata.ID == plugin.ID); + } } } diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index aac620cce..3b38a56b7 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { public static class QueryBuilder { - public static Query Build(string originalQuery, string trimmedQuery, Dictionary nonGlobalPlugins) + public static Query Build(string originalQuery, string trimmedQuery, Dictionary> nonGlobalPlugins) { // home query if (string.IsNullOrEmpty(trimmedQuery)) @@ -34,7 +35,8 @@ namespace Flow.Launcher.Core.Plugin string possibleActionKeyword = terms[0]; string[] searchTerms; - if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) + if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPairs) + && pluginPairs.Any(plugin => !plugin.Metadata.Disabled)) { // use non global plugin for query actionKeyword = possibleActionKeyword; From 8760fe29eeded1c43d6e016ae531ea1443903954 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:05:12 +0800 Subject: [PATCH 04/17] Return deep copy in GetNonGlobalPlugins to protect state GetNonGlobalPlugins now returns a new dictionary with copied lists, preventing external modification of the internal _nonGlobalPlugins collection. This change improves encapsulation and safeguards plugin manager state integrity. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index e837843f4..ddc3a2d52 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -588,7 +588,12 @@ namespace Flow.Launcher.Core.Plugin public static Dictionary> GetNonGlobalPlugins() { - return _nonGlobalPlugins.ToDictionary(); + var nonGlobalPlugins = new Dictionary>(); + foreach (var kvp in _nonGlobalPlugins) + { + nonGlobalPlugins.Add(kvp.Key, [.. kvp.Value]); + } + return nonGlobalPlugins; } public static List GetTranslationPlugins() From 452e60f3b5dbdc0597d78df6ae8adf95d448188e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:07:09 +0800 Subject: [PATCH 05/17] Use GetNonGlobalPlugins() instead of field access Replaced direct _nonGlobalPlugins field access with the GetNonGlobalPlugins() method to improve encapsulation and ensure up-to-date plugin data is used when retrieving non-global plugins by action keyword. No other logic was changed. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index ddc3a2d52..13308c233 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -380,7 +380,7 @@ namespace Flow.Launcher.Core.Plugin if (query is null) return Array.Empty(); - if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugins)) + if (!GetNonGlobalPlugins().TryGetValue(query.ActionKeyword, out var plugins)) { if (dialogJump) return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; From 3b771d155da131e856112410448c96c869c48c99 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:07:41 +0800 Subject: [PATCH 06/17] Add locking for thread safety in GetNonGlobalPlugins Wrap kvp.Value access in a lock when copying to nonGlobalPlugins to prevent race conditions and ensure thread safety during concurrent access. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 13308c233..0223affeb 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -591,7 +591,10 @@ namespace Flow.Launcher.Core.Plugin var nonGlobalPlugins = new Dictionary>(); foreach (var kvp in _nonGlobalPlugins) { - nonGlobalPlugins.Add(kvp.Key, [.. kvp.Value]); + lock (kvp.Value) + { + nonGlobalPlugins.Add(kvp.Key, [.. kvp.Value]); + } } return nonGlobalPlugins; } From 1294d722f2b33880d4488e99caaf017cf2bc8738 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:12:46 +0800 Subject: [PATCH 07/17] Improve action keyword management in plugin metadata Prevent duplicate action keywords by checking for existence before adding. Remove all instances of an old action keyword instead of just the first. Ensures action keyword lists remain unique and consistent. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 0223affeb..c7443ff4c 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -778,7 +778,10 @@ namespace Flow.Launcher.Core.Plugin } // Update action keywords and action keyword in plugin metadata - plugin.Metadata.ActionKeywords.Add(newActionKeyword); + if (!plugin.Metadata.ActionKeywords.Contains(newActionKeyword)) + { + plugin.Metadata.ActionKeywords.Add(newActionKeyword); + } if (plugin.Metadata.ActionKeywords.Count > 0) { plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0]; @@ -818,7 +821,7 @@ namespace Flow.Launcher.Core.Plugin } // Update action keywords and action keyword in plugin metadata - plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); + plugin.Metadata.ActionKeywords.RemoveAll(k => k == oldActionkeyword); if (plugin.Metadata.ActionKeywords.Count > 0) { plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0]; From 43a74ba8e634fa0144d49d06bb6f51302bcadb75 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Thu, 26 Feb 2026 17:13:49 +0800 Subject: [PATCH 08/17] Fix code comments Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index b15aa844e..79be520a7 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -294,8 +294,8 @@ namespace Flow.Launcher.Plugin /// The actionkeyword for checking /// True if the actionkeyword is already assigned, False otherwise /// - /// Flow now supports to one action keyword to multiple plugins, - /// so this method is only used for old Flow compatibility. + /// Flow now supports assigning one action keyword to multiple plugins. + /// This method is kept only for legacy Flow compatibility. /// bool ActionKeywordAssigned(string actionKeyword); From 2529efeed11f59ec4c27ea211e02ca03a4bf1190 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:15:29 +0800 Subject: [PATCH 09/17] Filter out disabled plugins from valid plugin list Previously, the code only excluded modified plugins from the valid plugin list. This update adds an additional check to also exclude plugins marked as disabled in their metadata, ensuring that disabled plugins are not considered valid or processed further. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index c7443ff4c..9bd573179 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -388,7 +388,7 @@ namespace Flow.Launcher.Core.Plugin return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))]; } - var validPlugins = plugins.Where(p => !PluginModified(p.Metadata.ID)); + var validPlugins = plugins.Where(p => !p.Metadata.Disabled && !PluginModified(p.Metadata.ID)); if (dialogJump) validPlugins = validPlugins.Where(p => p.Plugin is IAsyncDialogJump); From 4537013cde8acd362431a2106ba1e92fe98536d1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:19:36 +0800 Subject: [PATCH 10/17] Clean up empty plugin lists from _nonGlobalPlugins After removing plugins, also remove dictionary entries if their associated lists become empty. This prevents unused empty lists from accumulating and keeps the plugin manager's state clean. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 9bd573179..a4fcb866e 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -816,6 +816,11 @@ namespace Flow.Launcher.Core.Plugin lock (plugins) { plugins.RemoveAll(p => p.Metadata.ID == id); + + if (plugins.Count == 0) + { + _nonGlobalPlugins.TryRemove(new KeyValuePair>(oldActionkeyword, plugins)); + } } } } @@ -1081,6 +1086,11 @@ namespace Flow.Launcher.Core.Plugin lock (entry.Value) { entry.Value.RemoveAll(p => p.Metadata.ID == plugin.ID); + + if (entry.Value.Count == 0) + { + _nonGlobalPlugins.TryRemove(entry.Key, out var __); + } } } } From 17d675e5ce9dce524647c6fb49324b76080c30b4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:22:00 +0800 Subject: [PATCH 11/17] Update QueryBuilder tests for new nonGlobalPlugins type Refactored QueryBuilderTest.cs to use Dictionary> for nonGlobalPlugins, updating test cases to use lists of PluginPair. Also replaced empty dictionary instantiation with shorthand [] where appropriate. --- Flow.Launcher.Test/QueryBuilderTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index 0ede781f8..0064f23e5 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -11,9 +11,9 @@ namespace Flow.Launcher.Test [Test] public void ExclusivePluginQueryTest() { - var nonGlobalPlugins = new Dictionary + var nonGlobalPlugins = new Dictionary> { - {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}}}} + { ">", new List(){ new() { Metadata = new PluginMetadata { ActionKeywords = [">"] } } } } }; Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins); @@ -34,9 +34,9 @@ namespace Flow.Launcher.Test [Test] public void ExclusivePluginQueryIgnoreDisabledTest() { - var nonGlobalPlugins = new Dictionary + var nonGlobalPlugins = new Dictionary> { - {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}, Disabled = true}}} + { ">", new List(){ new() { Metadata = new PluginMetadata { ActionKeywords = [">"] } } } } }; Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins); @@ -51,7 +51,7 @@ namespace Flow.Launcher.Test [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", "file.txt file2 file3", []); ClassicAssert.AreEqual("file.txt file2 file3", q.Search); ClassicAssert.AreEqual("", q.ActionKeyword); From 211eb8a747ea5b1ee13ca6764202ecc5cac78fa8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:24:41 +0800 Subject: [PATCH 12/17] Refactor plugin enabled check with thread safety Extract plugin enabled check into a new CheckPlugin method, adding a lock for thread safety. Update the if statement to use this method instead of inline logic. --- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 3b38a56b7..69d75616e 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -35,8 +35,7 @@ namespace Flow.Launcher.Core.Plugin string possibleActionKeyword = terms[0]; string[] searchTerms; - if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPairs) - && pluginPairs.Any(plugin => !plugin.Metadata.Disabled)) + if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPairs) && CheckPlugin(pluginPairs)) { // use non global plugin for query actionKeyword = possibleActionKeyword; @@ -61,5 +60,13 @@ namespace Flow.Launcher.Core.Plugin IsHomeQuery = false }; } + + private static bool CheckPlugin(List pluginPairs) + { + lock (pluginPairs) + { + return pluginPairs.Any(plugin => !plugin.Metadata.Disabled); + } + } } } From a90c787b4ad2eb3bb6a936e7c2a209beea536403 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Thu, 26 Feb 2026 17:37:05 +0800 Subject: [PATCH 13/17] Improve obsolete description Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 79be520a7..e67c844aa 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -297,6 +297,7 @@ namespace Flow.Launcher.Plugin /// Flow now supports assigning one action keyword to multiple plugins. /// This method is kept only for legacy Flow compatibility. /// + [Obsolete("Flow now supports assigning one action keyword to multiple plugins. This method always returns false for compatibility.")] bool ActionKeywordAssigned(string actionKeyword); /// From e103d649ed3e820eb7807a632a3a8c935d4049ba Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 26 Feb 2026 17:37:15 +0800 Subject: [PATCH 14/17] Update test to mark exclusive plugin as disabled ExclusivePluginQueryIgnoreDisabledTest now sets the ">" plugin's Disabled property to true in PluginMetadata. This verifies that QueryBuilder correctly ignores disabled exclusive plugins. --- Flow.Launcher.Test/QueryBuilderTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index 0064f23e5..d8e8e4973 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -36,7 +36,7 @@ namespace Flow.Launcher.Test { var nonGlobalPlugins = new Dictionary> { - { ">", new List(){ new() { Metadata = new PluginMetadata { ActionKeywords = [">"] } } } } + { ">", new List(){ new() { Metadata = new PluginMetadata { ActionKeywords = [">"], Disabled = true } } } } }; Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins); From baa3a690a309e6694106776fa6cc217372e0dbc3 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Thu, 26 Feb 2026 17:38:21 +0800 Subject: [PATCH 15/17] Fix potential race condition in the uninstall logic Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index a4fcb866e..199d4e7b1 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1089,7 +1089,7 @@ namespace Flow.Launcher.Core.Plugin if (entry.Value.Count == 0) { - _nonGlobalPlugins.TryRemove(entry.Key, out var __); + _nonGlobalPlugins.TryRemove(new KeyValuePair>(entry.Key, entry.Value)); } } } From 88ac19af7646b16aa4d895acd7c2b47046ab64d4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 1 Mar 2026 12:39:49 +0800 Subject: [PATCH 16/17] Refactor non-global plugin retrieval for performance optimization Refactored plugin lookup in ValidPluginsForQuery to use a new TryGetNonGlobalPlugins method, which safely copies plugin lists using a lock. This improves thread safety and performance when accessing non-global plugins. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 199d4e7b1..e6b2bc2a2 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -380,7 +380,7 @@ namespace Flow.Launcher.Core.Plugin if (query is null) return Array.Empty(); - if (!GetNonGlobalPlugins().TryGetValue(query.ActionKeyword, out var plugins)) + if (TryGetNonGlobalPlugins(query.ActionKeyword, out var plugins)) { if (dialogJump) return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; @@ -395,6 +395,20 @@ namespace Flow.Launcher.Core.Plugin return [.. validPlugins]; } + private static bool TryGetNonGlobalPlugins(string actionKeyword, out List plugins) + { + if (_nonGlobalPlugins.TryGetValue(actionKeyword, out var list)) + { + lock (list) + { + plugins = [.. list]; + } + return true; + } + plugins = []; + return false; + } + public static ICollection ValidPluginsForHomeQuery() { return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))]; From f10f71661978cb7e0a4e503d57528748fa39bf11 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Sun, 1 Mar 2026 12:43:29 +0800 Subject: [PATCH 17/17] Fix logic inversion bug Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index e6b2bc2a2..8a6ce0ee0 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -380,7 +380,7 @@ namespace Flow.Launcher.Core.Plugin if (query is null) return Array.Empty(); - if (TryGetNonGlobalPlugins(query.ActionKeyword, out var plugins)) + if (!TryGetNonGlobalPlugins(query.ActionKeyword, out var plugins)) { if (dialogJump) return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))];