mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
CmdPal: replace custom fuzzy matching in Window Walker (#44807)
## Summary of the Pull Request This PR replaces the custom search controller and fuzzy matching with standard classes from the Extension SDK Toolkit. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class housing fuzzy matching methods
|
|
||||||
/// </summary>
|
|
||||||
internal static class FuzzyMatching
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 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
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text to search inside of</param>
|
|
||||||
/// <param name="searchText">the text to search for</param>
|
|
||||||
/// <returns>returns the index location of each of the letters of the matches</returns>
|
|
||||||
internal static List<int> 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<List<int>> allMatches = GetAllMatchIndexes(matches);
|
|
||||||
|
|
||||||
// return the score that is the max
|
|
||||||
var maxScore = allMatches.Count > 0 ? CalculateScoreForMatches(allMatches[0]) : 0;
|
|
||||||
List<int> bestMatch = allMatches.Count > 0 ? allMatches[0] : new List<int>();
|
|
||||||
|
|
||||||
foreach (var match in allMatches)
|
|
||||||
{
|
|
||||||
var score = CalculateScoreForMatches(match);
|
|
||||||
if (score > maxScore)
|
|
||||||
{
|
|
||||||
bestMatch = match;
|
|
||||||
maxScore = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all the possible matches to the search string with in the text
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="matches"> 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</param>
|
|
||||||
/// <returns>a list of the possible combinations that match the search text</returns>
|
|
||||||
internal static List<List<int>> GetAllMatchIndexes(bool[,] matches)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(matches);
|
|
||||||
|
|
||||||
List<List<int>> results = new List<List<int>>();
|
|
||||||
|
|
||||||
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<int> { 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the score for a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="matches">the index of the matches</param>
|
|
||||||
/// <returns>an integer representing the score</returns>
|
|
||||||
internal static int CalculateScoreForMatches(List<int> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.CmdPal.Ext.WindowWalker.Commands;
|
using Microsoft.CmdPal.Ext.WindowWalker.Commands;
|
||||||
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
|
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
|
||||||
@@ -19,33 +19,58 @@ internal static class ResultHelper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of all results for the query.
|
/// Returns a list of all results for the query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="searchControllerResults">List with all search controller matches</param>
|
/// <param name="scoredWindows">List with all search controller matches</param>
|
||||||
/// <returns>List of results</returns>
|
/// <returns>List of results</returns>
|
||||||
internal static List<WindowWalkerListItem> GetResultList(List<SearchResult> searchControllerResults, bool isKeywordSearch)
|
internal static WindowWalkerListItem[] GetResultList(ICollection<Scored<Window>>? scoredWindows)
|
||||||
{
|
{
|
||||||
if (searchControllerResults is null || searchControllerResults.Count == 0)
|
if (scoredWindows is null || scoredWindows.Count == 0)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultsList = new List<WindowWalkerListItem>(searchControllerResults.Count);
|
var list = scoredWindows as IList<Scored<Window>> ?? new List<Scored<Window>>(scoredWindows);
|
||||||
var addExplorerInfo = searchControllerResults.Any(x =>
|
|
||||||
string.Equals(x.Result.Process.Name, "explorer.exe", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
x.Result.Process.IsShellProcess);
|
|
||||||
|
|
||||||
// Process each SearchResult to convert it into a Result.
|
var addExplorerInfo = false;
|
||||||
// Using parallel processing if the operation is CPU-bound and the list is large.
|
for (var i = 0; i < list.Count; i++)
|
||||||
resultsList = searchControllerResults
|
{
|
||||||
.AsParallel()
|
var window = list[i].Item;
|
||||||
.Select(x => CreateResultFromSearchResult(x))
|
if (window?.Process is null)
|
||||||
.ToList();
|
{
|
||||||
|
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)
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -53,16 +78,15 @@ internal static class ResultHelper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="searchResult">The SearchResult object to convert.</param>
|
/// <param name="searchResult">The SearchResult object to convert.</param>
|
||||||
/// <returns>A Result object populated with data from the SearchResult.</returns>
|
/// <returns>A Result object populated with data from the SearchResult.</returns>
|
||||||
private static WindowWalkerListItem CreateResultFromSearchResult(SearchResult searchResult)
|
private static WindowWalkerListItem CreateResultFromSearchResult(Scored<Window> searchResult)
|
||||||
{
|
{
|
||||||
var item = new WindowWalkerListItem(searchResult.Result)
|
var item = new WindowWalkerListItem(searchResult.Item)
|
||||||
{
|
{
|
||||||
Title = searchResult.Result.Title,
|
Title = searchResult.Item.Title,
|
||||||
Subtitle = GetSubtitle(searchResult.Result),
|
Subtitle = GetSubtitle(searchResult.Item),
|
||||||
Tags = GetTags(searchResult.Result),
|
Tags = GetTags(searchResult.Item),
|
||||||
};
|
};
|
||||||
item.MoreCommands = ContextMenuHelper.GetContextMenuResults(item).ToArray();
|
item.MoreCommands = ContextMenuHelper.GetContextMenuResults(item).ToArray();
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Responsible for searching and finding matches for the strings provided.
|
|
||||||
/// Essentially the UI independent model of the application
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class SearchController
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// the current search text
|
|
||||||
/// </summary>
|
|
||||||
private string searchText;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Open window search results
|
|
||||||
/// </summary>
|
|
||||||
private List<SearchResult>? searchMatches;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Singleton pattern
|
|
||||||
/// </summary>
|
|
||||||
private static SearchController? instance;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the current search text
|
|
||||||
/// </summary>
|
|
||||||
internal string SearchText
|
|
||||||
{
|
|
||||||
get => searchText;
|
|
||||||
|
|
||||||
set =>
|
|
||||||
searchText = value.ToLower(CultureInfo.CurrentCulture).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the open window search results
|
|
||||||
/// </summary>
|
|
||||||
internal List<SearchResult> SearchMatches => new List<SearchResult>(searchMatches ?? []).OrderByDescending(x => x.Score).ToList();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets singleton Pattern
|
|
||||||
/// </summary>
|
|
||||||
internal static SearchController Instance
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
instance ??= new SearchController();
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SearchController"/> class.
|
|
||||||
/// Initializes the search controller object
|
|
||||||
/// </summary>
|
|
||||||
private SearchController()
|
|
||||||
{
|
|
||||||
searchText = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event handler for when the search text has been updated
|
|
||||||
/// </summary>
|
|
||||||
internal void UpdateSearchText(string searchText)
|
|
||||||
{
|
|
||||||
SearchText = searchText;
|
|
||||||
SyncOpenWindowsWithModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Syncs the open windows with the OpenWindows Model
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Search method that matches the title of windows with the user search text
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="openWindows">what windows are open</param>
|
|
||||||
/// <returns>Returns search results</returns>
|
|
||||||
private List<SearchResult> FuzzySearchOpenWindows(List<Window> openWindows)
|
|
||||||
{
|
|
||||||
List<SearchResult> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Search method that matches all the windows with a title
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="openWindows">what windows are open</param>
|
|
||||||
/// <returns>Returns search results</returns>
|
|
||||||
private List<SearchResult> AllOpenWindows(List<Window> openWindows)
|
|
||||||
{
|
|
||||||
List<SearchResult> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event args for a window list update event
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class SearchResultUpdateEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains search result windows with each window including the reason why the result was included
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class SearchResult
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the actual window reference for the search result
|
|
||||||
/// </summary>
|
|
||||||
internal Window Result
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of indexes of the matching characters for the search in the title window
|
|
||||||
/// </summary>
|
|
||||||
internal List<int> SearchMatchesInTitle
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of indexes of the matching characters for the search in the
|
|
||||||
/// name of the process
|
|
||||||
/// </summary>
|
|
||||||
internal List<int> SearchMatchesInProcessName
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of match (shortcut, fuzzy or nothing)
|
|
||||||
/// </summary>
|
|
||||||
internal SearchType SearchResultMatchType
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a score indicating how well this matches what we are looking for
|
|
||||||
/// </summary>
|
|
||||||
internal int Score
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the source of where the best score was found
|
|
||||||
/// </summary>
|
|
||||||
internal TextType BestScoreSource
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SearchResult"/> class.
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
internal SearchResult(Window window, List<int> matchesInTitle, List<int> matchesInProcessName, SearchType matchType)
|
|
||||||
{
|
|
||||||
Result = window;
|
|
||||||
SearchMatchesInTitle = matchesInTitle;
|
|
||||||
SearchMatchesInProcessName = matchesInProcessName;
|
|
||||||
SearchResultMatchType = matchType;
|
|
||||||
CalculateScore();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SearchResult"/> class.
|
|
||||||
/// </summary>
|
|
||||||
internal SearchResult(Window window)
|
|
||||||
{
|
|
||||||
Result = window;
|
|
||||||
SearchMatchesInTitle = new List<int>();
|
|
||||||
SearchMatchesInProcessName = new List<int>();
|
|
||||||
SearchResultMatchType = SearchType.Empty;
|
|
||||||
CalculateScore();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the score for how closely this window matches the search string
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Higher Score is better
|
|
||||||
/// </remarks>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The type of text that a string represents
|
|
||||||
/// </summary>
|
|
||||||
internal enum TextType
|
|
||||||
{
|
|
||||||
ProcessName,
|
|
||||||
WindowTitle,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The type of search
|
|
||||||
/// </summary>
|
|
||||||
internal enum SearchType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// the search string is empty, which means all open windows are
|
|
||||||
/// going to be returned
|
|
||||||
/// </summary>
|
|
||||||
Empty,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Regular fuzzy match search
|
|
||||||
/// </summary>
|
|
||||||
Fuzzy,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user has entered text that has been matched to a shortcut
|
|
||||||
/// and the shortcut is now being searched
|
|
||||||
/// </summary>
|
|
||||||
Shortcut,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A class to represent a search string
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Class was added in order to be able to attach various context data to
|
|
||||||
/// a search string</remarks>
|
|
||||||
internal sealed class SearchString
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets where is the search string coming from (is it a shortcut
|
|
||||||
/// or direct string, etc...)
|
|
||||||
/// </summary>
|
|
||||||
internal SearchResult.SearchType SearchType
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the actual text we are searching for
|
|
||||||
/// </summary>
|
|
||||||
internal string SearchText
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SearchString"/> class.
|
|
||||||
/// Constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="searchText">text from search</param>
|
|
||||||
/// <param name="searchType">type of search</param>
|
|
||||||
internal SearchString(string searchText, SearchResult.SearchType searchType)
|
|
||||||
{
|
|
||||||
SearchText = searchText;
|
|
||||||
SearchType = searchType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,8 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using Microsoft.CmdPal.Ext.WindowWalker.Components;
|
using Microsoft.CmdPal.Ext.WindowWalker.Components;
|
||||||
|
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
||||||
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
|
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
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);
|
RaiseItemsChanged(0);
|
||||||
|
}
|
||||||
|
|
||||||
public List<WindowWalkerListItem> Query(string query)
|
private WindowWalkerListItem[] Query(string query)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(query);
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
|
||||||
@@ -46,13 +47,37 @@ internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposabl
|
|||||||
|
|
||||||
WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.UpdateDesktopList();
|
WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.UpdateDesktopList();
|
||||||
OpenWindows.Instance.UpdateOpenWindowsList(_cancellationTokenSource.Token);
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IListItem[] GetItems() => Query(SearchText).ToArray();
|
var results = new Scored<Window>[windows.Count];
|
||||||
|
for (var i = 0; i < windows.Count; i++)
|
||||||
|
{
|
||||||
|
results[i] = new Scored<Window> { Item = windows[i], Score = 100 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultHelper.GetResultList(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scored = ListHelpers.FilterListWithScores(windows, query, ScoreFunction);
|
||||||
|
return ResultHelper.GetResultList([.. scored]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user