TreeSitter – это набор инструментов, созданных сотрудниками GitHub. Если совсем грубо, зато кратко, TreeSitter строит синтактическое древо для переданного на вход исходного кода. Это не разбор регулярных выражений, а кое-что получше. Но самое главное преимущество TreeSitter познаётся в сравнении.
Вот раньше как было? Хочешь поддерживать какой-то синтаксис в своём редакторе или IDE – садись и пиши модуль, который будет это делать.
Как сейчас? Скачай грамматику TreeSitter и передавай ей на вход исходный код, а она на выходе даст синтаксическое древо.
Другими словами, поддержку любого синтаксиса достаточно написать всего один раз (всем миром), а потом можно использовать где хочешь.
Грамматика описывается в специальной нотации на языке JavaScript. Файл транслируется в библиотеку на языке C, которая на целевой системе компилируется в динамически загружаемую библиотеку (.so в *nix, .dll в Windows).
Настройка окружения
Нам понадобятся:
Emacs.
Набор компиляторов.
Настройки
treesit.el.
Компиляторы нужны для сборки библиотек с грамматиками. В DEB-системах достаточно установить пакет built-essential:
apt-get install build-essential --yes
Настройка treesit.el
Перейдём к настройкам treesit.el. Нам нужно заполнить список treesit-language-source-alist. Каждая запись должна содержать название грамматики, ссылку на репозиторий с её исходным кодом, и путь к каталогу с кодом, если он отличается от src/. Кроме того, записи могут содержать номер версии (тег или ветка Git). Если вы не укажете номер версии, будет использован последний коммит в последней ветке. Это сделает результаты сборки библиотеки с грамматикой непредсказуемыми. Не используйте, это антипаттерн “детонатор”.
Добавить грамматики в список мало, нужно их ещё установить. Некоторые пакеты умеют сами устанавливать нужные грамматики, но мы автоматизируем этот процесс полностью.
Также было бы неплохо везде где можно включать режим на базе TreeSitter вместо “классического” (на базе регулярных выражений). Для этого мы заполним список major-mode-remap-alist.
Итоговый код для вашего init.el будет выглядеть так:
;; 📦 TREESIT ;; Встроенный пакет для работы с TreeSitter (use-package treesit :config ;; Грамматики (setq treesit-language-source-alist '( (asciidoc "https://github.com/cathaysia/tree-sitter-asciidoc.git" "v0.3.0" "tree-sitter-asciidoc/src/") (asciidoc-inline "https://github.com/cathaysia/tree-sitter-asciidoc.git" "v0.3.0" "tree-sitter-asciidoc_inline/src/") (bash "https://github.com/tree-sitter/tree-sitter-bash.git" ;; "v0.25.1" ;; ABI v15 "v0.23.3") (c "https://github.com/tree-sitter/tree-sitter-c.git" ;; "v0.24.1" ;; ABI v15 "v0.23.5") (cpp "https://github.com/tree-sitter/tree-sitter-cpp.git" "v0.23.4") (c-sharp "https://github.com/tree-sitter/tree-sitter-c-sharp.git" ;; "v0.23.5" ;; ABI v15 "v0.23.1") ;; ABI v14 (css "https://github.com/tree-sitter/tree-sitter-css.git" ;; "v0.25.0" ;; ABI v15 "v0.23.2") (dockerfile "https://github.com/camdencheek/tree-sitter-dockerfile.git" "v0.2.0" "src/") (go "https://github.com/tree-sitter/tree-sitter-go.git" ;; "v0.25.0" ;; ABI v15 "v0.23.4") (gomod "https://github.com/camdencheek/tree-sitter-go-mod.git" "v1.1.0") (html "https://github.com/tree-sitter/tree-sitter-html.git" "v0.23.2") (java "https://github.com/tree-sitter/tree-sitter-java.git" "v0.23.5") (javascript "https://github.com/tree-sitter/tree-sitter-javascript.git" "v0.23.1" "src/") (json "https://github.com/tree-sitter/tree-sitter-json.git" "v0.24.8") (make "https://github.com/tree-sitter-grammars/tree-sitter-make.git" "v1.1.1" "src/") (markdown "https://github.com/tree-sitter-grammars/tree-sitter-markdown.git" "v0.4.1" "tree-sitter-markdown/src") (markdown-inline "https://github.com/tree-sitter-grammars/tree-sitter-markdown.git" "v0.4.1" "tree-sitter-markdown-inline/src") (python "https://github.com/tree-sitter/tree-sitter-python.git" "v0.23.6" "src/") (ruby "https://github.com/tree-sitter/tree-sitter-ruby.git" "v0.23.1" "src/") (rust "https://github.com/tree-sitter/tree-sitter-rust.git" "v0.23.3") (tsx "https://github.com/tree-sitter/tree-sitter-typescript.git" "v0.23.2" "tsx/src/") (typescript "https://github.com/tree-sitter/tree-sitter-typescript.git" "v0.23.2" "typescript/src/") (typst "https://github.com/uben0/tree-sitter-typst.git" "0.11" "src/") (yaml "https://github.com/tree-sitter-grammars/tree-sitter-yaml.git" "v0.7.2" "src/"))) ;; Сборка и установка грамматик (dolist (source treesit-language-source-alist) (let ((lang (car source))) (unless (treesit-ready-p lang t) (with-demoted-errors "Treesit: ошибка установки грамматики: %s" (treesit-install-language-grammar lang))))) ;; Замена "классических" режимов на TreeSitter (add-to-list 'major-mode-remap-alist '(bash-mode . bash-ts-mode)) (add-to-list 'major-mode-remap-alist '(c++-mode . c++-ts-mode)) (add-to-list 'major-mode-remap-alist '(c-mode . c-ts-mode)) (add-to-list 'major-mode-remap-alist '(csharp-mode . csharp-ts-mode)) (add-to-list 'major-mode-remap-alist '(css-mode . css-ts-mode)) (add-to-list 'major-mode-remap-alist '(dockerfile-mode . dockerfile-ts-mode)) (add-to-list 'major-mode-remap-alist '(go-mode . go-ts-mode)) (add-to-list 'major-mode-remap-alist '(html-mode . html-ts-mode)) (add-to-list 'major-mode-remap-alist '(java-mode . java-ts-mode)) (add-to-list 'major-mode-remap-alist '(js-mode . js-ts-mode)) (add-to-list 'major-mode-remap-alist '(json-mode . json-ts-mode)) (add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode)) (add-to-list 'major-mode-remap-alist '(ruby-mode . ruby-ts-mode)) (add-to-list 'major-mode-remap-alist '(yaml-mode . yaml-ts-mode)))
Обратили внимание на то, что для некоторых синтаксисов добавлены 2 грамматики? Объясняется это просто: либо в языке есть две специфичных грамматики, как в Golang, либо используется двупроходный парсинг.
Обычно двупроходный парсинг нужен для легковесных разметок, например, Markdown и AsciiDoc: сначала делим текст на абзацы, заголовки, таблицы и так далее, а затем уже их разбираем на более мелкие части. Отсюда грамматики с суффиксом _inline.
Ещё одна особенность приведённого выше кода – привязки к конкретным тегам Git. В Emacs 30.1, которым я пользуюсь, используется TreeSitter ABI v14, и более новые версии грамматик хоть и собираются, но не могут быть использованы. Если у вас более новый Emacs, попробуйте подключить более новую версию нужной грамматики.
Режимы
Что касается самих режимов, достаточно добавить в init.el загрузку нужных пакетов:
Dockerfile / Containerfile:
;; 📦 DOCKERFILE-TS-MODE ;; Встроенный пакет на базе TreeSitter для работы с Dockerfile. (use-package dockerfile-ts-mode :defer t :mode ("Containerfile\\'" . dockerfile-ts-mode))С
Dockerfileрежим связывается автоматически, а вот сContainerfileпридётся настроить связь вручную.Golang:
;; 📦 GO-TS-MODE ;; Встроенный пакет для поддержки языка Golang (use-package go-ts-mode :defer t)HTML:
;; 📦 HTML-TS-MODE ;; Встроенный пакет для работы с HTML и SGML. (use-package html-ts-mode :defer t)Java:
;; 📦 JAVA ;; Встроенный пакет для работы с Java. (use-package java-ts-mode :defer t)JavaScript:
;; 📦 JS-MODE ;; Встроенный пакет для работы с JavaScript. (use-package js :defer t :custom (js-chain-indent t "Выравнивание при цепочке вызовов через точку.") (js-indent-level 2 "Отступ в 2 пробела, а не 4 (по умолчанию).") (js-switch-indent-offset 2 "Отступ в 2 пробела для switch/case."))JSON:
;; 📦 JSON-TS-MODE ;; Встроенный пакет для работы с JSON через TreeSitter (use-package json-ts-mode :defer t)Markdown: режим
markdown-ts-modeдоступен в Emacs 31. Будет ли он доступен в GNU ELPA – неизвестно.Python:
;; 📦 PYTHON-MODE ;; Встроенный пакет для работы с Python через TreeSitter (use-package python :pin gnu :init (unless (alist-get 'python package-alist) (with-demoted-errors "Ошибка обновления `python': %s" (package-upgrade 'python))) :interpreter ("python[0-9]" . python-mode) :custom (py-pylint-command-args "--max-line-length 120" "Дополнительные параметры, передаваемые pylint") (python-indent-guess-indent-offset-verbose nil "Выключить уведомления") (python-indent-offset 4 "Отступ по умолчанию — 4 пробела"))Ruby:
;; 📦 RUBY-TS-MODE ;; Встроенный пакет для работы с Ruby. (use-package ruby-ts-mode :interpreter ("bundler" . ruby-ts-mode) ("ruby" . ruby-ts-mode) :mode ("\\.rb\\'" . ruby-ts-mode) ("\\Vagrantfile\\'" . ruby-ts-mode))TypeScript:
;; 📦 TYPESCRIPT-TS-MODE ;; Встроенный пакет для работы с TypeScript (use-package typescript-ts-mode :defer t)

