From 03f663deba52d9fcb2b247143c5d10c9ce0c7e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E8=BD=B2?= Date: Thu, 28 Sep 2023 02:44:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AF=AD=E6=B3=95=E9=AB=98?= =?UTF-8?q?=E4=BA=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/module/LineNumberTextArea.java | 201 +++++++++++++++++- src/main/resources/css/java_code_styles.css | 27 +++ 2 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/css/java_code_styles.css diff --git a/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java b/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java index ff995bd..78a4a41 100644 --- a/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java +++ b/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java @@ -1,18 +1,38 @@ package org.jcnc.jnotepad.ui.module; +import javafx.application.Platform; import javafx.geometry.Insets; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import org.fxmisc.richtext.CodeArea; +import org.fxmisc.richtext.GenericStyledArea; import org.fxmisc.richtext.LineNumberFactory; import org.fxmisc.richtext.StyleClassedTextArea; +import org.fxmisc.richtext.model.Paragraph; +import org.fxmisc.richtext.model.StyleSpans; +import org.fxmisc.richtext.model.StyleSpansBuilder; import org.jcnc.jnotepad.util.LogUtil; import org.jcnc.jnotepad.views.manager.BottomStatusBoxManager; import org.jcnc.jnotepad.views.manager.CenterTabPaneManager; import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTab; +import org.reactfx.Subscription; +import org.reactfx.collection.ListModification; import org.slf4j.Logger; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * 行号文本区域 @@ -21,8 +41,41 @@ import java.io.IOException; * * @author luke */ -public class LineNumberTextArea extends StyleClassedTextArea { +public class LineNumberTextArea extends CodeArea { + 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)*?\\*/" // 用于整体文本处理(文本块) + + "|" + "/\\*[^\\v]*" + "|" + "^\\h*\\*([^\\v]*|/)"; // 用于可见段落处理(逐行) + + // 使用正则表达式将关键字、括号、分号、字符串和注释的模式组合成一个复合模式 + private static final Pattern PATTERN = Pattern.compile( + "(?" + KEYWORD_PATTERN + ")" + + "|(?" + PAREN_PATTERN + ")" + + "|(?" + BRACE_PATTERN + ")" + + "|(?" + BRACKET_PATTERN + ")" + + "|(?" + SEMICOLON_PATTERN + ")" + + "|(?" + STRING_PATTERN + ")" + + "|(?" + COMMENT_PATTERN + ")" + ); /** * 用于记录日志的静态Logger对象 @@ -35,14 +88,142 @@ public class LineNumberTextArea extends StyleClassedTextArea { * 用于创建 LineNumberTextArea 对象 */ public LineNumberTextArea() { - //上、右、下、左 - setPadding(new Insets(8, 0, 0, 0)); - setStyle("-fx-font-family: 'Courier New';"); - // 设置 LineNumberTextArea 的样式,包括边框和背景颜色 - getStyleClass().add("line-number-text-area"); - this.setParagraphGraphicFactory(LineNumberFactory.get(this)); - initListeners(); + // 上、右、下、左 + this.setPadding(new Insets(8, 0, 0, 0)); + // 在区域左侧添加行号 + this.setParagraphGraphicFactory(LineNumberFactory.get(this)); + this.setContextMenu(new DefaultContextMenu()); + + // 重新计算所有文本的语法高亮,用户停止编辑区域后的500毫秒内 + Subscription cleanupWhenNoLongerNeedIt = this + .multiPlainChanges() + .successionEnds(Duration.ofMillis(500)) + .subscribe(ignore -> this.setStyleSpans(0, computeHighlighting(this.getText()))); + + this.getVisibleParagraphs().addModificationObserver + ( + new JavaKeywordsDemo.VisibleParagraphStyler<>(this, this::computeHighlighting) + ); + + // 自动缩进:在按下回车键时插入上一行的缩进 + final Pattern whiteSpace = Pattern.compile("^\\s+"); + this.addEventHandler(KeyEvent.KEY_PRESSED, kE -> + { + if (kE.getCode() == KeyCode.ENTER) { + int caretPosition = this.getCaretPosition(); + int currentParagraph = this.getCurrentParagraph(); + Matcher m0 = whiteSpace.matcher(this.getParagraph(currentParagraph - 1).getSegments().get(0)); + if (m0.find()) { + Platform.runLater(() -> this.insertText(caretPosition, m0.group())); + } + } + }); + + initListeners(); + this.getStylesheets().add(Objects.requireNonNull(getClass().getResource("/css/java_code_styles.css")).toString()); + + } + + + private 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; /* 永远不会发生 */ + 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(); + } + + static class VisibleParagraphStyler implements Consumer>> { + private final GenericStyledArea area; + private final Function> computeStyles; + private int prevParagraph, prevTextLength; + + public VisibleParagraphStyler(GenericStyledArea area, Function> computeStyles) { + this.computeStyles = computeStyles; + this.area = area; + } + + @Override + public void accept(ListModification> lm) { + if (lm.getAddedSize() > 0) { + Platform.runLater(() -> + { + 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) { + if (paragraph < area.getParagraphs().size() - 1) { + int startPos = area.getAbsolutePosition(paragraph, 0); + area.setStyleSpans(startPos, computeStyles.apply(text)); + } + prevTextLength = text.length(); + prevParagraph = paragraph; + } + }); + } + } + } + + private class DefaultContextMenu extends ContextMenu { + private MenuItem fold, unfold, print; + + public DefaultContextMenu() { + fold = new MenuItem("折叠所选文本"); + fold.setOnAction(AE -> { + hide(); + fold(); + }); + + unfold = new MenuItem("从光标处展开"); + unfold.setOnAction(AE -> { + hide(); + unfold(); + }); + + print = new MenuItem("打印"); + print.setOnAction(AE -> { + hide(); + print(); + }); + + getItems().addAll(fold, unfold, print); + } + + /** + * 折叠多行所选文本,仅显示第一行并隐藏其余部分。 + */ + private void fold() { + ((CodeArea) getOwnerNode()).foldSelectedParagraphs(); + } + + /** + * 展开当前行/段落(如果有折叠)。 + */ + private void unfold() { + CodeArea area = (CodeArea) getOwnerNode(); + area.unfoldParagraphs(area.getCurrentParagraph()); + } + + private void print() { + System.out.println(((CodeArea) getOwnerNode()).getText()); + } } /** @@ -87,8 +268,8 @@ public class LineNumberTextArea extends StyleClassedTextArea { // 记录保存操作的日志信息 LogUtil.getLogger(this.getClass()).info("正在自动保存---"); } catch (IOException ignored) { - // 如果发生IO异常,记录忽略的日志信息,但不中断程序执行 + // 如果发生IO异常,记录忽视的日志信息,但不中断程序执行 LogUtil.getLogger(this.getClass()).info("已忽视IO异常!"); } } -} \ No newline at end of file +} diff --git a/src/main/resources/css/java_code_styles.css b/src/main/resources/css/java_code_styles.css new file mode 100644 index 0000000..bc3dddc --- /dev/null +++ b/src/main/resources/css/java_code_styles.css @@ -0,0 +1,27 @@ +.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; +} +