В 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.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 важных проблемы:

  1. Если в будущем мы захотим добавить активацию flymake-mode при включении ещё одного из режимов conf-mode.el, нужно будет модифицировать имеющийся код:

    (use-package flymake
      :hook
      (conf-toml-mode
       conf-unix-mode
       conf-ppd-mode) . flymake-mode)
  2. Похожие правки нужно сделать во множестве других мест. Мы же можем активировать в буфере множество дополнительных режимов. И что теперь, делать однотипные правки в десяти местах?

Проблема ещё более усугубляется благодаря 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.

Генеалогия

Как узнать, какой предок у режима?

  1. Напишите в init.el название нужного режима:

    (markdown-mode)
  2. Нажмите [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.