diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs
deleted file mode 100644
index d640058c98..0000000000
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-
-namespace Microsoft.CmdPal.Ext.WindowWalker.Components;
-
-///
-/// Class housing fuzzy matching methods
-///
-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
- ///
- /// The text to search inside of
- /// the text to search for
- /// returns the index location of each of the letters of the matches
- 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
- var matches = new bool[text.Length, searchText.Length];
- for (var firstIndex = 0; firstIndex < text.Length; firstIndex++)
- {
- for (var secondIndex = 0; secondIndex < searchText.Length; secondIndex++)
- {
- matches[firstIndex, secondIndex] =
- searchText[secondIndex] == text[firstIndex] ?
- true :
- false;
- }
- }
-
- // use this table to get all the possible matches
- List> allMatches = GetAllMatchIndexes(matches);
-
- // return the score that is the max
- var maxScore = allMatches.Count > 0 ? CalculateScoreForMatches(allMatches[0]) : 0;
- List bestMatch = allMatches.Count > 0 ? allMatches[0] : new List();
-
- foreach (var match in allMatches)
- {
- var score = CalculateScoreForMatches(match);
- if (score > maxScore)
- {
- bestMatch = match;
- maxScore = score;
- }
- }
-
- 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>();
-
- for (var secondIndex = 0; secondIndex < matches.GetLength(1); secondIndex++)
- {
- for (var firstIndex = 0; firstIndex < matches.GetLength(0); firstIndex++)
- {
- if (secondIndex == 0 && matches[firstIndex, secondIndex])
- {
- results.Add(new List { firstIndex });
- }
- else if (matches[firstIndex, secondIndex])
- {
- var tempList = results.Where(x => x.Count == secondIndex && x[x.Count - 1] < firstIndex).Select(x => x.ToList()).ToList();
-
- foreach (var pathSofar in tempList)
- {
- pathSofar.Add(firstIndex);
- }
-
- results.AddRange(tempList);
- }
- }
-
- results = results.Where(x => x.Count == secondIndex + 1).ToList();
- }
-
- return results.Where(x => x.Count == matches.GetLength(1)).ToList();
- }
-
- ///
- /// Calculates the score for a string
- ///
- /// the index of the matches
- /// an integer representing the score
- internal static int CalculateScoreForMatches(List matches)
- {
- ArgumentNullException.ThrowIfNull(matches);
-
- var score = 0;
-
- for (var currentIndex = 1; currentIndex < matches.Count; currentIndex++)
- {
- var previousIndex = currentIndex - 1;
-
- score -= matches[currentIndex] - matches[previousIndex];
- }
-
- return score == 0 ? -10000 : score;
- }
-}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs
index 8739d88a2f..f2428fd6c4 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.WindowWalker.Commands;
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
@@ -19,33 +19,58 @@ internal static class ResultHelper
///
/// Returns a list of all results for the query.
///
- /// List with all search controller matches
+ /// List with all search controller matches
/// List of results
- internal static List GetResultList(List searchControllerResults, bool isKeywordSearch)
+ internal static WindowWalkerListItem[] GetResultList(ICollection>? scoredWindows)
{
- if (searchControllerResults is null || searchControllerResults.Count == 0)
+ if (scoredWindows is null || scoredWindows.Count == 0)
{
return [];
}
- var resultsList = new List(searchControllerResults.Count);
- var addExplorerInfo = searchControllerResults.Any(x =>
- string.Equals(x.Result.Process.Name, "explorer.exe", StringComparison.OrdinalIgnoreCase) &&
- x.Result.Process.IsShellProcess);
+ var list = scoredWindows as IList> ?? new List>(scoredWindows);
- // Process each SearchResult to convert it into a Result.
- // Using parallel processing if the operation is CPU-bound and the list is large.
- resultsList = searchControllerResults
- .AsParallel()
- .Select(x => CreateResultFromSearchResult(x))
- .ToList();
+ var addExplorerInfo = false;
+ for (var i = 0; i < list.Count; i++)
+ {
+ var window = list[i].Item;
+ if (window?.Process is null)
+ {
+ continue;
+ }
+
+ if (string.Equals(window.Process.Name, "explorer.exe", StringComparison.OrdinalIgnoreCase) && window.Process.IsShellProcess)
+ {
+ addExplorerInfo = true;
+ break;
+ }
+ }
+
+ var projected = new WindowWalkerListItem[list.Count];
+ if (list.Count >= 32)
+ {
+ Parallel.For(0, list.Count, i =>
+ {
+ projected[i] = CreateResultFromSearchResult(list[i]);
+ });
+ }
+ else
+ {
+ for (var i = 0; i < list.Count; i++)
+ {
+ projected[i] = CreateResultFromSearchResult(list[i]);
+ }
+ }
if (addExplorerInfo && !SettingsManager.Instance.HideExplorerSettingInfo)
{
- resultsList.Insert(0, GetExplorerInfoResult());
+ var withInfo = new WindowWalkerListItem[projected.Length + 1];
+ withInfo[0] = GetExplorerInfoResult();
+ Array.Copy(projected, 0, withInfo, 1, projected.Length);
+ return withInfo;
}
- return resultsList;
+ return projected;
}
///
@@ -53,16 +78,15 @@ internal static class ResultHelper
///
/// The SearchResult object to convert.
/// A Result object populated with data from the SearchResult.
- private static WindowWalkerListItem CreateResultFromSearchResult(SearchResult searchResult)
+ private static WindowWalkerListItem CreateResultFromSearchResult(Scored searchResult)
{
- var item = new WindowWalkerListItem(searchResult.Result)
+ var item = new WindowWalkerListItem(searchResult.Item)
{
- Title = searchResult.Result.Title,
- Subtitle = GetSubtitle(searchResult.Result),
- Tags = GetTags(searchResult.Result),
+ Title = searchResult.Item.Title,
+ Subtitle = GetSubtitle(searchResult.Item),
+ Tags = GetTags(searchResult.Item),
};
item.MoreCommands = ContextMenuHelper.GetContextMenuResults(item).ToArray();
-
return item;
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs
deleted file mode 100644
index 2e5345bdfd..0000000000
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
-
-namespace Microsoft.CmdPal.Ext.WindowWalker.Components;
-
-///
-/// Responsible for searching and finding matches for the strings provided.
-/// Essentially the UI independent model of the application
-///
-internal sealed class SearchController
-{
- ///
- /// the current search text
- ///
- private string searchText;
-
- ///
- /// Open window search results
- ///
- private List? searchMatches;
-
- ///
- /// Singleton pattern
- ///
- private static SearchController? instance;
-
- ///
- /// Gets or sets the current search text
- ///
- internal string SearchText
- {
- get => searchText;
-
- set =>
- searchText = value.ToLower(CultureInfo.CurrentCulture).Trim();
- }
-
- ///
- /// Gets the open window search results
- ///
- internal List SearchMatches => new List(searchMatches ?? []).OrderByDescending(x => x.Score).ToList();
-
- ///
- /// Gets singleton Pattern
- ///
- internal static SearchController Instance
- {
- get
- {
- instance ??= new SearchController();
-
- return instance;
- }
- }
-
- ///
- /// Initializes a new instance of the class.
- /// Initializes the search controller object
- ///
- private SearchController()
- {
- searchText = string.Empty;
- }
-
- ///
- /// Event handler for when the search text has been updated
- ///
- internal void UpdateSearchText(string searchText)
- {
- SearchText = searchText;
- SyncOpenWindowsWithModel();
- }
-
- ///
- /// Syncs the open windows with the OpenWindows Model
- ///
- internal void SyncOpenWindowsWithModel()
- {
- System.Diagnostics.Debug.Print("Syncing WindowSearch result with OpenWindows Model");
-
- var snapshotOfOpenWindows = OpenWindows.Instance.Windows;
-
- searchMatches = string.IsNullOrWhiteSpace(SearchText) ? AllOpenWindows(snapshotOfOpenWindows) : FuzzySearchOpenWindows(snapshotOfOpenWindows);
- }
-
- ///
- /// Search method that matches the title of windows with the user search text
- ///
- /// what windows are open
- /// Returns search results
- private List FuzzySearchOpenWindows(List openWindows)
- {
- List result = [];
- var searchStrings = new SearchString(searchText, SearchResult.SearchType.Fuzzy);
-
- foreach (var window in openWindows)
- {
- var titleMatch = FuzzyMatching.FindBestFuzzyMatch(window.Title, searchStrings.SearchText);
- var processMatch = FuzzyMatching.FindBestFuzzyMatch(window.Process.Name ?? string.Empty, searchStrings.SearchText);
-
- if ((titleMatch.Count != 0 || processMatch.Count != 0) && window.Title.Length != 0)
- {
- result.Add(new SearchResult(window, titleMatch, processMatch, searchStrings.SearchType));
- }
- }
-
- System.Diagnostics.Debug.Print("Found " + result.Count + " windows that match the search text");
-
- return result;
- }
-
- ///
- /// Search method that matches all the windows with a title
- ///
- /// what windows are open
- /// Returns search results
- private List AllOpenWindows(List openWindows)
- {
- List result = [];
-
- foreach (var window in openWindows)
- {
- if (window.Title.Length != 0)
- {
- result.Add(new SearchResult(window));
- }
- }
-
- return SettingsManager.Instance.InMruOrder
- ? result.ToList()
- : result
- .OrderBy(w => w.Result.Title)
- .ToList();
- }
-
- ///
- /// Event args for a window list update event
- ///
- internal sealed class SearchResultUpdateEventArgs : EventArgs
- {
- }
-}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs
deleted file mode 100644
index bfe51344ce..0000000000
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/
-using System.Collections.Generic;
-
-namespace Microsoft.CmdPal.Ext.WindowWalker.Components;
-
-///
-/// Contains search result windows with each window including the reason why the result was included
-///
-internal sealed class SearchResult
-{
- ///
- /// Gets the actual window reference for the search result
- ///
- internal Window Result
- {
- get;
- private set;
- }
-
- ///
- /// Gets the list of indexes of the matching characters for the search in the title window
- ///
- internal List SearchMatchesInTitle
- {
- get;
- private set;
- }
-
- ///
- /// Gets the list of indexes of the matching characters for the search in the
- /// name of the process
- ///
- internal List SearchMatchesInProcessName
- {
- get;
- private set;
- }
-
- ///
- /// Gets the type of match (shortcut, fuzzy or nothing)
- ///
- internal SearchType SearchResultMatchType
- {
- get;
- private set;
- }
-
- ///
- /// Gets a score indicating how well this matches what we are looking for
- ///
- internal int Score
- {
- get;
- private set;
- }
-
- ///
- /// Gets the source of where the best score was found
- ///
- internal TextType BestScoreSource
- {
- get;
- private set;
- }
-
- ///
- /// Initializes a new instance of the class.
- /// Constructor
- ///
- internal SearchResult(Window window, List matchesInTitle, List matchesInProcessName, SearchType matchType)
- {
- Result = window;
- SearchMatchesInTitle = matchesInTitle;
- SearchMatchesInProcessName = matchesInProcessName;
- SearchResultMatchType = matchType;
- CalculateScore();
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- internal SearchResult(Window window)
- {
- Result = window;
- SearchMatchesInTitle = new List();
- SearchMatchesInProcessName = new List();
- SearchResultMatchType = SearchType.Empty;
- CalculateScore();
- }
-
- ///
- /// Calculates the score for how closely this window matches the search string
- ///
- ///
- /// Higher Score is better
- ///
- private void CalculateScore()
- {
- if (FuzzyMatching.CalculateScoreForMatches(SearchMatchesInProcessName) >
- FuzzyMatching.CalculateScoreForMatches(SearchMatchesInTitle))
- {
- Score = FuzzyMatching.CalculateScoreForMatches(SearchMatchesInProcessName);
- BestScoreSource = TextType.ProcessName;
- }
- else
- {
- Score = FuzzyMatching.CalculateScoreForMatches(SearchMatchesInTitle);
- BestScoreSource = TextType.WindowTitle;
- }
- }
-
- ///
- /// The type of text that a string represents
- ///
- internal enum TextType
- {
- ProcessName,
- WindowTitle,
- }
-
- ///
- /// The type of search
- ///
- internal enum SearchType
- {
- ///
- /// the search string is empty, which means all open windows are
- /// going to be returned
- ///
- Empty,
-
- ///
- /// Regular fuzzy match search
- ///
- Fuzzy,
-
- ///
- /// The user has entered text that has been matched to a shortcut
- /// and the shortcut is now being searched
- ///
- Shortcut,
- }
-}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs
deleted file mode 100644
index c61d193637..0000000000
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/
-namespace Microsoft.CmdPal.Ext.WindowWalker.Components;
-
-///
-/// A class to represent a search string
-///
-/// Class was added in order to be able to attach various context data to
-/// a search string
-internal sealed class SearchString
-{
- ///
- /// Gets where is the search string coming from (is it a shortcut
- /// or direct string, etc...)
- ///
- internal SearchResult.SearchType SearchType
- {
- get;
- private set;
- }
-
- ///
- /// Gets the actual text we are searching for
- ///
- internal string SearchText
- {
- get;
- private set;
- }
-
- ///
- /// Initializes a new instance of the class.
- /// Constructor
- ///
- /// text from search
- /// type of search
- internal SearchString(string searchText, SearchResult.SearchType searchType)
- {
- SearchText = searchText;
- SearchType = searchType;
- }
-}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs
index f0cbc01995..b9531163f9 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Pages/WindowWalkerListPage.cs
@@ -3,9 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.Collections.Generic;
-using System.Globalization;
using Microsoft.CmdPal.Ext.WindowWalker.Components;
+using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -33,10 +32,12 @@ internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposabl
};
}
- public override void UpdateSearchText(string oldSearch, string newSearch) =>
+ public override void UpdateSearchText(string oldSearch, string newSearch)
+ {
RaiseItemsChanged(0);
+ }
- public List Query(string query)
+ private WindowWalkerListItem[] Query(string query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -46,13 +47,37 @@ internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposabl
WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.UpdateDesktopList();
OpenWindows.Instance.UpdateOpenWindowsList(_cancellationTokenSource.Token);
- SearchController.Instance.UpdateSearchText(query);
- var searchControllerResults = SearchController.Instance.SearchMatches;
- return ResultHelper.GetResultList(searchControllerResults, !string.IsNullOrEmpty(query));
+ var windows = OpenWindows.Instance.Windows;
+
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ if (!SettingsManager.Instance.InMruOrder)
+ {
+ windows.Sort(static (a, b) => string.Compare(a?.Title, b?.Title, StringComparison.OrdinalIgnoreCase));
+ }
+
+ var results = new Scored[windows.Count];
+ for (var i = 0; i < windows.Count; i++)
+ {
+ results[i] = new Scored { Item = windows[i], Score = 100 };
+ }
+
+ return ResultHelper.GetResultList(results);
+ }
+
+ var scored = ListHelpers.FilterListWithScores(windows, query, ScoreFunction);
+ return ResultHelper.GetResultList([.. scored]);
}
- public override IListItem[] GetItems() => Query(SearchText).ToArray();
+ private static int ScoreFunction(string q, Window window)
+ {
+ var titleScore = FuzzyStringMatcher.ScoreFuzzy(q, window.Title);
+ var processNameScore = FuzzyStringMatcher.ScoreFuzzy(q, window.Process?.Name ?? string.Empty);
+ return Math.Max(titleScore, processNameScore);
+ }
+
+ public override IListItem[] GetItems() => Query(SearchText);
public void Dispose()
{