[Из песочницы] Покорение Emacs-режимов: руководство для самоделкина
Программисты делятся на две категории:
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.: Если что неправильно во мною приведённом коде или подходе — велком.