diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index c8f22cf7c92..6d53cc72204 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -66,13 +66,13 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption var currentAcronymQueryIndex = 0; var acronymMatchData = new List(); - // preset acronymScore - int acronymScore = 100; + int acronymsTotalCount = 0; + int acronymsMatched = 0; var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query; - var querySubstrings = queryWithoutCase.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); + var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); int currentQuerySubstringIndex = 0; var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; var currentQuerySubstringCharacterIndex = 0; @@ -87,12 +87,18 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption var indexList = new List(); List spaceIndices = new List(); - bool spaceMet = false; - for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) { - if (currentAcronymQueryIndex >= queryWithoutCase.Length - || allQuerySubstringsMatched && acronymScore < (int) UserSettingSearchPrecision) + // If acronyms matching successfully finished, this gets the remaining not matched acronyms for score calculation + if (currentAcronymQueryIndex >= query.Length && acronymsMatched == query.Length) + { + if (IsAcronymCount(stringToCompare, compareStringIndex)) + acronymsTotalCount++; + continue; + } + + if (currentAcronymQueryIndex >= query.Length || + currentAcronymQueryIndex >= query.Length && allQuerySubstringsMatched) break; // To maintain a list of indices which correspond to spaces in the string to compare @@ -101,42 +107,21 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption spaceIndices.Add(compareStringIndex); // Acronym check - if (char.IsUpper(stringToCompare[compareStringIndex]) || - char.IsNumber(stringToCompare[compareStringIndex]) || - char.IsWhiteSpace(stringToCompare[compareStringIndex]) || - spaceMet) + if (IsAcronym(stringToCompare, compareStringIndex)) { if (fullStringToCompareWithoutCase[compareStringIndex] == queryWithoutCase[currentAcronymQueryIndex]) { - if (!spaceMet) - { - char currentCompareChar = stringToCompare[compareStringIndex]; - spaceMet = char.IsWhiteSpace(currentCompareChar); - // if is space, no need to check whether upper or digit, though insignificant - if (!spaceMet && compareStringIndex == 0 || char.IsUpper(currentCompareChar) || - char.IsDigit(currentCompareChar)) - { - acronymMatchData.Add(compareStringIndex); - } - } - else if (!(spaceMet = char.IsWhiteSpace(stringToCompare[compareStringIndex]))) - { - acronymMatchData.Add(compareStringIndex); - } + acronymMatchData.Add(compareStringIndex); + acronymsMatched++; currentAcronymQueryIndex++; } - else - { - spaceMet = char.IsWhiteSpace(stringToCompare[compareStringIndex]); - // Acronym Penalty - if (!spaceMet) - { - acronymScore -= 10; - } - } } + + if (IsAcronymCount(stringToCompare, compareStringIndex)) + acronymsTotalCount++; + // Acronym end if (allQuerySubstringsMatched || fullStringToCompareWithoutCase[compareStringIndex] != @@ -204,11 +189,16 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption } } - // return acronym Match if possible - if (acronymMatchData.Count == query.Length && acronymScore >= (int) UserSettingSearchPrecision) + // return acronym match if all query char matched + if (acronymsMatched > 0 && acronymsMatched == query.Length) { - acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); - return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore); + int acronymScore = acronymsMatched * 100 / acronymsTotalCount; + + if (acronymScore >= (int)UserSettingSearchPrecision) + { + acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); + return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore); + } } // proceed to calculate score if every char or substring without whitespaces matched @@ -225,20 +215,49 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption return new MatchResult(false, UserSettingSearchPrecision); } + private bool IsAcronym(string stringToCompare, int compareStringIndex) + { + if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex)) + return true; + + return false; + } + + // When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5 + private bool IsAcronymCount(string stringToCompare, int compareStringIndex) + { + if (IsAcronymChar(stringToCompare, compareStringIndex)) + return true; + + if (IsAcronymNumber(stringToCompare, compareStringIndex)) + return compareStringIndex == 0 || char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); + + + return false; + } + + private bool IsAcronymChar(string stringToCompare, int compareStringIndex) + => char.IsUpper(stringToCompare[compareStringIndex]) || + compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym + char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); + + private bool IsAcronymNumber(string stringToCompare, int compareStringIndex) => stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9; + // 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 + var closestSpaceIndex = -1; + + // spaceIndices should be ordered asc + foreach (var index in spaceIndices) { - int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)) - .FirstOrDefault(item => firstMatchIndex > item); - int closestSpaceIndex = ind ?? -1; - return closestSpaceIndex; + if (index < firstMatchIndex) + closestSpaceIndex = index; + else + break; } + + return closestSpaceIndex; } private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index 78c918b6150..bbddcbd2ad4 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -131,16 +131,17 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat [TestCase(Chrome, Chrome, 157)] [TestCase(Chrome, LastIsChrome, 147)] - [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 90)] + [TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)] + [TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)] [TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)] [TestCase(Chrome, CandyCrushSagaFromKing, 0)] - [TestCase("sql", MicrosoftSqlServerManagementStudio, 90)] + [TestCase("sql", MicrosoftSqlServerManagementStudio, 110)] [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( string queryString, string compareString, int expectedScore) { // When, Given - var matcher = new StringMatcher(); + var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should @@ -151,13 +152,22 @@ public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( [TestCase("goo", "Google Chrome", SearchPrecisionScore.Regular, true)] [TestCase("chr", "Google Chrome", SearchPrecisionScore.Low, true)] [TestCase("chr", "Chrome", SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)] [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Low, true)] [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.Regular, false)] [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.None, true)] - [TestCase("ccs", "Candy Crush Saga from King", SearchPrecisionScore.Regular, true)] + [TestCase("ccs", "Candy Crush Saga from King", SearchPrecisionScore.Low, true)] [TestCase("cand", "Candy Crush Saga from King", SearchPrecisionScore.Regular, true)] - [TestCase("cand", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, - false)] + [TestCase("cand", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)] + [TestCase("vsc", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vs", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vc", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vts", VisualStudioCode, SearchPrecisionScore.Regular, false)] + [TestCase("vcs", VisualStudioCode, SearchPrecisionScore.Regular, false)] + [TestCase("wt", "Windows Terminal From Microsoft Store", SearchPrecisionScore.Regular, false)] + [TestCase("vsp", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)] + [TestCase("vsp", "2019 Visual Studio Preview", SearchPrecisionScore.Regular, true)] + [TestCase("2019p", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)] public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( string queryString, string compareString, @@ -180,18 +190,16 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( // Should Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), - $"Query:{queryString}{Environment.NewLine} " + - $"Compare:{compareString}{Environment.NewLine}" + + $"Query: {queryString}{Environment.NewLine} " + + $"Compare: {compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + $"Precision Score: {(int)expectedPrecisionScore}"); } [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", SearchPrecisionScore.Regular, false)] [TestCase("term", "Windows Terminal (Preview)", SearchPrecisionScore.Regular, true)] - [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, - false)] - [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, - false)] + [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] [TestCase("sql manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] [TestCase("sql", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] @@ -204,18 +212,13 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( [TestCase("mssms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] [TestCase("msms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] [TestCase("chr", "Shutdown", SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", - SearchPrecisionScore.Regular, false)] - [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", - SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, false)] + [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, true)] [TestCase("a test", "This is a test", SearchPrecisionScore.Regular, true)] [TestCase("test", "This is a test", SearchPrecisionScore.Regular, true)] [TestCase("cod", VisualStudioCode, SearchPrecisionScore.Regular, true)] [TestCase("code", VisualStudioCode, SearchPrecisionScore.Regular, true)] [TestCase("codes", "Visual Studio Codes", SearchPrecisionScore.Regular, true)] - [TestCase("vsc", VisualStudioCode, SearchPrecisionScore.Regular, true)] - [TestCase("vs", VisualStudioCode, SearchPrecisionScore.Regular, true)] - [TestCase("vc", VisualStudioCode, SearchPrecisionScore.Regular, true)] public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings( string queryString, string compareString, @@ -300,15 +303,19 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( $"Should be greater than{Environment.NewLine}" + $"Name of second: \"{secondName}\", Final Score: {secondScore}{Environment.NewLine}"); } - - [TestCase("vsc","Visual Studio Code", 100)] - [TestCase("jbr","JetBrain Rider",100)] - [TestCase("jr","JetBrain Rider",90)] - [TestCase("vs","Visual Studio",100)] - [TestCase("vs","Visual Studio Preview",100)] - [TestCase("vsp","Visual Studio Preview",100)] - [TestCase("vsp","Visual Studio",0)] - [TestCase("pc","Postman Canary",100)] + + [TestCase("vsc", "Visual Studio Code", 100)] + [TestCase("jbr", "JetBrain Rider", 100)] + [TestCase("jr", "JetBrain Rider", 66)] + [TestCase("vs", "Visual Studio", 100)] + [TestCase("vs", "Visual Studio Preview", 66)] + [TestCase("vsp", "Visual Studio Preview", 100)] + [TestCase("pc", "postman canary", 100)] + [TestCase("psc", "Postman super canary", 100)] + [TestCase("psc", "Postman super Canary", 100)] + [TestCase("vsp", "Visual Studio", 0)] + [TestCase("vps", "Visual Studio", 0)] + [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 75)] public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString, int desiredScore) { diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs index 3ea78156d77..159c3a9a0ad 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs @@ -18,6 +18,7 @@ using Flow.Launcher.Plugin.Program.Logger; using IStream = AppxPackaing.IStream; using Rect = System.Windows.Rect; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.Program.Programs { @@ -206,12 +207,11 @@ private static IEnumerable CurrentUserPackages() } catch (Exception e) { - ProgramLogger.LogException("UWP" ,"CurrentUserPackages", $"id","An unexpected error occured and " + ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and " + $"unable to verify if package is valid", e); return false; } - - + return valid; }); return ps; @@ -263,24 +263,42 @@ public class Application : IProgram public string LogoPath { get; set; } public UWP Package { get; set; } - public Application(){} + public Application() { } public Result Result(string query, IPublicAPI api) { - var title = (Name, Description) switch - { - (var n, null) => n, - (var n, var d) when d.StartsWith(n) => d, - (var n, var d) when n.StartsWith(d) => n, - (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}", - _ => Name - }; + string title; + MatchResult matchResult; - var matchResult = StringMatcher.FuzzySearch(query, title); + // We suppose Name won't be null + if (Description == null || Name.StartsWith(Description)) + { + title = Name; + matchResult = StringMatcher.FuzzySearch(query, title); + } + else if (Description.StartsWith(Name)) + { + title = Description; + matchResult = StringMatcher.FuzzySearch(query, Description); + } + else + { + title = $"{Name}: {Description}"; + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var desciptionMatch = StringMatcher.FuzzySearch(query, Description); + if (desciptionMatch.Score > nameMatch.Score) + { + for (int i = 0; i < desciptionMatch.MatchData.Count; i++) + { + desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + } + matchResult = desciptionMatch; + } + else matchResult = nameMatch; + } - if (!matchResult.Success) - return null; + if (!matchResult.Success) return null; var result = new Result { @@ -311,7 +329,7 @@ public List ContextMenus(IPublicAPI api) Action = _ => { - Main.StartProcess(Process.Start, + Main.StartProcess(Process.Start, new ProcessStartInfo( !string.IsNullOrEmpty(Main._settings.CustomizedExplorer) ? Main._settings.CustomizedExplorer @@ -403,14 +421,14 @@ internal string ResourceFromPri(string packageFullName, string packageName, stri public string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue) { const string prefix = "ms-resource:"; - + if (string.IsNullOrWhiteSpace(rawPriReferenceValue) || !rawPriReferenceValue.StartsWith(prefix)) return rawPriReferenceValue; string key = rawPriReferenceValue.Substring(prefix.Length); if (key.StartsWith("//")) return $"{prefix}{key}"; - + if (!key.StartsWith("/")) { key = $"/{key}"; diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index 77278330a47..fd994aeb347 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -12,6 +12,7 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.Program.Programs { @@ -36,19 +37,38 @@ public class Win32 : IProgram public Result Result(string query, IPublicAPI api) { - var title = (Name, Description) switch + string title; + MatchResult matchResult; + + // We suppose Name won't be null + if (Description == null || Name.StartsWith(Description)) { - (var n, null) => n, - (var n, var d) when d.StartsWith(n) => d, - (var n, var d) when n.StartsWith(d) => n, - (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}", - _ => Name - }; + title = Name; + matchResult = StringMatcher.FuzzySearch(query, title); + } + else if (Description.StartsWith(Name)) + { + title = Description; + matchResult = StringMatcher.FuzzySearch(query, Description); + } + else + { + title = $"{Name}: {Description}"; + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var desciptionMatch = StringMatcher.FuzzySearch(query, Description); + if (desciptionMatch.Score > nameMatch.Score) + { + for (int i = 0; i < desciptionMatch.MatchData.Count; i++) + { + desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + } + matchResult = desciptionMatch; + } + else matchResult = nameMatch; + } - var matchResult = StringMatcher.FuzzySearch(query, title); + if (!matchResult.Success) return null; - if (!matchResult.Success) - return null; var result = new Result { @@ -58,7 +78,7 @@ public Result Result(string query, IPublicAPI api) Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, - Action = e => + Action = _ => { var info = new ProcessStartInfo { @@ -268,10 +288,10 @@ private static IEnumerable ProgramPaths(string directory, string[] suffi try { var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions - { - IgnoreInaccessible = true, - RecurseSubdirectories = true - }) + { + IgnoreInaccessible = true, + RecurseSubdirectories = true + }) .Where(x => suffixes.Contains(Extension(x))); return paths;