CommandSearchControlHelper.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. using OneOf;
  2. using OneOf.Types;
  3. using PixiEditor.Helpers;
  4. using PixiEditor.Models.Commands;
  5. using PixiEditor.Models.Commands.Search;
  6. using PixiEditor.ViewModels;
  7. using SkiaSharp;
  8. using System.IO;
  9. using System.Text.RegularExpressions;
  10. namespace PixiEditor.Views.UserControls.CommandSearch;
  11. #nullable enable
  12. internal static class CommandSearchControlHelper
  13. {
  14. public static (List<SearchResult> results, List<string> warnings) ConstructSearchResults(string query)
  15. {
  16. // avoid xaml designer error
  17. if (ViewModelMain.Current is null)
  18. return (new(), new());
  19. List<SearchResult> newResults = new();
  20. List<string> warnings = new();
  21. if (string.IsNullOrWhiteSpace(query))
  22. {
  23. // show all recently opened
  24. newResults.AddRange(ViewModelMain.Current.FileSubViewModel.RecentlyOpened
  25. .Select(file => (SearchResult)new FileSearchResult(file.FilePath)
  26. {
  27. SearchTerm = query
  28. }));
  29. return (newResults, warnings);
  30. }
  31. var controller = CommandController.Current;
  32. // add matching colors
  33. MaybeParseColor(query).Switch(
  34. (SKColor color) =>
  35. {
  36. newResults.Add(new ColorSearchResult(color)
  37. {
  38. SearchTerm = query
  39. });
  40. },
  41. (Error _) => warnings.Add("Invalid color"),
  42. static (None _) => { }
  43. );
  44. // add matching commands
  45. newResults.AddRange(
  46. controller.Commands
  47. .Where(x => x.Description.Contains(query, StringComparison.OrdinalIgnoreCase))
  48. .Where(static x => ViewModelMain.Current.DebugSubViewModel.UseDebug ? true : !x.IsDebug)
  49. .OrderByDescending(x => x.Description.Contains($" {query} ", StringComparison.OrdinalIgnoreCase))
  50. .Take(18)
  51. .Select(command => new CommandSearchResult(command)
  52. {
  53. SearchTerm = query,
  54. Match = Match(command.Description, query)
  55. }));
  56. try
  57. {
  58. // add matching files
  59. newResults.AddRange(MaybeParseFilePaths(query));
  60. }
  61. catch
  62. {
  63. // ignored
  64. }
  65. // add matching recent files
  66. newResults.AddRange(
  67. ViewModelMain.Current.FileSubViewModel.RecentlyOpened
  68. .Where(x => x.FilePath.Contains(query))
  69. .Select(file => new FileSearchResult(file.FilePath)
  70. {
  71. SearchTerm = query,
  72. Match = Match(file.FilePath, query)
  73. }));
  74. return (newResults, warnings);
  75. }
  76. private static Match Match(string text, string searchTerm) =>
  77. Regex.Match(text, $"(.*)({Regex.Escape(searchTerm ?? string.Empty)})(.*)", RegexOptions.IgnoreCase);
  78. private static IEnumerable<SearchResult> MaybeParseFilePaths(string query)
  79. {
  80. var filePath = query.Trim(' ', '"', '\'');
  81. if (filePath.StartsWith("~"))
  82. filePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), filePath[1..]);
  83. if (!Path.IsPathFullyQualified(filePath))
  84. return Enumerable.Empty<SearchResult>();
  85. GetDirectory(filePath, out var directory, out var name);
  86. var files = Directory.EnumerateFiles(directory)
  87. .Where(x => SupportedFilesHelper.IsExtensionSupported(Path.GetExtension(x)));
  88. if (name is not (null or ""))
  89. {
  90. files = files.Where(x => x.Contains(name, StringComparison.OrdinalIgnoreCase));
  91. }
  92. return files
  93. .Select(static file => Path.GetFullPath(file))
  94. .Select(path => new FileSearchResult(path)
  95. {
  96. SearchTerm = name,
  97. Match = Match($".../{Path.GetFileName(path)}", name ?? "")
  98. });
  99. }
  100. private static bool GetDirectory(string path, out string directory, out string file)
  101. {
  102. if (Directory.Exists(path))
  103. {
  104. directory = path;
  105. file = string.Empty;
  106. return true;
  107. }
  108. directory = Path.GetDirectoryName(path) ?? @"C:\";
  109. file = Path.GetFileName(path);
  110. return Directory.Exists(directory);
  111. }
  112. public static OneOf<SKColor, Error, None> MaybeParseColor(string query)
  113. {
  114. if (query.StartsWith('#'))
  115. {
  116. if (!SKColor.TryParse(query, out var color))
  117. return new Error();
  118. return color;
  119. }
  120. else if (query.StartsWith("rgb") || query.StartsWith("rgba"))
  121. {
  122. // matches strings that:
  123. // - start with "rgb" or "rgba"
  124. // - have a list of 3 or 4 numbers each up to 255 (rgb with 4 numbers is still allowed)
  125. // - can have parenteses around the list
  126. // - can have spaces in any reasonable places
  127. Match match = Regex.Match(query, @"^rgba? *(?(?=\()\((?=.+\))|(?!.+\))) *(?<r>(?:1?\d{1,2})|(?:2[1-4]\d)|(?:25[1-5])) *, *(?<g>(?:1?\d{1,2})|(?:2[1-4]\d)|(?:25[1-5])) *, *(?<b>(?:1?\d{1,2})|(?:2[1-4]\d)|(?:25[1-5])) *(?:, *(?<a>(?:1?\d{1,2})|(?:2[1-4]\d)|(?:25[1-5])))?\)?$");
  128. if (match.Success)
  129. {
  130. var maybeColor = ParseRGB(match);
  131. return maybeColor is null ? new Error() : maybeColor.Value;
  132. }
  133. else if (query.StartsWith("rgb(") || query.StartsWith("rgba("))
  134. {
  135. return new Error();
  136. }
  137. }
  138. return new None();
  139. }
  140. private static SKColor? ParseRGB(Match match)
  141. {
  142. bool invalid = !(
  143. byte.TryParse(match.Groups["r"].ValueSpan, out var r) &
  144. byte.TryParse(match.Groups["g"].ValueSpan, out var g) &
  145. byte.TryParse(match.Groups["b"].ValueSpan, out var b)
  146. );
  147. if (invalid)
  148. return null;
  149. var aText = match.Groups["a"].Value;
  150. byte a = 255;
  151. if (!string.IsNullOrEmpty(aText) && !byte.TryParse(aText, out a))
  152. return null;
  153. return new SKColor(r, g, b, a);
  154. }
  155. }