首页
归档
友情链接
关于
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-08-30
《elisp 每日一练》——Window 树与布局状态恢复
前面的文章中,我们介绍了可以通过 split-window 来创建窗口,也介绍了通过 walk-windows 遍历窗口并且通过 overlay 机制实现了一个简单、直观的快速跳转窗口的功能。今天我们来深入了解背后的原理背景知识各位读者如果观察过Emacs执行 split-window 的效果,应该会发现它只会将当前窗口的区域进行划分,将当前窗口区域分为两个部分。这样我们很容易想到一个树的结构,光标所在的位置是树的一个叶子节点,在新建窗口时将该叶子节点作为新的根节点,在下方创建两个新的叶子节点。其实Emacs底层正是这样做的。我们可以通过 window-tree 来获取当前frame下窗口布局的树结构。我们用一个简单的例子来描述一下返回的具体结构, 假设我们有如下一个窗口布局+------------+-------------+ | | Window | | Window | B | | A +-------------+ | | Window | | | C | +------------+-------------+我们调用 window-tree 得到的结果可能是这样的( (nil (0 3 84 38) #<window 3 on *scratch*> (t (42 3 84 38) #<window 8 on *scratch*> #<window 10 on *scratch*>)) #<window 4 on *Minibuf-0*>)从整体上看它分为两个大的部分,第一个部分是一个复杂的结构,而第二个部分是一个window对象,它实际上是 mini-buffer 的窗口。我们再来看看前面一个大的结构。我们将它转换成一个树状结构├── (nil (0 3 84 38) │ ├── #<window 3 on *scratch*> │ └── (t (42 3 84 38) │ ├── #<window 8 on *scratch*> │ ├── #<window 10 on *scratch*> └── #<window 4 on *Minibuf-0*>通过这个我们发现,一个根节点实际是当前区域的分割方式和区域的大小,第一个参数为 nil 表示它是水平分割,而 t 表示垂直分割。各位读者请注意,这里的水平和数值的理解,这里并不是在窗口上按水平或者竖直方向画一条线将窗口进行划分,而是表示将新窗口按照水平或者竖直方向进行排列。所以上述结构真正含义其实是:先通过 nil 将窗口区域划分为水平方向的两个窗口,也就是左右两个其中一个为window3将右边区域一分为二,通过 t 表明它应该是竖直方向的两个窗口,也就是在中间水平位置画一条线进行区分在过去如果想要保存和恢复这个窗口布局,我们需要通过 window-tree 函数保存当前的树结构,恢复时解析每个节点并且调用 split-window 重新根据节点进行。但是从Emacs 24.1 之后提供了两个函数,可以方便的进行保存和后续的恢复: window-state-get 和 window-state-put(window-state-get &optional WINDOW WRITABLE)它可以返回指定窗口的布局状态,默认就是root 窗口,也就是当前frame 的主视图区域。我们可以在关闭时通过 window-state-get 获取root 的布局信息,保存到一个文件中,再下一次启动Emacs 时通过window-state-put 进行恢复窗口。如果我们只是临时性的保存和恢复当前窗口布局,可以使用 current-window-configuration 和 set-window-configuration 。它们的作用与前面介绍的 save-excursion、save-restriction 和 with-current-buffer 类似,如果包含临时性的窗口操作,为了让用户没有察觉或者不改变当前窗口布局,可以使用练习好了,我们来实现一个保存和恢复窗口布局的例子,因为我们还没有接触过Emacs文件的操作,所以这里简单的实现一个在Emacs启动期间执行保存和恢复的功能,后续学习了如何读取和保存文件之后我们可以继续完善因为不涉及到持久化的保存,所以我们可以简单的使用 current-window-configuration 和 set-window-configuration 函数。(defvar current-window-state nil) (defun my/save-current-window-state() (interactive) (setq current-window-state (current-window-configuration))) (defun my/restore-current-window-state() (interactive) (set-window-configuration current-window-state))我们的练习比较简单,如果读者觉得难度不过瘾,可以考虑通过保存和遍历 window-tree 的方式实现这个功能小结本篇我们介绍的函数有:window-treecurrent-window-configuration 和 set-window-configurationget-window-state 和 put-window-state
2026年08月30日
6 阅读
0 评论
0 点赞
2026-08-29
《elisp 每日一练》—— 变量定义与赋值
平时我们在看网上的一些配置Emacs的代码时应该注意到了,有多种多样的elisp 变量声明方式,defvar、setq、setq-default、setq-local、let 等等。今天我们深入buffer和window 机制的第二天我们打算来引入一种新的变量类型:buffer local变量,并以此来区分这几种声明变量的方式。背景知识全局变量如果各位读者有其他编程语言的经验,应该对这个概念不会陌生,全局变量是随着程序加载被初始化,在程序的任意位置都可以访问,修改。在程序结束之后它才会消亡。Emacs 中也有这类变量,在Emacs中声明这类变量可以使用 defvar 和 defcustom。虽然它们二者都是定义全局变量,但是二者设计的初衷完全不同,defvar 是为了定义普通的全局变量,而 defcustom 是为了定义用户可定制选项。我们先来聊聊 defvar 的一些行为:defvar 用来定义全局变量,不管它是在哪执行的,它所定义的变量都是全局可以访问的。它可以在定义变量的时候给一个初始值, 并且可以为变量增加一个字符串的说明文档。如果在定义时发现该变量已经存在,那么它将什么也不做对于第三条特性,我们可以使用下面的代码来测试(progn (defvar custom "Jack") (message "Hi, %s" custom) (defvar custom "Tom") (message "Hi, %s" custom))我们可以看到它打印了两次 "Hi,Jack"。我们按照上述 defvar 的特征,对 defcustom 逐条审视,发现它在此基础上有一些增强的特性:对于第一条,它完全继承并且行为也一样对于第二条,它的行为有增强,对于描述性的内容,不光可以使用字符串,还可以使用一些关键字进行详细的描述对于第三条,同样的它后续可以通过 setq 来设置新值,但是它支持用户通过customize 设置变量的新值,用户后续Customize界面设置的值可以覆盖原始值,并且这个值会被写入 ~/.emacs.d/custom.el 获取通过 custom-file 变量指定的文件内进行持久化保存我们可以尝试使用使用一下(defgroup my-system-config nil "Configuration group for my personal system settings" :group 'emacs) ;;挂载到emacs 基础分组下,方便查找 (defcustom my-system-type 0 "Target operating system type for my custom scripts 0 represents Windows, 1 represents Linux, 2 represents MacOS" :type '(choice (const :tag "Windows(0) 0") (const :tag "Linux (1) 1") (const :tag "MacOS (2) 2")) :group 'my-system-config)我们定义了一个操作系统的设置变量,其中 0代表Windows、1代表 Linux、2 代表MacOS。而变量的默认值是0,后续读者可以尝试执行 M-x customise 来尝试通过界面设置这个变量定义全局变量实际上还有一个 defconst,它一般用于定义常量,但是在执行时它并不阻止我们后续为变量赋新值。更像是开发者的一个约定,使用它定义了之后,就不要修改它了变量的修改变量修改我们可以使用 setq 和 setq-default。 setq 负责给变量赋值,但是如果赋值时变量未定义,它可以定义一个变量,然后再赋值。所以在某种程度上,它也兼职一些 defvar 的工作。但是它定义的变量的声明周期与执行该代码的位置有关。如果是全局位置的,那么它就可以定义全局变量,在函数中那么定义的就是局部变量。setq-default 同样可以设置变量的值。它与 setq 的核心区别是当一个变量是“缓冲区局部变量”时,它们修改的是不同层级的值。setq 修改的是当前缓冲区的局部值,而 setq-default 修改的是变量的全局默认值缓冲区局部变量缓冲区局部变量,英文是 buffer local variable 这是一类特殊的全局变量。我想有一个段子可以更好的描述它。话说当年军阀混战时,有一个没有文化的军阀看到一群人在打篮球。军阀说“这群人真可怜,一群人只有一个篮球,难怪他们都要追着一个球跑”,然后吩咐手下,给每个人分配一个球。buffer local 变量与上述段子中军阀给每个人都分配一个篮球类似,Emacs给每个buffer都分配了一个局部的变量。虽然变量名称没变,但是变量在buffer中都是相互独立的。例如我们要设置行号,可以使用 (setq display-line-numbers t) 来显示行号。但是我们切换到其他buffer 中发现其他的buffer无法显示行号。在当前显示行号的buffer中,查看该变量的值发现它是 t,而其他buffer中的值是 nil。就像我们前面说到的,setq 只改变当前buffer中的变量,如果要修改变量的全局默认值,可以使用 setq-default。定义 buffer local 变量,可以使用函数 defvar-local。defvar-local 与 defvar 类似,也是一处定义,后续再次使用 defvar-local 定义式,新给出的值会被忽略。(progn (defvar-local test 0) (with-current-buffer (get-buffer "*Messages*") (setq test 1) (message "test in message buffer is: %d" test)) (message "test in current buffer is: %d" test))设置 buffer local 变量除了使用 setq 和 setq-default 外还可以使用 setq-local。 这三个我暂时称他们为 赋值三兄弟。他们的主要区别在于:场景setqsetq-defaultsetq-local变量未定义自动创建一个全局动态变量强行在当前 buffer 中创建该变量的 buffer-local 绑定,并赋值,此时全局变量依然未定义自动创建一个全局变量 效果等同于 setq变量不是buffer local 类型直接修改该变量的全局默认值强制将其变为当前 buffer 的局部变量然后赋值,全局变量保持不变直接修改该变量的全局默认值,如果已经是buffer local变量,它会修改全局默认值我们将一个变量设置为buffer local 变量可以使用 make-local-variable。它要求这个变量必须是存在的,实际上,我们查看 defvar-local 的源码发现它内部实际调用了 make-variable-local 这个函数。如果我们将上述段子中的篮球比作一个buffer local 变量而运动员比作一个buffer,make-variable-local 要求篮球必须先有一个,然后照着"复制"一个,但是它不负责将篮球交到某个运动员手中。而 defvar-local 则没有这些限制,即使篮球不存在,它也会复制一堆篮球,然后一一交给各个运动员。练习我想今天我们主要介绍的是一些基础的概念,所以今天就不像之前那样写一些实用性的工具,读者可以针对这些内容自行修改并创建测试用例来深入理解它们的概念。defvar 的“已定义则不重新赋值”特性;; 第一次定义 (defvar my-test-var "initial") (message "1: %s" my-test-var) ; => "initial" ;; 第二次定义(新值被忽略) (defvar my-test-var "new") (message "2: %s" my-test-var) ; => 仍然 "initial" ;; 但可以用 setq 强制修改 (setq my-test-var "changed") (message "3: %s" my-test-var) ; => "changed"setq 在函数内创建局部变量(defun test-setq-local () (setq my-func-var "inside") (message "Inside function: %s" my-func-var)) (test-setq-local) ; => "inside" (message "Outside: %s" my-func-var) ;; => Outside: insidesetq-default 与 setq 在 buffer-local 变量上的区别;; 先定义一个全局变量 (defvar my-global-var "global-default") ;; 创建一个新的 buffer 并在其中设置局部值 (with-current-buffer (get-buffer-create "test-buffer") (make-local-variable 'my-global-var) ; 显式设为 buffer-local (setq my-global-var "buffer-local-value") (message "In test-buffer, setq: %s" my-global-var)) ;; 切换回原 buffer,查看全局值 (message "In original buffer: %s" my-global-var) ; => "global-default" ;; 用 setq-default 修改全局默认值 (setq-default my-global-var "new-default") ;; 再次查看 test-buffer 中的值(不受影响) (with-current-buffer "test-buffer" (message "In test-buffer after setq-default: %s" my-global-var)) ; => 仍为 "buffer-local-value"defvar-local 的自动 buffer local 特性;; 定义 buffer-local 变量 (defvar-local my-buffer-var "default") ;; 在另一个 buffer 中修改局部值 (with-current-buffer (get-buffer-create "buffer-A") (setq my-buffer-var "value-in-A") (message "A: %s" my-buffer-var)) ;; 当前 buffer 中的值不变 (message "Current: %s" my-buffer-var) ; => "default" ;; 再次用 defvar-local 定义,新值被忽略(同 defvar) (defvar-local my-buffer-var "ignored") (message "After redef: %s" my-buffer-var) ; => 仍为 "default"setq-local 与 make-local-variable 的行为对比;; 先定义全局变量 (defvar my-var "global") ;; 情况1:用 make-local-variable 显式创建绑定,但尚未赋值 (make-local-variable 'my-var) ; 此时全局值仍为 "global",但当前 buffer 有了自己的局部槽 (message "After make-local: %s" my-var) ; => "global"(因为还未赋值,显示全局值) ;; 此时调用setq 实际在给buffer local 变量赋值 (setq my-var "local") (message "After setq: %s" my-var) ; => "local" ;; 其他buffer中仍然输出 "global" (with-current-buffer (get-buffer "*Messages*") (message "my-var in messages buffer is: %s" my-var)) ;; 情况2:直接使用 setq-local(它会自动创建全局变量并设为 buffer-local) (setq-local my-var2 "new-local") (message "setq-local created: %s" my-var2) ; => "new-local" ;; 切换到其他 buffer,查看 my-var2 是否被定义(全局变量存在,但不是 buffer-local) (with-current-buffer (get-buffer-create "buffer-B") (message "In B, my-var2: %s" my-var2)) ; => 如果全局存在且不是局部,则显示全局值(未定义则为 nil)setq-default 对未定义变量的行为;; 变量未定义, 此时它会定义新变量,行为类似 setq (setq-default my-new-var "default") ;; 此时 my-new-var 是一个全局变量 (message "Global: %s" my-new-var) ; => "default" ;; 判断变量是否为 buffer local 变量 (local-variable-p 'my-new-var) ; => nil 它实际是一个全局变量 ;; 切换到另一个 buffer (with-current-buffer (get-buffer-create "buffer-C") (message "In C: %s" my-new-var)) ; => 实际创建的是全局变量,所以这里也输出 `In C default`小结本次我们介绍了Emacs lisp 中的各种变量类型,以及各种变量的定义赋值方式和它们的区别,我们将他们区分为变量定义的三兄弟和变量赋值的三兄弟:defvardefcustomdefvar-localsetqsetq-defaultsetq-local以及相对底层的函数 make-variable-local
2026年08月29日
6 阅读
0 评论
0 点赞
2026-08-28
《elisp 每日一练》——Indirect Buffer
前面我们了解了buffer、window相关的机制,并且学习了操作它们的一些api函数。现在我们继续更深入的探索有关buffer和window的内容。背景知识Indirect Buffer 就像它的名字一样是一个间接的东西,它的本源是原始的buffer,但是它却拥有独立的buffer状态,像point、marker、narrowing、major mode等等。它与真实buffer的关系让我想起七龙珠中的天神和比克大魔王,二者本是是同源,因为天神剥离邪恶人格而诞生比克大魔王,二者虽然同源但是有不同的行为、不同的性格。二者一个的死亡也会牵扯到另一个。Indirect Buffer 就是从源buffer中诞生出来的,并且Indirect Buffer 内容的变化会影响到源buffer。在Emacs 中创建 Indirect Buffer 的方法是 make-indirect-buffer(make-indirect-buffer BUFFER NAME &optional CLONE)参数中的 BUFFER 表示源buffer对象,而 NAME 是 Indirect Buffer 的名称,CLONE 表示是否需要克隆当前buffer的状态。如果你想克隆这些状态,我们可以 clone-indirect-buffer 它会将当前buffer作为一个源buffer,创建一个新的 indirect buffer,并且将源buffer的一些状态信息拷贝到新的buffer中。另外,在引入indirect buffer 之后,我们获取到的buffer可能是 indirect buffer,如果我们想要获取源buffer可以使用 buffer-base-buffer。练习我们来实现一个小型的代码查看器,它会创建一个 indirect buffer 只显示用户选中区域,这样用户在新的buffer中修改也会直接同步到源buffer中(defun my/region-view-scratch() (interactive) (if (use-region-p) (let* ((begin (region-beginning)) (end (region-end)) (base-buffer (current-buffer)) (view-buffer-name (format "Indirect: %s" (buffer-name base-buffer))) (view-buffer (make-indirect-buffer base-buffer view-buffer-name t))) (pop-to-buffer view-buffer) (narrow-to-region begin end) (goto-char (point-min)))))小结本篇的内容到此就结束了,本篇我们介绍了 indirect buffer 的机制,本篇介绍的函数如下:make-indirect-bufferbuffer-base-bufferclone-indirect-buffer
2026年08月28日
5 阅读
0 评论
0 点赞
2026-08-27
《elisp 每日一练》——命令与快捷键的综合练习
到目前我们已经全面了解如何将一个函数定义为交互式命令并且将它绑定到一个快捷键上。现在我们来针对之前的内容做一个综合性的小练习前期内容回顾按照前面的规矩,我们先回顾一下之前涉及到的函数首先可以通过在函数开始位置加入 interactive 来使一个函数变为一个可以由用户主动触发的交互式函数。我们将交互式函数绑定到一个快捷键上,可以使用如下函数:define-keyglobal-set-keylocal-set-key以及Emacs 29以后提供了它们的替代版本:keymap-setkeymap-global-setkeymap-local-setkeymap-unset之后我们又介绍了如何将一个keymap 绑定到快捷键上,使快捷键成为一个前缀make-sparse-keymapdefine-prefix-command练习本次练习也比较简单,我们也是围绕着快捷键的定义和绑定来。我们本次实现一些简单的vim方式的操作,我们实现一个名为 my-simple-vim-normal-mode 它对应着 vim 中的普通模式,我们可以通过 Esc 键来进入该mode,并且按i可以关闭该模式,O、o 可以分别在当前行的上一行和下一行插入新行并退出当前模式基于这个目标我们可以先定义一些基本的快捷键和函数(defvar my-simple-vim-normal-mode-map (let ((map (make-sparse-keymap))) (keymap-set map "h" #'backward-char) (keymap-set map "l" #'forward-char) (keymap-set map "j" #'next-line) (keymap-set map "k" #'previous-line) (keymap-set map "u" #'undo) map))接着我们来实现快捷键的加载与卸载,这里我们使用 set-transient-map 来实现。它可以临时的启用一个全局最高等级的keymap。它可以接受一个keymap和一个函数。每一次用户按键并执行完对应的命令之后会执行这个函数,根据函数返回值来判断是否需要继续维持这个keymap。所以在进行切换的时候可以这么做:(defvar-local my-simple-vim-normal-mode-active-p nil "判断当前mode 是否处于激活状态") (defun my/simple-vim-normal-mode-exit() "退出当前mode" (interactive) (setq my-simple-vim-normal-mode-active-p nil)) (defun my/simple-vim-normal-mode-active() "激活当前mode" (interactive) (setq my-simple-vim-normal-mode-active-p t) (set-transient-map my-simple-vim-normal-mode-map (lambda() my-simple-vim-normal-mode-active-p)))最后我们绑定一个从当前模式进入vim 普通模式的快捷键,并且给出一些从vim普通模式退出的函数(defun my/insert-new-line-next() "当前行下方插入新空白行" (interactive) (goto-char (line-end-position)) (newline)) (defun my/insert-new-line-previous() (interactive) (forward-line -1) (goto-char (line-end-position)) (newline)) ;; 绑定使用 esc 激活该mode ;; 这里为了延续习惯绑定了 esc 键,但是在Emacs 中,捕获了esc键会导致meta键失效,读者如果可以考虑换一个键进行绑定 (global-set-key [escape] (lambda() (interactive) (if (not my-simple-vim-normal-mode-active-p) (my/simple-vim-normal-mode-active)))) (keymap-set my-simple-vim-normal-mode-map "i" #'my/simple-vim-normal-mode-exit) (keymap-set my-simple-vim-normal-mode-map "o" (lambda() (interactive) (my/insert-new-line-next) (my/simple-vim-normal-mode-exit))) (keymap-set my-simple-vim-normal-mode-map "O" (lambda() (interactive) (my/insert-new-line-previous) (my/simple-vim-normal-mode-exit)))到此本篇的练习差不多结束了,读者如果有兴趣可以考虑再实现一些别的功能,进行填充,例如yy、p、v、A、I的绑定,使编辑器更趁手
2026年08月27日
4 阅读
0 评论
0 点赞
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日
6 阅读
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日
5 阅读
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日
4 阅读
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日
2 阅读
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日
5 阅读
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日
4 阅读
0 评论
0 点赞
1
2
3
4
...
8