package org.fxmisc.richtext.demo; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; import javafx.application.Application; import javafx.concurrent.Task; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.Stage; import org.fxmisc.flowless.VirtualizedScrollPane; import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.LineNumberFactory; import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyleSpansBuilder; public class JavaKeywordsAsync 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)*?\\*/"; private static final Pattern PATTERN = Pattern.compile( "(?" + KEYWORD_PATTERN + ")" + "|(?" + PAREN_PATTERN + ")" + "|(?" + BRACE_PATTERN + ")" + "|(?" + BRACKET_PATTERN + ")" + "|(?" + SEMICOLON_PATTERN + ")" + "|(?" + STRING_PATTERN + ")" // + "|(?" + 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 {", "\n\n\n\n\n\n\n\n\n\n\n\n\n\n", " /*", " * 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); } private CodeArea codeArea; private ExecutorService executor; @Override public void start(Stage primaryStage) { executor = Executors.newSingleThreadExecutor(); codeArea = new CodeArea(); Button resetStyles = new Button("reset"); resetStyles.setOnAction((e) -> { codeArea.clearStyle(0, codeArea.getText().length() - 1); }); codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea)); codeArea.richChanges() .filter(ch -> !ch.getInserted().equals(ch.getRemoved())) // XXX .successionEnds(Duration.ofMillis(500)) .supplyTask(this::computeHighlightingAsync) .awaitLatest(codeArea.richChanges()) .filterMap(t -> { if(t.isSuccess()) { return Optional.of(t.get()); } else { t.getFailure().printStackTrace(); return Optional.empty(); } }) .subscribe(this::applyHighlighting); VBox vbox = new VBox(); HBox hBox = new HBox(); StackPane stackPane = new StackPane(new VirtualizedScrollPane<>(codeArea)); HBox.setHgrow(vbox, Priority.ALWAYS); VBox.setVgrow(stackPane, Priority.ALWAYS); hBox.getChildren().addAll(resetStyles); vbox.getChildren().addAll(hBox, stackPane); Scene scene = new Scene(vbox, 600, 400); scene.getStylesheets().add(JavaKeywordsAsync.class.getResource("java-keywords.css").toExternalForm()); primaryStage.setScene(scene); primaryStage.setTitle("Java Keywords Async Demo"); primaryStage.show(); codeArea.replaceText(sampleCode); } @Override public void stop() { executor.shutdown(); } private Task>> computeHighlightingAsync() { String text = codeArea.getText(); Task>> task = new Task>>() { @Override protected StyleSpans> call() throws Exception { return computeHighlighting(text); } }; executor.execute(task); return task; } private void applyHighlighting(StyleSpans> highlighting) { codeArea.setStyleSpans(0, highlighting); } private static StyleSpans> computeHighlighting(String text) { Matcher matcher = PATTERN.matcher(text); int lastKwEnd = 0; StyleSpansBuilder> 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(); } }