如何为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
相关产品推荐
相关产品推荐

