В 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.

Комментарии (9)


  1. Patrick139
    25.09.2026 10:31

    раньше все смеялись над книгой "Как выйти из vim", теперь вот это растет и пухнет совершенно в ненужную сторону.


    1. dunmaksim Автор
      25.09.2026 10:31

      Что и в какую сторону, по-вашему, должно расти?


  1. DTLA
    25.09.2026 10:31

    чего не хватает в емакс, так это текстового редактора


    1. krendelbok
      25.09.2026 10:31

      Думаю кому-то серого вещества в голове не хватает, так как в Емаксе есть все возможные редакторы мира


  1. kolezz
    25.09.2026 10:31

    Не задумывался про такие трюки с наследованием.


    Интересно что в примерах то flycheck-mode, то flymake-mode.

    Всё также первый намного легче расширяемый и от коммьюнити, а второй оформленный монолит насильно впихиваемый в дистрибутив (как и lsp-mode в противовес eglot-у от комьюнити) или есть изменения в этой истории?


    1. dunmaksim Автор
      25.09.2026 10:31

      У меня самого Flymake только для Emacs Lisp, в остальных местах Flycheck.

      Претензии к Eglot не понимаю. Он развивается, обновления иногда выходят, настраивается просто, работает нормально.

      Что касается TreeSitter, то разработчики пакета tree-sitter.el пишут, что лучше использовать встроенный treesit.el.


      1. kolezz
        25.09.2026 10:31

        Flycheck тоже имеет поддержку Emacs Lisp. Я сам на лиспе не пишу, но любопытно почему тут Flymake предпочтительнее.

        Я тоже Eglot использую. И у него вроде было больше пользователей и развивался сильнее. Но вроде джон вигли включил Lsp-mode (с меньшим набором фич) в дистрибутив и людям стало непонятно, зачем ставить сторонний пакет, если в дистрибутиве уже что-то поддерживает lsp. Пользователей у Eglot приуменьшилось от такого. Как я понял Eglot был и хотел оставаться комьюнити дривен, а джон хотел более авторитарного управления что и послужилось причиной конфликта.

        По treesitter-у у меня только:

        (global-tree-sitter-mode)

        (add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode)


        1. dunmaksim Автор
          25.09.2026 10:31

          Наоборот, Eglot встроенный, а lsp-mode.el надо отдельно ставить.


          1. kolezz
            25.09.2026 10:31

            Ой, да, точно. Это Eglot enforced. Вместе с Flymake. Давно разбирался с этой битвой и даже участников попутал.

            Всё, что я писал про eglot vs lsp-mode - прямо противоположно :-)