本人最近在尝试在JavaFX框架下实现一个代码编辑器,并实现文本高亮功能。经过一番调研,我找到了一个名为CodeArea的组件,它是一个基于RichTextFX库的代码编辑器组件。通过使用CodeArea,我能够在JavaFX应用程序中实现文本高亮功能。
参考源码中给出的示例代码,我实现了基本的功能。鉴于实现过程较为复杂,故在此博客中作记录。
RichTextFX
RichTextFX是一个基于JavaFX的富文本编辑器库。它提供了一系列功能,如文本样式、段落样式、图像插入等,以帮助开发者构建功能丰富的文本编辑器。源代码仓库中给出了诸多demo示例来展示RichTextFX库的各种用法。
本篇博客中使用到了其中的CodeArea组件来实现编辑器的高亮功能。
CodeArea实现高亮
首先,在pom.xml文件中添加依赖:
1 2 3 4 5
| <dependency> <groupId>org.fxmisc.richtext</groupId> <artifactId>richtextfx</artifactId> <version>0.11.0</version> </dependency>
|
关键词匹配及高亮样式
为了实现文本高亮,首先需要准备关键词匹配,可以使用正则表达式Pattern类来匹配关键词:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private static final String[] KEYWORDS = new String[]{"package", "class", "interface", "enum"}; 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>" + KEYWORD_PATTERN + ")" + "|(?<PAREN>" + PAREN_PATTERN + ")" + "|(?<BRACE>" + BRACE_PATTERN + ")" + "|(?<BRACKET>" + BRACKET_PATTERN + ")" + "|(?<SEMICOLON>" + SEMICOLON_PATTERN + ")" + "|(?<STRING>" + STRING_PATTERN + ")" + "|(?<COMMENT>" + COMMENT_PATTERN + ")" );
|
这段代码中定义了七种匹配模式,分别用于匹配关键字、括号、大括号、中括号、分号、字符串和注释。
同时需要分别为七种模式定义对应的样式,可以单独使用css文件来定义样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| .keyword { -fx-fill: purple; -fx-font-weight: bold; } .semicolon { -fx-font-weight: bold; } .paren { -fx-fill: firebrick; -fx-font-weight: bold; } .bracket { -fx-fill: darkgreen; -fx-font-weight: bold; } .brace { -fx-fill: teal; -fx-font-weight: bold; } .string { -fx-fill: blue; } .comment { -fx-fill: cadetblue; }
.paragraph-box:has-caret { -fx-background-color: #f2f9fc; }
|
核心工作流代码
需要注意的是,接下来的代码是在继承自CodeArea的子类中实现的,其中的this关键字指代的其实是CodeArea对象。
接下来就是实现代码的高亮功能。这一功能实现需要使用到ExecutorService类来处理多线程任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| CodeArea codeArea = new CodeArea(); ExecutorService executor = Executors.newSingleThreadExecutor(); Subscription cleanupWhenDone = codeArea.multiPlainChanges().successionEnds(Duration.ofMillis(500)) .retainLatestUntilLater(executor) .supplyTask(this::computeHighlightingAsync) .awaitLatest(this.multiPlainChanges()) .filterMap(t -> { if (t.isSuccess()) { return Optional.of(t.get()); } else { t.getFailure().printStackTrace(); return Optional.empty(); } }) .subscribe(this::applyHighlighting);
this.getStylesheets().add(getClass().getResource("keywords.css").toExternalForm());
|
这段代码使用了RxJava库来处理CodeArea中文本的异步高亮显示。
RxJava 是一个用于构建异步、事件驱动的程序的库,它提供了一套操作符来处理数据流。
codeArea.multiPlainChanges()这个方法每当CodeArea的文本内容发生变化时会触发一个事件。
successionEnds(Duration.ofMillis(500))设置了500毫秒的延迟,确保在连续的文本变化之间有足够的间隔,即若500毫秒内文本内容没有发生变化,则触发事件。
retainLatestUntilLater(executor)则用于保留数据流中的最新事件,直到下一个事件到来。
supplyTask(this::computeHighlightingAsync)则会创建一个异步任务,用于计算高亮显示的结果。this::computeHighlightingAsync方法需要实现对CodeArea文本内容的处理逻辑。
awaitLatest(this.multiPlainChanges())负责在computeHighlightingAsync任务执行期间,等待最新的文本变化事件。如果这段时间有新的文本变化时间,它会取消当前的任务并开始执行新的任务。确保不会同时执行多个任务。
filterMap(t -> {...})则用于过滤和映射事件,只保留成功的事件,并将它们传递给applyHighlighting方法进行处理。
subscribe(this::applyHighlighting)则用于订阅事件流,并在事件发生时执行相应的操作。this::applyHighlighting方法需要实现对高亮显示结果的处理逻辑。
this.getStylesheets().add(getClass().getResource("keywords.css").toExternalForm());将keywords.css文件添加到CodeArea的样式表中,以便在文本高亮显示时应用相应的样式。
通过以上代码,可以实现对CodeArea中文本的异步高亮显示。
其中额外涉及到的两个方法,computeHighlightingAsync负责对CodeArea中的文本进行分析,给出需要高亮的文本分割;而applyHighlighting负责对分割后的文本进行高亮显示。
业务逻辑执行方法
computeHighlightingAsync
computeHighlightingAsync方法执行异步任务,而其中的computeHighlighting方法则负责对CodeArea中的文本进行分析,给出需要高亮的文本分割,这一过程中使用了正则表达式相关的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| private Task<StyleSpans<Collection<String>>> computeHighlightingAsync() { String text = this.getText(); Task<StyleSpans<Collection<String>>> task = new Task<StyleSpans<Collection<String>>>() { @Override protected StyleSpans<Collection<String>> call() throws Exception { return computeHighlighting(text); } }; executor.execute(task); return task; } private static 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; 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(); }
|
applyHighlighting
依据传递的参数设置样式,不过多赘述。
1 2 3
| private void applyHighlighting(StyleSpans<Collection<String>> highlighting) { this.setStyleSpans(0, highlighting); }
|