You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Java RichTextFX的CodeArea行指示器处添加折叠展开图标并实现代码折叠

使用RichTextFX CodeArea实现括号/尖括号代码折叠功能

核心思路

实现IDE风格的括号折叠需要三个核心环节:

  • 解析成对的括号/尖括号,定位折叠块的起止行
  • 自定义行指示器(Gutter)绘制折叠/展开图标,处理点击交互
  • 基于折叠状态动态控制CodeArea的段落可见性

关键代码实现

1. 括号匹配工具类

用于定位指定位置的匹配括号,返回折叠块的起止行号:

import org.fxmisc.richtext.CodeArea;
import java.util.Stack;

public class BracketMatcher {
    private static final char[][] BRACKET_PAIRS = {{'(', ')'}, {'[', ']'}, {'{', '}'}, {'<', '>'}};

    public static int[] findMatchingBlock(CodeArea codeArea, int caretPosition) {
        char currentChar = codeArea.getText().charAt(caretPosition);
        char matchingChar = getMatchingChar(currentChar);
        if (matchingChar == 0) return null;

        Stack<Integer> bracketStack = new Stack<>();
        String fullText = codeArea.getText();
        int startLine = -1, endLine = -1;

        if (isOpeningBracket(currentChar)) {
            bracketStack.push(caretPosition);
            for (int i = caretPosition + 1; i < fullText.length(); i++) {
                char c = fullText.charAt(i);
                if (c == currentChar) bracketStack.push(i);
                else if (c == matchingChar) {
                    bracketStack.pop();
                    if (bracketStack.isEmpty()) {
                        startLine = codeArea.getCurrentParagraph();
                        endLine = codeArea.offsetToParagraph(i);
                        break;
                    }
                }
            }
        } else if (isClosingBracket(currentChar)) {
            bracketStack.push(caretPosition);
            for (int i = caretPosition - 1; i >= 0; i--) {
                char c = fullText.charAt(i);
                if (c == currentChar) bracketStack.push(i);
                else if (c == matchingChar) {
                    bracketStack.pop();
                    if (bracketStack.isEmpty()) {
                        startLine = codeArea.offsetToParagraph(i);
                        endLine = codeArea.getCurrentParagraph();
                        break;
                    }
                }
            }
        }
        return bracketStack.isEmpty() ? new int[]{startLine, endLine} : null;
    }

    private static char getMatchingChar(char c) {
        for (char[] pair : BRACKET_PAIRS) {
            if (pair[0] == c) return pair[1];
            if (pair[1] == c) return pair[0];
        }
        return 0;
    }

    private static boolean isOpeningBracket(char c) {
        for (char[] pair : BRACKET_PAIRS) {
            if (pair[0] == c) return true;
        }
        return false;
    }

    private static boolean isClosingBracket(char c) {
        for (char[] pair : BRACKET_PAIRS) {
            if (pair[1] == c) return true;
        }
        return false;
    }
}

2. 自定义折叠行指示器

扩展RichTextFX的Gutter组件,实现折叠图标绘制与交互:

import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.Gutter;
import org.fxmisc.richtext.GutterRow;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;

import java.util.HashMap;
import java.util.Map;

public class FoldGutter {
    private final Map<Integer, Boolean> foldStates = new HashMap<>();
    private final CodeArea codeArea;

    public FoldGutter(CodeArea codeArea) {
        this.codeArea = codeArea;
        setupGutter();
    }

    private void setupGutter() {
        Gutter<CodeArea> gutter = codeArea.getGutter();
        gutter.setFactory(this::createFoldableGutterRow);
    }

    private GutterRow createFoldableGutterRow(int paragraphIndex) {
        GutterRow row = new GutterRow();
        if (isFoldStartLine(paragraphIndex)) {
            Label foldIcon = createFoldToggleIcon(paragraphIndex);
            row.getChildren().add(foldIcon);
        }
        return row;
    }

    private boolean isFoldStartLine(int paragraphIndex) {
        String lineText = codeArea.getParagraph(paragraphIndex).getText().trim();
        return lineText.endsWith("(") || lineText.endsWith("[") 
            || lineText.endsWith("{") || lineText.endsWith("<");
    }

    private Label createFoldToggleIcon(int startLine) {
        Label icon = new Label();
        updateFoldIcon(icon, foldStates.getOrDefault(startLine, false));
        
        icon.setOnMouseClicked(e -> {
            boolean isFolded = foldStates.getOrDefault(startLine, false);
            foldStates.put(startLine, !isFolded);
            updateFoldIcon(icon, !isFolded);
            applyFoldState(startLine, !isFolded);
        });

        icon.setStyle("-fx-cursor: hand;");
        return icon;
    }

    private void updateFoldIcon(Label icon, boolean isFolded) {
        if (isFolded) {
            Polygon expandIcon = new Polygon(0, 0, 8, 0, 4, 6);
            expandIcon.setFill(Color.GRAY);
            icon.setGraphic(expandIcon);
        } else {
            Polygon foldIcon = new Polygon(0, 0, 6, 4, 0, 8);
            foldIcon.setFill(Color.GRAY);
            icon.setGraphic(foldIcon);
        }
    }

    private void applyFoldState(int startLine, boolean isFolded) {
        int lineEndOffset = codeArea.getParagraph(startLine).getLength();
        int[] blockRange = BracketMatcher.findMatchingBlock(
            codeArea, codeArea.paragraphToOffset(startLine) + lineEndOffset
        );
        if (blockRange == null || blockRange[0] != startLine) return;

        int endLine = blockRange[1];
        for (int i = startLine + 1; i < endLine; i++) {
            codeArea.setParagraphVisible(i, !isFolded);
        }
        codeArea.getGutter().refresh();
    }
}

3. 初始化CodeArea并启用折叠功能

在UI初始化代码中集成折叠功能:

import org.fxmisc.richtext.CodeArea;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class CodeFoldDemo extends Application {
    @Override
    public void start(Stage stage) {
        CodeArea codeArea = new CodeArea();
        // 填充示例代码
        codeArea.appendText("public class FoldDemo {\n");
        codeArea.appendText("    public void testMethod() {\n");
        codeArea.appendText("        String[] arr = {\"a\", \"b\", \"c\"};\n");
        codeArea.appendText("        if (arr.length > 0) {\n");
        codeArea.appendText("            for (String s : arr) {\n");
        codeArea.appendText("                System.out.println(s);\n");
        codeArea.appendText("            }\n");
        codeArea.appendText("        }\n");
        codeArea.appendText("    }\n");
        codeArea.appendText("}\n");

        // 绑定折叠 gutter
        new FoldGutter(codeArea);

        StackPane root = new StackPane(codeArea);
        Scene scene = new Scene(root, 800, 600);
        stage.setScene(scene);
        stage.setTitle("Code Fold Demo");
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

优化提示

  • 简化版的括号匹配逻辑未处理字符串、注释中的括号,实际项目可结合语法解析器(如ANTLR)优化匹配精度
  • 可扩展折叠状态持久化功能,通过保存foldStates实现重启后恢复折叠状态
  • 嵌套折叠场景下可调整图标样式,区分不同层级的折叠块

内容的提问来源于stack exchange,提问作者garawaa garawaa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 06:50:28