From 823cd941d197ab30bfd0448827c8121722bc6615 Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Sat, 31 Oct 2026 05:29:30 +0000 Subject: [PATCH 1/2] Match a whole-string glob containing a space against the whole text [patch] Under ByWholeString, a filter with only plain tokens and inner whitespace is first tried as one glob against the whole text, so "my file.txt" matches "my file.txt". The per-token pass still runs afterwards, so "*.jpg *.png" keeps matching "a.png", and filters with excluded or required tokens are unchanged. Fixes #137 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5pNp5mLi7vkHCEABR --- TextFilter.Test/TextFilterTests.cs | 33 ++++++++++++++++++++++++++++++ TextFilter/TextFilter.cs | 14 +++++++++++++ 2 files changed, 47 insertions(+) diff --git a/TextFilter.Test/TextFilterTests.cs b/TextFilter.Test/TextFilterTests.cs index 3250a64..04ae6ec 100644 --- a/TextFilter.Test/TextFilterTests.cs +++ b/TextFilter.Test/TextFilterTests.cs @@ -1372,4 +1372,37 @@ static int ScoreOf(string s) CollectionAssert.AreEqual(scores.OrderByDescending(s => s).ToList(), scores); Assert.IsNotEmpty(result); } + + [TestMethod] + [DataRow("foo bar", "foo bar")] + [DataRow("my file.txt", "my file.txt")] + [DataRow("my file.txt", " my file.txt ")] + [DataRow("my file.txt", "my *.txt")] + public void GlobByWholeStringMatchesTextContainingASpace(string text, string filter) + { + Assert.IsTrue(TextFilter.IsMatch(text, filter, TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + } + + [TestMethod] + public void GlobByWholeStringWithASpaceStillNeedsTheWholeTextToMatch() + { + Assert.IsFalse(TextFilter.IsMatch("foo bar baz", "foo bar", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + Assert.IsFalse(TextFilter.IsMatch("foo bar", "foo bar", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + Assert.IsFalse(TextFilter.IsMatch("Foo bar", "foo bar", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + Assert.IsTrue(TextFilter.IsMatch("Foo Bar", "foo bar", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString, TextFilterCaseSensitivity.CaseInsensitive)); + } + + [TestMethod] + public void GlobByWholeStringKeepsMultiplePatternsAndPrefixedTokens() + { + // #113: each pattern is still tried on its own. + Assert.IsTrue(TextFilter.IsMatch("a.png", "*.jpg *.png", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + Assert.IsFalse(TextFilter.IsMatch("a.gif", "*.jpg *.png", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + + // A filter with an excluded or required token is not tried whole, so those tokens behave as before. + Assert.IsFalse(TextFilter.IsMatch("a.png", "*.png -a.*", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + Assert.IsTrue(TextFilter.IsMatch("b.png", "*.png -a.*", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + Assert.IsTrue(TextFilter.IsMatch("a.png", "*.png +a.*", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + Assert.IsFalse(TextFilter.IsMatch("b.png", "*.png +a.*", TextFilterType.Glob, TextFilterMatchOptions.ByWholeString)); + } } diff --git a/TextFilter/TextFilter.cs b/TextFilter/TextFilter.cs index 927c00c..52f9809 100644 --- a/TextFilter/TextFilter.cs +++ b/TextFilter/TextFilter.cs @@ -344,6 +344,20 @@ public static bool DoesMatchGlob(string text, string filter, TextFilterMatchOpti return true; // empty filter matches all text } + // The filter is split on whitespace, so under ByWholeString no single token can equal a text that + // contains a space ("my file.txt"). With only plain tokens, try the whole filter as one glob first; + // the per-token pass below still runs, so "*.jpg *.png" keeps matching "a.png" (#113). + if (textFilterMatchOptions is TextFilterMatchOptions.ByWholeString + && filterTokens.Count == 1 + && filterTokens.ContainsKey(TextFilterTokenType.Optional) + && filter.Trim() is string wholeFilter + && wholeFilter.Any(char.IsWhiteSpace) + && ResolveGlob(wholeFilter, caseSensitivity) is Glob[] wholeFilterGlobs + && IsGlobMatch(wholeFilterGlobs, text)) + { + return true; + } + if (!filterTokens.TryGetValue(TextFilterTokenType.Excluded, out HashSet? excludedTokens)) { excludedTokens = []; From 2dab1c588b26253811be18a735e85d054a80bc33 Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Sat, 31 Oct 2026 05:39:53 +0000 Subject: [PATCH 2/2] Hoist the trimmed filter into a local, clearing Sonar S2589 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5pNp5mLi7vkHCEABR --- TextFilter/TextFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TextFilter/TextFilter.cs b/TextFilter/TextFilter.cs index 52f9809..82d4780 100644 --- a/TextFilter/TextFilter.cs +++ b/TextFilter/TextFilter.cs @@ -347,10 +347,10 @@ public static bool DoesMatchGlob(string text, string filter, TextFilterMatchOpti // The filter is split on whitespace, so under ByWholeString no single token can equal a text that // contains a space ("my file.txt"). With only plain tokens, try the whole filter as one glob first; // the per-token pass below still runs, so "*.jpg *.png" keeps matching "a.png" (#113). + string wholeFilter = filter.Trim(); if (textFilterMatchOptions is TextFilterMatchOptions.ByWholeString && filterTokens.Count == 1 && filterTokens.ContainsKey(TextFilterTokenType.Optional) - && filter.Trim() is string wholeFilter && wholeFilter.Any(char.IsWhiteSpace) && ResolveGlob(wholeFilter, caseSensitivity) is Glob[] wholeFilterGlobs && IsGlobMatch(wholeFilterGlobs, text))