diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index b203967de..44c34968c 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -35,7 +35,8 @@ namespace Flow.Launcher.Core UpdateInfo newUpdateInfo; if (!silentUpdate) - api.ShowMsg("Please wait...", "Checking for new update"); + api.ShowMsg(api.GetTranslation("pleaseWait"), + api.GetTranslation("update_flowlauncher_update_check")); using var updateManager = await GitHubUpdateManager(GitHubRepository).ConfigureAwait(false); @@ -51,12 +52,13 @@ namespace Flow.Launcher.Core if (newReleaseVersion <= currentVersion) { if (!silentUpdate) - MessageBox.Show("You already have the latest Flow Launcher version"); + MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest")); return; } if (!silentUpdate) - api.ShowMsg("Update found", "Updating..."); + api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), + api.GetTranslation("update_flowlauncher_updating")); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -67,8 +69,9 @@ namespace Flow.Launcher.Core var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) - MessageBox.Show("Flow Launcher was not able to move your user profile data to the new update version. Please manually " + - $"move your profile data folder from {DataLocation.PortableDataPath} to {targetDestination}"); + MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), + DataLocation.PortableDataPath, + targetDestination)); } else { @@ -79,7 +82,7 @@ namespace Flow.Launcher.Core Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); - if (MessageBox.Show(newVersionTips, "New Update", MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { UpdateManager.RestartApp(Constant.ApplicationFileName); } @@ -87,7 +90,8 @@ namespace Flow.Launcher.Core catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) { Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); - api.ShowMsg("Update Failed", "Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com."); + api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), + api.GetTranslation("update_flowlauncher_check_connection")); return; } } diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 5af50bc6d..de2e82359 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -16,13 +16,7 @@ namespace Flow.Launcher.Infrastructure.Http { private const string UserAgent = @"Mozilla/5.0 (Trident/7.0; rv:11.0) like Gecko"; - private static HttpClient client; - - private static SocketsHttpHandler socketsHttpHandler = new SocketsHttpHandler() - { - UseProxy = true, - Proxy = WebProxy - }; + private static HttpClient client = new HttpClient(); static Http() { @@ -32,8 +26,8 @@ namespace Flow.Launcher.Infrastructure.Http | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; - client = new HttpClient(socketsHttpHandler, false); client.DefaultRequestHeaders.Add("User-Agent", UserAgent); + HttpClient.DefaultProxy = WebProxy; } private static HttpProxy proxy; @@ -45,6 +39,7 @@ namespace Flow.Launcher.Infrastructure.Http { proxy = value; proxy.PropertyChanged += UpdateProxy; + UpdateProxy(ProxyProperty.Enabled); } } diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 80fd12820..6c2a94e82 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Text; using JetBrains.Annotations; @@ -8,14 +9,109 @@ using ToolGood.Words.Pinyin; namespace Flow.Launcher.Infrastructure { + public class TranslationMapping + { + private bool constructed; + + private List originalIndexs = new List(); + private List translatedIndexs = new List(); + private int translaedLength = 0; + + public string key { get; private set; } + + public void setKey(string key) + { + this.key = key; + } + + public void AddNewIndex(int originalIndex, int translatedIndex, int length) + { + if (constructed) + throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); + + originalIndexs.Add(originalIndex); + translatedIndexs.Add(translatedIndex); + translatedIndexs.Add(translatedIndex + length); + translaedLength += length - 1; + } + + public int MapToOriginalIndex(int translatedIndex) + { + if (translatedIndex > translatedIndexs.Last()) + return translatedIndex - translaedLength - 1; + + int lowerBound = 0; + int upperBound = originalIndexs.Count - 1; + + int count = 0; + + // Corner case handle + if (translatedIndex < translatedIndexs[0]) + return translatedIndex; + if (translatedIndex > translatedIndexs.Last()) + { + int indexDef = 0; + for (int k = 0; k < originalIndexs.Count; k++) + { + indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2]; + } + + return translatedIndex - indexDef - 1; + } + + // Binary Search with Range + for (int i = originalIndexs.Count / 2;; count++) + { + if (translatedIndex < translatedIndexs[i * 2]) + { + // move to lower middle + upperBound = i; + i = (i + lowerBound) / 2; + } + else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1) + { + lowerBound = i; + // move to upper middle + // due to floor of integer division, move one up on corner case + i = (i + upperBound + 1) / 2; + } + else + return originalIndexs[i]; + + if (upperBound - lowerBound <= 1 && + translatedIndex > translatedIndexs[lowerBound * 2 + 1] && + translatedIndex < translatedIndexs[upperBound * 2]) + { + int indexDef = 0; + + for (int j = 0; j < upperBound; j++) + { + indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2]; + } + + return translatedIndex - indexDef - 1; + } + } + } + + public void endConstruct() + { + if (constructed) + throw new InvalidOperationException("Mapping has already been constructed"); + constructed = true; + } + } + public interface IAlphabet { - string Translate(string stringToTranslate); + public (string translation, TranslationMapping map) Translate(string stringToTranslate); } public class PinyinAlphabet : IAlphabet { - private ConcurrentDictionary _pinyinCache = new ConcurrentDictionary(); + private ConcurrentDictionary _pinyinCache = + new ConcurrentDictionary(); + private Settings _settings; public void Initialize([NotNull] Settings settings) @@ -23,7 +119,7 @@ namespace Flow.Launcher.Infrastructure _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public string Translate(string content) + public (string translation, TranslationMapping map) Translate(string content) { if (_settings.ShouldUsePinyin) { @@ -34,14 +130,7 @@ namespace Flow.Launcher.Infrastructure var resultList = WordsHelper.GetPinyinList(content); StringBuilder resultBuilder = new StringBuilder(); - - for (int i = 0; i < resultList.Length; i++) - { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) - resultBuilder.Append(resultList[i].First()); - } - - resultBuilder.Append(' '); + TranslationMapping map = new TranslationMapping(); bool pre = false; @@ -49,6 +138,7 @@ namespace Flow.Launcher.Infrastructure { if (content[i] >= 0x3400 && content[i] <= 0x9FD5) { + map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1); resultBuilder.Append(' '); resultBuilder.Append(resultList[i]); pre = true; @@ -60,15 +150,21 @@ namespace Flow.Launcher.Infrastructure pre = false; resultBuilder.Append(' '); } + resultBuilder.Append(resultList[i]); } } - return _pinyinCache[content] = resultBuilder.ToString(); + map.endConstruct(); + + var key = resultBuilder.ToString(); + map.setKey(key); + + return _pinyinCache[content] = (key, map); } else { - return content; + return (content, null); } } else @@ -78,7 +174,7 @@ namespace Flow.Launcher.Infrastructure } else { - return content; + return (content, null); } } } diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index 2a4270fb4..3ffa9f7b1 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -1,8 +1,7 @@ +using Flow.Launcher.Plugin.SharedModels; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using static Flow.Launcher.Infrastructure.StringMatcher; namespace Flow.Launcher.Infrastructure { @@ -32,7 +31,20 @@ namespace Flow.Launcher.Infrastructure } /// - /// Current method: + /// Current method has two parts, Acronym Match and Fuzzy Search: + /// + /// Acronym Match: + /// Charater listed below will be considered as acronym + /// 1. Character on index 0 + /// 2. Character appears after a space + /// 3. Character that is UpperCase + /// 4. Character that is number + /// + /// Acronym Match will succeed when all query characters match with acronyms in stringToCompare. + /// If any of the characters in the query isn't matched with stringToCompare, Acronym Match will fail. + /// Score will be calculated based the percentage of all query characters matched with total acronyms in stringToCompare. + /// + /// Fuzzy Search: /// Character matching + substring matching; /// 1. Query search string is split into substrings, separator is whitespace. /// 2. Check each query substring's characters against full compare string, @@ -44,20 +56,21 @@ namespace Flow.Launcher.Infrastructure /// public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt) { - if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) return new MatchResult (false, UserSettingSearchPrecision); - - query = query.Trim(); + if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) + return new MatchResult(false, UserSettingSearchPrecision); - if (_alphabet != null) - { - query = _alphabet.Translate(query); - stringToCompare = _alphabet.Translate(stringToCompare); - } + query = query.Trim(); + TranslationMapping translationMapping; + (stringToCompare, translationMapping) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null); + + var currentAcronymQueryIndex = 0; + var acronymMatchData = new List(); + int acronymsTotalCount = 0; + int acronymsMatched = 0; var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; - var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query; - + var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); int currentQuerySubstringIndex = 0; var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; @@ -75,17 +88,44 @@ namespace Flow.Launcher.Infrastructure for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) { + // If acronyms matching successfully finished, this gets the remaining not matched acronyms for score calculation + if (currentAcronymQueryIndex >= query.Length && acronymsMatched == query.Length) + { + if (IsAcronymCount(stringToCompare, compareStringIndex)) + acronymsTotalCount++; + continue; + } + + if (currentAcronymQueryIndex >= query.Length || + currentAcronymQueryIndex >= query.Length && allQuerySubstringsMatched) + break; // To maintain a list of indices which correspond to spaces in the string to compare // To populate the list only for the first query substring - if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0) - { + if (fullStringToCompareWithoutCase[compareStringIndex] == ' ' && currentQuerySubstringIndex == 0) spaceIndices.Add(compareStringIndex); + + // Acronym Match + if (IsAcronym(stringToCompare, compareStringIndex)) + { + if (fullStringToCompareWithoutCase[compareStringIndex] == + queryWithoutCase[currentAcronymQueryIndex]) + { + acronymMatchData.Add(compareStringIndex); + acronymsMatched++; + + currentAcronymQueryIndex++; + } } - if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex]) + if (IsAcronymCount(stringToCompare, compareStringIndex)) + acronymsTotalCount++; + + if (allQuerySubstringsMatched || fullStringToCompareWithoutCase[compareStringIndex] != + currentQuerySubstring[currentQuerySubstringCharacterIndex]) { matchFoundInPreviousLoop = false; + continue; } @@ -107,14 +147,16 @@ namespace Flow.Launcher.Infrastructure // in order to do so we need to verify all previous chars are part of the pattern var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex; - if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring)) + if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, + fullStringToCompareWithoutCase, currentQuerySubstring)) { matchFoundInPreviousLoop = true; // if it's the beginning character of the first query substring that is matched then we need to update start index firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex; - indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList); + indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, + firstMatchIndexInWord, indexList); } } @@ -127,49 +169,96 @@ namespace Flow.Launcher.Infrastructure if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length) { // if any of the substrings was not matched then consider as all are not matched - allSubstringsContainedInCompareString = matchFoundInPreviousLoop && allSubstringsContainedInCompareString; + allSubstringsContainedInCompareString = + matchFoundInPreviousLoop && allSubstringsContainedInCompareString; currentQuerySubstringIndex++; - allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); + allQuerySubstringsMatched = + AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); + if (allQuerySubstringsMatched) - break; + continue; // otherwise move to the next query substring currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; currentQuerySubstringCharacterIndex = 0; } } - + + // return acronym match if all query char matched + if (acronymsMatched > 0 && acronymsMatched == query.Length) + { + int acronymScore = acronymsMatched * 100 / acronymsTotalCount; + + if (acronymScore >= (int)UserSettingSearchPrecision) + { + acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); + return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore); + } + } + // proceed to calculate score if every char or substring without whitespaces matched if (allQuerySubstringsMatched) { var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex); - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, + lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); - return new MatchResult(true, UserSettingSearchPrecision, indexList, score); + var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); + return new MatchResult(true, UserSettingSearchPrecision, resultList, score); } return new MatchResult(false, UserSettingSearchPrecision); } + private bool IsAcronym(string stringToCompare, int compareStringIndex) + { + if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex)) + return true; + + return false; + } + + // When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5 + private bool IsAcronymCount(string stringToCompare, int compareStringIndex) + { + if (IsAcronymChar(stringToCompare, compareStringIndex)) + return true; + + if (IsAcronymNumber(stringToCompare, compareStringIndex)) + return compareStringIndex == 0 || char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); + + return false; + } + + private bool IsAcronymChar(string stringToCompare, int compareStringIndex) + => char.IsUpper(stringToCompare[compareStringIndex]) || + compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym + char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); + + private bool IsAcronymNumber(string stringToCompare, int compareStringIndex) + => stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9; + // To get the index of the closest space which preceeds the first matching index private int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) { - if (spaceIndices.Count == 0) + var closestSpaceIndex = -1; + + // spaceIndices should be ordered asc + foreach (var index in spaceIndices) { - return -1; - } - else - { - int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault(); - int closestSpaceIndex = ind ?? -1; - return closestSpaceIndex; + if (index < firstMatchIndex) + closestSpaceIndex = index; + else + break; } + + return closestSpaceIndex; } private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, - string fullStringToCompareWithoutCase, string currentQuerySubstring) + string fullStringToCompareWithoutCase, string currentQuerySubstring) { var allMatch = true; for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) @@ -183,8 +272,9 @@ namespace Flow.Launcher.Infrastructure return allMatch; } - - private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List indexList) + + private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, + int firstMatchIndexInWord, List indexList) { var updatedList = new List(); @@ -202,10 +292,12 @@ namespace Flow.Launcher.Infrastructure private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, int querySubstringsLength) { + // Acronym won't utilize the substring to match return currentQuerySubstringIndex >= querySubstringsLength; } - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString) + private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, + bool allSubstringsContainedInCompareString) { // A match found near the beginning of a string is scored more than a match found near the end // A match is scored more if the characters in the patterns are closer to each other, @@ -239,74 +331,6 @@ namespace Flow.Launcher.Infrastructure return score; } - - public enum SearchPrecisionScore - { - Regular = 50, - Low = 20, - None = 0 - } - } - - public class MatchResult - { - public MatchResult(bool success, SearchPrecisionScore searchPrecision) - { - Success = success; - SearchPrecision = searchPrecision; - } - - public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) - { - Success = success; - SearchPrecision = searchPrecision; - MatchData = matchData; - RawScore = rawScore; - } - - public bool Success { get; set; } - - /// - /// The final score of the match result with search precision filters applied. - /// - public int Score { get; private set; } - - /// - /// The raw calculated search score without any search precision filtering applied. - /// - private int _rawScore; - - public int RawScore - { - get { return _rawScore; } - set - { - _rawScore = value; - Score = ScoreAfterSearchPrecisionFilter(_rawScore); - } - } - - /// - /// Matched data to highlight. - /// - public List MatchData { get; set; } - - public SearchPrecisionScore SearchPrecision { get; set; } - - public bool IsSearchPrecisionScoreMet() - { - return IsSearchPrecisionScoreMet(_rawScore); - } - - private bool IsSearchPrecisionScoreMet(int rawScore) - { - return rawScore >= (int)SearchPrecision; - } - - private int ScoreAfterSearchPrecisionFilter(int rawScore) - { - return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0; - } } public class MatchOption diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 769237bcb..76a370978 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Drawing; using System.Text.Json.Serialization; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Infrastructure.UserSettings { @@ -38,7 +39,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// public bool ShouldUsePinyin { get; set; } = false; - internal StringMatcher.SearchPrecisionScore QuerySearchPrecision { get; private set; } = StringMatcher.SearchPrecisionScore.Regular; + internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular; [JsonIgnore] public string QuerySearchPrecisionString @@ -48,8 +49,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings { try { - var precisionScore = (StringMatcher.SearchPrecisionScore)Enum - .Parse(typeof(StringMatcher.SearchPrecisionScore), value); + var precisionScore = (SearchPrecisionScore)Enum + .Parse(typeof(SearchPrecisionScore), value); QuerySearchPrecision = precisionScore; StringMatcher.Instance.UserSettingSearchPrecision = precisionScore; @@ -58,8 +59,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings { Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e); - QuerySearchPrecision = StringMatcher.SearchPrecisionScore.Regular; - StringMatcher.Instance.UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular; + QuerySearchPrecision = SearchPrecisionScore.Regular; + StringMatcher.Instance.UserSettingSearchPrecision = SearchPrecisionScore.Regular; throw; } diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 698802ba3..0eefe5c4f 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 diff --git a/Flow.Launcher.Plugin/IPublicAPI.cs b/Flow.Launcher.Plugin/IPublicAPI.cs index 12e430e07..dd73eb0e5 100644 --- a/Flow.Launcher.Plugin/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/IPublicAPI.cs @@ -1,5 +1,9 @@ -using System; +using Flow.Launcher.Plugin.SharedModels; +using JetBrains.Annotations; +using System; using System.Collections.Generic; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace Flow.Launcher.Plugin @@ -83,5 +87,18 @@ namespace Flow.Launcher.Plugin /// if you want to hook something like Ctrl+R, you should use this event /// event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; + + MatchResult FuzzySearch(string query, string stringToCompare); + + Task HttpGetStringAsync(string url, CancellationToken token = default); + + Task HttpGetStreamAsync(string url, CancellationToken token = default); + + Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath); + + void AddActionKeyword(string pluginId, string newActionKeyword); + + void RemoveActionKeyword(string pluginId, string oldActionKeyword); + } } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 27cd1a558..be33bd86c 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -121,7 +121,7 @@ namespace Flow.Launcher.Plugin.SharedCommands public static void OpenPath(string fileOrFolderPath) { - var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = fileOrFolderPath }; + var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' }; try { if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath)) @@ -146,31 +146,23 @@ namespace Flow.Launcher.Plugin.SharedCommands /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. /// - public static bool IsLocationPathString(string querySearchString) + public static bool IsLocationPathString(this string querySearchString) { - if (string.IsNullOrEmpty(querySearchString)) + if (string.IsNullOrEmpty(querySearchString) || querySearchString.Length < 3) return false; // // shared folder location, and not \\\location\ - if (querySearchString.Length >= 3 - && querySearchString.StartsWith(@"\\") - && char.IsLetter(querySearchString[2])) + if (querySearchString.StartsWith(@"\\") + && querySearchString[2] != '\\') return true; // c:\ - if (querySearchString.Length == 3 - && char.IsLetter(querySearchString[0]) + if (char.IsLetter(querySearchString[0]) && querySearchString[1] == ':' && querySearchString[2] == '\\') - return true; - - // c:\\ - if (querySearchString.Length >= 4 - && char.IsLetter(querySearchString[0]) - && querySearchString[1] == ':' - && querySearchString[2] == '\\' - && char.IsLetter(querySearchString[3])) - return true; + { + return querySearchString.Length == 3 || querySearchString[3] != '\\'; + } return false; } diff --git a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs new file mode 100644 index 000000000..5144eb61d --- /dev/null +++ b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin.SharedModels +{ + public class MatchResult + { + public MatchResult(bool success, SearchPrecisionScore searchPrecision) + { + Success = success; + SearchPrecision = searchPrecision; + } + + public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) + { + Success = success; + SearchPrecision = searchPrecision; + MatchData = matchData; + RawScore = rawScore; + } + + public bool Success { get; set; } + + /// + /// The final score of the match result with search precision filters applied. + /// + public int Score { get; private set; } + + /// + /// The raw calculated search score without any search precision filtering applied. + /// + private int _rawScore; + + public int RawScore + { + get { return _rawScore; } + set + { + _rawScore = value; + Score = ScoreAfterSearchPrecisionFilter(_rawScore); + } + } + + /// + /// Matched data to highlight. + /// + public List MatchData { get; set; } + + public SearchPrecisionScore SearchPrecision { get; set; } + + public bool IsSearchPrecisionScoreMet() + { + return IsSearchPrecisionScoreMet(_rawScore); + } + + private bool IsSearchPrecisionScoreMet(int rawScore) + { + return rawScore >= (int)SearchPrecision; + } + + private int ScoreAfterSearchPrecisionFilter(int rawScore) + { + return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0; + } + } + + public enum SearchPrecisionScore + { + Regular = 50, + Low = 20, + None = 0 + } +} diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index 468b94457..bbddcbd2a 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Test { @@ -37,8 +38,8 @@ namespace Flow.Launcher.Test { var listToReturn = new List(); - Enum.GetValues(typeof(StringMatcher.SearchPrecisionScore)) - .Cast() + Enum.GetValues(typeof(SearchPrecisionScore)) + .Cast() .ToList() .ForEach(x => listToReturn.Add((int)x)); @@ -92,7 +93,8 @@ namespace Flow.Launcher.Test [TestCase("cand")] [TestCase("cpywa")] [TestCase("ccs")] - public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm) + public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults( + string searchTerm) { var results = new List(); var matcher = new StringMatcher(); @@ -108,9 +110,9 @@ namespace Flow.Launcher.Test foreach (var precisionScore in GetPrecisionScores()) { var filteredResult = results.Where(result => result.Score >= precisionScore) - .Select(result => result) - .OrderByDescending(x => x.Score) - .ToList(); + .Select(result => result) + .OrderByDescending(x => x.Score) + .ToList(); Debug.WriteLine(""); Debug.WriteLine("###############################################"); @@ -119,6 +121,7 @@ namespace Flow.Launcher.Test { Debug.WriteLine("SCORE: " + item.Score.ToString() + ", FoundString: " + item.Title); } + Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -128,37 +131,47 @@ namespace Flow.Launcher.Test [TestCase(Chrome, Chrome, 157)] [TestCase(Chrome, LastIsChrome, 147)] - [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 25)] + [TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)] + [TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)] [TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)] [TestCase(Chrome, CandyCrushSagaFromKing, 0)] [TestCase("sql", MicrosoftSqlServerManagementStudio, 110)] - [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)]//double spacing intended + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( string queryString, string compareString, int expectedScore) { // When, Given - var matcher = new StringMatcher(); + var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should - Assert.AreEqual(expectedScore, rawScore, + Assert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}"); } - [TestCase("goo", "Google Chrome", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Google Chrome", StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("chr", "Chrome", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.None, true)] - [TestCase("ccs", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("cand", "Candy Crush Saga from King",StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("cand", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("goo", "Google Chrome", SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Google Chrome", SearchPrecisionScore.Low, true)] + [TestCase("chr", "Chrome", SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Low, true)] + [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Candy Crush Saga from King", SearchPrecisionScore.None, true)] + [TestCase("ccs", "Candy Crush Saga from King", SearchPrecisionScore.Low, true)] + [TestCase("cand", "Candy Crush Saga from King", SearchPrecisionScore.Regular, true)] + [TestCase("cand", "Help cure hope raise on mind entity Chrome", SearchPrecisionScore.Regular, false)] + [TestCase("vsc", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vs", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vc", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("vts", VisualStudioCode, SearchPrecisionScore.Regular, false)] + [TestCase("vcs", VisualStudioCode, SearchPrecisionScore.Regular, false)] + [TestCase("wt", "Windows Terminal From Microsoft Store", SearchPrecisionScore.Regular, false)] + [TestCase("vsp", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)] + [TestCase("vsp", "2019 Visual Studio Preview", SearchPrecisionScore.Regular, true)] + [TestCase("2019p", "Visual Studio 2019 Preview", SearchPrecisionScore.Regular, true)] public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( string queryString, string compareString, - StringMatcher.SearchPrecisionScore expectedPrecisionScore, + SearchPrecisionScore expectedPrecisionScore, bool expectedPrecisionResult) { // When @@ -170,48 +183,50 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); - Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); + Debug.WriteLine( + $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), - $"Query:{queryString}{Environment.NewLine} " + - $"Compare:{compareString}{Environment.NewLine}" + + $"Query: {queryString}{Environment.NewLine} " + + $"Compare: {compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + $"Precision Score: {(int)expectedPrecisionScore}"); } - [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("term", "Windows Terminal (Preview)", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql serv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("servez", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql servz", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("sql studio", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("mic", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Shutdown", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("mssms", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("a test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("cod", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("code", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("codes", "Visual Studio Codes", StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", SearchPrecisionScore.Regular, false)] + [TestCase("term", "Windows Terminal (Preview)", SearchPrecisionScore.Regular, true)] + [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql serv", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("servez", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql servz", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, false)] + [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("sql studio", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("mic", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("mssms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("msms", MicrosoftSqlServerManagementStudio, SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Shutdown", SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, false)] + [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", SearchPrecisionScore.Regular, true)] + [TestCase("a test", "This is a test", SearchPrecisionScore.Regular, true)] + [TestCase("test", "This is a test", SearchPrecisionScore.Regular, true)] + [TestCase("cod", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("code", VisualStudioCode, SearchPrecisionScore.Regular, true)] + [TestCase("codes", "Visual Studio Codes", SearchPrecisionScore.Regular, true)] public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings( string queryString, string compareString, - StringMatcher.SearchPrecisionScore expectedPrecisionScore, + SearchPrecisionScore expectedPrecisionScore, bool expectedPrecisionResult) { // When - var matcher = new StringMatcher { UserSettingSearchPrecision = expectedPrecisionScore }; + var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore}; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -219,7 +234,8 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); - Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); + Debug.WriteLine( + $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -238,7 +254,7 @@ namespace Flow.Launcher.Test string queryString, string compareString1, string compareString2) { // When - var matcher = new StringMatcher { UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular }; + var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; // Given var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); @@ -247,8 +263,10 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}"); - Debug.WriteLine($"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}"); - Debug.WriteLine($"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -256,13 +274,13 @@ namespace Flow.Launcher.Test Assert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + - $"Should be greater than{ Environment.NewLine}" + + $"Should be greater than{Environment.NewLine}" + $"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}"); } [TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")] public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( - string queryString, string firstName, string firstDescription, string firstExecutableName, + string queryString, string firstName, string firstDescription, string firstExecutableName, string secondName, string secondDescription, string secondExecutableName) { // Act @@ -275,15 +293,39 @@ namespace Flow.Launcher.Test var secondDescriptionMatch = matcher.FuzzyMatch(queryString, secondDescription).RawScore; var secondExecutableNameMatch = matcher.FuzzyMatch(queryString, secondExecutableName).RawScore; - var firstScore = new[] { firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch }.Max(); - var secondScore = new[] { secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch }.Max(); + var firstScore = new[] {firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch}.Max(); + var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max(); // Assert Assert.IsTrue(firstScore > secondScore, $"Query: \"{queryString}\"{Environment.NewLine} " + $"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" + - $"Should be greater than{ Environment.NewLine}" + + $"Should be greater than{Environment.NewLine}" + $"Name of second: \"{secondName}\", Final Score: {secondScore}{Environment.NewLine}"); } + + [TestCase("vsc", "Visual Studio Code", 100)] + [TestCase("jbr", "JetBrain Rider", 100)] + [TestCase("jr", "JetBrain Rider", 66)] + [TestCase("vs", "Visual Studio", 100)] + [TestCase("vs", "Visual Studio Preview", 66)] + [TestCase("vsp", "Visual Studio Preview", 100)] + [TestCase("pc", "postman canary", 100)] + [TestCase("psc", "Postman super canary", 100)] + [TestCase("psc", "Postman super Canary", 100)] + [TestCase("vsp", "Visual Studio", 0)] + [TestCase("vps", "Visual Studio", 0)] + [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 75)] + public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString, + int desiredScore) + { + var matcher = new StringMatcher(); + var score = matcher.FuzzyMatch(queryString, compareString).Score; + Assert.IsTrue(score == desiredScore, + $@"Query: ""{queryString}"" + CompareString: ""{compareString}"" + Score: {score} + Desired Score: {desiredScore}"); + } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 09c7d9a30..3d0a9a64f 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -24,7 +24,7 @@ namespace Flow.Launcher.Test.Plugins return new List(); } - private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString) + private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token) { return new List { @@ -60,8 +60,8 @@ namespace Flow.Launcher.Test.Plugins $"Actual: {result}{Environment.NewLine}"); } - [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\'")] - [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\'")] + [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")] + [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString) { // Given @@ -79,7 +79,7 @@ namespace Flow.Launcher.Test.Plugins [TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + "FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" + - " AND directory='file:C:\\SomeFolder'")] + " AND directory='file:C:\\SomeFolder' ORDER BY System.FileName")] public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -116,11 +116,8 @@ namespace Flow.Launcher.Test.Plugins [TestCase("scope='file:'")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) { - // Given - var queryConstructor = new QueryConstructor(new Settings()); - //When - var resultString = queryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch; // Then Assert.IsTrue(resultString == expectedString, @@ -130,7 +127,7 @@ namespace Flow.Launcher.Test.Plugins [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:'")] + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -205,7 +202,7 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:'")] + "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -243,6 +240,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"cc:\", false)] [TestCase(@"\\\SomeNetworkLocation\", false)] [TestCase("RandomFile", false)] + [TestCase(@"c:\>*", true)] + [TestCase(@"c:\>", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) { // When, Given @@ -295,9 +295,9 @@ namespace Flow.Launcher.Test.Plugins } [TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")] - [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + - "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + - "scope='file:c:\\SomeFolder'")] + [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + + "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + + "scope='file:c:\\SomeFolder'")] public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) { // Given @@ -317,11 +317,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase("c:\\somefolder\\", "*")] public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSearchCriteriaShouldUseCriteriaString(string path, string expectedString) { - // Given - var criteriaConstructor = new DirectoryInfoSearch(new PluginInitContext()); //When - var resultString = criteriaConstructor.ConstructSearchCriteria(path); + var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path); // Then Assert.IsTrue(resultString == expectedString, diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 06bb16e3b..7c4c6a367 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -61,6 +61,8 @@ namespace Flow.Launcher _settingsVM = new SettingWindowViewModel(_updater, _portable); _settings = _settingsVM.Settings; + Http.Proxy = _settings.Proxy; + _alphabet.Initialize(_settings); _stringMatcher = new StringMatcher(_alphabet); StringMatcher.Instance = _stringMatcher; @@ -85,8 +87,6 @@ namespace Flow.Launcher ThemeManager.Instance.Settings = _settings; ThemeManager.Instance.ChangeTheme(_settings.Theme); - Http.Proxy = _settings.Proxy; - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); RegisterExitEvents(); diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index 5f4cdff19..a97f90733 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -5,7 +5,7 @@ Icon="Images\app.png" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" - Title="Custom Plugin Hotkey" Height="200" Width="674.766"> + Title="{DynamicResource customeQueryHotkeyTitle}" Height="200" Width="674.766"> diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index fddaaab6a..289a502d0 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -1,4 +1,4 @@ - + WinExe diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 1e0e6a7e0..b6bf76b7f 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -17,6 +17,7 @@ Flow Launcher Settings General + Portable Mode Start Flow Launcher on system startup Hide Flow Launcher when focus is lost Do not show new version notifications @@ -39,12 +40,13 @@ Plugin Find more plugins + Enable Disable Action keyword: Current action keyword: New action keyword: - Current Priority: - New Priority: + Current Priority: + New Priority: Plugin Directory Author Init time: @@ -109,7 +111,7 @@ Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number Please provide an valid integer for Priority! - + Old Action Keyword New Action Keyword @@ -122,6 +124,7 @@ Use * if you don't want to specify an action keyword + Custom Plugin Hotkey Preview Hotkey is unavailable, please select a new hotkey Invalid plugin hotkey @@ -146,11 +149,23 @@ Failed to send report Flow Launcher got an error + + Please wait... + + Checking for new update + You already have the latest Flow Launcher version + Update found + Updating... + Flow Launcher was not able to move your user profile data to the new update version. + Please manually move your profile data folder from {0} to {1} + New Update New Flow Launcher release {0} is now available An error occurred while trying to install software updates Update Cancel + Update Failed + Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com. This upgrade will restart Flow Launcher Following files will be updated Update files diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index 85fc1462c..64230a93a 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -17,6 +17,7 @@ Nastavenia Flow Launchera Všeobecné + Prenosný režim Spustiť Flow Launcher po štarte systému Schovať Flow Launcher po strate fokusu Nezobrazovať upozornenia na novú verziu @@ -39,10 +40,13 @@ Plugin Nájsť ďalšie pluginy + Povoliť Zakázať Skratka akcie Aktuálna akcia skratky: Nová akcia skratky: + Aktuálna priorita: + Nová priorita: Priečinok s pluginmi Autor Príprava: @@ -104,6 +108,10 @@ Poznámky k vydaniu + + Vyššie číslo znamená, že výsledok bude vyššie. Skúste nastaviť napr. 5. Ak chcete, aby boli výsledky nižšie ako ktorékoľvek iné doplnky, zadajte záporné číslo + Prosím, zadajte platné číslo pre prioritu! + Stará skratka akcie Nová skratka akcie @@ -116,6 +124,7 @@ Použite * ak nechcete určiť skratku pre akciu + Vlastná klávesová skratka pre plugin Náhľad Klávesová skratka je nedostupná, prosím, zadajte novú Neplatná klávesová skratka pluginu @@ -140,11 +149,23 @@ Odoslanie hlásenia zlyhalo Flow Launcher zaznamenal chybu + + Čakajte, prosím… + - Je dostupná nová verzia Flow Launcher {0} + Kontrolujú sa akutalizácie + Už máte najnovšiu verizu Flow Launchera + Bola nájdená aktualizácia + Aktualizuje sa… + Flow Launcher nedokázal presunúť používateľské údaje do aktualizovanej verzie. + Prosím, presuňte profilový priečinok „data“ z {0} do {1} + Nová aktualizácia + Je dostupná nová verzia Flow Launchera {0} Počas inštalácie aktualizácií došlo k chybe Aktualizovať Zrušiť + Aktualizácia zlyhala + Skontrolujte pripojenie a skúste aktualizovať nastavenia servera proxy na github-cloud.s3.amazonaws.com. Tento upgrade reštartuje Flow Launcher Nasledujúce súbory budú aktualizované Aktualizovať súbory diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index a2cfe569d..4cc0b4428 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -97,7 +97,7 @@ Background="Transparent"/> diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 3812b4e1f..04a1063f8 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -26,6 +26,7 @@ namespace Flow.Launcher #region Private Fields private readonly Storyboard _progressBarStoryboard = new Storyboard(); + private bool isProgressBarStoryboardPaused; private Settings _settings; private NotifyIcon _notifyIcon; private MainViewModel _viewModel; @@ -52,7 +53,7 @@ namespace Flow.Launcher private void OnInitialized(object sender, EventArgs e) { - + } private void OnLoaded(object sender, RoutedEventArgs _) @@ -73,7 +74,7 @@ namespace Flow.Launcher { if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility)) { - if (Visibility == Visibility.Visible) + if (_viewModel.MainWindowVisibility == Visibility.Visible) { Activate(); QueryTextBox.Focus(); @@ -84,7 +85,34 @@ namespace Flow.Launcher QueryTextBox.SelectAll(); _viewModel.LastQuerySelected = true; } + + if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Resume(); + isProgressBarStoryboardPaused = false; + } } + else if (!isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Pause(); + isProgressBarStoryboardPaused = true; + } + } + else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility)) + { + Dispatcher.Invoke(() => + { + if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Pause(); + isProgressBarStoryboardPaused = true; + } + else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused) + { + _progressBarStoryboard.Resume(); + isProgressBarStoryboardPaused = false; + } + }, System.Windows.Threading.DispatcherPriority.Render); } }; _settings.PropertyChanged += (o, e) => @@ -170,6 +198,7 @@ namespace Flow.Launcher _progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever; ProgressBar.BeginStoryboard(_progressBarStoryboard); _viewModel.ProgressBarVisibility = Visibility.Hidden; + isProgressBarStoryboardPaused = true; } private void OnMouseDown(object sender, MouseButtonEventArgs e) diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 17673a62a..427fd9fc6 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -5,7 +5,6 @@ using System.Net; using System.Threading.Tasks; using System.Windows; using Squirrel; -using Flow.Launcher.Core; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -14,6 +13,11 @@ using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; +using Flow.Launcher.Plugin.SharedModels; +using System.Threading; +using System.IO; +using Flow.Launcher.Infrastructure.Http; +using JetBrains.Annotations; namespace Flow.Launcher { @@ -127,6 +131,32 @@ namespace Flow.Launcher public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; + public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); + + public Task HttpGetStringAsync(string url, CancellationToken token = default) + { + return Http.GetAsync(url); + } + + public Task HttpGetStreamAsync(string url, CancellationToken token = default) + { + return Http.GetStreamAsync(url); + } + + public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath) + { + return Http.DownloadAsync(url, filePath); + } + + public void AddActionKeyword(string pluginId, string newActionKeyword) + { + PluginManager.AddActionKeyword(pluginId, newActionKeyword); + } + + public void RemoveActionKeyword(string pluginId, string oldActionKeyword) + { + PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword); + } #endregion #region Private Methods @@ -139,6 +169,7 @@ namespace Flow.Launcher } return true; } + #endregion } } diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml index 21d7e88f7..4c7eac114 100644 --- a/Flow.Launcher/SettingWindow.xaml +++ b/Flow.Launcher/SettingWindow.xaml @@ -37,7 +37,7 @@ - + @@ -166,7 +166,7 @@ - diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8195c745a..05dbb3a8b 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; using System.Windows; using System.Windows.Input; using NHotkey; @@ -18,7 +19,6 @@ using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; using Flow.Launcher.Infrastructure.Logger; -using System.Threading.Tasks.Dataflow; namespace Flow.Launcher.ViewModel { @@ -45,6 +45,7 @@ namespace Flow.Launcher.ViewModel private bool _saved; private readonly Internationalization _translator = InternationalizationManager.Instance; + private BufferBlock _resultsUpdateQueue; private Task _resultsViewUpdateTask; @@ -74,30 +75,14 @@ namespace Flow.Launcher.ViewModel _selectedResults = Results; InitializeKeyCommands(); - RegisterViewUpdate(); RegisterResultsUpdatedEvent(); - SetHotkey(_settings.Hotkey, OnHotkey); SetCustomPluginHotkey(); SetOpenResultModifiers(); } - private void RegisterResultsUpdatedEvent() - { - foreach (var pair in PluginManager.GetPluginsForInterface()) - { - var plugin = (IResultUpdated) pair.Plugin; - plugin.ResultsUpdated += (s, e) => - { - PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); - if (e.Query.Search == _lastQuery.Search) - _resultsUpdateQueue.Post(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)); - }; - } - } - private void RegisterViewUpdate() { _resultsUpdateQueue = new BufferBlock(); @@ -136,6 +121,22 @@ namespace Flow.Launcher.ViewModel } } + private void RegisterResultsUpdatedEvent() + { + foreach (var pair in PluginManager.GetPluginsForInterface()) + { + var plugin = (IResultUpdated)pair.Plugin; + plugin.ResultsUpdated += (s, e) => + { + if (e.Query.RawQuery == QueryText) // TODO: allow cancellation + { + PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); + _resultsUpdateQueue.Post(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)); + } + }; + } + } + private void InitializeKeyCommands() { @@ -233,7 +234,6 @@ namespace Flow.Launcher.ViewModel public ResultsViewModel Results { get; private set; } public ResultsViewModel ContextMenu { get; private set; } public ResultsViewModel History { get; private set; } - private string _lastQueryText; private string _queryText; @@ -393,7 +393,7 @@ namespace Flow.Launcher.ViewModel Title = string.Format(title, h.Query), SubTitle = string.Format(time, h.ExecutedDateTime), IcoPath = "Images\\history.png", - OriginQuery = new Query {RawQuery = h.Query}, + OriginQuery = new Query { RawQuery = h.Query }, Action = _ => { SelectedResults = Results; diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 1b8dd602d..feab3a751 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -139,39 +139,9 @@ namespace Flow.Launcher.ViewModel /// public void AddResults(List newRawResults, string resultId) { - lock (_collectionLock) - { - var newResults = NewResults(newRawResults, resultId); + var newResults = NewResults(newRawResults, resultId); - // https://social.msdn.microsoft.com/Forums/vstudio/en-US/5ff71969-f183-4744-909d-50f7cd414954/binding-a-tabcontrols-selectedindex-not-working?forum=wpf - // fix selected index flow - var updateTask = Task.Run(() => - { - // update UI in one run, so it can avoid UI flickering - - Results.Update(newResults); - if (Results.Any()) - SelectedItem = Results[0]; - }); - if (!updateTask.Wait(300)) - { - updateTask.Dispose(); - throw new TimeoutException("Update result use too much time."); - } - - } - - if (Visbility != Visibility.Visible && Results.Count > 0) - { - Margin = new Thickness { Top = 8 }; - SelectedIndex = 0; - Visbility = Visibility.Visible; - } - else - { - Margin = new Thickness { Top = 0 }; - Visbility = Visibility.Collapsed; - } + UpdateResults(newResults); } /// /// To avoid deadlock, this method should not called from main thread @@ -179,12 +149,18 @@ namespace Flow.Launcher.ViewModel public void AddResults(IEnumerable resultsForUpdates, CancellationToken token) { var newResults = NewResults(resultsForUpdates); + if (token.IsCancellationRequested) return; + + UpdateResults(newResults, token); + } + + private void UpdateResults(List newResults, CancellationToken token = default) + { lock (_collectionLock) { // update UI in one run, so it can avoid UI flickering - Results.Update(newResults, token); if (Results.Any()) SelectedItem = Results[0]; @@ -202,7 +178,6 @@ namespace Flow.Launcher.ViewModel Visbility = Visibility.Collapsed; break; } - } private List NewResults(List newRawResults, string resultId) @@ -212,10 +187,10 @@ namespace Flow.Launcher.ViewModel var results = Results as IEnumerable; - var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)).ToList(); + var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); return results.Where(r => r.Result.PluginID != resultId) - .Concat(results.Intersect(newResults).Union(newResults)) + .Concat(newResults) .OrderByDescending(r => r.Result.Score) .ToList(); } @@ -228,8 +203,7 @@ namespace Flow.Launcher.ViewModel var results = Results as IEnumerable; return results.Where(r => r != null && !resultsForUpdates.Any(u => u.Metadata.ID == r.Result.PluginID)) - .Concat( - resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) + .Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) .OrderByDescending(rv => rv.Result.Score) .ToList(); } @@ -266,49 +240,50 @@ namespace Flow.Launcher.ViewModel } #endregion - public class ResultCollection : ObservableCollection + public class ResultCollection : List, INotifyCollectionChanged { private long editTime = 0; - private bool _suppressNotifying = false; - private CancellationToken _token; - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + public event NotifyCollectionChangedEventHandler CollectionChanged; + + + protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - if (!_suppressNotifying) - { - base.OnCollectionChanged(e); - } + CollectionChanged?.Invoke(this, e); } - public void BulkAddRange(IEnumerable resultViews) + public void BulkAddAll(List resultViews) { - // suppress notifying before adding all element - _suppressNotifying = true; - foreach (var item in resultViews) - { - Add(item); - } - _suppressNotifying = false; - // manually update event - // wpf use directx / double buffered already, so just reset all won't cause ui flickering + AddRange(resultViews); + + // can return because the list will be cleared next time updated, which include a reset event if (_token.IsCancellationRequested) return; + + // manually update event + // wpf use directx / double buffered already, so just reset all won't cause ui flickering OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - public void AddRange(IEnumerable Items) + private void AddAll(List Items) { - foreach (var item in Items) + for (int i = 0; i < Items.Count; i++) { + var item = Items[i]; if (_token.IsCancellationRequested) return; Add(item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); } } - public void RemoveAll() + public void RemoveAll(int Capacity = 512) { - ClearItems(); + Clear(); + if (this.Capacity > 8000 && Capacity < this.Capacity) + this.Capacity = Capacity; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// @@ -323,15 +298,19 @@ namespace Flow.Launcher.ViewModel if (editTime < 10 || newItems.Count < 30) { - if (Count != 0) ClearItems(); - AddRange(newItems); + if (Count != 0) RemoveAll(newItems.Count); + AddAll(newItems); editTime++; return; } else { Clear(); - BulkAddRange(newItems); + BulkAddAll(newItems); + if (Capacity > 8000 && newItems.Count < 3000) + { + Capacity = newItems.Count; + } editTime++; } } diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 3c90f8712..98685dc1b 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.ViewModel { @@ -153,7 +154,7 @@ namespace Flow.Launcher.ViewModel { var precisionStrings = new List(); - var enumList = Enum.GetValues(typeof(StringMatcher.SearchPrecisionScore)).Cast().ToList(); + var enumList = Enum.GetValues(typeof(SearchPrecisionScore)).Cast().ToList(); enumList.ForEach(x => precisionStrings.Add(x.ToString())); diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs index c7013aa67..60c4a0ee6 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.BrowserBookmark.Commands { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs index c9a0b7303..21eb844b4 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs @@ -7,12 +7,13 @@ using System.Windows; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Linq; using MessageBox = System.Windows.Forms.MessageBox; using MessageBoxIcon = System.Windows.Forms.MessageBoxIcon; using MessageBoxButton = System.Windows.Forms.MessageBoxButtons; using DialogResult = System.Windows.Forms.DialogResult; +using Flow.Launcher.Plugin.Explorer.ViewModels; namespace Flow.Launcher.Plugin.Explorer { @@ -22,10 +23,13 @@ namespace Flow.Launcher.Plugin.Explorer private Settings Settings { get; set; } - public ContextMenu(PluginInitContext context, Settings settings) + private SettingsViewModel ViewModel { get; set; } + + public ContextMenu(PluginInitContext context, Settings settings, SettingsViewModel vm) { Context = context; Settings = settings; + ViewModel = vm; } public List LoadContextMenus(Result selectedResult) @@ -50,6 +54,58 @@ namespace Flow.Launcher.Plugin.Explorer var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath; var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder"; + + if (!Settings.QuickAccessLinks.Any(x => x.Path == record.FullPath)) + { + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_title"), + SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_subtitle"), fileOrFolder), + Action = (context) => + { + Settings.QuickAccessLinks.Add(new AccessLink { Path = record.FullPath, Type = record.Type }); + + Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess"), + string.Format( + Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess_detail"), + fileOrFolder), + Constants.ExplorerIconImageFullPath); + + ViewModel.Save(); + + return true; + }, + SubTitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_titletooltip"), + TitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_titletooltip"), + IcoPath = Constants.QuickAccessImagePath + }); + } + else + { + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_remove_from_quickaccess_title"), + SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_remove_from_quickaccess_subtitle"), fileOrFolder), + Action = (context) => + { + Settings.QuickAccessLinks.Remove(Settings.QuickAccessLinks.FirstOrDefault(x => x.Path == record.FullPath)); + + Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess"), + string.Format( + Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess_detail"), + fileOrFolder), + Constants.ExplorerIconImageFullPath); + + ViewModel.Save(); + + return true; + }, + SubTitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_remove_titletooltip"), + TitleToolTip = Context.API.GetTranslation("plugin_explorer_contextmenu_remove_titletooltip"), + IcoPath = Constants.RemoveQuickAccessImagePath + }); + } + contextMenus.Add(new Result { Title = Context.API.GetTranslation("plugin_explorer_copypath"), @@ -228,7 +284,7 @@ namespace Flow.Launcher.Plugin.Explorer Action = _ => { if(!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath)) - Settings.IndexSearchExcludedSubdirectoryPaths.Add(new FolderLink { Path = record.FullPath }); + Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink { Path = record.FullPath }); Task.Run(() => { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png new file mode 100644 index 000000000..470a6782f Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/quickaccess.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png new file mode 100644 index 000000000..fbfb0b960 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/removequickaccess.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index 2fb16e0e1..9ba0da3f6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -16,7 +16,7 @@ Edit Add Customise Action Keywords - Quick Folder Access Paths + Quick Access Links Index Search Excluded Paths Indexing Options Search Activation: @@ -42,5 +42,15 @@ Open Windows Indexing Options Manage indexed files and folders Failed to open Windows Indexing Options + Add to Quick Access + Add the current {0} to Quick Access + Successfully Added + Successfully added to Quick Access + Successfully Removed + Successfully removed from Quick Access + Add to Quick Access so it can be opened with Explorer's Search Activation action keyword + Remove from Quick Access + Remove from Quick Access + Remove the current {0} from Quick Access \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 7b56df691..ae7bf57d2 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -1,8 +1,10 @@ using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.ViewModels; using Flow.Launcher.Plugin.Explorer.Views; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; @@ -32,8 +34,17 @@ namespace Flow.Launcher.Plugin.Explorer viewModel = new SettingsViewModel(context); await viewModel.LoadStorage(); Settings = viewModel.Settings; - contextMenu = new ContextMenu(Context, Settings); + + // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards. + if (Settings.QuickFolderAccessLinks.Any()) + { + Settings.QuickAccessLinks = Settings.QuickFolderAccessLinks; + Settings.QuickFolderAccessLinks = new List(); + } + + contextMenu = new ContextMenu(Context, Settings, viewModel); searchManager = new SearchManager(Settings, Context); + ResultManager.Init(Context); } public List LoadContextMenus(Result selectedResult) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs index 38939e244..78c7c98a5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs @@ -15,6 +15,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search internal const string ExplorerIconImagePath = "Images\\explorer.png"; internal const string DifferentUserIconImagePath = "Images\\user.png"; internal const string IndexingOptionsIconImagePath = "Images\\windowsindexingoptions.png"; + internal const string QuickAccessImagePath = "Images\\quickaccess.png"; + internal const string RemoveQuickAccessImagePath = "Images\\removequickaccess.png"; internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory"; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs index 88d7d6927..acd960ef1 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs @@ -4,29 +4,27 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo { - public class DirectoryInfoSearch + public static class DirectoryInfoSearch { - private readonly ResultManager resultManager; - - public DirectoryInfoSearch(PluginInitContext context) - { - resultManager = new ResultManager(context); - } - - internal List TopLevelDirectorySearch(Query query, string search) + internal static List TopLevelDirectorySearch(Query query, string search, CancellationToken token) { var criteria = ConstructSearchCriteria(search); - if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator)) - return DirectorySearch(SearchOption.AllDirectories, query, search, criteria); + if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > + search.LastIndexOf(Constants.DirectorySeperator)) + return DirectorySearch(new EnumerationOptions + { + RecurseSubdirectories = true + }, query, search, criteria, token); - return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria); + return DirectorySearch(new EnumerationOptions(), query, search, criteria, token); // null will be passed as default } - public string ConstructSearchCriteria(string search) + public static string ConstructSearchCriteria(string search) { string incompleteName = ""; @@ -45,7 +43,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo return incompleteName; } - private List DirectorySearch(SearchOption searchOption, Query query, string search, string searchCriteria) + private static List DirectorySearch(EnumerationOptions enumerationOption, Query query, string search, + string searchCriteria, CancellationToken token) { var results = new List(); @@ -58,38 +57,38 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo { var directoryInfo = new System.IO.DirectoryInfo(path); - foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, searchOption)) + foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption)) { - if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; - if (fileSystemInfo is System.IO.DirectoryInfo) { - folderList.Add(resultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, fileSystemInfo.FullName, query, true, false)); + folderList.Add(ResultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, + fileSystemInfo.FullName, query, true, false)); } else { - fileList.Add(resultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false)); + fileList.Add(ResultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false)); } + + token.ThrowIfCancellationRequested(); } } catch (Exception e) { - if (e is UnauthorizedAccessException || e is ArgumentException) - { - results.Add(new Result { Title = e.Message, Score = 501 }); + if (!(e is ArgumentException)) + throw e; + + results.Add(new Result {Title = e.Message, Score = 501}); - return results; - } + return results; #if DEBUG // Please investigate and handle error from DirectoryInfo search - throw e; #else Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e); -#endif +#endif } - // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. + // Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList(); } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs index 6a870f149..1e9815cb9 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs @@ -76,7 +76,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { var expandedPath = environmentVariables[search]; - results.Add(new ResultManager(context).CreateFolderResult($"%{search}%", expandedPath, expandedPath, query)); + results.Add(ResultManager.CreateFolderResult($"%{search}%", expandedPath, expandedPath, query)); return results; } @@ -95,7 +95,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { if (p.Key.StartsWith(search, StringComparison.InvariantCultureIgnoreCase)) { - results.Add(new ResultManager(context).CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query)); + results.Add(ResultManager.CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query)); } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs deleted file mode 100644 index 8bd19956e..000000000 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks -{ - public class QuickFolderAccess - { - internal List FolderListMatched(Query query, List folderLinks, PluginInitContext context) - { - if (string.IsNullOrEmpty(query.Search)) - return new List(); - - string search = query.Search.ToLower(); - - var queriedFolderLinks = folderLinks.Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)); - - return queriedFolderLinks.Select(item => - new ResultManager(context) - .CreateFolderResult(item.Nickname, item.Path, item.Path, query)) - .ToList(); - } - - internal List FolderListAll(Query query, List folderLinks, PluginInitContext context) - => folderLinks - .Select(item => - new ResultManager(context).CreateFolderResult(item.Nickname, item.Path, item.Path, query)) - .ToList(); - } -} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs similarity index 80% rename from Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs rename to Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs index 43ecdad97..f623cc2ca 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/AccessLink.cs @@ -1,14 +1,15 @@ using System; using System.Linq; -using System.Text.Json; using System.Text.Json.Serialization; -namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks +namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks { - public class FolderLink + public class AccessLink { public string Path { get; set; } + public ResultType Type { get; set; } = ResultType.Folder; + [JsonIgnore] public string Nickname { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs new file mode 100644 index 000000000..d71e9ab49 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks +{ + internal static class QuickAccess + { + internal static List AccessLinkListMatched(Query query, List accessLinks) + { + if (string.IsNullOrEmpty(query.Search)) + return new List(); + + string search = query.Search.ToLower(); + + var queriedAccessLinks = + accessLinks + .Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)) + .OrderBy(x => x.Type) + .ThenBy(x => x.Nickname); + + return queriedAccessLinks.Select(l => l.Type switch + { + ResultType.Folder => ResultManager.CreateFolderResult(l.Nickname, l.Path, l.Path, query), + ResultType.File => ResultManager.CreateFileResult(l.Path, query), + _ => throw new ArgumentOutOfRangeException() + + }).ToList(); + } + + internal static List AccessLinkListAll(Query query, List accessLinks) + => accessLinks + .OrderBy(x => x.Type) + .ThenBy(x => x.Nickname) + .Select(l => l.Type switch + { + ResultType.Folder => ResultManager.CreateFolderResult(l.Nickname, l.Path, l.Path, query), + ResultType.File => ResultManager.CreateFileResult(l.Path, query), + _ => throw new ArgumentOutOfRangeException() + + }).ToList(); + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index 6a336c59a..600b573cf 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -4,19 +4,21 @@ using System; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Windows; namespace Flow.Launcher.Plugin.Explorer.Search { - public class ResultManager + public static class ResultManager { - private readonly PluginInitContext context; + private static PluginInitContext Context; - public ResultManager(PluginInitContext context) + public static void Init(PluginInitContext context) { - this.context = context; + Context = context; } - internal Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false) + + internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false) { return new Result { @@ -41,7 +43,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator; - context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ? + Context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ? changeTo : query.ActionKeyword + " " + changeTo); return false; @@ -52,7 +54,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search }; } - internal Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false) + internal static Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false) { var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); @@ -94,7 +96,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search }; } - internal Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false) + internal static Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false) { var result = new Result { @@ -140,7 +142,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search public bool ShowIndexState { get; set; } } - internal enum ResultType + public enum ResultType { Volume, Folder, diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 6b3a96912..2af09bf2c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -1,5 +1,5 @@ using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; using Flow.Launcher.Plugin.SharedCommands; using System; @@ -14,19 +14,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search { private readonly PluginInitContext context; - private readonly IndexSearch indexSearch; - - private readonly QuickFolderAccess quickFolderAccess = new QuickFolderAccess(); - - private readonly ResultManager resultManager; - private readonly Settings settings; public SearchManager(Settings settings, PluginInitContext context) { this.context = context; - indexSearch = new IndexSearch(context); - resultManager = new ResultManager(context); this.settings = settings; } @@ -40,15 +32,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false); // This allows the user to type the assigned action keyword and only see the list of quick folder links - if (settings.QuickFolderAccessLinks.Count > 0 - && query.ActionKeyword == settings.SearchActionKeyword - && string.IsNullOrEmpty(query.Search)) - return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks, context); + if (string.IsNullOrEmpty(query.Search)) + return QuickAccess.AccessLinkListAll(query, settings.QuickAccessLinks); - var quickFolderLinks = quickFolderAccess.FolderListMatched(query, settings.QuickFolderAccessLinks, context); + var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, settings.QuickAccessLinks); - if (quickFolderLinks.Count > 0) - results.AddRange(quickFolderLinks); + if (quickaccessLinks.Count > 0) + results.AddRange(quickaccessLinks); var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch); @@ -58,7 +48,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search // Query is a location path with a full environment variable, eg. %appdata%\somefolder\ var isEnvironmentVariablePath = querySearch[1..].Contains("%\\"); - if (!FilesFolders.IsLocationPathString(querySearch) && !isEnvironmentVariablePath) + if (!querySearch.IsLocationPathString() && !isEnvironmentVariablePath) { results.AddRange(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token).ConfigureAwait(false)); @@ -70,22 +60,26 @@ namespace Flow.Launcher.Plugin.Explorer.Search if (isEnvironmentVariablePath) locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath); + // Check that actual location exists, otherwise directory search will throw directory not found exception if (!FilesFolders.LocationExists(FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath))) return results; var useIndexSearch = UseWindowsIndexForDirectorySearch(locationPath); - results.Add(resultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch)); + results.Add(ResultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch)); - if (token.IsCancellationRequested) - return null; + token.ThrowIfCancellationRequested(); - results.AddRange(await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync, - DirectoryInfoClassSearch, - useIndexSearch, - query, - locationPath, - token).ConfigureAwait(false)); + var directoryResult = await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync, + DirectoryInfoClassSearch, + useIndexSearch, + query, + locationPath, + token).ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); + + results.AddRange(directoryResult); return results; } @@ -97,7 +91,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search if (string.IsNullOrEmpty(querySearchString)) return new List(); - return await indexSearch.WindowsIndexSearchAsync(querySearchString, + return await IndexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForFileContentSearch, query, @@ -109,23 +103,21 @@ namespace Flow.Launcher.Plugin.Explorer.Search return actionKeyword == settings.FileContentSearchActionKeyword; } - private List DirectoryInfoClassSearch(Query query, string querySearch) + private List DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token) { - var directoryInfoSearch = new DirectoryInfoSearch(context); - - return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch); + return DirectoryInfoSearch.TopLevelDirectorySearch(query, querySearch, token); } public async Task> TopLevelDirectorySearchBehaviourAsync( Func>> windowsIndexSearch, - Func> directoryInfoClassSearch, + Func> directoryInfoClassSearch, bool useIndexSearch, Query query, string querySearchString, CancellationToken token) { if (!useIndexSearch) - return directoryInfoClassSearch(query, querySearchString); + return directoryInfoClassSearch(query, querySearchString, token); return await windowsIndexSearch(query, querySearchString, token); } @@ -134,7 +126,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { var queryConstructor = new QueryConstructor(settings); - return await indexSearch.WindowsIndexSearchAsync(querySearchString, + return await IndexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForAllFilesAndFolders, query, @@ -145,7 +137,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { var queryConstructor = new QueryConstructor(settings); - return await indexSearch.WindowsIndexSearchAsync(path, + return await IndexSearch.WindowsIndexSearchAsync(path, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForTopLevelDirectorySearch, query, @@ -164,7 +156,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))) return false; - return indexSearch.PathIsIndexed(pathToDirectory); + return IndexSearch.PathIsIndexed(pathToDirectory); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs index 5b1d47ef8..b1e1d7622 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Logger; using Microsoft.Search.Interop; using System; using System.Collections.Generic; @@ -10,23 +10,16 @@ using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { - internal class IndexSearch + internal static class IndexSearch { - private readonly ResultManager resultManager; // Reserved keywords in oleDB - private readonly string reservedStringPattern = @"^[\/\\\$\%_]+$"; + private const string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$"; - internal IndexSearch(PluginInitContext context) + internal async static Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token) { - resultManager = new ResultManager(context); - } - - internal async Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token) - { - var folderResults = new List(); - var fileResults = new List(); var results = new List(); + var fileResults = new List(); try { @@ -55,7 +48,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex if (dataReaderResults.GetString(2) == "Directory") { - folderResults.Add(resultManager.CreateFolderResult( + results.Add(ResultManager.CreateFolderResult( dataReaderResults.GetString(0), path, path, @@ -63,7 +56,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex } else { - fileResults.Add(resultManager.CreateFileResult(path, query, true, true)); + fileResults.Add(ResultManager.CreateFileResult(path, query, true, true)); } } } @@ -83,11 +76,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex LogException("General error from performing index search", e); } + results.AddRange(fileResults); + // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. - return results.Concat(folderResults.OrderBy(x => x.Title)).Concat(fileResults.OrderBy(x => x.Title)).ToList(); ; + return results; } - internal async Task> WindowsIndexSearchAsync(string searchString, string connectionString, + internal async static Task> WindowsIndexSearchAsync(string searchString, string connectionString, Func constructQuery, Query query, CancellationToken token) { @@ -101,14 +96,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex } - internal bool PathIsIndexed(string path) + internal static bool PathIsIndexed(string path) { var csm = new CSearchManager(); var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager(); return indexManager.IncludedInCrawlScope(path) > 0; } - private void LogException(string message, Exception e) + private static void LogException(string message, Exception e) { #if DEBUG // Please investigate and handle error from index search throw e; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs index 5718fdb0a..20e85bbb5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs @@ -42,7 +42,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex // Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer var queryHelper = catalogManager.GetQueryHelper(); - + return queryHelper; } @@ -81,11 +81,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex var previousLevelDirectory = path.Substring(0, indexOfSeparator); if (string.IsNullOrEmpty(itemName)) - return searchDepth + $"{previousLevelDirectory}'"; + return $"{searchDepth}{previousLevelDirectory}'"; - return $"(System.FileName LIKE '{itemName}%' " + - $"OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND " + - searchDepth + $"{previousLevelDirectory}'"; + return $"(System.FileName LIKE '{itemName}%' OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND {searchDepth}{previousLevelDirectory}'"; } /// @@ -96,9 +94,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE "; if (path.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > path.LastIndexOf(Constants.DirectorySeperator)) - return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path); + return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path) + QueryOrderByFileNameRestriction; - return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path); + return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path) + QueryOrderByFileNameRestriction; } /// @@ -107,16 +105,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex public string QueryForAllFilesAndFolders(string userSearchString) { // Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause - return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch + + QueryOrderByFileNameRestriction; } /// /// Set the required WHERE clause restriction to search for all files and folders. /// - public string QueryWhereRestrictionsForAllFilesAndFoldersSearch() - { - return $"scope='file:'"; - } + public const string QueryWhereRestrictionsForAllFilesAndFoldersSearch = "scope='file:'"; + + public const string QueryOrderByFileNameRestriction = " ORDER BY System.FileName"; + /// /// Search will be performed on all indexed file contents for the specified search keywords. @@ -125,7 +124,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE "; - return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch + + QueryOrderByFileNameRestriction; } /// diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index e62ea93fc..a8eac986d 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -1,7 +1,6 @@ using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Collections.Generic; -using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin.Explorer { @@ -9,11 +8,14 @@ namespace Flow.Launcher.Plugin.Explorer { public int MaxResult { get; set; } = 100; - public List QuickFolderAccessLinks { get; set; } = new List(); + public List QuickAccessLinks { get; set; } = new List(); + + // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards. + public List QuickFolderAccessLinks { get; set; } = new List(); public bool UseWindowsIndexForDirectorySearch { get; set; } = true; - public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List(); + public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List(); public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 21bc49741..791c06b66 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -1,7 +1,7 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Diagnostics; using System.Threading.Tasks; @@ -32,9 +32,9 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels storage.Save(); } - internal void RemoveFolderLinkFromQuickFolders(FolderLink selectedRow) => Settings.QuickFolderAccessLinks.Remove(selectedRow); + internal void RemoveLinkFromQuickAccess(AccessLink selectedRow) => Settings.QuickAccessLinks.Remove(selectedRow); - internal void RemoveFolderLinkFromExcludedIndexPaths(FolderLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow); + internal void RemoveAccessLinkFromExcludedIndexPaths(AccessLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow); internal void OpenWindowsIndexingOptions() { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml index 9d6f4976e..13d46394c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml @@ -7,7 +7,7 @@ mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> - + @@ -40,22 +40,22 @@ - + x:Name="lbxAccessLinks" AllowDrop="True" + Drop="lbxAccessLinks_Drop" + DragEnter="lbxAccessLinks_DragEnter" + ItemTemplate="{StaticResource ListViewTemplateAccessLinks}"/> diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs index 3b67b408d..5d2980c55 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.ViewModels; using System; using System.Collections.Generic; @@ -29,7 +29,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views this.viewModel = viewModel; - lbxFolderLinks.ItemsSource = this.viewModel.Settings.QuickFolderAccessLinks; + lbxAccessLinks.ItemsSource = this.viewModel.Settings.QuickAccessLinks; lbxExcludedPaths.ItemsSource = this.viewModel.Settings.IndexSearchExcludedSubdirectoryPaths; @@ -54,7 +54,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views public void RefreshView() { - lbxFolderLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending)); + lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending)); lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending)); @@ -62,7 +62,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views btnEdit.Visibility = Visibility.Hidden; btnAdd.Visibility = Visibility.Hidden; - if (expFolderLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded) + if (expAccessLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded) { if (!expActionKeywords.IsExpanded) btnAdd.Visibility = Visibility.Visible; @@ -71,7 +71,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views && btnEdit.Visibility == Visibility.Hidden) btnEdit.Visibility = Visibility.Visible; - if ((lbxFolderLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0) + if ((lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0) && btnDelete.Visibility == Visibility.Visible && btnEdit.Visibility == Visibility.Visible) { @@ -79,8 +79,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views btnEdit.Visibility = Visibility.Hidden; } - if (expFolderLinks.IsExpanded - && lbxFolderLinks.Items.Count > 0 + if (expAccessLinks.IsExpanded + && lbxAccessLinks.Items.Count > 0 && btnDelete.Visibility == Visibility.Hidden && btnEdit.Visibility == Visibility.Hidden) { @@ -98,7 +98,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views } } - lbxFolderLinks.Items.Refresh(); + lbxAccessLinks.Items.Refresh(); lbxExcludedPaths.Items.Refresh(); @@ -113,8 +113,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views if (expExcludedPaths.IsExpanded) expExcludedPaths.IsExpanded = false; - if (expFolderLinks.IsExpanded) - expFolderLinks.IsExpanded = false; + if (expAccessLinks.IsExpanded) + expAccessLinks.IsExpanded = false; RefreshView(); } @@ -125,10 +125,10 @@ namespace Flow.Launcher.Plugin.Explorer.Views expActionKeywords.Height = Double.NaN; } - private void expFolderLinks_Click(object sender, RoutedEventArgs e) + private void expAccessLinks_Click(object sender, RoutedEventArgs e) { - if (expFolderLinks.IsExpanded) - expFolderLinks.Height = 215; + if (expAccessLinks.IsExpanded) + expAccessLinks.Height = 215; if (expExcludedPaths.IsExpanded) expExcludedPaths.IsExpanded = false; @@ -139,19 +139,19 @@ namespace Flow.Launcher.Plugin.Explorer.Views RefreshView(); } - private void expFolderLinks_Collapsed(object sender, RoutedEventArgs e) + private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e) { - if (!expFolderLinks.IsExpanded) - expFolderLinks.Height = Double.NaN; + if (!expAccessLinks.IsExpanded) + expAccessLinks.Height = Double.NaN; } private void expExcludedPaths_Click(object sender, RoutedEventArgs e) { if (expExcludedPaths.IsExpanded) - expFolderLinks.Height = Double.NaN; + expAccessLinks.Height = Double.NaN; - if (expFolderLinks.IsExpanded) - expFolderLinks.IsExpanded = false; + if (expAccessLinks.IsExpanded) + expAccessLinks.IsExpanded = false; if (expActionKeywords.IsExpanded) expActionKeywords.IsExpanded = false; @@ -161,7 +161,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views private void btnDelete_Click(object sender, RoutedEventArgs e) { - var selectedRow = lbxFolderLinks.SelectedItem as FolderLink?? lbxExcludedPaths.SelectedItem as FolderLink; + var selectedRow = lbxAccessLinks.SelectedItem as AccessLink?? lbxExcludedPaths.SelectedItem as AccessLink; if (selectedRow != null) { @@ -169,11 +169,11 @@ namespace Flow.Launcher.Plugin.Explorer.Views if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) { - if (expFolderLinks.IsExpanded) - viewModel.RemoveFolderLinkFromQuickFolders(selectedRow); + if (expAccessLinks.IsExpanded) + viewModel.RemoveLinkFromQuickAccess(selectedRow); if (expExcludedPaths.IsExpanded) - viewModel.RemoveFolderLinkFromExcludedIndexPaths(selectedRow); + viewModel.RemoveAccessLinkFromExcludedIndexPaths(selectedRow); RefreshView(); } @@ -199,7 +199,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views } else { - var selectedRow = lbxFolderLinks.SelectedItem as FolderLink ?? lbxExcludedPaths.SelectedItem as FolderLink; + var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink; if (selectedRow != null) { @@ -207,9 +207,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views folderBrowserDialog.SelectedPath = selectedRow.Path; if (folderBrowserDialog.ShowDialog() == DialogResult.OK) { - if (expFolderLinks.IsExpanded) + if (expAccessLinks.IsExpanded) { - var link = viewModel.Settings.QuickFolderAccessLinks.First(x => x.Path == selectedRow.Path); + var link = viewModel.Settings.QuickAccessLinks.First(x => x.Path == selectedRow.Path); link.Path = folderBrowserDialog.SelectedPath; } @@ -235,36 +235,36 @@ namespace Flow.Launcher.Plugin.Explorer.Views var folderBrowserDialog = new FolderBrowserDialog(); if (folderBrowserDialog.ShowDialog() == DialogResult.OK) { - var newFolderLink = new FolderLink + var newAccessLink = new AccessLink { Path = folderBrowserDialog.SelectedPath }; - AddFolderLink(newFolderLink); + AddAccessLink(newAccessLink); } RefreshView(); } - private void lbxFolders_Drop(object sender, DragEventArgs e) + private void lbxAccessLinks_Drop(object sender, DragEventArgs e) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); if (files != null && files.Count() > 0) { - if (expFolderLinks.IsExpanded && viewModel.Settings.QuickFolderAccessLinks == null) - viewModel.Settings.QuickFolderAccessLinks = new List(); + if (expAccessLinks.IsExpanded && viewModel.Settings.QuickAccessLinks == null) + viewModel.Settings.QuickAccessLinks = new List(); foreach (string s in files) { if (Directory.Exists(s)) { - var newFolderLink = new FolderLink + var newFolderLink = new AccessLink { Path = s }; - AddFolderLink(newFolderLink); + AddAccessLink(newFolderLink); } RefreshView(); @@ -272,28 +272,28 @@ namespace Flow.Launcher.Plugin.Explorer.Views } } - private void AddFolderLink(FolderLink newFolderLink) + private void AddAccessLink(AccessLink newAccessLink) { - if (expFolderLinks.IsExpanded - && !viewModel.Settings.QuickFolderAccessLinks.Any(x => x.Path == newFolderLink.Path)) + if (expAccessLinks.IsExpanded + && !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path)) { - if (viewModel.Settings.QuickFolderAccessLinks == null) - viewModel.Settings.QuickFolderAccessLinks = new List(); + if (viewModel.Settings.QuickAccessLinks == null) + viewModel.Settings.QuickAccessLinks = new List(); - viewModel.Settings.QuickFolderAccessLinks.Add(newFolderLink); + viewModel.Settings.QuickAccessLinks.Add(newAccessLink); } if (expExcludedPaths.IsExpanded - && !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newFolderLink.Path)) + && !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newAccessLink.Path)) { if (viewModel.Settings.IndexSearchExcludedSubdirectoryPaths == null) - viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List(); + viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List(); - viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newFolderLink); + viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newAccessLink); } } - private void lbxFolders_DragEnter(object sender, DragEventArgs e) + private void lbxAccessLinks_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index 68ab9f562..9aa54fb83 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -7,7 +7,7 @@ "Name": "Explorer", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Author": "Jeremy Wu", - "Version": "1.3.0", + "Version": "1.7.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj index 2e352d832..cb2507a2b 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj @@ -1,5 +1,4 @@  - Library netcoreapp3.1 diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs index 3ea78156d..5db26aa70 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs @@ -18,6 +18,7 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; using IStream = AppxPackaing.IStream; using Rect = System.Windows.Rect; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.Program.Programs { @@ -206,12 +207,11 @@ namespace Flow.Launcher.Plugin.Program.Programs } catch (Exception e) { - ProgramLogger.LogException("UWP" ,"CurrentUserPackages", $"id","An unexpected error occured and " + ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and " + $"unable to verify if package is valid", e); return false; } - - + return valid; }); return ps; @@ -263,21 +263,40 @@ namespace Flow.Launcher.Plugin.Program.Programs public string LogoPath { get; set; } public UWP Package { get; set; } - public Application(){} + public Application() { } public Result Result(string query, IPublicAPI api) { - var title = (Name, Description) switch - { - (var n, null) => n, - (var n, var d) when d.StartsWith(n) => d, - (var n, var d) when n.StartsWith(d) => n, - (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}", - _ => Name - }; + string title; + MatchResult matchResult; - var matchResult = StringMatcher.FuzzySearch(query, title); + // We suppose Name won't be null + if (Description == null || Name.StartsWith(Description)) + { + title = Name; + matchResult = StringMatcher.FuzzySearch(query, title); + } + else if (Description.StartsWith(Name)) + { + title = Description; + matchResult = StringMatcher.FuzzySearch(query, Description); + } + else + { + title = $"{Name}: {Description}"; + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var desciptionMatch = StringMatcher.FuzzySearch(query, Description); + if (desciptionMatch.Score > nameMatch.Score) + { + for (int i = 0; i < desciptionMatch.MatchData.Count; i++) + { + desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + } + matchResult = desciptionMatch; + } + else matchResult = nameMatch; + } if (!matchResult.Success) return null; @@ -311,7 +330,7 @@ namespace Flow.Launcher.Plugin.Program.Programs Action = _ => { - Main.StartProcess(Process.Start, + Main.StartProcess(Process.Start, new ProcessStartInfo( !string.IsNullOrEmpty(Main._settings.CustomizedExplorer) ? Main._settings.CustomizedExplorer @@ -403,14 +422,14 @@ namespace Flow.Launcher.Plugin.Program.Programs public string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue) { const string prefix = "ms-resource:"; - + if (string.IsNullOrWhiteSpace(rawPriReferenceValue) || !rawPriReferenceValue.StartsWith(prefix)) return rawPriReferenceValue; string key = rawPriReferenceValue.Substring(prefix.Length); if (key.StartsWith("//")) return $"{prefix}{key}"; - + if (!key.StartsWith("/")) { key = $"/{key}"; diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index 77278330a..fd994aeb3 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -12,6 +12,7 @@ using Shell; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.Program.Programs { @@ -36,19 +37,38 @@ namespace Flow.Launcher.Plugin.Program.Programs public Result Result(string query, IPublicAPI api) { - var title = (Name, Description) switch + string title; + MatchResult matchResult; + + // We suppose Name won't be null + if (Description == null || Name.StartsWith(Description)) { - (var n, null) => n, - (var n, var d) when d.StartsWith(n) => d, - (var n, var d) when n.StartsWith(d) => n, - (var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}", - _ => Name - }; + title = Name; + matchResult = StringMatcher.FuzzySearch(query, title); + } + else if (Description.StartsWith(Name)) + { + title = Description; + matchResult = StringMatcher.FuzzySearch(query, Description); + } + else + { + title = $"{Name}: {Description}"; + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var desciptionMatch = StringMatcher.FuzzySearch(query, Description); + if (desciptionMatch.Score > nameMatch.Score) + { + for (int i = 0; i < desciptionMatch.MatchData.Count; i++) + { + desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + } + matchResult = desciptionMatch; + } + else matchResult = nameMatch; + } - var matchResult = StringMatcher.FuzzySearch(query, title); + if (!matchResult.Success) return null; - if (!matchResult.Success) - return null; var result = new Result { @@ -58,7 +78,7 @@ namespace Flow.Launcher.Plugin.Program.Programs Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, - Action = e => + Action = _ => { var info = new ProcessStartInfo { @@ -268,10 +288,10 @@ namespace Flow.Launcher.Plugin.Program.Programs try { var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions - { - IgnoreInaccessible = true, - RecurseSubdirectories = true - }) + { + IgnoreInaccessible = true, + RecurseSubdirectories = true + }) .Where(x => suffixes.Contains(Extension(x))); return paths; diff --git a/Plugins/Flow.Launcher.Plugin.Program/plugin.json b/Plugins/Flow.Launcher.Plugin.Program/plugin.json index 1dedd9909..f713a33ec 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Program/plugin.json @@ -4,7 +4,7 @@ "Name": "Program", "Description": "Search programs in Flow.Launcher", "Author": "qianlifeng", - "Version": "1.3.0", + "Version": "1.4.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Program.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj index c542dc89b..d3042722b 100644 --- a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj +++ b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj @@ -54,10 +54,10 @@ PreserveNewest - - MSBuild:Compile - Designer - + + MSBuild:Compile + Designer + diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj index 8ef4dc0fb..c25e759d3 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj +++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj @@ -48,10 +48,10 @@ PreserveNewest - - MSBuild:Compile - Designer - + + MSBuild:Compile + Designer + diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 624fe05bc..0aa37cdf5 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Threading.Tasks; using System.Windows; using System.Windows.Forms; using System.Windows.Interop; diff --git a/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml index 452be00ee..eff1ac263 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Url/Languages/en.xaml @@ -2,6 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> + Open search in: + New Window + New Tab + Open url:{0} Can't open url:{0} diff --git a/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml index 69640735e..97568be5a 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.Url/Languages/sk.xaml @@ -2,6 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> + Otvoriť vyhľadávanie v: + Nové okno + Nová karta + Otvoriť url:{0} Adresa URL sa nedá otvoriť: {0} diff --git a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml index f54aea878..9219a0009 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml @@ -10,11 +10,11 @@ - - -