diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 78464ddc9..56abfb251 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -30,7 +30,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 = []; @@ -333,7 +333,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; } } @@ -369,7 +381,7 @@ namespace Flow.Launcher.Core.Plugin if (query is null) return Array.Empty(); - if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) + if (!TryGetNonGlobalPlugins(query.ActionKeyword, out var plugins)) { if (dialogJump) return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; @@ -377,13 +389,25 @@ 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 => !p.Metadata.Disabled && !PluginModified(p.Metadata.ID)); + if (dialogJump) + validPlugins = validPlugins.Where(p => p.Plugin is IAsyncDialogJump); - if (PluginModified(plugin.Metadata.ID)) - return Array.Empty(); + return [.. validPlugins]; + } - return [plugin]; + 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() @@ -577,9 +601,17 @@ namespace Flow.Launcher.Core.Plugin return [.. _globalPlugins.Values]; } - public static Dictionary GetNonGlobalPlugins() + public static Dictionary> GetNonGlobalPlugins() { - return _nonGlobalPlugins.ToDictionary(); + var nonGlobalPlugins = new Dictionary>(); + foreach (var kvp in _nonGlobalPlugins) + { + lock (kvp.Value) + { + nonGlobalPlugins.Add(kvp.Key, [.. kvp.Value]); + } + } + return nonGlobalPlugins; } public static List GetTranslationPlugins() @@ -722,12 +754,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; } /// @@ -737,17 +769,34 @@ 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 - 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]; @@ -765,6 +814,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 @@ -775,11 +826,22 @@ 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); + + if (plugins.Count == 0) + { + _nonGlobalPlugins.TryRemove(new KeyValuePair>(oldActionkeyword, plugins)); + } + } + } } // 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]; @@ -1063,10 +1125,18 @@ 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); + + if (entry.Value.Count == 0) + { + _nonGlobalPlugins.TryRemove(new KeyValuePair>(entry.Key, entry.Value)); + } + } } } diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index aac620cce..69d75616e 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,7 @@ 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) && CheckPlugin(pluginPairs)) { // use non global plugin for query actionKeyword = possibleActionKeyword; @@ -59,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); + } + } } } diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 93844159f..e67c844aa 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -293,6 +293,11 @@ namespace Flow.Launcher.Plugin /// /// The actionkeyword for checking /// True if the actionkeyword is already assigned, False otherwise + /// + /// 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); /// diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index 0ede781f8..d8e8e4973 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 = [">"], Disabled = true } } } } }; 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); 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);