Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -34,6 +34,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv

### Fixed

- We fixed the group filter to use OR semantics for space-separated terms, so typing `machine learning` now shows all groups containing either word instead of requiring both. [#12721](https://github.com/JabRef/jabref/issues/12721)
- We fixed RIS export writing the full page range into both start page and end page fields instead of splitting them correctly. [#15106](https://github.com/JabRef/jabref/issues/15106)
- We fixed an issue where shortcut keys did not work for linked files in the entry editor. [#12564](https://github.com/JabRef/jabref/issues/12564)
- We fixed the issue where incomplete search produced noisy error logs when entering input in the search bar. [#14632](https://github.com/JabRef/jabref/issues/14632)
Expand Down
16 changes: 14 additions & 2 deletions jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.logic.groups.GroupsFactory;
import org.jabref.logic.layout.format.LatexToUnicodeFormatter;
import org.jabref.logic.search.query.GroupNameFilterVisitor;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.logic.util.strings.StringUtil;
Expand Down Expand Up @@ -55,6 +56,8 @@
import org.jabref.model.search.event.IndexClosedEvent;
import org.jabref.model.search.event.IndexRemovedEvent;
import org.jabref.model.search.event.IndexStartedEvent;
import org.jabref.model.search.query.SearchQuery;
import org.jabref.search.SearchParser;

import com.google.common.eventbus.Subscribe;
import com.tobiasdiez.easybind.EasyBind;
Expand Down Expand Up @@ -332,7 +335,17 @@ void toggleExpansion() {
}

boolean isMatchedBy(String searchString) {
return StringUtil.isBlank(searchString) || StringUtil.containsIgnoreCase(getDisplayName(), searchString);
if (StringUtil.isBlank(searchString)) {
return true;
}
try {
SearchParser.StartContext ctx = SearchQuery.getStartContext(searchString);
GroupNameFilterVisitor visitor = new GroupNameFilterVisitor(getDisplayName());
return visitor.visit(ctx);
} catch (org.antlr.v4.runtime.misc.ParseCancellationException e) {
// Malformed query: fall back to plain contains
return StringUtil.containsIgnoreCase(getDisplayName(), searchString);
}
}

public Color getColor() {
Expand All @@ -351,7 +364,6 @@ public Optional<GroupNodeViewModel> getChildByPath(String pathToSource) {
///
/// - another group (will be added as subgroup on drop)
/// - entries if the group implements {@link GroupEntryChanger} (will be assigned to group on drop)
///
public boolean acceptableDrop(Dragboard dragboard) {
// TODO: we should also check isNodeDescendant
boolean canDropOtherGroup = dragboard.hasContent(DragAndDropDataFormats.GROUP);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.jabref.gui.groups;

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

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class GroupNodeViewModelFilterTest {

private boolean matches(String groupName, String query) {
if (query == null || query.isBlank()) {
return true;
}
try {
var ctx = SearchQuery.getStartContext(query);
return new GroupNameFilterVisitor(groupName).visit(ctx);
} catch (org.antlr.v4.runtime.misc.ParseCancellationException e) {
return groupName.toLowerCase().contains(query.toLowerCase());
}
}

@Test
void spaceImpliesOrFirstToken() {
assertTrue(matches("machine", "machine learning"));
}

@Test
void spaceImpliesOrSecondToken() {
assertTrue(matches("learning", "machine learning"));
}

@Test
void spaceImpliesOrBothTokens() {
assertTrue(matches("machine learning", "machine learning"));
}

@Test
void explicitAndRequiresBothWords() {
assertFalse(matches("machine", "machine AND neural"));
assertTrue(matches("machine neural", "machine AND neural"));
}

@Test
void explicitOrWorks() {
assertTrue(matches("machine", "machine OR neural"));
assertTrue(matches("neural", "machine OR neural"));
assertFalse(matches("unrelated", "machine OR neural"));
}

@Test
void notWorks() {
assertFalse(matches("machine", "NOT machine"));
assertTrue(matches("learning", "NOT machine"));
}

@Test
void blankQueryMatchesAll() {
assertTrue(matches("anything", ""));
assertTrue(matches("anything", " "));
}

@Test
void caseInsensitiveMatch() {
assertTrue(matches("Machine Learning", "machine"));
assertTrue(matches("machine learning", "MACHINE"));
}

@Test
void malformedQueryFallsBackToContains() {
assertTrue(matches("test group", "test"));
}

@Test
void parenthesesWithNotWork() {
assertTrue(matches("Deep Learning", "(deep OR neural) NOT vision"));
assertTrue(matches("Neural Networks", "(deep OR neural) NOT vision"));
assertFalse(matches("Computer Vision", "(deep OR neural) NOT vision"));
assertFalse(matches("Machine Learning", "(deep OR neural) NOT vision"));
}

@Test
void complexExpressionWithNotLearning() {
assertTrue(matches("Computer Vision", "(machine OR computer) NOT learning"));
assertFalse(matches("Machine Learning", "(machine OR computer) NOT learning"));
assertFalse(matches("Neural Networks", "(machine OR computer) NOT learning"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.jabref.logic.search.query;

import org.jabref.search.SearchBaseVisitor;
import org.jabref.search.SearchParser;

/**
* 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) {
this.groupName = groupName.toLowerCase();
}

@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) {
String term = SearchQueryConversion.unescapeSearchValue(ctx.searchValue()).toLowerCase();
return groupName.contains(term);
}
}
Loading