!121 增加代码高亮和代码折叠功能

Merge pull request !121 from Luke/release-v1.1.13
This commit is contained in:
Luke 2023-09-27 19:19:34 +00:00 committed by Gitee
commit c385b9429f
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
3 changed files with 250 additions and 11 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,18 +1,37 @@
package org.jcnc.jnotepad.ui.module; package org.jcnc.jnotepad.ui.module;
import javafx.application.Platform;
import javafx.geometry.Insets; 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.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.util.LogUtil;
import org.jcnc.jnotepad.views.manager.BottomStatusBoxManager; import org.jcnc.jnotepad.views.manager.BottomStatusBoxManager;
import org.jcnc.jnotepad.views.manager.CenterTabPaneManager; import org.jcnc.jnotepad.views.manager.CenterTabPaneManager;
import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTab; 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 org.slf4j.Logger;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; 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 +40,49 @@ import java.io.IOException;
* *
* @author luke * @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>" + KEYWORD_PATTERN + ")"
+ "|(?<PAREN>" + PAREN_PATTERN + ")"
+ "|(?<BRACE>" + BRACE_PATTERN + ")"
+ "|(?<BRACKET>" + BRACKET_PATTERN + ")"
+ "|(?<SEMICOLON>" + SEMICOLON_PATTERN + ")"
+ "|(?<STRING>" + STRING_PATTERN + ")"
+ "|(?<COMMENT>" + COMMENT_PATTERN + ")"
);
/** /**
* 用于记录日志的静态Logger对象 * 用于记录日志的静态Logger对象
@ -35,14 +95,153 @@ public class LineNumberTextArea extends StyleClassedTextArea {
* 用于创建 LineNumberTextArea 对象 * 用于创建 LineNumberTextArea 对象
*/ */
public LineNumberTextArea() { public LineNumberTextArea() {
// //
setPadding(new Insets(8, 0, 0, 0)); this.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.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 LineNumberTextArea.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<Collection<String>> computeHighlighting(String text) {
Matcher matcher = PATTERN.matcher(text);
int lastKwEnd = 0;
StyleSpansBuilder<Collection<String>> spansBuilder
= new StyleSpansBuilder<>();
while (matcher.find()) {
String styleClass = getStyleClass(matcher);
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 static String getStyleClass(Matcher matcher) {
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;
return styleClass;
}
static 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) {
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 static class DefaultContextMenu extends ContextMenu {
private final MenuItem fold;
private final MenuItem unfold;
private final MenuItem 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 +286,8 @@ public class LineNumberTextArea extends StyleClassedTextArea {
// 记录保存操作的日志信息 // 记录保存操作的日志信息
LogUtil.getLogger(this.getClass()).info("正在自动保存---"); LogUtil.getLogger(this.getClass()).info("正在自动保存---");
} catch (IOException ignored) { } catch (IOException ignored) {
// 如果发生IO异常记录忽的日志信息但不中断程序执行 // 如果发生IO异常记录忽的日志信息但不中断程序执行
LogUtil.getLogger(this.getClass()).info("已忽视IO异常!"); LogUtil.getLogger(this.getClass()).info("已忽视IO异常!");
} }
} }
} }

View File

@ -0,0 +1,40 @@
/* CSS样式注释 */
/* 标记关键字 */
.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; /* 设置文本颜色为军校蓝 */
}