Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ PublishScripts/
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# and except a Unity project's Packages/, which is source: Unity's package manifest and its
# resolved lock file are both meant to be committed, and a NuGet restore folder never contains
# a file by either name.
!**/[Pp]ackages/manifest.json
!**/[Pp]ackages/packages-lock.json
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
Expand Down Expand Up @@ -651,3 +656,16 @@ Temporary Items

# ImGui.ini files
imgui.ini

# Game engine projects
#
# Godot: the import cache, and the mono/temp bin+obj a C# build writes.
.godot/

# Unity: .meta files are source, not the Visual Studio C++ build artifact that the `*.meta` rule
# further up targets. Unity generates one per asset and it carries the GUID that scenes, prefabs
# and serialized references point at, so ignoring them gives every clone fresh GUIDs and silently
# breaks those references - including for a plug-in whose .dll is itself a build output. This
# negation has to come after that rule to win, and is scoped to the asset tree so the Visual
# Studio artifact stays ignored everywhere else.
!**/[Aa]ssets/**/*.meta
51 changes: 51 additions & 0 deletions TextFilter.Test/TextFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,57 @@
TextFilter.Filter(["", "hello"], "hel", TextFilterType.Regex, TextFilterMatchOptions.ByWordAll).ToList());
}

[TestMethod]
public void GlobStarMatchesAcrossSlash()
{
Assert.IsTrue(TextFilter.IsMatch("AC/DC", "AC*", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny));
}

[TestMethod]
public void GlobQuestionMarkMatchesSlash()
{
Assert.IsTrue(TextFilter.IsMatch("1/2", "1?2", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny));
}

[TestMethod]
public void GlobStarMatchesPath()
{
Assert.IsTrue(TextFilter.IsMatch("src/Foo.cs", "*.cs", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString));
}

[TestMethod]
public void GlobStarMatchesBackslashPath()
{
Assert.IsTrue(TextFilter.IsMatch(@"C:\temp\a.txt", "*.txt", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString));
}

[TestMethod]
public void GlobFilterKeepsPathItems()
{
List<string> result = [.. TextFilter.Filter(["docs/readme.md", "readme.md"], "*readme*")];

Assert.AreEqual(2, result.Count);

Check warning on line 890 in TextFilter.Test/TextFilterTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.HasCount' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_TextFilter&issues=AaDjFPuiEGH9SgCwmA2b&open=AaDjFPuiEGH9SgCwmA2b&pullRequest=119
}

[TestMethod]
public void GlobStarMatchesAcrossSlashCaseInsensitively()
{
Assert.IsTrue(TextFilter.IsMatch("docs/README.md", "*readme*", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny, TextFilterCaseSensitivity.CaseInsensitive));
}

[TestMethod]
public void GlobExcludedTokenMatchesAcrossSlash()
{
Assert.IsFalse(TextFilter.IsMatch("docs/readme.md", "-*readme*", TextFilterType.Glob, TextFilterMatchOptions.ByWordAny));
}

[TestMethod]
public void GlobLiteralSlashInPatternStillMatchesSlashInText()
{
Assert.IsTrue(TextFilter.IsMatch("docs/readme.md", "docs/*", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString));
Assert.IsFalse(TextFilter.IsMatch("docsXreadme.md", "docs/*", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString));
}

[TestMethod]
[DataRow("foo \t")]
[DataRow("foo \r\n")]
Expand Down
21 changes: 16 additions & 5 deletions TextFilter/TextFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(1);

[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "SYSLIB1045:Convert to 'GeneratedRegexAttribute'.", Justification = "Not available in older frameworks")]
private static Regex RegexMatchAnything() => new(".*", RegexOptions.Compiled);

Check warning on line 127 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

Check warning on line 127 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

Check warning on line 127 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

Check warning on line 127 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Pass a timeout to limit the execution time.

/// <summary>
/// Gets a hint for the specified filter type.
Expand Down Expand Up @@ -276,7 +276,7 @@

return ExcludedTokenPrefixes.Contains(prefix)
? TextFilterTokenType.Excluded
: RequiredTokenPrefixes.Contains(prefix)

Check warning on line 279 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 279 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 279 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 279 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 279 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 279 in TextFilter/TextFilter.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Extract this nested ternary operation into an independent statement.
? TextFilterTokenType.Required
: TextFilterTokenType.Optional;
})
Expand Down Expand Up @@ -336,7 +336,7 @@

// An unparseable excluded token is skipped rather than treated as match-anything, which here
// would exclude every item while the user is still typing the token.
bool anyExcludedMatches = excludedTokens.Any(filterToken => ResolveGlob(filterToken, caseSensitivity) is Glob glob && textTokens.Any(glob.IsMatch));
bool anyExcludedMatches = excludedTokens.Any(filterToken => ResolveGlob(filterToken, caseSensitivity) is Glob glob && textTokens.Any(token => IsGlobMatch(glob, token)));

if (anyExcludedMatches)
{
Expand Down Expand Up @@ -382,7 +382,7 @@

Glob? glob = ResolveGlob(filterToken, caseSensitivity);

return glob is null || textTokens.Any(glob.IsMatch);
return glob is null || textTokens.Any(token => IsGlobMatch(glob, token));
}

/// <summary>
Expand All @@ -399,9 +399,19 @@

Glob? glob = ResolveGlob(filterToken, caseSensitivity);

return glob is null || textTokens.All(glob.IsMatch);
return glob is null || textTokens.All(token => IsGlobMatch(glob, token));
}

// DotNet.Glob is a file-path glob, so its * and ? stop at / and \. TextFilter filters arbitrary
// text, where a slash is an ordinary character, so both separators are swapped for a private-use
// character in the pattern and in the text before DotNet.Glob sees either.
private const char MaskedPathSeparator = '\uE000';

private static string MaskPathSeparators(string value) =>
value.Replace('/', MaskedPathSeparator).Replace('\\', MaskedPathSeparator);

private static bool IsGlobMatch(Glob glob, string textToken) => glob.IsMatch(MaskPathSeparators(textToken));

// Returns null for a token that cannot be parsed, so each caller can decide what ignoring it means.
private static Glob? ResolveGlob(string filterToken, TextFilterCaseSensitivity caseSensitivity)
{
Expand All @@ -411,9 +421,10 @@
{
try
{
string maskedToken = MaskPathSeparators(filterToken);
glob = caseSensitivity is TextFilterCaseSensitivity.CaseInsensitive
? Glob.Parse(filterToken, CaseInsensitiveGlobOptions)
: Glob.Parse(filterToken);
? Glob.Parse(maskedToken, CaseInsensitiveGlobOptions)
: Glob.Parse(maskedToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
Expand Down
Loading