首页
归档
友情链接
关于
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-08-26
《elisp 每日一练》——Minor Mode Keymap 绑定
在之前的内容中,介绍了可以存储keymap绑定的几个位置,并且说明了查找的顺序。同时我们也提到绑定到minor mode 中的快捷键只有在minor mode 被激活时才有能生效。那么Emacs 是如何管理 minor mode 中的快捷键的呢?背景知识Emacs 为所有minor mode 维护了一张公共的表—— minor-mode-map-alist。它内部的元素结构为:((minor-mode-variable . keymap) ...)前面是一个minor-mode 的变量,你可以简单的理解为:( (company-mode . company-active-map) (flyspell-mode . flyspell-mode-map) (some-mode . some-mode-map) )虽然实际的数据结构会更加复杂,但是为了方便理解,可以暂时认为它就是这么组织的。当一个 minor mode 被激活时,它的变量部分就被设置为激活状态。在实际查找时会沿着表中的顺序依次查找那些激活的项中是否有对应的绑定。为minor mode 创建新的绑定从上面我们就能看出,如果我们要往特定的 minor mode 中新建自己的快捷键,该如何做呢?如果是一个新建的 minor mode,我们可以先创建一个 keymap,然后通过 define-minor-mode 宏中的 :key 进行关联。define-minor-mode 是用来创建新的minor mode 。它的具体内容等我们介绍 minor mode 的时候讲解。(defvar my-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c m") #'my-command) (define-key map (kbd "C-c n") #'my-other-command) map)) ;; 创建新的 mode 并且绑定 (define-minor-mode my-mode "My minor mode." :lighter " My" :keymap my-mode-map) ;; 激活mode (my-mode 1)如果是已经存在的 minor-mode ,我们可以通过获取mode中的keymap变量,然后通过 keymap-set 传入这个变量进行绑定。一般约定俗称的规矩是 mode 名称后加上 map 就是它的 keymap。例如 my-mode 里面的 keymap 一般命名为 my-mode-map。但是这个并不是强制的,所以更稳妥做法是先看看是否有 my-mode-map 变量的文档,看看说明是否是该mode的map。更稳妥的做法是查找 minor-mode-map-alist 变量。对于这种键值对的结构,我们可以使用 assq 通过键来匹配值。(assq 'company-mode minor-mode-map-alist)如果返回 (company-mode . company-mode-map) 后续我们就可以拿 company-mode-map 来进行操作了。解决快捷键重复的问题我们说实际上每个minor mode 都有一个自己的keymap, 保存在 minor-mode-map-alist,只有在对应的 minor mode 启用时才生效。这样就可能出现一种情况:如果两个minor 拥有同一个快捷键绑定到不同的命令,但是又都处于激活状态,就出现了快捷键冲突,Emacs会根据它们在 minor-mode-map-alist 中的先后顺序进行匹配,这样在后面的快捷键永远不会被匹配到了。我们可以先删除 mode 在 minor-mode-map-alist 中对应的项,然后利用 push 将原来删除的项插入alist 的顶部。(let ((cell (assq mode-var minor-mode-map-alist))) (when cell ;; 1. 先从列表中移除 (setq minor-mode-map-alist (assq-delete-all mode-var minor-mode-map-alist)) ;; 2. 重新推入最头部 (push cell minor-mode-map-alist)))或者我们也可以对于另一个mode中不想要的映射执行 keymap-set 将快捷键绑定的函数置空。练习本次的练习,我们尝试创建一个windows-notepad-mode 绑定一些Windows 文本编辑器的快捷键,将Windows的操作习惯移植到Emacs中。例如 C-c 、C-v、C-x、C-z 这类经典的快捷键绑定到具体命令。(defvar windows-notepad-mode-map (make-sparse-keymap)) (keymap-set windows-notepad-mode-map "C-a" #'mark-whole-buffer) ;;全选 (keymap-set windows-notepad-mode-map "C-c" #'kill-ring-save) ;;拷贝 (keymap-set windows-notepad-mode-map "C-s" #'save-buffer) ;;保存 (keymap-set windows-notepad-mode-map "C-v" #'yank) ;;粘贴 (keymap-set windows-notepad-mode-map "C-z" #'undo) ;;撤销 (keymap-set windows-notepad-mode-map "C-x" #'kill-region) ;;剪切 (define-minor-mode windows-notepad-mode "Windows notepad mode" :lighter " 已开启Windows快捷键绑定" ;;提示信息 :keymap windows-notepad-mode-map ) (windows-notepad-mode)
2026年08月26日
8 阅读
0 评论
0 点赞
2026-08-25
《elisp 每日一练》—— 前缀按键
上一节我们提到Emacs中keymap上某个键可以绑定一个具体的函数,也可以绑定另一个keymap,如果绑定另一个keymap,它就变成了一个前缀键,本篇我们来讨论前缀和自建keymap背景知识(defun my/hello() (interactive) (message "hello world")) (keymap-local-set "C-c p h" 'my/hello)我们先从上面的例子开始。上面我们通过 keymap-local-set 在当前buffer中绑定了一个快捷键。根据之前介绍的查找顺序,它在当前buffer的局部keymap中找到了这个绑定。而我们将它拆解开来,实际上它先找到 C-c ,发现它绑定了一个 keymap 实际上 C-c 就是一个前缀。然后在它绑定的 keymap 中找到了关于 p 的内容,p也绑定了一个 keymap,接着在这个 keymap 中找到了 h 绑定的函数,最终执行这个函数。我们可以用这个图来描述这个树形结构C-c ↓ keymap ↓ p ↓ keymap ↓ h → my/hello也就是说我们调用 keymap-local-set 绑定快捷键时,Emacs已经帮我们构建了这样一个多层的keymap 结构。只不过这个过程我们之前没注意到。既然Emacs已经默默帮我们构造了这样一个keymap,为什么还要提供让我们手动构造自己keymap的方式呢?因为这样可以使前缀的 keymap 成为一个独立的、可复用、可管理的对象,方便插件的开发者组织自己的快捷键。例如 Projectile 有一组 C-c p 的快捷键,那么我们就可以创建一个keymap,将它绑定到 C-c p 的位置,然后我们只要操作这个 keymap ,就可以做到自然的将这些操作都绑定到 C-c p 下面创建一个 keymap 可以使用 make-sparse-keymap。后续我们可以将它创建的 keymap 放入到 define-key 或者 keymap-set 函数的 keymap 参数中。当然你也可以使用 define-prefix-command 这个宏来定义一个专门用于前缀的 keymap 。这个函数会将创建的keymap 映射为一个函数,后续我们可以调用该函数获得底层的 keymap。练习本节我们利用这两种机制来创建一个类似 Projectile 插件的一组以 C-c p 开头的函数。我们绑定这么几个函数:C-c p f 绑定到 find-fileC-c p b 绑定到 switch-bufferC-c p g s 绑定到 vc-dirC-c p g l 绑定到 vc-print-log(defvar my/git-sub-map (make-sparse-keymap) "用于二级git菜单的keymap") (define-prefix-command 'my/project-sparse-map) ;; 绑定一级菜单 (keymap-set my/project-sparse-map "f" #'find-file) (keymap-set my/project-sparse-map "b" #'switch-buffer) ;; 绑定二级 (keymap-set my/git-sub-map "s" #'vc-dir) (keymap-set my/git-sub-map "l" #'vc-print-log) ;; 将二级map绑定到一级map (keymap-set my/project-sparse-map "g" my/git-sub-map) ;; 绑定一级菜单 (global-set-key (kbd "C-c p") my/project-sparse-map) (message "my/project-sparse-map 内部结构: %S" my/project-sparse-map)小结本节我们简单了解了一下前缀和嵌套keymap的内容,一般来讲,创建一个keymap可以使用下面两个函数make-sparse-keymapdefine-prefix-command
2026年08月25日
11 阅读
0 评论
0 点赞
2026-08-24
《elisp 每日一练》——全局与局域 Keymap 按键映射
在上一篇,我们了解了如何通过 interactive 构建交互式函数。Emacs中可以说所有用户的操作都是靠这类函数完成,就连基本的插入删除,我们虽然是通过键鼠在UI上操作,但是它的底层仍然是依赖这类交互式函数。我们不可能每个操作都要依靠M-x 执行交互式函数,因此Emacs提供了快捷键绑定的方式,提升效率背景知识介绍在 Emacs 中,按键并不是直接映射给函数的,而是通过 Keymap(按键映射表) 进行中转。keymap本质上是一个将“事件类型”映射到“命令”或其他keymap的查找表。下面我们来探讨一下,从按下快捷键到最终执行对应的交互式函数,这中间发生了什么。首先我们按下快捷键,Emacs 通过 read-key-sequence 拿到了这个快捷键。然后它会按照以下顺序查找按键:overriding-terminal-local-mapoverriding-local-map文本属性键映射 : 如果按键发生在特定文本上,会查找该文本的keymap或local-map属性emulation-mode-map-alists: 用于仿真其他编辑器的键映射minor-mode-overriding-map-alist: 用于覆盖 minor mode 键映射minor-mode-map-alist : 当前激活的 minor mode 键映射当前Buffer的本地键映射: 由当前 Major Mode 定义全局键映射 : 全局生效的键映射如果找到的是一个keymap对象,也就是说这个键其实绑定了另一个keymap对象而不是一个函数,那么此时它会被视为一个前缀。如果后面跟了一个输入,则按照输入从当前前缀绑定的keymap中继续往下找,直到遍历完整个链。如果没有输入它会停在那里等待用户的进一步输入。我们建立快捷键映射实际就是在往这些表中插入一条记录。Emacs提供了通用的函数 define-key 和 keymap-set 来操作map记录。(define-key KEYMAP KEY DEF &optional REMOVE)keymap 对应着上面的一个keymapkey 为快捷键def 对应着绑定的函数上述几类keymap,我们在使用 define-key 插入时通常有固定的模式。直接使用变量名的这类主要是 global-map ,例如 (define-key global-map (kbd "C-c a") 'my-global-command)对应具体 Mode的每个Mode 都有一个类似 mode-map 的变量,例如 python-mode 中有一个 python-mode-map、elisp 中有一个 elisp-mode-map,这种情况,我们可以使用 (define-key elisp-mode-map (kbd "C-c e") 'my-elisp-mode-command)但是对于minor mode 来说在绑定前要确保对应的 map 存在, 可以通过查找当前名称是否在 minor-mode-map-alist 当中来判断。并且需要保证 minor mode 处于激活状态需要通过 (current-local-map) 获取的对于当前buffer的本地映射来说,我们需要使用 current-local-map 来获取当前buffer中的快捷键绑定map,后续通过这个map来进行绑定 (define-key (current-local-map) (kbd "C-c c") 'my-current-command)需要先创建再激活的对于 overriding-local-map 和 overriding-terminal-local-map 来说,它们默认的值是 nil 我们必须将它指向具体的keymap对象,然后进行操作.;; 1. 创建一个临时的 keymap (setq my-temp-map (make-sparse-keymap)) ;; 2. 向这个临时 map 里添加绑定 (define-key my-temp-map (kbd "C-c e") 'my-emergency-command) ;; 3. 激活它(注意:这会瞬间劫持所有键盘输入!) (setq overriding-local-map my-temp-map) ;; 如果要取消劫持: (setq overriding-local-map nil)无法使用 define-key 操作对于 文本属性键映射 来说,它本质上是一个文本属性,并没有对应具体的keymap 对象,这种情况下,可以通过之前介绍过的 put-text-property 设置一个keymap对象;; 让缓冲区第 1 到第 10 个字符拥有独立的按键映射 (let ((my-text-map (make-sparse-keymap))) (define-key my-text-map (kbd "RET") 'my-special-enter) (put-text-property 1 10 'keymap my-text-map))define-key 是一个使用C编写的非常底层的函数了,从上面可以看到,每种类型的keymap 在传入它的keymap 参数时都有不同的处理手段。为了简便Emacs还提供了一些函数:global-set-key 相当于keymap参数传入了 global-maplocal-set-key 相当于keymap参数传入了 current-local-map而且从 Emacs(29) 开始引入了更为简便的新的绑定快捷键的函数。keymap-set 用于取代 define-key、keymap-global-set 用于取代 globak-set-key、keymap-local-set 用于取代 local-set-key它们与原来的函数相比,直观上看最大的区别就是不再需要使用 kdb 宏包裹,而是支持直接使用字符串来表示快捷键。例如可以写类似 (keymap-set (current-local-map) "C-c c" 'my-current-command)。Emacs底层并不接受这种用字符串包装的快捷键,它接受一个特殊结构的向量,而kbd就是一个将字符串转成对应这个结构的转换器。对于已经设置上的快捷键我们可以使用 keymap-unset 或者 define-key 将对应的 def 参数设置为 nil我们可以查看函数绑定的快捷键以及快捷键绑定的函数。我们可以使用 describe-key 查看某个快捷键绑定到了哪个函数上,也可以使用 describe-function 看到对应函数绑定的快捷键如果我们希望通过elisp编程的方式实现上述内容,我们可以使用如下的一些函数:(key-binding KEY) 获取当前所有活动键映射中,某个按键序列 KEY 所绑定的命令(lookup-key KEYMAP KEY) 在指定的 keymap 中查找key 的绑定(where-is-internal COMMAND KEYMAP) 查找对应命令在 keymap 中的绑定练习前面我们实现了很多常见的小功能,如果读者觉得某些功能有用,可以考虑利用今天所学的绑定快捷键的方式来为那些函数绑定一个快捷键以提升效率。本节的练习我不打算进行添加一个快捷键绑定就完事了。本节我们利用这些查找函数完成两个小功能,查找当前快捷键绑定的函数以及查找函数所绑定的快捷键。(defun my/inspect-key-to-function(key) (interactive "skeymap:") (let* ((key-seq (kbd key)) ;;这里需要进行一个字符串到Emacs认可格式的转化 (cmd (key-binding key-seq))) (if cmd (message "快捷键[%s]绑定到 --> [%s]")) (message "未找到当前快捷键绑定的函数"))) (defun my/inspect-function-to-key(command) (interactive (list (read-command "Input Command:"))) (let ((keybinds (where-is-internal command))) (if keybinds (let ((read-keys (mapcar #'key-description keybinds))) (message "find binds: %s" (mapconcat (lambda (k) (format "[%s]" k)) read-keys " "))))))上述代码都是很基础的函数调用。可能有多个快捷键绑定到同一个函数上,所以 where-is-internal 实际返回的是一个列表。我们通过 mapcar 对列表中每一个成员进行迭代,并通过 key-description 获取对应的快捷键。最终我们利用 mapconcat 将前一步返回的 read-keys 做一个迭代,拼接成一个字符串进行显示总结本篇我们介绍了快捷键绑定的基础内容,并介绍了Emacs中的多层keymap,以及操作这些keymap的一些方法。下面是今天介绍的api函数:define-keykeymap-setglobal-set-keylocal-set-keykeymap-global-setkeymap-local-setkeymap-unsetkey-bindingwhere-is-internal
2026年08月24日
9 阅读
0 评论
0 点赞
2026-08-23
《elisp 每日一练》——interactive 交互式命令
在前面我们写的所有能在命令行执行的函数,第一个语句都是 (interactive)。 过去我们仅仅机械性的加上这么一个语句,似乎没有对它进行深入的了解。本节我们将探讨有关 interactive 的内容背景知识在Emacs中,普通函数仅仅能被其他elisp代码调用,而交互式函数既能够被用户通过 M-x 的方式调用,也可以被其他lisp 代码调用。我们如果要判断一个函数是否是交互式命令可以使用 commandp。想要一个函数成为交互式函数, 可以在函数中包含 interactive 的声明。interactive 的定义如下:(interactive &optional ARG-DESCRIPTOR &rest MODES)其中 ARG-DESCRIPTOR 参数定义了当用户交互式地调用该命令时,如何获取其参数。它主要有三种形式:nil 或者省略时表示该函数不需要参数使用字符串:交互式读取参数使用 Lisp 表达式:完全自定义第二种方式可能使用的频率最高。它包含了一组有特殊字符组成的代码字符和对应的提示信息。Emacs 会根据代码字符的类型,自动读取用户输入,进行类型的转化并传递给命令。常见的一些代码字符有:r: 自动提取当前 Region 的起点与终点 Point(无需用户手动输入!)s: 读取用户输入的字符串n: 读取用户输入的数字f: 交互式选择已有文件F: 选择一个文件,文件可以不存在p: 自动获取前缀参数(例如按下 C-u 或 M-4)B: 一个已经存在的buffer的名称b: buffer的名称,buffer可以不存在其他的参数还有很多,读者可以自行查阅对应的文档信息。我们可以在每一个代码字符后面加上一个提示信息的字符串,多个代码字符的话使用\n作为分隔符,下面我们看一个具体的例子我们可以利用上述内容实现一个简单的打开文件的功能,如果文件不存在则创建新文件。并且允许用户提供一个需要写入的字符串,如果我们带上对应的提示信息,就可以有下面一个简单的实现(defun my/open-file(file input-str) (interactive "F输入文件名称:\ns输入文件中的字符串:") (let ((buffer (find-file file))) (goto-char (point-max)) (newline) (insert input-str)))因为我们需要接受用户输入的文件名和字符串,所以函数需要两个参数。而文件可以不存在所以我们采用 F 作为代码字符,后面接上提示信息,然后利用 \n 分隔符写上另一个参数的信息。如果你觉得以上这些代码字符不能满足你的需求,我们可以采用第三种方式,也就是使用list来自定义表达式。list 中可以包含多个表达式,它会依次对表达式进行求值并将求值的结果按函数参数定义的顺序进行赋值。如果我们将上面的函数使用列表的方式重新组织可以得到如下的代码(defun my/open-file(file input-str) (interactive (list (read-file-name "输入文件名称:") (read-string "输入字符串:"))) (let ((buffer (find-file file))) (goto-char (point-max)) (newline) (insert input-str)))练习我们来实现一个C/C++ 语言版本的快速注释函数。可以有一下几种情况:如果用户激活了一个 Region,则采用 /**/ 注释区间的代码如果用户通过 C-u 输入了数字,那么就通过 // 注释当前行以及后续的n行如果用户未进行上述操作,则通过 // 注释当前行(defun my/comment-region (start end) "激活区间的情况下,通过 /**/注释区间的代码" (let ((text (buffer-substring-no-properties start end))) (delete-region start end) (insert (format "/* %s */" text)))) (defun my/comment-lines (arg-lines) "当前行开始,逐行添加 // 注释,总共添加 arg-lines 行" (dotimes (_ (or arg-lines 1)) (save-excursion (beginning-of-line) (insert "// ")) (forward-line 1))) (defun my/quick-comment(start end arg-lines) "C/C++语言版本的快速注释函数" (interactive (if (use-region-p) (list (region-beginning) (1+ (region-end)) nil) (list nil nil (prefix-numeric-value current-prefix-arg)))) (cond ;;优先处理选区的情况 ((and start end) (my/comment-region start end)) (t (my/comment-lines arg-lines))))为了实现上述功能,我们提供三个函数,其中两个工具函数,一个用来做交互的主函数。我们一个个来分析:my/comment-region,在激活 Region 的情况下调用该函数。它有两个参数分别是当前 region 的起始位置和结束位置。它主要是提取region 区域内的文本,并使用 /**/ 将文本包括,然后先delete区域内的文本然后在区域开始位置插入新文本实现注释。这里其实也可以在开始和结束位置执行两次insert 函数分别插入 /* 和 */my/comment-lines,该函数用来注释多行。它接受一个代表行数的整数参数。我们通过 dotimes 宏进行一个循环。因为它不需要迭代参数,所以迭代结果那块使用 _ 进行忽略。而迭代次数我这里使用了 or。or 后面可以跟任意个表达式,它会依次往后求值,直到得到一个不为空的值。所以这里表示如果arg-lines 的值不为空则采用它的值,否则我只在当前行执行该操作。插入注释时我们先通过 beginning-of-line 将光标移动到行首位置,然后在行首插入 //,接着移动到下一行准备进行插入操作最后我们来看看主函数 my/quick-comment 的实现。在 interactive 参数部分,我们考虑了两种情况,如果当前region激活了,则采用region的范围给 start 和 end 进行赋值。但是因为 region 的范围是一个前开后闭的区间,所以想要包含区间的后一个字符,需要在此基础上加1如果当前没有区间,我们通过 prefix-numeric-value 来获取输入的前置数字。然后通过list依次将每个表达式的求值结果赋值到函数参数。最后我们进行判断,如果 start 和 end 都不是空,则表明用户激活了一个选区,此时就需要调用 my/comment-region 对选区进行处理。否则调用 my/comment-lines arg-lines 进行行注释总结本篇我们了解了 interactive 的交互机制,并详细介绍了它接受的三种参数模式:nil 或者空参、提示的字符串以及自定义的list形式。
2026年08月23日
5 阅读
0 评论
0 点赞
2026-08-22
《elisp 每日一练》—— 综合练习
到此为止我们已经对处理文本有了一个新的认识。回顾之前的内容,我们主要介绍了针对文本的普通特性,通过正则表达式进行搜索和捕获,以及关于属性的内容。针对编程语言的类型的文本,我们可以通过syntax-table 和 tree-sitter相关机制进行处理。知识回顾针对正则表达式的晦涩以及emacs中特有的连续 \的处理,我们可以使用 rx 宏处理在有关文本属性方面,我们介绍了如下一些函数add-text-propertiesput-text-propertyremove-text-propertiesset-text-propertiesadd-face-text-propertyget-text-property后面我们介绍了emacs 中的撤销与反撤销,以及如何进行文本变动方面的现场保护和还原,有以下几个函数:undoundo-redoundo-boundaryatomic-change-group如果我们将文本视为一个编程语言的源码或者配置文件这种有一定语法规则的文本,emacs 为我们提供了基本的词法分析的 Syntax-Table, 操作的相关函数有:forward-sexp 和 backward-sexpscan-sexpssyntax-ppss如果我们想从语法层面区分每个部分,可以通过新引入的 treesitter机制。有关的函数比较多,主要有:treesit-available-ptreesit-language-available-ptreesit-parser-createtreesit-parser-listtreesit-parser-buffer`treesit-parser-languagetreesit-buffer-root-nodetreesit-node-typetreesit-node-texttreesit-node-start 和 treesit-node-endtreesit-node-childtreesit-node-parenttreesit-node-prev-sibling 和 treesit-node-next-siblingtreesit-node-checktreesit-node-ptreesit-node-eqtreesit-query-capturetreesit-query-stringtreesit-query-validate综合练习本次的练习,我们结合前面的内容,来完成一个简单的显示当前buffer函数定义的面板。它的主要功能有:搜集当前buffer中的所有elisp函数定义将这些函数定义集中到新的面板中进行显示在新的面板中,我们将光标放到对应的函数上可以回到函数定义的位置我们将上一节的代码拿过来做一个修改(defun my/get-all-functions-define () "遍历当前buffer中的所有函数定义,并将它们一一记录到对应面板中" (interactive) (unless (cl-some (lambda (p) (eq (treesit-parser-language p) 'elisp)) (treesit-parser-list)) (treesit-parser-create 'elisp)) (let* ((root (treesit-buffer-root-node)) ;; 捕获 defun 的根节点 @def_node 以获取整个函数的起始位置 (captures (treesit-query-capture root "(function_definition name: (symbol) @func_name) @def_node"))) (dolist (capture captures) ;; captures 包含 @func_name 和 @def_node,只需处理 @func_name 匹配项 (when (eq (car capture) 'func_name) (let* ((node (cdr capture)) (name (treesit-node-text node)) ;; 拿到该函数所在 defun 节点的起点位置 (def-node (treesit-node-parent node)) (start (treesit-node-start def-node)) (buffer (current-buffer))) (my/add-function-to-function-panel name start buffer))))))这部分的代码,我们大部分都延续上一节的内容,不过在存储数据时我们调用一个额外的函数将数据存储到一个新的buffer中。存储buffer的部分,我们主要是通过创建新的buffer,并且将这些数据以属性的方式加入。它的实现如下:(defun my/add-function-to-function-panel (name start buffer) "将函数信息加入新的面板中" (let ((panel-buffer (get-buffer-create "*function-panel*"))) (with-current-buffer panel-buffer (let ((inhibit-read-only t)) (goto-char (point-max)) ;; 1. 记录插入前的位置 (let ((start-point (point))) (insert name "\n") ;; 2. 使用反引号 `(...) 允许 ,buffer 和 ,start 动态求值 (add-text-properties start-point (1- (point)) `(source-buf ,buffer function-point ,start)))))))有了这些信息,在我们打开面板之后,执行跳转的函数,我们读取当前位置的属性,根据事先存储的属性信息,找到原始的buffer和对应的point值,根据这些值执行跳转(defun my/jump-to-source-buffer () "跳转回到源码位置" (interactive) (let ((source-buffer (get-text-property (point) 'source-buf)) (func-point (get-text-property (point) 'function-point))) (when (and source-buffer func-point) (pop-to-buffer source-buffer) ;; 修正变量引用与位置跳转 (goto-char func-point))))总结到此我们关于文本部分的学习就都结束了。上述的练习只实现了基础功能,很多地方考虑的不完善,例如多次执行有重复信息加入。没有考虑后续源码buffer被清理掉的问题。这些问题读者如果有兴趣可以自行完善。
2026年08月22日
8 阅读
0 评论
0 点赞
2026-08-20
《elisp 每日一练》——tree-sitter
前面一篇文章中,我们提到 Syntax-Table。它相当于一张巨大的表,通过字符串索引到它的属性信息。但是它仅仅停留在词法层面,例如下面的一段c代码int main(){return 0;}它仅仅能识别它们是 单词 单词 () {单词 数字 分号} 也就是给每个语法单元大了一个 Token,进行了简单的词法分析。但是它没有办法将这段代码解析成有意义的语法单元。导致我们没办法利用它进行高级的语法单元的解构。要实现类似IDE那种跳转或者语法高亮,需要借助第三方库,或者自己根据词法单元写一套语法分析器。但是从Emacs 29开始,引入了 tree-sitter,针对所有编程语言提供了统一的接口,让我们能基于语法形成的AST,做一些高级的功能背景知识Emacs中使用 tree-sitter 围绕着 解析器 、语法节点 和 查询 展开的。这么说有些抽象,我们换一种说法就是调用函数解析当前buffer中的内容形成抽象语法树,遍历树节点获得每个节点中的属性,例如它是一个function或者是一个变量等等。最后对于一些提取数据的功能我们不需要遍历整个树,而是可以采用类似SQL的查询语句来找到想要的内容。解析器在执行语法分析前,我们需要检查环境以及定义使用的解析器。这方面的函数有:treesit-available-p : 检查当前Emacs是否支持 tree-sittertreesit-language-available-p : 检查特定语言的语法定义是否已安装treesit-parser-create : 为当前缓冲区创建一个新的解析器treesit-parser-list : 获取当前缓冲区中的解析器列表treesit-parser-buffer : 获取解析器关联的缓冲区treesit-parser-language : 获取解析器所使用的语言例如我们使用 (treesit-language-available-p 'elisp) 来判断当前elisp的解析器是否安装,对于一个裸Emacs来说,它会返回nil。我们可以手动的通过 treesit-install-language-grammar 命令安装对应的语法解释器。或者我希望我的elisp配置帮我安装我需要的解析器这样即使我换了一台新的电脑,只要执行这些配置它也能帮我准备对应的环境。我们可以先往列表 treesit-language-source-alist 中添加成员。它的每一项都是一个 (LANG . (URL REVISION SOURCE-DIR CC C++)) 结构,默认只需要填写LANG 和 URL,它们分别是对应的语言和解析器的github地址。后面几个参数: REVISION 表示要下载哪个版本,SOURCE-DIR 下载的源码在哪个目录、C 和 C++ 表示编译时使用的C/C++编译器。针对elisp我们可以这样指定(add-to-list 'treesit-language-source-alist)之后我们可以通过下面的代码来一次安装 treesit-language-source-alist 中定义的所有的语言解析器(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))如果你使用的操作系统是Windows,因为Windows没有原生的gcc,我们可以使用MinGW-w64。如果你不想折腾这些,可以直接从GitHub上找到一些预编译项目例如 casouri/tree-sitter-module ,下载已经编译好的Windows版本,将它们拷贝到 ~/.emacs.d/tree-sitter/ 目录中。因为tree-sitter 解析器的名称一般为 libtree-sitter-<LANGUAGE>.so 所以在Windows上,如果我们要启动elisp的,需要将它改名为 libtree-sitter-elisp.dll。 如果你不想改名,我们可以设置往列表 treesit-load-name-override-list 中添加一条对应的语言和dll的映射。每条记录的格式为 (LANG LIB-NAME [BASE-NAME])。其中 LANG 是语言名称,而 LIB-NAME 是对应的dll名称,BASE_NAME 是库道出的函数名称。默认来讲,每个对应的dll都有一个可以调用的导出函数,命名规则为 tree_sitter_<LANGUAGE> 但是极少数可能出现名称不匹配的规则,所以我们可以添加一条记录。(add-to-list 'treesit-load-name-override-list '(elisp "elisp" "tree_sitter_elisp"))之后我们就可以为当前的elisp缓冲区创建一个新的解析器 (treesit-parser-create 'elisp)语法节点创建了语法解析器之后,我们就可以利用一些API函数来获取树上节点的属性了。例如可以使用 treesit-node-at 可以获取当前光标位置在树上的节点。有了节点,我们可以获取节点属性或者进行节点的遍历,对应的函数有:treesit-buffer-root-node 获取当前树结构的根节点treesit-node-type 获取节点类型,例如function_definition或者 if_statement 等等treesit-node-text 获取节点对应的源码文本treesit-node-start 和 treesit-node-end 获取节点在缓冲区的起始或者结束位置treesit-node-child 获取节点的第N个子节点treesit-node-parent 获取节点的父节点treesit-node-prev-sibling 和 treesit-node-next-sibling 获取节点的前一个或者后一个兄弟节点treesit-node-check 检查节点的属性treesit-node-p 检查对象是否是一个treesitter 节点treesit-node-eq 比较两个节点是否代表树中的同一个节点有了这些函数,我们简单实现的实现一个高亮当前光标所在语法单元的功能:(defun my/mark-current-node() (interactive) (let* ((parser (treesit-parser-create 'elisp)) ;;创建解析器 (node (treesit-node-at (point))) ;; 获取当前位置所在的节点 (start (treesit-node-start node)) ;;获取节点对应文本的起始位置 (end (treesit-node-end node))) ;;获取结尾位置 ;; 在起点和节点间高亮一个 region (goto-char start) (push-mark end t t)))查询如果我们想要快速的扫描,提取出类似于类定义、类中的属性、方法等内容,依靠遍历整个树还是显得太繁琐了,对于这种查询的需求,我们可以使用 treesitter 提供的查询功能。它提供了基于树和节点的查询功能:treesit-search-forward: 从指定节点开始,向前(或向后) 搜索第一个匹配特定条件的节点treesit-search-subtree : 在节点的子树中搜索匹配条件的后代节点treesit-parent-until: 从当前节点向上遍历,直到找到满足条件的祖先节点treesit-induce-sparse-tree: 根据条件在语法树上构建一棵稀疏树,用于快速概览代码结构虽然上述函数提供了一些搜索节点的方法,但是仍然不够直观。就像数据库,虽然MySQL、或者 Oracle 底层采用的树的结构来提高查找速度,但是在实际操作数据库查找数据时没有人用树的操作来查找。treesitter 在处理诸如查找这一类需求时也提供了对应的高级查找接口:treesit-query-capture: 利用特定的语法,查找符合条件的接口, 可以接收list列表或者字符串treesit-query-string 直接对字符串进行解析和查询treesit-query-validate 验证查询语法是否正确想要使用 tree-query-capture ,需要先了解一下查询的语法。一个查询由一个或者多个模式组成,每个模式都是一个S-表达式,其结构类似于 (type (child) (child) ...)type 表示要查找的节点类型如 function_definition 或者 if_statementchild 嵌套的子节点模式,用于进一步限定父节点的结构例如我们要捕获函数定义所在节点可以这么写(tree-query-capture node ;;查询开始的节点 '((function_definition (function_name) @func-name)))上述代码表示,我们捕获函数定义的节点,并将它的节点名称命名为 func-name你可能觉得上述内容比较抽象,看不懂,没关系。实际上它就是treesitter结构中的一个变种,我们了解一段代码在treesitter眼中的样子,也就知道如何编写对应查询条件。在早期的学习使用过程中,我们可以使用命令, treesit-explore-mode,它会将我们当前buffer中在treesitter中解析的内容给列出来,我们只需要点击其中的某项,它就会将对应的代码进行高亮显示。这样我们就可以很容易找到某段代码在treesitter眼中的实际模样。或者开启 treesit-inspect-mode 它会显示当前光标所在位置的节点信息,帮助我们在一堆treesitter 信息中快速找到对应的位置例如上述高亮当前光标所在语法单元的示例在treesiter眼中,它是这样的(function_definition ( defun name: (symbol) parameters: (list ( )) (special_form ( interactive )) (special_form ( let* (list ( (list ( (symbol) (list ( (symbol) (quote ' (symbol)) )) )) (comment) (list ( (symbol) (list ( (symbol) (list ( (symbol) )) )) )) (comment) (list ( (symbol) (list ( (symbol) (symbol) )) )) (comment) (list ( (symbol) (list ( (symbol) (symbol) )) )) ))所以根据上述内容,我们如果想获取提取定义的函数名,就可以找 function_definition 类型的节点。根据节点,如果我们想获取对应的函数定义,可以这么写查询条件(treesit-query-capture root "(function_definition name: (symbol) @func_name)")这段代码查询之后的返回值是 (func_name. node) 结构的列表。因为我们给定的名称就是 func_name。后面还有一些高级的匹配模式和用法,这里就不介绍了。读者可以自行查阅 treesit-query-capture root 的文档。练习本节我们的练习基于上述内容,我们实现一个获取当前buffer中所有函数定义的功能。我们尝试一下多使用一下相关的函数,以便加深印象。所以本次的实现思路是:函数开始之前先调用 treesit-parser-list 遍历一下当前buffer中加载的解析器然后调用 treesit-parser-language 获取当前解析器对应的语言如果所有的语言都没有elisp,那么就创建elisp语言的解析器接着通过 treesit-buffer-root-node 获取根节点然后通过 treesit-query-captur 从根节点往下找,找到所有函数的定义我们遍历返回的结果,取每项的第二个成员,它就是对应的节点对象调用 treesit-node-text 获取节点中文本,也就是函数的定义最后打印函数定义,或者存储到一个列表中最终实现如下:(defun my/get-all-function-define() (interactive) ;; 先判断当前是否创建了 elisp 的解析器 (unless (cl-some (lambda(p) (eq (treesit-parser-language p) 'elisp)) (treesit-parser-list)) (treesit-parser-create 'elisp)) (let* ((root (treesit-buffer-root-node)) (captures (treesit-query-capture root "(function_definition name: (symbol) @func_name)")) (func-names '())) (if (not captures) (message "未找到对应的内容") (progn (dolist (capture captures) (let* ((node (cdr capture)) (name (treesit-node-text node))) (message "找到函数定义:%s" name) (push name func-names)))))))总结本篇我们介绍了treesit 在Emacs中的一些用法,包括与解析器相关的操作、遍历节点、以及在此之上的简单的查询功能。查询还有很多高级的功能,各位读者如果感兴趣可以阅读一下对应的文档。本篇介绍的函数如下:treesit-available-p : 检查当前Emacs是否支持 tree-sittertreesit-language-available-p : 检查特定语言的语法定义是否已安装treesit-parser-create : 为当前缓冲区创建一个新的解析器treesit-parser-list : 获取当前缓冲区中的解析器列表treesit-parser-buffer : 获取解析器关联的缓冲区treesit-parser-language : 获取解析器所使用的语言treesit-buffer-root-node 获取当前树结构的根节点treesit-node-type 获取节点类型,例如function_definition或者 if_statement 等等treesit-node-text 获取节点对应的源码文本treesit-node-start 和 treesit-node-end 获取节点在缓冲区的起始或者结束位置treesit-node-child 获取节点的第N个子节点treesit-node-parent 获取节点的父节点treesit-node-prev-sibling 和 treesit-node-next-sibling 获取节点的前一个或者后一个兄弟节点treesit-node-check 检查节点的属性treesit-node-p 检查对象是否是一个treesitter 节点treesit-node-eq 比较两个节点是否代表树中的同一个节点treesit-query-capture: 利用特定的语法,查找符合条件的接口, 可以接收list列表或者字符串treesit-query-string 直接对字符串进行解析和查询treesit-query-validate 验证查询语法是否正确
2026年08月20日
12 阅读
0 评论
0 点赞
2026-08-18
《elisp 每日一练》——文本属性
在前面的例子中,我们已经见识过文本属性。我们通过将buffer、file-name、line-number 等信息作为属性附加到字符串上,在用户选择了这个字符串执行跳转时又读取它的属性,找到之前保存的信息,从而完成了跳转。背景知识从上面的例子中我们知道,属性实际上是一个标签,也可以是一个信息存储的媒介。既可以存储官方支持的属性,也可以存储自定义的属性。我们可以通过 add-text-properties 来添加一个属性。它的定义如下:(add-text-properties START END PROPERTIES &optional OBJECT)各个参数的含义如下:START和 END :表示一个范围,给这个范围内的字符串添加属性PROPERTIES: 是一个表示各种属性的列表OBJECT: 是一个buffer,表示往哪个buffer中的字符串添加属性,默认不填的话表示当前bufferPROPERTIES 是一个属性列表,它的形式为 (prop1 value1 prop2 value2 ...) 。在Emacs中,可以通过设置buffer中字符串的相关属性实现例如文本高亮、交互效果等操作。这些都可以通过设置官方提供的属性来支持。官方提供的常用效果有:文本显示效果的有:属性名作用常用值示例face控制文本外观(颜色、粗体、斜体等)'bold、'font-lock-keyword-face、'(:foreground "red" :weight bold)font-lock-face专门给 Font Lock 使用的 face(推荐用于语法高亮)同 facemouse-face鼠标悬停时显示的 face'highlightcursor-face光标位于该文本时显示的 face(需开启 cursor-face-highlight-mode)'highlight交互与提示的属性有:属性名作用常用值示例help-echo鼠标悬停时在 echo area 或 tooltip 显示提示"""点击查看详情"" 或函数"keymap为这段文本添加局部按键映射(优先级高于 buffer 本地 map)一个 keymap 对象local-map替换 buffer 本地 map(较少用,一般优先用 keymap)一个 keymap 对象显示控制的属性属性名作用常用值示例display改变文本显示方式(替换为图片、字符串、调整高度/宽度等)"'(image :type png :file ""icon.png"") 或 ""替换文本"""invisible使文本不可见t 或某个符号(配合 buffer-invisibility-spec)composition字符组合显示(用于复杂脚本或特殊效果)较少直接使用编译与移动控制的属性属性名作用常用值示例read-only禁止修改该字符t 或错误提示字符串inhibit-read-only即使在只读 buffer 中也允许编辑tcursor-intangible禁止光标停留(配合 cursor-intangible-mode)tfield定义“字段”某些移动命令(如 forward-word)会停在字段边界任意符号或值其他常用属性属性名作用category属性分类符号,该符号上的属性会作为默认值继承syntax-table覆盖该字符的语法表属性fontified标记文本是否已完成字体化(Font Lock 内部使用)inhibit-isearch让 isearch 跳过这段文本cursor控制光标在 overlay / display 字符串中的显示位置add-text-properties 可以一次性设置多个属性,而且前面设置的属性,后面可以通过该函数修改。如果调用该函数发生了属性修改,则函数返回 t 否则返回 nil除了该函数,关于属性设置的函数还有:put-text-property 只设置一个属性remove-text-properties 删除指定属性set-text-properties 完全替换整段文本的所有属性,如果为 nil 则表示清空所有属性add-face-text-property 专门用来添加 face 属性另外我们可以通过 get-text-property 来获取属性值。练习不知道各位读者是否还记得,关于buffer的综合练习中的 App-Log 的内容。今天的练习我尝试对log函数进行一些改进,根据log等级来用不同的颜色显示,我这里规定:红色表示error、黄色表示warning、绿色表示debug、灰色表示info。(defun my/log(level message) (let* ((level-sym (if (stringp level) (intern (upcase level)) level)) (src-buf (current-buffer)) (file-or-buf (or (buffer-file-name) (buffer-name))) (line-num (line-number-at-pos)) (log-buf (get-buffer-create "*App-Log*")) ;; 定义不同等级日志的字体颜色 (level-face (pcase level-sym ('ERROR '(:foreground "red" :weight bold)) ('WARNING '(:foreground "orange" :weight bold)) ('DEBUG '(:foreground "green")) ('INFO '(:foreground "gray50"))))) (with-current-buffer log-buf (let ((inhibit-read-only t)) (goto-char (point-max)) (let ((start-pos (point))) ;; 格式 [LEVEL][buffer-name][line-number]:message (insert (format "[%s][%s][%d]: %s\n" level-sym file-or-buf line-num message)) ;;为了便于后续跳转回来,这里我们保存一些隐藏信息 (add-text-properties start-pos (point) `(log-file ,file-or-buf face ,level-face log-line ,line-num log-buf ,src-buf)))))))这里使用了一个 pcase 宏用来作为条件判断,与 cond 相比它更为灵活,写出来的代码更加简洁。我们通过 pcase 判断当前等级,根据等级进行匹配,最终找到合适的颜色。在设置属性的时候我们将属性添加进来,同样的需要使用 , 对属性值的变量进行求值并替换。总结本篇我们了解了属性的相关概念和应用,下面是今天提到的一些api函数add-text-propertiesput-text-propertyremove-text-propertiesset-text-propertiesadd-face-text-propertyget-text-property
2026年08月18日
10 阅读
0 评论
0 点赞
2026-08-17
《elisp 每日一练》正则搜索与 rx 宏
看到这个标题我想读者可能有这样的疑问,前面已经介绍了正则表达式了,这次又来,不会是冷饭重炒没什么新东西了吧。实际上Emacs的正则表达式算是比较高级,操作相对复杂的部分了,一篇简单的函数介绍还不足以覆盖。所以在关于文本操作相关的主题中值得专门拿出一篇介绍一些新鲜内容。各位读者请放心往下看,绝不浪费时间。背景知识铺垫在前面我们介绍过,想要使用Emacs中的正则表达式,有下面一个函数:re-search-forward 和 re-search-backward 用于搜索match-string 用于提取搜索到的文本replace-match 用于替换但是Emacs 中使用正则表达式有一个比较坑的点是转义字符必须使用 \\ 来表示一个 \ , 这个细节如果不熟悉Emacs正则表达式的读者可能经常发生明明写了 \\([0-9]+\\) 进行查找结果就是什么都没有,你只能写下 \\\\([0-9]+\\\\) 。 这也太反人类了。好在Emacs解决了这个问题,它提供了 rx 宏。rx宏使我们可以用S 表达式来组织正则表达式。本节我们的重点是介绍rx宏,以便摆脱正则表达式的纠缠。它可以使我们像写elisp代码一样来描述我们的匹配需求。例如看到下面这个Emacs中的正则表达式:"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]\\{2,\\}"我们看到它前面匹配任意字符,中间加一个@符号,后面又是任意字符表示域名,而最后一个 . 后面接至少两个由A到Z组成的后缀。而使用 rx 表达式则可以写成:;; 使用 rx:可读性极大提升 (rx (one-or-more (any "a-z" "A-Z" "0-9" "._%+-")) ;; 第一部分这些任意字符,至少一个 "@" ;; 第二部分是一个@固定字符 (one-or-more (any "a-z" "A-Z" "0-9" ".-")) ;; 第三个部分又是这些字符里面任意字符, 至少一个 "." ;; 第四部分是一个 . (repeat 2 nil (any "a-z" "A-Z"))) ;; 最后一个部分是英文字母但是至少两个我们看到rx 表达式表示的是否更清楚呢。如果你的答案是肯定的,那么本篇的内容值得你继续往下阅读。rx的定义如下(rx REGEXPS...)rx 接收一个或者多个 rx 形式的s表达式,这些形式最终会被翻译成普通的正则表达式字符串,下面按类别介绍最常用的构造。字面量字面量主要有字符串和字符,它们没有特殊含义就是表示字符本身。例如(rx "hello") ; → "hello" (rx ?a) ; → "a" (rx "a.b") ; → "a\\.b"(点被自动转义)序列与选择我们可以通过 (seq FORM...) 或 (sequence FORM...)、(: FORM...)、(and FORM...):按顺序匹配。也可以通过 (or FORM...) 或者 (| FORM...) 表示匹配其中的任意一个。就像上面的例子那样,通过多个基础的rx 单元进行组合,拼接成一个完整的匹配项。又或者如下面的例子:(rx (seq "http" (opt "s") "://")) ; → "https?://" (rx (or "cat" "dog" "bird")) ; → "cat\\|dog\\|bird"重复有下列方式表示重复:zero-or-more 表示0次或者多次one-or-more 表示至少出现一次zero-or-one 表示至多一次,或者说出现0次或1次repeat N 或者 = N 恰好出现N次>= N 至少N次** N M N次到M次rx 中重复默认都采用贪婪模式,如果想要使用非贪婪模式,可以加上 ?字符匹配如果我们想要表示匹配对应集合中任意一个字符,那么可以提供集合,并且通过 (any SET...) / (in SET...) / (char SET...) 表示匹配集合中的任意一个字符。使用 (not CHARSPEC) 匹配不在集合中的字符集合我们可以采用正则表达式中的表现方式例如: a-z 表示小写英文字母,0-9 表示数字。在rx中也可以使用预定义的集合:digit、numeric、num 表示数字alpha、alphabetic、 letter 表示字母alnum 、alphanumeric 表示数字+大写字母+小写字母的序列xdigit 、hex-digit 表示16进制数blank、space / whitespace 表示空白字符word 、 wordchar 表示单词位置匹配如果我们想在上述匹配基础上加上位置,可以考虑使用下面列举的一些内容:line-start、bol:行首(^)line-end、 eol:行尾($)string-start、 bos、 buffer-start:字符串/缓冲区开头(\`)string-end、 eos、 buffer-end:结尾(\')word-start、bow:单词开头(\<)word-end、eow:单词结尾(\>)word-boundary:单词边界(\b)not-word-boundary:非单词边界(\B)symbol-start、symbol-end:符号边界point:当前 point 位置(\=)捕获组对于捕获组,有如下方式:(group FORM...) 、 (submatch FORM...) : 对应着正则表达式中的捕获组 \(...\)(group-n N FORM...) :命名编号的捕获组(backref N):反向引用第 N 个组对于 rx 更加详细的内容,读者可以通过 describe-function 查阅有关 rx 的内容。另外我们也可以通过 rx-to-string 来将一个rx 表达式转化为正则表达式.练习本节的练习,我们利用rx表达式来实现一个将 markdown 中的标题修改为 org mode 中标题的小工具。为了实现这个转换,我们大致需要经历这么几步:通过正则表达式找到 markdown 的标题行获取匹配到的 # 数量以便确定是几级的标题根据#数量替换成等数量的 *根据上面步骤,我写下如下的代码:(defun my/convert-md-tilte-to-org() (interactive) (save-excursion (save-restriction (widen) (goto-char (point-min)) (let ((count 0) (header-regexp (rx line-start (group (repeat 1 6 "#")) (one-or-more space))) ;;位于行首,捕获组1的特征是,# 重复1到6次,后面跟一个或者多个空格 ) (while (re-search-forward header-regexp nil t) (let* ((hashes (match-string 1)) ;; 从捕获组1当中提取匹配到的#字符串 (has-count (length hashes)) ;; 计算#数量 (starts (make-string has-count ?*)) ;; 生成对应数量的 * 字符串 (replacement (concat starts " ")) ;; 组装org mode 标题行 ) (replace-match replacement t t) (setq 1+ count))) (if (> count 0) (message "共找到%d处并替换" count) (message "未找到对应的标题行"))))))小结本节内容主要介绍了通过 rx 来生成生成正则表达式,采用这种方式的好处在于正则表达式代码容易理解,不容易出错,而且还可以带上注释,更进一步降低正则表达式的阅读门槛
2026年08月17日
8 阅读
0 评论
0 点赞
2026-08-16
《elisp 每日一练》—— 专题综合实战
到此我们关于Buffer 和window 的内容就全部介绍完了。按照惯例本节对之前内容进行总结并完成一个综合性的实战练习知识回顾首先我们学习关于buffer管理的一些函数get-buffer-create 和 get-bufferkill-bufferset-bufferwitch-current-bufferbuffer-namebuffer-live-p接着我们学习了一些控制buffer属性相关的函数buffer-namerename-bufferbuffer-file-name 和 set-visited-file-namebuffer-modified-p 和 set-buffer-modified-pbuffer-read-onlyinhibit-read-only后面我们进入关于Window的内容,window有下列函数split-windowset-window-bufferwindow-bufferselect-windowselected-windowwalk-windowsdelete-window 和 quit-windowdisplay-buffer 和 pop-to-buffer之后介绍了如何在buffer中贴上一个标识的标签,通过 overlay机制make-overlayoverlay-put 和 overlay-getoverlay-start 和 overlay-endoverlays-at 和 overlays-in最后我们介绍了如何跨buffer进行文本提取,主要是一个函数 insert-buffer-substring综合练习今天的综合练习,我们尝试将这些内容结合起来,我们完成一个简单的交互式的日志buffer。它具有下面的功能:对外提供一个简单的 log 函数,它支持 ERROR、WARNING、DEBUG、INFO 四种类型的日志代码种每调用一次 log 函数都会往 *App-Log* buffer种记录一条日志日志的格式为 ERROR[line-num]: message我们可以通过日志中记录的信息快速回到对应写日志的位置我们先实现一个对外的日志函数, 主要用来记录日志,并保存一些隐藏信息(defun my/log(level message) (let* ((level-sym (if (stringp level) (intern (upcase level)) level)) (src-buf (current-buffer)) (file-or-buf (or (buffer-file-name) (buffer-name))) (line-num (line-number-at-pos)) (log-buf (get-buffer-create "*App-Log*"))) (with-current-buffer log-buf (let ((inhibit-read-only t)) (goto-char (point-max)) (let ((start-pos (point))) ;; 格式 [LEVEL][buffer-name][line-number]:message (insert (format "[%s][%s][%d]: %s\n" level-sym file-or-buf line-num message)) ;;为了便于后续跳转回来,这里我们保存一些隐藏信息 (add-text-properties start-pos (point) `(log-file ,file-or-buf log-line ,line-num log-buf ,src-buf)))))))文本属性我们目前没有介绍到,属于朝纲的内容,但是相对来放在这里并不难理解。另外注意这里我们使用的是反引号而不是列表中常用的单引号。单引号的作用是禁止对表达式求值,但是我们实际需要对后面的 file-or-buf这些变量进行求值。所以我们使用反引号表示将这个作为列表的模版,对逗号后面的表达式求值。然后我们添加一个从log 信息中跳转到对应位置的函数,该函数主要作用是从日志文件中读取隐藏数据,并跳转(defun my/log-jump-to-source() (interactive) (let ((file (get-text-property (point) 'log-file)) (line (get-text-property (point) 'log-line)) (src-buf (get-text-property (point) 'log-buf))) (message "%s %s %d" file (buffer-name src-buf) line) (if (not (and file line)) (message "没有有效的日志源信息") (if (and src-buf (buffer-live-p src-buf)) ;;弹出或切换到源buffer (pop-to-buffer src-buf) (find-file file)) ;;移动到对应行 (goto-char (point-min)) (forward-line (1- line)))))在跳转的时候,我认为在跳转回源buffer时,应该重点仍然在日志上,我们只是简单的看看报错的日志在哪里。也就是要保留日志buffer、短暂的显示源buffer。所以就排除了使用 split-window 的想法,但是我们可能需要跳转之后短暂的修改代码,所以我认为使用 pop-to-buffer 比较合适。到此我们简单的日志工具就完成了。虽然它不那么完美,例如我们记录的其实并不是调用log函数时的行,和buffer,而是执行时的当前buffer和光标所在位置。实际上我们可以通过宏来解决,就像c/c++ 中的 __FILE__、__LINE__ 一样。读者可以查阅相关资料尝试解决这些问题。
2026年08月16日
11 阅读
0 评论
0 点赞
2026-08-15
《elisp 每日一练》——跨 Buffer 文本提取
在之前的文章中,我们通过with-current-buffer 和 buffer-substring 实现了一个从指定buffer中拷贝文本到本buffer的功能。这个算是我们入门了跨buffer操作。实际上Emacs为这种需求提供了专门的函数背景知识Emacs中的跨buffer文本操作的基础函数是 (insert-buffer-substring BUFFER &optional START END) 。它用于将参数中 BUFFER 指定的缓冲区中从 START 到 END 这个范围内的文本插入到当前缓冲区的光标所在位置。我们可以用下面的代码感受一下(defun my/copy-messages-buffer-to-this() (interactive) (let ((message-buffer (get-buffer "*Messages*"))) (when message-buffer (let ((start) (end)) (with-current-buffer message-buffer (save-restriction (widen) (setq start (point-min)) (setq end (point-max)))) (insert-buffer-substring message-buffer start end)))))因为 START 和 END 参数实际上是对应的是 BUFFER 中的范围。所以我们需要切入到对应的buffer中去获取这个坐标的最大和最小值。基于这个函数我们可以实现很多好用的跨buffer的文本操作,例如将当前buffer中的内容拷贝到目标buffer中,或者将目标buffer中的内容替换为我们想要的内容等等。因为各种跨buffer的文本操作实在是太频繁了,所以Emacs提供很多有用的函数,下面我列举一些常用的函数append-to-buffer: 从当前buffer中提取 START 和 END 范围内的文本并追加到参数 BUFFER 所指定的缓冲区的光标位置copy-to-buffer : 从当前 buffer 中提取 START 和 END 范围内的文本并对参数中 BUFFER 所指定的缓冲区进行替换上述这些函数提取的内容都带有文本属性,如果我们只想提取文本本身,可以使用 buffer-substring-no-properties 。查阅相关文档,我们发现 buffer-substring-no-properties 实际上对标的是 buffer-substring,都是提取当前buffer的文本,而不涉及到改变buffer。在跨buffer的函数中,只有 insert-buffer-substring 是使用底层C函数编写的代码,而其他两个都是依靠这个函数,在lisp层面实现的。所以说,关于跨buffer的文本操作,我们只需要掌握基本的 insert-buffer-substring 其他的函数我们可以自己实现。这有点像数学,上学的时候老师告诉我初中的几何只有三条基本公理,其他的定理和自己利用这三条推导出来。Emacs也是这样,只提供基本的功能,其余高级的功能可以自己利用基本函数实现出来。但是也提供了这些高级函数的官方实现。练习今天我们来实现一个日志收集的功能。我们将当前用户选中的行记录到一个名为 Log-Collection 的buffer中,该buffer平时不允许写入,只能通过调用特定的函数进行收集。如果用户选择并激活了一个区间,则提取区间的内容进行激活,否则提取当前所在行的文本。(defun my/collection-log() (interactive) (let ((log-buffer (get-buffer-create "*Log-Collection*")) (cur-buf (current-buffer)) (start) (end)) (if (use-region-p) ;; 激活了选区,则按照选取来决定范围 (progn (setq start (region-beginning)) (setq end (region-end))) ;; 未激活选取,则按照光标所在行来决定范围 (progn (setq start (line-beginning-position)) (setq end (line-end-position)))) (with-current-buffer log-buffer (let ((inhibit-read-only t)) (goto-char (point-max)) (insert-buffer-substring cur-buf start end)))))总结本节介绍的重点函数只有一个 insert-buffer-substring 因为功能导致它的参数比较割裂,BUFFER 是目标buffer、而START 和 END 对应着源buffer的位置。这也就意味着想要成功调用该函数,无论如何都得使用 with-current-buffer 进行buffer的切换,无法一步到位其余的函数我们可以自己实现,所以就不在这里列举了。
2026年08月15日
10 阅读
0 评论
0 点赞
1
...
5
6
7
...
11