QScintilla中的色素

BPL*_*BPL 12 python scintilla pygments qscintilla pyqt5

考虑这个mcve:

import math
import sys
import textwrap
import time
from pathlib import Path
from collections import defaultdict

from PyQt5.Qsci import QsciLexerCustom, QsciScintilla
from PyQt5.Qt import *

from pygments import lexers, styles, highlight, formatters
from pygments.lexer import Error, RegexLexer, Text, _TokenType
from pygments.style import Style


EXTRA_STYLES = {
    "monokai": {
        "background": "#272822",
        "caret": "#F8F8F0",
        "foreground": "#F8F8F2",
        "invisibles": "#F8F8F259",
        "lineHighlight": "#3E3D32",
        "selection": "#49483E",
        "findHighlight": "#FFE792",
        "findHighlightForeground": "#000000",
        "selectionBorder": "#222218",
        "activeGuide": "#9D550FB0",
        "misspelling": "#F92672",
        "bracketsForeground": "#F8F8F2A5",
        "bracketsOptions": "underline",
        "bracketContentsForeground": "#F8F8F2A5",
        "bracketContentsOptions": "underline",
        "tagsOptions": "stippled_underline",
    }
}


def convert_size(size_bytes):
    if size_bytes == 0:
        return "0B"
    size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
    i = int(math.floor(math.log(size_bytes, 1024)))
    p = math.pow(1024, i)
    s = round(size_bytes / p, 2)
    return f"{s} {size_name[i]}"


class ViewLexer(QsciLexerCustom):

    def __init__(self, lexer_name, style_name):
        super().__init__()

        # Lexer + Style
        self.pyg_style = styles.get_style_by_name(style_name)
        self.pyg_lexer = lexers.get_lexer_by_name(lexer_name, stripnl=False)
        self.cache = {
            0: ('root',)
        }
        self.extra_style = EXTRA_STYLES[style_name]

        # Generate QScintilla styles
        self.font = QFont("Consolas", 8, weight=QFont.Bold)
        self.token_styles = {}
        index = 0
        for k, v in self.pyg_style:
            self.token_styles[k] = index
            if v.get("color", None):
                self.setColor(QColor(f"#{v['color']}"), index)
            if v.get("bgcolor", None):
                self.setPaper(QColor(f"#{v['bgcolor']}"), index)

            self.setFont(self.font, index)
            index += 1

    def defaultPaper(self, style):
        return QColor(self.extra_style["background"])

    def language(self):
        return self.pyg_lexer.name

    def get_tokens_unprocessed(self, text, stack=('root',)):
        """
        Split ``text`` into (tokentype, text) pairs.

        ``stack`` is the inital stack (default: ``['root']``)
        """
        lexer = self.pyg_lexer
        pos = 0
        tokendefs = lexer._tokens
        statestack = list(stack)
        statetokens = tokendefs[statestack[-1]]
        while 1:
            for rexmatch, action, new_state in statetokens:
                m = rexmatch(text, pos)
                if m:
                    if action is not None:
                        if type(action) is _TokenType:
                            yield pos, action, m.group()
                        else:
                            for item in action(lexer, m):
                                yield item
                    pos = m.end()
                    if new_state is not None:
                        # state transition
                        if isinstance(new_state, tuple):
                            for state in new_state:
                                if state == '#pop':
                                    statestack.pop()
                                elif state == '#push':
                                    statestack.append(statestack[-1])
                                else:
                                    statestack.append(state)
                        elif isinstance(new_state, int):
                            # pop
                            del statestack[new_state:]
                        elif new_state == '#push':
                            statestack.append(statestack[-1])
                        else:
                            assert False, "wrong state def: %r" % new_state
                        statetokens = tokendefs[statestack[-1]]
                    break
            else:
                # We are here only if all state tokens have been considered
                # and there was not a match on any of them.
                try:
                    if text[pos] == '\n':
                        # at EOL, reset state to "root"
                        statestack = ['root']
                        statetokens = tokendefs['root']
                        yield pos, Text, u'\n'
                        pos += 1
                        continue
                    yield pos, Error, text[pos]
                    pos += 1
                except IndexError:
                    break

    def highlight_slow(self, start, end):
        style = self.pyg_style
        view = self.editor()
        code = view.text()[start:]
        tokensource = self.get_tokens_unprocessed(code)

        self.startStyling(start)
        for _, ttype, value in tokensource:
            self.setStyling(len(value), self.token_styles[ttype])

    def styleText(self, start, end):
        view = self.editor()
        t_start = time.time()
        self.highlight_slow(start, end)
        t_elapsed = time.time() - t_start
        len_text = len(view.text())
        text_size = convert_size(len_text)
        view.setWindowTitle(f"Text size: {len_text} - {text_size} Elapsed: {t_elapsed}s")

    def description(self, style_nr):
        return str(style_nr)


class View(QsciScintilla):

    def __init__(self, lexer_name, style_name):
        super().__init__()
        view = self

        # -------- Lexer --------
        self.setEolMode(QsciScintilla.EolUnix)
        self.lexer = ViewLexer(lexer_name, style_name)
        self.setLexer(self.lexer)

        # -------- Shortcuts --------
        self.text_size = 1
        self.s1 = QShortcut(f"ctrl+1", view, self.reduce_text_size)
        self.s2 = QShortcut(f"ctrl+2", view, self.increase_text_size)
        # self.gen_text()

        # # -------- Multiselection --------
        self.SendScintilla(view.SCI_SETMULTIPLESELECTION, True)
        self.SendScintilla(view.SCI_SETMULTIPASTE, 1)
        self.SendScintilla(view.SCI_SETADDITIONALSELECTIONTYPING, True)

        # -------- Extra settings --------
        self.set_extra_settings(EXTRA_STYLES[style_name])

    def get_line_separator(self):
        m = self.eolMode()
        if m == QsciScintilla.EolWindows:
            eol = '\r\n'
        elif m == QsciScintilla.EolUnix:
            eol = '\n'
        elif m == QsciScintilla.EolMac:
            eol = '\r'
        else:
            eol = ''
        return eol

    def set_extra_settings(self, dct):
        self.setIndentationGuidesBackgroundColor(QColor(0, 0, 255, 0))
        self.setIndentationGuidesForegroundColor(QColor(0, 255, 0, 0))

        if "caret" in dct:
            self.setCaretForegroundColor(QColor(dct["caret"]))

        if "line_highlight" in dct:
            self.setCaretLineBackgroundColor(QColor(dct["line_highlight"]))

        if "brackets_background" in dct:
            self.setMatchedBraceBackgroundColor(QColor(dct["brackets_background"]))

        if "brackets_foreground" in dct:
            self.setMatchedBraceForegroundColor(QColor(dct["brackets_foreground"]))

        if "selection" in dct:
            self.setSelectionBackgroundColor(QColor(dct["selection"]))

        if "background" in dct:
            c = QColor(dct["background"])
            self.resetFoldMarginColors()
            self.setFoldMarginColors(c, c)

    def increase_text_size(self):
        self.text_size *= 2
        self.gen_text()

    def reduce_text_size(self):
        if self.text_size == 1:
            return
        self.text_size //= 2
        self.gen_text()

    def gen_text(self):
        content = Path(__file__).read_text()
        while len(content) < self.text_size:
            content *= 2
        self.setText(content[:self.text_size])


if __name__ == '__main__':
    app = QApplication(sys.argv)
    view = View("python", "monokai")
    view.setText(textwrap.dedent("""\
        '''
        Ctrl+1 = You'll decrease the size of existing text
        Ctrl+2 = You'll increase the size of existing text

        Warning: Check the window title to see how long it takes rehighlighting
        '''
    """))
    view.resize(800, 600)
    view.show()
    app.exec_()
Run Code Online (Sandbox Code Playgroud)

要运行它,您需要安装:

QScintilla==2.10.8
Pygments==2.3.1
PyQt5==5.12
Run Code Online (Sandbox Code Playgroud)

我试图弄清楚如何在QScintilla小部件上使用pygments,现在我需要解决的主要问题是处理非微小文档时的性能

我希望编辑器在处理大型文档(> = 100kb)时变得敏感并且可用,但是我不太清楚在这里应该采用什么方法。为了测试性能,您可以使用Ctrl+ 1Ctrl+ 2,小部件文本将分别减少/增加。

当我说“响应”时,是指可见屏幕的突出显示计算不再需要[1-2]帧/高光<=> [17-34] ms /高光(假设60fps),因此键入时您不会不要感到任何放缓。

注意:如您在上面的mcve中所见,我已经包含了pygments标记生成器,因此您可以使用它...感觉是为了实现“实时突出显示”,我需要使用备忘录/缓存以某种聪明的方式,但我正在努力找出需要缓存的数据是什么,以及缓存它的最佳方法是什么...:/

演示:

在此处输入图片说明

在上面的演示中,您可以看到使用此幼稚的功能突出显示编辑器很快将无法使用,在我的笔记本电脑中,突出显示32kb的文本块仍可提供交互式帧速率,但高于该值的编辑器将变得完全不可用。

注意事项:

  • 当您在可见屏幕上无选择地键入/编码时,将发生最典型的情况
  • 您可能正在编辑分布在整个文档中的多个选择,这意味着您将不知道这些选择是否在可见屏幕附近。例如,在Sublime中,当您按下时Alt+F3,选择光标下的所有事件
  • 在上面的代码片段中,我使用了python词法分析器,但是该算法不应过多地关注该代码。Pygments毕竟支持约300个词法分析器
  • 最坏的情况是,如果可见屏幕位于文件末尾,而其中一个选项恰好位于屏幕开始处...如果您需要突出显示整个文档,则需要找到一个另一种方法,即使这意味着“突出显示”在第一次通过时不正确
  • 最重要的是性能,同时还要保证正确性……也就是说,如果您有足够的时间,整个文档应该正确地突出显示

参考资料:

以下文档不是特定于此特定问题的,但它们讨论了缓存和语法突出显示的可能策略:

Nat*_*eks 19

在中highlight_slow,您正在接收startend值,但是您忽略了最终值。如此一来,只要您键入一个字符,该代码就会重新突出显示整个缓冲区。这就是为什么,如果您在长缓冲区的末尾键入,则计时非常快-大约.1-.2 ms-但是如果您在开始处键入,则非常慢。

在大多数情况下(至少使用Python),仅在正确突出显示时考虑,当您引入新字符时,仅需要重新设置当前行的样式。有时,例如,如果您开始一个函数定义或打开一个括号,可能需要设置多行样式。仅当您打开或关闭多行"""'''字符串时,才需要重新设置缓冲区的其余部分的样式。

如果在日志记录中包含start和,则end在大多数情况下,键入时它们会覆盖很小的范围。如果您将highlight_code方法的一行更改为

code = view.text()[start:]
Run Code Online (Sandbox Code Playgroud)

code = view.text()[start:end]
Run Code Online (Sandbox Code Playgroud)

您会发现该方法现在几乎总是花费不到一毫秒的时间,并且几乎总是能够正确显示突出显示。

据我所知,这仅在涉及多行引号时才导致样式错误。但是,您当前的代码存在相同的问题:尝试打开多行字符串,键入enter,然后在下一行继续该字符串。第二行将突出显示为代码。Qscintilla通过给出一个start不包含多行引号开头的,使您误入歧途。不过,这并不是在试图做到完美-文档说

实际上,QScintilla说:“嘿,我认为您应该重新设置位置起始字符到位置末尾字符之间的文本样式”。您完全可以忽略此建议。

正确处理多行引用会有些棘手!如果是我,并且想要快速工作,我可能希望通过按键来刷新整个缓冲区的突出显示,并在出现问题时使用它。

  • 您说您需要处理的主要问题是性能。我建议的更改使您的代码可以快速使用,而不会使其行为更不正确。您的问题中没有提到多行问题,这只是我注意到的问题。如果您需要帮助弄清楚如何用多种语言,编辑器尚不具备的功能(例如多选)进行更好的高亮显示,建议将这些因素添加到您的问题中。 (21认同)
  • 我一直在想那个问题!我同意我的答案没有解决您真正想要的问题,尽管我仍然认为这不是对原始问题的不好回答。周末我一直在做更多的事情,我有一些想法,但是在赏金到期之前,我没有时间将它们变成有用的形式。我确实计划在一周中花更多时间,我将用我的想法更新答案,但我不能保证最终结果会满足您的要求。 (2认同)