首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
302 阅读
2
从零开始配置 vim(15)——状态栏配置
176 阅读
3
nvim番外之将配置的插件管理器更新为lazy
165 阅读
4
2018总结与2019规划
154 阅读
5
wsl 启动报错:Wsl/0x80080005
132 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
emacs
elisp
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
471
篇文章
累计收到
33
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
471
篇与
的结果
2026-10-23
《elisp 每日一练》——异步进程基础与生命周期管理
在上一节,我们讨论了Emacs中同步方式调用进程,它比较简单,适合那些能快速返回的命令行程序,对于一些耗时操作,如果下一步确实依赖耗时操作的返回,那么等待可能是必须的。但是某些时候,如果因为一些功能增强的后台应用导致界面卡顿,例如拼写检查、lsp 代码分析 如果耗时太久反而影响用户编写代码的体验。对于这种我们可以考虑将它们放到后台作为异步进程背景知识Emacs 提供了两个启动异步进程的函数: start-process 和 make-process。前者是早期Emacs提供的方式,它比较简单和传统,函数签名如下:(start-process NAME BUFFER PROGRAM &rest PROGRAM-ARGS)它的功能比较简单,它启动一个异步进程并将进程输出放到指定的buffer中,这种情况下我们无法对进程生命周期和输出进行精准的控制,如果需要更精确的控制,更推荐使用 make-process(make-process &rest ARGS)它的签名看着简单,似乎只有一个 ARGS 作为可选,但是实际上可选的参数十分丰富。我们传入一些关键字参数来精确的控制异步进程。最关键的几个参数如下::name 进程名称,Emacs必要时会自动修改这个名称,确保唯一;请注意这个名称是我们为这个进程对象取得名字,并不是可执行程序得名称:command 这个是进程真实执行得命令行,可以是一个个命令行参数组成得字符串列表:buffer 指定进程输出关联的缓冲区,可以是缓冲区名称或者对象:filter 输出过滤函数,如果指定了函数,有输出时会调用该函数进行过滤,并将过滤后的内容输出到指定buffer中:sentinel 进程状态变更的回调函数:stderr 标准错误输出的buffer调用这些函数之后,会返回一个进程对象。我们后面可以以对象作为参数,调用下面的函数来控制它的生命周期:process-live-p: 检查进程当前是否还在活跃运行process-status: 获取进程状态,例如 'run、'exit、'signalinterrupt-process: 向进程发送中断信号kill-process: 强行终止进程练习我们实现一个调用cmake编译的小插件。假设我有一个项目在Windows上,我执行编译的命令如下:cmake . -B build -G"Visual Studio 18 2026" -T "v143" -A x64 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL cmake --build build将编译的正常输出放到名为 *compile-build* 的buffer中,将错误和警告信息放到 *compile-error* 我们可以在该buffer中开启 compilation-minor-mode,这样我们就可以根据行号快速打开报错的位置(defun my-cmake-msvc-build (source-dir) "打开当前buffer所在目录,执行cmake编译" (interactive "D选择项目根目录:") (let* ((default-directory (file-name-as-directory source-dir)) (build-dir "build") (out-buf (get-buffer-create "*compile-build*")) (err-buf (get-buffer-create "*compile-error*")) (config-proc nil) ;; 构建进程 (build-proc nil)) ;; 编译进程 (with-current-buffer err-buf ;;启用 compilation-minor-mode 支持从错误信息跳转到具体位置 (erase-buffer) (compilation-minor-mode t)) (with-current-buffer out-buf (erase-buffer)) (message "开始cmake 配置") (setq config-proc (make-process :name "cmake-config" :command (list "cmake" "." "-B" build-dir "-G" "Visual Studio 18 2026" "-T" "v143" "-A" "x64" "-DCMAKE_BUILD_TYPE=Debug" "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL") :buffer out-buf :stderr err-buf :noquery t :sentinel ;; 在构建成功后执行编译 (lambda (p event) (when (memq (process-status p) '(exit signal)) (if (zerop (process-exit-status p)) (progn (message "构建成功,开始编译...") (setq build-proc (make-process :name "cmake-build" :command (list "cmake" "--build" build-dir) :buffer out-buf :stderr err-buf :noquery t :sentinel ;; 通知结果 (lambda (p1 event1) (when (memq (process-status p1) '(exit signal)) (if (zerop (process-exit-status p1)) (message "构建成功") (message "构建失败,查看*compile-error*") (display-buffer err-buf)))))))))))) config-proc))这个例子,我们演示了 make-process 的基本用法,并且给出了状态变更的回调函数的使用。在Windows上实测时因为msvc将编译时的错误信息和正常输出都作为标准输出了,所以读者可能会发现报错出现在 *compile-build* 缓冲。为了适配Windows,读者可以尝试在 *compile-build* 也加上 compilation-minor-mode总结本节我们介绍了异步方式调用的进程的方法,本节的相关函数如下:make-processprocess-live-pprocess-statusinterrupt-processkill-process
2026年10月23日
7 阅读
0 评论
0 点赞
2026-10-22
《elisp 每日一练》—— 同步进程调用
在前面我们了解了定时器的使用机制,但在日常使用中,通常是利用Emacs配合外部命名行工具的方式使用。所以我们今天讨论如何利用Emacs调用外部命令行工具基础知识我们今天主要讨论同步的方式调用,虽然它会阻塞Emacs的主线程,但是对于搜索、编译等工作来说,我们可能就是需要等待它们的执行结果。这种情况下使用同步的方式,反而简单一些。对于同步方式调用,Emacs提供了三个不同的接口。shell-command-to-stringprocess-linescall-processshell-command-to-string 的签名如下:(shell-command-to-string COMMAND)它通过系统的标准shell,执行 COMMAND 参数中输入的命令,并且等待结果,将结果以字符串的形式返回。当然我们也可以传入管道符号,对输出进行过滤,例如(shell-command-to-string "gcc -c main.c -o main | grep \\"error\\"")process-lines 与 shell-command-to-string 类似,但是它会自动将程序输出按行分隔为字符串列表返回(process-lines PROGRAM &rest ARGS)同时它也支持将程序的参数以多个字符串参数的形式输入。例如 (process-lines "git" "status" "--porcelain")call-process 是比较底层的函数,它不经过shell,直接创建一个新的进程,调用指定的二进制可执行程序,并将参数以列表形式逐个传入。它的签名如下:(call-process PROGRAM \&optional INFILE DESTINATION DISPLAY &rest ARGS)PROGRAM 是被调用的程序名称,如果是相对路径,Emacs 会在变量 exec-path 列出的目录中查找,需要注意的是这个参数应该只包含程序名,不能包含命令行参数INFILE 指定子进程的标准输入来源,如果为 nil 则从空设备(/dev/null) 中读取,否则应该是一个文件名,程序从指定文件中读取。DESTINATION 决定程序的输出,可以指定标准输出或者错误输出。DISPLAY 结果显示方式,如果是 nil,仅在函数完全结束后,结果才可见; 非 nil 会根据程序输出,即时刷新屏幕ARGS 是真正需要传递给子进程的参数DESTINATION 的取值可以有下面几个:缓冲区对象或名称,此时它会将输出打印到指定缓冲区光标所在位置的后面t 将输出插入到当前缓冲区中nil 丢弃所有输出0 丢弃输出并立即返回,不等待子进程结束。此时实际上是在异步执行,但是Emacs认为子进程已经执行完毕, Windows 上不支持(:file 文件名) 此时会将输出放入到一个指定文件中我们在上面说,它可以分别重定向标准输出和标准错误,它可以使用 (标准输出目标 标准错误目标) 这样的形式分别指定。各位读者如果对操作系统、子进程、shell等概念比较熟悉的话,应该很容易区分这些函数的使用场景。这里就不再深入讨论了。练习我们来实现一个简单的 git 信息显示的功能,以此来演示一下这些函数的用法。我们实现以下功能:通过 shell-command-to-string 调用 git branch --show-current 显示当前branch通过 process-lines 调用 git status --porcelain 获取当前修改的文件列表, 并显示到临时buffer中在用户选择上一步中某个文件之后,通过 call-process 调用 git diff, 查看差异(defun my/git-current-branch() (interactive) (let* ((output (shell-command-to-string "git branch --show-current")) (branch (string-trim output))) (if (string-empty-p branch) (message "当前目录可能不是一个git 仓库") (message "当前分支: %s" branch)))) (defun my/git-status() "在临时缓冲区中列出当前仓库修改的文件" (interactive) (let ((lines (process-lines "git" "status" "--porcelain")) (buf (get-buffer-create my/git-status-buffer-name))) (with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer) ;;插入提示信息 (insert (format "Branch: %s\n" (my/git-current-branch))) (insert (make-string 60 ?-) "\n") (if lines (dolist (line lines) (insert line "\n")) (goto-char (point-min))))) (pop-to-buffer buf))) (defun my/git-status--path-at-point () "从当前行解析出文件路径;若不是文件行则返回 nil。" (save-excursion (beginning-of-line) (let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) ;; 跳过头部信息行 (when (and (>= (length line) 4) (not (string-prefix-p "Branch:" line)) (not (string-prefix-p "---" line))) (let ((path (substring line 3))) ; 去掉 "XY " 前缀 ;; 处理重命名 "OLD -> NEW" (when (string-match " -> " path) (setq path (substring path (match-end 0)))) ;; 处理含空格的路径(porcelain 会用引号包裹) (when (string-prefix-p "\"" path) (setq path (string-trim path "\"" "\""))) path))))) (defun my/git-show-diff-at-point() (interactive) (let ((path (my/git-status--path-at-point))) (if (not path) (message "当前行没有文件路径") (let ((diff-buff (get-buffer-create my/git-diff-buffer-name))) (with-current-buffer diff-buff (let ((inhibit-read-only t)) (erase-buffer) (insert (format "diff for: %s" path)) (insert (make-string 60 ?-) "\n") (call-process "git" nil t nil "diff" "HEAD" "--" path) ;; DESTINATION 设置为t,直接显示内容到当前buffer (goto-char (point-min)))) (display-buffer diff-buff)))))总结本节我们了解了Emacs中同步方式调用进程的几种方式,它们各有千秋:shell-command-to-stringprocess-linescall-process
2026年10月22日
5 阅读
0 评论
0 点赞
2026-10-21
《elisp 每日一练》——定时器机制
在前面的一系列文章,我们介绍了hook和advice。现在我们开始进入下一个专题,关于定时器、外部进程调用的部分。Emacs本质上仍然是一个单线程同步的程序,一旦遇到比较耗时的操作就会卡死主界面。通过这一专题的相关技术,可以有效的缓解卡死的现象。背景知识我们首先要介绍的是里面的定时器机制。在常用的编程语言中我们都接触过定时器。它的使用非常简单,通过函数设置一个定时器,函数通常包括时间和回调。然后通过另一个函数启动定时器,在时间到达之后调用回调函数。Emacs也是类似的。启动定时器的函数是 run-at-time。它的签名如下:(run-at-time TIME REPEAT FUNCTION &rest ARGS)TIME 是相对时间,单位是秒;REPEAT 表示是否重复触发函数,如果为nil 触发一次并销毁,如果是秒数,则表示每间隔这段时间重复触发FUNCTION 表示触发的函数,后面的 ARGS 是传递给函数的参数函数会返回一个定时器对象,如果 REPEAT 参数是nil,在触发一次之后会自动销毁。或者我们可以随时手动调用 cancel-timer。销毁定时器使用 run-at-time 定义的定时器是在调用开始之后计时。Emacs还有一种空闲计时器,它是在用户停止操作一段时间之后执行操作。它用来执行一些后台的耗时操作,在用户不频繁操作之后即使操作耗时导致界面卡住,它的用户体验也会稍微好一点。这种定时器使用函数 run-with-idle-timer。函数签名如下:(run-with-idle-timer SECS REPEAT FUNCTION &rest ARGS)它的签名与 run-at-time 相同,但是要注意其中的 repeat 参数。run-at-time 中的 repeat 参数表示下一次经过多长时间后再触发,也是就说第一次触发后面触发的时间间隔可以不一样, repeat 参数实际上传入一个数值。但是 run-with-idle-timer 中没有这种机制, repeat 仅仅是一个bool 开关,用来表示下次是否重复。它不能用来控制下一次触发的时间间隔。练习我们来实现一个定时提醒喝水的功能,使用定时器定义一个时间,当时间到达之后提示用户该喝水休息了。当用户休息时,我们使用空闲定时器触发一个任务,统计当前编辑的buffer中的字数(defvar my-hydration-timer nil "定时喝水定时器对象") (defvar my-idle-audit-timer nil "用户空闲时,静默统计字数的定时器") (defun my-hydration-alert-callback() "定时触发提示,提醒用户该喝水了" (message "已经连续工作一段时间了,喝口水休息一下把")) (defun my-start-hydration-timer() "开启一个每隔30秒触发一次的循环提醒定时喝水" (interactive) (when my-hydration-timer (cancel-timer my-hydration-timer)) (setq my-hydration-timer (run-at-time 30 30 #'my-hydration-alert-callback))) (defun my-silent-heavy-audit() "定时器回调函数,会统计当前字数并输出" (when (derived-mode-p 'text-mode 'prog-mode) (let ((word-count (count-words-region (point-min) (point-max)))) (message "当前buffer 中共 %d 个字" word-count)))) (defun my-start-idle-audit-timer() "开启空闲定时器, 用户停止输入2s后会触发" (interactive) (when my-idle-audit-timer (cancel-timer my-idle-audit-timer)) (setq my-idle-audit-timer (run-with-idle-timer 2.0 t #'my-silent-heavy-audit)))总结我们今天主要介绍了两种定时器,一种常规的定时器用 run-at-time 启动。另一种是空闲定时器,用户空闲达到一定时间时触发,它使用 run-with-idle-timer 触发。
2026年10月21日
5 阅读
0 评论
0 点赞
2026-10-20
《elisp 每日一练》——hook 与 advice 综合练习
在前面的一系列文章中我们系统性的介绍了hook与advice 机制,现在我们来进行一个回顾和对应的综合练习回顾首先我们了解到Emacs中hook实际上就是一个存储了一个或者多个函数的变量。我们可以调用以下函数传入这些变量,Emacs会自动执行其中保存的函数:run-hooksrun-hook-with-argsrun-hook-with-args-until-successrun-hook-wrapped对于这个hook变量我们可以通过下面的函数添加或者删除hook函数add-hookremove-hookemacs 自身为我们定义了一系列的内置hook,我们选取了几个典型的例子:after-change-major-mode-hookkill-buffer-hookkill-buffer-query-functionsfind-file-hookbefore-save-hookafter-save-hookwindow-configuration-change-hook我们定义minor mode 和 major mode 时宏默认埋下了hook点。对于minor mode 来说:每次切换mode状态都会无条件的调用普通hook,但是如果我们关系开启和关闭状态,它还额外有调用off-hook 和 on-hook的机制;对于major mode,通过 delay-mode-hooks 保证其在执行初始化代码时没有hook运行,在初始化完毕之后,通过调用 run-mode-hooks 做到先执行之前被挂起的父类hook,最后执行子类hook关于 advice 就比较简单了,它主要通过 advice-add 和 advice-remove 来管理。它可以通过一些关键字详细的控制行为。练习我们本次的练习尝试结合hook 和 advice 机制。实现一个无感的自动备份插件,它的主要功能如下:通过 advice 在执行 write-region 保存的时候,创建一个带有时间戳的备份在备份完成之后,注册自定义hook,由hook负责对外输出保存的信息我们先定义具体的配置变量(defgroup my-safe-guard nil "自动备份系统" :group 'tools) (defcustom my-safe-guard-backup-dir (expand-file-name "~/.emacs.d/safe-backups/") "自动备份的存放路径" :type 'directory :group 'my-safe-guard) (defcustom my-safe-guard-audit-hook nil "当文件成功备份后触发该hook 函数接收三个参数:(FILE-PATH BACKUP-PATH TIMESTAME-STR)" :type 'hook :group 'my-safe-guard)接着我们来编写这个核心函数,它主要是通过 :around 关键字接管函数 write-region,在执行函数之前先备份源文件。备份成功之后执行原函数并触发hook,传入三个函数,外部可以接收这个事件,以便进行记录(defun my-safe-guard-backup-internal (orig-fun start end filename &rest args) (if (and filename (stringp filename) (file-exists-p filename)) (let* ((backup-dir my-safe-guard-backup-dir) (time-str (format-time-string "%Y%m%d-%H%M%S")) (safe-backup-path (expand-file-name (concat (file-name-nondirectory filename) "." time-str ".bak") backup-dir))) ;; 确保备份目录存在 (unless (file-directory-p backup-dir) (make-directory backup-dir t)) (copy-file filename safe-backup-path) ;;执行原函数 (let ((result (apply orig-fun start end filename args))) ;;触发hook (run-hook-with-args 'my-safe-guard-audit-hook filename safe-backup-path time-str) result)) ;;路径不是有效路径,直接执行原函数 (apply orig-fun start end filename args)))最后我们来注册advice 和 hook启用这些功能;; 挂载 :around advice 到 write-region (advice-add 'write-region :around #'my-safe-guard-backup-internal) ;; 监听hook: 这里简单的输出到message buffer (defun my-audit-listener-notify (file backup time) (message "保存成功, 文件 [:%s] 写入成功, 快照已归档" (file-name-nondirectory file))) (add-hook 'my-safe-guard-audit-hook #'my-audit-listener-notify)这里我们简单的演示了将信息写入到 *Messages* buffer 中,读者也可以进行扩展,弹框提示或者写入日志,但是一定要注意写入日志时要记得确保写日志的函数不能调用到 write-region,否则会造成死循环
2026年10月20日
5 阅读
0 评论
0 点赞
2026-10-19
《elisp 每日一练》——Advice 函数修饰机制入门
在前面我们了解了hook机制以及如何定义和执行hook。利用hook可以很方便的对一些事件进行处理。但是它最大的问题在于,需要程序的设计者预先留下可供hook的槽点。有时候设计者没有预留对应的hook,但是我们又需要在它执行的前后做文章、或者修改它的行为。这个时候我们可以使用Advice机制背景知识它可以在不修改原函数的情况下,为其附加额外行为。你可以把它理解为对函数进行“建议”或“增强”。它是一种可以不需要预先埋设hook点位的hook。现代Emacs更推荐使用 advice-add 和 advice-remove 进行添加和删除函数的advice。函数的签名如下:(advice-add SYMBOL HOW FUNCTION &optional PROPS)第一个参数 SYMBOL 是函数的符号。而HOW 则是advice接管函数的方式,它有以下几种形式:around, 这是最强大、最灵活的类型。我们传入的 FUNCTION 函数会完全接管原始函数,但原始函数会作为一个额外的参数 (orig-fun) 传递给你。此时在函数内部你可以自由的调整调用原函数的时机、参数等:before, 在原始函数之前执行 advice 函数, 两者接收相同参数,但返回值始终是原始函数的返回值。这类似于在函数入口处执行一个“前置动作”。:after, 在原始函数之后执行 advice 函数。两者接收相同参数,返回值仍是原始函数的返回值。这类似于在函数调用完成之后执行一个“清理动作”。:override, 完全替换原始函数, 它类似于使用 fset 替换函数符号绑定的函数,对于一些重要功能的函数,替换之后会出现大面积的问题,所以应该谨慎使用除了这四个基本的以外,还提供了 :after-until、after-while 这种根据原函数返回值决定是否调用advice函数和 :before-until、:before-while 这类根据advice 函数返回值来决定是否执行原函数的控制方式。advice 机制还能处理原函数的返回值和接收参数,可以通过 :filter-args 和 :filter-args。例如下面是一个控制原函数返回值的示例:(defun my-double (x) (* x 2)) (defun my-increase(x) (1+ x)) (advice-add 'my-double :filter-return 'my-increase) (my-double 3) ;; ==> 这里返回的值不再是6,而是7从这里,相比读者已经看出来它的弊端了。某些时候它会破坏原函数的行为,就像这个例子,如果函数的调用者不知道某个插件或者配置导致增加了函数的advice,他可能无论如何也无法理解为什么这里返回值跟预想中不一致。而且有时候advice使用不当会导致无限递归,或者其他致命的错误。所以在存在hook的情况下,优先选择使用hook,应该将 advie 作为没办法的办法。作为最后的手段。对于一些特别重要的函数,你不确定它是否存在 advice、可以使用 advice-member-p 进行判断。然后利用 addvice-remove 进行清理。练习本节我们来对某些重要的函数实现一个统计耗时的功能。例如我们以find-file-noselect 为例(defun my-timed-find-file-noselect (origin-fun &rest args) (let ((start (float-time)) (file (car args))) (unwind-protect (apply origin-fun args) (message "find-file-noselect %S: %.3fs" file (- (float-time) start))))) (advice-add 'find-file-noselect :around 'my-timed-find-file-noselect)这段代码不难理解,我们通过 :around 包裹,在函数调用之前记录一次时间,然后调用函数,在函数结束之后再调用一次。最终返回时间。上面代码调用原始函数的部分使用 unwind-protect,该函数是一种异常处理机制,你可以理解为一般编程语言中异常处理中的final。它保证BODY中的代码不论以何种形式退出,都会执行 CLEANUP 里面的代码总结我们今天了解了 addvice 机制。今天介绍的函数如下:advice-addadvice-removeadvice-member-p
2026年10月19日
7 阅读
0 评论
0 点赞
2026-10-18
《elisp 每日一练》——mode hook
在前面,我们分析minor mode 和 major mode 时忽略了hook相关的内容,一笔带过了。现在我们有了hook相关的知识,今天带着这些知识在回过头来看mode中的hook是如何被定义和调用的背景知识minor mode我们先从 minor mode 开始, 下面是我使用最基本的 define-minor-mode 得到的部分内容。在mode 的开关变量中我们发现下面几处与 hook 相关的内容(run-hooks 'my/focus-mode-hook (if my/focus-mode 'my/focus-mode-on-hook 'my/focus-mode-off-hook))run-hooks 后面可以接入多个hooks变量,它会依次执行变量中保存的hook函数。所以从这段代码中我们可以知道,不管当前是准备开启或者关闭minor mode,都会无条件的执行 my/focus-mode-hook。然后根据开启或者关闭的状态执行 my/focus-mode-on-hook 或者 my/focus-mode-off-hook在开关函数定义结束之后,还有下面的代码(defvar my/focus-mode-hook nil) (unless (get 'my/focus-mode-hook 'variable-documentation) (put 'my/focus-mode-hook 'variable-documentation "Hook run after entering or leaving `my/focus-mode'.\nNo problems result if this variable is not bound.\n`add-hook' automatically binds it. (This is true for all hook variables.)")) (put 'my/focus-mode-hook 'custom-type 'hook) (put 'my/focus-mode-hook 'standard-value (list nil))我们可以看到这里它采用 defvar 定义hook变量,但是后面又调用了 (put 'my/focus-mode-hook 'custom-type 'hook) 设置它在 customize 界面中的type。又设置了 standard-value 属性,这个也是 customize 变量的属性。实际上这里是将一个由 defvar 定义的普通变量转化成了 customize 类型的变量。也就是说Emacs区分变量是否是 customize 变量并不是根据是否调用 defcusom 定义,而是根据变量属性中是否有对应的属性。至于这里为什么不采用 defcustom 来定义,我也不太清楚,如果有了解的读者可以在评论区留言讨论。当然我们也可以在外部使用 defcustom 或者 defvar 定义同名的hook变量。后面的这些属性可以保证它被正确归类到 customize 变量中的 hook 类型。我们只看到了 define-minor-mode 定义 my/focus-mode-hook 变量,而没有定义 my/focus-mode-on-hook 和 my/focus-mode-off-hook。这意味着,开启和关闭的事件交由设计者自行处理,如果设计时需要关心开启或者关闭事件可以定义这两个变量,否则可以忽略。major modemajor mode 中我们已继承的模板进行展开,观察一下它里面有关hook的代码首先我们可以看到,启动函数中,大部分的代码被包裹在 delay-mode-hooks 中,它可以保证所有hook 在代码运行之后在运行。所以在它里面调用父类的mode启动函数并不会执行函数中定义的hook。此时父类的hook在 delay-mode-hooks 执行的最后被调用。而关于hook 的定义,我们可以找到(defvar my/foucs-mode-hook nil) (unless (get 'my/foucs-mode-hook 'variable-documentation) (put 'my/foucs-mode-hook 'variable-documentation "Hook run after entering `my/foucs-mode'.\nNo problems result if this variable is not bound.\n`add-hook' automatically binds it. (This is true for all hook variables.)"))也就是说在major mode中,它不再帮我们管理 hook 的定义,强行将它变为 customize 类型。或者换一个角度思考,对于 major mode中的hook,Emacs的设计者似乎更愿意使用 defvar 形式的普通变量。我们发现最终调用 hook时不再使用 run-hooks 而是调用 (run-mode-hooks 'my/foucs-mode-hook)。它与普通的 run-hooks 有什么区别呢?run-hook 会立即调用,但是 run-mode-hooks 更加复杂一点,我们这里不详细分析它的代码,我们只需要知道它配合 delay-mode-hooks 使用时,如果在 delay-mode-hooks 内部,它会将所有的hook暂时保存起来,在下一次调用 run-mode-hooks 时再调用。所以在这个基础上,我们可以这么理解调用hook的流程:调用父类的mode函数,最后会调用父类的 run-mode-hooks但是它父类的mode函数被放置在 delay-mode-hooks 内部,此时它会暂存需要执行的父类hookdelay-mode-hooks 内部开始执行子类的初始化代码子类的初始化代码执行完毕,此时跳出 delay-mode-hooks 宏在 delay-mode-hooks 宏外部,子类调用 run-mode-hooks此时会先调用之前被暂存的hook,然后调用子类的hook总结到此我们详细分析了major mode 和minor mode中有关hook的处理。现在总结如下:minor mode 中推荐使用 customize 类型的hook 变量,并且我们只要定义同名的hook变量,它会自动帮我们进行一些处理。调用hook的过程为,每次切换mode状态都会无条件的调用普通hook,但是如果我们关系开启和关闭状态,它还额外有调用off-hook 和 on-hook的机制major mode 中推荐使用 defvar 定义普通的hook变量,我们可以在外界定义同名的 customize 变量进行覆盖。在执行顺序上虽然会首先调用父类的mode函数,但是delay-mode-hooks 和最后调用的 run-mode-hooks 会保证在所有初始化都完成之后先调用 父类的hook、再调用子类hook
2026年10月18日
7 阅读
0 评论
0 点赞
2026-10-17
《elisp 每日一练》——自定义hook
在前两天的学习中,我们掌握了 Hook 的挂载控制以及系统内置的各类生命周期 Hook。但是在实际编程中仅仅依靠系统自带的hook是远远不够的,例如我们开发mode或者功能可能要定义自己的hook,以便使用者能更灵活的控制。本篇我们来讨论如何亲手为自己编写的插件定义专属的自定义 Hook,并在合适的时机优雅地触发它们背景知识在之前介绍hook的时候我们说过,hook本质上一个存储了一个或者多个函数的变量,通过 run-hooks 将hook 变量作为参数就能调用其中存储的函数。而使用 add-hook和 remove-hook 本质上也是在操作这个存储了函数的列表。所以自定义hook就比较容易理解了。我们只需要定义一个hook变量、然后在自己的代码中调用 run-hooks 或者 run-hook-with-args 这类函数即可。但是我们要陈清一点,如果你在Emacs中查看系统hook变量发现,它们实际上是使用 defcustom 定义的,因此我们定义自己的hook最好也使用 defcustom 定义,并且type属性应该设置成 'hook。hook 本身就是供用户使用,需要用户额外进行配置的功能,所以选用 defcustom 更能体现这一点。而运行hook,我们只需要在适当的时机调用 run-hooks 或者 run-hook-with-args 这类函数即可总结今天的内容比较简单也比较短小,本来一句话就能结束的。现在想想这些内容应该在前面介绍hook或者系统hook的之后顺便介绍一下的。但是那个时候我忘记提了。所以今天的内容就显得有点水分了。最终我们来总结一下今天的内容:自定hook,需要使用 defcustom 定义hook变量,然后在我们的代码中调用 run-hook 或者 run-hook-with-args。外界使用hook的时候可以使用 add-hook 往变量中添加函数
2026年10月17日
6 阅读
0 评论
0 点赞
2026-10-16
《elisp 每日一练》——系统内置hook
在前面一篇文章我们讨论了hook的基本概念,以及Emacs 中添加、移除hook的方法。现在我们开始慢慢深入理解hook背景知识查找hook的一般方法本文我们主要罗列Emacs系统中常用的hook,如果读者觉得啰嗦也可以跳过,在未来需要的时候,自己看文档。为了方便读者我们先谈谈查询自己想要的系统hook的一般方法。第一种方法是,通过 apropos 按事件名称搜索。当我们想要在某个事件上注册hook,例如创建一个buffer、打开文件、关闭buffer,这种情况下最直接的方法就是让 Emacs 自己去找。例如我想要看看与buffer相关的hook有哪些,可以输入 apropos 后面接着输入 buffer hook,就可以看到与buffer相关的hook。第二种方法就是查询官方文档,可以在Emacs中使用 info 命令打开官方文档,在文档中导航到 Elisp --> Standard Hooks 但是文档中列举的hook太多了,适合我们在不确定具体的名称,但是有一个大概的记忆时使用第三种方式,可以使用 describe-variable 命名,hook本质上是一个存储函数的列表变量,而且hook 一般会以事件或者触发的函数后面加上 -hook、-function 命名。例如 find-file-hook这种。我们可以尝试搜索一下对应的函数是否有类似的名称第四种方式自然就是当今的AI了,它的使用就不多说了。在了解了基本的查找方式之后,读者如果对下面的不感兴趣,可以跳过了,后面按照这些方式找到之后再在文档中详细了解。常用hook与 buffer 相关的hook有:after-change-major-mode-hook: 当一个 Buffer 的 Major Mode 彻底加载并初始化完成后触发。如果我们想为对应的major mode 搭配一些小功能的 minor mode, 例如行号、高亮当前行,可以在hook函数中开启对应的 minor modekill-buffer-hook 当一个buffer被销毁时被调用。释放与该 Buffer 绑定的后台临时文件、定时器或子进程句柄,防止资源泄漏kill-buffer-query-functions 在buffer即将被销毁前期调用,我们在上一篇的例子中使用过。与 文件读写相关的hookfind-file-hook 当我们使用find-file 成功打开一个文件并且创建了对应的buffer 之后被调用。before-save-hook 在文件正式被保存之前调用,我们可以用它来实现保存前格式化的操作after-save-hook 在文件被成功保存到磁盘之后执行。我们可以实现保存自动编译、推送文章上传到博客之类的操作与窗口布局相关的hookwindow-configuration-change-hook 在窗口布局发生变化,例如分屏、关屏、调整窗口大小时触发。我们用它来调整某些工具栏的窗口大小练习在昨天的练习,我们实现了关闭buffer时自动保存的功能,现在我们实现一个在保存前自动清楚末尾空白行和行尾空白字符的操作。但是我们只想在编程语言中启用。对于删除空白行和行尾多余的空格一个内置的函数 delete-trailing-whitespace。而编程语言的major mode 默认都是继承自 prog-mode,所以我们可以简单的使用 derived-mode-p 判断它是否继承自 prog-mode 来实现。因此我们可以使用如下代码:(defun my-auto-cleanup-whitespace () "在文件保存前,自动清理当前 Buffer 行尾的多余空白字符。" (when (derived-mode-p 'prog-mode) (save-excursion (goto-char (point-min)) (delete-trailing-whitespace)))) ;; 将其挂载到全局 before-save-hook (add-hook 'before-save-hook #'my-auto-cleanup-whitespace)总结本篇我们介绍了一些系统自带的hook、以及查阅它们的方式方法。目前ai普及的情况下,读者可以按照自己的需求使用ai帮忙查找对应的hook
2026年10月16日
8 阅读
0 评论
0 点赞
2026-10-15
《elisp 每日一练》——hook 机制详解
在前面的文章中,我们完成了对于Major Mode 和 Minor Mode 的学习。今天开始我们正式进入有关hook 和 advice 机制的讨论。背景知识什么是 hook?我们在各种编程语言中都见过hook,我个人理解的hook就是一组程序预留的插口,我们可以利用这些插口在程序生成某种行为时能自动执行自己的代码进行干预,以便修改原程序的行为。就像钓鱼一样,我们在对应水域下钩子,鱼群按照预定的流程在水中游动就像程序执行一样。在鱼上钩之后,作为钩子程序的钓鱼佬需要有所动作。这里的鱼群游动就是原程序,钓鱼佬就是钩子程序,下钩的构成被称为添加一个hook。但是达到这种执行自定义代码以便修改程序行为的方式还有一种就是回调。它们二者有什么不同呢?二者都是由原程序在特定时候主动调用。但是hook比起回调来将更为灵活。回调本质上是传入一个函数,在特定的时机由原程序主动调用。而hook是系统主动留下扩展槽位,使用者可以挂载多个hook函数,而调用者会一一调用这些挂载的函数。回调更像是一对一的约定,发生事件了触发单点,而hook更像是群发,事件发生了,所有注册了的都能收到通知。我现在也没有把握将它们很清晰的给各位读者进行区分,或者以更形象的例子让读者理解hook的概念,但是Emacs中使用hook并不需要对hook有什么很深的理解。我们只需要知道我们可以对对应事件注册一些hook,在事件发生时能调用这些hook就行了hook 分类Emacs中 Hook 的本质就是一个存储了函数列表的变量,当某个事件发生时Emacs会遍历这个变量里登记的所有函数。根据调用方式不同,Emacs中的hook可以分为两类:标准hook,命名一般以 -hook 结尾。例如保存文件之后的 after-save-hook、关闭window窗口的 quit-window-hook异常hook, 命名一般以 -functions 结尾,例如 find-file-not-found-functions这里的异常hook 只是调用方式不同,不一定代表发生了错误或者异常。调用 hook标准hook 中的函数不接受任何参数,函数的返回值被忽略,可以使用 run-hooks 执行。该函数的签名如下:(run-hooks &rest HOOKS)参数 HOOKS 是对应的hook 变量。它会依次执行变量中注册的所有函数。对于异常hook 来说,列表中的函数必须接收特定的参数,或者返回值会影响后续逻辑。触发这类hook 可以使用 run-hook-with-args、run-hook-with-args-until-success、run-hook-wrapped。run-hook-with-args 可以把相同的参数传给钩子列表中的每一个函数,并依次全部执行完。它的函数原形为:(run-hook-with-args HOOK &rest ARGS)run-hook-with-args-until-success 依次调用钩子中的函数,一旦某个函数返回非 nil,就立刻停止,并返回那个非 nil 值。它的函数原形为:(run-hook-with-args-until-success HOOK &rest ARGS)它适合在“尝试直到有人成功”的场景下使用。run-hook-wrapped 更底层、更灵活的运行方式。它不是直接调用钩子函数,而是把每个钩子函数交给一个包装函数去调用。(run-hook-wrapped HOOK WRAP-FUNCTION &rest ARGS)它会遍历 hook 中所有函数,将函数和对应的参数都交给包装函数 WRAP-FUNCTION。直到某个包装函数返回非nil则停止。可以将它理解为 run-hook-with-args-until-success 将hook 中的函数都加了一层包装。执行逻辑与 run-hook-with-args-until-success 相同。注册 hook我们可以通过 add-hook 往hook 变量中注册一个函数,它的原形是(add-hook HOOK FUNCTION &optional DEPTH LOCAL)HOOK 对应着hook 变量,存储了需要执行的函数FUNCTION 要挂载的函数,它是一个符号或者lambdaDEPTH 控制挂载顺序,默认是 0 或者 nil 表示追加到列表头部, t 或者 90 表示追加到 Hook 列表尾部,数值越小越早执行。我们可以使用这个参数精准的控制函数执行顺序LOCAL 如果为 t 表示注册的是一个buffer local 类型的hook函数,仅仅在当前buffer 中生效删除hook 函数从 hook 列表中删除某个函数可以使用 remove-hook,它的函数签名是(remove-hook HOOK FUNCTION &optional LOCAL)它的参数与 add-hook 类似。这里就不细说了练习我们使用hook 来实现一个简单的保存功能,在关闭当前buffer, 也就是调用 kill-buffer 时,如果当前buffer关联了具体的文件,我们就行保存操作。(defun my-save-buffer-on-kill() "关闭buffer时,如果buffer关联了文件且已修改,则自动保存" (when (and buffer-file-name (buffer-modified-p)) (save-buffer))) (add-hook 'kill-buffer-query-functions #'my-save-buffer-on-kill)这里我们使用的是 kill-buffer-query-functions 异常hook,根据Emacs的文档,在kill-buffer 执行的时候,它会首先调用 kill-buffer-query-functions 这个hook的主要作用是检查被关闭的buffer是否没有被保存,没有的话就弹出一个提示,让用户选择是否保存或者停止关闭的操作。我们的保存函数应该先于这个动作,因此这里不适合注册到 kill-buffer-hook。总结我们本节简单介绍了 hook 的基本概念,在Emacs中,hook就是存储了一组函数,由Emacs在适当的时机遍历并调用它们。今天介绍了几个函数:add-hookremove-hookrun-hooksrun-hook-with-argsrun-hook-with-args-until-successrun-hook-wrapped
2026年10月15日
5 阅读
0 评论
0 点赞
2026-10-13
《elisp 每日一练》——major mode 综合练习
截止到上一篇,我们完成了对major mode 相关概念的讨论,我们还是按照惯例先回顾一下前面的内容,接着来一个综合性的练习收尾知识回顾一个buffer 可以有多个 minor mode 但是有且只有一个major mode。我们可以通过 define-derived-mode 定义major mode。major mode 的定义具有继承的概念,一般是利用现在的major mode 扩展出新的。我们常用的基础major mode 有:fundamental-mode 最通用的major mode,Emacs新建buffer如果没有特殊指定,默认就是 fundamental-modetext-mode 一般的文本buffer都可以从它继承,它含有基础的单词、段落分割、拼写检查special-mode 它是只读交互buffer的基础, 绑定了 q 作为退出prog-mode 编程语言的基础mode、默认启用语法高亮、代码缩进引擎机制,绑定代码注释逻辑,排斥拼写检查等文本特征在定义我们的mode时,会管理 keymap、abbrev-table、syntax-table。它们都支持继承。继承方式各有不同:keymap 通过 set-keymap-parent 设置父类。abbrev-table 则使用 abbrev-table-put 为当前table 设置上 :parent 属性syntax-table 通过 set-char-table-parent 设置父类在设置完成之后,我们需要更新对应的buffer local 变量,更新的方式:(use-local-map my-code-mode-map) (set-syntax-table my-code-mode-syntax-table) (setq local-abbrev-table my-code-mode-abbrev-table)这些工作都由宏来解决,我们的工作就是定义与 major mode 同名的 map、syntax-table、abbrev-table 变量。宏会自动帮我们完成设置继承关系、更新buffer local 变量这些工作。major mode 的一个重要工作是进行语法高亮,我们利用 font lock 和 syntax-table 配合进行也可以使用treesit 来完成这个工作。如果使用传统的 font lock,我们需要定义一个由正则表达式到对应face的映射关系,并且在最后将这个关系赋值到 font-lock-defaults 上如果使用 treesit, 我们采用查询语句的方式进行高亮。我们需要利用 treesit-font-lock-rules 将查询语句和对应face的映射关系进行编译,生成一个对应的list列表,后面我们需要将返回的列表设置到 buffer local 变量 treesit-font-lock-settings 上。并且需要设置 treesit-font-lock-feature-list 填入对应的feature 开启上面设置的某条高亮规则。最后需要调用 treesit-major-mode-setup 启动这套规则最后我们可以通过 auto-mode-alist 设置文件关联,这样下次在打开对应后缀的文件之后会直接使用我们定义的mode练习这部分的练习,我们来实现一个简单的支持 markdown 高亮显示的 mode。它的功能如下:对一些元素进行高亮显示:标题按照不同字号加粗显示,支持一些特殊的字符效果,例如加粗、斜体、删除线我们添加一个 cb 快速创建table、<c 快速创建代码块定义一个函数并绑定快捷键为 M-<Enter> 实现列表/标题智能追加。对于高亮,我们先定义基本的face(defface my-md-title-1 '((t :height 1.4 :weight bold :inherit font-lock-keyword-face)) "一级标题 Face(大字号+加粗)") (defface my-md-title-2 '((t :height 1.2 :weight bold :inherit font-lock-keyword-face)) "二级标题 Face (中字号+加粗)") (defface my-md-title-3 '((t :height 1.1 :weight bold :inherit font-lock-keyword-face)) "三级标题 Face(中小字号 + 加粗)") (defface my-md-title-4 '((t :weight bold :inherit font-lock-keyword-face)) "四级标题 Face(加粗)") (defface my-md-bold '((t :weight bold :inherit font-lock-type-face)) "粗体 Face") (defface my-md-italic '((t :slant italic :inherit font-lock-variable-name-face)) "斜体 Face") (defface my-md-strike '((t :strike-through "gray50" :inherit font-lock-comment-face)) "删除线 Face") ;; 针对这些face,模仿官方的 font-lock-face 变量定义 (defvar my-md-title-1 'my-md-title-1) (defvar my-md-title-2 'my-md-title-2) (defvar my-md-title-3 'my-md-title-3) (defvar my-md-title-4 'my-md-title-4) (defvar my-md-bold 'my-md-bold) (defvar my-md-italic 'my-md-italic) (defvar my-md-strike 'my-md-strike)然后我们针对markdown中这些标识通过正则表达式定义匹配规则(defvar my-md-font-lock-keywords `( ;; 一级标题 ("^#\\s-+\\(.*\\)$" 0 my-md-title-1) ;;二级标题 ("^##\\s-+\\(.*\\)$" 0 my-md-title-2) ;;三级标题 ("^###\\s-+\\(.*\\)$" 0 my-md-title-3) ;;四级及更高标题 ("^####+\\s-+\\(.*\\)$" 0 my-md-title-4) ;;粗体 ("\\*\\*\\([^*]+\\)\\*\\*" 1 my-md-bold) ;; 斜线 ("\\*\\([^*]+\\)\\*" 1 my-md-italic) ;;删除线 ("~~\\([^~]+\\)~~" 1 my-md-strike)) "My-Markdown 的 Font Lock 正则样式规则")然后我们定义一个简单 syntax-table,我们只识别字符串这一种词法块(defvar my-markdown-mode-syntax-table (let ((st (make-syntax-table))) (modify-syntax-entry ?\" "\"" st) st) "My Markdown Mode 语法表")接着我们定义一个 abbrev-table 添加两条规则(defvar my-markdown-mode-abbrev-table "My Markdown Mode 缩写表") (define-abbrev-table 'my-markdown-mode-abbrev-table '( ;;输入cel 自动展开代码块模板 (create elisp) ("cel" "```elisp\n\n```") ;;输出 tbl 创建一个表格的表头 ("tbl" "| Header 1 | Header 2|\n|:-----|:-----|\n| Item1 | Item2|")))对于自动追加对应的单元,我们通过正则表达式实现这么一个函数:(defun my-markdown-insert-sibling-line() "在当前行的下方插入同类型、同级别的行" (interactive) (let* ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position))) (new-prefix (cond ;;无序列表, 以 - 、* 、+ 开头的行,前面可能带有缩进 ((string-match "^\\([ \t]*\\)\\([-*+]\\)\\([ \t]+\\)" line) (concat (match-string 1 line) (match-string 2 line) (match-string 3 line))) ;;有序列表 1. 或者 1) 等情形 ((string-match "^\\([ \t]*\\)\\([0-9]+\\)\\([.)]\\)\\([ \t]+\\)" line) (concat (match-string 1 line) (number-to-string (1+ (string-to-number (match-string 2 line)))) (match-string 3 line) (match-string 4 line))) ;; 标题 ((string-match "^\\(#+\\)[ \t]+" line) (concat (match-string 1 line) " ")) (t "")))) (end-of-line) (newline) (insert new-prefix)))我们实现了一个在这个函数,我们首先通过 buffer-substring-no-properties 获取当前行的文本,但是我们只需要文本而不关心它的属性。然后利用正则表达式进行匹配,分别尝试匹配各级标题、有序列表和无序列表。string-match 会将匹配结果存储,后续我们通过 match-string 按分组获取并最终合成一个对应的前缀。最后我们将光标移动到当前行的末尾并插入一行,然后在新行插入之前组织的前缀。最后我们对这个功能进行快捷键绑定(defvar my-markdown-mode-map (let ((map (make-sparse-keymap))) (keymap-set map "M-RET" #'my-markdown-insert-sibling-line) map))有了这些我们就可以组织成一个简单的 major mode(define-derived-mode my-markdown-mode text-mode "My Markdown" "简单的自定义 markdown major mode" (setq-default font-lock-defaults '(my-md-font-lock-keywords nil nil)) (abbrev-mode t) )根据前面我们分析的结果,只要 abbrev-table、syntax-table、keymap 符合默认的命名规则,define-derived-mode 会帮助我们处理这些,我们唯一需要额外设置的就是 font-lock 了。最后要启用缩写机制别忘了开启 abbrev-mode。我们可以用最后学到的 autoload 机制和文件绑定的方式,进行绑定。但是我这里就不再演示了。读者可以尝试使用它们使mode变得看起来更专业一些。总结到此为止,我们关于 minor mode 和 major mode 都结束了。但是我们在分析 define-derived-mode 和 define-minjor-mode 的时候忽略了hook。这部分我们将在接下来的hook专题中讨论。
2026年10月13日
5 阅读
0 评论
0 点赞
1
2
3
...
48