切换主模式时如何保持dir-local变量?

Yar*_*ary 7 emacs elisp

我正在致力于一个项目,标准缩进和标签是3个字符宽,它使用HTML,PHP和JavaScript的混合.由于我将Emacs用于所有内容,并且只想要该项目的3-char缩进,我在项目的根目录设置了一个".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)
         ))
 )
Run Code Online (Sandbox Code Playgroud)

我第一次打开文件时工作正常.切换主要模式时会出现问题 - 例如,在PHP文件中处理一大块文字HTML.然后我丢失了所有dir-local变量.

我还尝试明确说明我在".dir-locals.el"中使用的所有模式,并添加到我的.emacs文件"dir-locals-set-class-variables/dir-locals-set-directory-class ".我很高兴地说他们都表现得很稳定,最初设置dir-local变量,然后在切换主模式时丢失它们.

我正在使用GNU Emacs 24.3.1.

在切换缓冲区的主模式时,重新加载dir-local变量的优雅方法是什么?

- 编辑 - 感谢Aaron和phils的优秀答案和评论!在这里发帖之后,我认为它"闻起来"就像一个bug,因此向GNU发送报告将向他们发送这些讨论的参考.

phi*_*ils 12

根据对Aaron Miller的回答的评论,这里概述了调用模式函数时会发生什么(解释派生模式); 如何手动调用模式与Emacs自动调用它有何不同; 并在after-change-major-mode-hookhack-local-variables适应这一点,下面的建议代码的情况下:

(add-hook 'after-change-major-mode-hook 'hack-local-variables)
Run Code Online (Sandbox Code Playgroud)

在访问文件后,Emacs调用normal-mode"为缓冲区建立正确的主模式和缓冲区局部变量绑定".它通过首先调用set-auto-mode,然后立即调用hack-local-variables,确定缓冲区的所有目录本地和文件局部变量,并相应地设置它们的值来完成此操作.

有关如何set-auto-mode选择要呼叫的模式的详细信息,请参阅C-hig (elisp) Auto Major Mode RET.它实际上涉及一些早期的局部变量交互(它需要检查一个mode变量,因此在设置模式之前会发生特定的查找),但之后会发生"正确的"局部变量处理.

当实际调用所选模式函数时,有一个巧妙的事件序列值得详细说明.这需要我们了解一下"衍生模式"和"延迟模式挂钩"......

派生模式和模式挂钩

大多数主要模式都是用宏定义的define-derived-mode.(当然没有什么可以阻止你简单地编写(defun foo-mode ...)和做任何你想做的事;但如果你想确保你的主要模式与其余的Emacs很好地配合,你将使用标准的宏.)

定义派生模式时,必须指定派生的父模式.如果模式没有逻辑父项,您仍然使用此宏来定义它(为了获得所有标准的好处),并且您只需nil为父项指定.或者你可以指定fundamental-mode为父母,因为效果nil与我们大致相同,正如我们将要看到的那样.

define-derived-mode 然后使用标准模板为您定义模式函数,调用模式函数时发生的第一件事是:

(delay-mode-hooks
  (PARENT-MODE)
  ,@body
  ...)
Run Code Online (Sandbox Code Playgroud)

或者如果没有设置父级:

(delay-mode-hooks
  (kill-all-local-variables)
  ,@body
  ...)
Run Code Online (Sandbox Code Playgroud)

fundamental-mode它自己调用(kill-all-local-variables)然后在这种情况下调用时立即返回,将其指定为父项的效果等同于父项的效果nil.

请注意,在执行任何其他操作之前kill-all-local-variables运行change-major-mode-hook,因此这将是在整个序列期间运行的第一个挂钩(并且在前一主要模式仍处于活动状态时,在评估新模式的任何代码之前发生).

所以这是第一件事.非常最后,该模式功能就是打电话(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)
Run Code Online (Sandbox Code Playgroud)

怎么delay-mode-hooks办?它只是绑定变量delay-mode-hooks,由变量检查run-mode-hooks.当此变量为非变量时nil,run-mode-hooks只需将其参数推送到将来某个时间运行的挂钩列表,然后立即返回.

只有当delay-mode-hooksnilrun-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)
Run Code Online (Sandbox Code Playgroud)

回到局部变量......

这让我们回到after-change-major-mode-hook并使用它来打电话hack-local-variables:

(add-hook 'after-change-major-mode-hook 'hack-local-variables)
Run Code Online (Sandbox Code Playgroud)

我们现在可以清楚地看到,如果我们这样做,有两个可能的顺序:

  1. 我们手动更改为foo-mode:

    (foo-mode)
     => (kill-all-local-variables)
     => [...]
     => (run-hooks 'after-change-major-mode-hook)
         => (hack-local-variables)
    
    Run Code Online (Sandbox Code Playgroud)
  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)
    
    Run Code Online (Sandbox Code Playgroud)

这是一个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)))
Run Code Online (Sandbox Code Playgroud)

缺点呢?

主要的一点是你不能再改变使用局部变量设置其主要模式的缓冲区的模式.或者更确切地说,由于局部变量处理,它将立即更改回来.

这不是不可能克服的,但我暂时将其称为超出范围:)