I am using the mechanism exemplified by JavaKeywordsDemo.java to highlight paragraphs as they become visible. This works perfectly when scrolling.
I have use-cases for moveTo() - jumping to the previous error by clicking a button, for example. This mostly works just as well, except that somehow the first visible paragraph does not get styled.
I'm new to ReactFX (and react in general) so my debugging has not been too helpful, but I did notice a suspicious behaviour. In the demo to reproduce below,
Position 1 seems to be skipped somehow, which would explain why it does not get highlighted.
The below is a slightly modified JavaKeywordsDemo.java (you will need the java-keywords.css from the demos, unaltered). Pressing the '0' key calls a moveTo(currentParagraph - 10, 0), and the issue becomes visible once you start scrolling up with it.
package org.fxmisc.richtext.demo;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.GenericStyledArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.model.Paragraph;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import org.reactfx.collection.ListModification;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JavaKeywordsDemo extends Application {
private static final String[] KEYWORDS = new String[] {
"abstract", "assert", "boolean", "break", "byte",
"case", "catch", "char", "class", "const",
"continue", "default", "do", "double", "else",
"enum", "extends", "final", "finally", "float",
"for", "goto", "if", "implements", "import",
"instanceof", "int", "interface", "long", "native",
"new", "package", "private", "protected", "public",
"return", "short", "static", "strictfp", "super",
"switch", "synchronized", "this", "throw", "throws",
"transient", "try", "void", "volatile", "while"
};
private static final String KEYWORD_PATTERN = "\\b(" + String.join("|", KEYWORDS) + ")\\b";
private static final String PAREN_PATTERN = "\\(|\\)";
private static final String BRACE_PATTERN = "\\{|\\}";
private static final String BRACKET_PATTERN = "\\[|\\]";
private static final String SEMICOLON_PATTERN = "\\;";
private static final String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";
private static final String COMMENT_PATTERN = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/" // for whole text processing (text blocks)
+ "|" + "/\\*[^\\v]*" + "|" + "^\\h*\\*([^\\v]*|/)"; // for visible paragraph processing (line by line)
private static final Pattern PATTERN = Pattern.compile(
"(?<KEYWORD>" + KEYWORD_PATTERN + ")"
+ "|(?<PAREN>" + PAREN_PATTERN + ")"
+ "|(?<BRACE>" + BRACE_PATTERN + ")"
+ "|(?<BRACKET>" + BRACKET_PATTERN + ")"
+ "|(?<SEMICOLON>" + SEMICOLON_PATTERN + ")"
+ "|(?<STRING>" + STRING_PATTERN + ")"
+ "|(?<COMMENT>" + COMMENT_PATTERN + ")"
);
private static final String sampleCode = String.join("\n", new String[] {
"package com.example;",
"",
"import java.util.*;",
"",
"public class Foo extends Bar implements Baz {",
"",
" /*",
" * multi-line comment",
" */",
" public static void main(String[] args) {",
" // single-line comment",
" for(String arg: args) {",
" if(arg.length() != 0)",
" System.out.println(arg);",
" else",
" System.err.println(\"Warning: empty string as argument\");",
" }",
" }",
"",
"package com.example;",
"",
"import java.util.*;",
"",
"public class Foo extends Bar implements Baz {",
"",
" /*",
" * multi-line comment",
" */",
" public static void main(String[] args) {",
" // single-line comment",
" for(String arg: args) {",
" if(arg.length() != 0)",
" System.out.println(arg);",
" else",
" System.err.println(\"Warning: empty string as argument\");",
" }",
" }",
"",
"package com.example;",
"",
"import java.util.*;",
"",
"public class Foo extends Bar implements Baz {",
"",
" /*",
" * multi-line comment",
" */",
" public static void main(String[] args) {",
" // single-line comment",
" for(String arg: args) {",
" if(arg.length() != 0)",
" System.out.println(arg);",
" else",
" System.err.println(\"Warning: empty string as argument\");",
" }",
" }",
"",
"package com.example;",
"",
"import java.util.*;",
"",
"public class Foo extends Bar implements Baz {",
"",
" /*",
" * multi-line comment",
" */",
" public static void main(String[] args) {",
" // single-line comment",
" for(String arg: args) {",
" if(arg.length() != 0)",
" System.out.println(arg);",
" else",
" System.err.println(\"Warning: empty string as argument\");",
" }",
" }",
"",
"}"
});
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
CodeArea codeArea = new CodeArea();
// add line numbers to the left of area
codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
// recompute syntax highlighting only for visible paragraph changes
// Note that this shows how it can be done but is not recommended for production where multi-
// line syntax requirements are needed, like comment blocks without a leading * on each line.
codeArea.getVisibleParagraphs().addModificationObserver
(
new VisibleParagraphStyler<>( codeArea, this::computeHighlighting )
);
codeArea.addEventHandler( KeyEvent.KEY_PRESSED, KE ->
{
int currentParagraph = codeArea.getCurrentParagraph();
if (KE.getCode() == KeyCode.DIGIT0) {
codeArea.moveTo(currentParagraph - 10, 0);
codeArea.requestFollowCaret();
}
});
codeArea.replaceText(0, 0, sampleCode);
codeArea.requestFollowCaret();
codeArea.setEditable(false);
Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(codeArea)), 600, 400);
scene.getStylesheets().add(JavaKeywordsDemo.class.getResource("java-keywords.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setTitle("Java Keywords Demo");
primaryStage.show();
}
private StyleSpans<Collection<String>> computeHighlighting(String text) {
Matcher matcher = PATTERN.matcher(text);
int lastKwEnd = 0;
StyleSpansBuilder<Collection<String>> spansBuilder
= new StyleSpansBuilder<>();
while(matcher.find()) {
String styleClass =
matcher.group("KEYWORD") != null ? "keyword" :
matcher.group("PAREN") != null ? "paren" :
matcher.group("BRACE") != null ? "brace" :
matcher.group("BRACKET") != null ? "bracket" :
matcher.group("SEMICOLON") != null ? "semicolon" :
matcher.group("STRING") != null ? "string" :
matcher.group("COMMENT") != null ? "comment" :
null; /* never happens */ assert styleClass != null;
spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
lastKwEnd = matcher.end();
}
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
return spansBuilder.create();
}
private class VisibleParagraphStyler<PS, SEG, S> implements Consumer<ListModification<? extends Paragraph<PS, SEG, S>>>
{
private final GenericStyledArea<PS, SEG, S> area;
private final Function<String,StyleSpans<S>> computeStyles;
private int prevParagraph, prevTextLength;
public VisibleParagraphStyler( GenericStyledArea<PS, SEG, S> area, Function<String,StyleSpans<S>> computeStyles )
{
this.computeStyles = computeStyles;
this.area = area;
}
@Override
public void accept( ListModification<? extends Paragraph<PS, SEG, S>> lm )
{
if ( lm.getAddedSize() > 0 )
{
int paragraph = Math.min( area.firstVisibleParToAllParIndex() + lm.getFrom(), area.getParagraphs().size()-1 );
String text = area.getText( paragraph, 0, paragraph, area.getParagraphLength( paragraph ) );
if ( paragraph != prevParagraph || text.length() != prevTextLength )
{
int startPos = area.getAbsolutePosition( paragraph, 0 );
Platform.runLater( () -> area.setStyleSpans( startPos, computeStyles.apply( text ) ) );
prevTextLength = text.length();
prevParagraph = paragraph;
}
}
}
}
}
I am using the mechanism exemplified by JavaKeywordsDemo.java to highlight paragraphs as they become visible. This works perfectly when scrolling.
I have use-cases for moveTo() - jumping to the previous error by clicking a button, for example. This mostly works just as well, except that somehow the first visible paragraph does not get styled.
I'm new to ReactFX (and react in general) so my debugging has not been too helpful, but I did notice a suspicious behaviour. In the demo to reproduce below,
Position 1 seems to be skipped somehow, which would explain why it does not get highlighted.
Environment info:
Reproducible Demo
The below is a slightly modified JavaKeywordsDemo.java (you will need the java-keywords.css from the demos, unaltered). Pressing the '0' key calls a moveTo(currentParagraph - 10, 0), and the issue becomes visible once you start scrolling up with it.