为use-package添加创建临时钩子的关键字

作为一个(被迫)在windows下使用Emacs的人, 我常常为emacs在windows下的龟速启动感到头大.

虽然emacs27的pdumper算是emacs启动速度的终极方案, 但是pdumper版本现在仍然有无法使用profiler的bug, 而且我也不是一个喜欢尝鲜的人. 受制于工作与学习原因, 换系统是不可能的, 因此, 剩下的方案就只有字节码编译配置和延迟加载了.

然而事情并不可能会如想象中那么简单, 有的包比如隔壁老王的aweshell, 加载的步骤无非就一句 (require 'aweshell). 这种情况下是没有办法通过正常的autoload系统来处理的, 而且aweshell必须在eshell运行前完成加载, 因此也不能简单的向 eshell-load-hook 添加钩子函数(这个钩子里的内容会在eshell完成加载后才运行).

再比如evil的一系列包, 最明显的莫过于evil-nerd-commenter. 陈斌为了保证没有evil的用户也能正常使用, 默认的情况下是不会加载 evilnc-comment-operator 的, 然而陈斌默认的按键设置于我来说又及其的感到不适. 只能手动 (require 'evilnc-operator) 自行绑定, 这种情况, 直接使用autoload系统也没有办法正常懒加载.

针对以上的情况, 最好的解决方案就是为函数/钩子 创建一个临时的函数(姑且叫做temp-loader)执行加载任务, 让这个函数/钩子第一次被触发的时侯,temp-loader也被触发进行配置, 然后temp-loader该自行销毁, 被GC回收. 为了方便, 我创建了一个use-pacakge:thook 关键字来统一处理这种情况.

使用方法


(defun cm/lazyload-aweshell ()
  (require 'aweshell))

;; 直接hook函数
(use-pacakge eshell
  :ensure nil
  :thook (eshell . cm/lazyload-aweshell))

;; 也可以把语句包在一个列表里, 会自动用一个`progn'包裹
(use-pacakge eshell
  :ensure nil
  :thook (eshell . ((require 'aweshell))))

(defun cm/test-hook ()
  (message "temporary message!")

(use-package evil
  :thook (evil-insert-state-entry-hook . cm/test-hook))

;; 也可以混在一起写
(use-pacakge eshell
  :ensure nil
  :thook ((after-init-hook eshell ielm evil-insert-state-entry-hook) . ((message "Test message"))))

注意: :thook 无法支持:hook那种把after-init-hook缩写成after-init的做法, 原因很简单, 如果同时有函数xxx-mode和钩子xxx-mode-hook 那不就乱套了?

代码

(eval-when-compile
  (require 'use-package)

  (defun use-package-normalize/:thook (name keyword args)
    (use-package-as-one (symbol-name keyword) args
      (lambda (label arg)
        (unless (or (use-package-non-nil-symbolp arg) (consp arg))
          (use-package-error
           (concat label " a <symbol/function> "
                   "or (<symbol/functions or list of symbols/functions> . <symbol or function>) "
                   "or list of these")))
        (use-package-normalize-pairs
         (lambda (k)
           (or (use-package-non-nil-symbolp k)
               (and k (let ((every t))
                        (while (and every k)
                          (if (and (consp k)
                                   (use-package-non-nil-symbolp (car k)))
                              (setq k (cdr k))
                            (setq every nil)))
                        every))))
         (lambda (v)
           (or (listp v)
               (use-package-recognize-function v)))
         name label arg))))

  (defalias 'use-package-autoloads/:thook 'use-package-autoloads-mode)

  (defvar cm/transient-hook-count 0)

  (defun use-package-handler/:thook (name _keyword args rest state)
    (use-package-concat
     (use-package-process-keywords name rest state)
     (cl-mapcan (lambda (def)
                  (let ((syms (car def))
                        (forms (cdr def)))
                    (when forms
                      (mapcar
                       (lambda (sym)
                         (let* ((fn (intern
                                     (format "cm/thook-fn-name-%d" cm/transient-hook-count)))
                                (add-clause (cond
                                             ((functionp sym)
                                              `(advice-add #',sym :before #',fn))
                                             ((symbolp sym)
                                              `(add-hook ',sym #',fn))))
                                (self-remove-clause (list (cond
                                                           ((functionp sym)
                                                            `(advice-remove #',sym #',fn))
                                                           ((symbolp sym)
                                                            `(remove-hook ',sym #',fn)))
                                                          `(unintern ',fn nil))))
                           (cl-incf cm/transient-hook-count 1)
                           `(progn
                              (fset ',fn (lambda (&rest _)
                                           ,(if (functionp forms)
                                                `(funcall #',forms)
                                              (cons #'progn
                                                    forms))
                                           ,@self-remove-clause))
                              ,add-clause)))
                       (if (use-package-non-nil-symbolp syms)
                           (list syms)
                         syms)))))
                (use-package-normalize-commands args))))

  (add-to-list 'use-package-keywords :thook))
1 个赞

你用的是不是假的 use-package

(use-package aweshell :after eshell)

还有这种操作 :joy:

看了下:after能应对的场景还是有点少, 这个轮子其实也算没白造(强行安慰)

我看了下use-package的:after也就是简单的包了eval-after-load而已, eshell貌似是emacs内建部分,启动时就会被认为已经require? 然后导致aweshell也一起加载了, 达不到懒加载的效果了.

最简单的, 在配置文件写入

(eval-after-load 'eshell
  (y-or-n-p "Continue?"))

直接就在加载过程中挂住了

use-package这部分的源码

1 个赞

顺道问个相关问题,如果我用use-package载入包B,但是我想让载入包B的时候自动载入包A,应该怎么做呢?如果我用after关键字,结果是由于包A没有加载,包B也不加载。

:preface

我不是很懂你的意思, 如果包A的autoloads被正确创建, 而且包B的配置里用了包a的函数, 那么就可以加载包B的同时自动加载包A了啊

Stil not the things I want

:preface只能创建静态的加载上下文, 并不能随心所欲的控制加载的时机.

比如某个包我为他创建了几个胶水函数方便我自己使用, 这种情况放在:preface里就比较合适

我已经从Windows版本的emacs换到WSL上的archlinux版emacs了。

  1. 开启WSL
  2. 下载安装archWSL: GitHub - yuk7/ArchWSL: ArchLinux based WSL Distribution. Supports multiple install. ,按照说明配置一下
  3. 下载打开wsl-terminal: GitHub - mskyaxl/wsl-terminal: Terminal emulator for Windows Subsystem for Linux (WSL) ,运行open-wsl.exe就能默认进入ArchLinux
1 个赞

:config里直接加(require ...)或者(use-package ...)

用我的lazy-set-key.el吧,不管啥包,第一次按快捷键的时候才加载

Autoload、Eval-after-load、Hook 已经满足不了你,需要用 Advice 来配置了。

一次性 Hook、Advice 以前想过,感觉有些疯狂,没用过。刚刚想了个一次性 Hook 的实现:

;; -*- lexical-binding: t; -*-
(defmacro chunyang-add-transient-hook (hook function &optional append local)
  (let ((wrap (make-symbol "wrap")))
    `(letrec ((,wrap (lambda ()
                       (funcall ,function)
                       (remove-hook ,hook ,wrap ,local))))
       (add-hook ,hook ,wrap ,append ,local))))

需要借助 Lexical Binding,不然得定义个有名字的函数。

测试:

(progn (chunyang-add-transient-hook 'foo-hook (lambda () (message "+> a")))
       (chunyang-add-transient-hook 'foo-hook (lambda () (message "=> b")))
       (run-hooks 'foo-hook)
       foo-hook)
;; => nil
1 个赞

我觉得use-package默认的 keywords 已经满足楼主需求了呀。 preface, after, requireconfig 这些还不够吗?

你看我回复ldbeth那里就知道了,eshell这个东西水土不服,逼我开大招(我有优化强迫症):joy:

我这里有个类似的

(use-package eshell-z
    :hook (eshell-mode
           .
           (lambda () (require 'eshell-z)))))

你这直接挂这个钩子是方便了, 但是每次运行eshell的时候钩子都会被触发, 就显得不干净了. 而且你这个钩子也是在eshell加载完之后才会被运行. 老王的aweshell必须在eshell加载完成前就被require才会生效(否则第一次加载的时候是没有aweshell的). 我能想到的方式就只有adviceeshell的入口函数了

直接来也不是不行

(use-package eshell
  :load-path "site-lisp/lazycat-collection/aweshell"
  :preface
  (defun cm/lazyload-aweshell ()
    (require 'aweshell)
    (advice-remove #'eshell #'cm/lazyload-aweshell))
  :init (advice-add #'eshell #'cm/lazyload-aweshell))

但是lisp的宏功能那么厉害, 这个懒我为什么不去偷呢(滑稽)


(defun myeshell ()
  (interactive)
  (require 'aweshell)
  (eshell))

我的配置

(use-package aweshell
  :commands (aweshell-new aweshell-next aweshell-prev aweshell-toggle))

aweshell-toggle很方便的