Как выделить QScintilla с помощью ANTLR4?

Я пытаюсь изучить ANTLR4, и у меня уже возникли проблемы с моим первым экспериментом.

Цель здесь — научиться использовать ANTLR для подсветки синтаксиса компонента QScintilla. Чтобы немного попрактиковаться, я решил научиться правильно выделять *.ini файлы.

Перво-наперво, для запуска mcve вам понадобится:

  • Скачайте antlr4 и убедитесь, что он работает, читайте инструкцию на основном сайте
  • Установите среду выполнения python antlr, просто выполните: pip install antlr4-python3-runtime
  • Сгенерируйте лексер/парсер ini.g4:

    grammar ini;
    
    start : section (option)*;
    section : '[' STRING ']';
    option : STRING '=' STRING;
    
    COMMENT : ';'  ~[\r\n]*;
    STRING  : [a-zA-Z0-9]+;
    WS      : [ \t\n\r]+;
    

запустив antlr ini.g4 -Dlanguage=Python3 -o ini

  • Наконец, сохраните main.py:

    import textwrap
    
    from PyQt5.Qt import *
    from PyQt5.Qsci import QsciScintilla, QsciLexerCustom
    
    from antlr4 import *
    from ini.iniLexer import iniLexer
    from ini.iniParser import iniParser
    
    
    class QsciIniLexer(QsciLexerCustom):
    
        def __init__(self, parent=None):
            super().__init__(parent=parent)
    
            lst = [
                {'bold': False, 'foreground': '#f92472', 'italic': False},  # 0 - deeppink
                {'bold': False, 'foreground': '#e7db74', 'italic': False},  # 1 - khaki (yellowish)
                {'bold': False, 'foreground': '#74705d', 'italic': False},  # 2 - dimgray
                {'bold': False, 'foreground': '#f8f8f2', 'italic': False},  # 3 - whitesmoke
            ]
            style = {
                "T__0": lst[3],
                "T__1": lst[3],
                "T__2": lst[3],
                "COMMENT": lst[2],
                "STRING": lst[0],
                "WS": lst[3],
            }
    
            for token in iniLexer.ruleNames:
                token_style = style[token]
    
                foreground = token_style.get("foreground", None)
                background = token_style.get("background", None)
                bold = token_style.get("bold", None)
                italic = token_style.get("italic", None)
                underline = token_style.get("underline", None)
                index = getattr(iniLexer, token)
    
                if foreground:
                    self.setColor(QColor(foreground), index)
                if background:
                    self.setPaper(QColor(background), index)
    
        def defaultPaper(self, style):
            return QColor("#272822")
    
        def language(self):
            return self.lexer.grammarFileName
    
        def styleText(self, start, end):
            view = self.editor()
            code = view.text()
            lexer = iniLexer(InputStream(code))
            stream = CommonTokenStream(lexer)
            parser = iniParser(stream)
    
            tree = parser.start()
            print('parsing'.center(80, '-'))
            print(tree.toStringTree(recog=parser))
    
            lexer.reset()
            self.startStyling(0)
            print('lexing'.center(80, '-'))
            while True:
                t = lexer.nextToken()
                print(lexer.ruleNames[t.type-1], repr(t.text))
                if t.type != -1:
                    len_value = len(t.text)
                    self.setStyling(len_value, t.type)
                else:
                    break
    
        def description(self, style_nr):
            return str(style_nr)
    
    
    if __name__ == '__main__':
        app = QApplication([])
        v = QsciScintilla()
        lexer = QsciIniLexer(v)
        v.setLexer(lexer)
        v.setText(textwrap.dedent("""\
            ; Comment outside
    
            [section s1]
            ; Comment inside
            a = 1
            b = 2
    
            [section s2]
            c = 3 ; Comment right side
            d = e
        """))
        v.show()
        app.exec_()
    

и запустите его, если все прошло хорошо, вы должны получить такой результат:

витрина

Вот мои вопросы:

  • Как видите, результат демонстрации далек от того, чтобы его можно было использовать, вам определенно это не нужно, это действительно беспокоит. Вместо этого вы хотели бы получить такое же поведение, как и все IDE. К сожалению, я не знаю, как этого добиться, как бы вы изменили фрагмент, обеспечивающий такое поведение?
  • Прямо сейчас я пытаюсь имитировать выделение, аналогичное снимку ниже:

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

вы можете видеть на этом снимке экрана выделение по-разному при назначении переменных (переменная = темно-розовый и значения = желтоватый), но я не знаю, как этого добиться, я пытался использовать эту слегка измененную грамматику:

grammar ini;

start : section (option)*;
section : '[' STRING ']';
option : VARIABLE '=' VALUE;

COMMENT : ';'  ~[\r\n]*;
VARIABLE  : [a-zA-Z0-9]+;
VALUE  : [a-zA-Z0-9]+;
WS      : [ \t\n\r]+;

а затем изменить стили на:

style = {
    "T__0": lst[3],
    "T__1": lst[3],
    "T__2": lst[3],
    "COMMENT": lst[2],
    "VARIABLE": lst[0],
    "VALUE": lst[1],
    "WS": lst[3],
}

но если вы посмотрите на вывод лексирования, вы увидите, что не будет различия между VARIABLE и VALUES, потому что приоритет порядка в грамматике ANTLR. Итак, мой вопрос: как бы вы изменили грамматику/фрагмент, чтобы добиться такого внешнего вида?


person BPL    schedule 07.06.2019    source источник


Ответы (3)


Проблема в том, что лексер должен быть контекстно-зависимым: все, что находится слева от =, должно быть переменной, а справа от нее — значением. Вы можете сделать это, используя лексические режимы ANTLR< /а>. Вы начинаете с классификации последовательных не-пробелов как переменных, а встречая =, вы переходите в свой режим значений. Находясь внутри режима значения, вы выходите из этого режима всякий раз, когда сталкиваетесь с разрывом строки.

Обратите внимание, что лексические режимы работают только в грамматике лексера, а не в комбинированной грамматике, которая у вас есть. Кроме того, для подсветки синтаксиса вам, вероятно, понадобится только лексер.

Вот краткая демонстрация того, как это может работать (вставьте ее в файл с именем IniLexer.g4):

lexer grammar IniLexer;

SECTION
 : '[' ~[\]]+ ']'
 ;

COMMENT
 : ';' ~[\r\n]*
 ;

ASSIGN
 : '=' -> pushMode(VALUE_MODE)
 ;

KEY
 : ~[ \t\r\n]+
 ;

SPACES
 : [ \t\r\n]+ -> skip
 ;

UNRECOGNIZED
 : .
 ;

mode VALUE_MODE;

  VALUE_MODE_SPACES
   : [ \t]+ -> skip
   ;

  VALUE
   : ~[ \t\r\n]+
   ;

  VALUE_MODE_COMMENT
   : ';' ~[\r\n]* -> type(COMMENT)
   ;

  VALUE_MODE_NL
   : [\r\n]+ -> skip, popMode
   ;

Если вы сейчас запустите следующий скрипт:

source = """
; Comment outside

[section s1]
; Comment inside
a = 1
b = 2

[section s2]
c = 3 ; Comment right side
d = e
"""

lexer = IniLexer(InputStream(source))
stream = CommonTokenStream(lexer)
stream.fill()

for token in stream.tokens[:-1]:
    print("{0:<25} '{1}'".format(IniLexer.symbolicNames[token.type], token.text))

вы увидите следующий вывод:

COMMENT                   '; Comment outside'
SECTION                   '[section s1]'
COMMENT                   '; Comment inside'
KEY                       'a'
ASSIGN                    '='
VALUE                     '1'
KEY                       'b'
ASSIGN                    '='
VALUE                     '2'
SECTION                   '[section s2]'
KEY                       'c'
ASSIGN                    '='
VALUE                     '3'
COMMENT                   '; Comment right side'
KEY                       'd'
ASSIGN                    '='
VALUE                     'e'

А сопровождающая грамматика парсера может выглядеть так:

parser grammar IniParser;

options {
  tokenVocab=IniLexer;
}

sections
 : section* EOF
 ;

section
 : COMMENT
 | SECTION section_atom*
 ;

section_atom
 : COMMENT
 | KEY ASSIGN VALUE
 ;

который проанализирует ваш пример ввода в следующем дереве синтаксического анализа:

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

person Bart Kiers    schedule 09.06.2019
comment
Действительно классный ответ с новыми темами, которых я не знал, спасибо, я проверю. Кстати, вы предлагаете использовать лексер вместо парсера, но в конечном итоге я хотел бы добиться чего-то вроде это, проверьте с 6:20 до 12:30 в моем реальном случае (это GLSL IDE). В любом случае, как насчет другой части моего вопроса? Как вы справляетесь с ошибками, чтобы подсветка не испортилась? +1 тем временем - person BPL; 09.06.2019
comment
но в конечном итоге я хотел бы добиться [...], хорошо, тогда просто лексер не собирается его резать, и да, вам нужен парсер. Что касается второй части вашего вопроса, я не могу дать на него осмысленный ответ: я никогда не использовал ANTLR таким образом (инкрементальный анализ для плагинов/инструментов IDE) - person Bart Kiers; 09.06.2019
comment
Интересный разговор, кстати. - person Bart Kiers; 09.06.2019
comment
Действительно, очень приятный разговор! Я должен сказать, что на самом деле мне было довольно сложно выбрать между antlr4 и tree-sitter tbh, оба инструмента довольно крутые. В любом случае, я думаю, что ваш ответ в значительной степени удовлетворяет мой текущий вопрос, я уже проверил его, и он отлично работает. Теперь пришло время настроить мой тривиальный фрагмент hello world для использования синтаксического анализатора вместо лексера, я сделаю это, прежде чем пытаться использовать более сложную грамматику, такую ​​как GLSL. Плюс... не уверен, насколько сложно будет применить эти лексические режимы к сложным грамматикам, таким как GLSL, пора проверить ;) - person BPL; 09.06.2019

Я уже реализовал что-то подобное на C++.

https://github.com/tora-tool/tora/blob/master/src/editor/tosqltext.cpp

Подкласс класса QScintilla и реализованный пользовательский Lexer на основе сгенерированного ANTLR источника.

Можно даже использовать парсер ANTLR (я им не пользовался), QScitilla позволяет иметь более одного анализатора (с разным весом), так что можно периодически выполнять семантическую проверку текста. Чего нельзя легко сделать в QScintilla, так это связать токен с некоторыми дополнительными данными.

person ibre5041    schedule 10.06.2019
comment
Вау, так что у вас тоже была эта идея, здорово, я посмотрю ... об использовании синтаксического анализатора ANTLR, не уверен насчет среды выполнения antlr c ++, вероятно, намного быстрее, чем у python. Дело в том, что вчера я пытался разобрать 28 КБ закомментированного кода glsl с помощью парсера glsl antlr, и это заняло у меня 1,9 секунды! это просто безумие, и определенно вы не можете использовать его в реальном времени (разбор при каждом нажатии клавиши)... где время анализа должно быть ~ 50-100 мс - person BPL; 10.06.2019
comment
Я использую среду выполнения C++ для ANTLR3, синтаксический анализ выполняется в фоновом потоке, и Qscintilla обычно отправляет для анализа только одну строку текста. Поэтому мне пришлось реализовать несколько хаков для многострочных комментариев. - person ibre5041; 10.06.2019

Подсветка синтаксиса в Sctintilla осуществляется специальными классами подсветки, которые являются лексерами. Парсер не очень подходит для такой работы, потому что функция подсветки синтаксиса должна работать, даже если ввод содержит ошибки. Парсер — это инструмент для проверки правильности ввода — две совершенно разные задачи.

Поэтому я рекомендую вам перестать думать об использовании ANTLR4 для этого и просто взять один из существующих классов Lex и создать новый для языка, который вы хотите выделить.

person Mike Lischke    schedule 09.06.2019
comment
Я использую ANTLR4 2 дня, и я думаю, что это правильный инструмент для работы здесь ... И я говорю это после использования QScintilla со {встроенными лексерами Scintilla, pygments, syntect, pyparsing, lark}. Так что это не значит, что я выбираю ANTLR4 на ровном месте... на самом деле я рассматривал либо ANTLR4, либо Tree-sitter, но я выбираю первое в основном из-за большого количества существующих доступных грамматик. Вы говорите для языка, который хотите выделить... ну, в реальном случае я кодирую несколько IDE, одна из них - GLSL IDE, а другая - многоязычный текстовый редактор, так что... - person BPL; 09.06.2019
comment
Кроме того, я вижу в этом другом вопросе, что вы также рекомендовали использовать лексер вместо синтаксического анализатора, и парень решил иди с парсером. Что ж, для меня самым важным будет производительность, поэтому сначала мне нужно проверить, сколько времени потребуется для анализа файлов GLSL размером ~ 30 КБ ... Вероятно, мое решение будет основано на этих измерениях, поскольку анализ/нажатие клавиши не должно быть больше чем ~100 мс - person BPL; 09.06.2019