Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e3d7134
Fix group filter to use OR semantics for space-separated terms
NishantDG-SST Mar 16, 2026
fa84c44
Update CHANGELOG for #12721
NishantDG-SST Mar 16, 2026
253bba2
Merge branch 'main' into fix-group-filter-or-semantics-12721
NishantDG-SST Mar 16, 2026
cfad844
Address review feedback: Locale.ROOT, /// javadoc, parse-once, rename…
NishantDG-SST Mar 16, 2026
43705d7
added comment for lowerCase, converted Tests to parameterized tests, …
NishantDG-SST Mar 16, 2026
117f846
Fixed visitComparison to fallback to plain text for fielded/operator …
NishantDG-SST Mar 17, 2026
184bf78
Restore accidentally removed blank
NishantDG-SST Mar 17, 2026
b74ebd2
Implemented static matches method, resolved logic handeling test help…
NishantDG-SST Mar 17, 2026
7f9460e
Removed wrapper matches class Utilised the static matches method dire…
NishantDG-SST Mar 17, 2026
98b3f3b
ci: retrigger
NishantDG-SST Mar 17, 2026
4fe94ed
ci: retrigger
NishantDG-SST Mar 17, 2026
380d1bb
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 17, 2026
4dba5b7
ci: retrigger
NishantDG-SST Mar 17, 2026
d9e7b02
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 18, 2026
f182393
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 18, 2026
f0c7a12
Merge branch 'main' into fix-group-filter-or-semantics-12721
NishantDG-SST Mar 19, 2026
72a8b8d
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 19, 2026
e6034c9
Merge branch 'fix-group-filter-or-semantics-12721' of https://github.…
NishantDG-SST Mar 19, 2026
6ed0f51
Merge branch 'main' into fix-group-filter-or-semantics-12721
NishantDG-SST Mar 20, 2026
570a8eb
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 23, 2026
2cb27f5
Merge branch 'fix-group-filter-or-semantics-12721' of https://github.…
NishantDG-SST Mar 23, 2026
8d2b862
Merge branch 'main' into fix-group-filter-or-semantics-12721
NishantDG-SST Mar 25, 2026
fc598d1
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 25, 2026
19a81cb
Merge branch 'fix-group-filter-or-semantics-12721' of https://github.…
NishantDG-SST Mar 25, 2026
54c81f2
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 25, 2026
5930bba
Retrigger CI checks
NishantDG-SST Mar 25, 2026
83ffe3e
Retrigger CI checks
NishantDG-SST Mar 25, 2026
f88e8f5
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 25, 2026
32efe4b
Merge branch 'main' into fix-group-filter-or-semantics-12721
NishantDG-SST Mar 28, 2026
146a5ac
Merge remote-tracking branch 'upstream/main' into fix-group-filter-or…
NishantDG-SST Mar 28, 2026
2af1f2e
Merge branch 'fix-group-filter-or-semantics-12721' of https://github.…
NishantDG-SST Mar 28, 2026
1ad23ed
Merge branch 'main' into fix-group-filter-or-semantics-12721
NishantDG-SST Apr 1, 2026
327b6e1
Update CHANGELOG.md
calixtus Apr 4, 2026
19f9b97
Merge branch 'main' into fix-group-filter-or-semantics-12721
calixtus Apr 4, 2026
36daa50
Merge branch 'main' into fix-group-filter-or-semantics-12721
calixtus Apr 5, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv

### Fixed

- We improved the group filter to support full boolean search syntax. [#12721](https://github.com/JabRef/jabref/issues/12721)
- We fixed the column chooser context menu appearing when right-clicking the empty library table body. [#15384](https://github.com/JabRef/jabref/issues/15384)
- We fixed web search rejecting queries with non-standard syntax. [#12637](https://github.com/JabRef/jabref/issues/12637)
- We fixed an issue where multiline property of fields could not be removed properly. [#11897](https://github.com/JabRef/jabref/issues/11897)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.groups.GroupsFactory;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.search.query.GroupNameFilterVisitor;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
Expand Down Expand Up @@ -109,7 +110,9 @@ public GroupTreeViewModel(@NonNull StateManager stateManager,
EasyBind.subscribe(selectedGroups, this::onSelectedGroupChanged);

// Set-up bindings
filterPredicate.bind(EasyBind.map(filterText, text -> group -> group.isMatchedBy(text)));
filterPredicate.bind(EasyBind.map(filterText, text ->
group -> GroupNameFilterVisitor.matches(group.getDisplayName(), text)
));
}

private void refresh() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.jabref.gui.groups;

import org.jabref.logic.search.query.GroupNameFilterVisitor;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

class GroupNodeViewModelFilterTest {

@ParameterizedTest
@CsvSource({
"machine, machine learning, true",
"learning, machine learning, true",
"machine learning, machine learning, true",
"Neural Networks, machine learning, false",
"test group, test, true"
})
void spaceImpliesOr(String groupName, String query, boolean expected) {
assertEquals(expected, GroupNameFilterVisitor.matches(groupName, query));
}

@ParameterizedTest
@CsvSource({
"machine neural, machine AND neural, true",
"machine, machine AND neural, false",
"neural, machine AND neural, false"
})
void explicitAndRequiresBothWords(String groupName, String query, boolean expected) {
assertEquals(expected, GroupNameFilterVisitor.matches(groupName, query));
}

@ParameterizedTest
@CsvSource({
"machine, machine OR neural, true",
"neural, machine OR neural, true",
"unrelated, machine OR neural, false"
})
void explicitOrWorks(String groupName, String query, boolean expected) {
assertEquals(expected, GroupNameFilterVisitor.matches(groupName, query));
}

@ParameterizedTest
@CsvSource({
"machine, NOT machine, false",
"learning, NOT machine, true"
})
void notWorks(String groupName, String query, boolean expected) {
assertEquals(expected, GroupNameFilterVisitor.matches(groupName, query));
}

@ParameterizedTest
@CsvSource({
"Machine Learning, machine, true",
"machine learning, MACHINE, true"
})
void caseInsensitiveMatch(String groupName, String query, boolean expected) {
assertEquals(expected, GroupNameFilterVisitor.matches(groupName, query));
}

@ParameterizedTest
@CsvSource({
"Deep Learning, (deep OR neural) NOT vision, true",
"Neural Networks, (deep OR neural) NOT vision, true",
"Computer Vision, (deep OR neural) NOT vision, false",
"Machine Learning, (deep OR neural) NOT vision, false"
})
void parenthesesWithNotWork(String groupName, String query, boolean expected) {
assertEquals(expected, GroupNameFilterVisitor.matches(groupName, query));
}

@ParameterizedTest
@CsvSource({
"Computer Vision, (machine OR computer) NOT learning, true",
"Machine Learning, (machine OR computer) NOT learning, false",
"Neural Networks, (machine OR computer) NOT learning, false"
})
void complexExpressionWithNotLearning(String groupName, String query, boolean expected) {
assertEquals(expected, GroupNameFilterVisitor.matches(groupName, query));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.jabref.logic.search.query;

import java.util.Locale;

import org.jabref.logic.util.strings.StringUtil;
import org.jabref.model.search.query.SearchQuery;
import org.jabref.search.SearchBaseVisitor;
import org.jabref.search.SearchParser;

import org.antlr.v4.runtime.misc.ParseCancellationException;

/// Evaluates a Search.g4 parse tree against a group display name string.
/// Key behavioral difference from {@link SearchQueryVisitor}:
/// {@code visitImplicitAndExpression} uses OR semantics (anyMatch) instead of AND,
/// so space-separated bare terms like "machine learning" match any group containing
/// either word. Explicit AND/OR/NOT and parentheses work as expected.
public class GroupNameFilterVisitor extends SearchBaseVisitor<Boolean> {

private final String groupName;

public GroupNameFilterVisitor(String groupName) {
// Lowercase for case-insensitive matching: typing "machine" matches "Machine Learning"
this.groupName = groupName.toLowerCase(Locale.ROOT);
}

@Override
public Boolean visitStart(SearchParser.StartContext ctx) {
if (ctx.andExpression() == null) {
return true;
}
return visit(ctx.andExpression());
}

@Override
public Boolean visitImplicitAndExpression(SearchParser.ImplicitAndExpressionContext ctx) {
if (ctx.expression().size() == 1) {
return visit(ctx.expression().getFirst());
}
boolean allSimpleTerm = ctx.expression().stream()
.allMatch(e -> e instanceof SearchParser.ComparisonExpressionContext);
if (allSimpleTerm) {
return ctx.expression().stream().anyMatch(this::visit);
} else {
return ctx.expression().stream().allMatch(this::visit);
}
}

@Override
public Boolean visitBinaryExpression(SearchParser.BinaryExpressionContext ctx) {
boolean left = visit(ctx.left);
boolean right = visit(ctx.right);
return ctx.bin_op.getType() == SearchParser.AND
? left && right
: left || right;
}

@Override
public Boolean visitNegatedExpression(SearchParser.NegatedExpressionContext ctx) {
return !visit(ctx.expression());
}

@Override
public Boolean visitParenExpression(SearchParser.ParenExpressionContext ctx) {
return visit(ctx.andExpression());
}

@Override
public Boolean visitComparisonExpression(SearchParser.ComparisonExpressionContext ctx) {
return visit(ctx.comparison());
}

@Override
public Boolean visitComparison(SearchParser.ComparisonContext ctx) {
// Determine whether this is a simple term or a fielded/operator comparison.
// If the full node text differs from the searchValue text, we assume the
// presence of a field and/or operator (e.g., "name != learning").
String fullText = ctx.getText();
String valueText = ctx.searchValue().getText();

if (!fullText.equals(valueText)) {
// Fielded/operator comparisons are not supported by this visitor.
// Fall back to treating the entire comparison as a plain-text term,
// to avoid misinterpreting operators like "!=" as a positive match.
String plain = fullText.toLowerCase(Locale.ROOT);
return groupName.contains(plain);
}

String term = SearchQueryConversion.unescapeSearchValue(ctx.searchValue()).toLowerCase(Locale.ROOT);
return groupName.contains(term);
}

/// Implemented a static method that checks whether the group name matches the given query string.
public static boolean matches(String groupName, String query) {
if (StringUtil.isBlank(query)) {
return true;
}
try {
SearchParser.StartContext ctx = SearchQuery.getStartContext(query);
return new GroupNameFilterVisitor(groupName).visit(ctx);
} catch (ParseCancellationException e) {
return StringUtil.containsIgnoreCase(groupName, query);
}
}
}
Loading