diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/FuzzyMatching.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/FuzzyMatching.cs index c761f4986e15..6c840c20d79f 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/FuzzyMatching.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/FuzzyMatching.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; namespace Microsoft.Plugin.WindowWalker.Components { @@ -16,99 +15,128 @@ namespace Microsoft.Plugin.WindowWalker.Components internal static class FuzzyMatching { /// - /// Finds the best match (the one with the most - /// number of letters adjacent to each other) and - /// returns the index location of each of the letters - /// of the matches + /// Find the best match (the one with the smallest span) using a Dynamic Programming approach + /// to minimize candidate matches. /// - /// The text to search inside of - /// the text to search for - /// returns the index location of each of the letters of the matches + /// The text to search inside of. + /// The text to search for. + /// The index location of each of the letters in the best match. internal static List FindBestFuzzyMatch(string text, string searchText) { ArgumentNullException.ThrowIfNull(searchText); ArgumentNullException.ThrowIfNull(text); - // Using CurrentCulture since this is user facing - searchText = searchText.ToLower(CultureInfo.CurrentCulture); - text = text.ToLower(CultureInfo.CurrentCulture); - - // Create a grid to march matches like - // e.g. - // a b c a d e c f g - // a x x - // c x x - bool[,] matches = new bool[text.Length, searchText.Length]; - for (int firstIndex = 0; firstIndex < text.Length; firstIndex++) + var sLower = searchText.ToLower(CultureInfo.CurrentCulture); + var tLower = text.ToLower(CultureInfo.CurrentCulture); + int m = sLower.Length; + int n = tLower.Length; + + // A subsequence longer than the candidate text can never match. + if (m > n) { - for (int secondIndex = 0; secondIndex < searchText.Length; secondIndex++) - { - matches[firstIndex, secondIndex] = - searchText[secondIndex] == text[firstIndex] ? - true : - false; - } + return []; } - // use this table to get all the possible matches - List> allMatches = GetAllMatchIndexes(matches); + // bestStart[k, i] stores the latest possible start index of a match for s[0..k] that + // ends exactly at t[i], or -1 if no such match exists. + // + // Tracking the latest start ensures that we only retain the smallest span of all matches + // that end at i. + int[,] bestStart = new int[m, n]; - // return the score that is the max - int maxScore = allMatches.Count > 0 ? CalculateScoreForMatches(allMatches[0]) : 0; - List bestMatch = allMatches.Count > 0 ? allMatches[0] : new List(); + // parent[k, i] stores the index where the previous character matched to allow for + // reconstruction of the best path once the DP step completes. + int[,] parent = new int[m, n]; - foreach (var match in allMatches) + // Initialize tables. + for (int k = 0; k < m; k++) { - int score = CalculateScoreForMatches(match); - if (score > maxScore) + for (int i = 0; i < n; i++) { - bestMatch = match; - maxScore = score; + bestStart[k, i] = -1; } } - return bestMatch; - } - - /// - /// Gets all the possible matches to the search string with in the text - /// - /// a table showing the matches as generated by - /// a two dimensional array with the first dimension the text and the second - /// one the search string and each cell marked as an intersection between the two - /// a list of the possible combinations that match the search text - internal static List> GetAllMatchIndexes(bool[,] matches) - { - ArgumentNullException.ThrowIfNull(matches); - - List> results = new List>(); + // Base case: match the first character of the search string s[0]. + for (int i = 0; i < n; i++) + { + if (tLower[i] == sLower[0]) + { + bestStart[0, i] = i; + parent[0, i] = -1; + } + } - for (int secondIndex = 0; secondIndex < matches.GetLength(1); secondIndex++) + // Dynamic programming step: extend matches for the remaining characters s[1..m-1]. + for (int k = 1; k < m; k++) { - for (int firstIndex = 0; firstIndex < matches.GetLength(0); firstIndex++) + int currentMaxStart = -1; + int currentParentIndex = -1; + + for (int i = 0; i < n; i++) { - if (secondIndex == 0 && matches[firstIndex, secondIndex]) + // 1. Try to match s[k] at t[i]. + // We must use a valid start from the previous row (k-1) that appeared BEFORE i. + // 'currentMaxStart' holds the best start value from indices 0 to i-1. + if (tLower[i] == sLower[k]) { - results.Add(new List { firstIndex }); + if (currentMaxStart != -1) + { + bestStart[k, i] = currentMaxStart; + parent[k, i] = currentParentIndex; + } } - else if (matches[firstIndex, secondIndex]) + + // 2. Maintain the dominating predecessor for the next column. + // We only keep the match with the latest start index, as it strictly dominates + // all earlier-starting matches for the purpose of minimizing the match span. + if (bestStart[k - 1, i] > currentMaxStart) { - var tempList = results.Where(x => x.Count == secondIndex && x[x.Count - 1] < firstIndex).Select(x => x.ToList()).ToList(); + currentMaxStart = bestStart[k - 1, i]; + currentParentIndex = i; + } + } + } - foreach (var pathSofar in tempList) - { - pathSofar.Add(firstIndex); - } + // Select the ending position that minimizes span. + int bestEndIndex = -1; + int maxScore = int.MinValue; + + // Score logic: -(LastIndex - StartIndex). + // We want to Maximize Score => Minimize Span. + for (int i = 0; i < n; i++) + { + if (bestStart[m - 1, i] != -1) + { + int start = bestStart[m - 1, i]; + int score = -(i - start); - results.AddRange(tempList); + if (score > maxScore) + { + maxScore = score; + bestEndIndex = i; } } + } + + if (bestEndIndex == -1) + { + return []; + } - results = results.Where(x => x.Count == secondIndex + 1).ToList(); + // Reconstruct only the winning path. + var result = new List(m); + int curr = bestEndIndex; + + for (int k = m - 1; k >= 0; k--) + { + result.Add(curr); + curr = parent[k, curr]; } - return results.Where(x => x.Count == matches.GetLength(1)).ToList(); + result.Reverse(); + return result; } ///