Пигменты в QScintilla

Рассмотрим этот 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_()

Для запуска необходимо установить:

QScintilla==2.10.8
Pygments==2.3.1
PyQt5==5.12

Я пытаюсь понять, как использовать пигменты в виджете QScintilla, и сейчас основная проблема, которую мне нужно решить, — это производительность при работе с немаленькими документами.

Я хотел бы, чтобы редактор стал отзывчивым и удобным при работе с большими документами (> = 100 КБ), но я не очень хорошо знаю, какой подход я должен использовать здесь. Для проверки производительности вы можете использовать Ctrl+1 или Ctrl+2, и текст виджета будет уменьшен. /увеличилась соответственно.

Когда я говорю «отзывчивый», я имею в виду, что вычисление подсветки видимого экрана должно занимать не более [1-2]кадр/выделение ‹=> [17-34]мс/выделение (при 60fps ), поэтому при наборе текста вы не почувствуете замедления.

Примечание. Как вы можете видеть в приведенном выше mcve, я включил токенизатор pygments, чтобы вы могли поиграть с ним... похоже, что для достижения «выделения в реальном времени» мне нужно использовать мемоизацию/кеширование. каким-то умным способом, но я изо всех сил пытаюсь понять, какие данные мне нужно кэшировать и как лучше всего их кэшировать...:/

Демо:

введите описание изображения здесь

В приведенной выше демонстрации вы можете видеть, что с помощью этого наивного выделения редактор очень скоро станет непригодным для использования, в моем ноутбуке повторное выделение текстовых фрагментов размером 32 КБ все еще дает интерактивную частоту кадров, но с чем-то большим, чем это, редактор становится совершенно непригодным для использования.

СООБРАЖЕНИЯ:

  • Наиболее типичный случай произойдет, когда вы печатаете/кодируете на видимом экране без выбора.
  • Может случиться так, что вы редактируете несколько выделений, разбросанных по всему документу, что означает, что вы не будете знать, находятся ли эти выделения рядом с видимым экраном или нет. Например, в Sublime, когда вы нажимаете Alt+F3, вы выбираете все вхождения под курсором.
  • В приведенном выше фрагменте я использовал лексер Python, но алгоритм не должен слишком сильно фокусироваться на нем. В конце концов, пигменты поддерживают ~300 лексеров.
  • Наихудший сценарий может произойти, если видимый экран находится в конце файла, а один из выбранных элементов находится в начале экрана... Если вам нужно повторно выделить весь документ, вам нужно будет найти альтернативный способ, даже если это означает, что «подсветка» неверна при первом проходе
  • Наиболее важным является производительность, но также и правильность... то есть, если вы уделите достаточно времени, весь документ должен быть правильно выделен.

ИСПОЛЬЗОВАННАЯ ЛИТЕРАТУРА:

Следующие документы не относятся к этой конкретной проблеме, но в них рассказывается о возможных стратегиях кэширования и подсветки синтаксиса:


person BPL    schedule 20.04.2019    source источник
comment
Связано: code.visualstudio.com/blogs/2017/02/ 08/ рассказывает, как работает эффективная подсветка синтаксиса.   -  person ivan_pozdeev    schedule 27.04.2019
comment
Просто к сведению: этот вопрос обсуждается на Meta, поэтому он может привлечь больше внимания, чем обычно.   -  person Makoto    schedule 27.04.2019
comment
Худший случай всегда будет отстой. Либо вам придется смириться с этим и работать над типичными случаями (и/), либо найти способ выполнять синтаксический анализ асинхронно, чтобы не блокировать пользовательский интерфейс. Что касается последнего, я не уверен, насколько велика поддержка Scintilla, но я бы рассмотрел ее, если вы этого не сделали. Что касается первого, @Nathan уже обратился к нему: вам нужно перестать игнорировать параметр end; это предусмотрено именно по таким причинам.   -  person user541686    schedule 29.04.2019
comment
@Mehrdad Причина, по которой я никогда не рассматривал возможность использования параметра end, заключается в том, что природа автоматов Pushdown, таких как pygments, движки на основе синтаксиса textmate. Scintilla не может правильно предложить параметр end, потому что он не знает, что это за конец, просто ответ @Nathan плохой / недействительный, и он даст неверные результаты, подумайте об этом ... независимо от того, если вы' при работе с одиночным или множественным выбором потенциально может быть повторно выделен весь документ, и конечный параметр не будет информировать об этом. Вы говорите, что в худшем случае всегда будет отстой, но SublimeText прекрасно работает :/   -  person BPL    schedule 29.04.2019
comment
@BPL: я не понимал, что end указано неправильно, но разве end не должно быть просто самым правым символом в последней видимой строке на экране? Вам не нужно думать ни о чем, кроме того, чтобы выделить то, что на экране; вы могли бы также не рассматривать ничего, кроме того, что выходит. Худший случай, который я себе представлял, - это когда начало документа выбрано, а затем изменено, когда вы прокрутили до конца, а затем что-то удалили. Я не знаю, что делает Sublime, но в этот момент просто невозможно обойти синтаксический анализ всего документа. Но это должно быть редко.   -  person user541686    schedule 29.04.2019
comment
@BPL Я объяснил сейчас. Я думаю, что мое решение решает проблемы, присущие Натану, и немного меньше проблем, присущих вам. Однако это требует немного больше усилий по разработке, и, к сожалению, у меня нет инструментов для этого.   -  person wizzwizz4    schedule 29.04.2019
comment
Поскольку это явно не минимальный пример, я думаю, что он лучше подходит для codereview.stackexchange.com.   -  person Greg Schmit    schedule 29.04.2019
comment
@GregSchmit: мне это не совсем понятно. Также это не требует проверки кода.   -  person user541686    schedule 30.04.2019
comment
@BPL Я прочитал это как оптимизацию этого конкретного кода (который абсолютно требует проверки кода), а не общий вопрос алгоритма. Если это то, в чем заключается настоящий вопрос, то код должен быть значительно минимизирован. Дело в том, что это не так, поэтому некоторым людям кажется, что вы просто просите их написать код для вас. Лучший ответ, который вы назвали хакерским, но это только потому, что вы слишком многого хотите от одного вопроса / ответа на SO. Вот почему его следует свести к минимуму, а вопрос ограничить по объему, или он должен быть на codereview.   -  person Greg Schmit    schedule 30.04.2019
comment
В чем здесь собственно вопрос? Я не нахожу ни одного вопросительного знака в тексте вопроса. Возможно, просто добавьте абзац типа Вопрос: ...о чем вы спрашиваете?   -  person hyde    schedule 30.04.2019


Ответы (2)


В highlight_slow вы получаете значения start и end, но игнорируете конечное значение. В результате каждый раз, когда вы вводите один символ, код повторно выделяет всю оставшуюся часть буфера. Вот почему, если вы печатаете в конце длинного буфера, время очень быстрое — около 0,1–0,2 мс, но если вы печатаете в начале, оно очень медленное.

Думая только с точки зрения правильного выделения, в большинстве случаев (по крайней мере, с Python) при вводе нового символа необходимо изменить стиль только текущей строки. Иногда, например, если вы начинаете определение функции или открываете скобку, может потребоваться стилизация нескольких строк. Только когда вы открываете или закрываете многострочную строку """ или ''', остальную часть буфера нужно будет менять.

Если вы включите start и end в свой журнал, вы увидите, что большую часть времени, когда вы вводите, они охватывают очень небольшой диапазон. Если вы измените одну строку вашего метода highlight_code с

code = view.text()[start:]

to

code = view.text()[start:end]

вы увидите, что метод теперь почти всегда занимает меньше миллисекунды, и он почти всегда правильно выделяет.

Из того, что я смог сказать, это неправильно стилизует только тогда, когда задействованы многострочные кавычки. Однако ваш текущий код имеет ту же проблему: попробуйте открыть многострочную строку, ввести Enter и продолжить строку на следующей строке. Вторая строка будет выделена как код. Qscintilla немного вводит вас в заблуждение, давая start, которое не включает начало многострочной кавычки. Однако он не пытается быть идеальным, говорят документы.

На самом деле QScintilla говорит: «Эй, я думаю, вам следует изменить стиль текста между символом в начальной позиции и символом в конце позиции». Вы можете полностью игнорировать это предложение.

Правильная обработка многострочных кавычек будет немного сложной! Если бы это был я, и я хотел бы, чтобы что-то работало быстро, я бы, вероятно, воспрепятствовал нажатию клавиши, чтобы обновить выделение для всего буфера и использовать это, когда что-то выглядит неправильно.

person Nathan Vērzemnieks    schedule 26.04.2019
comment
Вы сказали, что основная проблема, с которой вам нужно справиться, — это производительность. Изменение, которое я предложил, делает ваш код более быстрым, не заставляя его вести себя более неправильно. Многострочная проблема не упоминается в вашем вопросе, это просто то, что я заметил. Если вам нужна помощь в выяснении того, как сделать более качественную подсветку на нескольких языках с функциями, которых еще нет в вашем редакторе (например, множественный выбор), я предлагаю добавить эти факторы к вашему вопросу. - person Nathan Vērzemnieks; 26.04.2019
comment
Это интересная проблема! Оглядываясь назад, должно было быть очевидно, что вы не упустили что-то настолько очевидное, но - в мою защиту, мы часто упускаем очевидное ;) Я мог бы более подробно изучить ресурсы, которые вы указываете, на этих выходных, если у меня будет время. - person Nathan Vērzemnieks; 27.04.2019
comment
Я думал об этой самой проблеме! Я согласен с тем, что мой ответ не касается того, что вы действительно хотели, хотя я все еще думаю, что это не был плохой ответ на первоначальный вопрос. На выходных я поработал еще больше, и у меня есть несколько идей, но у меня не будет времени воплотить их в полезную форму до истечения срока действия награды. Я планирую уделить этому еще немного времени в течение недели, и я обновлю свой ответ тем, что придумаю, но я не могу обещать, что конечный результат вас удовлетворит :) - person Nathan Vērzemnieks; 29.04.2019
comment
Так что, похоже, я не ошибся :) . В конце концов, кажется, что этот неверный ответ был просто предназначен для того, чтобы заработать репо, а также заработать награды ... ну, я не виню вас, это доказывает, что в некоторых случаях SO каким-то образом нарушается. Тем не менее, если вы когда-нибудь снова заинтересуетесь этой темой и придумаете хороший правильный ответ, я с радостью вознагражу его 500 наградами... при этом мне сначала нужно будет подтвердить, что такой ответ удовлетворит меня. В любом случае, это был хороший опыт для меня, я не должен был давать столько щедрот на эту сложную тему, во-первых, мой плохой ;D - person BPL; 13.05.2019
comment
Я был немного ошеломлен, когда вы удалили все свои комментарии, и, честно говоря, меня это до сих пор беспокоит - удаление контекста из комментариев других людей делает их странными, и мотивация мне была непонятна. Так что я чувствовал себя немного менее склонным возвращаться к этому. На самом деле я потратил довольно много времени на это на следующей неделе, но это действительно сложная проблема :) Я могу опубликовать еще один ответ с описанием некоторых вещей, которые я нашел, если хотите. В частности, полагаться на метод styleText просто не получится. - person Nathan Vērzemnieks; 13.05.2019
comment
О, извините за это... Я не хотел беспокоить вас, удаляя все свои комментарии... Обычно моя политика заключается в том, что когда я создаю тему, я стараюсь максимально очистить ее от оффтопных комментариев, и я просто стараюсь оставлять онтопические комментарии. Обычно я предупреждаю пользователей, с которыми я общаюсь (через комментарии), сделать то же самое... в этом случае я забыл сделать то же самое здесь. Просто для протокола: я честен с намерением раздать еще 500 наград, но позвольте мне прояснить, я был бы довольно строг, оценивая ответ и проверяя его, прежде чем делать это... но дело в том, что мне очень интересна эта сложная тема-ветка;) - person BPL; 14.05.2019

Если вы хотите написать свою собственную подсветку синтаксиса, вот возможный способ значительно ускорить ее. Вы можете сделать это с Pygments, приложив немного усилий; см. внизу ответа один из возможных способов сделать это.

Подсветка синтаксиса проста. Он имеет небольшую внутреннюю структуру данных, представляющую текущий контекст, который он обновляет по мере продвижения. Итак, для следующего кода Python:

import time

def sleep_ms(ms):
    """sleeps for a length of time
    given in milliseconds"""

    time.sleep(
        ms / 1000
    )

sleep_ms(1000)
syntax error

его контекст может меняться следующим образом, когда он проходит через токены¹:

>>> [nothing]
>>> IMPORT
    IMPORT modulename
>>> [nothing]
>>> DEF
    DEF functionname
    DEF functionname, OPENPAREN
    DEF functionname, OPENPAREN
    DEF functionname ARGLIST
    DEF functionname ARGLIST COLON
>>> FUNCBODY 4s
    FUNCBODY 4s, DOUBLE_MLSTR
>>> FUNCBODY 4s, DOUBLE_MLSTR
    FUNCBODY 4s
>>> FUNCBODY 4s
>>> FUNCBODY 4s, varname
    FUNCBODY 4s, varname ATTR
    FUNCBODY 4s, varname ATTR attrname
    FUNCBODY 4s, varname ATTR attrname, OPENPAREN
>>> FUNCBODY 4s, varname ATTR attrname, OPENPAREN
>>> FUNCBODY 4s, varname ATTR attrname, OPENPAREN, varname
    FUNCBODY 4s, varname ATTR attrname, OPENPAREN, TRUEDIV varname
    FUNCBODY 4s, varname ATTR attrname, OPENPAREN, TRUEDIV varname intliteral
>>> FUNCBODY 4s, FUNCCALL
>>> FUNCBODY 4s
>>> [nothing]
    varname
    varname, OPENPAREN
    varname, OPENPAREN, intliteral
    FUNCCALL
>>> [nothing]
    varname
    ERROR

Если вы кешируете окончательные контексты каждой строки, вы можете начать подсветку синтаксиса в строке, которая изменилась, и продолжать работать, пока не дойдете до строки, где контекст такой же, как в кэше; вам не нужно пересчитывать весь файл, но если вы добавите что-то вроде """, он будет пересчитываться до конца. Если вы доберетесь до ERROR, вы можете просто остановиться там; нет смысла пересчитывать подсветку синтаксиса после синтаксической ошибки, потому что вы не знаете, что контекст имеет в виду. (Для начальной версии, когда вы открываете файл, вы можете предположить, что после синтаксической ошибки нет контекста; эта эвристика, кажется, работает достаточно хорошо.)

Этот подсветчик синтаксиса потенциально может быть смехотворно точным или просто «достаточно хорошим», практически без заметной разницы в скорости между ними. Подсветчики для конкретных языков могут даже быть динамически связанными плагинами, и это все равно будет достаточно быстро! Кроме того, если вы добавите debouncing для выделения последующих строк, достаточно быстрый ввод """""" будет таким же быстрым, как ввод "" или 42, независимо от размера файла.

Обратите внимание, что этот подсветчик является однопроходным — например, он не выделяет известные имена переменных иначе, чем неизвестные. Если вы хотите сделать это, проблема становится значительно сложнее.


¹: Этот пример выделения Python является «смехотворно точным»; Я, наверное, не пошел бы с чем-то вроде этого, если бы у меня было ограничение по времени. Тем не менее, я спланировал это в своей голове и — по крайней мере, сейчас — мог бы объяснить это подробно, если потребуется.


Ваш код требует на удивление мало изменений для работы с этой техникой.

  • Измените начало вашего get_tokens_unprocessed на:

        def get_tokens_unprocessed(self, text, stack=('root',), mutate_stack=False):
            """
            Split ``text`` into (tokentype, text) pairs.
    
            ``stack`` is the inital stack (default: ``['root']``)
            """
            lexer = self.pyg_lexer
            pos = 0
            tokendefs = lexer._tokens
            if not mutate_stack:
                statestack = list(stack)
            statetokens = tokendefs[statestack[-1]]
    
  • Найдите способ определить номер строки.
  • В цикле highlight_slow сделайте что-то вроде этого (только лучше):

            stack = list(self.cache[line_no_of(start)])
            tokensource = self.get_tokens_unprocessed(code, stack, True)
    
            self.startStyling(start)
            pos = start;
            for _, ttype, value in tokensource:
                self.setStyling(len(value), self.token_styles[ttype])
                pos += len(value)
                if is_line_end(pos):
                    if pos >= end and stack == self.cache[line_no_of(start)]:
                        break
                    self.cache[line_no_of(start)] = tuple(stack)
    

    Очевидно, что код должен быть лучше этого, и вам нужно найти какой-нибудь эффективный способ реализации is_line_end и line_no_of; вероятно, есть какой-то способ Pygments сделать это.

У этого решения есть как минимум одно преимущество перед вашим: оно поддерживает многострочные комментарии.

person wizzwizz4    schedule 28.04.2019
comment
@BPL Первый - замена Pygments. Ну, я полагаю, что технически вы можете взять любую подсветку синтаксиса, для которой у вас есть исходный код, и использовать ее, выгружая состояние переменных и загружая ее по мере необходимости. - person wizzwizz4; 28.04.2019
comment
Я не уверен, как уточнить. Какие части do вы понимаете? - person wizzwizz4; 28.04.2019
comment
@BPL Вы _можете реализовать устранение дребезга независимо от того (следовательно, выделено жирным шрифтом). Постараюсь разобраться и объяснить получше. - person wizzwizz4; 28.04.2019
comment
@БПЛ stack. stack kwarg — это контекст, о котором я говорю, и вы можете передать его в функцию. Вы лучше знакомы с библиотекой, чем я, и мой код почти наверняка не сработает. - person wizzwizz4; 28.04.2019
comment
Выделите строку за строкой и настройте get_tokens_unprocessed, чтобы каким-то образом вывести state в конце, чтобы вы могли сохранить его, а затем вернуть для следующей строки. Затем, после изменения, нужно только пересчитать подсветку для измененной строки и всех последующих строк, пока state не перестанет меняться. - person wizzwizz4; 28.04.2019