能否在PyQt的QsciLexerCustom中结合Tree-sitter实现正确的语法高亮?
看起来你已经走对了方向——用Tree-sitter做语法分析确实比正则靠谱多了,但你的代码里有几个关键问题导致样式错乱,我来帮你梳理并修正:
核心问题分析
setStyling方法用法完全错误
QScintilla的setStyling(length, style)是从当前已设置的位置开始,往后设置length个字符的样式,而不是直接传入节点的结束byte偏移。你现在直接传end_byte,相当于每次都从起始位置开始设置几百上千个字符,必然导致样式重叠、错位。偏移量对应关系错误
Tree-sitter返回的节点byte偏移是相对于解析的文本片段,而QsciScintilla的startStyling(start)是相对于整个文档的偏移,两者没有做对应转换,导致样式应用到了错误的位置。节点类型判断太粗糙
比如你把整个function_definition节点都设为FUNCTION_DEF样式,但实际上只有其中的identifier子节点(函数名)需要高亮;call节点直接设为TYPES,会把整个函数调用(括号、参数全包括)都标成类型样式,显然不符合预期。
修正后的完整代码
我调整了核心逻辑,修复了上述问题,同时优化了节点匹配的精确性和性能:
import tree_sitter_python as PYTHON from tree_sitter import Parser, Node, Language from PyQt5.Qsci import QsciLexerCustom, QsciScintilla from PyQt5.QtGui import QColor, QFont class PythonLexer(QsciLexerCustom): DEFAULT = 0 KEYWORD = 1 TYPES = 2 STRING = 3 KEYARGS = 4 BRACKETS = 5 COMMENTS = 6 CONSTANTS = 7 FUNCTIONS = 8 CLASS_NAME = 9 FUNCTION_NAME = 10 def __init__(self, editor: QsciScintilla): super().__init__(editor, 'Python') self.editor = editor self.language_name = 'Python' # 样式基础配置 defaults = { 'color': '#ffffff', 'paper': '#1e1e1e', 'font': ('JetBrains Mono', 14) } self.setDefaultColor(QColor(defaults['color'])) self.setDefaultPaper(QColor(defaults['paper'])) self.setDefaultFont(QFont(defaults['font'][0], defaults['font'][1])) self.createStyle() # 初始化Tree-sitter解析器,缓存解析树避免重复解析 self.parser = Parser(Language(PYTHON.language())) self.last_tree = None self.last_full_text = b"" def language(self): return self.language_name def description(self, style): style_map = { self.DEFAULT: 'DEFAULT', self.KEYWORD: 'KEYWORD', self.TYPES: 'TYPES', self.STRING: 'STRING', self.KEYARGS: 'KWARGS', self.BRACKETS: 'BRACKETS', self.COMMENTS: 'COMMENTS', self.CONSTANTS: 'CONSTANTS', self.FUNCTIONS: 'FUNCTIONS', self.CLASS_NAME: 'CLASS_NAME', self.FUNCTION_NAME: 'FUNCTION_NAME' } return style_map.get(style, '') def createStyle(self): normal = QColor('#abb2bf') italic_font = QFont('JetBrains Mono', 14) italic_font.setItalic(True) self.setFont(italic_font, self.COMMENTS) self.setColor(normal, self.DEFAULT) self.setColor(normal, self.BRACKETS) self.setColor(QColor('#7f848e'), self.COMMENTS) self.setColor(QColor('#c678dd'), self.KEYWORD) self.setColor(QColor('#e5c07b'), self.CLASS_NAME) self.setColor(QColor('#61afef'), self.FUNCTION_NAME) self.setColor(QColor('#61afef'), self.FUNCTIONS) self.setColor(QColor('#56b6c2'), self.TYPES) self.setColor(QColor('#d19a66'), self.CONSTANTS) self.setColor(QColor('#98c379'), self.STRING) def styleText(self, start, end): self.startStyling(start) full_text = self.editor.text().encode('utf-8') # 仅当文档内容变化时重新解析 if full_text != self.last_full_text: self.last_tree = self.parser.parse(full_text) self.last_full_text = full_text tree = self.last_tree highlights = [] self.buildHighlights(tree.root_node, highlights) highlights.sort(key=lambda h: h[0]) current_pos = start for node_start, node_end, style in highlights: # 跳过已处理的区域 if node_end <= current_pos: continue # 填充中间未处理的默认样式 if node_start > current_pos: self.setStyling(node_start - current_pos, self.DEFAULT) current_pos = node_start # 应用当前节点的高亮样式 length = node_end - current_pos self.setStyling(length, style) current_pos = node_end # 处理剩余的默认样式 if current_pos < end: self.setStyling(end - current_pos, self.DEFAULT) def buildHighlights(self, node: Node, highlights: list): # 先递归处理子节点,避免父节点覆盖子节点样式 for child in node.children: self.buildHighlights(child, highlights) node_text = node.text.decode('utf-8') if hasattr(node, 'text') else '' style = None # 基础类型匹配 if node.type == 'comment': style = self.COMMENTS elif node.type == 'string': style = self.STRING elif node.type in ('integer', 'float', 'boolean', 'none'): style = self.CONSTANTS elif node.type in ('(', ')', '{', '}', '[', ']'): style = self.BRACKETS elif node.type == 'keyword': style = self.KEYWORD # 函数/类定义的名称 elif node.parent and node.parent.type == 'function_definition' and node.type == 'identifier': style = self.FUNCTION_NAME elif node.parent and node.parent.type == 'class_definition' and node.type == 'identifier': style = self.CLASS_NAME # 函数调用的名称(含内置函数) elif node.parent and node.parent.type == 'call' and node.type == 'identifier': style = self.FUNCTIONS # 关键字参数名 elif node.parent and node.parent.type == 'keyword_argument' and node.type == 'identifier': style = self.KEYARGS # 类型注解 elif node.type == 'type_annotation': style = self.TYPES # 有效节点才添加到高亮列表 if style and node.start_byte != node.end_byte: highlights.append((node.start_byte, node.end_byte, style))
关键改进说明
样式应用逻辑修复:
用current_pos跟踪当前已设置到的位置,先填充默认样式再应用高亮,确保每个位置都被正确覆盖,完全符合QsciScintilla的setStyling用法要求。精确的节点匹配:
只高亮函数/类定义中的名称节点,函数调用的名称节点,避免了整块区域被错误染色的问题。性能优化:
缓存解析树,仅当文档内容变化时重新解析,避免QsciScintilla频繁调用styleText时重复解析导致的卡顿。
进阶优化建议
- 使用Tree-sitter Query:可以用Tree-sitter的查询语法(类似CSS选择器)来批量匹配需要高亮的节点,替代硬编码的if/else,比如:
这样维护起来更灵活。query = Language(PYTHON.language()).query(""" (comment) @comment (string) @string (function_definition (identifier) @function.name) (class_definition (identifier) @class.name) """) - 增量解析:当文档有局部修改时,用Tree-sitter的
parser.edit方法更新解析树,而不是全量解析,进一步提升大文档的性能。 - 线程解析:把Tree-sitter的解析逻辑放到后台线程,避免阻塞UI线程导致编辑器卡顿。
内容来源于stack exchange

