首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
287 阅读
2
从零开始配置 vim(15)——状态栏配置
169 阅读
3
nvim番外之将配置的插件管理器更新为lazy
159 阅读
4
2018总结与2019规划
145 阅读
5
PDF标准详解(五)——图形状态
121 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
emacs
windows
elisp
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
442
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
76
篇与
的结果
2026-09-11
《elisp 每日一练》——minor mode 开关变量的管理
上一节我们将定义 minor mode 的宏 define-minor-mode 展开并分析了它的作用。它主要是定义用于切换mode启用开关的变量和函数,以及在切换时进行模式的管理和hook的切换。默认ninor mode 创建时处于管理状态,而且minor mode启动时只对当前buffer生效。本节我们来讨论如何通过控制开关变量来改变这两点。背景知识:init-value在介绍之前,我们补充一个知识点。那就是 define-minor-mode 的 :init-value 关键字。我们还是将下面的定义展开,看看与默认的相比多了什么部分的代码(define-minor-mode my/focus-mode "This is a focus mode" :init-value t)展开之后得到的代码实在是太多了,我们就不一一介绍了。我们只看关键的部分(defvar-local my/focus-mode t)默认定义时,切换开关的变量被定义成了 nil 也就是关闭状态。现在被设置成了打开。其他的部分没有什么大的变化。所以据此我们可以得出结论 :init-value 用来控制定义时是否打开对应的mode。customize 机制现在我们正式开始讨论 Customize 机制。Customize 机制是Emacs提供的一套配置UI框架,任何人都可以通过对应的函数将对应的变量作为配置通过UI界面显示内容并进行设置。我们可以通过 customize 函数打开该界面。我们通过界面设置的值被保存到一个文件中,该文件由Emacs自己维护,默认放在我们的主配置文件中。我们可以通过 custom-file 设置这些值放置的位置。或者通过 custom-file 函数获取custom file配置文件的位置。我们通过 customize 设置的值被放在 custom file 包裹在 custom-set-variables 函数中。我们可以通过 defcustom 宏定义一个能被customize 机制显示和管理的宏。(defcustom SYMBOL STANDARD DOC &rest ARGS)其中 symbol 是一个符号,表示一个 customize 类型的变量。STANDARD 表示默认值。DOC 表示变量得说明文档。ARGS 是一组由关键字组成得属性定义。常见得属性有::type 变量得数据类型,有 'string、'integer、'boolean '(choice ...):set 是一个函数,当变量被赋值时会调用这个函数,通过函数来给变量赋值,而不是直接赋值。在函数中需要使用 setq-default 来为变量重新设置值,setq 也会触发该函数,容易引起递归调用:version 该标记用来提醒用户,这个变量得使用适用于哪个版本,可以显示Emacs版本或者对应 package 版本:tag 覆盖显示界面中得变量名,用于给用户一个更好的提示:link 在 Customize 缓冲区中显示一个可点击的链接,通常指向相关文档或 info 节点:group 指定变量所属的自定义组。这将决定它在customize 树中显示的位置,一个变量可以属于多个组Emacs 中customize group 作用是像文件目录一样,把成千上万的用户选项按类收集整理方便用户查看。Emacs 中有一些常用的默认组可以使用:Editing:基础文本编辑功能相关的选项Convenience: 用于加快编辑功能的快捷选项External: 与外部工具交互的组我们也可以自定义自己的组,自定义组使用 defgroup ,它的签名如下:(defgroup SYMBOL MEMBERS DOC &rest ARGS)SYMBOL 表示自定义组的名称、MEMBERS 表示组中的成员、ARGS 也是一个由关键字组成的属性定义。常用的属性有::group 父组:prefix 变量名前缀建议其他的属性含义则与 defcustom 的相同。现在我们来看看 define-minor-mode 是如何管理我们的customize 变量的。(defgroup my-focus-group nil "My focus group" :group 'tools :prefix "my-focus-") (define-minor-mode my/focus-mode "This is my focus mode" :init-value nil :type 'boolean :global t)我们展开这个宏,发现它将我们的切换开关变量改为了 (defcustom my/focus-mode nil),也就是它将变量设置成了一个customize 变量。如果我们在 gloabl 为 t 的情况下指定了 group 那么,它此时就是该组中的一个变量。为什么只有gloabl 为 t 时才会设置为customize 变量呢,因为 minor mode 是挂载到单个buffer上的,它依附与buffer,所以默认情况下minor mode 的切换开关也是一个 buffer local 变量,如果我们希望切换对全局都有效。为了配置这种一处修改所有buffer都启用,为了管理这种情况,宏也在其他地方进行了适配(when (boundp 'global-minor-modes) (setq global-minor-modes (delq 'my/focus-mode global-minor-modes)) (when (default-value 'my/focus-mode) (push 'my/focus-mode global-minor-modes)))我们发现,它现在管理的不再是 local-minor-modes 而是全局的 global-minor-modes。另外需要注意的是,既然我们的 minor mode 打开了 global 开关变成了全局可用的,那么为了一致性,mode 中使用的相关变量也尽量使用全局变量,如果使用 buffer local 变量会导致有的buffer有这个变量,而有的没有,或者不同buffer中同名的变量值不同,导致行为不一致。如果我们希望定义customize 变量来控制minor mode 的一些行为,那么还是要回归到自己利用 defcustom defgroup 定义组。练习我们来定义一个提醒休息的小功能,它的作用是每隔一段时间就提醒我们休息一下,我们没有学定时器相关的内容,所以只能做一个手动触发的功能。无法(defgroup my-timer-group nil "自定义定时提醒模式的配置组" :group 'convenience :prefix "my-timer-") (defcustom my-timer-message "时间到!休息一下吧" "定时提醒时弹出的文本" :type 'string :group 'my-timer-group) (defcustom my-timer-interval-seconds 30 "提醒时间间隔 (单位:秒)" :type 'integer :group 'my-timer-group) (defcustom my-timer-enable-sound nil "是否播放提示音" :type 'boolean :group 'my-timer-group) (define-minor-mode my-timer-mode "定时提醒" :init-value nil :group 'my-timer-group) (defun my-timer-show-message() (interactive) "显示提示信息" (let* ((buffer (get-buffer-create "MyTimer")) (show-message (message "你已经坚持了%d, %s" my-timer-interval-seconds my-timer-message))) (with-current-buffer buffer (erase-buffer) (goto-char (point)) (insert show-message)) (display-buffer buffer)))小结本节我们讨论了minor mode 中是如何通过 :init-value 、 :global 和 :group 来管理开关变量。简单来说就是默认创建的 Minor Mode 都是关闭状态,想要创建时启动可以通过 :init-value 将它设置为 t,如果希望启用之后全局生效可以 设置 :global 为 t,它会将变量定义为 customize 中的变量,在这种情况下我们可以通过 :group 为它设置一个组。
2026年09月11日
4 阅读
0 评论
0 点赞
2026-09-10
《elisp 每日一练》—— Minor Mode 变量生成机制与状态控制
前面我们以一个简单的 dired-mode 实战案例结束了关于文件和目录专题的学习。现在开始进入Emacs中的Minor Mode 和 Major Mode的学习。define-minor-mode在前面我们已经使用了 define-minor-mode 定义了一个 windows-notepad 的mode,它主要绑定Windows常用的快捷键,例如:C-c 复制、C-v 粘贴。我们定义一个最简单的minor mode(define-minor-mode my/focus-mode "这是一个专注模式。")对于这种使用宏的代码,我们可以使用将光标放在代码块的最后使用 pp-macroexpand-last-sexp 来得到展开之后的内容。上述代码展开之后得到如下内容(progn (progn :autoload-end (defvar-local my/focus-mode nil "Non-nil if My/Focus mode is enabled.\nUse the command `my/focus-mode' to change this variable.")) (defun my/focus-mode (&optional arg) "这是一个专注模式。\n\nThis is a minor mode. If called interactively, toggle the `My/Focus\nmode' mode. If the prefix argument is positive, enable the mode, and if\nit is zero or negative, disable the mode.\n\nIf called from Lisp, toggle the mode if ARG is `toggle'. Enable the\nmode if ARG is nil, omitted, or is a positive number. Disable the mode\nif ARG is a negative number.\n\nTo check whether the minor mode is enabled in the current buffer,\nevaluate the variable `my/focus-mode'.\n\nThe mode's hook is called both when the mode is enabled and when it is\ndisabled." (interactive (list (if current-prefix-arg (prefix-numeric-value current-prefix-arg) 'toggle))) (let ((last-message (current-message))) (setq my/focus-mode (cond ((eq arg 'toggle) (not my/focus-mode)) ((and (numberp arg) (< arg 1)) nil) (t t))) (when (boundp 'local-minor-modes) (setq local-minor-modes (delq 'my/focus-mode local-minor-modes)) (when my/focus-mode (push 'my/focus-mode local-minor-modes))) (run-hooks 'my/focus-mode-hook (if my/focus-mode 'my/focus-mode-on-hook 'my/focus-mode-off-hook)) (if (called-interactively-p 'any) (progn nil (unless (and (current-message) (not (equal last-message (current-message)))) (let ((local " in current buffer")) (message "%s %sabled%s" "My/Focus mode" (if my/focus-mode "en" "dis") local)))))) (force-mode-line-update) my/focus-mode) :autoload-end (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)) nil (with-no-warnings (add-minor-mode 'my/focus-mode 'nil (if (boundp 'my/focus-mode-map) my/focus-mode-map) nil nil)))从这段代码的注释上看,它它主要做了以下事情:定义一个名为 my/focus-mode 也就是与mode同名的 buffer-local 的状态变量;定义了一个和 mode 同名为 my/focus-mode 交互式切换函数;定义并配置 hook 变量;把这个 minor mode 注册到 Emacs 的 minor mode 系统里。我们依次来阅读一下这段代码,看看各个部分是如何工作的。首先我们看到 my/focus-mode 函数中的注释,这段注释详细的介绍了这段代码的作用,我们翻译一下得到如下内容这是一个次要模式。如果以交互方式调用该模式,则会切换“My/Focus模式”。如果前缀参数为正数,则启用该模式;如果为负数或零,则禁用该模式。 如果通过Lisp调用该模式,并且ARG的值为“toggle”,则切换该模式。如果ARG为nil、被省略,或者是一个正数,则启用该模式。如果ARG为负数,则禁用该模式。 要检查当前缓冲区中是否启用了该次要模式,可以评估变量`my/focus-mode`的值。 在该模式被启用和禁用时,都会调用相应的钩子函数。从这里可以看出,实际上切换该mode的状态实际上是在修改 my/focus-mode 变量的值。my/foucs-mode 函数下面我们来看一下该函数中的重点操作1. 设置变量值切换mode 状态我们先来看一下最前面的 interactive 它的内容如下:(interactive (list (if current-prefix-arg (prefix-numeric-value current-prefix-arg) 'toggle)))这段代码的意思是如何用户调用时没有参数,那么设置它的值为 'toggle 表示切换。如果有使用 C-u 输入前置参数,那么它会尝试将输入的参数转为数字。后面紧跟着的就是对 my/focus-mode 的赋值,它的代码如下:(setq my/focus-mode (cond ((eq arg 'toggle) (not my/focus-mode)) ((and (numberp arg) (< arg 1)) nil) (t t)))它的赋值规则很简单,如果是 'toggle ,那么对原值取反以便进行切换,如果是数字,负数和0的时候关闭mode,也就是将 my/focus-mode 设置为ni,正数则设置为 t。2. 维护 local-minor-modes接着我们往后看(when (boundp 'local-minor-modes) (setq local-minor-modes (delq 'my/focus-mode local-minor-modes)) (when my/focus-mode (push 'my/focus-mode local-minor-modes)))local-minor-modes 是一个变量,它记录当前buffer中启用了哪些 minor mode。上面的代码先判断该变量是否绑定了值,如果绑定了,则先删除当前mode,后面根据 my/focus-mode 的真假来决定是否将当前mode添加到变量中3. 运行hookhook 的概念我们还没有开始接触,但是如果读者对其他编程语言熟悉,相比对hook应该也不陌生。这里你可以简单的类比事件的回调。它提供了一个外部干预事件的契机。(run-hooks 'my/focus-mode-hook (if my/focus-mode 'my/focus-mode-on-hook 'my/focus-mode-off-hook))从这里我们能看出来,不管启用还是关闭,都会调用 my/foucus-mode-hook,后续如果启用则会调用 my/focus-mode-on-hook。关闭会调用 my/focus-mode-off-hook。4. 刷新mode line 并返回状态函数最后一句代码如下: (force-mode-line-update) my/focus-mode)force-mode-line-update 用来刷新 modeline 的状态,最后对 my/focus-mode 求值并返回它的值。定义并配置 hook 变量函数结束之后,它又定义了一个 my/focus-mode-hook 这个变量我们刚刚看到,它在函数中被使用。(defvar my/focus-mode-hook nil)我们看到该变量没有采用 defvar-local 定义成 buffer-local 类型的变量,而是使用 defvar 定义成了全局变量。Emacs 中的hook变量通常都是全局变量,这个我们在讨论hook的时候再来解释(unless (get 'my/focus-mode-hook 'variable-documentation) (put 'my/focus-mode-hook 'variable-documentation "Hook run after entering or leaving `my/focus-mode'..."))这段代码虽然我们不知道 variable-documentation 是个什么东西,但是从名字上应该能猜出来,它是变量文档。我们通过 C-h v 查看hook变量时显示的内容。它的作用是如果没有指定 hook变量的文档,它帮我们指定一个默认的变量文档。接着我们看到两个 put(put 'my/focus-mode-hook 'custom-type 'hook) (put 'my/focus-mode-hook 'standard-value (list nil))它向 custom 系统注册了 my/focus-mode-hook 的信息,我们可以使用 customize 命令打开 custom 的界面,查到该变量的信息。它们设置变量类型为 hook、标准值为空列表。最后以下面的代码作为结尾:(with-no-warnings (add-minor-mode 'my/focus-mode 'nil (if (boundp 'my/focus-mode-map) my/focus-mode-map) nil nil))我们重点关注 add-minor-mode 部分的代码,它是一个底层C写的函数,负责将该mode注册到Emacs 管理的 minor mode 系统中。它的函数签名为:(add-minor-mode TOGGLE NAME &optional KEYMAP AFTER TOGGLE-FUN)根据函数签名,我们可以看到,注册时填写了如下信息:切换状态的变量为 my/focus-modeNAME 为 nil,也就是不在mode line 上显示mode 的名称KEYMAP 部分的参数表示该mode下的 keymap 。叠加一个if语句,表明如果有名为 my/foucus-mode-map 的keymap则采用它作为keymap,否则keymap部分为 nilAFTER 和 TOGGLE-FUN 两个参数为 nil。总结到此我们描述完了 define-minor-mode 宏在底层所干的事情:定义切换变量,与mode名称相同定义切换函数,与mode名称相同切换时根据传入参数来切换mode状态:为负数或者0表示关闭,1表示开启,'toggle 表示根据当前状态切换切换状态时会无条件执行 my/foucs-mode-hook ,在开启和关闭时则会再额外单独执行 my/foucs-mode-on-hook 或者 my/foucs-mode-off-hook中间它还会根据mode状态维护 local-minor-mode 变量、注册minor mode 到Emacs,更新显示一些内容本节到此就结束了,本节因为主要是理论性的内容,不涉及实操,所以就不进行练习了,如果说非要按照惯例进行练习的话,读者可以自行查看源代码并跟着本文的内容梳理一下思路。或者也可以尝试根据文档使用诸如 :keymap、:init-value 之类的关键字看看它们在背后都做了什么
2026年09月10日
5 阅读
0 评论
0 点赞
2026-09-09
《elisp 每日一练》——文件与目录的回顾与综合练习
到此为止,我们关于文件和目录的内容就介绍完了,Emacs中的文件与目录操作和其他编程语言相比唯一可能需要注意的是emacs 中的普通的文件读写操作与buffer和一些hook相关,读写也是基于buffer在进行的。主要知识点回顾首先我们介绍了一些可能最常用的读写方式find-filefind-file-noselectsave-buffersave-some-buffers因为这些函数涉及到一些额外的操作,例如绑定到buffer并设置buffer的信息,加上一些hook,在用户不关注它的内容时显得比较笨重,所以又介绍了轻量级的读写文件的方式:insert-file-contentsinsert-file-contents-literallywrite-regionappend-file这些函数配合 with-temp-buffer 可以创建临时buffer作为读取文件的缓冲区。或者可以通过 with-temp-file 来做到将创建的临时buffer中的内容写入文件。后面我们介绍了一些通过路径获取属性的函数:expand-file-namefile-name-directoryfile-name-nondirectoryfile-name-extensionfile-name-concatfile-exists-pfile-regular-pfile-symlink-pfile-readable-p / file-writeable-pfile-attributes关于目录部分的内容,我们介绍了下面几个函数:file-directory-pfile-name-as-directorydirectory-filesdirectory-files-and-attributes在目录下,有这么几个操作文件或者目录的函数:make-directoryrename-filecopy-filecopy-directorydelete-filedelete-directory练习有什么练习能做到既实用又能使用上面大部分的函数呢?当然是通过快捷键快速操作文件和目录了。将这个事情做到极致的应该是Emacs的 dired-mode。我使用Emacs有一段时间了,目前我在Windows平台下使用,对于Windows平台来说,最让我离不开的功能主要是 dired-mode 和 org mode。所以今天我们的练习就是实现一个简易版的 dired-mode。它的主要功能如下:由用户输入一个目录,通过打开新的buffer显示目录中文件的属性用户选择某个目录可以进入该目录用户选择某个文件可以打开文件绑定一些快捷键来快速执行操作我们绑定如下快捷键:a/A (append),添加文件或者目录c/C (copy) 拷贝文件或者目录d/D (delete) 删除文件或者目录r/R (rename) 重命名或者文件我们首先在上一节的基础上做一些修改,后续多次切换目录,buffer要显示不同目录的内容,所以我们先提供一个刷新buffer的函数(defun my/refresh-dired-buffer() (let ((buffer (current-buffer)) (dir buffer-dir-path)) (when (and dir (file-directory-p dir)) (erase-buffer) (dolist (file-and-attr (directory-files-and-attributes dir nil nil t 'string)) (let* ((filename (car file-and-attr)) (attr (cdr file-and-attr)) (modes (file-attribute-modes attr)) (owner (file-attribute-user-id attr)) (group (file-attribute-group-id attr)) (size (file-attribute-size attr)) (modified-time (format-time-string "%Y-%m-%d %H:%M:%S" (file-attribute-modification-time attr))) (file-information (format "%-11s %-8s %-8s %8d Bytes %s %s" modes owner group size modified-time filename))) (insert file-information) (add-text-properties (line-beginning-position) (line-end-position) `(file-name ,filename)) (newline)))))) (defun my/active-dired-mode(dir-path) (interactive "fdir path: ") (let ((abs-dir-path (expand-file-name dir-path))) (if (and (file-exists-p abs-dir-path) (file-directory-p abs-dir-path)) (let ((buffer (get-buffer-create "*MyDiredBuffer*"))) (if buffer (progn (switch-to-buffer buffer) (setq-local buffer-dir-path abs-dir-path) ;;设置并绑定快捷键 (keymap-local-set "a" #'my/dired-create-new-file) (keymap-local-set "A" #'my/dired-create-new-directory) (keymap-local-set "c" #'my/dired-copy-file) (keymap-local-set "C" #'my/dired-copy-directory) (keymap-local-set "d" #'my/dired-delete-file) (keymap-local-set "D" #'my/dired-delete-directory) (keymap-local-set "r" #'my/dired-rename-file) (keymap-local-set "R" #'my/dired-rename-directory) (my/refresh-dired-buffer)))))))接着我们再来编写这些关联的函数(defun my/dired-create-new-file(filename) (interactive (list (read-file-name "New File: " buffer-dir-path))) (let* ((abs-file-name (expand-file-name filename)) (abs-file-directory (file-name-as-directory (file-name-directory abs-file-name)))) (if (and (file-directory-p abs-file-directory) (file-exists-p abs-file-directory) (not (file-exists-p abs-file-name))) (progn (write-region "" nil abs-file-name) (my/refresh-dired-buffer)) (message "目录或者文件名无效")))) (defun my/dired-create-new-directory(dirname) (interactive (list (read-directory-name "New Directory: " buffer-dir-path))) (let ((abs-dir-name (file-name-as-directory (expand-file-name dirname)))) (if (not (file-exists-p abs-dir-name)) (progn (make-directory abs-dir-name t) (my/refresh-dired-buffer)) (message "目录已经存在")))) (defun my/dired-copy-file (dest-file-name) (interactive (list (read-file-name "Destination file: " buffer-dir-path))) (let* ((abs-dest-file-name (expand-file-name dest-file-name)) (abs-dest-file-directory (file-name-as-directory (file-name-directory abs-dest-file-name))) (src-file-name (expand-file-name (get-text-property (point) 'file-name) buffer-dir-path))) (if (and (file-directory-p abs-dest-file-directory) (file-exists-p abs-dest-file-directory) (not (file-exists-p abs-dest-file-name)) (file-regular-p src-file-name)) (progn (copy-file src-file-name abs-dest-file-name) (my/refresh-dired-buffer))))) (defun my/dired-copy-directory (dest-dir-name) (interactive (list (read-directory-name "Destionation directory: " buffer-dir-path))) (let ((abs-dest-dir-name (expand-file-name dest-dir-name)) (src-dir-name (expand-file-name (get-text-property (point) 'file-name) buffer-dir-path))) (if (not (file-exists-p abs-dest-dir-name)) (progn (copy-directory src-dir-name abs-dest-dir-name) (my/refresh-dired-buffer)) (message "目标目录已存在")))) (defun my/dired-delete-directory (dirname) (interactive (list (read-directory-name "Delete directory: " buffer-dir-path))) (if (file-exists-p dirname) (progn (delete-directory dirname t) (my/refresh-dired-buffer)) (message "目标目录不存在"))) (defun my/dired-rename-file(rename-file-name) (interactive (list (read-file-name "Rename file name: " buffer-dir-path))) (let* ((abs-new-file-name (expand-file-name rename-file-name)) (abs-new-dir-name (file-name-as-directory (file-name-directory abs-new-file-name))) (src-file-name (expand-file-name (get-text-property (point) 'file-name) buffer-dir-path))) (if (and (file-exists-p abs-new-dir-name) (file-directory-p abs-new-dir-name) (not (file-exists-p abs-new-file-name))) (progn (rename-file src-file-name abs-new-file-name) (my/refresh-dired-buffer)) (message "新文件名错误")))) (defun my/dired-rename-directory(rename-dir-name) (interactive (list (read-directory-name "Rename directory name: " buffer-dir-path))) (let ((abs-new-dir-name (expand-file-name rename-dir-name)) (src-dir-name (expand-file-name (get-text-property (point) 'file-name) buffer-dir-path))) (if (not (file-exists-p abs-new-dir-name)) (progn (rename-file src-dir-name abs-new-dir-name) (my/refresh-dired-buffer)) (message "重命名的目录名称错误"))))到这里我们完成了目录中基本的拷贝、重命名、删除、新增操作,我们还需要一个打开目录或者文件的函数,并将它们做一个绑定(defun my/dired-open-file-or-directory() (interactive) (let ((abs-path (expand-file-name (get-text-property (point) 'file-name) buffer-dir-path))) (if (file-exists-p abs-path) (cond ((file-regular-p abs-path) (find-file abs-path)) ((file-directory-p abs-path) (let ((full-path (file-truename abs-path))) ;; 获取真实路径,主要用来处理类似 "." , ".." 这样的路径 (setq-local buffer-dir-path full-path) (my/refresh-dired-buffer))) (t (message "无法识别的条目"))))))我们可以在 my/active-dired-mode 函数中添加一条快捷键绑定, (keymap-local-set "RET" #'my/dired-open-file-or-directory)到此为止,我们就完成了这么一个简单的 dired-mode。读者如果想更进一步的练习,可以考虑实现一个写入的功能,用户可以随意改变这个buffer中的内容,写入时根据修改的文件名或者目录名批量的修改真实文件或者目录。
2026年09月09日
4 阅读
0 评论
0 点赞
2026-09-08
《elisp 每日一练》——目录的CRUD
在之前的一系列的文章中,我们讨论了有关文件的一些操作,包括文件读写与文件的属性。我们今天开始讨论目录的操作背景知识我们今天来讨论目录的CRUD,所谓的CRUD也就是我们常说的增删改查。目录内由多个文件和目录组成,所以它的增删改查也就是操作里面的文件和目录以及获取目录中的子元素。路径我们首先还是介绍一些与路径有关的函数file-directory-p 判断路径是否为目录file-name-as-directory 确保传入的路径末尾有 / ,这在我们拼接路径时很有用所以对于一个陌生的路径,我们可以先通过 file-exists-p(函数不区分是文件或者目录,只判断路径是否真实存在) 判断它是否存在。如果存在的话可以使用 file-regular-p 和 file-directory-p 判断是否是文件或者目录,如果是目录可以通过 file-name-as-directory 返回一个末尾带 / 的标准目录路径列目录与属性我们查询目录一般是查询目录中的文件和子目录,查询目录中的文件可以使用 directory-files 。它的函数签名如下:(directory-files DIR &optional FULL MATCH NOSORT COUNT)DIR 表示要查询的目录FULL 为 t 时表示返回目录中子文件和目录的全路径MATCH 是一个正则表达式,用来筛选指定条件的子内容NOSORT 对输出的目录内容不排序COUNT 如果目录下文件太多了,我们可以使用这个参数限制返回的数据项在Linux中,文件与目录的属性是类似的,所以目录的属性实际也可以使用 file-attributes 来获取。对于目录中所有文件的属性获取,常规的方式是遍历 directory-files 中列举的文件和目录名称,最后对每个文件或者目录使用 file-attributes 获取属性。这个工作Emacs在底层的C代码端已经帮我们做了,我们可以通过 directory-files-and-attributes 一次性获得目录的所有内容以及对应的属性。它的函数原形如下:(directory-files-and-attributes DIRECTORY &optional FULL MATCH NOSORT ID-FORMAT COUNT)它的函数原形就是 directory-files 和 file-attributes 的组合。返回值类似于 ((FILE1 . FILE1-ATTRS) (FILE2 . FILE2-ATTRS) ...) , FILE是文件的名称,FILE-ATTRS 是文件属性,属性的组织方式与 file-attributes 的返回值一样。文件操作针对文件和目录的增删,有以下函数可以了解一下:make-directory 创建目录,它可以额外接受一个 PARENTS 参数,该参数为 t 时可以递归创建,相对于 mkdir -prename-file 移动或者重命名文件和目录,OK-IF-ALREADY-EXISTS 参数如果为 t 则会覆盖同名文件,否则报错copy-file 复制文件copy-directory 复制目录,默认递归复制所有子目录delete-file 删除文件delete-directory 删除目录练习至此我们对目录的操作差多不都了解完了,本次的练习我在上一节的基础上完成一个简单的查看目录或者文件属性的功能。(defun my/format-file-attribute (filename attrs) "格式化文件名和文件属性 FILENAME 为文件名 ATTRS 为 file-attributes 返回的属性列表" (let ((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)))) (format "%-11s %-8s %-8s %8d Bytes %s %s" modes owner group size modified-time filename))) (defun my/list-directory-attribute(dir-path) "在新buffer中列出所有文件和子目录" (interactive "fDirPath: ") (let ((abs-dir-path (expand-file-name dir-path))) ;;路劲必须存在并且为目录 (if (and (file-exists-p abs-dir-path) (file-directory-p abs-dir-path)) (let* ((file-attrs (directory-files-and-attributes abs-dir-path)) (buffer (get-buffer "DirAttribute"))) (if (not buffer) (setq buffer (get-buffer-create "DirAttribute")) (switch-to-buffer buffer) (setq-local buffer-dir-path abs-dir-path) (erase-buffer) (goto-char (point-min)) (dolist (file-and-attr file-attrs) (let* ((filename (car file-and-attr)) (attr (cdr file-and-attr)) (attr-string (my/format-file-attribute filename attr))) (insert attr-string) (add-text-properties (point) (1- (point)) `(file-name ,filename)) (newline))))))))这些代码与上一篇的基本类似,我们新加了一个属性用于记录当前行对应的文件名,并且添加了一个 buffer local 变量,记录当前buffer对应的是哪个目录的内容。后续我们可以定义一系列的文件和目录操作的函数, 可以以 buffer local 为基础实现文件的拷贝、重命名等快速操作小结本节我们介绍了目录和文件的属性以及一些操作,本节介绍的api函数如下:file-directory-pfile-name-as-directorydirectory-filesdirectory-files-and-attributesmake-directoryrename-filecopy-filecopy-directorydelete-filedelete-directory
2026年09月08日
4 阅读
0 评论
0 点赞
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日
2 阅读
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日
3 阅读
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日
5 阅读
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日
4 阅读
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日
6 阅读
0 评论
0 点赞
1
2
3
...
8