如何使用 ANTLR4 突出显示 QScintilla?

Posted

技术标签:

【中文标题】如何使用 ANTLR4 突出显示 QScintilla?【英文标题】:How to highlight QScintilla using ANTLR4? 【发布时间】:2019-10-23 04:36:06 【问题描述】:

我正在努力学习 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 类似的行为。不幸的是,我不知道如何实现这一点,您将如何修改提供这种行为的 sn-p? 现在我正在尝试模仿与以下快照类似的突出显示:

您可以在该屏幕截图中看到变量赋值(variable=deeppink 和 values=yellowish)的突出显示不同,但我不知道如何实现,我尝试使用这种稍微修改的语法:

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],

但是如果您查看词法分析输出,您会发现 VARIABLEVALUES 之间没有区别,因为 ANTLR 语法中的顺序优先。所以我的问题是,你将如何修改语法/sn-p 以实现这样的视觉外观?

【问题讨论】:

【参考方案1】:

问题在于词法分析器需要对上下文敏感:= 左侧的所有内容都需要是一个变量,而在它的右侧是一个值。您可以通过使用 ANTLR 的 lexical modes 来做到这一点。您首先将连续的非空格分类为变量,当遇到= 时,您将进入您的值模式。在 value-mode 中,只要遇到换行符,就会退出此模式。

请注意,词法模式仅适用于词法分析器语法,而不适用于您现在拥有的组合语法。此外,对于语法高亮,您可能只需要词法分析器。

这是一个如何工作的快速演示(将其粘贴在一个名为 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
 ;

它将在以下解析树中解析您的示例输入:

【讨论】:

对我不知道的新主题的非常酷的回答,谢谢,我会测试一下。顺便说一句,您建议使用词法分析器而不是解析器,但最终我想实现this 之类的东西,在我的真实案例(这是一个 GLSL IDE)中检查从 6:20 到 12:30。无论如何,我的问题的另一部分呢?您如何处理错误以使突出显示不会搞砸?同时+1 “但最终我想实现 [...]”,好的,那么仅仅一个词法分析器就不会削减它,是的,你确实需要一个解析器。关于您问题的第二部分,我无法给出有意义的答案:我从未以这种方式使用过 ANTLR(IDE 插件/工具的增量解析) 有趣的谈话,顺便说一句。 确实,讲得真好!我必须说,在 antlr4 或 tree-sitter tbh 之间我真的很难接受,这两个工具都非常棒。无论如何,我认为您的回答非常满足我当前的问题,我已经检查过了,它工作正常。现在是时候调整我琐碎的 hello world sn-p 以使用解析器而不是词法分析器,在尝试使用更复杂的语法(如 GLSL)之前,我会这样做。另外...不确定将这些词法模式应用于 GLSL 等复杂语法有多困难,检查时间;)【参考方案2】:

我已经在 C++ 中实现了类似的东西。

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

子类 QScintilla 类并基于 ANTLR 生成的源实现自定义 Lexer。

您甚至可以使用 ANTLR 解析器(我没有使用它),QScitilla 允许您拥有多个分析器(具有不同的权重),因此您可以定期对文本执行一些语义检查。在 QScintilla 中不能轻易做到的是将令牌与一些附加数据相关联。

【讨论】:

哇,所以你也有这个想法,太棒了,我来看看...关于使用 ANTLR 解析器,不确定 c++ antlr 运行时,可能比蟒蛇之一。问题是,昨天我尝试使用 glsl antlr 解析器解析 28kb 的注释 glsl 代码,结果花了我 1.9 秒!这太疯狂了,绝对不能实时使用它(每次击键解析)......解析时间应该是~50-100ms 我使用 ANTLR3 的 c++ 运行时,解析在后台线程中运行,Qscintilla 通常只发送一行要解析的文本。所以我不得不为多行 cmets 实现一些 hack。【参考方案3】:

Sctintilla 中的语法高亮是由专门的高亮类(即词法分析器)完成的。解析器不太适合这种工作,因为语法高亮功能必须工作,即使输入包含错误。解析器是一种验证输入正确性的工具 - 2 个完全不同的任务。

因此,我建议您停止考虑为此使用 ANTLR4,而只需采用现有 Lex 类之一,并为您要突出显示的语言创建一个新类。

【讨论】:

我已经使用了 2 天的 ANTLR4,我认为它是适合这里工作的工具......我在使用 QScintilla 和 builtin Scintilla lexers, pygments, syntect, pyparsing,云雀。所以这并不是我突然选择 ANTLR4 ......实际上我正在考虑 ANTLR4 或 tree-sitter 但我选择前者主要是因为大量现有的可用语法。你说“为了语言”你想突出显示......好吧,在实际情况下,我正在编写几个 IDE,其中一个是 GLSL IDE,另一个是多语言文本编辑器,所以......跨度> 另外,我可以在另一个question 中看到您还建议使用词法分析器而不是解析器,而那个人决定使用解析器。好吧,对我来说最重要的是性能,所以首先我需要检查解析 ~30kb 的 GLSL 文件需要多长时间......可能我的决定将基于这些测量值,因为解析/击键不应该更大大于~100ms

以上是关于如何使用 ANTLR4 突出显示 QScintilla?的主要内容,如果未能解决你的问题,请参考以下文章

1.ANTLR4 helloworld基础开发与IDEA插件使用

1.ANTLR4 helloworld基础开发与IDEA插件使用

使用 Antlr4 解析 PlSQL 时如何提取带有语法错误的行

使用 antlr4 包括对 matlab 语法的注释

如何使用 PDFKit 突出显示 pdf 中的选定文本?

如何在文本突出显示期间保留语法突出显示