[Из песочницы] Покорение Emacs-режимов: руководство для самоделкина

d0b42797535a4231a1ece02d3bc8b092.jpg

Программисты делятся на две категории:
1) Те, кто уже использует Vim.
2) Те, кто уже использует Emacs.
3) Те, кто ещё не использует.

Предисловие


Как-то пришла идея поставить Emacs во второй раз, чтобы ещё раз убедиться, что это какой-то неправильный редактор с кучей разных игр, но никак не функций для работы с текстом. Так и остался на нём.

Добавление режима


В Emacs'е есть множество разных режимов, добавляющих функциональность в него. Как правило, когда нужна какая-то фича, она скачивается в виде пакета, состоящего из файлов .el (Emacs Lisp), и они уже подключаются к встроенным .el файлам, отвечающим за загрузку редактора.

Сначала это всё удобно использовать, но потом начинает чего-то не хватать и приходится думать о добавлении своей функциональности.
Когда тебе нужно добавить одно клавиатурное сочетание, ты можешь это сделать напрямую в каком-нибудь из файлов настройки. Поначалу это работает и приносит сплошную радость, но в скором времени это начинает работать не так, как ожидалось: одни клавиши залазят на другие, другие — на первые, третьи — на вторые. Приходится всё переделывать или от чего-то отказываться.

Чем дольше работаешь с Emacs'ом, тем больше он нравится. Поэтому мотивация не заставила себя долго ждать — появилась задача: «Мне нужно иметь какую-то коробочку с инструментами для автоматического форматирования ответов на форум.»

Как решить (естественно, не выключая Emacs)?

Ответ прост: нужно добавить свой режим, который можно включить, когда нужен, и отключить в обратном случае.

Обратная инженерия


Естественно, для добавления режима нужно прочитать кучу документации, посмотреть, как сделаны другие режимы, изучить лисп и случайно не запортачить что-нибудь в работающем Emacs'е.

Начались поиски документации. Сначала туториальные статьи… статей нет. Тогда примеры режимов… примеры переполнены лишними конструкциями. Тогда документация… это читать и проверять несколько дней. Тогда готовые режимы… ничего не понятно, надо учить лисп.

Круг замкнулся. Пришлось использовать всё сразу и много.

Первое приближение


Информация:

1) Режимы бывают главные (major) и побочные (minor).
2) Режимы могут наследоваться от существующих режимов.
3) Главный режим может быть выбран только один, тогда как побочных — много.
4) К главному режиму можно прицепить множество побочных.

Сперва был составлен самый минимальный режим:
;; My test mode

(define-derived-mode test-mode python-mode "Test"
  "Major mode for editing Test source text."
  (set (make-local-variable 'test-variable) "Test variable value"))

(add-to-list 'auto-mode-alist (cons "\\.test\\'" 'test-mode))

(provide 'test-mode)


Файл test-mode.el с этим текстом кладётся в удобную папку, а затем режим подключается где-нибудь в файле инициализации, как и любой другой.
(add-to-list 'load-path "~/.emacs.d/packages/modes/test-mode/")
(require 'test-mode)



Объяснение содержимого:
Создаём режим, наследующий всё из питоновского режима:
(define-derived-mode test-mode python-mode


Эта строка будет показываться в полоске названия режима:
 "Test"


Это комментарий, который показывается, когда открываешь помощь для режима:
"Major mode for editing Test source text."


Это переменная, которая создаётся при входе в режим и разрушается при выходе из него:
  (set (make-local-variable 'test-variable) "Test variable value"))


Это назначает файлам с расширением .test этот режим автоматически:
(add-to-list 'auto-mode-alist (cons "\\.test\\'" 'test-mode))


Это расшаривает режим, чтобы его можно было подключить из других файлов:
(provide 'test-mode)



С минимумом закончили.

Как этим всем пользоваться?

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

Добавляем функцию и назначаем клавишу для неё:
;; My test function mode

(defun test-func-hello()
  (interactive)
  (message "Pressed C-c 1, test-func-hello()"))

(defvar test-func-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "C-c 1") 'test-func-hello)
     map)
   "Keymap for `test-func-mode'.")

(define-derived-mode test-func-mode python-mode "TestFunc"
  "Major mode for editing TestFunc source text."
  (set (make-local-variable 'test-func-variable) "TestFunc variable value"))

(add-to-list 'auto-mode-alist (cons "\\.testfunc\\'" 'test-func-mode))

(provide 'test-func-mode)



Здесь изменено только имя режима и расширение файлов, в которых он будет включаться автоматически. И ещё добавлено две штуки: функция и отображение клавиш.

Когда мы создаём файл file.testfunc и открываем его в Emacs'е, в нём устанавливается режим TestFunc. Так как режим унаследован от питоновского, у нас работает подсветка синтаксиса и другие вещи. Но при нажатии Ctrl + c + 1 у нас высвечивается сообщение о том, что нажата эта комбинация, и имя сработавшей функции.

Теперь мы можем создать директорию режима в директории yasnippet с именем нашего режима test-mode или test-func-mode и положить туда какие-нибудь снипеты, которые будут отделены от обычных питоновских и даже при перекрытии будут лишь дополнять друг друга.

Минимальное связывание


Спустя какое-то время понимаешь, что это всё хорошо, конечно, но когда сидишь в уже правильном режиме, нужно в нём и оставаться. А текущий режим со своими фишками может не совпасть с тем, который наследовался изначально.

Поэтому для подмешивания функциональности в текущий режим на помощь приходит побочный режим (малый или минорный). У него есть всё то же самое, что и у главного режима, но он не заставляет текущий режим бросать обрабатываемые данные и срочно куда-то переключаться.

Переделываем главный режим с функцией в побочный:
;; My test minor mode

(defun test-minor-hello()
  (interactive)
  (message "Pressed C-c 1, test-minor-hello()"))

(defvar test-minor-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "C-c 1") 'test-minor-hello)
     map)
   "Keymap for `test-minor-mode'.")

(define-minor-mode test-minor-mode
  "Minor mode for editing TestMinor text."
  nil
  " TestMinor"
  nil
  (if test-minor-mode
      (set (make-local-variable 'test-minor-variable)
           "TestMinor variable value")
    (setq test-minor-variable nil)))

(add-to-list 'auto-mode-alist (cons "\\.testminor\\'" 'test-minor-mode))

(provide 'test-minor-mode)



Как видно, здесь всё не так радостно, как при создании главного режима. Появились какие-то странные конструкции, совсем не понятные.

Если объяснять по-простому, побочный режим представляет из себя переключаемую конструкцию. Его можно включить или выключить, поэтому он должен уметь определять своё состояние, чтобы не включаться повторно, когда он уже включен. К тому же в отличие от главного режима он не чистит за собой свои переменные, поэтому требуется иметь такую ветвь, которая срабатывает при отключении.

Если объяснять по-сложному, в Emacs'е зашито определённое поведение для работы с такими режимами. Можно было их сделать лучше, но уже поздно.

Главное, что нужно помнить про малые режимы, — у них вверху четыре переменные. Там можно случайно это упустить, и он выражение, которое должно выполняться при включении режима, подставит вместо последней переменной, которая никак об этом не сообщит — будешь два часа сидеть в документации, чтобы понять, почему не работает выражение.

Решение поставленной задачи


Конечный вариант
;; Forum minor mode

(defun forum-minor-wrap-code()
  (interactive)
  (if (not (mark))
      (set-mark (point)))
  (narrow-to-region (mark) (point))
  (goto-char (point-min))
  (insert "<code>")
  (goto-char (point-max))
  (insert "</code>")
  (set-mark (point-min))
  (widen))

(defun forum-minor-wrap-quote()
  (interactive)
  (if (not (mark))
      (set-mark (point)))
  (narrow-to-region (mark) (point))
  (goto-char (point-min))
  (insert "<quote>")
  (goto-char (point-max))
  (insert "</quote>")
  (set-mark (point-min))
  (widen))

(defvar forum-minor-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "C-c 1") 'forum-minor-wrap-quote)
     (define-key map (kbd "C-c 2") 'forum-minor-wrap-code)
     map)
   "Keymap for `forum-minor-mode'.")

(define-minor-mode forum-minor-mode
  "Minor mode for editing text for `http://forum.example.ru'."
  nil
  " Forum"
  nil
  (if forum-minor-mode
      (set (make-local-variable 'forum-minor-nick)
           "Nick")
    (setq forum-minor-nick nil)))

(add-to-list 'auto-mode-alist (cons "\\.forum\\'" 'forum-minor-mode))
(add-to-list 'yas-extra-modes 'forum-minor-mode)

(provide 'forum-minor-mode)



Здесь добавлена пара функций для оборачивания в теги и выделенного текста. К тому же пришлось добавлять режим в yasnippet, потому что тот обнаруживает только главные режимы.

Здесь нужно отметить, что режим можно привязать к файлу, запустить вручную через Alt + x + название, либо прицепить к другому режиму через хук.

Вот так выглядит присоединение к питоновскому:

(add-hook 'python-mode-hook 'forum-minor-mode)


Теперь можно заняться написание более продвинутых функций по обработке текста. Режимы можно добавлять и удалять, не влияя на остальные настройки.

Заключение


Когда я поставил Emacs в первый раз, то не понял его, он показался мне сложным, неудобным и некрасивым (из-за древних шрифтов). Теперь же, поставив его во второй раз, я его не узнал. В нём оказалось так много чудесных приложений, а удобство зашкаливает.

До него я пользовался Vim'ом, это было хорошо и интересно, но его настройки казались какими-то неродными, всё время нужно было читать что-то, чтобы понять мнемонику. Из-за этого потом долго программировал в kwrite, потому что изучать ничего не надо, а синтаксис выравнивает и подкрашивает хорошо.

Но только установив и влившись в Emacs, я понял, что такое мощь и простота (до поры до времени, конечно, пока не попробуешь открыть файл на 20Мb).

P.S.: Если что неправильно во мною приведённом коде или подходе — велком.

© Habrahabr.ru