切换主要模式时如何保留目录局部变量? [英] How to keep dir-local variables when switching major modes?

查看:20
本文介绍了切换主要模式时如何保留目录局部变量?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我致力于一个标准缩进和制表符为 3 个字符宽的项目,它混合使用了 HTML、PHP 和 JavaScript.由于我对所有内容都使用 Emacs,并且只希望此项目使用 3 个字符的缩进,因此我在项目的根目录下设置了一个.dir-locals.el"文件以应用于其下的所有文件/所有模式:

<代码>;匹配项目的默认缩进每级 3 个空格 - 并且不添加制表符((无.((标签宽度 . 3)(c-基本偏移量.3)(缩进制表模式.零))))

当我第一次打开一个文件时它工作正常.切换主要模式时会出现问题 - 例如,处理 PHP 文件中的文本 HTML 块.然后我丢失了所有目录局部变量.

我还尝试明确说明我在.dir-locals.el"中使用的所有模式,并将dir-locals-set-class-variables/dir-locals-set-"添加到我的 .emacs 文件中目录类".我很高兴地说它们的行为都是一致的,最初设置目录局部变量,然后在我切换主要模式时丢失它们.

我使用的是 GNU Emacs 24.3.1.

在切换缓冲区的主要模式时重新加载目录局部变量的优雅方式是什么?

——编辑——感谢亚伦和菲尔斯的出色回答和评论!在这里发帖后,我觉得它闻起来像"一个错误,所以输入了一份报告给 GNU-将向他们发送这些讨论的参考.

解决方案

根据对 Aaron Miller 回答的评论,这里概述了调用模式函数时发生的情况(以及对派生模式的解释);手动调用模式与 Emacs 自动调用模式有何不同;在以下建议代码的上下文中,after-change-major-mode-hookhack-local-variables 适合于此:

(add-hook 'after-change-major-mode-hook 'hack-local-variables)

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

有关set-auto-mode如何选择调用模式的详细信息,请参阅Chig(elisp) 自动主模式 RET.它实际上涉及一些早期的局部变量交互(它需要检查 mode 变量,所以有一个特定的查找发生在模式设置之前),但是正确的"局部变量变量处理发生在之后.

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

派生模式和模式挂钩

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

定义派生模式时,必须指定它派生的父模式来自.如果模式没有逻辑父级,你仍然使用这个宏来定义它(为了获得所有标准的好处),你只需为父级指定 nil .或者,您可以指定 fundamental-mode 作为父级,因为效果与 nil 非常相似,我们马上就会看到.

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

(delay-mode-hooks(父母模式),@身体...)

或者如果没有设置父项:

(delay-mode-hooks(kill-all-local-variables),@身体...)

由于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 派生的,而 parent-mode 是从 grandparent-mode 派生的,当我们调用 (child-mode) 时,整个事件链看起来像这样:

(delay-mode-hooks(延迟模式挂钩(延迟模式挂钩(kill-all-local-variables) ;;运行 change-major-mode-hook,@祖父母身体)(运行模式挂钩'祖父母模式挂钩),@parent-body)(运行模式挂钩'父模式挂钩),@child-body)(运行模式挂钩'子模式挂钩)

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);;实际上做的第一件事(kill-all-local-variables) ;;<-- 这个函数,@祖父母身体,@parent-body,@child-body(run-hooks 'change-major-mode-after-body-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是自动选择的文件:

    (普通模式)=>(设置自动模式)=>(foo 模式)=>(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是否在主要模式更改后处理局部变量.如果模式更改由正常模式"触发,则通过建议禁用,因为在该实例中会自动处理局部变量.")(defadvice 正常模式(围绕 my-do-not-hack-local-variables-twice)防止‘after-change-major-mode-hook’处理局部变量.参见‘my-after-change-major-mode-hack-local-variables’."(让 ((my-hack-local-variables-after-major-mode-change nil))广告做它))(广告激活正常模式")(add-hook 'after-change-major-mode-hook'my-after-change-major-mode-hack-local-variables)(defun my-after-change-major-mode-hack-local-variables ()`after-change-major-mode-hook'的回调函数."(当 my-hack-local-variables-after-major-mode-change(hack-local-variables)))

这样做的缺点?

主要的问题是您不能再更改缓冲区的模式,该模式使用局部变量设置其主要模式.或者更确切地说,它会作为局部变量处理的结果立即变回.

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

I'm committing to a project where standard indentations and tabs are 3-chars wide, and it's using a mix of HTML, PHP, and JavaScript. Since I use Emacs for everything, and only want the 3-char indentation for this project, I set up a ".dir-locals.el" file at the root of the project to apply to all files/all modes under it:

; 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)
         ))
 )

Which works fine when I first open a file. The problem happens when switching major modes- for example to work on a chunk of literal HTML inside of a PHP file. Then I lose all the dir-local variables.

I've also tried explicitly stating all of the modes I use in ".dir-locals.el", and adding to my .emacs file "dir-locals-set-class-variables / dir-locals-set-directory-class". I'm glad to say they all behave consistently, initially setting the dir-local variables, and then losing them as I switch the major mode.

I'm using GNU Emacs 24.3.1.

What's an elegant way of reloading dir-local variables upon switching a buffer's major-mode?

-- edit -- Thanks for the excellent answers and commentary both Aaron and phils! After posting here, I thought it "smelled" like a bug, so entered a report to GNU- will send them a reference to these discussions.

解决方案

As per comments to Aaron Miller's answer, here is an overview of what happens when a mode function is called (with an explanation of derived modes); how calling a mode manually differs from Emacs calling it automatically; and where after-change-major-mode-hook and hack-local-variables fit into this, in the context of the following suggested code:

(add-hook 'after-change-major-mode-hook 'hack-local-variables)

After visiting a file, Emacs calls normal-mode which "establishes the proper major mode and buffer-local variable bindings" for the buffer. It does this by first calling set-auto-mode, and immediately afterwards calling hack-local-variables, which determines all the directory-local and file-local variables for the buffer, and sets their values accordingly.

For details of how set-auto-mode chooses the mode to call, see C-hig (elisp) Auto Major Mode RET. It actually involves some early local-variable interaction (it needs to check for a mode variable, so there's a specific look-up for that which happens before the mode is set), but the 'proper' local variable processing happens afterwards.

When the selected mode function is actually called, there's a clever sequence of events which is worth detailing. This requires us to understand a little about "derived modes" and "delayed mode hooks"...

Derived modes, and mode hooks

The majority of major modes are defined with the macro define-derived-mode. (Of course there's nothing stopping you from simply writing (defun foo-mode ...) and doing whatever you want; but if you want to ensure that your major mode plays nicely with the rest of Emacs, you'll use the standard macros.)

When you define a derived mode, you must specify the parent mode which it derives from. If the mode has no logical parent, you still use this macro to define it (in order to get all the standard benefits), and you simply specify nil for the parent. Alternatively you could specify fundamental-mode as the parent, as the effect is much the same as for nil, as we shall see momentarily.

define-derived-mode then defines the mode function for you using a standard template, and the very first thing that happens when the mode function is called is:

(delay-mode-hooks
  (PARENT-MODE)
  ,@body
  ...)

or if no parent is set:

(delay-mode-hooks
  (kill-all-local-variables)
  ,@body
  ...)

As fundamental-mode itself calls (kill-all-local-variables) and then immediately returns when called in this situation, the effect of specifying it as the parent is equivalent to if the parent were nil.

Note that kill-all-local-variables runs change-major-mode-hook before doing anything else, so that will be the first hook which is run during this whole sequence (and it happens while the previous major mode is still active, before any of the code for the new mode has been evaluated).

So that's the first thing that happens. The very last thing that the mode function does is to call (run-mode-hooks MODE-HOOK) for its own MODE-HOOK variable (this variable name is literally the mode function's symbol name with a -hook suffix).

So if we consider a mode named child-mode which is derived from parent-mode which is derived from grandparent-mode, the whole chain of events when we call (child-mode) looks something like this:

(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)

What does delay-mode-hooks do? It simply binds the variable delay-mode-hooks, which is checked by run-mode-hooks. When this variable is non-nil, run-mode-hooks just pushes its argument onto a list of hooks to be run at some future time, and returns immediately.

Only when delay-mode-hooks is nil will run-mode-hooks actually run the hooks. In the above example, this is not until (run-mode-hooks 'child-mode-hook) is called.

For the general case of (run-mode-hooks HOOKS), the following hooks run in sequence:

  • change-major-mode-after-body-hook
  • delayed-mode-hooks (in the sequence in which they would otherwise have run)
  • HOOKS (being the argument to run-mode-hooks)
  • after-change-major-mode-hook

So when we call (child-mode), the full sequence is:

(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)

Back to local variables...

Which brings us back to after-change-major-mode-hook and using it to call hack-local-variables:

(add-hook 'after-change-major-mode-hook 'hack-local-variables)

We can now see clearly that if we do this, there are two possible sequences of note:

  1. We manually change to foo-mode:

    (foo-mode)
     => (kill-all-local-variables)
     => [...]
     => (run-hooks 'after-change-major-mode-hook)
         => (hack-local-variables)
    

  2. We visit a file for which foo-mode is the automatic choice:

    (normal-mode)
     => (set-auto-mode)
         => (foo-mode)
             => (kill-all-local-variables)
             => [...]
             => (run-hooks 'after-change-major-mode-hook)
                 => (hack-local-variables)
     => (hack-local-variables)
    

Is it a problem that hack-local-variables runs twice? Maybe, maybe not. At minimum it's slightly inefficient, but that's probably not a significant concern for most people. For me, the main thing is that I wouldn't want to rely upon this arrangement always being fine in all situations, as it's certainly not the expected behaviour.

(Personally I do actually cause this to happen in certain specific cases, and it works just fine; but of course those cases are easily tested -- whereas doing this as standard means that all cases are affected, and testing is impractical.)

So I would propose a small tweak to the technique, so that our additional call to the function does not happen if normal-mode is executing:

(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)))

Disadvantages to this?

The major one is that you can no longer change the mode of a buffer which sets its major mode using a local variable. Or rather, it will be changed back immediately as a result of the local variable processing.

That's not impossible to overcome, but I'm going to call it out of scope for the moment :)

这篇关于切换主要模式时如何保留目录局部变量?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆