From 80f8c0399bbdd8928d279e1fa9430becd69eb8e8 Mon Sep 17 00:00:00 2001 From: Alekhya Date: Fri, 21 Aug 2020 12:21:42 -0700 Subject: [PATCH] To remove the condition that space is needed after the Action keyword (#6003) * Returning individual queries for each plugin * Changed cancellation token from Query type to directly using the rawQuery * Changed the way we get the plugins for which we execute the query * updated UpdateResultView to take a string instead of query * Changed the way we set a query for each plugin * removed todo comment * global plugins are added as a part of the query builder * Fix for plugin.json of Folder plugin being copied into the shell plugin * >,< and : are not allowed in file paths and indexer creates a query which searches compares if a file name is greater than or lesser than the query * Reformatted the regex * catching the exception * fixed existing tests * modified it so that it works with action keyword as well as action keywords * Added unit tests for non global plugins * fixed test * add back whitespace that was removed by mistake * fix regex * modified the cold start query * remove extra condition * terms being modified as expected * used key value pairs to iterate through the dictionary * renamed variable * added check for an empty dictionary * remove : because it may appear in the file path * fix some whitespace warnings that were being treated as errors --- .../Plugins/Microsoft.Plugin.Indexer/Main.cs | 2 +- .../PowerLauncher/ViewModel/MainViewModel.cs | 62 +++--- .../launcher/Wox.Core/Plugin/QueryBuilder.cs | 66 ++++--- .../launcher/Wox.Test/QueryBuilderTest.cs | 178 ++++++++++++++++-- 4 files changed, 247 insertions(+), 61 deletions(-) diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs index 259a0d577e..5443265d9c 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs @@ -40,7 +40,7 @@ namespace Microsoft.Plugin.Indexer private readonly IndexerDriveDetection _driveDetection = new IndexerDriveDetection(new RegistryWrapper()); // Reserved keywords in oleDB - private readonly string reservedStringPattern = @"^[\/\\\$\%]+$"; + private readonly string reservedStringPattern = @"^[\/\\\$\%]+$|^.*[<>].*$"; private string WarningIconPath { get; set; } diff --git a/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs b/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs index 133b28f56f..9b6982f20a 100644 --- a/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs +++ b/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs @@ -29,7 +29,9 @@ namespace PowerLauncher.ViewModel { public class MainViewModel : BaseModel, ISavable, IDisposable { - private static readonly Query _emptyQuery = new Query(); + private string _currentQuery; + private static string _emptyQuery = string.Empty; + private static bool _disposed; private readonly WoxJsonStorage _historyItemsStorage; @@ -43,7 +45,6 @@ namespace PowerLauncher.ViewModel private readonly Internationalization _translator = InternationalizationManager.Instance; private readonly System.Diagnostics.Stopwatch _hotkeyTimer = new System.Diagnostics.Stopwatch(); - private Query _currentQuery; private string _queryTextBeforeLeaveResults; private CancellationTokenSource _updateSource; @@ -113,7 +114,7 @@ namespace PowerLauncher.ViewModel () => { PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); - UpdateResultView(e.Results, e.Query, _updateToken); + UpdateResultView(e.Results, e.Query.RawQuery, _updateToken); }, _updateToken); }; } @@ -461,24 +462,32 @@ namespace PowerLauncher.ViewModel _updateSource = currentUpdateSource; var currentCancellationToken = _updateSource.Token; _updateToken = currentCancellationToken; + var queryText = QueryText.Trim(); - var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); - if (query != null) + var pluginQueryPairs = QueryBuilder.Build(ref queryText, PluginManager.NonGlobalPlugins); + if (pluginQueryPairs != null && pluginQueryPairs.Count > 0) { - _currentQuery = query; + _currentQuery = queryText; Task.Run( () => { Thread.Sleep(20); - var plugins = PluginManager.ValidPluginsForQuery(query); + + // Contains all the plugins for which this raw query is valid + var plugins = pluginQueryPairs.Keys.ToList(); try { currentCancellationToken.ThrowIfCancellationRequested(); var resultPluginPair = new List<(List, PluginMetadata)>(); - foreach (PluginPair plugin in plugins) + + // To execute a query corresponding to each plugin + foreach (KeyValuePair pluginQueryItem in pluginQueryPairs) { + var plugin = pluginQueryItem.Key; + var query = pluginQueryItem.Value; + if (!plugin.Metadata.Disabled) { var results = PluginManager.QueryForPlugin(plugin, query); @@ -489,12 +498,12 @@ namespace PowerLauncher.ViewModel lock (_addResultsLock) { - if (query.RawQuery == _currentQuery.RawQuery) + if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) { Results.Clear(); foreach (var p in resultPluginPair) { - UpdateResultView(p.Item1, query, currentCancellationToken); + UpdateResultView(p.Item1, queryText, currentCancellationToken); currentCancellationToken.ThrowIfCancellationRequested(); } @@ -504,7 +513,8 @@ namespace PowerLauncher.ViewModel } currentCancellationToken.ThrowIfCancellationRequested(); - UpdateResultsListViewAfterQuery(query); + + UpdateResultsListViewAfterQuery(queryText); // Run the slower query of the DelayedExecution plugins currentCancellationToken.ThrowIfCancellationRequested(); @@ -514,13 +524,16 @@ namespace PowerLauncher.ViewModel { if (!plugin.Metadata.Disabled) { + Query query; + pluginQueryPairs.TryGetValue(plugin, out query); + var results = PluginManager.QueryForPlugin(plugin, query, true); currentCancellationToken.ThrowIfCancellationRequested(); if ((results?.Count ?? 0) != 0) { lock (_addResultsLock) { - if (query.RawQuery == _currentQuery.RawQuery) + if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) { currentCancellationToken.ThrowIfCancellationRequested(); @@ -529,14 +542,15 @@ namespace PowerLauncher.ViewModel currentCancellationToken.ThrowIfCancellationRequested(); // Add the new results from the plugin - UpdateResultView(results, query, currentCancellationToken); + UpdateResultView(results, queryText, currentCancellationToken); + currentCancellationToken.ThrowIfCancellationRequested(); Results.Sort(); } } currentCancellationToken.ThrowIfCancellationRequested(); - UpdateResultsListViewAfterQuery(query, true); + UpdateResultsListViewAfterQuery(queryText, true); } } } @@ -556,7 +570,7 @@ namespace PowerLauncher.ViewModel { QueryTimeMs = queryTimer.ElapsedMilliseconds, NumResults = Results.Results.Count, - QueryLength = query.RawQuery.Length, + QueryLength = queryText.Length, }; PowerToysTelemetry.Log.WriteEvent(queryEvent); }, currentCancellationToken); @@ -572,11 +586,11 @@ namespace PowerLauncher.ViewModel } } - private void UpdateResultsListViewAfterQuery(Query query, bool isDelayedInvoke = false) + private void UpdateResultsListViewAfterQuery(string queryText, bool isDelayedInvoke = false) { Application.Current.Dispatcher.BeginInvoke(new Action(() => { - if (query.RawQuery == _currentQuery.RawQuery) + if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) { Results.Results.NotifyChanges(); } @@ -740,7 +754,7 @@ namespace PowerLauncher.ViewModel /// /// To avoid deadlock, this method should not called from main thread /// - public void UpdateResultView(List list, Query originQuery, CancellationToken ct) + public void UpdateResultView(List list, string originQuery, CancellationToken ct) { if (list == null) { @@ -764,7 +778,7 @@ namespace PowerLauncher.ViewModel } } - if (originQuery.RawQuery == _currentQuery.RawQuery) + if (originQuery.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) { ct.ThrowIfCancellationRequested(); Results.AddResults(list, ct); @@ -786,10 +800,14 @@ namespace PowerLauncher.ViewModel // Fix Cold start for plugins string s = "m"; - var query = QueryBuilder.Build(s.Trim(), PluginManager.NonGlobalPlugins); - var plugins = PluginManager.ValidPluginsForQuery(query); - foreach (PluginPair plugin in plugins) + var pluginQueryPairs = QueryBuilder.Build(ref s, PluginManager.NonGlobalPlugins); + + // To execute a query corresponding to each plugin + foreach (KeyValuePair pluginQueryItem in pluginQueryPairs) { + var plugin = pluginQueryItem.Key; + var query = pluginQueryItem.Value; + if (!plugin.Metadata.Disabled && plugin.Metadata.Name != "Window Walker") { _ = PluginManager.QueryForPlugin(plugin, query); diff --git a/src/modules/launcher/Wox.Core/Plugin/QueryBuilder.cs b/src/modules/launcher/Wox.Core/Plugin/QueryBuilder.cs index 7a6f6af455..d9976e4752 100644 --- a/src/modules/launcher/Wox.Core/Plugin/QueryBuilder.cs +++ b/src/modules/launcher/Wox.Core/Plugin/QueryBuilder.cs @@ -11,7 +11,7 @@ namespace Wox.Core.Plugin { public static class QueryBuilder { - public static Query Build(string text, Dictionary nonGlobalPlugins) + public static Dictionary Build(ref string text, Dictionary nonGlobalPlugins) { // replace multiple white spaces with one white space var terms = text.Split(new[] { Query.TermSeparator }, StringSplitOptions.RemoveEmptyEntries); @@ -20,32 +20,54 @@ namespace Wox.Core.Plugin return null; } + // This Dictionary contains the corresponding query for each plugin + Dictionary pluginQueryPair = new Dictionary(); + var rawQuery = string.Join(Query.TermSeparator, terms); - string actionKeyword, search; + + // This is the query on removing extra spaces which would be executed by global Plugins + text = rawQuery; + string possibleActionKeyword = terms[0]; - List actionParameters; - if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) - { // use non global plugin for query - actionKeyword = possibleActionKeyword; - actionParameters = terms.Skip(1).ToList(); - search = actionParameters.Count > 0 ? rawQuery.Substring(actionKeyword.Length + 1) : string.Empty; - } - else - { // non action keyword - actionKeyword = string.Empty; - actionParameters = terms.ToList(); - search = rawQuery; - } - var query = new Query + foreach (string pluginActionKeyword in nonGlobalPlugins.Keys) { - Terms = terms, - RawQuery = rawQuery, - ActionKeyword = actionKeyword, - Search = search, - }; + if (possibleActionKeyword.StartsWith(pluginActionKeyword)) + { + if (nonGlobalPlugins.TryGetValue(pluginActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) + { + // The search string is the raw query excluding the action keyword + string search = rawQuery.Substring(pluginActionKeyword.Length).Trim(); - return query; + // To set the terms of the query after removing the action keyword + if (possibleActionKeyword.Length > pluginActionKeyword.Length) + { + // If the first term contains the action keyword, then set the remaining string to be the first term + terms[0] = possibleActionKeyword.Substring(pluginActionKeyword.Length); + } + else + { + // If the first term is the action keyword, then skip it. + terms = terms.Skip(1).ToArray(); + } + + // A new query is constructed for each plugin as they have different action keywords + var query = new Query(rawQuery, search, terms, pluginActionKeyword); + + pluginQueryPair.TryAdd(pluginPair, query); + } + } + } + + var globalplugins = PluginManager.GlobalPlugins; + + foreach (PluginPair globalPlugin in PluginManager.GlobalPlugins) + { + var query = new Query(rawQuery, rawQuery, terms, string.Empty); + pluginQueryPair.Add(globalPlugin, query); + } + + return pluginQueryPair; } } } diff --git a/src/modules/launcher/Wox.Test/QueryBuilderTest.cs b/src/modules/launcher/Wox.Test/QueryBuilderTest.cs index 0f080245c2..774b09fdb8 100644 --- a/src/modules/launcher/Wox.Test/QueryBuilderTest.cs +++ b/src/modules/launcher/Wox.Test/QueryBuilderTest.cs @@ -11,45 +11,191 @@ namespace Wox.Test { public class QueryBuilderTest { - [Test] - public void ExclusivePluginQueryTest() + private bool AreEqual(Query firstQuery, Query secondQuery) { + return firstQuery.ActionKeyword.Equals(secondQuery.ActionKeyword) + && firstQuery.Search.Equals(secondQuery.Search) + && firstQuery.RawQuery.Equals(secondQuery.RawQuery); + } + + [Test] + public void QueryBuilder_ShouldRemoveExtraSpaces_ForNonGlobalPlugin() + { + // Arrange var nonGlobalPlugins = new Dictionary { { ">", new PluginPair { Metadata = new PluginMetadata { ActionKeywords = new List { ">" } } } }, }; + string searchQuery = "> file.txt file2 file3"; - Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins); + // Act + var pluginQueryPairs = QueryBuilder.Build(ref searchQuery, nonGlobalPlugins); - Assert.AreEqual("file.txt file2 file3", q.Search); - Assert.AreEqual(">", q.ActionKeyword); + // Assert + Assert.AreEqual("> file.txt file2 file3", searchQuery); } [Test] - public void ExclusivePluginQueryIgnoreDisabledTest() + public void QueryBuilder_ShouldRemoveExtraSpaces_ForDisabledNonGlobalPlugin() { + // Arrange var nonGlobalPlugins = new Dictionary { { ">", new PluginPair { Metadata = new PluginMetadata { ActionKeywords = new List { ">" }, Disabled = true } } }, }; + string searchQuery = "> file.txt file2 file3"; - Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins); + // Act + var pluginQueryPairs = QueryBuilder.Build(ref searchQuery, nonGlobalPlugins); - Assert.AreEqual("> file.txt file2 file3", q.Search); + // Assert + Assert.AreEqual("> file.txt file2 file3", searchQuery); } [Test] - public void GenericPluginQueryTest() + public void QueryBuilder_ShouldRemoveExtraSpaces_ForGlobalPlugin() { - Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); + // Arrange + string searchQuery = "file.txt file2 file3"; - Assert.AreEqual("file.txt file2 file3", q.Search); - Assert.AreEqual(string.Empty, q.ActionKeyword); + // Act + var pluginQueryPairs = QueryBuilder.Build(ref searchQuery, new Dictionary()); - Assert.AreEqual("file.txt", q.FirstSearch); - Assert.AreEqual("file2", q.SecondSearch); - Assert.AreEqual("file3", q.ThirdSearch); - Assert.AreEqual("file2 file3", q.SecondToEndSearch); + // Assert + Assert.AreEqual("file.txt file2 file3", searchQuery); + } + + [Test] + public void QueryBuilder_ShouldGenerateSameQuery_IfEitherActionKeywordOrActionKeywordsListIsSet() + { + // Arrange + string searchQuery = "> query"; + var firstPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeywords = new List { ">" } } }; + var secondPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = ">" } }; + + var nonGlobalPluginWithActionKeywords = new Dictionary + { + { ">", firstPlugin }, + }; + + var nonGlobalPluginWithActionKeyword = new Dictionary + { + { ">", secondPlugin }, + }; + string[] terms = { ">", "query" }; + Query expectedQuery = new Query("> query", "query", terms, ">"); + + // Act + var queriesForPluginsWithActionKeywords = QueryBuilder.Build(ref searchQuery, nonGlobalPluginWithActionKeywords); + var queriesForPluginsWithActionKeyword = QueryBuilder.Build(ref searchQuery, nonGlobalPluginWithActionKeyword); + + var firstQuery = queriesForPluginsWithActionKeyword.GetValueOrDefault(firstPlugin); + var secondQuery = queriesForPluginsWithActionKeywords.GetValueOrDefault(secondPlugin); + + // Assert + Assert.IsTrue(AreEqual(firstQuery, expectedQuery)); + Assert.IsTrue(AreEqual(firstQuery, secondQuery)); + } + + [Test] + public void QueryBuilder_ShouldGenerateCorrectQueries_ForPluginsWithMultipleActionKeywords() + { + // Arrange + var plugin = new PluginPair { Metadata = new PluginMetadata { ActionKeywords = new List { "a", "b" } } }; + var nonGlobalPlugins = new Dictionary + { + { "a", plugin }, + { "b", plugin }, + }; + + var firstQueryText = "asearch"; + var secondQueryText = "bsearch"; + + // Act + var firstPluginQueryPair = QueryBuilder.Build(ref firstQueryText, nonGlobalPlugins); + var firstQuery = firstPluginQueryPair.GetValueOrDefault(plugin); + + var secondPluginQueryPairs = QueryBuilder.Build(ref secondQueryText, nonGlobalPlugins); + var secondQuery = secondPluginQueryPairs.GetValueOrDefault(plugin); + + // Assert + Assert.IsTrue(AreEqual(firstQuery, new Query { ActionKeyword = "a", RawQuery = "asearch", Search = "search" })); + Assert.IsTrue(AreEqual(secondQuery, new Query { ActionKeyword = "b", RawQuery = "bsearch", Search = "search" })); + } + + [Test] + public void QueryBuild_ShouldGenerateSameSearchQuery_WithOrWithoutSpaceAfterActionKeyword() + { + // Arrange + var plugin = new PluginPair { Metadata = new PluginMetadata { ActionKeywords = new List { "a" } } }; + var nonGlobalPlugins = new Dictionary + { + { "a", plugin }, + }; + + var firstQueryText = "asearch"; + var secondQueryText = "a search"; + + // Act + var firstPluginQueryPair = QueryBuilder.Build(ref firstQueryText, nonGlobalPlugins); + var firstQuery = firstPluginQueryPair.GetValueOrDefault(plugin); + + var secondPluginQueryPairs = QueryBuilder.Build(ref secondQueryText, nonGlobalPlugins); + var secondQuery = secondPluginQueryPairs.GetValueOrDefault(plugin); + + // Assert + Assert.IsTrue(firstQuery.Search.Equals(secondQuery.Search)); + Assert.IsTrue(firstQuery.ActionKeyword.Equals(secondQuery.ActionKeyword)); + } + + [Test] + public void QueryBuild_ShouldGenerateCorrectQuery_ForPluginsWhoseActionKeywordsHaveSamePrefix() + { + // Arrange + string searchQuery = "abcdefgh"; + var firstPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "ab", ID = "plugin1" } }; + var secondPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "abcd", ID = "plugin2" } }; + + var nonGlobalPlugins = new Dictionary + { + { "ab", firstPlugin }, + { "abcd", secondPlugin }, + }; + + // Act + var pluginQueryPairs = QueryBuilder.Build(ref searchQuery, nonGlobalPlugins); + + var firstQuery = pluginQueryPairs.GetValueOrDefault(firstPlugin); + var secondQuery = pluginQueryPairs.GetValueOrDefault(secondPlugin); + + // Assert + Assert.IsTrue(AreEqual(firstQuery, new Query { RawQuery = searchQuery, Search = searchQuery.Substring(firstPlugin.Metadata.ActionKeyword.Length), ActionKeyword = firstPlugin.Metadata.ActionKeyword } )); + Assert.IsTrue(AreEqual(secondQuery, new Query { RawQuery = searchQuery, Search = searchQuery.Substring(secondPlugin.Metadata.ActionKeyword.Length), ActionKeyword = secondPlugin.Metadata.ActionKeyword })); + } + + [Test] + public void QueryBuilder_ShouldSetTermsCorrently_WhenCalled() + { + // Arrange + string searchQuery = "abcd efgh"; + var firstPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "ab", ID = "plugin1" } }; + var secondPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "abcd", ID = "plugin2" } }; + + var nonGlobalPlugins = new Dictionary + { + { "ab", firstPlugin }, + { "abcd", secondPlugin }, + }; + + // Act + var pluginQueryPairs = QueryBuilder.Build(ref searchQuery, nonGlobalPlugins); + + var firstQuery = pluginQueryPairs.GetValueOrDefault(firstPlugin); + var secondQuery = pluginQueryPairs.GetValueOrDefault(secondPlugin); + + // Assert + Assert.IsTrue(firstQuery.Terms[0].Equals("cd") && firstQuery.Terms[1].Equals("efgh") && firstQuery.Terms.Length == 2); + Assert.IsTrue(secondQuery.Terms[0].Equals("efgh") && secondQuery.Terms.Length == 1); } } }