Статьи

Как создать редактор с подсветкой синтаксиса для вашего языка, используя ANTLR и Kotlin

Что мы собираемся построить

В этом посте мы рассмотрим, как создать автономный редактор с подсветкой синтаксиса для нашего языка. Функция подсветки синтаксиса будет основана на лексере ANTLR, который мы создали в первом посте . Код будет на Kotlin, однако он должен быть легко конвертируемым в Java. Редактор будет называться Kanvas .

снимок экрана

Предыдущие посты

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

  1. Построение лексера
  2. Сборка парсера

Код

Код доступен на GitHub . Код, описанный в этом посте, связан с тегом syntax_highlighting

Настроить

Мы собираемся использовать Gradle в качестве нашей системы сборки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
buildscript {
   ext.kotlin_version = '1.0.3'
  
   repositories {
     mavenCentral()
     maven {
        name 'JFrog OSS snapshot repo'
     }
     jcenter()
   }
  
   dependencies {
     classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
   }
}
  
apply plugin: 'kotlin'
apply plugin: 'application'
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'antlr'
  
repositories {
  mavenLocal()
  mavenCentral()
  jcenter()
}
  
dependencies {
  antlr "org.antlr:antlr4:4.5.1"
  compile "org.antlr:antlr4-runtime:4.5.1"
  compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
  compile 'com.fifesoft:rsyntaxtextarea:2.5.8'
  compile 'me.tomassetti:antlr-plus:0.1.1'
  testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
  testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
}
  
mainClassName = "KanvasKt"
  
generateGrammarSource {
    maxHeapSize = "64m"
    arguments += ['-package', 'me.tomassetti.lexers']
    outputDirectory = new File("generated-src/antlr/main/me/tomassetti/lexers".toString())
}
compileJava.dependsOn generateGrammarSource
sourceSets {
    generated {
        java.srcDir 'generated-src/antlr/main/'
    }
}
compileJava.source sourceSets.generated.java, sourceSets.main.java
  
clean {
    delete "generated-src"
}
  
idea {
    module {
        sourceDirs += file("generated-src/antlr/main")
    }
}

Это должно быть довольно просто. Несколько комментариев:

  • наш редактор будет основан на компоненте RSyntaxTextArea, поэтому мы добавили соответствующую зависимость
  • мы указываем версию Kotlin, которую мы используем. Вам не нужно устанавливать Kotlin в вашей системе: Gradle загрузит компилятор и будет использовать его на основе этой конфигурации
  • мы используем ANTLR для генерации лексера из нашей грамматики лексера
  • мы используем плагин идеи: запустив идею ./gradlew мы можем создать проект IDEA. Обратите внимание, что мы добавляем сгенерированный код в список исходных каталогов
  • когда мы запускаем ./gradlew clean, мы также хотим удалить сгенерированный код

Изменения в лексере ANTLR

При использовании лексера для подачи парсера мы можем спокойно игнорировать некоторые токены. Например, мы можем игнорировать пробелы и комментарии. При использовании лексера для подсветки синтаксиса мы хотим захватить все возможные токены. Так что нам нужно немного адаптировать грамматику лексера, которую мы определили в первом посте:

  • определение второго канала, куда мы будем отправлять токены, которые бесполезны для парсера, но необходимы для подсветки синтаксиса
  • мы добавим новый реал с именем UNMATCHED, чтобы захватить всех персонажей, которые не принадлежат к правильным токенам. Это необходимо как потому, что недопустимые токены должны отображаться в редакторе, так и потому, что при вводе пользователь будет постоянно вводить недопустимые токены

Парсер будет рассматривать только токены в канале по умолчанию, в то время как мы будем использовать канал по умолчанию и канал EXTRA в нашей подсветке синтаксиса.

Это результирующая грамматика.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
lexer grammar SandyLexer;
  
@lexer::members {
    private static final int EXTRA = 1;
}
  
  
// Whitespace
NEWLINE            : '\r\n' | 'r' | '\n' ;
WS                 : [\t ]+ -> channel(EXTRA) ;
  
// Keywords
VAR                : 'var' ;
  
// Literals
INTLIT             : '0'|[1-9][0-9]* ;
DECLIT             : '0'|[1-9][0-9]* '.' [0-9]+ ;
  
// Operators
PLUS               : '+' ;
MINUS              : '-' ;
ASTERISK           : '*' ;
DIVISION           : '/' ;
ASSIGN             : '=' ;
LPAREN             : '(' ;
RPAREN             : ')' ;
  
// Identifiers
ID                 : [_]*[a-z][A-Za-z0-9_]* ;
  
UNMATCHED          : .  -> channel(EXTRA);

Наш редактор

Наш редактор будет поддерживать подсветку синтаксиса для нескольких языков. На данный момент я реализовал поддержку Python и Sandy, простого языка, над которым мы работаем в этой серии постов. Для реализации подсветки синтаксиса для Python я начал с грамматики ANTLR для Python . Я начал с удаления правил парсера, оставив только правила лексера. Затем U сделал очень минимальные изменения, чтобы получить последний лексер в репозитории.

Для создания GUI нам понадобится довольно скучный код Swing. Нам нужно создать рамку с меню. Рамка будет содержать панель с вкладками. У нас будет одна вкладка на файл. В каждой вкладке у нас будет только один редактор, реализованный с использованием RSyntaxTextArea.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
fun createAndShowKanvasGUI() {
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
  
    val xToolkit = Toolkit.getDefaultToolkit()
    val awtAppClassNameField = xToolkit.javaClass.getDeclaredField("awtAppClassName")
    awtAppClassNameField.isAccessible = true
    awtAppClassNameField.set(xToolkit, APP_TITLE)
     
    val frame = JFrame(APP_TITLE)
    frame.background = BACKGROUND_DARKER
    frame.contentPane.background = BACKGROUND_DARKER
    frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
  
    val tabbedPane = MyTabbedPane()
    frame.contentPane.add(tabbedPane)
  
    val menuBar = JMenuBar()
    val fileMenu = JMenu("File")
    menuBar.add(fileMenu)
    val open = JMenuItem("Open")
    open.addActionListener { openCommand(tabbedPane) }
    fileMenu.add(open)
    val new = JMenuItem("New")
    new.addActionListener { addTab(tabbedPane, "<UNNAMED>", font) }
    fileMenu.add(new)
    val save = JMenuItem("Save")
    save.addActionListener { saveCommand(tabbedPane) }
    fileMenu.add(save)
    val saveAs = JMenuItem("Save as")
    saveAs.addActionListener { saveAsCommand(tabbedPane) }
    fileMenu.add(saveAs)
    val close = JMenuItem("Close")
    close.addActionListener { closeCommand(tabbedPane) }
    fileMenu.add(close)
    frame.jMenuBar = menuBar
  
    frame.pack()
    if (frame.width < 500) {
        frame.size = Dimension(500, 500)
    }
    frame.isVisible = true
}
  
fun main(args: Array<String>) {
    languageSupportRegistry.register("py", pythonLanguageSupport)
    languageSupportRegistry.register("sandy", sandyLanguageSupport)
    SwingUtilities.invokeLater { createAndShowKanvasGUI() }
}

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

Специфическая поддержка Sandy определена в объекте sandyLanguageSupport (для разработчиков на Java: объект — это просто одноэлементный экземпляр класса). Для поддержки требуется SyntaxScheme и AntlrLexerFactory .

SyntaxScheme просто возвращает стиль для каждого данного типа токена. Довольно легко, а?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
object sandySyntaxScheme : SyntaxScheme(true) {
    override fun getStyle(index: Int): Style {
        val style = Style()
        val color = when (index) {
            SandyLexer.VAR -> Color.GREEN
            SandyLexer.ASSIGN -> Color.GREEN
            SandyLexer.ASTERISK, SandyLexer.DIVISION, SandyLexer.PLUS, SandyLexer.MINUS -> Color.WHITE
            SandyLexer.INTLIT, SandyLexer.DECLIT -> Color.BLUE
            SandyLexer.UNMATCHED -> Color.RED
            SandyLexer.ID -> Color.MAGENTA
            SandyLexer.LPAREN, SandyLexer.RPAREN -> Color.WHITE
            else -> null
        }
        if (color != null) {
            style.foreground = color
        }
        return style
    }
}
  
object sandyLanguageSupport : LanguageSupport {
    override val syntaxScheme: SyntaxScheme
        get() = sandySyntaxScheme
    override val antlrLexerFactory: AntlrLexerFactory
        get() = object : AntlrLexerFactory {
            override fun create(code: String): Lexer = SandyLexer(org.antlr.v4.runtime.ANTLRInputStream(code))
        }
}

Теперь давайте посмотрим на AntlrLexerFactory . Эта фабрика просто создает ANTLR Lexer для определенного языка. Может использоваться вместе с AntlrTokenMaker . AntlrTokenMaker — это адаптер между ANTLR Lexer и TokenMaker, используемый RSyntaxTextArea для обработки текста. В основном TokenMakerBase вызывается для каждой строки файла отдельно при изменении строки. Поэтому мы просим наш ANTLR Lexer обработать только строку, и мы получаем полученные токены и создаем экземпляры экземпляров TokenImpl, которые ожидаются RSyntaxTextArea.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface AntlrLexerFactory {
    fun create(code: String) : Lexer
}
  
class AntlrTokenMaker(val antlrLexerFactory : AntlrLexerFactory) : TokenMakerBase() {
  
    fun toList(text: Segment, startOffset: Int, antlrTokens:List<org.antlr.v4.runtime.Token>) : Token?{
        if (antlrTokens.isEmpty()) {
            return null
        } else {
            val at = antlrTokens[0]
            val t = TokenImpl(text, text.offset + at.startIndex, text.offset + at.startIndex + at.text.length - 1, startOffset + at.startIndex, at.type, 0)
            t.nextToken = toList(text, startOffset, antlrTokens.subList(1, antlrTokens.size))
            return t
        }
    }
  
    override fun getTokenList(text: Segment?, initialTokenType: Int, startOffset: Int): Token {
        if (text == null) {
            throw IllegalArgumentException()
        }
        val lexer = antlrLexerFactory.create(text.toString())
        val tokens = LinkedList<org.antlr.v4.runtime.Token>()
        while (!lexer._hitEOF) {
            tokens.add(lexer.nextToken())
        }
        return toList(text, startOffset, tokens) as Token
    }
  
}

Это будет проблемой для токенов, охватывающих несколько строк. Для этого есть обходные пути, но пока давайте будем простыми и не будем это учитывать, учитывая, что у Сэнди нет токенов, охватывающих несколько строк.

Выводы

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

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