Как сохранить 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)

Теперь мы можем ясно видеть, что если мы сделаем это, есть две возможные последовательности примечания:

  1. Мы вручную меняем на foo-mode:

    (foo-mode)
     => (kill-all-local-variables)
     => [...]
     => (run-hooks 'after-change-major-mode-hook)
         => (hack-local-variables)
    
  2. Мы посещаем файл, для которого 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)
Другие вопросы по тегам