Emacs: основные режимы

В Emacs режимы делятся на основные (major) и дополнительные (minor). Основной режим определяет правила работы с буфером: подсветку синтаксиса, поведение редактора при нажатии клавиш, какие-то дополнительные команды.
Если провести аналогию, то в других редакторах вы задаёте основной режим когда указываете тип файла. Например, в Zed тип файла по умолчанию — Plain Text. Однако, вы можете в любой момент поменять его на один из доступных, и вам тут же станут доступны и подсветка синтаксиса, и его проверка, и другие удобства.
Основной режим в буфере должен быть только одним. Как говорится:
There can be only one.
Существуют пакеты, которые позволяют включать в буферах несколько основных режимов, но я их максимально осуждаю и не рассматриваю.
Наследование
Основные режимы, как правило, создаются на базе одного из четырёх базовых режимов.
fundamental-mode— минимальные удобства для ввода и вывода информации. Никакой подсветки синтаксиса и так далее.special-mode— режим для специальных буферов, то есть таких, которые не предназначены для редактирования текстов. Режимы для просмотра изображений и логов, вывода ошибок и сообщений строятся на базеspecial-mode.text-mode— режим для работы с текстами на естественных языках. На базеtext-modeсозданы такие режимы, как:rst-mode— для работы с ReStructured Text.markdown-mode— Markdown.adoc-mode— AsciiDoc.
prog-mode— режим для буферов с текстами на различных языках программирования. Техническиprog-mode— потомокfundamental-mode. На базеprog-modeстроятся все остальные режимы для работы с языками программирования, например, эти:python-mode;c-mode;makefile-mode;rust-mode;r-mode.
Впрочем, наследование не является обязательным. В коде многих пакетов вы можете увидеть примерно такое:
;;;###autoload
(define-derived-mode conf-mode nil "Conf[?"
;; Код режима
)Здесь nil — предок, то есть в данном случае режим создаётся с нуля, а не на основе одного из базовых режимов.
Хуки
Итак, мы теперь знаем, что режимы наследуются. Но что нам это даёт? А даёт нам это несколько интересных побочных свойств.
При активации режима-потомка срабатывает и хук активации режима-предка. Рассмотрим на примере.

conf-mode.elДалее я буду оформлять такие диаграммы как обычные маркированные списки:
conf-mode;conf-unix-mode;conf-space-mode;conf-colon-mode;conf-xdefaults-mode;conf-ppd-mode;
conf-desktop-mode;
conf-windows-mode;conf-javaprop-mode;conf-toml-mode.
Хабр не даёт вставлять SVG, а использовать зашакаленные PNG я не хочу.
Встроенный пакет conf-mode.el предоставляет 10 основных режимов, из них 4 являются прямыми наследниками conf-mode, ещё 3 наследуют от conf-unix-mode, и ещё 2 от conf-colon-mode.
Допустим, мы хотим включать дополнительный режим flymake-mode в буферах с основными режимами conf-toml-mode и conf-unix-mode. В этом случае наивный код будет выглядеть так:
(use-package flymake
:hook
(conf-toml-mode . flymake-mode)
(conf-unix-mode . flymake-mode))Можно чуть-чуть оптимизировать его:
(use-package flymake
:hook
((conf-toml-mode
conf-unix-mode) . flymake-mode))В обоих случаях мы не решаем 2 важных проблемы:
Если в будущем мы захотим добавить активацию
flymake-modeпри включении ещё одного из режимовconf-mode.el, нужно будет модифицировать имеющийся код:(use-package flymake :hook (conf-toml-mode conf-unix-mode conf-ppd-mode) . flymake-mode)Похожие правки нужно сделать во множестве других мест. Мы же можем активировать в буфере множество дополнительных режимов. И что теперь, делать однотипные правки в десяти местах?
Проблема ещё более усугубляется благодаря TreeSitter. Множество пакетов для языков программирования теперь предоставляют 2 основных режима подсветки синтаксиса:
"Классический" режим на базе регулярных выражений.
Построение синтаксического древа с помощью TreeSitter.
Оптимизация
Обратимся к пакету python.el. Он предоставляет эти режимы:
python-base-mode;python-mode;python-ts-mode.
В зависимости от того, классический режим используется или на базе TreeSitter, настройка дополнительных режимов может выглядеть по-разному:
(use-package flycheck
:pin gnu
:ensure t
:hook
((python-mode
python-ts-mode) . flycheck-mode))
(use-package eglot
:pin gnu
:ensure t
:hook
((python-mode
python-ts-mode) . eglot-ensure))
(use-package indent-bars
:pin gnu
:ensure t
:hook
((python-mode
python-ts-mode) . indent-bars-mode))Если вы используете python-mode, то хук для python-ts-mode явно не нужен. Соответственно, если вы установили грамматику и настроили TreeSitter, то не нужен хук для python-mode. Код явно не оптимален. Однако, можно упростить его, если повесить обработчик на общего предка:
(use-package flycheck
:pin gnu
:ensure t
:hook
(python-base-mode . flycheck-mode))
(use-package eglot
:pin gnu
:ensure t
:hook
(python-base-mode . eglot-ensure))
(use-package indent-bars
:pin gnu
:ensure t
:hook
(python-base-mode . indent-bars-mode))Это делает код более отказоустойчивым: какой бы режим для Python мы по факту не использовали, все дополнительные режимы будут активироваться без изменения init.el.
Вернёмся к примеру с conf-mode.el:
(use-package flymake
:hook
(conf-colon-mode
conf-desktop-mode
conf-javaprop-mode
conf-ppd-mode
conf-space-mode
conf-toml-mode
conf-unix-mode
conf-windows-mode
conf-xdefaults-mode) . flymake-mode)Этот код можно сократить до такого:
(use-package flymake
:hook (conf-mode . flymake-mode))Rust
Множество режимов имеют общего предка, поэтому привязка минорных режимов к его хуку весьма эффективна. Однако, из этого правила есть одно исключение. Пакет rust-mode от авторов языка Rust работает немного не так, как вы думаете.
Он предоставляет режим rust-mode, но является он потомком встроенного режима rust-ts-mode или нет, зависит от настройки rust-mode-treesitter-derive:
(use-package rust-mode
:pin nongnu
:ensure t
:custom
(rust-mode-treesitter-derive t "Наследование от `rust-ts-mode' ВКЛ.")
:mode ("\\.rs\\'" . rust-mode))Таким образом, вы можете использовать rust-mode и с TreeSitter, и без него.
JavaScript
Если мы говорим о встроенном пакете js-mode.el, то там такое наследование:
js-base-mode;js-mode;js-ts-mode.
Достаточно повесить обработчик на js-base-mode.
Генеалогия
Как узнать, какой предок у режима?
Напишите в
init.elназвание нужного режима:(markdown-mode)Нажмите [M-.]. Emacs откроет код пакета, предоставляющего режим. В строке с
define-derived-modeвы и увидите предка, например:;;;###autoload (define-derived-mode markdown-mode text-mode "Markdown" "Major mode for editing Markdown files." (when buffer-read-only ;;; ... ))
Кстати, у режимов из markdown-mode.el интересная диаграмма наследования:
text-mode;markdown-mode;gfm-mode;gfm-view-mode;
markdown-view-mode.
Здесь gfm означает GitHub Flavored Markdown, специальный режим для работы с диалектом Markdown, используемым на GitHub.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.