Что мы собираемся построить
В этом посте мы рассмотрим, как создать автономный редактор с подсветкой синтаксиса для нашего языка. Функция подсветки синтаксиса будет основана на лексере ANTLR, который мы создали в первом посте . Код будет на Kotlin, однако он должен быть легко конвертируемым в Java. Редактор будет называться Kanvas .
Предыдущие посты
Этот пост является частью серии о том, как создать полезный язык и все вспомогательные инструменты.
Код
Код доступен на 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.
В следующих постах мы увидим, как обогатить этот редактор такими вещами, как автозаполнение.