首页
归档
友情链接
关于
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-09-07
《elisp 每日一练》——文件属性
很抱歉,因为上个周末有一些事情要处理所以停更了两天。现在我们恢复更新。在之前我们介绍了文件读写的一些api,以及使用 with-temp-buffer 和 insert-file-content 实现低成本的文件读取。今天我们来介绍文件属性相关的内容背景知识铺垫文件属性获取到的信息与其他编程语言获取文件信息类似,我将它分为以下几种属性的操作:路径解析这类api我们需要传入一个无歧义的文件路径,但是路径可以不必真实存在。expand-file-name 将相对路径或者带有 ~ 家目录字符的路径转化为绝对路径file-name-directory 获取参数传入的文件路径所在的目录file-name-nondirectory 去掉文件路径中的目录,得到单纯的文件名file-name-extension 获取文件的扩展名file-name-concat 路径拼接路径拼接这部分,我们可以使用 expand-file-name 或者 file-name-concat,在Emacs 28以后推荐使用 file-name-concat 。它可以将传入的多个路径参数转化为一个完整的目录, 例如(file-name-concat "~" ".emacs.d" "init.el") ;; ==> ~/.emacs.d/init.el (expand-file-name "init.el" "~/.emacs.d") ;; ==> C:/Users/user/AppData/Roaming/.emacs.d/init.elexpand-file-name 最多只能接受两个参数,第一个参数是文件的相对路径,而第二个参数是文件所在目录的路径。从上面的例子中可以看出,file-name-concat 缺少了一个转化 ~ 字符的功能。对于Windows上来讲,有时候使用 ~ 作为家目录可能会出现问题,虽然环境变量中已经给出了家目录但是,Emacs真正在读取对应文件时不一定认,所以最好的办法是使用 expand-file-name 转化为Windows上的绝对路径文件状态与断言函数在获取文件之前我们可以快速的对上述文件路径进行快速校验:file-exists-p 文件或者目录是否存在file-regular-p 是否为普通文件,排除目录、符号链接、Socket等特殊文件file-symlink-p 是否为符号链接,如果是,则返回链接指向的真实文件的路径file-readable-p / file-writeable-p 文件是否具有读写权限文件属性读取当我们确认了文件真实存在之后就可以调用函数 file-attributes 获取文件的真实属性了。函数签名如下:(file-attributes FILENAME &optional ID-FORMAT)FILENAME 这个参数比较好理解,就是一个文件路径的字符串。我们需要重点关注一下 ID-FORMAT 这个参数。ID-FORMAT 参数用于控制返回的文件属主(UID)和属组(GID)信息的格式。它接受两个特定的符号作为值:integer 默认值,返回数字类型的用户ID和用户组IDstring 以字符串的形式尽量返回详细的用户名和用户组名我们以具体的例子来讲解(let ((file (expand-file-name "init.el" "~/.emacs.d"))) (message "%s" (file-attributes file 'string)))我们得到这样一个返回值(nil 1 user user (27294 6305 0 0) (27286 33330 0 0) (27258 57596 0 0) 1034 -rw-rw-rw- t 17451448556305724 1353222740)列表的第0个元素表示类型,t 代表着这是一个目录, nil 表示这是一个文件第1个元素表示链接数第2个和第3个表示用户和用户组,因为我们的 ID-FORMAT 指定了 'string 所以它以字符串的形式显示名称第4、5、6是一个时间戳,表示最后访问时间、最后修改时间和状态最后被修改的时间第7个元素是文件大小,单位是字节第8个元素是文件权限第9个元素是inode 节点第10个元素是设备标识符三个时间戳都是以 (SECOND MICROSECOND PICONANOSECOND PICONANOSECOND) 列表格式返回的,对这个时间戳可以使用 format-time-string 来将其转化为人类可以读的字符串(let* ((file (expand-file-name "init.el" "~/.emacs.d")) (attribute (file-attributes file 'string))) (message "last modified:%s" (format-time-string "%Y-%m-%d %H:%M:%S" (nth 5 attribute)))) ;; ==> "last modified:2026-09-01 15:43:46"既然上述信息都是固定的,那么我们就可以封装一些意义更明确的函数,例如 get-file-attribute-mode 之类的函数从返回的属性列表中获取文件权限信息。这个工作Emacs已经为我们做了,针对属性中的内容 Emacs 有一组以 file-attribute- 开头的函数,我们可以使用 C-h f 查看这一组函数练习我们使用上述知识点做一个练习,以 ls 的格式输出文件详细属性信息。(defun my/format-file-attributes (file-path) (let ((abs-file-path (expand-file-name file-path))) (if (not (file-exists-p abs-file-path)) (error "File :%s not exists" abs-file-path) (let* ((attrs (file-attributes abs-file-path 'string)) (modes (file-attribute-modes attrs)) (owner (file-attribute-user-id attrs)) (group (file-attribute-group-id attrs)) (size (file-attribute-size attrs)) (modified-time (format-time-string "%Y-%m-%d %H:%M:%S" (file-attribute-modification-time attrs))) (filename (file-name-nondirectory abs-file-path))) (format "%-11s %-8s %-8s %8d Bytes %s %s" modes owner group size modified-time filename))))) (defun my/show-file-attributes(file-name) (interactive "fFile Path:") (let ((buffer (generate-new-buffer "FileAttribute")) (attr-message (my/format-file-attributes file-name))) (switch-to-buffer buffer) (erase-buffer) (goto-char (point-min)) (insert attr-message)))总结本篇我们介绍了有关文件属性的一些函数,今天介绍的主要函数有:expand-file-name 将相对路径或者带有 ~ 家目录字符的路径转化为绝对路径file-name-directory 获取参数传入的文件路径所在的目录file-name-nondirectory 去掉文件路径中的目录,得到单纯的文件名file-name-extension 获取文件的扩展名file-name-concat 路径拼接file-exists-p 文件或者目录是否存在file-regular-p 是否为普通文件,排除目录、符号链接、Socket等特殊文件file-symlink-p 是否为符号链接,如果是,则返回链接指向的真实文件的路径file-readable-p / file-writeable-p 文件是否具有读写权限file-attributes 读取文件或者目录的属性关于属性读取,我们可以使用 nth 加上索引来获取列表中的内容,也可以调用 file-attribute 开头的一组函数来获取。
2026年09月07日
3 阅读
0 评论
0 点赞
2026-09-04
《elisp 每日一练》—— 低成本的文件读写
上一节我们了解了文件读写的一些api,并且通过一个保存灵感的小例子了解了Emacs中文本的操作。实际在Emacs中读写文本无外乎两种目的,第一种是用户需要编辑文本并最终保存,第二种是要读取文件内容作为参数或者内容完成后续的工作。例如利用配置文件控制Emacs的行为或者插件的行为。对于第一种我们可以使用正规的 find-file 和 find-file-noselect 将文件加载到一个buffer中,并且保存的时候使用 save-buffer 进行保存。但是这种方式太重了。Emacs在正式读写文件的时候有一些附加动作,如果我们只是简单的要里面的文本,这种开销有点太大了。今天我们主要介绍使用 with-temp-file 、insert-file-contents 、write-region 进行低成本的文件读写背景知识在上一节我们提到 visit 参数,但是我们没有详细展开,现在我们详细展开看看 find-file-noselect 在基础的读写需求上的额外操作。首先它会通过 visit 参数构建一个访问关系,它会创建一个新的缓冲区,或者复用一个已存在的缓冲区。最关键的是,它会将缓冲区与文件“关联”起来。这意味着它会设置 buffer-file-name 并记录文件的修改时间,后续Emacs就将这个buffer和对应的文件关联起来了。在读取文件的时候它还会额外进行编码转化,例如将文件由其他编码转化为utf-8,或者其他规定的编码。另外它也会进行文件格式的转化,也就是将DOS 格式的 \r\n 转化为 \n。在打开文件之后,它会根据文件的类型设置文件的Major Mode。例如python文件会设置 python-mode 在该mode下可能会进行格式化、语法高亮等等操作。在文件加载完之后,可能会调用 find-file-hook 让用户代码有机会执行额外的动作。而 save-buffer 也有一些额外的动作。首先它会检查是否真的需要保存。它会根据我们前面介绍过的 buffer 的 modified 状态来判断当前是否需要保存。接着它会进行额外的备份,例如将文件保存为 foo~.txt 这类与源文件类似的文件。在真正执行写入操作之前,它会运行 write-contents-functions write-file-functions 这类hook函数,以便用户代码在写入前有机会介入,控制写入行为。写入完成之后,它会更新buffer的状态,将它更新为未修改状态,并更新文件的上次修改时间。从上面过程看,这两个函数纯粹就是为了让用户编辑文件所设计的,它纯粹是编辑器层面的函数。如果我们只是临时的读一些像保存上次退出保存的一些状态文件,下一次启动时用于恢复,对于这类临时文件,用户甚至都不知道该文件的存在。我们如果使用 find-file-noselect 这类的函数,就有点浪费了。这种类型它们合适的方式是读取时使用 with-temp-buffer + insert-file-contents 来进行轻量级读取。如果要写入则采用 write-region。如果你需要简单的操作buffer之后将buffer内容写入,可以使用 with-temp-buffer 。你可以将 with-temp-file 看成 with-temp-buffer + insert-file-content + write-region 的组合技。练习假设我们记录了多天的工作日志。每天的日志格式为:** <2026-09-04 Fri> content content content .....我们现在提供一个函数按日志查看某天的工作日志。(defun my/check-daily-work (date) "按输入日期 DATE (格式 YYYY-MM-DD) 查看工作日志。" (interactive (list (read-string "Input date (YYYY-MM-DD): " (format-time-string "%Y-%m-%d")))) (let ((content "") (found nil) (collecting nil)) (with-temp-buffer (when (file-exists-p my/work-log-path) (insert-file-contents my/work-log-path) (let ((lines (split-string (buffer-string) "\n" t))) (while lines (let ((line (car lines))) (cond ;; 如果是日期标题行 ((string-prefix-p "** <" line) (if collecting ;; 如果正在收集,说明遇到了下一个日期,停止收集并退出循环 (setq lines nil) ;; 否则检查是否匹配目标日期 (when (string-match (concat "\\*\\* <" date) line) (setq found t) (setq collecting t) (setq content (concat line "\n"))))) ;; 如果是非标题行 (t (when collecting (setq content (concat content line "\n"))))) (setq lines (cdr lines))))))) (if (not found) (message "未找到 %s 的日志记录" date) (let ((buffer (generate-new-buffer "WorkLog"))) (switch-to-buffer buffer) (erase-buffer) (insert content)))))上述代码,我们通过 with-temp-buffer 创建一个临时的buffer,用来读取数据。处理时将所有数据按行划分,找到以 ** 开头的日期行,如果当前日期匹配上目标日期,则将 found 设置为真,后面连着的几行是我们实际得到的当前的日志内容,如果后续又找到了 ** 开头的内容表示到下一个日期了应该退出。小结本篇我们理解了find-file-noselect 在临时读取数据的弊端,并且介绍了在这种情况下的一半处理方式。本篇的api函数在上一篇已经列举过了,这次就不在说明了。
2026年09月04日
2 阅读
0 评论
0 点赞
2026-09-03
《elisp》 每日一练——文件的读写
截至到上一篇博文,我们讨论了buffer、window和快捷键相对深入一点的问题。本来作为章节的结尾今天要进行回顾和综合性的练习。但是这个专题的有些内容在前面的练习中已经出现过,并且关于buffer、window 的综合练习我现在也想不出什么能减少对新内容依赖的东西了。所以这次就打破惯例,跳过练习,直接进入文件相关专题背景知识emacs 中文件读写最底层的函数是 insert-file-contents。它的函数签名为:(insert-file-contents FILENAME &optional VISIT BEG END REPLACE)它会将文件读取到的内容插入到当前 buffer中光标所在位置之后。我们可以通过指定参数 BEG 和 END 的值控制只读取文件的部分内容到buffer中。其中 visit 如果为非 nil,它会将buffer 设置为未修改状态,表示你正在对文件进行访问操作。这个时候你可以认为Emacs将文件和当前的buffer做了绑定,虽然它不会主动同步buffer的内容到文件,但是二者已经建立起关联了。此时我们无法做到只拷贝部分内容到buffer了。而如果为非 nil 你可以理解为当前buffer只是文件内容的一个拷贝,它们二者没有关系。根据官方文档的说明,visit这个特性应该由 find-file-noselect 函数使用,我们不应该自己使用它。与insert-file-contents 类似的函数还有一个 insert-file-contents-literally。insert-file-contents 有一些高级的副作用,例如它会根据文件类型进行解码或者进行解压缩。它会忠实的读取文件中的二进制数据,没有其他额外多余的动作。它有点像C语言中的read 函数,如果你需要处理原始的二进制数据,或需要完全控制数据解码过程,你就可以使用它来读取最原始的数据。在它们上面比较高级一点的函数就是 find-file 和 find-file-noselect了。它们都是将文件读取到buffer中并建立文件和buffer 的关联。一般来讲我们打开一个文件,然后使用Emacs操作文件对应的buffer、最后写回文件,最标准的做法就是使用这两个函数了。它们的区别在于find-file 在打开并读取文件之后会将窗口跳转到新打开文件所在buffer。将buffer 内容写入到文件中相对底层的函数是 write-region, 从名字上看出它就是将buffer中的一个区域写入到文件中。(write-region START END FILENAME &optional APPEND VISIT LOCKNAME MUSTBENEW)其中 START 和 END 表示buffer中的一个 region。FILENAME 是一个文件名。比较特殊的一种情况是 START 可以是一个字符串,此时 END 将被忽略,该函数会将字符串写入到文件中。APPEND 可以是一个 t 表示将内容追加到文件尾部,或者是一个整数,表示从文件开头定位到该字节偏移处,并从那里写入数据。VISIT 的含义与我们前面介绍的 insert-file-contents 中 VISIT 参数类似,也是一个用来建立联系的参数。mustbenew 表示写入的文件是否必须是一个不存在的文件,当我们传入非 nil 时emacs 会请求确认,而传入 excl 符号时,如果存在同名文件,Emacs会直接报错。如果我们仅仅想要追加的话,可以使用 append-to-file。另外我们使用更频繁的函数可能是 save-buffer 和 save-some-buffers。如果我们要对当前buffer执行另存为操作的话,可以使用 write-file。另外,有一个创建临时buffer并将buffer内容保存到文件中的宏 with-temp-file。它创建一个临时buffer,并设置当前buffer为新建的临时buffer,在宏结束时会将buffer切换回来并且将临时buffer的内容保存到设置的文件。练习本次的练习我们来实现一个小的灵感捕获器,与org-capture 类似,但是我们没有那么多复杂的功能。它的主要功能如下:使用 C-x c 来打开一个新的buffer在新buffer中写下你的想法在新的buffer中,如果使用 C-x s 会调用我们的保存函数如果我们定义了记录灵感的文件,会进行追加,否则会提示我们输入保存灵感文件的位置记录时会先写入当前时间戳,然后将当前buffer内容追加到文件我们实现的代码如下:(defvar my/inspiration-file nil) (defun my/inspiration-capture() (interactive) "捕获灵感" (let ((buffer (generate-new-buffer "*inspiration*"))) (switch-to-buffer buffer) (goto-char (point-min)) (keymap-local-set "C-x s" #'my/save-inspiration))) (defun my/save-inspiration() "保存灵感到指定文件" (interactive) (unless my/inspiration-file (setq my/inspiration-file (read-file-name "请输入保存灵感的文件名称:"))) (let ((content (buffer-string)) (timestamp (format-time-string "[%Y-%m-%d %H:%M:%S]"))) (write-region (concat "\n" timestamp content) nil ;;START 参数为 字符串,所以这个参数被忽略 my/inspiration-file t ;; 追加 nil nil nil) (kill-buffer (current-buffer)))) ;; 作为临时buffer,可以选择关闭 (keymap-global-set "C-x c" #'my/inspiration-capture)小结本篇我们介绍了关于emacs 中文件读写的操作,本文中介绍的函数如下:insert-file-contentsinsert-file-contents-literallyfind-file-noselectfind-filewrite-regionappend-to-filesave-buffersave-some-bufferswith-temp-file
2026年09月03日
8 阅读
0 评论
0 点赞
2026-09-02
《elisp 每日一练》——minibuffer输入读取和set-transient-map 临时热键状态机
我们在前面介绍了 completing-read 给用户提供选项,让用户输入,但是如何处理这种需要用户主动输入的情况呢?本节我们将介绍一些主动读取用户输入的接口,然后补充一个我们在前面练习中涉及到的函数 set-transient-map背景知识completing-read 主要是提供几个候选项供用户选择,有点类似一般GUI程序的下拉列表框。而我们今天介绍的 read-from-minibuffer 就像GUI程序中的文本框。它的函数签名如下:(read-from-minibuffer PROMPT &optional INITIAL-CONTENTS KEYMAP READ HIST DEFAULT-VALUE)其中常用参数的含义如下:PROMPT 提示词INITIAL-CONTENTS 预先填写的内容,后续可以修改READ 如果为 t,会自动调用 Lisp Reader,将输入的文本直接解析为 Lisp 对象DEFAULT-VALUE 当用户直接按回车时返回的字符串该接口相对来说是一个偏底层的接口,如果程序预先指定用户输入数据的类型,可以选择一些相对上层的封装函数:read-string 读取纯文本read-number 读取数字read-command 读取一个命令read-file-name 读取一个文件名read-directory-name 读取一个目录名称我们可以通过 read 作为前缀搜索类似的函数。另外一个今天要补充的函数就是 set-transient-map(set-transient-map KEYMAP &optional KEEP-PRED ON-EXIT)它会临时将对应的keymap挂起,在下一次按键之后,会调用 KEEP-PRED 参数绑定的函数返回值决定是否卸载keymap。它的函数返回值有三种情况:nil 执行一个命令后理解卸载,也就是一次性的t 保持,直到下一次调用时该函数返回nil数字 如果是数字值,保持激活状态,直到连续执行了指定次数的命令后自动卸载练习今天我们的练习,我们来实现一个快速的文本大小写与替换功能。它的功能如下:利用region 选中一段文本按 C-c t 进入该模式,此时我们利用 set-tranient-map 挂载一个快捷键的keymap按 u 将选中文本转为大写按 l 转为小写按 c 转为首字母大写(Captial)按 r 调用 read-string 读取你要替换的字符串,然后替换选中区域按 q 或任意其他键退出我们先定义这些功能的函数(defun my/upcase-region() "选中区域的文本转化为大写" (interactive) (when (use-region-p) (upcase-region (region-beginning) (region-end)))) (defun my/downcase-region() "选中区域文本转为小写" (interactive) (when (use-region-p) (downcase-region (region-beginning) (region-end)))) (defun my/capitalize-region() "选中区域首字母大写" (interactive) (when (use-region-p) (capitalize-region (region-beginning) (region-end)))) (defun my/replace-region() "替换选中区域的文本" (interactive) (when (use-region-p) (let* ((old-text (buffer-substring (region-beginning) (region-end))) (new-text (read-string (format "替换 '%s' 为:" old-text)))) (delete-region (region-beginning) (rregion-end)) (insert new-text))))然后我们可以定义一个keymap,将这些操作进行绑定(defvar my/text-operator-transient-map (let ((map (make-sparse-keymap))) (keymap-set map "u" #'my/upcase-region) (keymap-set map "l" #'my/downcase-region) (keymap-set map "c" #'my/capitalize-region) (keymap-set map "r" #'my/replace-region) (keymap-set map "q" (lambda() (interactive) (set-transient-map nil))) map))最后我们绑定一个快捷键进入该模式(defun my/start-text-operate() (interactive) (if (use-region-p) (set-transient-map my/text-operator-transient-map) (message "请先选中一段文本"))) (keymap-global-set "C-c t" #'my/start-text-operate)小结本篇我们介绍了如何读取minibuffer 中用户的输入,以及补充了一个关于快捷键绑定的额外函数。本节涉及到的函数有:read-from-minibuffer 以及基于该函数实现的read 系列函数set-transient-map
2026年09月02日
8 阅读
0 评论
0 点赞
2026-09-01
《elisp 每日一练》—— Minibuffer 与 completing-read
通过前面一段时间的学习,我们学习了如何与窗口进行交互。现在我们进入minibuffer的交互。从标题上看我们本节主要就是再介绍一遍 completing-read,这个函数我们前面已经多次用到过,读者如果不熟悉,可以再一次阅读本文,如果已经熟悉了,可以跳过。背景知识minibuffer 的相关概念就不在这里展开了。我们主要聊的是如何通过控制minibuffer相关的API来实现交互。本次我们将介绍一个我们之前遇到过相对比较熟悉的老朋友—— completing-read。我们知道,它可以通过往minibuffer中注入一些选项供用户选择。并返回用户选择的字符串。它的函数原形如下:(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)PROMPT 是给用户的提示词COLLECTION 是我们传入到minibuffer中的候选数据源, 可以是单纯的字符串、Alist、Hash Table、或者是动态生成候选词的函数PREDICATE 它是一个函数,对加入的选项进行过滤,函数返回非 nil 表示接受,否则视为放弃REQUIRE-MATCH 控制用户是否必须输入一个有效的选项DEF 默认值,当用户不选择内容时选中的默认值HIST 历史记录, 传入一个 Symbol,Emacs 会自动把用户的历史选择存入该变量,支持用 M-p / M-n 翻阅前面有好几个例子已经用到了该函数,各位读者对它的常规用法应该不陌生。我们一般通过 alist 结构,将key 设置成用户友好的提示词,而它的背后绑定了相对复杂的数据,在用户选择之后通过背后绑定的数据来实现一些功能。练习前面我们在学习其他内容时已经通过该函数实现了一些相对复杂的功能了,这次练习我们来点简单的,仅仅为了回顾一下它的用法。我们实现一个快速打开指定文件的功能。我们预先定义一些常用文件,例如emacs 的配置文件、自己的工作日志等等。然后将这些内容组织一下传入COLLECTION,最后根据用户选择打开(defvar my/favorite-file-lists '("~/.emacs.d/init.el" "d:/worklog.el")) (defun my/quick-open-favorite-file() (interactive) (let ((selected (completing-read "Input filename to open:(default ~/.emacs.d/init.el)" my/favorite-file-lists nil nil nil nil "~/.emacs.d/init.el"))) (when selected (find-file selected))))小结因为本节涉及到的函数,我们已经多次在其他练习中使用过,对于它读者应该比较熟悉了。因为本人能力不够,所以在练习上面我目前也想不出有什么深入的,又尽量少涉及其他概念内容。所以就来一个最简单的,读者如果觉得例子太简单了,可以考虑自己实现一些复杂的功能。本节涉及的函数只有一个 completing-read
2026年09月01日
6 阅读
0 评论
0 点赞
2026-08-31
《elisp 每日一练》——Window 锁定与专属窗口
我们想象这样一个场景,假设你精心布局了当前的编程窗口,左边是文件树、右边一个大窗口区域是代码编辑区域,右侧有一个小的显示当前类和函数定义的缩率图,下面有输出的日志。但是你运行某个命令弹出了 *Help* 或者 *Compile* 占用了你的某个窗口,本来对整个项目可以一览无余的,现在不得不重新关掉这个新显示的窗口。整个场景想想就糟心。我们有没有办法固定这些布局,新弹出的窗口不要破话它。Emacs一般是只有你想不到,没有它办不到的。背景知识上述场景的解决办法是通过 专属窗口(Dedicated Window) 与 窗口参数(Window Parameters),我们可以给特定 Window 加上“锁”,强制 Emacs 将新 Buffer 弹出到指定位置,保护主窗口不被侵占。各位读者可能有这样的疑问,我们前面介绍了 display-buffer 和 pop-to-buffer 已经解决了新显示的buffer乱弹窗导致布局混乱的问题,为什么又有一个 deddicated window 和 window parameters 这类解决方案呢?其实它们是不同的机制,侧重于从不同方面解决。display-buffer 和 pop-to-buffer 讲究的是这次应该将buffer放在哪。它侧重于将一个内容显示在窗口的某个位置,如果这个位置有了窗口,则重用这个窗口。Dedicated Window 和 Window Parameters 解决的是“这个窗口本身有什么规则”例如我们上面那个布局,display-buffer 和 pop-to-buffer 虽然可以靠算法精确的控制它显示的区域,但是无法保证我们主要的代码区域永远属于显示代码的buffer,设置专有窗口之后,这个窗口就专门用来显示这个buffer的内容。每个窗口都有一个 dedicated 的标志位,当设置这个标志之后,Emacs会尽量避免在本窗口中显示其他内容,例如 switch-buffer、pop-to-buffer、display-buffer 会尽量避免在对应窗口中显示新内容。我们可以通过 set-window-dedicated-p 将一个窗口设置为专属窗口,它接受两个参数:WINDOW 表示设置哪个窗口flag 为 t 时设置专属窗口,为 nil 解除该设置通过 window-dedicated-p 函数可以查看窗口的专属状态。但是仅仅设置 dedicated 标签并不意味着可以精细的管理窗口。我们可以通过设置窗口参数来更细致的控制窗口。Emacs对每个窗口都维护着一个参数的 alist 列表,可以控制窗口的一些行为。窗口参数实际是一推窗口的标签,有些标签Emacs自己就可以利用来改变窗口行为,我们也可以自定这些标签,然后在我们的程序中通过这些标签控制窗口行为和显示。也就是说窗口参数给我们扩展窗口带来了极大的便利设置窗口属性可以使用 set-window-parameter 设置属性,通过 window-parameters 获取对应的属性练习我们来实现一个小例子,来体会一下该如何使用窗口自定义参数改变窗口行为。我们实现一个通过自定义参数 this-window-height 和 this-window-width 参数来修改光标所在窗口大小的函数我们先封装一些获取和设置窗口大小的基本函数(defun my/set-window-height(height) (let ((window (selected-window))) (set-window-parameter window 'this-window-height height) (window-resize window (- height (window-total-height window))))) (defun my/set-window-width(width) (let ((window (selected-window))) (set-window-parameter window 'this-window-width width) (window-resize window (- width (window-total-width window)) t))) (defun my/get-window-height() (window-height (selected-window))) (defun my/get-window-width() (window-width (selected-window)))然后提供一个根据当前参数中存储的大小数据来更新窗口实际大小的函数(defun my/update-window-size() (let* ((params (window-parameters (selected-window))) (height (cdr (assq 'this-window-height params))) (width (cdr (assq 'this-window-width params)))) (if (not) (setq height (my/get-window-height))) (if (not width) (setq width (my/get-window-width))) (my/set-window-height height) (my/set-window-width width)))最终我们在此基础上封装一套修改大小的函数:(defun my/width_zoomin() (interactive) (let* ((params (window-parameters (selected-window))) (width (cdr (assq 'this-window-width params)))) (if (not width) (setq width (my/get-window-width))) (setq width (- 10 width)) (my/set-window-width width))) (defun my/width_zoomout() (interactive) (let* ((params (window-parameters (selected-window))) (width (cdr (assq 'this-window-width params)))) (if (not width) (setq width (my/get-window-width))) (setq width (+ 10 width)) (my/set-window-width width))) (defun my/height_zoomin() (interactive) (let* ((params (window-parameters (selected-window))) (height (cdr (assq 'this-window-height params)))) (if (not height) (setq height (my/get-window-height))) (setq height (- 5 height)) (my/set-window-height height))) (defun my/height_zoomout() (interactive) (let* ((params (window-parameters (selected-window))) (height (cdr (assq 'this-window-height params)))) (if (not height) (setq height (my/get-window-height))) (setq height (+ 5 height)) (my/set-window-height height)))小结本节我们介绍了专属窗口和窗口属性。本节有关的函数有:set-window-dedicated-pwindow-dedicated-pset-window-parameterwindow-parameter
2026年08月31日
8 阅读
0 评论
0 点赞
2026-08-30
《elisp 每日一练》——Window 树与布局状态恢复
前面的文章中,我们介绍了可以通过 split-window 来创建窗口,也介绍了通过 walk-windows 遍历窗口并且通过 overlay 机制实现了一个简单、直观的快速跳转窗口的功能。今天我们来深入了解背后的原理背景知识各位读者如果观察过Emacs执行 split-window 的效果,应该会发现它只会将当前窗口的区域进行划分,将当前窗口区域分为两个部分。这样我们很容易想到一个树的结构,光标所在的位置是树的一个叶子节点,在新建窗口时将该叶子节点作为新的根节点,在下方创建两个新的叶子节点。其实Emacs底层正是这样做的。我们可以通过 window-tree 来获取当前frame下窗口布局的树结构。我们用一个简单的例子来描述一下返回的具体结构, 假设我们有如下一个窗口布局+------------+-------------+ | | Window | | Window | B | | A +-------------+ | | Window | | | C | +------------+-------------+我们调用 window-tree 得到的结果可能是这样的( (nil (0 3 84 38) #<window 3 on *scratch*> (t (42 3 84 38) #<window 8 on *scratch*> #<window 10 on *scratch*>)) #<window 4 on *Minibuf-0*>)从整体上看它分为两个大的部分,第一个部分是一个复杂的结构,而第二个部分是一个window对象,它实际上是 mini-buffer 的窗口。我们再来看看前面一个大的结构。我们将它转换成一个树状结构├── (nil (0 3 84 38) │ ├── #<window 3 on *scratch*> │ └── (t (42 3 84 38) │ ├── #<window 8 on *scratch*> │ ├── #<window 10 on *scratch*> └── #<window 4 on *Minibuf-0*>通过这个我们发现,一个根节点实际是当前区域的分割方式和区域的大小,第一个参数为 nil 表示它是水平分割,而 t 表示垂直分割。各位读者请注意,这里的水平和数值的理解,这里并不是在窗口上按水平或者竖直方向画一条线将窗口进行划分,而是表示将新窗口按照水平或者竖直方向进行排列。所以上述结构真正含义其实是:先通过 nil 将窗口区域划分为水平方向的两个窗口,也就是左右两个其中一个为window3将右边区域一分为二,通过 t 表明它应该是竖直方向的两个窗口,也就是在中间水平位置画一条线进行区分在过去如果想要保存和恢复这个窗口布局,我们需要通过 window-tree 函数保存当前的树结构,恢复时解析每个节点并且调用 split-window 重新根据节点进行。但是从Emacs 24.1 之后提供了两个函数,可以方便的进行保存和后续的恢复: window-state-get 和 window-state-put(window-state-get &optional WINDOW WRITABLE)它可以返回指定窗口的布局状态,默认就是root 窗口,也就是当前frame 的主视图区域。我们可以在关闭时通过 window-state-get 获取root 的布局信息,保存到一个文件中,再下一次启动Emacs 时通过window-state-put 进行恢复窗口。如果我们只是临时性的保存和恢复当前窗口布局,可以使用 current-window-configuration 和 set-window-configuration 。它们的作用与前面介绍的 save-excursion、save-restriction 和 with-current-buffer 类似,如果包含临时性的窗口操作,为了让用户没有察觉或者不改变当前窗口布局,可以使用练习好了,我们来实现一个保存和恢复窗口布局的例子,因为我们还没有接触过Emacs文件的操作,所以这里简单的实现一个在Emacs启动期间执行保存和恢复的功能,后续学习了如何读取和保存文件之后我们可以继续完善因为不涉及到持久化的保存,所以我们可以简单的使用 current-window-configuration 和 set-window-configuration 函数。(defvar current-window-state nil) (defun my/save-current-window-state() (interactive) (setq current-window-state (current-window-configuration))) (defun my/restore-current-window-state() (interactive) (set-window-configuration current-window-state))我们的练习比较简单,如果读者觉得难度不过瘾,可以考虑通过保存和遍历 window-tree 的方式实现这个功能小结本篇我们介绍的函数有:window-treecurrent-window-configuration 和 set-window-configurationget-window-state 和 put-window-state
2026年08月30日
9 阅读
0 评论
0 点赞
2026-08-29
《elisp 每日一练》—— 变量定义与赋值
平时我们在看网上的一些配置Emacs的代码时应该注意到了,有多种多样的elisp 变量声明方式,defvar、setq、setq-default、setq-local、let 等等。今天我们深入buffer和window 机制的第二天我们打算来引入一种新的变量类型:buffer local变量,并以此来区分这几种声明变量的方式。背景知识全局变量如果各位读者有其他编程语言的经验,应该对这个概念不会陌生,全局变量是随着程序加载被初始化,在程序的任意位置都可以访问,修改。在程序结束之后它才会消亡。Emacs 中也有这类变量,在Emacs中声明这类变量可以使用 defvar 和 defcustom。虽然它们二者都是定义全局变量,但是二者设计的初衷完全不同,defvar 是为了定义普通的全局变量,而 defcustom 是为了定义用户可定制选项。我们先来聊聊 defvar 的一些行为:defvar 用来定义全局变量,不管它是在哪执行的,它所定义的变量都是全局可以访问的。它可以在定义变量的时候给一个初始值, 并且可以为变量增加一个字符串的说明文档。如果在定义时发现该变量已经存在,那么它将什么也不做对于第三条特性,我们可以使用下面的代码来测试(progn (defvar custom "Jack") (message "Hi, %s" custom) (defvar custom "Tom") (message "Hi, %s" custom))我们可以看到它打印了两次 "Hi,Jack"。我们按照上述 defvar 的特征,对 defcustom 逐条审视,发现它在此基础上有一些增强的特性:对于第一条,它完全继承并且行为也一样对于第二条,它的行为有增强,对于描述性的内容,不光可以使用字符串,还可以使用一些关键字进行详细的描述对于第三条,同样的它后续可以通过 setq 来设置新值,但是它支持用户通过customize 设置变量的新值,用户后续Customize界面设置的值可以覆盖原始值,并且这个值会被写入 ~/.emacs.d/custom.el 获取通过 custom-file 变量指定的文件内进行持久化保存我们可以尝试使用使用一下(defgroup my-system-config nil "Configuration group for my personal system settings" :group 'emacs) ;;挂载到emacs 基础分组下,方便查找 (defcustom my-system-type 0 "Target operating system type for my custom scripts 0 represents Windows, 1 represents Linux, 2 represents MacOS" :type '(choice (const :tag "Windows(0) 0") (const :tag "Linux (1) 1") (const :tag "MacOS (2) 2")) :group 'my-system-config)我们定义了一个操作系统的设置变量,其中 0代表Windows、1代表 Linux、2 代表MacOS。而变量的默认值是0,后续读者可以尝试执行 M-x customise 来尝试通过界面设置这个变量定义全局变量实际上还有一个 defconst,它一般用于定义常量,但是在执行时它并不阻止我们后续为变量赋新值。更像是开发者的一个约定,使用它定义了之后,就不要修改它了变量的修改变量修改我们可以使用 setq 和 setq-default。 setq 负责给变量赋值,但是如果赋值时变量未定义,它可以定义一个变量,然后再赋值。所以在某种程度上,它也兼职一些 defvar 的工作。但是它定义的变量的声明周期与执行该代码的位置有关。如果是全局位置的,那么它就可以定义全局变量,在函数中那么定义的就是局部变量。setq-default 同样可以设置变量的值。它与 setq 的核心区别是当一个变量是“缓冲区局部变量”时,它们修改的是不同层级的值。setq 修改的是当前缓冲区的局部值,而 setq-default 修改的是变量的全局默认值缓冲区局部变量缓冲区局部变量,英文是 buffer local variable 这是一类特殊的全局变量。我想有一个段子可以更好的描述它。话说当年军阀混战时,有一个没有文化的军阀看到一群人在打篮球。军阀说“这群人真可怜,一群人只有一个篮球,难怪他们都要追着一个球跑”,然后吩咐手下,给每个人分配一个球。buffer local 变量与上述段子中军阀给每个人都分配一个篮球类似,Emacs给每个buffer都分配了一个局部的变量。虽然变量名称没变,但是变量在buffer中都是相互独立的。例如我们要设置行号,可以使用 (setq display-line-numbers t) 来显示行号。但是我们切换到其他buffer 中发现其他的buffer无法显示行号。在当前显示行号的buffer中,查看该变量的值发现它是 t,而其他buffer中的值是 nil。就像我们前面说到的,setq 只改变当前buffer中的变量,如果要修改变量的全局默认值,可以使用 setq-default。定义 buffer local 变量,可以使用函数 defvar-local。defvar-local 与 defvar 类似,也是一处定义,后续再次使用 defvar-local 定义式,新给出的值会被忽略。(progn (defvar-local test 0) (with-current-buffer (get-buffer "*Messages*") (setq test 1) (message "test in message buffer is: %d" test)) (message "test in current buffer is: %d" test))设置 buffer local 变量除了使用 setq 和 setq-default 外还可以使用 setq-local。 这三个我暂时称他们为 赋值三兄弟。他们的主要区别在于:场景setqsetq-defaultsetq-local变量未定义自动创建一个全局动态变量强行在当前 buffer 中创建该变量的 buffer-local 绑定,并赋值,此时全局变量依然未定义自动创建一个全局变量 效果等同于 setq变量不是buffer local 类型直接修改该变量的全局默认值强制将其变为当前 buffer 的局部变量然后赋值,全局变量保持不变直接修改该变量的全局默认值,如果已经是buffer local变量,它会修改全局默认值我们将一个变量设置为buffer local 变量可以使用 make-local-variable。它要求这个变量必须是存在的,实际上,我们查看 defvar-local 的源码发现它内部实际调用了 make-variable-local 这个函数。如果我们将上述段子中的篮球比作一个buffer local 变量而运动员比作一个buffer,make-variable-local 要求篮球必须先有一个,然后照着"复制"一个,但是它不负责将篮球交到某个运动员手中。而 defvar-local 则没有这些限制,即使篮球不存在,它也会复制一堆篮球,然后一一交给各个运动员。练习我想今天我们主要介绍的是一些基础的概念,所以今天就不像之前那样写一些实用性的工具,读者可以针对这些内容自行修改并创建测试用例来深入理解它们的概念。defvar 的“已定义则不重新赋值”特性;; 第一次定义 (defvar my-test-var "initial") (message "1: %s" my-test-var) ; => "initial" ;; 第二次定义(新值被忽略) (defvar my-test-var "new") (message "2: %s" my-test-var) ; => 仍然 "initial" ;; 但可以用 setq 强制修改 (setq my-test-var "changed") (message "3: %s" my-test-var) ; => "changed"setq 在函数内创建局部变量(defun test-setq-local () (setq my-func-var "inside") (message "Inside function: %s" my-func-var)) (test-setq-local) ; => "inside" (message "Outside: %s" my-func-var) ;; => Outside: insidesetq-default 与 setq 在 buffer-local 变量上的区别;; 先定义一个全局变量 (defvar my-global-var "global-default") ;; 创建一个新的 buffer 并在其中设置局部值 (with-current-buffer (get-buffer-create "test-buffer") (make-local-variable 'my-global-var) ; 显式设为 buffer-local (setq my-global-var "buffer-local-value") (message "In test-buffer, setq: %s" my-global-var)) ;; 切换回原 buffer,查看全局值 (message "In original buffer: %s" my-global-var) ; => "global-default" ;; 用 setq-default 修改全局默认值 (setq-default my-global-var "new-default") ;; 再次查看 test-buffer 中的值(不受影响) (with-current-buffer "test-buffer" (message "In test-buffer after setq-default: %s" my-global-var)) ; => 仍为 "buffer-local-value"defvar-local 的自动 buffer local 特性;; 定义 buffer-local 变量 (defvar-local my-buffer-var "default") ;; 在另一个 buffer 中修改局部值 (with-current-buffer (get-buffer-create "buffer-A") (setq my-buffer-var "value-in-A") (message "A: %s" my-buffer-var)) ;; 当前 buffer 中的值不变 (message "Current: %s" my-buffer-var) ; => "default" ;; 再次用 defvar-local 定义,新值被忽略(同 defvar) (defvar-local my-buffer-var "ignored") (message "After redef: %s" my-buffer-var) ; => 仍为 "default"setq-local 与 make-local-variable 的行为对比;; 先定义全局变量 (defvar my-var "global") ;; 情况1:用 make-local-variable 显式创建绑定,但尚未赋值 (make-local-variable 'my-var) ; 此时全局值仍为 "global",但当前 buffer 有了自己的局部槽 (message "After make-local: %s" my-var) ; => "global"(因为还未赋值,显示全局值) ;; 此时调用setq 实际在给buffer local 变量赋值 (setq my-var "local") (message "After setq: %s" my-var) ; => "local" ;; 其他buffer中仍然输出 "global" (with-current-buffer (get-buffer "*Messages*") (message "my-var in messages buffer is: %s" my-var)) ;; 情况2:直接使用 setq-local(它会自动创建全局变量并设为 buffer-local) (setq-local my-var2 "new-local") (message "setq-local created: %s" my-var2) ; => "new-local" ;; 切换到其他 buffer,查看 my-var2 是否被定义(全局变量存在,但不是 buffer-local) (with-current-buffer (get-buffer-create "buffer-B") (message "In B, my-var2: %s" my-var2)) ; => 如果全局存在且不是局部,则显示全局值(未定义则为 nil)setq-default 对未定义变量的行为;; 变量未定义, 此时它会定义新变量,行为类似 setq (setq-default my-new-var "default") ;; 此时 my-new-var 是一个全局变量 (message "Global: %s" my-new-var) ; => "default" ;; 判断变量是否为 buffer local 变量 (local-variable-p 'my-new-var) ; => nil 它实际是一个全局变量 ;; 切换到另一个 buffer (with-current-buffer (get-buffer-create "buffer-C") (message "In C: %s" my-new-var)) ; => 实际创建的是全局变量,所以这里也输出 `In C default`小结本次我们介绍了Emacs lisp 中的各种变量类型,以及各种变量的定义赋值方式和它们的区别,我们将他们区分为变量定义的三兄弟和变量赋值的三兄弟:defvardefcustomdefvar-localsetqsetq-defaultsetq-local以及相对底层的函数 make-variable-local
2026年08月29日
12 阅读
0 评论
0 点赞
2026-08-28
《elisp 每日一练》——Indirect Buffer
前面我们了解了buffer、window相关的机制,并且学习了操作它们的一些api函数。现在我们继续更深入的探索有关buffer和window的内容。背景知识Indirect Buffer 就像它的名字一样是一个间接的东西,它的本源是原始的buffer,但是它却拥有独立的buffer状态,像point、marker、narrowing、major mode等等。它与真实buffer的关系让我想起七龙珠中的天神和比克大魔王,二者本是是同源,因为天神剥离邪恶人格而诞生比克大魔王,二者虽然同源但是有不同的行为、不同的性格。二者一个的死亡也会牵扯到另一个。Indirect Buffer 就是从源buffer中诞生出来的,并且Indirect Buffer 内容的变化会影响到源buffer。在Emacs 中创建 Indirect Buffer 的方法是 make-indirect-buffer(make-indirect-buffer BUFFER NAME &optional CLONE)参数中的 BUFFER 表示源buffer对象,而 NAME 是 Indirect Buffer 的名称,CLONE 表示是否需要克隆当前buffer的状态。如果你想克隆这些状态,我们可以 clone-indirect-buffer 它会将当前buffer作为一个源buffer,创建一个新的 indirect buffer,并且将源buffer的一些状态信息拷贝到新的buffer中。另外,在引入indirect buffer 之后,我们获取到的buffer可能是 indirect buffer,如果我们想要获取源buffer可以使用 buffer-base-buffer。练习我们来实现一个小型的代码查看器,它会创建一个 indirect buffer 只显示用户选中区域,这样用户在新的buffer中修改也会直接同步到源buffer中(defun my/region-view-scratch() (interactive) (if (use-region-p) (let* ((begin (region-beginning)) (end (region-end)) (base-buffer (current-buffer)) (view-buffer-name (format "Indirect: %s" (buffer-name base-buffer))) (view-buffer (make-indirect-buffer base-buffer view-buffer-name t))) (pop-to-buffer view-buffer) (narrow-to-region begin end) (goto-char (point-min)))))小结本篇的内容到此就结束了,本篇我们介绍了 indirect buffer 的机制,本篇介绍的函数如下:make-indirect-bufferbuffer-base-bufferclone-indirect-buffer
2026年08月28日
9 阅读
0 评论
0 点赞
2026-08-27
《elisp 每日一练》——命令与快捷键的综合练习
到目前我们已经全面了解如何将一个函数定义为交互式命令并且将它绑定到一个快捷键上。现在我们来针对之前的内容做一个综合性的小练习前期内容回顾按照前面的规矩,我们先回顾一下之前涉及到的函数首先可以通过在函数开始位置加入 interactive 来使一个函数变为一个可以由用户主动触发的交互式函数。我们将交互式函数绑定到一个快捷键上,可以使用如下函数:define-keyglobal-set-keylocal-set-key以及Emacs 29以后提供了它们的替代版本:keymap-setkeymap-global-setkeymap-local-setkeymap-unset之后我们又介绍了如何将一个keymap 绑定到快捷键上,使快捷键成为一个前缀make-sparse-keymapdefine-prefix-command练习本次练习也比较简单,我们也是围绕着快捷键的定义和绑定来。我们本次实现一些简单的vim方式的操作,我们实现一个名为 my-simple-vim-normal-mode 它对应着 vim 中的普通模式,我们可以通过 Esc 键来进入该mode,并且按i可以关闭该模式,O、o 可以分别在当前行的上一行和下一行插入新行并退出当前模式基于这个目标我们可以先定义一些基本的快捷键和函数(defvar my-simple-vim-normal-mode-map (let ((map (make-sparse-keymap))) (keymap-set map "h" #'backward-char) (keymap-set map "l" #'forward-char) (keymap-set map "j" #'next-line) (keymap-set map "k" #'previous-line) (keymap-set map "u" #'undo) map))接着我们来实现快捷键的加载与卸载,这里我们使用 set-transient-map 来实现。它可以临时的启用一个全局最高等级的keymap。它可以接受一个keymap和一个函数。每一次用户按键并执行完对应的命令之后会执行这个函数,根据函数返回值来判断是否需要继续维持这个keymap。所以在进行切换的时候可以这么做:(defvar-local my-simple-vim-normal-mode-active-p nil "判断当前mode 是否处于激活状态") (defun my/simple-vim-normal-mode-exit() "退出当前mode" (interactive) (setq my-simple-vim-normal-mode-active-p nil)) (defun my/simple-vim-normal-mode-active() "激活当前mode" (interactive) (setq my-simple-vim-normal-mode-active-p t) (set-transient-map my-simple-vim-normal-mode-map (lambda() my-simple-vim-normal-mode-active-p)))最后我们绑定一个从当前模式进入vim 普通模式的快捷键,并且给出一些从vim普通模式退出的函数(defun my/insert-new-line-next() "当前行下方插入新空白行" (interactive) (goto-char (line-end-position)) (newline)) (defun my/insert-new-line-previous() (interactive) (forward-line -1) (goto-char (line-end-position)) (newline)) ;; 绑定使用 esc 激活该mode ;; 这里为了延续习惯绑定了 esc 键,但是在Emacs 中,捕获了esc键会导致meta键失效,读者如果可以考虑换一个键进行绑定 (global-set-key [escape] (lambda() (interactive) (if (not my-simple-vim-normal-mode-active-p) (my/simple-vim-normal-mode-active)))) (keymap-set my-simple-vim-normal-mode-map "i" #'my/simple-vim-normal-mode-exit) (keymap-set my-simple-vim-normal-mode-map "o" (lambda() (interactive) (my/insert-new-line-next) (my/simple-vim-normal-mode-exit))) (keymap-set my-simple-vim-normal-mode-map "O" (lambda() (interactive) (my/insert-new-line-previous) (my/simple-vim-normal-mode-exit)))到此本篇的练习差不多结束了,读者如果有兴趣可以考虑再实现一些别的功能,进行填充,例如yy、p、v、A、I的绑定,使编辑器更趁手
2026年08月27日
5 阅读
0 评论
0 点赞
1
...
4
5
6
...
11