mirror of
https://github.com/ppy/osu.git
synced 2025-01-22 17:12:54 +08:00
Merge pull request #24058 from peppy/full-term-exact-match
Add support for matching full terms at song select using suffixed `!`
This commit is contained in:
commit
1d4380cfd0
@ -169,6 +169,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
[TestCase("\"tags to\"", true)]
|
[TestCase("\"tags to\"", true)]
|
||||||
[TestCase("\"version\"", false)]
|
[TestCase("\"version\"", false)]
|
||||||
[TestCase("\"an auteur\"", true)]
|
[TestCase("\"an auteur\"", true)]
|
||||||
|
[TestCase("\"Artist\"!", true)]
|
||||||
|
[TestCase("\"The Artist\"!", false)]
|
||||||
|
[TestCase("\"the artist\"!", false)]
|
||||||
[TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex.
|
[TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex.
|
||||||
public void TestCriteriaMatchingExactTerms(string terms, bool filtered)
|
public void TestCriteriaMatchingExactTerms(string terms, bool filtered)
|
||||||
{
|
{
|
||||||
@ -234,6 +237,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
[TestCase("the artist AND then something else", true)]
|
[TestCase("the artist AND then something else", true)]
|
||||||
[TestCase("unicode too", false)]
|
[TestCase("unicode too", false)]
|
||||||
[TestCase("unknown", true)]
|
[TestCase("unknown", true)]
|
||||||
|
[TestCase("\"Artist\"!", true)]
|
||||||
|
[TestCase("\"The Artist\"!", false)]
|
||||||
|
[TestCase("\"the artist\"!", false)]
|
||||||
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
|
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
|
||||||
{
|
{
|
||||||
var exampleBeatmapInfo = getExampleBeatmap();
|
var exampleBeatmapInfo = getExampleBeatmap();
|
||||||
|
@ -26,26 +26,58 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestApplyQueriesBareWordsWithExactMatch()
|
public void TestApplyQueriesBareWordsWithExactMatch()
|
||||||
{
|
{
|
||||||
const string query = "looking for \"a beatmap\" like \"this\"";
|
const string query = "looking for \"a beatmap\"! like \"this\"";
|
||||||
var filterCriteria = new FilterCriteria();
|
var filterCriteria = new FilterCriteria();
|
||||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
Assert.AreEqual("looking for \"a beatmap\" like \"this\"", filterCriteria.SearchText);
|
Assert.AreEqual("looking for \"a beatmap\"! like \"this\"", filterCriteria.SearchText);
|
||||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||||
|
|
||||||
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap"));
|
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap"));
|
||||||
Assert.That(filterCriteria.SearchTerms[0].Exact, Is.True);
|
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
|
||||||
|
|
||||||
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this"));
|
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this"));
|
||||||
Assert.That(filterCriteria.SearchTerms[1].Exact, Is.True);
|
Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
|
|
||||||
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking"));
|
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking"));
|
||||||
Assert.That(filterCriteria.SearchTerms[2].Exact, Is.False);
|
Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
|
||||||
Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for"));
|
Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for"));
|
||||||
Assert.That(filterCriteria.SearchTerms[3].Exact, Is.False);
|
Assert.That(filterCriteria.SearchTerms[3].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
|
||||||
Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like"));
|
Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like"));
|
||||||
Assert.That(filterCriteria.SearchTerms[4].Exact, Is.False);
|
Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyFullPhraseQueryWithExclamationPointInTerm()
|
||||||
|
{
|
||||||
|
const string query = "looking for \"circles!\"!";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual("looking for \"circles!\"!", filterCriteria.SearchText);
|
||||||
|
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("circles!"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("looking"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("for"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyBrokenFullPhraseQuery()
|
||||||
|
{
|
||||||
|
const string query = "\"!";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual("\"!", filterCriteria.SearchText);
|
||||||
|
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||||
|
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("!"));
|
||||||
|
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -260,7 +292,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("find me songs with please", filterCriteria.SearchText.Trim());
|
Assert.AreEqual("find me songs with please", filterCriteria.SearchText.Trim());
|
||||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||||
Assert.AreEqual("a certain title", filterCriteria.Title.SearchTerm);
|
Assert.AreEqual("a certain title", filterCriteria.Title.SearchTerm);
|
||||||
Assert.That(filterCriteria.Title.Exact, Is.True);
|
Assert.That(filterCriteria.Title.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -272,7 +304,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
|
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
|
||||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||||
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
|
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
|
||||||
Assert.That(filterCriteria.Artist.Exact, Is.False);
|
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -284,7 +316,19 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
|
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
|
||||||
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||||
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
|
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
|
||||||
Assert.That(filterCriteria.Artist.Exact, Is.True);
|
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestApplyArtistQueriesWithSpacesFullPhrase()
|
||||||
|
{
|
||||||
|
const string query = "artist=\"The Only One\"!";
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.That(filterCriteria.SearchText.Trim(), Is.Empty);
|
||||||
|
Assert.AreEqual(0, filterCriteria.SearchTerms.Length);
|
||||||
|
Assert.AreEqual("The Only One", filterCriteria.Artist.SearchTerm);
|
||||||
|
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -63,7 +64,7 @@ namespace osu.Game.Screens.Select
|
|||||||
string remainingText = value;
|
string remainingText = value;
|
||||||
|
|
||||||
// First handle quoted segments to ensure we keep inline spaces in exact matches.
|
// 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 });
|
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
|
||||||
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
||||||
@ -139,7 +140,7 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
public bool HasFilter => !string.IsNullOrEmpty(SearchTerm);
|
public bool HasFilter => !string.IsNullOrEmpty(SearchTerm);
|
||||||
|
|
||||||
public bool Exact { get; private set; }
|
public MatchMode MatchMode { get; private set; }
|
||||||
|
|
||||||
public bool Matches(string value)
|
public bool Matches(string value)
|
||||||
{
|
{
|
||||||
@ -150,10 +151,18 @@ namespace osu.Game.Screens.Select
|
|||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (Exact)
|
switch (MatchMode)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case MatchMode.Substring:
|
||||||
|
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
|
case MatchMode.IsolatedPhrase:
|
||||||
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
|
case MatchMode.FullPhrase:
|
||||||
|
return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string searchTerm;
|
private string searchTerm;
|
||||||
@ -163,12 +172,46 @@ namespace osu.Game.Screens.Select
|
|||||||
get => searchTerm;
|
get => searchTerm;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
searchTerm = value.Trim('"');
|
searchTerm = value;
|
||||||
Exact = searchTerm != value;
|
|
||||||
|
if (searchTerm.StartsWith('\"'))
|
||||||
|
{
|
||||||
|
// length check ensures that the quote character in the `StartsWith()` check above and the `EndsWith()` check below is not the same character.
|
||||||
|
if (searchTerm.EndsWith("\"!", StringComparison.Ordinal) && searchTerm.Length >= 3)
|
||||||
|
{
|
||||||
|
searchTerm = searchTerm.TrimEnd('!').Trim('\"');
|
||||||
|
MatchMode = MatchMode.FullPhrase;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
searchTerm = searchTerm.Trim('\"');
|
||||||
|
MatchMode = MatchMode.IsolatedPhrase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
MatchMode = MatchMode.Substring;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm;
|
public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum MatchMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Match using a simple "contains" substring match.
|
||||||
|
/// </summary>
|
||||||
|
Substring,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Match for the search phrase being isolated by spaces, or at the start or end of the text.
|
||||||
|
/// </summary>
|
||||||
|
IsolatedPhrase,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Match for the search phrase matching the full text in completion.
|
||||||
|
/// </summary>
|
||||||
|
FullPhrase,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select
|
|||||||
public static class FilterQueryParser
|
public static class FilterQueryParser
|
||||||
{
|
{
|
||||||
private static readonly Regex query_syntax_regex = new Regex(
|
private static readonly Regex query_syntax_regex = new Regex(
|
||||||
@"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))",
|
@"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*""[!]?)|(\S*))",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
internal static void ApplyQueries(FilterCriteria criteria, string query)
|
internal static void ApplyQueries(FilterCriteria criteria, string query)
|
||||||
|
Loading…
Reference in New Issue
Block a user