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() {