如何解决prog-mode-hook影响elisp代码加载问题

如果我在prog-mode-hook挂了一个xx-mode,那么每次加载elisp代码的时候,因为elisp buffer自动加载elisp-mode,它继承prog-mode,导致xx-mode也会立刻被加载。

如果xx-mode这个时候还没有准备好,我遇到的一个场景是,xx-mode引用了yy-mode.el里的变量yy-1,但是yy-mode.el还没有安装完毕。到了要编译yy-mode.el的时候,emacs加载yy-mode.el的此时xx-mode随着prog-mode-hook被使能,因为yy-mode.el还没有完成加载,可能导致xx-mode无法正常工作(找不到yy-1的定义)。

我想到的一个办法是把prog-mode-hook推迟到最后再挂上去,但是这个方案并不是那么牢靠。因为只要出现需要加载elisp代码的情况,这些minor mode都会被自动加载。有什么其它办法能规避这个行为吗?

说实话,没太能理解你的问题。是说 emacs 打开 elisp file,自动启动 xx,xx 依赖 yy,而编译 yy 的时候因为打开 elisp buffer 而循环引用了 yy?理论上应该不会有这种问题吧,编译时通过 xx 引用的 yy 应该是解释运行的,应该可以正常加载解释代码?

应该是这样的,我遇到的可能是一种更特殊的情况。这是我遇到问题的配置块:

(use-package company
  :ensure t
  :defines (company-backends)
  :preface
  (defun my:add-grouped-company-backends (backends)
    (add-to-list
     (if company-fuzzy-mode
         'company-fuzzy--backends
       'company-backends)
     (append backends
             '(:with company-yasnippet company-dabbrev-code))))
  :hook
  (prog-mode . company-mode)
  (emacs-lisp-mode
   . (lambda ()
       (require 'company-capf)
       (my:add-grouped-company-backends '(company-capf))))
  )

(use-package company-fuzzy
  :ensure t
  :defines (company-fuzzy-mode
            company-fuzzy--backends)
  :hook (company-mode . company-fuzzy-mode)
  )

在我昨天尝试部署这个配置的时候,加载company-mode就会报错,因为找不到符号company-fuzzy-mode的值导致company-fuzzy.el的编译失败。

这个问题要解决是很简单的,让company-mode在使用外部符号前检查符号的定义即可。但是这让我意识到一件事,在自动加载elisp代码的时候,company-mode也会被自动加载,如果company-mode出了问题,会影响到后续所有代码的加载。我是把prog-mode作为一个基础交互模式使用的,但emacs并不这么处理,而且也没有一个明确不同的交互模式。所以我想问的其实是:

有什么办法让company-mode只在与用户交互的情况下加载?

你不能在 my:add-grouped-company-backends 这个函数里面 require company-fuzzy 吗?

或者你在 company-fuzzy 的 use-package 里加一个 :after company 这样就等价于 company 在加载以后 company-fuzzy 就会立即加载。

不,我不希望在company的配置里假设company-fuzzy是存在的。

假如某一天我突然不想要company-fuzzy了,我不希望还要修改company的配置块。

我同样也不希望company-fuzzy假设company存在,虽然这里company-fuzzy本身是依赖company的。我想讨论的并不是这个例子本身的问题怎么解决。

(require 'company-fuzzy nil t)

我同样也不希望company-fuzzy 假设company 存在,虽然这里company-fuzzy 本身是依赖company 的。我想讨论的并不是这个例子本身的问题怎么解决。

那你就不要把 :after company) 加在你的 company-fuzzy use-package 配置里了。直接在你的 company 的配置里面加一行


(with-eval-after-load 'company
    (require 'company-fuzzy nil t)
)

这样不管你到底用不用 company 或者用不用 company-fuzzy,都不会导致你的配置报错。

有什么办法让company-mode 只在与用户交互的情况下加载?”

你的这个问题给了一个具体的例子。现在其实大家已经想到有别的办法解决你的这个问题了。所以最后其实就是我个人感觉你提出的这个问题有一点在钻牛角尖了,因为我想不到这个问题具体要解决什么。

请让我解释一下,我的想法是,原本挂hook启动company是为了延迟加载。但是因为emacs启动时就要加载emacs-lisp-mode,导致company每次一启动就自动加载了。但是company这类包是服务于用户交互的,在加载阶段不需要它介入。在不需要它的情况下,为什么还要加载它呢?

我猜一些major mode应该有区分出是否应该用于交互的方法,我想了解这个。

怎么才算是交互?

反正在我看来,你的例子里,emacs-lisp-mode启动就是为了交互的,因为要创建*scratch*

你的代码只是需要完善一下而已。

  ;; 把草稿buffer的默认模式设置为org-mode
  (setq initial-major-mode 'org-mode) ;;scratch in org-mode

我认为情况不限于scratch,比如在启动时拉缺少的包,调用byte-compile-file的时候,因为编译的单元是sexp而不是文件,需要把文件内容加载到elisp buffer里处理。

有的兄弟,有的。

但这仅限于判断有没有terminal

首先要定义你在本 context 下所指的交互到底是什么意思?

如果你指的定义是定义函数里使用的 (interactive) ,那么我认为最接近的应该是 this-command 变量。你可以用 (eq this-command 'emacs-lisp-mode) 来判断 emacs-lisp-mode 到底是被一个程序调用的,还是用户交互式的调用的。类似的三个变量还有 real-this-command last-commnd real-last-command。

原本挂hook启动company是为了延迟加载。

如果仅仅只是为了延迟加载,那么你可以用 pre-command-hook,像这样子:

(add-hook 'pre-command-hook
  (defun load-company()
    (company-mode +1)
    (remove-hook 'pre-command-hook 'load-company)
)

当然你也可以写成一个嵌套的 hook

(add-hook 'prog-mode-hook
          (defun load-company-1 ()
            (add-hook 'pre-command-hook
                      (defun load-company-2)
                      (company-mode +1)
                      (remove-hook 'pre-command-hook 'load-company-2)
                      )
            (remove-hook 'prog-mode-hook 'load-company-1)))

感觉,你的“交互”可能会很宽泛,但反过来,你的“非交互”看上去更容易处理一些?

什么情况下,你不希望让 hook 加载某个 mode。

(如果你的配置允许修改的话,)加个标记变量造一层 context,让你的配置代码在该 context 下执行如何?

比如:

;; -*- lexical-binding: t; -*-

(defvar x/non-interactive?) ;; has no value

(use-package company-mode
  :hook
  (emacs-lisp-mode
   . (lambda ()
       (unless (bound-and-true-p x/non-interactive?)
         (message (format-time-string "h [%Y-%m-%d %H:%M:%S:%6N]"))
         (message "enable company mode.")
         (company-mode)
         (message "enable company mode done.")))))

;; hook will not run
(let ((x/non-interactive? t))
  ;; (package-initialize)
  ;; (package-install ...)
  ;; (byte-compile-file ...)
  (with-temp-buffer (emacs-lisp-mode))
  (message (format-time-string "1 [%Y-%m-%d %H:%M:%S:%6N]")))

;; hook will run
(with-temp-buffer (emacs-lisp-mode))
(message (format-time-string "2 [%Y-%m-%d %H:%M:%S:%6N]"))

;; eval-buffer =>
;; 1 [2025-05-19 14:06:29:795315]
;; h [2025-05-19 14:06:29:798610]
;; enable company mode.
;; enable company mode done.
;; 2 [2025-05-19 14:06:29:812763]

谢谢,虽然chat也是这么回我的。

pre-command-hook可以把它推迟到第一次命令交互,但实际上交互也并不一定是这个时候开始的。repl也可以排成prel,让第一个print不需要read。

read产生相对长的交互阻塞,print本身也产生io阻塞。一般而言,最适合的位置是在第一个阻塞,可能是read,也可能是print。

就之前提到的byte-compile-file的问题而言,我觉得mode hook某种程度上被滥用了。目前我的想法是,重新考虑下所有我的配置里的hook是不是可以在某些情况下移除。但我还没有想到一个比较优雅的方式。

我使用 evil,所以补全框架我是放在了 evil-insert-state-entry-hook 里加载这个包,用类似 remove-hook 的技巧。

如果你不用模式编辑的话,也可以考虑给 self-insert-command 加一个 advice,在这个 advice 里加载 company,然后在这个 advice 里 advice-remove 自己把自己移除掉。

我想你的主意是很实用的,我考虑的是这样的实现:

(defvar my/interactive-p)

(define-minor-mode my:prog-mode 
  "An adapter for interactive minor-modes mounted on prog-mode."
  :lighter "")

(use-package prog-mode
  :hook (prog-mode
             . (lambda () 
                 (when (my/interactive-p
                   (run-hooks 'my:prog-mode-hook)))))

类似adapter的设计,再套一层,不要直接绑定到prog-mode上。

不清楚你的使用场景。我一般会把 package 先 download、compile,然后才 use-package 之类的(即假定所有 package 都已经安装完毕),还没太复杂;但考虑到你可能会用某种 on demand 的配置,所以…

我之前也期待 emacs 有某种内置区分 interactively 与否的变量,不过某些命令可能被代码(而不是用户)触发,感觉会很难找到满足各类用户的“交互”模式的范围、边界。

小心 void variable error ,my/interactive-p 是 unbound 的。

我担心的一个场景就是这样的。因为我使用package.el从elpa上游自动拉包,并且在配置里使用use-package让拉包过程自动化。我不希望拉包的过程中出现这样那样的冲突。

我最终决定引入一个小小的框架解决这个问题,即,建立被叫做hook decorator的,功能上接近class的东西:

  (defvar my/hook-decorators nil
    "A alist of hook decorators, items in (HOOK-NAME . LOADER).")

  (defmacro define-hook-decorator (mode &optional loader)
    "Define a hook decorator for given name MODE."
    (unless (symbolp mode)
      (error "%s is not a symbol!" mode))
    `(let ((decorator
            ',(intern (concat "my/" (symbol-name mode) "-hook")))
           (docstring
            ,(concat "Hook decorator for `" (symbol-name mode) "'.")))
       ;; define a variable for it using `defvar'
       (eval `(defvar ,decorator nil ,docstring))
       (let* ((orig
               ',(intern (concat (symbol-name mode) "-hook"))))
         ;; set a loader for that hook decorator
         (setf (alist-get decorator my/hook-decorators)
               (if (and ,loader (functionp ,loader))
                   ,loader
                 (lambda ()
                   (with-eval-after-load ',mode
                     (eval `(setf ,orig
                                  (nconc ,orig ,decorator))))))))))

  (defun my:inject-hook-decorators ()
    "Inject hook decorators to those hooks to be decorated."
    (mapc (lambda (decorator) (funcall (cdr decorator)))
          my/hook-decorators))

 (use-package emacs
    :hook
    (after-init . my:inject-hook-decorators))

推迟所有外部hook的绑定,把它先绑定到对应的decorator上,然后再逐个注入。

注入decorator的动作挂在after-init-hook上,这样可以保证启动过程中没有任何hook被实际绑定。在启动完成后hook被注入,此时加载这些hook是安全的,加载的具体时机则由loader函数确定。

define-hook-decorator可以自动生成一个loader函数,加载时机是mode对应的包加载之后。例如:

#f(lambda () [(orig prog-mode-hook) (decorator my/prog-mode-hook)]
        (eval-after-load 'prog-mode
          #'(lambda nil
              (eval (list 'setf orig (list 'nconc orig decorator)))))))

这样防止了出现关于这个包的变量依赖问题。

1 个赞