diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs index a35cfdad45ae..31fb0b72cced 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs @@ -449,6 +449,10 @@ internal string ResourceFromPri(string packageFullName, string resourceReference { parsed = prefix + "//" + key; } + else if (key.Contains("resources", StringComparison.OrdinalIgnoreCase)) + { + parsed = prefix + key; + } else { parsed = prefix + "///resources/" + key; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs index cf9405def0ce..bcacfbd3e873 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs @@ -443,6 +443,46 @@ private static Win32 GetProgramFromPath(string path) return entry; } + public class removeDuplicatesComparer : IEqualityComparer + { + public bool Equals(Win32 app1, Win32 app2) + { + + if(!string.IsNullOrEmpty(app1.Name) && !string.IsNullOrEmpty(app2.Name) + && !string.IsNullOrEmpty(app1.ExecutableName) && !string.IsNullOrEmpty(app2.ExecutableName) + && !string.IsNullOrEmpty(app1.FullPath) && !string.IsNullOrEmpty(app2.FullPath)) + { + return app1.Name.Equals(app2.Name, StringComparison.OrdinalIgnoreCase) + && app1.ExecutableName.Equals(app2.ExecutableName, StringComparison.OrdinalIgnoreCase) + && app1.FullPath.Equals(app2.FullPath, StringComparison.OrdinalIgnoreCase); + } + return false; + } + + // Ref : https://stackoverflow.com/questions/2730865/how-do-i-calculate-a-good-hash-code-for-a-list-of-strings + public int GetHashCode(Win32 obj) + { + int namePrime = 13; + int executablePrime = 17; + int fullPathPrime = 31; + + int result = 1; + result = result * namePrime + obj.Name.GetHashCode(); + result = result * executablePrime + obj.ExecutableName.GetHashCode(); + result = result * fullPathPrime + obj.FullPath.GetHashCode(); + + return result; + } + } + + // Deduplication code + public static Func, Win32[]> DeduplicatePrograms = (programs) => + { + var uniqueExePrograms = programs.Where(x => !string.IsNullOrEmpty(x.LnkResolvedPath) || Extension(x.FullPath) != ExeExtension); + var uniquePrograms = uniqueExePrograms.Distinct(new removeDuplicatesComparer()); + return uniquePrograms.ToArray(); + }; + public static Win32[] All(Settings settings) { try @@ -464,15 +504,7 @@ public static Win32[] All(Settings settings) programs = programs.Concat(startMenu); } - var programsWithoutLnk = programs.Where(x => string.IsNullOrEmpty(x.LnkResolvedPath)); - var programsAsList = programs.ToList(); - - foreach(var app in programsWithoutLnk) - { - programsAsList.RemoveAll(x => (x.FullPath == app.FullPath) && string.IsNullOrEmpty(x.LnkResolvedPath)); - } - - return programsAsList.ToArray(); + return DeduplicatePrograms(programs); } #if DEBUG //This is to make developer aware of any unhandled exception and add in handling. catch (Exception e) diff --git a/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs b/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs index fd64c4391c5e..d569161a0ae1 100644 --- a/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs +++ b/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs @@ -83,9 +83,18 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption bool allSubstringsContainedInCompareString = true; var indexList = new List(); + List spaceIndices = new List(); for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) { + + // To maintain a list of indices which correspond to spaces in the string to compare + // To populate the list only for the first query substring + if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0) + { + spaceIndices.Add(compareStringIndex); + } + if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex]) { matchFoundInPreviousLoop = false; @@ -147,7 +156,8 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption // proceed to calculate score if every char or substring without whitespaces matched if (allQuerySubstringsMatched) { - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); + var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex); + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); return new MatchResult(true, UserSettingSearchPrecision, indexList, score); } @@ -155,6 +165,21 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption return new MatchResult (false, UserSettingSearchPrecision); } + // To get the index of the closest space which preceeds the first matching index + private int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) + { + if(spaceIndices.Count == 0) + { + return -1; + } + else + { + int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault(); + int closestSpaceIndex = ind ?? -1; + return closestSpaceIndex; + } + } + private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, string fullStringToCompareWithoutCase, string currentQuerySubstring) { diff --git a/src/modules/launcher/Wox.Test/FuzzyMatcherTest.cs b/src/modules/launcher/Wox.Test/FuzzyMatcherTest.cs index cce73f748213..a4062ab7fde4 100644 --- a/src/modules/launcher/Wox.Test/FuzzyMatcherTest.cs +++ b/src/modules/launcher/Wox.Test/FuzzyMatcherTest.cs @@ -187,8 +187,6 @@ public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual( [TestCase("sql manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] [TestCase("sql", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] [TestCase("sql serv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sqlserv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql servman", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] [TestCase("sql studio", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] [TestCase("mic", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] @@ -224,5 +222,20 @@ public void WhenGivenQueryShouldReturnResultsContainingAllQuerySubstrings( $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + $"Precision Score: {(int)expectedPrecisionScore}"); } + + [TestCase("Windows Terminal", "Windows_Terminal", "term")] + [TestCase("Windows Terminal", "WindowsTerminal", "term")] + public void FuzzyMatchingScore_ShouldBeHigher_WhenPreceedingCharacterIsSpace(string firstCompareStr, string secondCompareStr, string query) + { + // Arrange + var matcher = new StringMatcher(); + + // Act + var firstScore = matcher.FuzzyMatch(query, firstCompareStr).Score; + var secondScore = matcher.FuzzyMatch(query, secondCompareStr).Score; + + // Assert + Assert.IsTrue(firstScore > secondScore); + } } } \ No newline at end of file diff --git a/src/modules/launcher/Wox.Test/Plugins/ProgramPluginTest.cs b/src/modules/launcher/Wox.Test/Plugins/ProgramPluginTest.cs new file mode 100644 index 000000000000..54b8b95fc0b7 --- /dev/null +++ b/src/modules/launcher/Wox.Test/Plugins/ProgramPluginTest.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using Wox.Infrastructure; +using Wox.Plugin; +using Microsoft.Plugin.Program.Programs; +using Moq; +using System.IO; + +namespace Wox.Test.Plugins +{ + [TestFixture] + public class ProgramPluginTest + { + Win32 notepad_appdata = new Win32 + { + Name = "Notepad", + ExecutableName = "notepad.exe", + FullPath = "c:\\windows\\system32\\notepad.exe", + LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk" + }; + + Win32 notepad_users = new Win32 + { + Name = "Notepad", + ExecutableName = "notepad.exe", + FullPath = "c:\\windows\\system32\\notepad.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk" + }; + + Win32 azure_command_prompt = new Win32 + { + Name = "Microsoft Azure Command Prompt - v2.9", + ExecutableName = "cmd.exe", + FullPath = "c:\\windows\\system32\\cmd.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft azure\\microsoft azure sdk for .net\\v2.9\\microsoft azure command prompt - v2.9.lnk" + }; + + Win32 visual_studio_command_prompt = new Win32 + { + Name = "x64 Native Tools Command Prompt for VS 2019", + ExecutableName = "cmd.exe", + FullPath = "c:\\windows\\system32\\cmd.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\visual studio 2019\\visual studio tools\\vc\\x64 native tools command prompt for vs 2019.lnk" + }; + + Win32 command_prompt = new Win32 + { + Name = "Command Prompt", + ExecutableName = "cmd.exe", + FullPath = "c:\\windows\\system32\\cmd.exe", + LnkResolvedPath ="c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\command prompt.lnk" + }; + + Win32 file_explorer = new Win32 + { + Name = "File Explorer", + ExecutableName = "File Explorer.lnk", + FullPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\file explorer.lnk", + LnkResolvedPath = null + }; + + Win32 wordpad = new Win32 + { + Name = "Wordpad", + ExecutableName = "wordpad.exe", + FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\wordpad.lnk" + }; + + Win32 wordpad_duplicate = new Win32 + { + Name = "WORDPAD", + ExecutableName = "WORDPAD.EXE", + FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", + LnkResolvedPath = null + }; + + + [Test] + public void DedupFunction_whenCalled_mustRemoveDuplicateNotepads() + { + // Arrange + List prgms = new List(); + prgms.Add(notepad_appdata); + prgms.Add(notepad_users); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 1); + } + + [Test] + public void DedupFunction_whenCalled_mustNotRemovelnkWhichdoesNotHaveExe() + { + // Arrange + List prgms = new List(); + prgms.Add(file_explorer); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 1); + } + + [Test] + public void DedupFunction_mustRemoveDuplicates_forExeExtensionsWithoutLnkResolvedPath() + { + // Arrange + List prgms = new List(); + prgms.Add(wordpad); + prgms.Add(wordpad_duplicate); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 1); + Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkResolvedPath)); + } + + [Test] + public void DedupFunction_mustNotRemovePrograms_withSameExeNameAndFullPath() + { + // Arrange + List prgms = new List(); + prgms.Add(azure_command_prompt); + prgms.Add(visual_studio_command_prompt); + prgms.Add(command_prompt); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 3); + } + } +} diff --git a/src/modules/launcher/Wox.Test/Wox.Test.csproj b/src/modules/launcher/Wox.Test/Wox.Test.csproj index 12a35fbdefb8..c61fde2bbd53 100644 --- a/src/modules/launcher/Wox.Test/Wox.Test.csproj +++ b/src/modules/launcher/Wox.Test/Wox.Test.csproj @@ -40,6 +40,7 @@ +