diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs
index 22780acfc3..c8804528c7 100644
--- a/osu.Game/Screens/Select/FilterCriteria.cs
+++ b/osu.Game/Screens/Select/FilterCriteria.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
@@ -62,7 +63,7 @@ namespace osu.Game.Screens.Select
string remainingText = value;
// First handle quoted segments to ensure we keep inline spaces in exact matches.
- foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\")"))
+ foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)"))
{
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
@@ -138,7 +139,7 @@ namespace osu.Game.Screens.Select
{
public bool HasFilter => !string.IsNullOrEmpty(SearchTerm);
- public bool Exact { get; private set; }
+ public MatchMode MatchMode { get; private set; }
public bool Matches(string value)
{
@@ -149,10 +150,18 @@ namespace osu.Game.Screens.Select
if (string.IsNullOrEmpty(value))
return false;
- if (Exact)
- return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+ switch (MatchMode)
+ {
+ default:
+ case MatchMode.None:
+ return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
- return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
+ case MatchMode.IsolatedPhrase:
+ return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+
+ case MatchMode.FullPhrase:
+ return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0;
+ }
}
private string searchTerm;
@@ -162,12 +171,42 @@ namespace osu.Game.Screens.Select
get => searchTerm;
set
{
- searchTerm = value.Trim('"');
- Exact = searchTerm != value;
+ searchTerm = value;
+
+ if (searchTerm.EndsWith("\"!", StringComparison.Ordinal))
+ {
+ searchTerm = searchTerm.Trim('!', '\"');
+ MatchMode = MatchMode.FullPhrase;
+ }
+ else if (searchTerm.StartsWith('\"'))
+ {
+ searchTerm = searchTerm.Trim('\"');
+ MatchMode = MatchMode.IsolatedPhrase;
+ }
+ else
+ MatchMode = MatchMode.None;
}
}
public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm;
}
+
+ public enum MatchMode
+ {
+ ///
+ /// Match using a simple "contains" substring match.
+ ///
+ None,
+
+ ///
+ /// Match for the search phrase being isolated by spaces, or at the start or end of the text.
+ ///
+ IsolatedPhrase,
+
+ ///
+ /// Match for the search phrase matching the full text in completion.
+ ///
+ FullPhrase,
+ }
}
}
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index 474a9fdfea..4bc4448291 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select
public static class FilterQueryParser
{
private static readonly Regex query_syntax_regex = new Regex(
- @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*"")|(\S*))",
+ @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static void ApplyQueries(FilterCriteria criteria, string query)