Files
PowerToys/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Components/SearchController.cs
Heiko 34e4e7e5bd [PT Run] TimeDate plugin (#16662)
* create plugin

* Update plugin code

* fix deps

* last changes

* unix

* new results and small changes

* Update settings name

* make spellcheck happy

* new time/date formats

* add comment

* code cleanup, installer, signing pipeline

* fix unix result

* UnitTests

* spell fix

* Update tests, Timestamp query feature

* new formats

* last changes

* last changes

* unit tests and fixes

* cjhanges and fixes

* fix installer

* fix settings class init

* context menu

* fix tests

* add settings tests

* update/fix DateTimeResult tests

* small improvements

* update pipeline

* enable analyzer

* fixes and improvements

* spell fix

* dev docs

* doc fixes

* spell fix

* last changes

* changes and fixes

* fixes and test updates

* improvements

* last changes

* try to fix tests

* remove obsolete code

* add info to test log

* fix search

* tag fix

* tests

* change tests

* update dev docs

* fix spelling

* fix culture for ui strings

* improvements based on feedback

* improve global search

* improve text

* docs improvement

* add settings note

* fix and update tests

* fix spelling
2022-03-17 19:33:05 +00:00

183 lines
7.8 KiB
C#

// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.PowerToys.Run.Plugin.TimeDate.Properties;
using Wox.Infrastructure;
using Wox.Plugin;
namespace Microsoft.PowerToys.Run.Plugin.TimeDate.Components
{
/// <summary>
/// SearchController: Class tot hold the search method that filter available date time formats
/// Extra class to simplify code in <see cref="Main"/> class
/// </summary>
internal static class SearchController
{
/// <summary>
/// Var that holds the delimiter between format and date
/// </summary>
private const string InputDelimiter = "::";
/// <summary>
/// A list of conjunctions that we ignore on search
/// </summary>
private static readonly string[] _conjunctionList = Resources.Microsoft_plugin_timedate_Search_ConjunctionList.Split("; ");
/// <summary>
/// Searches for results
/// </summary>
/// <param name="query">Search query object</param>
/// <returns>List of Wox <see cref="Result"/>s.</returns>
internal static List<Result> ExecuteSearch(Query query, string iconTheme)
{
List<AvailableResult> availableFormats = new List<AvailableResult>();
List<Result> results = new List<Result>();
bool isKeywordSearch = !string.IsNullOrEmpty(query.ActionKeyword);
bool isEmptySearchInput = string.IsNullOrEmpty(query.Search);
string searchTerm = query.Search;
// Empty search without keyword => return no results
if (!isKeywordSearch && isEmptySearchInput)
{
return results;
}
// Conjunction search without keyword => return no results
// (This improves the results on global queries.)
if (!isKeywordSearch && _conjunctionList.Any(x => x.Equals(searchTerm, StringComparison.CurrentCultureIgnoreCase)))
{
return results;
}
// Switch search type
if (isEmptySearchInput)
{
// Return all results for system time/date on empty keyword search
availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch));
}
else if (Regex.IsMatch(searchTerm, @".+" + Regex.Escape(InputDelimiter) + @".+"))
{
// Search for specified format with specified time/date value
var userInput = searchTerm.Split(InputDelimiter);
if (TimeAndDateHelper.ParseStringAsDateTime(userInput[1], out DateTime timestamp))
{
availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, null, null, timestamp));
searchTerm = userInput[0];
}
}
else if (TimeAndDateHelper.ParseStringAsDateTime(searchTerm, out DateTime timestamp))
{
// Return all formats for specified time/date value
availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, null, null, timestamp));
searchTerm = string.Empty;
}
else
{
// Search for specified format with system time/date
availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch));
}
// Check searchTerm after getting results to select type of result list
if (string.IsNullOrEmpty(searchTerm))
{
// Generate list with all results
foreach (var f in availableFormats)
{
results.Add(new Result
{
Title = f.Value,
SubTitle = $"{f.Label} - {Resources.Microsoft_plugin_timedate_SubTitleNote}",
ToolTipData = ResultHelper.GetSearchTagToolTip(f, out Visibility v),
ToolTipVisibility = v,
IcoPath = f.GetIconPath(iconTheme),
Action = _ => ResultHelper.CopyToClipBoard(f.Value),
ContextData = f,
});
}
}
else
{
// Generate filtered list of results
foreach (var f in availableFormats)
{
var resultMatchScore = GetMatchScore(searchTerm, f.Label, f.AlternativeSearchTag, !isKeywordSearch);
if (resultMatchScore > 0)
{
results.Add(new Result
{
Title = f.Value,
SubTitle = $"{f.Label} - {Resources.Microsoft_plugin_timedate_SubTitleNote}",
ToolTipData = ResultHelper.GetSearchTagToolTip(f, out Visibility v),
ToolTipVisibility = v,
IcoPath = f.GetIconPath(iconTheme),
Action = _ => ResultHelper.CopyToClipBoard(f.Value),
Score = resultMatchScore,
ContextData = f,
});
}
}
}
// If search term is only a number that can't be parsed return an error message
if (!isEmptySearchInput && results.Count == 0 && searchTerm.Any(char.IsNumber) && Regex.IsMatch(searchTerm, @"\w+\d+$") &&
!searchTerm.Contains(InputDelimiter) && !searchTerm.Any(char.IsWhiteSpace) && !searchTerm.Any(char.IsPunctuation))
{
// Without plugin key word show only if message is not hidden by setting
if (isKeywordSearch || !TimeDateSettings.Instance.HideNumberMessageOnGlobalQuery)
{
results.Add(ResultHelper.CreateNumberErrorResult(iconTheme));
}
}
return results;
}
/// <summary>
/// Checks the format for a match with the user query and returns the score.
/// </summary>
/// <param name="query">The user query.</param>
/// <param name="label">The label of the format.</param>
/// <param name="tags">The search tag list as string.</param>
/// <param name="isGlobalSearch">Is this a global search?</param>
/// <returns>The score for the result.</returns>
private static int GetMatchScore(string query, string label, string tags, bool isGlobalSearch)
{
// The query is global and the first word don't match any word in the label or tags => Return score of zero
if (isGlobalSearch)
{
char[] chars = new char[] { ' ', ',', ';', '(', ')' };
string queryFirstWord = query.Split(chars)[0];
string[] words = $"{label} {tags}".Split(chars);
if (!words.Any(x => x.Trim().Equals(queryFirstWord, StringComparison.CurrentCultureIgnoreCase)))
{
return 0;
}
}
// Get match for label (or for tags if label score is <1)
int score = StringMatcher.FuzzySearch(query, label).Score;
if (score < 1)
{
foreach (string t in tags.Split(";"))
{
var tagScore = StringMatcher.FuzzySearch(query, t.Trim()).Score / 2;
if (tagScore > score)
{
score = tagScore / 2;
}
}
}
return score;
}
}
}