Как сохранить dir-локальные переменные при переключении основных режимов?
Я готовлюсь к проекту, в котором стандартные отступы и вкладки имеют ширину 3 символа, и в нем используется сочетание HTML, PHP и JavaScript. Поскольку я использую Emacs для всего и хочу использовать для этого проекта только трехсимвольный отступ, я установил файл ".dir-locals.el" в корне проекта, чтобы он применялся ко всем файлам / всем режимам в нем:
; Match projets's default indent of 3 spaces per level- and don't add tabs
(
(nil .
(
(tab-width . 3)
(c-basic-offset . 3)
(indent-tabs-mode . nil)
))
)
Что отлично работает, когда я впервые открываю файл. Проблема возникает при переключении основных режимов - например, для работы с фрагментом буквального HTML внутри файла PHP. Тогда я теряю все dir-локальные переменные.
Я также попытался явно указать все режимы, которые я использую в ".dir-locals.el", и добавить в мой файл.emacs "dir-locals-set-class-variable / dir-locals-set-directory-class". Я рад сказать, что все они ведут себя согласованно, сначала устанавливая локальные переменные dir, а затем теряя их, когда я переключаю основной режим.
Я использую GNU Emacs 24.3.1.
Какой элегантный способ перезагрузки локальных переменных dir при переключении основного режима буфера?
- edit - Спасибо за отличные ответы и комментарии как Аарона, так и Филса! После публикации здесь, я подумал, что это "пахнет" как ошибка, поэтому введите отчет в GNU- отправит им ссылку на эти обсуждения.
3 ответа
Согласно комментариям к ответу Аарона Миллера, здесь представлен обзор того, что происходит, когда вызывается функция режима (с объяснением производных режимов); чем вызов режима вручную отличается от вызова его автоматически в Emacs; и где after-change-major-mode-hook
а также hack-local-variables
вписаться в это, в контексте следующего предлагаемого кода:
(add-hook 'after-change-major-mode-hook 'hack-local-variables)
После посещения файла Emacs вызывает normal-mode
который "устанавливает надлежащий основной режим и привязки локальной переменной буфера" для буфера. Это делается путем первого вызова set-auto-mode
и сразу после звонка hack-local-variables
, который определяет все локальные каталоги и файловые переменные для буфера и устанавливает их значения соответственно.
Для деталей о том, как set-auto-mode
выбирает режим вызова, см. Ch i g (elisp) Auto Major Mode
RET. Это на самом деле включает в себя некоторое раннее взаимодействие локальных переменных (необходимо проверить mode
переменная, так что есть конкретный поиск того, что происходит до того, как установлен режим), но впоследствии происходит "правильная" обработка локальной переменной.
Когда выбранная функция режима действительно вызывается, есть умная последовательность событий, которую стоит подробно описать. Это требует, чтобы мы поняли немного о "производных режимах" и "ловушках отложенного режима"...
Производные режимы и модовые хуки
Большинство основных режимов определяются с помощью макроса define-derived-mode
, (Конечно, ничто не мешает вам просто писать (defun foo-mode ...)
и делать что хочешь; но если вы хотите, чтобы ваш основной режим хорошо сочетался с остальными Emacs, вы будете использовать стандартные макросы.)
Когда вы определяете производный режим, вы должны указать родительский режим, из которого он наследуется. Если у режима нет логического родителя, вы все равно используете этот макрос для его определения (чтобы получить все стандартные преимущества), и вы просто указываете nil
для родителя. В качестве альтернативы вы можете указать fundamental-mode
как родитель, так как эффект во многом такой же, как для nil
, как мы увидим на мгновение.
define-derived-mode
затем определяет функцию mode для вас, используя стандартный шаблон, и самое первое, что происходит при вызове функции mode:
(delay-mode-hooks
(PARENT-MODE)
,@body
...)
или если родитель не установлен:
(delay-mode-hooks
(kill-all-local-variables)
,@body
...)
Как fundamental-mode
сам зовет (kill-all-local-variables)
и затем сразу же возвращается при вызове в этой ситуации, эффект указания его в качестве родителя эквивалентен, если бы родитель nil
,
Обратите внимание, что kill-all-local-variables
работает change-major-mode-hook
прежде чем делать что-либо еще, так что это будет первый хук, который запускается во всей этой последовательности (и это происходит, когда предыдущий основной режим все еще активен, до того, как какой-либо код для нового режима был оценен).
Итак, это первое, что происходит. Самое последнее, что делает функция mode, это вызывает (run-mode-hooks MODE-HOOK)
для своего MODE-HOOK
переменная (это имя переменной буквально имя символа функции режима с -hook
суффикс).
Так что, если мы рассмотрим режим с именем child-mode
который получен из parent-mode
который получен из grandparent-mode
, вся цепочка событий, когда мы называем (child-mode)
выглядит примерно так:
(delay-mode-hooks
(delay-mode-hooks
(delay-mode-hooks
(kill-all-local-variables) ;; runs change-major-mode-hook
,@grandparent-body)
(run-mode-hooks 'grandparent-mode-hook)
,@parent-body)
(run-mode-hooks 'parent-mode-hook)
,@child-body)
(run-mode-hooks 'child-mode-hook)
Что значит delay-mode-hooks
делать? Это просто связывает переменную delay-mode-hooks
, который проверяется run-mode-hooks
, Когда эта переменная не nil
, run-mode-hooks
просто помещает свой аргумент в список хуков, которые будут запущены в будущем, и немедленно возвращается.
Только когда delay-mode-hooks
является nil
будут run-mode-hooks
на самом деле запустить крючки. В приведенном выше примере, это не до (run-mode-hooks 'child-mode-hook)
называется.
Для общего случая (run-mode-hooks HOOKS)
следующие последовательности выполняются последовательно:
change-major-mode-after-body-hook
delayed-mode-hooks
(в той последовательности, в которой они могли бы работать)HOOKS
(будучи аргументомrun-mode-hooks
)after-change-major-mode-hook
Поэтому, когда мы звоним (child-mode)
, полная последовательность:
(run-hooks 'change-major-mode-hook) ;; actually the first thing done by
(kill-all-local-variables) ;; <-- this function
,@grandparent-body
,@parent-body
,@child-body
(run-hooks 'change-major-mode-after-body-hook)
(run-hooks 'grandparent-mode-hook)
(run-hooks 'parent-mode-hook)
(run-hooks 'child-mode-hook)
(run-hooks 'after-change-major-mode-hook)
Вернуться к локальным переменным...
Что возвращает нас к after-change-major-mode-hook
и используя его для вызова hack-local-variables
:
(add-hook 'after-change-major-mode-hook 'hack-local-variables)
Теперь мы можем ясно видеть, что если мы сделаем это, есть две возможные последовательности примечания:
Мы вручную меняем на
foo-mode
:(foo-mode) => (kill-all-local-variables) => [...] => (run-hooks 'after-change-major-mode-hook) => (hack-local-variables)
Мы посещаем файл, для которого
foo-mode
это автоматический выбор:(normal-mode) => (set-auto-mode) => (foo-mode) => (kill-all-local-variables) => [...] => (run-hooks 'after-change-major-mode-hook) => (hack-local-variables) => (hack-local-variables)
Это проблема, которая hack-local-variables
работает дважды? Может быть, а может и нет. Как минимум, это немного неэффективно, но это, вероятно, не является серьезной проблемой для большинства людей. Для меня главное, что я не хотел бы полагаться на то, что эта схема всегда хороша во всех ситуациях, поскольку это, конечно, не ожидаемое поведение.
(Лично я действительно заставляю это происходить в определенных конкретных случаях, и это работает просто отлично; но, конечно, эти случаи легко тестируются - тогда как выполнение этого в качестве стандартного означает, что все случаи затрагиваются, и тестирование нецелесообразно.)
Поэтому я хотел бы предложить небольшую настройку этой техники, чтобы наш дополнительный вызов функции не происходил, если normal-mode
выполняет:
(defvar my-hack-local-variables-after-major-mode-change t
"Whether to process local variables after a major mode change.
Disabled by advice if the mode change is triggered by `normal-mode',
as local variables are processed automatically in that instance.")
(defadvice normal-mode (around my-do-not-hack-local-variables-twice)
"Prevents `after-change-major-mode-hook' from processing local variables.
See `my-after-change-major-mode-hack-local-variables'."
(let ((my-hack-local-variables-after-major-mode-change nil))
ad-do-it))
(ad-activate 'normal-mode)
(add-hook 'after-change-major-mode-hook
'my-after-change-major-mode-hack-local-variables)
(defun my-after-change-major-mode-hack-local-variables ()
"Callback function for `after-change-major-mode-hook'."
(when my-hack-local-variables-after-major-mode-change
(hack-local-variables)))
Недостатки к этому?
Основным из них является то, что вы больше не можете изменять режим буфера, который устанавливает его основной режим, используя локальную переменную. Или, скорее, он будет немедленно изменен в результате обработки локальной переменной.
Это не невозможно преодолеть, но я собираюсь назвать это за рамками на данный момент:)
Имейте в виду, что я не пробовал этого, так что это может привести к нежелательным результатам, начиная от ваших локальных переменных dir и заканчивая Emacs, пытающимся задушить вашу кошку; по любому разумному определению того, как должен вести себя Emacs, это почти наверняка обман. С другой стороны, это все в стандартной библиотеке, так что это не может быть большим грехом. (Я надеюсь.)
Оцените следующее:
(add-hook 'after-change-major-mode-hook
'hack-dir-local-variables-non-file-buffer)
С тех пор, когда вы меняете основные режимы, локальные переменные dir должны (я думаю) повторно применяться сразу после изменения.
Если это не работает или вам не нравится, вы можете отменить его, не перезапуская Emacs, заменив "add-hook" на "remove-hook" и снова оценив форму.
Мой взгляд на это:
(add-hook 'after-change-major-mode-hook #'hack-local-variables)
и либо
(defun my-normal-mode-advice
(function &rest ...)
(let ((after-change-major-mode-hook
(remq #'hack-local-variables after-change-major-mode-hook)))
(apply function ...)))
если вы можете жить с раздражающим
Создание буфера after-change-major-mode-hook локальным, в то время как локально разрешено!
сообщение или
(defun my-normal-mode-advice
(function &rest ...)
(remove-hook 'after-change-major-mode-hook #'hack-local-variables)
(unwind-protect
(apply function ...)
(add-hook 'after-change-major-mode-hook #'hack-local-variables)))
в противном случае и наконец
(advice-add #'normal-mode :around #'my-normal-mode-advice)