// 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; } } }