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

如何为QsciLexerCustom子类添加代码折叠功能?

为QsciLexerCustom子类添加代码折叠功能(以JSON Lexer为例)

要给自定义的QsciLexer子类添加代码折叠功能,核心是让Scintilla编辑器识别代码块的层级结构——也就是标记出哪些行是折叠块的起始(头)、结束(尾),以及每个块的嵌套深度。下面我会基于你提供的JSON lexer代码,一步步实现这个功能:

实现步骤

1. 理解Scintilla折叠的核心逻辑

Scintilla用**折叠级别(Fold Level)**定义每行的折叠状态,每个折叠级别由两部分组成:

  • 低16位:基础嵌套深度,以SC_FOLDLEVELBASE(默认值为0)为起点,每嵌套一层加1
  • 高16位:折叠标志,比如SC_FOLDLEVELHEADER表示这一行是折叠块的开头(点击可展开/折叠)

2. 修改LexerJson类,添加折叠级别计算逻辑

我们需要跟踪JSON中{}和[]的嵌套深度,为每行计算对应的折叠级别,并让lexer返回这些级别给编辑器。修改后的完整代码如下:

import sys
import textwrap
from PyQt5.Qt import * # noqa
from PyQt5.Qsci import QsciScintilla
from PyQt5.Qsci import QsciLexerCustom
from lark import Lark, inline_args, Transformer

class LexerJson(QsciLexerCustom):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.create_grammar()
        self.create_styles()
        self.fold_levels = []  # 存储每行的折叠级别
        # 初始化折叠级别
        if parent:
            self.compute_fold_levels()

    def create_styles(self):
        deeppink = QColor(249, 38, 114)
        khaki = QColor(230, 219, 116)
        mediumpurple = QColor(174, 129, 255)
        mediumturquoise = QColor(81, 217, 205)
        yellowgreen = QColor(166, 226, 46)
        lightcyan = QColor(213, 248, 232)
        darkslategrey = QColor(39, 40, 34)
        styles = {
            0: mediumturquoise,
            1: mediumpurple,
            2: yellowgreen,
            3: deeppink,
            4: khaki,
            5: lightcyan
        }
        for style, color in styles.items():
            self.setColor(color, style)
            self.setPaper(darkslategrey, style)
            self.setFont(self.parent().font(), style)
        self.token_styles = {
            "__COLON": 5,
            "__COMMA": 5,
            "__FALSE1": 0,
            "__LBRACE": 5,
            "__LSQB": 5,
            "__NULL2": 0,
            "__RBRACE": 5,
            "__RSQB": 5,
            "__TRUE0": 0,
            "ESCAPED_STRING": 4,
            "SIGNED_NUMBER": 1,
        }

    def create_grammar(self):
        grammar = '''
?start: value
?value: object
      | array
      | string
      | SIGNED_NUMBER -> number
      | "true" -> true
      | "false" -> false
      | "null" -> null

array : "[" [value ("," value)*] "]"
object : "{" [pair ("," pair)*] "}"
pair : string ":" value
string : ESCAPED_STRING

%import common.ESCAPED_STRING
%import common.SIGNED_NUMBER
%import common.WS
%ignore WS
'''
        class TreeToJson(Transformer):
            @inline_args
            def string(self, s):
                return s[1:-1].replace('\\"', '"')
            array = list
            pair = tuple
            object = dict
            number = inline_args(float)
            def null(self, _):
                return None
            def true(self, _):
                return True
            def false(self, _):
                return False
        self.lark = Lark(grammar, parser='lalr', transformer=TreeToJson())

    def defaultPaper(self, style):
        return QColor(39, 40, 34)

    def language(self):
        return "Json"

    def description(self, style):
        return {v: k for k, v in self.token_styles.items()}.get(style, "")

    def styleText(self, start, end):
        self.startStyling(start)
        text = self.parent().text()[start:end]
        last_pos = 0
        try:
            for token in self.lark.lex(text):
                ws_len = token.pos_in_stream - last_pos
                if ws_len:
                    self.setStyling(ws_len, 0)  # whitespace
                token_len = len(bytearray(token, "utf-8"))
                self.setStyling(
                    token_len, self.token_styles.get(token.type, 0))
                last_pos = token.pos_in_stream + token_len
        except Exception as e:
            print(e)
    
    # 新增:计算所有行的折叠级别
    def compute_fold_levels(self):
        text = self.parent().text()
        lines = text.split('\n')
        self.fold_levels = []
        current_depth = 0
        for line in lines:
            # 先处理闭合括号(对应上一层级的结束)
            close_count = line.count('}') + line.count(']')
            current_depth -= close_count
            
            # 标记折叠头:当前行有打开括号时,设置折叠标志
            fold_flag = 0
            open_count = line.count('{') + line.count('[')
            if open_count > 0:
                fold_flag |= QsciScintilla.SC_FOLDLEVELHEADER
            
            # 计算当前行的最终折叠级别
            level = QsciScintilla.SC_FOLDLEVELBASE + current_depth
            level |= fold_flag
            self.fold_levels.append(level)
            
            # 更新当前嵌套深度(处理打开括号)
            current_depth += open_count
    
    # 重写:返回指定行的折叠级别
    def foldLevel(self, line):
        if 0 <= line < len(self.fold_levels):
            return self.fold_levels[line]
        return QsciScintilla.SC_FOLDLEVELBASE

class EditorAll(QsciScintilla):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Set font defaults
        font = QFont()
        font.setFamily('Consolas')
        font.setFixedPitch(True)
        font.setPointSize(8)
        font.setBold(True)
        self.setFont(font)

        # Set margin defaults
        fontmetrics = QFontMetrics(font)
        self.setMarginsFont(font)
        self.setMarginWidth(0, fontmetrics.width("000") + 6)
        self.setMarginLineNumbers(0, True)
        self.setMarginsForegroundColor(QColor(128, 128, 128))
        self.setMarginsBackgroundColor(QColor(39, 40, 34))
        self.setMarginType(1, self.SymbolMargin)
        self.setMarginWidth(1, 12)

        # Set indentation defaults
        self.setIndentationsUseTabs(False)
        self.setIndentationWidth(4)
        self.setBackspaceUnindents(True)
        self.setIndentationGuides(True)

        # Set folding defaults
        self.setFolding(QsciScintilla.CircledFoldStyle)

        # Set caret defaults
        self.setCaretForegroundColor(QColor(247, 247, 241))
        self.setCaretWidth(2)

        # Set selection color defaults
        self.setSelectionBackgroundColor(QColor(61, 61, 52))
        self.resetSelectionForegroundColor()

        # Set multiselection defaults
        self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
        self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
        self.SendScintilla(
            QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)

        lexer = LexerJson(self)
        self.setLexer(lexer)
        # 新增:文本变化时重新计算折叠级别
        self.textChanged.connect(lexer.compute_fold_levels)

def main():
    app = QApplication(sys.argv)
    ex = EditorAll()
    ex.setWindowTitle(__file__)
    ex.setText(textwrap.dedent("""\
{
  "_id": "5b05ffcbcf8e597939b3f5ca",
  "about": "Excepteur consequat commodo esse voluptate aute aliquip ad sint deserunt commodo eiusmod irure. Sint aliquip sit magna duis eu est culpa aliqua excepteur ut tempor nulla. Aliqua ex pariatur id labore sit. Quis sit ex aliqua veniam exercitation laboris anim adipisicing. Lorem nisi reprehenderit ullamco labore qui sit ut aliqua tempor consequat pariatur proident.",
  "address": "665 Malbone Street, Thornport, Louisiana, 243",
  "age": 23,
  "balance": "$3,216.91",
  "company": "BULLJUICE",
  "email": "elisekelley@bulljuice.com",
  "eyeColor": "brown",
  "gender": "female",
  "guid": "d3a6d865-0f64-4042-8a78-4f53de9b0707",
  "index": 0,
  "isActive": false,
  "isActive2": true,
  "latitude": -18.660714,
  "longitude": -85.378048,
  "name": "Elise Kelley",
  "phone": "+1 (808) 543-3966",
  "picture": "http://placehold.it/32x32",
  "registered": "2017-09-30T03:47:40 -02:00",
  "tags": [
    "et",
    "nostrud",
    "in",
    "fugiat",
    "incididunt",
    "labore",
    "nostrud"
  ]
}\
"""))
    ex.resize(800, 600)
    ex.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

3. 关键逻辑说明

  • 折叠级别计算:通过统计每行的{/[(打开括号)和}/](闭合括号)数量跟踪嵌套深度,先处理闭合括号是因为它对应上一层级的结束。
  • 折叠头标记:如果当前行包含打开括号,就标记为SC_FOLDLEVELHEADER,编辑器会在该行左侧显示折叠图标。
  • 实时更新:通过textChanged信号触发重新计算,确保文本修改后折叠状态能及时同步。

修改完成后,你的JSON编辑器就能支持对象和数组的代码折叠了——点击左侧margin的圆圈图标即可展开/折叠对应的代码块。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:32:53