Skip to content

Commit 311a0d4

Browse files
authored
Fix: Fixed an issue where AND OR operators didn't work when searching for tags (#17896)
1 parent 3ac3253 commit 311a0d4

File tree

3 files changed

+109
-6
lines changed

3 files changed

+109
-6
lines changed

src/Files.App/Utils/Storage/Search/FolderSearch.cs

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.Extensions.Logging;
55
using System.IO;
6+
using System.Text.RegularExpressions;
67
using Windows.Storage;
78
using Windows.Storage.FileProperties;
89
using Windows.Storage.Search;
@@ -94,7 +95,7 @@ public Task SearchAsync(IList<ListedItem> results, CancellationToken token)
9495

9596
private async Task AddItemsForHomeAsync(IList<ListedItem> results, CancellationToken token)
9697
{
97-
if (AQSQuery.StartsWith("tag:", StringComparison.Ordinal))
98+
if (IsTagQuery(AQSQuery))
9899
{
99100
await SearchTagsAsync("", results, token); // Search tags everywhere, not only local drives
100101
}
@@ -183,19 +184,99 @@ private async Task AddItemsForLibraryAsync(LibraryLocationItem library, IList<Li
183184
}
184185
}
185186

187+
private bool IsTagQuery(string query)
188+
{
189+
return query?.Contains("tag:", StringComparison.OrdinalIgnoreCase) == true;
190+
}
191+
192+
private TagQueryExpression ParseTagQuery(string query)
193+
{
194+
var expression = new TagQueryExpression();
195+
var orParts = Regex.Split(query, @"\s+OR\s+", RegexOptions.IgnoreCase);
196+
197+
foreach (var orPart in orParts)
198+
{
199+
var andGroup = new List<TagTerm>();
200+
var andParts = Regex.Split(orPart, @"\s+AND\s+", RegexOptions.IgnoreCase);
201+
202+
foreach (var andPart in andParts)
203+
{
204+
var matches = Regex.Matches(andPart.Trim(), @"(NOT\s+)?tag:([^\s]+)", RegexOptions.IgnoreCase);
205+
foreach (Match match in matches)
206+
{
207+
var isExclude = !string.IsNullOrEmpty(match.Groups[1].Value);
208+
var tagValues = match.Groups[2].Value.Split(',', StringSplitOptions.RemoveEmptyEntries);
209+
var tagUids = new HashSet<string>();
210+
211+
foreach (var tagName in tagValues)
212+
{
213+
var uids = fileTagsSettingsService.GetTagsByName(tagName).Select(t => t.Uid);
214+
foreach (var uid in uids)
215+
{
216+
tagUids.Add(uid);
217+
}
218+
}
219+
220+
andGroup.Add(new TagTerm { TagUids = tagUids, IsExclude = isExclude });
221+
}
222+
}
223+
224+
if (andGroup.Count > 0)
225+
{
226+
expression.OrGroups.Add(andGroup);
227+
}
228+
}
229+
230+
return expression;
231+
}
232+
233+
private bool MatchesTagExpression(IEnumerable<string> fileTags, TagQueryExpression expression)
234+
{
235+
foreach (var orGroup in expression.OrGroups)
236+
{
237+
bool groupMatches = true;
238+
foreach (var term in orGroup)
239+
{
240+
if (term.IsExclude)
241+
{
242+
if (term.TagUids.Count > 0 && term.TagUids.Any(fileTags.Contains))
243+
{
244+
groupMatches = false;
245+
break;
246+
}
247+
}
248+
else
249+
{
250+
if (term.TagUids.Count == 0 || !term.TagUids.Any(fileTags.Contains))
251+
{
252+
groupMatches = false;
253+
break;
254+
}
255+
}
256+
}
257+
258+
if (groupMatches)
259+
{
260+
return true;
261+
}
262+
}
263+
264+
return false;
265+
}
266+
186267
private async Task SearchTagsAsync(string folder, IList<ListedItem> results, CancellationToken token)
187268
{
188269
//var sampler = new IntervalSampler(500);
189-
var tags = AQSQuery.Substring("tag:".Length)?.Split(',').Where(t => !string.IsNullOrWhiteSpace(t))
190-
.SelectMany(t => fileTagsSettingsService.GetTagsByName(t), (_, t) => t.Uid).ToHashSet();
191-
if (tags?.Any() != true)
270+
var expression = ParseTagQuery(AQSQuery);
271+
272+
if (expression.OrGroups.Count == 0)
192273
{
193274
return;
194275
}
195276

196277
var dbInstance = FileTagsHelper.GetDbInstance();
197278
var matches = dbInstance.GetAllUnderPath(folder)
198-
.Where(x => tags.All(x.Tags.Contains));
279+
.Where(x => MatchesTagExpression(x.Tags, expression));
199280
if (string.IsNullOrEmpty(folder))
200281
matches = matches.Where(x => !StorageTrashBinService.IsUnderTrashBin(x.FilePath));
201282

@@ -258,7 +339,7 @@ private async Task SearchTagsAsync(string folder, IList<ListedItem> results, Can
258339

259340
private async Task AddItemsAsync(string folder, IList<ListedItem> results, CancellationToken token)
260341
{
261-
if (AQSQuery.StartsWith("tag:", StringComparison.Ordinal))
342+
if (IsTagQuery(AQSQuery))
262343
{
263344
await SearchTagsAsync(folder, results, token);
264345
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
namespace Files.App.Utils.Storage
5+
{
6+
public sealed class TagQueryExpression
7+
{
8+
public List<List<TagTerm>> OrGroups { get; set; } = new();
9+
}
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
namespace Files.App.Utils.Storage
5+
{
6+
public class TagTerm
7+
{
8+
public HashSet<string> TagUids { get; set; } = new();
9+
10+
public bool IsExclude { get; set; }
11+
}
12+
}

0 commit comments

Comments
 (0)