首页
归档
友情链接
关于
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
菜谱
翻译
页面
归档
友情链接
关于
搜索到
102
篇与
的结果
2026-11-05
《elisp 每日一练》——深入第三方包管理机制
在前一篇文章中,我们了解了如何利用 package 的系列函数完成第三方包的下载安装、更新、删除、查看的相关功能。我们发现有时候必须调用 package-refresh-contents 刷新本地存储的信息。今天我带大家梳理一下Emacs是如何与 melpa、elpa 这种软件仓库交互的。让大家对此有一个更深的理解背景知识首先Emacs要查询远程软件包仓库,它查询的源来自 package-archives 中记录的url地址。所有的软件仓库都是通过一样的逻辑和Emacs进行交互。我们发现在清华源的官网上提示,在配置好了 package-archives 之后需要加上一句 (package-initialize)。这是为了帮助我们初始化已经安装的包。在早期的Emacs中,init.el 加载的时机早于安装的第三方包的加载时机。因此我们需要加上这一句确保第三方包能正常加载,之后才是通过 require 引用。而27之后启动逻辑变了。变成了先加载 earyl-init.el,然后加载第三方包,之后才是 init.el, 此时再调用 (package-initialize) 已经没有必要了。我们执行 package-list-packages 的时候,它会访问我们在 package-archives 中设置的远程服务器,获取包信息并在一个名为 *Packages* 的buffer中显示。如果网络不可用,它会访问本地的存储。同时它也会将获取的信息存储到本地。那么 package-list-packages 函数是否等同于调用了 package-refresh-contents 了。某种程度上确实是这样的,它们都会刷新本地缓存。但是区别在于后者会强制刷新本地缓存。前者没有保证一定会刷新。因此在需要确保刷新的场合最好还是调用 package-refresh-contents。本地缓存对于我们后面安装更新包十分重要。有时候我们会发现调用 package-list-packages 时显示包有新的版本,但是我们想执行 package-upgrade 时发现它无法更新。实际上我们每次调用 package-refresh-contents 时都是在下载一个名为 archive-contents 的文件,文件是一个s表达式,以list的形式记录了服务器中记录的包信息。我们可以查看 package-archive-contents 变量,找到它的内容。我随便找了一个我本地的包:(company-statistics #s(package-desc :name company-statistics :version (20250805 1524) :summary "Sort candidates using completion history" :reqs ((emacs (24 3)) (company (0 8 5))) :kind tar :archive #1# :dir nil :extras ((:url . "https://github.com/company-mode/company-statistics") (:commit . "120e982f47e01945c044e0762ba376741c41b76c") (:revdesc . "120e982f47e0") (:keywords "abbrev" "convenience" "matching") (:authors ("Ingo Lohmar" . "
[email protected]
")) (:maintainers ("Ingo Lohmar" . "
[email protected]
")) (:maintainer "Ingo Lohmar" . "
[email protected]
")) :signed nil)从这上面我们可以看出,它记录了包的版本、描述、url地址、依赖等等信息。后面Emacs要或者更新对应包都是读取本地信息然后去做的。当我们要执行安装动作时,它大概会取这些数据:根据 :archive 中记录的url地址下载对应的包根据 :kind 得知这是一个 tar 包将 tar 包下载并解压到 package-user-dir 记录的目录中,这个变量默认是 ~/.emacs.d/elpa ,以 包名-版本号 命名在真正加载包之前,会获取包信息中 :reqs 中记录的依赖按照上述过程安装依赖当所有依赖都安装了之后,会执行字节编译和生成 autoloads 文件, 并且将该目录所在路径加入到 load-path 中在安装成功之后,本次安装的所有包, 包括依赖包,信息都被记录在 package-desc 中这里又出现了一个新的变量 package-desc,它与我们前面提到的 package-selected-packages 有什么区别呢?一个区别是前者信息比较详细,后者只保存了安装的包名。最重要的区别是前者保存所有安装的包,包括根据依赖被动被安装的包。而后者只记录用户明确要求安装的包后续如果需要更新,Emacs会根据 package-desc 中记录的本地包的信息对比记录了远程服务器中各种包信息的 package-archive-contes,判断服务器上的版本是否比当前版本新。如果需要更新则按上面的流程进行更新。总结本篇我们详细讨论了Emacs在更新、安装包背后的一些动作。现在我们能知道 package-refresh-contents 背后实际在维护服务器上存储的包信息。所以每次安装和更新前都可以使用该命令进行一次信息的同步
2026年11月05日
1 阅读
0 评论
0 点赞
2026-11-04
《elisp 每日一练》——包管理入门
在前面一系列文章中,我们慢慢深入了elisp 编程的各个方面,相信各位读者现在应该有能力编写一些自己的脚本。对自己的工作流进行一些本地化的适配。但是在配置Emacs的过程中我们需要依赖大量的第三方包(当然不排除某些牛人用原生Emacs功能也能高效的工作)。所以我们接下来就是要面对安装第三方包的内容背景知识Emacs 内置了包管理系统 package.el, 我们在上一节了解到,package.el 包管理系统在启动时会扫描 ~/.emacs.d/elpa 目录,帮助我们配置并加速加载其中的插件。但是实际上它帮助我们实现包的下载、安装、更新等等一系列工作。我们先来看看它是如何帮助我们下载包的一般来讲,包是从互联网中专门负责管理包的服务器上下载下来的。在Emacs中有官方的 elpa 和非官方的 melpa。ELPA 通常特指 GNU ELPA (Emacs Lisp Package Archive),是 Emacs 的官方默认仓库, 它主要提供稳定的发布版本,更新相对稳健。MELPA (Milkypostman’s Emacs Lisp Package Archive), 由社区驱动, 它通常直接从上游源码(如 GitHub)自动构建。相对活跃,但是对包的稳健性要求不那么高。我们可以获取最新的开发版,但是这通常意味着一些bug。通常还有一个仓库 —— NonGNU ELPA,相对于 GNU ELPA 来说,它里面的包法律要求相对宽松。一般我们都会设置这三个仓库。由于众所周知的原因,连接内置的仓库地址不那么稳定。一般的选择是连接国内的镜像站。我一般喜欢选择清华的镜像。我们可以在 清华elpa镜像帮助页 看到配置的代码(setq package-archives '(("gnu" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/") ("nongnu" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu/") ("melpa" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/"))) (package-initialize) ;; You might already have this line至于为什么后面要使用 package-initialize 函数,我们会在后面讲解。在这里请各位读者暂时停下来思考一下,这段代码是应该放到 early-init.el 还是放在 init.el 中呢?根据官方文档的说明, package-archives 只影响新软件包的安装来源,不影响Emacs如何加载已经安装好的包。我们可以直接放在 init.el 中。early-init.el 设计的初衷是我们可以干预启动时包的加载过程,对于安装过程,我们无需干预。在配置完了package仓库之后,我们可以通过 package-list-packages 在交互式界面上查看仓库中存在的包。在该界面中可以使用下面的一些快捷键:i:标记(Install)某个选中的包准备安装。d:标记(Delete)某个包准备卸载。U:标记所有有新版本的包准备升级。x:执行(Execute)所有挂起的标记操作(真正开始下载、编译或删除)。r:刷新本地的包索引缓存(Refresh)。虽然有界面可以方便的进行操作,但是作为自己的配置来讲,还是希望能将安装的包固化下来,在新机器上能自动安装,否则安装的包一多,需要一个个的根据报错或者查看原来的配置代码来确定自己安装了哪些包,实在是一件费时的事。我们可以使用 package-install 命令来安装一个包。它接收一个包名的符号作为参数。如果想要更新已安装的包需要事先手动执行 package-refresh-contents,因为Emacs并不会主动同步服务器信息。Emacs更新包的时候是根据本地保存的版本信息来判断是否需要更新的。有时候Emacs存储的包信息落后于服务器,导致无法成功更新, 所以需要调用该函数重新获取服务器中保存的包的最新版本等信息。然后调用 package-upgrade 或者 package-upgrade-all。一个简单的想法就此产生了,我们可以在配置文件中写下类似的代码:(package-refresh-contents) (package-upgrade-all)确保每次Emacs启动时都自动更新所有包。但是刷新缓存和下载更新包是一个很缓慢的过程,在一定程度上会拖累我们的启动速度。而且相对尝鲜,稳定可能更重要。个中取舍由读者自行判断了。至于删除已经安装的包,我们可以使用 package-delete函数。另外随着我们使用Emacs的时间不断变长,肯定会出现某些包我们不再需要了。要么是出现更好的替代,要么包的质量有问题,又或者其他原因。总之有些包我们不再需要了,想要Emacs自动帮我们删除它。这个时候可以使用 package-autoremove。正常情况下我们通过 package-install 安装的包会被记录在变量 package-selected-packages。这个函数找出不在 package-selected-packages 中, 也不被其他包依赖的包,进行删除。所以要正常使用这个函数,需要手动的维护 package-selected-packages 变量。最后, 判断一个包是否安装,我们可以使用 package-installed-p,如果安装了,它会返回一个非nil值。否则返回nil练习我们利用今天所学,实现一个函数 my-require 它具有这样一个功能:会自动下载它所引用的包,如果已经安装,它会自动更新。(defun my-require(package &optional filename noerror) "自动安装或者更新所依赖的包" (unless (package-installed-p package) (package-refresh-contents t) (package-install package)) (if (package-installed-p package) (progn (package-refresh-contents t) (package-upgrade package))) (require package filename noerror))总结今天我们初步介绍了Emacs中使用package 相关函数进行包管理的一些操作,它们主要是:package-installpackage-refresh-contentspackage-upgradepackage-deletepackage-autoremovepackage-installed-p
2026年11月04日
3 阅读
0 评论
0 点赞
2026-11-03
《elisp 每日一练》——Emacs的启动加速
在前面的一系列博文中,我们接触到了两种能加速Emacs启动的方案:字节编译和 autoload 机制。今天我们从Emacs启动的角度理解它们是如何做到加速的。方便读者日后能更加灵活的使用这套机制。背景知识下面我们列举一些 Emacs 启动时的关键步骤,顺序大概是这么个顺序,但是某些我们并不关系的步骤没有列举在里面首先当然是我们点击 Emacs程序或者在终端输入 emacs 命令时,正式启动Emacs。early-init.el从 Emacs 27 之后引入了 early-init.el 。它早于传统的 init.el。它初始化的时机及早,它会在 Emacs 初始化包管理系统(package system)和图形界面(GUI)之前被加载。所以我们可以在它里面对GUI显示(如关闭滚动条、调整 Frame 尺寸)进行配置、优化垃圾回收阈值、接管包管理。早期配置入口来自 init.el。但是它加载的时机比较晚,通常在常规初始化完成之后。对于GUI界面来说,我们如果想要禁用滚动条,如果将代码放在 init.el中,那么它会加载GUI,此时GUI界面上有滚动条,随后又加载 init.el 的代码,发现我们要禁止加载滚动条,所以它又会取消滚动条显示,然后刷新界面。当然这是一个极快的过程,因此有时候我们可能会看到界面有一些闪烁。为了解决这些问题,所以提供了一种更早介入的手段。既然 early-init.el 初始化的时机较早,有些资源还没准备好,因此使用时不能无脑的将所有 init.el 中的内容都放到 early-init.el 中。例如,此时 GUI 尚未初始化,任何直接依赖图形界面特性的代码(如设置字体、创建复杂的 UI 组件)都无法可靠运行。包管理接着Emacs 会激活包管理系统。当然如果你想接管这个过程,可以在 early-init.el 中设置 package-enable-at-startup 为 nil Emacs会跳过包管理这步Emacs默认的包管理系统会调用 package-activate-all 激活第三方包。它会通过 package-user-dir 中记录的第三方包的目录进行扫描,并激活里面的每一个包。 我们可以通过 package-load-list 设置加载哪些包 对于每一个包,激活过程主要做两件事:更新 load-path,把该包的目录添加到 Emacs 的 Lisp 搜索路径中, 这样我们后续通过 load 或者 require 时才能找到具体的文件加载 autoload 文件,读取并执行该包目录下的 *-autoloads.el 文件。这个文件里存储了大量“函数占位符”,告诉 Emacs 某个函数将来需要时该去哪个文件加载。如果不存在 *-autoloads.el 文件就加载包的主代码文件。根据主代码文件中的 require 或者 load 加载里面的子代码文件如果执行主代码文件,就会经历执行s表达式,展开宏, 为里面的变量分配内存等操作,但是某些情况下有些函数用户可能并不使用,在启动时一股脑的执行会拖慢启动速度。因此一般都会采用 autoload 机制,以便启动时跳过,未来按需加载。init.el接着会进入 init.el 文件的加载。对于 init.el 中的内容,它会依次执行里面的 S-表达式。遇到 load 或者 require 根据 load-path 中记录的目录去寻找对应的文件,并执行。默认情况下,这部分即使我们在某个配置文件中写下了 autoload 标志,也无法做到启动加速。这是因为emacs 通过执行init.el 或者通过 init.el 中的 require load 加载其他配置时,是在执行 s表达式, 此时的 ;;;###autoload 对lisp计时器来说只是一个注释。真正想做到启动加载还是需要自己生成 autoloads.el 文件。自己生成的 *-autoloads.el 文件有两种方式:因为 *-autoloads.el 本质上还是一个elisp文件,所以我们可以自己手写。利用Emacs 自动扫描为我们生成一个本地的 loaddefs.el 文件我们可以下面的代码来扫描并加载对应的 loaddefs.el 文件;; 1. 指定你的本地插件根目录 (add-to-list 'load-path (expand-file-name "~/.emacs.d/lisp")) ;; 2. 定义自动生成 autoload 索引文件的路径 (setq custom-autoload-file (expand-file-name "~/.emacs.d/lisp/my-loaddefs.el")) ;; 3. 如果文件不存在则自动生成 (unless (file-exists-p custom-autoload-file) (make-directory (file-name-directory custom-autoload-file) t) (loaddefs-generate (expand-file-name "~/.emacs.d/lisp") custom-autoload-file)) ;; 4. 在启动时只加载这个轻量级的存根索引文件 (load custom-autoload-file t)上面代码我们本质上是通过 loaddefs-generate 为目录下 el 文件生成 loaddefs.el 文件。然后利用 load 加载对应的 loaddefs.el 文件。它与前面的 autoloads.el 文件一样,只是记录需要演示加载的函数信息。后面我们将原来 load el 文件改成了 load loaddefs.el 文件。从这里我们应该能深入的理解 autoload 机制,它并不是什么黑魔法,Emacs也无法做到在 require 或者 load 的时候识别 ;;;###autoload 注释,跳过这类函数的执行。它能做的仅仅是在扫描时识别注释并提取函数信息记录到另一个el文件。后面我们也是通过加载这个新生成的 el 文件达到加速的目的。我们无法通过加载原来的 el 文件加速。这就导致了一个问题,如果某个启动时必须执行的函数,与标记了 ;;;###autoload 的函数在同一文件下,而我们又恰好对这个文件生成了对应的 loaddefs.el 文件并加载了这个新生成的文件,这会导致这个必须执行的函数无法执行。所以在使用 autoload 机制时需要注意这一点,我们对于这两种函数应该分开放置Emacs 的新机制在早期的Emacs,对于我们自己的配置代码,需要手动的生成对应的文件。而在最新的 31 版本出现了 User Lisp Directory 机制。简单来说,它将我们放入 ~/.emacs.d/user-lisp/ 中的个人配置代码实现了自动生成对应的 autoload 配置文件。这样就不用我们操心它的生成问题,我们只需要在注释中加上 ;;;###autoload 标志。甚至我们都不需要显示的使用 load 去加载生成的文件,对于 autoload 机制的函数,Emacs会自动帮我们完成自动按需加载。但是对于那些必须启动就加载的代码,仍然需要使用 require 加载对应文件总结本篇我们通过Emacs的启动流程又进一步讨论了启动加速的一些方案:通过 early-init.el 进行优化配置通过 early-init.el 接管包管理系统,按真实流程优化对于第三方package,直接加载autoload 文件,这一步默认由包管理处理,不需要我们操心对于自己的代码,可以通过 loaddefs-generate 生成对应的文件,通过 load 加载对应的 autoload 文件而非原始的el文件如果你是Emacs 31 的用户,可以考虑将代码放入到 ~/.emacs.d/user-lisp/ 让Emacs帮我们自动完成延迟加载机制
2026年11月03日
0 阅读
0 评论
0 点赞
2026-10-30
《elisp 每日一练》——性能分析
在前面我们讨论了关于 elisp 的调试和异常相关的内容,关于工程化的内容还需要了解如何对自己的elisp配置进行性能分析,尝试解决某些影响使用的卡顿问题背景知识如果我们发现自己的elisp 代码执行卡顿,一般需要经历下面几个步骤来确定范围、分析问题、以及最后解决。首先可以使用emacs 内置的 Profiler 机制确定问题所在。该机制的启用配套使用的函数有 profiler-start、profiler-report、profiler-stop。一般使用的步骤如下:当你发现执行某个命令卡顿严重时,可以先调用 profiler-start 开启性能检测。它的签名如下(profiler-start MODE)MODE 参数可以是 'cpu、'memory 或者 'cpu+mem 之后我们可以执行导致卡顿的函数或者操作。接着调用 profilter-report 停止采样并弹出一个类似火焰图/调用树的剖析报告缓冲区。 分析完毕之后可以调用 profiler-stop 停止Emacs内部的采样,避免持续开销。我们以一个例子来说明如何解读数据(defun my-inefficient-string-join (n) (let ((res "")) (dotimes (i n) (setq res (concat res (number-to-string i)))) res)) (defun my-test-heavy-profiling-task() (interactive) (profiler-start 'cpu+mem) ;; 调用函数执行任务 (my-inefficient-string-join 5000) ;;停止任务并展示报告 (profiler-report) (profiler-stop))我们可以得到两个界面,一个显示cpu耗时、一个显示内存,它们的内容如下:46,779,444 99% - command-execute 46,779,444 99% - funcall-interactively 46,779,444 99% - execute-extended-command 46,779,444 99% - command-execute 46,779,444 99% - funcall-interactively 46,779,444 99% - my-test-heavy-profiling-task 45,218,433 96% + my-inefficient-string-join 1,560,380 3% profiler-report 631 0% profiler-start 46,984 0% Automatic GC 0 0% ... 46 100% Automatic GC 0 0% ...从这里我们可以看出耗时操作主要是调用 my-inefficient-string-join 它占据了96%的CPU时间。前面带有 + 的可以按 tab 进一步展开。如果我们进一步展开就可以发现,函数内部的主要耗时是调用 concat 函数。确定了可疑函数,我们就可以调用 elp 相关函数监控这两个函数的具体调用情况:定义一个存储函数符号的list 变量,表示需要监控哪些函数elp-instrument-list 开始监控,可以传入上一步定义的列表变量,或者默认使用 elp-function-list 中保存的函数elp-results 查看监控结果最后调用 elp-restore-function 恢复部分函数或者调用 elp-restore-all 恢复所有函数因此我们可以修改一下上述的函数,重点监控 my-inefficient-string-join(defun my-test-heavy-profiling-task() (interactive) (setq elp-function-list '(my-inefficient-string-join)) (elp-instrument-list elp-function-list) ;; 调用函数执行任务 (my-inefficient-string-join 5000) ;;停止任务并展示报告 (elp-results) (elp-restore-all))执行之后我们可以发现调用结果如下::my-inefficient-string-join 1 0.934222 0.934222 concat 5000 0.8685359999 0.0001737071结合具体代码来看,主要的消耗在 my-inefficient-string-join 内部调用 concat。concat 每一次的消耗可能并不多。事情真的是这样吗?elp 无法统计每一次调用concat 真实的消耗,上面显示的只是平均每次的消耗。也就是说它是通过 0.8685359999 / 5000 得到的平均值。实际上我们知道 concat 的实现机制是每次追加都会经历:分配新内存-->拷贝原始字符串到新内存-->删除旧的内存块-->追加字符。也就是说,随着原字符串越来越大,每次拷贝的开销会越来越大。也就说随着调用次数的增加,它的时间复杂度是 O(n²)。 为了验证这个,我们可以使用 benchmark-run 函数,它可以按照需求多次调用我们的函数,并输出调用结果。它的签名如下:(benchmark-run &optional REPETITIONS &rest FORMS)REPETITIONS 是一个数字表示应该运行后面的 FORMS 多少次。输出的结果是一个形如 (总耗时(秒) GC执行次数 GC总耗时(秒)) 的列表我们可以分别执行下面的测试代码(benchmark-run 1 (my-inefficient-string-join 1000)) (benchmark-run 1 (my-inefficient-string-join 2000)) (benchmark-run 1 (my-inefficient-string-join 4000))结果如下:(0.057176 2 0.05415199999999931) (0.159746 8 0.15689200000000003) (0.649431 36 0.6428240000000001)我们发现主要开销在GC上,调用次数是2倍增长,但是GC次数大概是 4倍增长符合我们上面提到的 O(n²) 的时间复杂度。所以对于这种情况我们不应该使用 concat 来连接。其实对于这种需要频繁插入新元素的场景,正确的做法是使用 list 作为基础数据结构。我们的上述代码可以改为:(defun my-efficient-string-join (n) (let ((lst '())) (dotimes (i n) (push (number-to-string i) lst)) (string-join (nreverse lst) "")))最终再次执行 benchmark-run 得到的结果如下:(0.032276 1 0.031180999999999237) (0.001953 0 0.0) (0.028748 1 0.02522500000000072)我们发现它的开销不再随着循环次数的上升而显著上升了。总结本篇我们通过一个例子,讨论了进行性能分析的一般方法。在此对上述方法进行总结:可以通过 profiler 机制分析某个操作具体卡在哪个函数, 配套的函数有:profiler-start、profiler-report、profiler-stop。然后通过 elp 系列统计函数的调用次数和消耗情况,对应的函数是: elp-instrument-list、elp-results、elp-restore-function 和 elp-restore-all最后针对具体函数可以使用 benchmark-run 单独多次执行表达式,获取性能信息
2026年10月30日
4 阅读
0 评论
0 点赞
2026-10-29
《elisp 每日一练》——调试
前面我们一直是通过message 打印信息,以及通过出错的调用栈来看问题在哪,这种对于一些简短的代码来说,人肉编译可能更快一些,但是一旦代码规模扩大,引入复杂的第三方包,可能就不那么直观了。而且有些elisp 函数返回的数据结构比较复杂,虽然文档有说明但是没有直观的看到具体数据,在使用它获取数据时仍然显得有点复杂。这个时候就可以祭出调试这个手段了。背景知识elisp 的调试目前没有好用的ide,可以让我们直接在某一处快速下断点。但是仍然有几种方式下断点进入调试状态。第一种方式是通过 edebug-defun 下一个函数断点,具体的操作是,我们要将光标放在被调试函数体的任意位置,然后执行 edebug-mode。只要函数被调用就会进入调试状态,此时会在当前buffer中启用一个 edebug-mode 的 minor mode。 我们可以通过下列快捷键进行调试:<SPC> 进入下一行i 进入被调函数的内部c 继续运行到下一个断点b 在光标所在位置下一个断点x 可以插入一个条件,只有条件为真时才启用断点q 退出调试模式e 对表达式或者变量进行求值,可以查看变量的值它的原理是在函数各个可能的位置进行插桩,插入调试钩子。代码运行到此处时就会触发钩子进入调试状态。后面如果修改了问题,不想再进入该函数调试,需要将光标放到函数内部,执行 eval-defun 取消之前的插桩edebug-defun 只能用来调试函数内部的代码,一旦出了函数就无法继续调试了,并且也看不到调用栈。因为它插入的调试钩子只在函数内部,如果想看到调用栈可以使用 debug-on-entry。此时我们操作的是弹出来的调用栈面板。可以在面板中按 d 进行单步求值。它没有 edebug-defun 那么直观的显示当前执行到了哪行代码,我们只能根据源码和面板的输出来判断。所以它只适合用来判断函数是否被调用,以及调用的栈是怎样的。不适合用来调试具体的函数代码。通过 debug-on-entry 下的断点可以使用 cancel-debug-on-entry 来取消如果你不想进行调试,而是希望打印目标函数每一次被调用时传入的参数以及返回的结果, 不进入调试, 可以使用 trace-function. 我们通过一个具体的例子来看它的输出(defun my-complex-data-processor (raw-list) "一个用于演示 调试的数据处理函数。" (let ((processed '())) (dolist (item raw-list) (let ((transformed (if (numberp item) (* item 2) (upcase item)))) (push transformed processed))) (nreverse processed))) (let* ((arg (list 10 "hello world" ?=)) (ret (my-complex-data-processor arg))) (setq ret (my-complex-data-processor arg)) (message "%s" ret))trace-function 会输出如下内容:1 -> (my-complex-data-processor (10 "hello world" 61)) 1 <- my-complex-data-processor: (20 "HELLO WORLD" 122) ====================================================================== 1 -> (my-complex-data-processor (10 "hello world" 61)) 1 <- my-complex-data-processor: (20 "HELLO WORLD" 122)前面的1 表示调用的深度,此时它被顶层代码调用,所以深度是1;-> 和 <-表示方向,前者表示调用函数,后者表示从函数出来第一行表示, 从顶层调用函数,参数为 (10 "hello world" 61),它对应着上面的let赋值第二行表示, 函数返回,返回值为 (20 "HELLO WORLD" 122)第三行表示, 第二次函数调用,此时参数为 (10 "hello world" 61), 它对应着后面的 setq第四行表示, 第二次调用的函数返回,返回值为 (20 "HELLO WORLD" 122)取消插桩我们可以使用 untrace-function除了这几种方式,我们还可以手动在对应代码位置插入 (debug) ,这样代码会在断点处断下来,然后弹出调用栈的窗口,它的效果就与 debug-on-entry 类似总结本篇我们介绍了几种elisp的调试方法,主要有以下几种:edebug-defun 调试函数, 不用时需要使用 eval-defun 解除debug-on-entry 会在函数被调用时弹出调用栈,不用时需要使用 cancel-debug-on-entry 解除trace-function 打印对于函数调用的信息,不用时需要使用 untrace-function 解除
2026年10月29日
3 阅读
0 评论
0 点赞
2026-10-28
《elisp 每日一练》—— 错误处理
前面我们各种练习编写的都是一些小的功能,20到50行代码的练习即使不进行错误处理,也能够很方便的定位到问题。但是如果我们希望功能更加强大,甚至需要依赖大量外部package时,情况就变得复杂起来,而适当的错误处理和提示能更快的帮我们排查问题,恢复现场。今天我们来讨论Emacs中的错误处理背景知识error 和 user-erro 报告错误Emacs代码出错时如果我们不干预,默认使用的是名为 error 的函数,它会终止程序并弹出调用的堆栈。它的签名如下:(error STRING &rest ARGS)它接收一个格式化的字符串,调用方式等同于 format 函数。例如下面的代码(defun my-find-file(file) (interactive "Ffile path: ") (if (not (file-exists-p file)) (error "invalid file path: %s" file) (find-file-noselect file)))如果我们输入一个不存在的路径,那么它会终止程序并弹出调用栈,直接调用,会得到如下的输出Debugger entered--Lisp error: (error "invalid file path: d:/test.txt") error("invalid file path: %s" "d:/test.txt") (if (not (file-exists-p file)) (error "invalid file path: %s" file) (find-file-noselect file)) my-find-file("d:/test.txt") (progn (my-find-file "d:/test.txt")) eval((progn (my-find-file "d:/test.txt")) t)这个信息对于调试来说非常有用,但是对于使用者来说却是不那么友好,如果想要更友好的信息,可以使用 user-error。它不会弹出这个恼人的错误栈信息,而是在底部输出信息并安全的终止程序。给各位读者提个醒:验证它们的行为需要通过 M-x 交互式的方式调用,如果直接在buffer中通过 (my-find-file "1.txt") 这种形式二者的结果是一样的。都会显示调用栈。如果你发现交互式二者都不显示调用栈,有可能是当前 debug-on-error 的值为 nil 我们需要将它赋值为 t。就像它的名字一样,这个标志用来在出错时显示调用栈进行调试,elisp的文档中提到 user-error 并不是错误,它主要是向用户报告操作失误,例如上面例子中用户选择了打开不存在的文件。condition-case 异常处理如果是代码内部出错,例如读取的json或者xml文件不完整,网络波动导致收包失败等等情况,就不合适使用 error这种函数来报告错误,在其他语言中一般采用的是try-catch 的方式,elisp中也有类似的机制。elisp中使用 condition-case(condition-case VAR BODYFORM &rest HANDLERS)如果我们类比其他语言中的异常处理,可以将上面的各个部分组织成try { BODYFORM } catch (exception VAR) { HANDLERS }根据这个结构,VAR 是代表异常的变量,它的结构为 (error-symbol . error-data) , error-data 是一个list,但是并没有详细规定list中的数据,因此我们无法事先判断list中各个数据成员。好在一般我们不需要通过这个结构来获取详细信息,可以使用 error-message-string。与其他语言类似,异常可以有不同的类型,也可以有对应的继承关系, 例如 file-error 下有具体的"派生类":file-missing、file-already-exists。而所有异常的"基类" 是 error , 这些类型对应着前面提到的异常结构中的 error-symbol 。它们只是一个符号并不是具体的数据结构。上面将它称为异常并不准确,它不仅仅可以捕获程序中出现的问题,而是可以捕获所有通过 signal 函数抛出的“条件”。我们可以通过 signal 函数抛出错误或者终止信息。为了照顾初学者,方便理解我们下文仍然采用异常这个称呼吧对于错误来说,我们使用 error 就能全部捕获但是对于用户操作的错误来说,例如用户手动终止程序,就不属于错误,这种情况我们要使用 'quit如果你想一劳永逸捕获所有情况,直接在 HANDLERS 中使用 t 即可。BODYFORM 是需要被包裹的真正执行的代码。HANDLERS 是具体处理异常的代码块。它的结构大致是 (error-symbol body)。表示 error-symbol 之类的错误交给 body 块处理。它对应着我们前面提到的异常结构 (error-symbol . error-data) 中的 error-symbol(condition-case err ;; 1. 保护区:可能会抛出错误的危险代码 (progn (json-parse-string invalid-json-string)) ;; 2. 错误处理器:当捕获到错误时执行 (error ;; err 的结构通常为 (error-symbol . error-data) (message "捕获到运行时错误: %s" (error-message-string err)) ;; 返回一个降级后的默认值 nil))我们可以多次使用这个结构,多次捕获不同类型的异常。但是与其他编程语言中的顺序类似,越是包含范围广的类型越要放在后面。例如 error 应该放在 file-missing 或者 error-symbol 这种错误之后signal 抛出异常在其他语言中,异常处理还有一个 throw 抛出异常的操作。我们可以使用 error 和 user-error。它们的运作原理与其他语言中的抛出异常类似,当通过 error 和 user-error 触发异常之后,会一级级往上找,看看是否有异常处理。如果被处理掉了,异常传递终止。如果直到最后没有被处理,则调用默认的异常处理,此时就会触发显示调用栈并终止程序的操作。其中 error 函数抛出异常类型中的 'error,user-error 会抛出 'user-error 类型的错误,根据继承关系,我们也可以通过 'error 来捕获。抛出异常,我们还可以使用更底层的 signal 函数。它的原形如下:(signal ERROR-SYMBOL DATA)第一个参数是错误的符号,第二个参数是错误的数据,数据是一个list,后续异常处理时可以使用这个list中的数据。例如上面的 my-find-file 可以进行改写(defun my-find-file(file) (interactive "Ffile path: ") (if (not (file-exists-p file)) (signal 'file-missing (list file)) (find-file-noselect file))) (condition-case err (my-find-file "d:/test") (file-missing (message "%s" (error-message-string err))))unwind-protect这个函数类似于异常处理体系中的 finally,它保证无论如何清理资源的操作都会执行。它的原形如下:(unwind-protect BODYFORM UNWINDFORMS...)BODYFORM 是真实执行业务层的代码,而 UNWINDFORMS 可以是多个语句,用来进行最终的清理工作。我们还可以与 condition-case 搭配使用,以达到其他语言中 try-catch-finally 的效果:(unwind-protect (condition-case err (progn ;; try 块 (my-risky-operation)) (error ;; catch 块 (message "捕获到错误: %s" (error-message-string err)))) ;; finally 块:无论 try 成功、抛错被捕获、还是错误穿透,都会执行 (cleanup-resources))总结本篇我们讨论了Elisp中异常处理,涉及的函数有:erroruser-errorcondition-caseerror-message-stringsignalunwind-protect
2026年10月28日
5 阅读
0 评论
0 点赞
2026-10-27
《elisp 每日一练》—— 动态作用域和词法作用域
在前面,我们从buffer、window到file,再到major mode 、minor mode 以及刚刚结束的定时器和进程调用。可以说基本将elisp编程涉及到的概念都介绍了个七七八八了。本专题将会是整个系列的最后一个专题,关于elisp代码的组织、工程化以及调试相关的内容。我们先从elisp的一种语法开始背景知识虽然本系列我假设各位读者对elisp代码有着一定了解,相信各位读者对所谓的动态作用域和词法作用域有一定的了解。先给各位读者提个醒,如果读者知道这二者的区别以及了解如何开启词法作用域,那么可以跳过本节。我们还是以下面这个经典例子开始(逢介绍动态作用域和词法作用域必会出现的例子):(defun foo(x) (getx)) (defun getx() x) (message "%s" (foo "hello,x")) ;; ==> "hello,x"或者这样的例子:(let ((x "hello, x")) (message "%s" (getx))) (defun getx() x)我们就以第一个例子为例来解释为何在动态作用域下能正常运行(第二个例子也是类似的效果)。调用函数 foo 时,进行符号 x 的变量绑定。后面调用 getx 的时候,getx 函数中没有对应的x 符号,此时它会往上找,找到了在 foo 中绑定的 x 变量,因此它会取这个 x 的值。在函数 foo 结束之后,函数中绑定的变量被销毁,函数调用结束之后就无法再使用里面绑定的变量。动态作用域的绑定规则:作用域从绑定时生效,到这段语句块结束时失效。期间所有对变量的访问都是运行时的"调用栈"上查找。从当前语句块往外围找,直到直到。这种绑定方式它只看调用栈不看定义的语义结构。这种也被称为 special variable你可以将这个过程想象成一个全局的栈,栈中的每一个成员都是一个符号名到绑定值得映射。当一个语句块定义了变量时往栈中压入一条记录,语句结束时销毁语句中绑定的所有变量。在查找变量时在栈中寻找,如果找到了就使用,否则报错。这种绑定模式可以实现这种隐式传参,但是容易出bug,也不利于函数的独立,增加了代码的耦合。因此新版的Emacs 推荐使用词法绑定。Emacs默认使用动态绑定,要开启语法绑定需要在文件的第一行用注释明确写上 -*- lexical-binding: t; -*- 。此时变量的作用域就跟我们熟悉的C/C++/Python之类的语言类似了。但是,开启 lexical-binding 之后并不能一劳永逸的解决所有潜在问题,为了加深理解,我们来看下面的例子;;; -*- lexical-binding: t; -*- (defun my-make-counter (initial-step) "创建一个闭包计数器,每次调用时以 INITIAL-STEP 递增。" (let ((counter 0)) ; 这个局部变量会被返回的 lambda 闭包安全捕获并长期持有 (lambda (step) (setq counter (+ counter step initial-step)) counter))) (setq c1 (my-make-counter 1)) ;;=> 1 (funcall c1 5);; -> 6 (funcall c1 5);; -> 11我们来分析一下,这段代码在动态作用域和词法作用域下不同的表现。在动态作用域下,c1 会调用 my-make-counter 函数。此时会先在栈中定义一个counter 符号并绑定到值0, 并返回一个使用 counter 的lambda表达式,在函数结束之后 counter 就被销毁了。此时lambda 表达式被绑定到c1变量。后面我们直接通过 funcall 调用 c1 里面绑定的lambda,因为没有经过 my-make-counter 对 counter 变量绑定,所以在动态作用域下,会报错: (void-variable counter)在词法作用域下,同样的 my-make-counter 函数会执行 counter 变量的绑定。并且在定义lambda 表达式时建立了对 counter 的引用。在函数退出时,因为lambda 中建立对 counter 变量的引用,所以该变量并不会因为函数退出导致被清理(请回忆一下垃圾回收的引用计数相关内容)。下一次在调用lambda表达式时它直接访问 counter 变量的当前值。完成我们想要的计数功能。我们再来看看这样一个例子:;;; -*- lexical-binding: t; -*- (defun my-bad-loop-closures () (let ((fns '())) (dolist (item '("A" "B" "C")) ;; 错误:这里的 item 是同一个变量的引用 (push (lambda () (message "Bad item: %s" item)) fns)) ;; 运行结果全都是 "C"! (dolist (f (nreverse fns)) (funcall f))))按照上面的思路来分析:在启用 lexcial-binding 之后,lambda 表达式中引用 item,但是循环执行完了之后,item因为被引用所以没有被销毁。在执行lambda 的时候item所在循环已经结束,此时item 的值是 C 所有上述三个lambda表达式输出的都是一样的结果。这也算是闭包的经典陷阱了。我们可以使用下面的代码来修改它:(defun my-good-loop-closures () (let ((fns '())) (dolist (item '("A" "B" "C")) ;; 正确:用 let 引入一个新的词法绑定,截获当前循环的值 (let ((captured-item item)) (push (lambda () (message "Good item: %s" captured-item)) fns))) ;; 运行结果会正确输出 A, B, C (dolist (f (nreverse fns)) (funcall f))))这里仅仅加了一层 let。为什么就解决问题了呢?let 实际上可以理解为创建变量并执行绑定,而dolist 每一次循环都相当于执行一次 setq。所以它们的区别就很明显了。使用 dolist 的时候只有一个变量,对这一个变量执行了3次 setq ,最终闭包中实际引用的是同一个变量。而使用 let 相当于建立了三个变量的绑定,但是三个变量都是用同一个变量名。因此在真正执行的时候,读取的是三个变量的值,结果自然就不同了。总结本节我们通过几个示例讨论了动态绑定和词法绑定在执行过程中的区别。并演示了词法绑定在闭包中要注意的问题。为了使各个模块独立,互补影响,我们应该养成在代码中使用 lexcial-binding 的习惯,毕竟你不想出现,外部随便定义一个变量,结果第三方库就出问题了。
2026年10月27日
3 阅读
0 评论
0 点赞
2026-10-26
《elisp 每日一练》——定时器与进程调用综合练习
在前面一系列文章中,我们讨论了定时器以及同步和异步方式调用进程。本篇是回顾和综合练习篇回顾首先我们介绍了两种定时器:run-at-time 创建普通定时器run-with-idle-timer 创建空闲定时器接着我们讨论了同步进程调用的方式:shell-command-to-stringprocess-linescall-process后面我们讨论了异步调用进程的方式早期版本使用 start-process 新版本使用控制粒度更细的 make-process最后介绍了两种解析json的方式:早期版本使用纯elisp实现的 json-read 和 json-read-from-string。后来新版本使用的接口是基于 Jansson 实现的 json-parse-string、json-parse-buffer练习本节的综合练习,我们来实现一个基于 curl 的异步http 调用功能。虽然 Emacs原生有一个 url-retrieve 的方案可以支持http的异步访问,但是它的问题也比较明显:相比较curl来说,它的使用显得十分诡异,异步函数 url-retrieve 在某些复杂场景并不能做到真正的异步。所以在一些对网络请求有较高要求的场景通常是通过调用 curl 程序实现通用的外部http请求。它的调用方式如下:(let ((target-url "https://api.ipify.org?format=json")) (my-async-http-request :url target-url :method "GET" :data nil :headers '(:Accept "application/json") :response (lambda (response) (condition-case err (let* (((json json-parse-string response :object-type 'alist)) (ip (alist-get 'ip json))) (message "解析成功:您的ip是 :%s" ip)) (error (message "json 解析失败"))))))header 是一个 alist / plist 结构,最终需要将这种风格的键值对转化为字符串形式的header参数,用来调用 curl。我们先封装一个函数用于将alist / plist 结构的header转化为对应的字符串(defun my--plist-to-curl-args(headers) "把 HEADERS(形如 (:Accept \"application/json\" :X-Foo \"bar\") 的 plist, 或 alist ((. \"Accept\") . \"application/json\") 结构)转换成 curl -H 参数列表。" (let ((args)) (cond ;; plist风格 (:Key "val" :Key2 "val2") ((and headers (keywordp (car headers))) (let ((rest headers)) (while rest (let ((k (car rest)) (v (cdr rest))) (push "-H" args) (push (format "%s: %s" (substring (symbol-name k) 1) v) args)) (setq rest (cddr rest))))) ;; alist 风格 ((\"Accept\" . \"application/json\") ...) ((listp headers) (dolist (pair headers) (push "-H" args) (push (format "%s: %s" (car pair) (cdr pair)) args)))) (nreverse args)))对于 alist 风格的数据转化比较好理解,它实际上就是一个列表,列表的每一项都是一个cons cell。所以我们只需要遍历它的每一子项,针对子项分别调用 car 和 cdr 就可以获取每一项。但是对于 plist 形式的数据处理的稍微复杂一些。我们在前面提到过 plist 本身也是一个个list, '(:key value) 的数据实际上是一个拥有两个成员的list,list的第一个元素是符号 :key,第二个是数据 value。所以我们可以使用通过 car cdr 分别取它的键和值,对于下一个键值对可以使用 cddr 获取。这里也是采用循环的思路获取每一项,需要注意的是 key 符号本身包含了最左边的 : 所以我们需要去掉它关于调用 curl 来说,跟我们之前编写的cmake调用的思路类似,这里就不多啰嗦了。(defun my-async-http-request (&rest param) "向远程接口发起异步HTTP请求 参数: :url 目标url (必填) :method HTTP 请求方法字符串,默认 \"GET\" :headers plist 或者 alist 结构,每一项对应着http header 中的一个键值对 :data 请求体 :response 接收响应 body 字符串的回调函数 " (let* ((url (plist-get param :url)) (method (or (plist-get param :method) "GET")) ;;默认是get请求 (headers (plist-get param :headers)) (data (plist-get param :data)) (callback (plist-get param :response)) (buf-name (format "*Async-HTTP-%s*" (format-time-string "%H%M%S-%3N"))) (output-buf (get-buffer-create buf-name)) (curl-args (list "-s" "-X" method url))) ;;拼接header 头部 (setq curl-args (append curl-args (my--plist-to-curl-args headers))) ;; 如果有请求的data,追加 -d 参数 (when data (setq curl-args (append curl-args (list "-d" data)))) ;;初始化输出缓冲区 (with-current-buffer output-buf (erase-buffer)) (message "正在向 [%s] 发起异步%s请求..." url method) (message "curl-args= %s" curl-args) ;;正式使用curl 发起请求 (make-process :name "my-async-curl-proc" :buffer output-buf :command (append '("curl") curl-args) :filter nil ;; 退出时根据退出状态负责调用用户回调 :sentinel (lambda (proc event) (when (eq (process-status proc) 'exit) (let ((exit-code (process-exit-status proc)) (buf (process-buffer proc))) ;;正常则读取返回的http响应包 (if (and (zerop exit-code) (buffer-live-p buf)) (let ((response-str (with-current-buffer buf (buffer-substring (point-min) (point-max))))) ;; 存在回调函数则调用回调 (when callback (funcall callback response-str)) ;;清理临时buffer (kill-buffer buf)) (message "请求失败,错误码:%d" exit-code) (when (buffer-live-p buf) (kill-buffer buf)))))))))这里我们将buffer的所有数据都通过一个参数传给回调函数,更加合理的设计是传入三个参数: status_code、response_header、response_data。调用者就不需要自己拆分http响应体的内容了。但是这部分的工作就交给各位读者去尝试了
2026年10月26日
2 阅读
0 评论
0 点赞
2026-10-24
《elisp 每日一练》——异步流式传输与 Process Filter
在之前一篇文章中,我们了解了通过Emacs异步调用进程,防止耗时进程卡住主界面。但是细心的读者会发现之前介绍的方法有一个很大的问题:我们的输出都是从buffer中获取的,创建buffer会额外消耗资源,另外如果要利用外部进程输出的内容做一些事情,我们需要固定的从buffer中读取。但是我们怎么知道buffer中有内容或者和上次相比有新内容出现,这就需要我们自己主动的不断去访问buffer。或者等待进程结束之后一次性读取所有内容。这样似乎又变成了需要等待的同步事件。对于这种场景,最理想的方式就是注册一个异步处理方式,只有在有新内容的时候触发。要实现这个需求,Emacs 为我们提供了 Process Filter 机制背景知识所谓的 Process Filter 是 Emacs 异步进程架构中的信息拦截器。它的工作原理是,Emacs调用进程时,进程的标准输出会被操作系统管道推送到Emacs端。Emacs 捕获到这些增量数据后,会立刻中断并调用你注册的 Filter 函数,将刚吐出的文本片段(String 增量)传递给你。如果不指定,那么会调用默认的 Filter 函数,该函数只是机械的将新内容写回到指定的buffer中。一个标准的 Filter 回调函数的签名通常如下:(defun my-custom-filter (proc output-string) )其中 proc 是当前的进程对象。而output-string 则是当前增量更新的程序输出。这里请注意, output-string 并不保证数据是完整一行或者是完整的结构,它只是机械的将本次输出的内容传进来。有时候数据会出现残缺,所以我们在处理的时候需要关注数据的完整性,有时候需要自己处理恢复完整数据,这算是另一种形式的“粘包”filter 函数的注册可以利用我们在前面介绍 make-process 的时候提到的 :filter 关键字练习我们对昨天的程序做一个简单的变更。cmake 编译会输出大量信息,一般来讲我们只关心当前编译的进度以及错误出现时的报错信息。如果cmake 大量输出,Emacs需要频繁往buffer中写入新内容并刷新显示可能会造成卡顿。因此我们利用filter实现按需分批次写入缓冲区的功能。(defvar my-build-progress nil "当前构建进度百分比,nil 表示未在构建。") (defun my-build-progress-filter (proc output) "节流并解析 OUTPUT 中的进度,同时把完整输出写入进程缓冲区。" (let ((buf (process-buffer proc))) (when (buffer-live-p buf) (with-current-buffer buf (goto-char (point-max)) (insert output)))) (let ((start 0) (last-pct nil)) (while (string-match "\\([0-9]+\\)%" output start) (setq last-pct (string-to-number (match-string 1 output))) (setq start (match-end 0))) (when (and last-pct (not (equal last-pct my-build-progress))) ;; 只有进度更新时才打印信息 (setq my-build-progress last-pct) (message "构建进度: %d%%" last-pct)))) (defun my-cmake-msvc-build (source-dir) "在 SOURCE-DIR 下执行 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)) ;; 错误缓冲区启用 compilation-minor-mode,支持跳转 (with-current-buffer err-buf (erase-buffer) (compilation-minor-mode t)) ;; 构建输出缓冲区也启用编译模式,兼容走 stdout 的链接错误 (with-current-buffer out-buf (erase-buffer) (compilation-minor-mode t)) (setq my-build-progress nil) (force-mode-line-update) (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 :filter #'my-build-progress-filter :noquery t :sentinel (lambda (p1 event1) (when (memq (process-status p1) '(exit signal)) (setq my-build-progress nil) (if (zerop (process-exit-status p1)) (message "构建成功") (message "构建失败,查看 *compile-error*") (display-buffer (if (> (with-current-buffer err-buf (buffer-size)) 0) err-buf out-buf)))))))) (message "CMake 配置失败,查看 *compile-error*") (display-buffer err-buf))))) config-proc))本次我们添加了一个名为 my-build-progress-filter 的过滤函数,它主要的作用是将新增的内容插入buffer、并且提取里面进度信息。虽然这里频繁的插入信息到buffer中,但是如果当前Emacs不显示buffer的话,它并不会立即重绘并刷新buffer,只有在调用 message 或者 force-mode-line-update 这种显式刷新的函数才会刷新当前buffer显示。这里只有在进度变化时才刷新,算是降低了刷新频率。后面编译进程相关的代码就与前面的类似。只是通过 :filter 指定了函数总结本篇我们讨论了 make-process 中的 :filter 用法,绑定一个函数作为回调,每当进程有新的输出时被调用,该函数可以进行数据的解读或者控制数据插入buffer中的行为。函数需要接受两个参数:proc 是对应的进程对象、output 是本次调用新增的字符串
2026年10月24日
4 阅读
0 评论
0 点赞
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 点赞
1
2
...
11