首页
归档
友情链接
关于
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-18
《elisp 每日一练》——Major Mode 中的keymap 管理
本节我们来介绍 Major Mode 中对于keymap 的管理,我们前面介绍过 define-minor-mode 中 :keymap 关键字。 但是我们查询文档的话发现,它并不支持 :keymap 关键字背景知识我们还是准备一个测试用例,看看宏展开是什么样的(defvar my-code-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c c c") #'compile) (define-key map (kbd "C-c c r") #'recompile) map) "Keymap for `my-code-mode'.") (define-derived-mode my-code-mode prog-mode "MyCode" "带有快捷键绑定的代码主模式。")在函数定义开始之前,它是这样处理keymap的(unless (boundp 'my/code-mode-map) (put 'my/code-mode-map 'definition-name 'my/code-mode)) (with-no-warnings (defvar my/code-mode-map (make-sparse-keymap))) (unless (get 'my/code-mode-map 'variable-documentation) (put 'my/code-mode-map 'variable-documentation (purecopy "Keymap for `my/code-mode'.")))这段代码表明,Major Mode 处理 keymap 的时候强制使用 mode 名称后带 -map 的符号。我们无法使用自己名称命名的keymap。(unless (boundp 'my/code-mode-map) (put 'my/code-mode-map 'definition-name 'my/code-mode))上述代码先检查对应名称的keymap 变量是否被绑定,如果没有被绑定,它会给这个符号挂一个 definition-name 属性,属性的值是mode 的符号。这个属性是 Emacs 29 引入的,用于告诉 find-function / describe-symbol 之类的工具:“这个 keymap 变量是由 my/code-mode 这个模式定义出来的”,方便用户跳转到定义处。后面它使用 defvar 直接重新定义了一个新的变量。请注意我们之前提到过 defvar 的特性,如果变量未定义则定义,如果变量已定义它就什么都不做。这里因为我们提前定义了同名的变量,所以这段代码实际上是什么都没有做。(unless (get 'my/code-mode-map 'variable-documentation) (put 'my/code-mode-map 'variable-documentation (purecopy "Keymap for `my/code-mode'.")))这段我们之前介绍过,它是在设置变量的文档,如果文档存在则不进行操作。函数中,keymap 采用的继承的方案,这部分的内容我们在之前已经详细分析过了,就不再继续了。总结本篇我们详细分析了Major Mode 中 keymap 的管理。与 minor mode 不同,我们发现它不支持通过 :keymap 关键字自定义 keymap 的名称,而是必须采用统一的名称:mode名+ -map
2026年09月18日
7 阅读
0 评论
0 点赞
2026-09-17
《elisp 每一一练》——Major Mode 继承机制
前面我们在介绍定义Major Mode 的宏 define-derived-mode 时提到,它本身是支持继承的,并且它有一个固定参数用于传入父Mode。今天我们来剖析一下它是如何实现继承以及继承哪些内容背景知识继承机制的分析我们还是一样的,使用简单的代码进行宏展开:(define-derived-mode my/code-mode prog-mode "MyCode" "继承自 prog-mode 的代码主模式")展开之后,我们观察一下,它主要发生了如下变化:(if (fboundp 'derived-mode-set-parent) (derived-mode-set-parent 'my/code-mode 'prog-mode) (put 'my/code-mode 'derived-mode-parent 'prog-mode))首先我们看到,这段代码通过 derived-mode-set-parent 为当前我们定义的mode 设置了一个父 mode。而之前的代码中调用 derived-mode-set-parent 设置的是 nil。derived-mode-set-parent 是Emacs 30.1 引入的新函数,用于声明Mode之前的继承关系。它的实现非常简单:(defun derived-mode-set-parent (mode parent) "Declare PARENT to be the parent of MODE." (put mode 'derived-mode-parent parent) (derived-mode--flush mode))我们看到它就干了两件事,一个是给mode的 'derived-mode-parent 属性设置成父mode,然后刷新相关的内部缓存,确保之后 Emacs 能正确处理继承关系。昨天我们简单的看了一下加载mode 的函数,在没有继承的情况下它比较简单,就是简单的设置了对应buffer local 变量的值为nil。加上继承之后,它的代码有了一些变化(defun my-code-mode nil "继承自 prog-mode 的代码主模式。\n\nIn addition to any hooks its parent mode `prog-mode' might have run,\nthis mode runs the hook `my-code-mode-hook', as the final or\npenultimate step during initialization.\n\n\\{my-code-mode-map}" (interactive) (delay-mode-hooks (prog-mode) (setq major-mode 'my-code-mode) (setq mode-name "MyCode") (progn (if (get 'prog-mode 'mode-class) (put 'my-code-mode 'mode-class (get 'prog-mode 'mode-class))) (unless (keymap-parent my-code-mode-map) (set-keymap-parent my-code-mode-map (current-local-map))) (let ((parent (char-table-parent my-code-mode-syntax-table))) (unless (and parent (not (eq parent (standard-syntax-table)))) (set-char-table-parent my-code-mode-syntax-table (syntax-table)))) (unless (or (abbrev-table-get my-code-mode-abbrev-table :parents) (eq my-code-mode-abbrev-table local-abbrev-table)) (abbrev-table-put my-code-mode-abbrev-table :parents (list local-abbrev-table)))) (use-local-map my-code-mode-map) (set-syntax-table my-code-mode-syntax-table) (setq local-abbrev-table my-code-mode-abbrev-table)) (run-mode-hooks 'my-code-mode-hook))通过后面的代码,我们看到,实际上继承管理的是这么6个资源: keymap、syntax-table、abbrev-table、mode-class、major-mode 变量和 mode-name 变量。这些代码正好为这些资源建立了对应的父子关系。首先它调用 (prog-mode) 先初始化 prog-mode 的环境,熟悉面向对象编程的读者应该对这种写法不陌生,初始化子类之前先初始化父类算是面向对象的基本操作。然后通过 setq 来修正 major-mode 和 mode-name 变量。之后就是设置 mode-class 属性了(if (get 'prog-mode 'mode-class) (put 'my-code-mode 'mode-class (get 'prog-mode 'mode-class)))mode-class 本身是Emacs用于标记Sepcial Mode 的一个符号属性。一个Major Mode 如何被标记为 special,表示这个buffer中的内容是由某个mode自主生成的数据,不允许进行编辑。所以这段代码就很明显了,它获取 prog-mode 下的 mode-class 属性,如果有特殊标记,那么将它的值赋值给当前我们的mode接下来就是设置keymap 的继承关系了。(unless (keymap-parent my-code-mode-map) (set-keymap-parent my-code-mode-map (current-local-map)))与面向对象中的继承类似,子类可以重写父类的方法,对于 keymap 来说, 子 Mode 的快捷键可以覆盖父Mode。上述代码的含义就是如果我们定义的 mode 里面的 keymap 如果没有 parnet,那么就将 (current-local-map) 赋值给 parent 作为它的父类 keymap,因为我们之前调用 (prog-mode) 用父 mode 中变量初始化了当前 buffer,所以 (current-local-map) 实际取到的是 prog-mode 的 keymap。后门我们一次把 syntax-table和 abbrev-table 看完,它们都是相同的逻辑(let ((parent (char-table-parent my-code-mode-syntax-table))) (unless (and parent (not (eq parent (standard-syntax-table)))) (set-char-table-parent my-code-mode-syntax-table (syntax-table)))) (unless (or (abbrev-table-get my-code-mode-abbrev-table :parents) (eq my-code-mode-abbrev-table local-abbrev-table)) (abbrev-table-put my-code-mode-abbrev-table :parents (list local-abbrev-table)))与 syntax-table 不同的是, abbrev-table 是通过属性 :parent 来存储父Mode信息的。最后在这些资源都初始化完成之后,就是修改当前 buffer 中的 keymap 、syntax-table、abbrev-table 为子mode中的内容了。(use-local-map my-code-mode-map) (set-syntax-table my-code-mode-syntax-table) (setq local-abbrev-table my-code-mode-abbrev-table)在初始化完了这些信息之后,正式通过上述代码由子mode中的资源接管原来 prog-mode 设置的buffer local 环境。常用的父类 mode一般来讲,我们开发一个新的major mode 并不会从0开始,而是选用合适的内置mode进行继承。一般来讲,常见的基础mode有:prog-mode 编程语言的基础mode,默认启用语法高亮、代码缩进引擎机制,绑定代码注释逻辑,排斥拼写检查等文本特征。text-mode 默认启用段落分隔规则、支持 flyspell-mode 拼写检查,代码高亮引擎默认留空special-mode 它是只读/交互式buffer 的基础,默认当前buffer 是只读的,并且绑定了 q 作为退出fundamental-mode 是Emacs最通用、无任何特殊定义的模式。因为所有的buffer都必须有一个mode,所以Emacs在无法确定当前buffer时候何种mode的时候就默认使用它上面我们提到,mode-class 是一个特殊的标记,读者可以尝试获取这些mode 的 mode-class 属性,看看它们具体是什么值。总结所以在存在继承关系的时候,它首先通过 derived-mode-set-parent 或者手动设置 mode 的 derived-mode-parent 属性的方式设置对应的继承关系,以便Emacs能正确处理它们。在加载mode 的函数中,它执行了下面的操作:启用父类的 mode ,用父类的"构造函数"初始化环境更新 major-mode 和 mode-name 变量更新 mode-class 标志,主要继承自父类设置 keymap 、syntax-table、abbrev-table 的父类,确定可以继承父类的内容更新当前buffer中 keymap 、syntax-table、abbrev-table 为子类的内容在编写自己的mode时通常使用内置的mode作为父类,Emacs中有一些父类提供了通用的模板: prog-mode 用于编程语言的mode、text-mode 用于文档类型的mode、special-mode 用于交互式内容
2026年09月17日
10 阅读
0 评论
0 点赞
2026-09-16
《elisp 每日一练》——Major Mode 初步探索
在前面的文章中,我们详细分析了关于 minor mode 对于各种资源的管理,最后以一个修改宏增加参数的例子结束了关于minor mode 的话题。今天我们正式进入有关 Major Mode 的话题背景知识与Minor Mode 不同,一个buffer 只能有一个Major Mode。我们可以使用宏 define-derived-mode 来定义 Major Mode。你可能会感觉很奇怪,为什么minor mode 的定义是使用 define-minor-mode 而 Major Mode 是不是使用 define-major-mode 来定义了。这是因为 Major Mode 引入了继承机制。(define-derived-mode CHILD PARENT NAME [DOCSTRING] [KEYWORD-ARGS...] &rest BODY)CHILD 新定义的Major Mode 的名称,与 define-minor-mode 的第一个参数相同PARENT 父类Major Mode 的名称NAME Major 的字符串名称KEYWORD-ARGS 可选的关键字参数对,用于精细控制模式的初始化行为我们还是用一个最简单的例子看看它展开之后是什么样子的(define-derived-mode my/foucs-mode nil "My Focus")展开之后,显示如下内容:(progn (defvar my/foucs-mode-hook nil) (unless (get 'my/foucs-mode-hook 'variable-documentation) (put 'my/foucs-mode-hook 'variable-documentation "Hook run after entering `my/foucs-mode'.\nNo problems result if this variable is not bound.\n`add-hook' automatically binds it. (This is true for all hook variables.)")) (unless (boundp 'my/foucs-mode-map) (put 'my/foucs-mode-map 'definition-name 'my/foucs-mode)) (with-no-warnings (defvar my/foucs-mode-map (make-sparse-keymap))) (unless (get 'my/foucs-mode-map 'variable-documentation) (put 'my/foucs-mode-map 'variable-documentation (purecopy "Keymap for `my/foucs-mode'."))) (progn (defvar my/foucs-mode-syntax-table) (unless (boundp 'my/foucs-mode-syntax-table) (put 'my/foucs-mode-syntax-table 'definition-name 'my/foucs-mode) (defvar my/foucs-mode-syntax-table (make-syntax-table))) (unless (get 'my/foucs-mode-syntax-table 'variable-documentation) (put 'my/foucs-mode-syntax-table 'variable-documentation (purecopy "Syntax table for `my/foucs-mode'.")))) (progn (defvar my/foucs-mode-abbrev-table) (unless (boundp 'my/foucs-mode-abbrev-table) (put 'my/foucs-mode-abbrev-table 'definition-name 'my/foucs-mode) (defvar my/foucs-mode-abbrev-table (progn (define-abbrev-table 'my/foucs-mode-abbrev-table nil) my/foucs-mode-abbrev-table))) (unless (get 'my/foucs-mode-abbrev-table 'variable-documentation) (put 'my/foucs-mode-abbrev-table 'variable-documentation (purecopy "Abbrev table for `my/foucs-mode'.")))) (if (fboundp 'derived-mode-set-parent) (derived-mode-set-parent 'my/foucs-mode 'nil) (put 'my/foucs-mode 'derived-mode-parent 'nil)) nil (defun my/foucs-mode nil "Major-mode.\nUses keymap `my/foucs-mode-map', abbrev table\n`my/foucs-mode-abbrev-table' and syntax-table\n`my/foucs-mode-syntax-table'.\n\nThis mode runs the hook `my/foucs-mode-hook', as the final or\npenultimate step during initialization.\n\n\\{my/foucs-mode-map}" (interactive) (delay-mode-hooks (kill-all-local-variables) (setq major-mode 'my/foucs-mode) (setq mode-name "My Focus") nil (use-local-map my/foucs-mode-map) (set-syntax-table my/foucs-mode-syntax-table) (setq local-abbrev-table my/foucs-mode-abbrev-table)) (run-mode-hooks 'my/foucs-mode-hook)))这段内容看起来比 define-minor-mode 生成的要简短一些。我们还是来看看它每一步都做了什么:首先定义并文档化了一系列的变量: hook、mode—keymap、syntax-table、abbrev-table通过 derived-mode-set-parent 记录mode间继承的父子关系定义了一个对应的 mode 函数请注意,这个函数与 minor mode 不同,它并不是一个切换的函数,也没有定义记录 mode 开启或者关闭的标志变量。这是因为Major Mode 本身在设计时就不支持随意的关闭或者开启。从设计上,每个buffer 必须有一个,但是最多只能有一个 Major Mode, Major Mode 是不支持随意开关的,如果想要替换Major Mode,它是强制清空上一个Major Mode 的所有设置并加载新的。所以每次调用这个mode函数实质上是强行占领了之前mode 的位置。有了这个前提,我们再来看看这个函数具体的操作。函数所有的代码都被包裹在 delay-mode-hooks 宏中,这个宏可以保证里面的所有代码在hook之前运行。通过 kill-all-local-variables 清理当前buffer的所有 buffer local 变量记录当前 major mode 到变量 major-mode 中设置变量 mode-name 为我们传入的mode 的use-local-map 来给mode安装快捷键映射set-syntax-table 和 local-abbrev-table 设置当前的 syntax table 和 abbrev table最后才运行对应的hook总结从这里我们看到,设计上 major mode 一个buffer 有且只有一个,所以在设计上就比较简单粗暴,它不考虑多个mode并存或者切换的场景,它简单的清理前任留下来的东西,直接加入自己的。所以它的代码比minor mode 要简单的多。但是它涉及到的概念可一点也不比 minor mode 少,除了hook、keymap 这些我们经常见到的内容,还有 syntax table、abbrev table和继承这类概念。这些我会在后面的文章中一一介绍
2026年09月16日
9 阅读
0 评论
0 点赞
2026-09-15
《elisp 每日一练》—— Minor Mode 回顾与综合练习
到此,我们关于 Minor Mode 的介绍差不多就完结了,按照惯例,我们先回顾一下之前的内容,然后展开今天的练习往期回顾前面我们花了一些篇章,介绍了定义 Minor Mode 的宏 define-minor-mode。它的主要流程是:定义Mode 切换的标志变量定义 Mode 切换的函数管理 Mode 对应的全局hook 变量调用 add-minor-mode 往Emacs中注册一个 Minor Mode切换函数主要的功能是:根据标志变量的值更新数据结构:local-minor-modes 注册或者删除对应的项无条件的执行同名的hook,并且在开启和关闭时会执行对应的 on hook 和 off hook更新显示信息后面我们介绍了它是如何通过一些关键字管理对应的内容::init-value 可以控制标志变量初始值为 t 或者 nil 表示注册时就开启或者后续由用户自己开启:global 将标志变量定义为 customize 变量,这样一处buffer启用,全局启用:keymap 管理我们的keymap定义,会将我们在此关键字下定义的 keymap 注册到 add-minor-mode 中:ligher 定义我们在mode line 上的显示,它会将内容注册到 add-minor-mode ,由该函数将内容追加到 minor-mode-alist 中body 代码部分,这部分用来进行相关配置代码的变更或者恢复这些内容看似很多,实际上都是围绕着 define-minor-mode 和 add-minor-mode 进行,读者感兴趣的话可以深入研究一下这个宏和函数的实现,相信能从中挖掘到更多宝藏。练习回顾完了之后,我们进入今天的练习。前面我们发现 :keymap 、:lighter 注册的顺序都与 add-minor-mode 中一个名为 after 的参数有关,但是 define-minor-mode 传入的 after 参数写死了,被标记为了 after。所以今天我们的练习就是:定义一个自己的扩展宏,在原来 define-minor-mode 的基础上支持一个新的 :after 关键字。并且我们实现的思路是调用 define-minor-mode 时修改传递给add-minor-mode 中 after 的值,而不是简单的将 define-minor-mode 的源码拷贝过来修修改改,或者在调用完 define-minor-mode 之后手动的修改对应的顺序。虽然在日常使用中需要关注这个顺序的场景并不多,但是我想通过这个练习可以让读者更深入的理解lisp 中代码即数据的理念,以及将这种理念发挥到极致的宏的思想。这样读者在日后碰到像 use-package 这种复杂的插件不至于束手无策。为了使各位读者更容易理解这个思路,我们先从lisp 中代码即数据这理念讲起。首先我们要明确的一个概念是,Emacs中大多数内容都是一个list,同一个list既可以作为代码,也可以作为数据。例如 (message "Hello World")。我们既可以说他调用message函数并传入字符串参数,也可以说它是一个list,有两个成员: message符号,和字符串。既然它是list,所以我们就可以通过 car、cdr 这些操作list的函数来操作它。例如:(let ((sexp '(message "hello world")) (user "Jack")) (eval (list (car sexp) ;; message "Hello, %s" user)))这里我们将 (message "hello world") 这个list作为数据。后续我们根据这个数据,取第一个符号 message,后续跟上 "Hello, %s" 和 user 这个变量组织成一个新的list (message "Hello, %s" user)。最终我们调用 eval 执行这个list,最终得到了一个动态变化的代码。相当于我写代码修改了原来的代码。我们再来看看宏,实际上宏就是利用上述操作形成一组新的 list 数据,调用宏就是对宏生成的 list 执行 eval 操作。例如:(defmacro say-hello() '(message "Hello World")) (say-hello)因为我们只是想要宏展开生成的 list 数据,并不希望执行表达式,所以这里使用 ' 。而调用它的过程就是将其进行展开得到 (message "Hello World") 数据结构,并调用 eval 进行执行。得到宏展开的函数有 macroexpand 和 macroexpand-1 ,顾名思义,后者只展开一层,而前者会完全展开整个宏。因为这个宏也是写死了输出的内容,我们尝试一下实现一个新的宏,接受一个user参数,调用时输出 hello, user 字样。因为 say-hello 宏展开会得到一个list,所以我们只要操作这个展开的list,就能实现修改并添加参数的需求。(defmacro say-hello-to-user (user) (let ((sexp (macroexpand-1 '(say-hello)))) (list (car sexp);;message "hello, %s" user))) (say-hello-to-user "Tom")上述代码我们的思路是,通过 macroexpand-1 得到展开后的list数据,然后操作list,构建一个新的list并返回,在执行的时候执行这个list 就好了。这里提醒一下对宏不太熟悉的读者,我们在展开宏的时候需要使用 ' 或者 ` 表示这是一个引用不需要执行这个表达式。后面我们使用list 的办法是为了与前面的例子作为呼应减少理解难度,更地道的写法是(defmacro say-hello-to-user (user) (let ((sexp (macroexpand-1 '(say-hello)))) `(,(car sexp) "Hello, %s" ,user))) ;;使用 `表示后面,开头的表达式需要求值,最终也是生成一个 list 结构实际上我们完成上述需求也是这个思路,展开 define-minor-mode、在一堆list中找到我们想要的以 add-minor-mode 开头的list,修改list 中 after 对应位置的内容。解释如何改的问题,下面再看看如何解析 :after 参数。根据 define-minor-mode 的定义来看,它接受的关键字实际上是位于参数 body 位置的。body位置本身传入的也是一个list。对于 :keyword 这种形式的内容,是一组特殊的符号,在求值时永远返回它自己。关键字后面跟随的是它对应的具体的值。对于这种结构组成的list是一个 property list,我们可以使用 plist-get 和 plist-put 增加或者获取一个属性对应的值。我们要判断list中某个成员是否是这种形式的符号,可以使用 keywordp。所以说不管body中有哪些内容,他本质上还是一个list,仍然可以用操作list 的办法操作它。下面我们来看一个具体的例子:(defmacro say-hello (&rest body) (let ((user nil) ;; 存储user的值 (rest nil) ;; 存储剔除 user 项之后的表达式 (body-content body)) ;; 用于循环的临时变量 ;; 扫描 body,抽离 :user (while body-content (if (and (keywordp (car body-content)) (eq (car body-content) :user)) (progn (setq user (cadr body-content)) ; :user 后面的表达式 (setq body-content (cddr body-content))) (push (car body-content) rest) (setq body-content (cdr body-content)))) (setq rest (nreverse rest)) ; 前面的 push 打乱了原本的顺序恢复顺序 ;; 构造list, 作为宏的返回值 `(progn (message "Hello, %s" ,user) ,@rest))) (say-hello :user "Tom" (message "doing something else") (message "finish"))上面的代码结合注释不难理解,我们通过一个循环来提取我们关注的关键字 :user 之后的表达式。然后将剩余部分的代码整个组装并返回。大家看到这里对宏是否有了新的理解呢?宏本身就是一组操作list的固定代码,它将我们传入的数据或者代码进行打散重新组装,并输出我们想要的list组合。现在再来看 define-minor-mode 的代码可能就不那么难以理解了。到了这里相信各位读者对于本次我们要完成的功能应该知道如何下手了,甚至有的读者也跃跃欲试了,但是别着急,我们先来看看define-minor-mode 中有哪些代码我们可以借鉴的。首先我觉得值得聊聊的一处是开头位置 : (declare (doc-string 2) (indent defun) (debug (&define name string-or-null-p [&optional [¬ keywordp] sexp &optional [¬ keywordp] sexp &optional [¬ keywordp] sexp] [&rest [keywordp sexp]] def-body)))这段代码对于我们后续开发自己的宏很有借鉴意义。这段代码对于实际功能没有什么大的帮助,它们表达的含义如下:(doc-string 2) 表示宏的第二参数,也是就是 doc 参数是一个文档字符串,这样Emacs中的文档生成工具可以正确收集这个宏的说明文档(indent defun) 定义代码的缩进格式,按照函数的缩进方式进行debug 告诉调试器如何解析这个宏的语法结构因为我们要为自己的宏加上关键字 :after,我们看看 define-minor-mode 是如何处理这类参数的,有没有可以借鉴的: (while (keywordp (setq keyw (car body))) (setq body (cdr body)) (pcase keyw (:init-value (setq init-value (pop body))) (:lighter (setq lighter (purecopy (pop body)))) (:global (setq globalp (pop body)) (when (and globalp (symbolp mode)) (setq setter `(setq-default ,mode)) (setq getter `(default-value ',mode)))) (:extra-args (setq extra-args (pop body))) (:set (setq set (list :set (pop body)))) (:initialize (setq initialize (list :initialize (pop body)))) (:type (setq type (list :type (pop body)))) (:keymap (setq keymap (pop body))) (:interactive (setq interactive (pop body))) (:variable (setq variable (pop body)) (if (not (and (setq tmp (cdr-safe variable)) (or (symbolp tmp) (functionp tmp)))) ;; PLACE is not of the form (GET . SET). (progn (setq setter `(setf ,variable)) (setq getter variable)) (setq getter (car variable)) (setq setter `(funcall #',(cdr variable))))) (:after-hook (setq after-hook (pop body))) (_ (push keyw extra-keywords) (push (pop body) extra-keywords))))它的处理方式没有脱离我们上面介绍的处理框架。它也是遍历body列表,然后依次获取想要的内容。一口气将它不关心的参数放到了 extra-keywords 中。剩余的内容就被放到了 body 变量中。后面就是根据这些变量的值生成对应的list结构了。基于这些知识我们就可以编写需求对应的代码了:(defmacro my/define-minor-mode-ex (mode doc &rest body) "自定义的 define-minor-mode,支持额外传入 :after 关键字参数。" (declare (doc-string 2) (indent defun) (debug (&define name string-or-null-p [&optional [¬ keywordp] sexp &optional [¬ keywordp] sexp &optional [¬ keywordp] sexp] [&rest [keywordp sexp]] def-body))) (let ((after-eval nil) (clean-body nil) ;;剔除 after 之后的list (temp-body body)) ;;循环变量 ;;循环剔除 :after 关键字 (while temp-body (if (and (keywordp (car temp-body)) (eq (car temp-body) :after)) (progn (setq after-eval (cadr temp-body)) (setq temp-body (cddr temp-body))) (push (pop temp-body) clean-body) (push (pop temp-body) clean-body))) (setq clean-body (nreverse clean-body)) ;;展开原生的宏,得到对应的list数据 (let ((expand-code (macroexpand-1 `(define-minor-mode ,mode ,doc ,@clean-body)))) ;;如果指定了 :after 则修改里面调用 add-minor-mode 的参数 (when after-eval (setq expand-code (mapcar (lambda (form) (if (and (listp form) (eq (car form) 'with-no-warnings) (listp (cadr form)) (eq (car (cadr form)) 'add-minor-mode)) ;;修改参数列表 (let ((add-form (cadr form))) ;;获取参数列表 `(with-no-warnings (add-minor-mode ,(nth 1 add-form) ;; TOGGLE ,(nth 2 add-form) ;; NAME ,(nth 3 add-form) ;; KEYMAP ,after-eval ;;这里就是我们要替换的 after 参数 ,@(nthcdr 5 add-form)))) ;;剩余的一口气全塞进去 form)) expand-code))) expand-code))) (my/define-minor-mode-ex my/focus-mode "test mode" :after 'bar-mode)到此我们的任务就完成了,这个是基于当前宏展开的,我们是按照 with-no-warnings 在顶层list中可以遍历到,并且 add-minor-mode 是 with-no-warnings 所在列表的第二个参数来编写的。如果后续它们的顺序改变了,这段代码可能就不那么准了,如果要严谨一些的话,我们可以递归遍历所有列表的子元素进行查找。这个读者可以自行尝试,我们就不在这里再展开了。
2026年09月15日
8 阅读
0 评论
0 点赞
2026-09-14
《elisp 每日一练》—— Body 代码块
前面我们介绍了 define-minor-mode 中的init-value、global、keymap、lighter 关键字,了解了它对于开关变量、快捷键和 mode line 显示上的管理。现在我们进入关于 define-minor-mode 的最后一个步骤,看看如何利用body管理minor mode 中自定义的资源背景知识我们来编写一个简单的测试代码(define-minor-mode my/focus-mode "" :lighter "My focus" (message "welcome to my focus mode"))展开之后,我们发现它被包裹在开关函数之中,无条件的被执行。根据我们前面介绍的该宏的几个核心动作,它位于维护 local-minor-modes 之后,但是在执行 hook 变量之前。所以我们在编写这部分代码时通常要先判断当前mode是处于卸载还是加载的状态。一个良好的 Minor Mode 应该不影响其他buffer,关闭和开启时也没有残留,所以我们这部分代码一定要重视资管的管理。Emacs中主要的资源有变量、hook、定时器、以及一些ui特效。对于它们的管理一般的规范如下:对于变量,尽量使用 setq-local 避免污染全局,同时在退出时记得将它还原成原来的值对于 hook 来说,add-hook 时尽量使用 buffer local 的 hook,在关闭当前mode时要记得卸载hook对于timer 来说, 定义也应该使用 setq-local,关闭时记得关闭在UI上面,开启时应该保存原值之后再修改,关闭时恢复原始模样练习本节的内容比较简单,因此我们的练习也比较简单,我们定义个简单的mode,用于改变当前buffer的一些UI特性,我这里简单的将它的特性定义为:显示行号,并且显示相对行号调整光标样式为一条细竖线标记光标所在行我们需要管理这些属性,以便退出mode 时它能正常恢复(define-minor-mode my/normal-notepad-mode "修改Emacs的显示效果" :lighter " LD" (if my/normal-notepad-mode (progn ;; 保存当前值(buffer-local 保存) (setq-local my-line-display--saved-line-numbers (if (bound-and-true-p display-line-numbers) display-line-numbers-type 'off)) (setq-local my-line-display--saved-hl-line (if (boundp' hl-line-mode) hl-line-mode global-hl-line-mode)) (setq-local my-line-display--saved-cursor-type (if (boundp 'cursor-type) cursor-type t)) ;; 应用新设置 (setq-local display-line-numbers-type 'relative) (display-line-numbers-mode 1) (hl-line-mode 1) (setq-local cursor-type 'bar)) (progn ;; 恢复行号 (if (eq my-line-display--saved-line-numbers 'off) (display-line-numbers-mode -1) (setq-local display-line-numbers-type my-line-display--saved-line-numbers) (display-line-numbers-mode 1)) ;; 恢复高亮行 (unless my-line-display--saved-hl-line (hl-line-mode -1)) ;; 恢复光标 (setq-local cursor-type my-line-display--saved-cursor-type))))上面的套路非常简单,先判断当前状态,如果处于开启状态下,我们可以先保存原始值,再进行覆盖。如果关闭我们使用原来保存的值进行恢复。需要注意的是,因为我们仅仅关注当前buffer的显示,所以这里我们应该使用 setq-local 定义局部缓冲区变量。另外要保存未启用时的原始值,我们只能在开启状态,设置这些属性之前进行处理,否则我们取到的就不是原始值。小结本篇我们的内容非常简单,我们主要提供一个代码为大家演示了如何正确获取原始值并在关闭mode的时候恢复原始状态。
2026年09月14日
5 阅读
0 评论
0 点赞
2026-09-13
《elisp 每日一练》——状态栏显示管理与modeline 动态更新
前面几篇我们陆陆续续的分析了 define-mode-line 中管理开关变量、快捷键相关的内容。我们可以通过 init-value、global来设置开关变量的初始值和是否定义为全局启动。通过 keymap 可以定义快捷键的keymap。现在我们来讨论 lighter 关键字的相关内容背景知识mode-line-formatlighter 关键字是用来定义当 Minor Mode 激活时在 Mode Line (状态栏) 上显示的文本。在详细描述 lighter 的作用之前,我们要先来讨论 Mode Line 的显示机制Emacs 的 Mode Line 位于每个窗口底部, 在未经任何美化的Emacs中,它位于 minibuffer 上方显示为灰色,用于显示缓冲区状态和模式信息。它的显示内容由 buffer local 变量 mode-line-format。这是一个由列表、字符串、符号和数字构成的递归数据结构。我在Windows上打开的 *scratch* buffer 中,它的显示的内容为 c\--- *scratc* All L4 (Lisp Interaction ElDoc)c 表示当前缓冲区的编码系统是c, 通常对应 utf-8 或 undecided 在中文环境下的显示,具体取决于你的 buffer-file-coding-system\ 表示换行符的类型是 Dos 风格,也就是采用 \r\n 来换行,对于unix 类型的换行,它显示为 :,Mac风格的换行符显示为 /第一个 - 表示当前缓冲区是可写的,对于不可写的通常表现是 %第二个 - 表示当前缓冲区未被修改,修改之后显示为 *第三个 - 表示是在本地存储,如果是远程的会显示为 @*scratch* 表示当前buffer 的名称ALL 表示当前窗口显示了整个buffer,如果buffer 内容较多,一个窗口显示不下的话,这里可能是 TOP表示显示的是开头位置,BOTTOM 表示显示的结尾,又或者是一个百分比L4 表示光标在第四行Lisp Interaction ElDoc 表示当前加载的 Major Mode 是 Lisp Interaction 、Minor Mode 是 ElDoc这些东西其实不用记忆,因为Emacs最大的特点是灵活,你不喜欢这些显示内容完全可以自己修改,显示自己想要的信息。我们可以通过调整 mode-line-format 变量的值来修改显示内容。我们查看当前 *scratch* buffer 中 mode-line-format 的值如下:(%e mode-line-front-space (:propertize (mode-line-mule-info mode-line-client mode-line-modified mode-line-remote mode-line-window-dedicated) display (min-width (6.0))) mode-line-frame-identification mode-line-buffer-identification " " mode-line-position (project-mode-line project-mode-line-format) (vc-mode vc-mode) " " mode-line-modes mode-line-misc-info mode-line-end-spaces)这个例子很好的演示了 mode-line-format 中的几种常见数据类型:符号例如 mode-line-front-space 、mode-line-buffer-identification 、mode-line-position、mode-line-modes 等等都是一个符号,对于符号,Emacs会取出它的值,这些值可以是列表、字符串、或者数字,后面会再把它当作新的结构递归处理。字符串它可以是固定的字符串,也可以以 % 开头的格式化内容,例如 %b 会显示buffer 名称、%f 显示对应的文件名。这些内容读者可以查看Emacs中关于 mode-line-format 的文档。我们这个例子中的字符串仅仅是几个空格列表整个 mode-line-format 就是一个列表,列表的处理规则取决于第一个元素。如果第一个元素是字符串或者另一个列表,则递归处理所有元素并拼接。% 构造这里开头位置是一个 %e ,虽然说字符串中也可以使用 %e ,但是这里显然不在字符串中。这里的 %e 表示当前缓冲区文件名或进程状态的简短表示。:propertize:propertize 构造规则是: 先递归处理第一个参数(这里是包含多个符号的列表),然后给结果加上文本属性。这里加的是 display 属性,值为 (min-width (6.0)),意思是这段内容至少占 6 个字符宽度。这解决了编码信息(如 c\)在窗口窄时被截断的问题。条件构造条件构造的语法规则是 (symbol then else) , (project-mode-line project-mode-line-format) 就是一个条件构造,如果 project-mode-line 是非 nil 则,处理 project-mode-line-format。后面的 vc-mode 也是同理,它表示,如果当前buffer 开启了 版本管理,就显示版本管理的信息。读者可以打开一个git管理的项目,看看显示效果根据上面的介绍,我们可以推测出上面 mode-line-format 的值将会如何显示:%e 是文件名/进程状态简短标记mode-line-front-space 前端空白,通常为空或一个空格:propertize 它是一个结构,你可以理解为一组信息。根据后面的符号我们知道它分别显示: 编码、客户端、修改状态、远程、专用窗口标记,并且根据后面的属性定义可以知道,它保证至少 6 字符宽mode-line-frame-identification 帧标识,在我的Emacs scratch buffer中显示为空mode-line-buffer-identification 缓冲区名称mode-line-position 光标所在位置project-mode-line 如果启用 project-mode 则通过 project-mode-line-format 显示项目相关的信息vc-mode 如果开启了版本管理,显示版本管理相关的信息mode-line-modes 显示当前开启的mode 信息mode-line-misc-info 其他信息mode-line-end-spaces 尾部空白,通常用于右对齐或者填充一般来讲,如果我们希望修改显示的内容,可以修改其中的某个符号。或者我们可以往 mode-line-misc-info ,这个是官方预留给我们用来自定义显示内容的接口。但是我们修改之后Emacs不会立马启用,需要调用 (force-mode-line-update) 让本次变更在下次重绘时生效。lighter(define-minor-mode my/focus-mode "" :lighter "Be Focus")展开之后,我们发现实际上与 Mode Line 显示相关的内容主要操作是:(add-minor-mode 'my/focus-mode '"Be Focus" (if (boundp 'my/focus-mode-map) my/focus-mode-map) nil nil)通过将 :lighter 中指定的内容传入 add-minor-mode 作为参数然后在切换状态的函数 my/focus-mode 最后调用 (force-mode-line-update) 刷新显示状态。我们再进入 add-minor-mode 中查看关于 Mode Line 显示信息的相关内容:(when name (let ((existing (assq toggle minor-mode-alist))) (if existing (setcdr existing (list name)) (let ((tail minor-mode-alist) found) (while (and tail (not found)) (if (eq after (caar tail)) (setq found tail) (setq tail (cdr tail)))) (if found (let ((rest (cdr found))) (setcdr found nil) (nconc found (list (list toggle name)) rest)) (push (list toggle name) minor-mode-alist))))))我们来分析一下,它是如何将我们的内容显示到 mode line 上的。这段代码与昨天我们分析 keymap 时的行为类似,它是通过修改 minor-mode-alist 来修改 mode line 的显示内容。我们发现它在插入时仍然与 after 有关,默认它会尝试插入到 after 之后。我们前面介绍过,默认的 mode-line-format 中通过 mode-line-modes 显示开启的mode信息。我们查看 mode-line-modes 的值可以找到这样一段代码:(:propertize (#1= minor-mode-alist) ; 要显示的 construct mouse-face mode-line-highlight ; 鼠标悬停时高亮 help-echo "Minor mode mouse-1: Display minor mode menu mouse-2: Show help for minor mode mouse-3: Toggle minor modes" local-map (keymap (header-line keymap (down-mouse-3 . #4=(menu-item "Menu Bar" #3# :filter bindings--sort-menu-keymap))) (mode-line keymap (down-mouse-3 . #4#) (mouse-2 . mode-line-minor-mode-help) (down-mouse-1 . mouse-minor-mode-menu))))这里我们又遇到了 :propertize 。我们还是一样,将第一个元素 (#1= minor-mode-alist) 作为显示的内容,第二个元素mouse-face mode-line-highlight 作为属性,表示这部分显示的内容鼠标放在上面要高亮。后面的 help-echo 表示鼠标放在上面弹出的提示信息。我们来看看 (#1= minor-mode-alist) 表示的含义。前面的 #1= 表示为 #1 。后面可以使用 #1# 来引用。这个内容在前面已经被引用了,所以说这里实际上将 minor-mode-alist 的值标记为 #1 并且进行了显示,后面的内容就是显示的属性。我们可以将鼠标放在上面显示提示信息。练习本节的练习,我们围绕着修改 mode line 的显示进行,我们定义一个 minor mode ,它可以统计buffer中文字数量并显示在 mode line 中。我们先定义一个统计函数(defun my/count-words() (save-excursion (goto-char (point-min)) (let ((count 0)) (while (forward-word 1) (setq count (1+ count))) count)))我们可以在lighter 中使用 eval 构造,这里我们要在后续动态更新的单词数量,所以我们不能使用 (format "%s" my/count-words) 这种形式。我们在前面看到 lighter 中的内容会被原样传入到 add-minor-mode 中,相当于先格式化一个字符串,然后将字符串作为 name 加入到 minor-mode-alist 作为后续显示。后面实际上我们就无法实时更新了。所以这里最好由 eval 来构造一个结构,每次刷新时由Emacs自己计算。(define-minor-mode my/count-words-mode "Count Words" :lighter (:eval (format " WC:%d" (my/count-words))) :init-value t)这样我们发现,每次我们输入一个新的单词之后,在mode line上都会更新显示。我们每一次输入都伴随着显示的刷新,Emacs每次刷新都会根据 mode-line-format 中的内容实时计算,每次计算时因为我们传入的是一个 eval 构造,所以每次都会调用 my/count-words 显示它的结果。总结本章的内容我们先介绍了 mode-line-format ,它是一个列表,Emacs每次更新显示都会解析列表并根据列表计算的值来更新 mode line 显示。我们通过 define-minor-mode 中的 :lighter 定义的内容会通过调用 add-minor-mode 被追加到 minor-mode-alist 中。而 mode-line-format 中存在一个名为 mode-line-modes 的符号,它也是一个列表,内部会通过解析 minor-mode-alist 来实现 minor mode 的名称。
2026年09月13日
8 阅读
0 评论
0 点赞
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日
8 阅读
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日
9 阅读
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日
7 阅读
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日
7 阅读
0 评论
0 点赞
1
...
3
4
5
...
11