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;
}
///