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

能否在PyQt的QsciLexerCustom中结合Tree-sitter实现正确的语法高亮?

能否在PyQt的QsciLexerCustom中结合Tree-sitter实现正确的语法高亮?

看起来你已经走对了方向——用Tree-sitter做语法分析确实比正则靠谱多了,但你的代码里有几个关键问题导致样式错乱,我来帮你梳理并修正:


核心问题分析

  1. setStyling方法用法完全错误
    QScintilla的setStyling(length, style)是从当前已设置的位置开始,往后设置length个字符的样式,而不是直接传入节点的结束byte偏移。你现在直接传end_byte,相当于每次都从起始位置开始设置几百上千个字符,必然导致样式重叠、错位。

  2. 偏移量对应关系错误
    Tree-sitter返回的节点byte偏移是相对于解析的文本片段,而QsciScintilla的startStyling(start)是相对于整个文档的偏移,两者没有做对应转换,导致样式应用到了错误的位置。

  3. 节点类型判断太粗糙
    比如你把整个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))

关键改进说明

  1. 样式应用逻辑修复:
    用current_pos跟踪当前已设置到的位置,先填充默认样式再应用高亮,确保每个位置都被正确覆盖,完全符合QsciScintilla的setStyling用法要求。

  2. 精确的节点匹配:
    只高亮函数/类定义中的名称节点,函数调用的名称节点,避免了整块区域被错误染色的问题。

  3. 性能优化:
    缓存解析树,仅当文档内容变化时重新解析,避免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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 11:19:29