// 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
{
///
/// SearchController: Class tot hold the search method that filter available date time formats
/// Extra class to simplify code in class
///
internal static class SearchController
{
///
/// Var that holds the delimiter between format and date
///
private const string InputDelimiter = "::";
///
/// A list of conjunctions that we ignore on search
///
private static readonly string[] _conjunctionList = Resources.Microsoft_plugin_timedate_Search_ConjunctionList.Split("; ");
///
/// Searches for results
///
/// Search query object
/// List of Wox s.
internal static List ExecuteSearch(Query query, string iconTheme)
{
List availableFormats = new List();
List results = new List();
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;
}
///
/// Checks the format for a match with the user query and returns the score.
///
/// The user query.
/// The label of the format.
/// The search tag list as string.
/// Is this a global search?
/// The score for the result.
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;
}
}
}