Skip to content

Commit

Permalink
Add initial version for autocompletion in text fields and text areas
Browse files Browse the repository at this point in the history
  • Loading branch information
Bios-Marcel committed Jul 19, 2024
1 parent a315933 commit f5d3a5a
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 62 deletions.
2 changes: 1 addition & 1 deletion src/main/antlr4/link/biosmarcel/baka/FilterLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ NOT_EQ: '!=';
STRING: ["].*?["];
BOOLEAN: ('true'|'false');
NUMBER: [,.0-9]+;
WORD: ~[ \u000B\t\r\n"]+;
WORD: ~[() \u000B\t\r\n"]+;
SPACES: [ \u000B\t\r\n] -> channel(HIDDEN);
2 changes: 1 addition & 1 deletion src/main/antlr4/link/biosmarcel/baka/FilterParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ options {

query: expression? EOF;
expression: field operator=(LT | LT_EQ | GT | GT_EQ | EQ | NOT_EQ | HAS) value #comparatorExpression
| expression operator=(AND | OR) expression #binaryExpression
| OPEN_PAR expression CLOSE_PAR #groupedExpression
| expression operator=(AND | OR) expression #binaryExpression
;

field: WORD;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public void setQuery(final String query) {
public boolean test(final Payment payment) {
if (compiled == null) {
compiled = new PaymentFilter();
if (!compiled.setQuery(query)) {
try {
compiled.setQuery(query);
} catch (final RuntimeException exception) {
return false;
}
}
Expand Down
85 changes: 41 additions & 44 deletions src/main/java/link/biosmarcel/baka/filter/Filter.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import link.biosmarcel.baka.FilterParserBaseListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ErrorNodeImpl;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.jspecify.annotations.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

Expand All @@ -30,14 +28,8 @@ public <ValueType> void register(
.put(operator, new FieldData<>(predicate, convertString));
}

public boolean setQuery(final String textQuery) {
try {
_setQuery(textQuery);
return true;
} catch (final RuntimeException exception) {
System.out.println("Failed to compile query: " + exception.getMessage());
return false;
}
public void setQuery(final String textQuery) {
_setQuery(textQuery);
}

private void _setQuery(final String textQuery) {
Expand All @@ -52,11 +44,13 @@ private void _setQuery(final String textQuery) {
ParseTreeWalker.DEFAULT.walk(new FilterParserBaseListener() {
@Override
public void enterQuery(final FilterParser.QueryContext ctx) {
if (ctx.getRuleContext().getChildCount() == 2) {
final Expression<FilterTarget> expression = new Expression<>();
root.set(expression);
contextToExpression.put(ctx, expression);
if (textQuery.isBlank()) {
throw new IncompleteQueryException(true, "", fieldToOperatorToExtractor.keySet().stream().toList());
}

final Expression<FilterTarget> expression = new Expression<>();
root.set(expression);
contextToExpression.put(ctx, expression);
}

private void insertPredicate(final Expression<FilterTarget> expression, final Predicate<FilterTarget> predicate) {
Expand All @@ -71,10 +65,8 @@ private void insertPredicate(final Expression<FilterTarget> expression, final Pr

@Override
public void enterGroupedExpression(final FilterParser.GroupedExpressionContext ctx) {
for (final var child : ctx.children) {
if (child instanceof ErrorNodeImpl) {
throw new IllegalStateException("Query incomplete");
}
if (ctx.getChild(1).getText().isBlank()) {
throw new IncompleteQueryException(false, "", fieldToOperatorToExtractor.keySet().stream().toList());
}

var targetContext = contextToExpression.get(ctx.parent);
Expand All @@ -85,12 +77,6 @@ public void enterGroupedExpression(final FilterParser.GroupedExpressionContext c

@Override
public void enterBinaryExpression(final FilterParser.BinaryExpressionContext ctx) {
for (final var child : ctx.children) {
if (child instanceof ErrorNodeImpl) {
throw new IllegalStateException("Query incomplete");
}
}

final var expression = new Expression<FilterTarget>();
if (ctx.operator.getText().equalsIgnoreCase("AND")) {
expression.binaryExpressionType = BinaryExpressionType.AND;
Expand All @@ -100,40 +86,51 @@ public void enterBinaryExpression(final FilterParser.BinaryExpressionContext ctx
throw new RuntimeException("Unknown Binary Operator");
}

if (ctx.getChildCount() == 3 && ctx.getChild(2).getText().isBlank()) {
throw new IncompleteQueryException(false, "", fieldToOperatorToExtractor.keySet().stream().toList());
}

var targetContext = contextToExpression.get(ctx.parent);

contextToExpression.put(ctx, expression);
insertPredicate(targetContext, expression);
}

private List<String> autocompleteOperators(final Collection<Operator> operators, final String text) {
final String textLowered = text.toLowerCase();
return operators
.stream()
.filter(operator -> operator.text.startsWith(textLowered))
.map(op -> op.text)
.sorted()
.toList();
}

@Override
public void enterComparatorExpression(final FilterParser.ComparatorExpressionContext ctx) {
for (final var child : ctx.children) {
if (child instanceof ErrorNodeImpl) {
throw new IllegalStateException("Query incomplete");
}
}

final var targetContext = contextToExpression.get(ctx.parent);
final var operatorToExtractor = fieldToOperatorToExtractor.get(ctx.field().getText());
if (operatorToExtractor == null) {
throw new IllegalStateException("Field not registered: " + ctx.field().getText());
throw new IncompleteQueryException(false,
ctx.field().getText(),
fieldToOperatorToExtractor.keySet()
.stream()
.filter(key -> key.startsWith(ctx.field().getText()))
.toList());
}

if (ctx.children.size() <= 1) {
throw new IllegalStateException("missing operator");
throw new IncompleteQueryException(false, "", autocompleteOperators(operatorToExtractor.keySet(), ""));
}

final Operator operator = Operator.match(ctx.operator.getText());
if (operator == null) {
throw new IncompleteQueryException(
false,
ctx.getChild(1).getText(),
autocompleteOperators(operatorToExtractor.keySet(), ctx.getChild(1).getText()));
}

final Operator operator = switch (ctx.operator.getText()) {
case "=" -> Operator.EQ;
case "!=" -> Operator.NOT_EQ;
case ">" -> Operator.GT;
case "<" -> Operator.LT;
case ">=" -> Operator.GT_EQ;
case "<=" -> Operator.LT_EQ;
case "contains", "has" -> Operator.HAS;
default -> throw new UnsupportedOperationException("Unknown operator: " + ctx.operator.getText());
};
final FieldData<FilterTarget, Object> extractor = (FieldData<FilterTarget, Object>) operatorToExtractor.get(operator);
if (extractor == null) {
throw new IllegalStateException("Extractor not registered: " + ctx.field().getText() + "." + ctx.operator.getText());
Expand All @@ -142,7 +139,7 @@ public void enterComparatorExpression(final FilterParser.ComparatorExpressionCon
final var value = ctx.value();
String unquoted = value.getText();
if (unquoted.isBlank()) {
throw new IllegalStateException("missing value");
throw new IncompleteQueryException(false, unquoted, Collections.EMPTY_LIST);
}

final var stringToken = value.STRING();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package link.biosmarcel.baka.filter;

import java.util.List;

public class IncompleteQueryException extends RuntimeException {
public final boolean empty;
public final String token;
public final List<String> options;

public IncompleteQueryException(final boolean empty,
final String token,
final List<String> options) {
this.empty = empty;
this.token = token;
this.options = options;
}
}
32 changes: 25 additions & 7 deletions src/main/java/link/biosmarcel/baka/filter/Operator.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
package link.biosmarcel.baka.filter;

import org.jspecify.annotations.Nullable;

public enum Operator {
EQ,
NOT_EQ,
HAS,
LT,
LT_EQ,
GT,
GT_EQ,
EQ("="),
NOT_EQ("!="),
HAS("has"),
LT("<"),
LT_EQ("<="),
GT(">"),
GT_EQ(">=");

public final String text;

Operator(final String text) {
this.text = text;
}

public static @Nullable Operator match(final String text) {
for (final var operator : Operator.values()) {
if (operator.text.equalsIgnoreCase(text)) {
return operator;
}
}

return null;
}
}
28 changes: 28 additions & 0 deletions src/main/java/link/biosmarcel/baka/view/AutocompleteField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package link.biosmarcel.baka.view;

import javafx.scene.control.TextField;
import javafx.scene.control.TextInputControl;
import javafx.scene.control.skin.TextFieldSkin;

import java.util.List;
import java.util.function.Function;

public class AutocompleteField extends AutocompleteInput {
public AutocompleteField(Function<String, List<String>> autocompleteGenerator) {
super(autocompleteGenerator);
}

@Override
void positionPopup() {
final var textFieldBounds = input.getBoundsInParent();
completionList.setTranslateY(textFieldBounds.getMaxY());

final var caretBounds = ((TextFieldSkin) input.getSkin()).getCharacterBounds(input.getCaretPosition());
completionList.setTranslateX(textFieldBounds.getMinX() + caretBounds.getMinX());
}

@Override
TextInputControl createInput() {
return new TextField();
}
}
Loading

0 comments on commit f5d3a5a

Please sign in to comment.