首页
归档
友情链接
关于
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
菜谱
翻译
页面
归档
友情链接
关于
搜索到
442
篇与
的结果
2026-09-01
《elisp 每日一练》—— Minibuffer 与 completing-read
通过前面一段时间的学习,我们学习了如何与窗口进行交互。现在我们进入minibuffer的交互。从标题上看我们本节主要就是再介绍一遍 completing-read,这个函数我们前面已经多次用到过,读者如果不熟悉,可以再一次阅读本文,如果已经熟悉了,可以跳过。背景知识minibuffer 的相关概念就不在这里展开了。我们主要聊的是如何通过控制minibuffer相关的API来实现交互。本次我们将介绍一个我们之前遇到过相对比较熟悉的老朋友—— completing-read。我们知道,它可以通过往minibuffer中注入一些选项供用户选择。并返回用户选择的字符串。它的函数原形如下:(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)PROMPT 是给用户的提示词COLLECTION 是我们传入到minibuffer中的候选数据源, 可以是单纯的字符串、Alist、Hash Table、或者是动态生成候选词的函数PREDICATE 它是一个函数,对加入的选项进行过滤,函数返回非 nil 表示接受,否则视为放弃REQUIRE-MATCH 控制用户是否必须输入一个有效的选项DEF 默认值,当用户不选择内容时选中的默认值HIST 历史记录, 传入一个 Symbol,Emacs 会自动把用户的历史选择存入该变量,支持用 M-p / M-n 翻阅前面有好几个例子已经用到了该函数,各位读者对它的常规用法应该不陌生。我们一般通过 alist 结构,将key 设置成用户友好的提示词,而它的背后绑定了相对复杂的数据,在用户选择之后通过背后绑定的数据来实现一些功能。练习前面我们在学习其他内容时已经通过该函数实现了一些相对复杂的功能了,这次练习我们来点简单的,仅仅为了回顾一下它的用法。我们实现一个快速打开指定文件的功能。我们预先定义一些常用文件,例如emacs 的配置文件、自己的工作日志等等。然后将这些内容组织一下传入COLLECTION,最后根据用户选择打开(defvar my/favorite-file-lists '("~/.emacs.d/init.el" "d:/worklog.el")) (defun my/quick-open-favorite-file() (interactive) (let ((selected (completing-read "Input filename to open:(default ~/.emacs.d/init.el)" my/favorite-file-lists nil nil nil nil "~/.emacs.d/init.el"))) (when selected (find-file selected))))小结因为本节涉及到的函数,我们已经多次在其他练习中使用过,对于它读者应该比较熟悉了。因为本人能力不够,所以在练习上面我目前也想不出有什么深入的,又尽量少涉及其他概念内容。所以就来一个最简单的,读者如果觉得例子太简单了,可以考虑自己实现一些复杂的功能。本节涉及的函数只有一个 completing-read
2026年09月01日
4 阅读
0 评论
0 点赞
2026-08-31
《elisp 每日一练》——Window 锁定与专属窗口
我们想象这样一个场景,假设你精心布局了当前的编程窗口,左边是文件树、右边一个大窗口区域是代码编辑区域,右侧有一个小的显示当前类和函数定义的缩率图,下面有输出的日志。但是你运行某个命令弹出了 *Help* 或者 *Compile* 占用了你的某个窗口,本来对整个项目可以一览无余的,现在不得不重新关掉这个新显示的窗口。整个场景想想就糟心。我们有没有办法固定这些布局,新弹出的窗口不要破话它。Emacs一般是只有你想不到,没有它办不到的。背景知识上述场景的解决办法是通过 专属窗口(Dedicated Window) 与 窗口参数(Window Parameters),我们可以给特定 Window 加上“锁”,强制 Emacs 将新 Buffer 弹出到指定位置,保护主窗口不被侵占。各位读者可能有这样的疑问,我们前面介绍了 display-buffer 和 pop-to-buffer 已经解决了新显示的buffer乱弹窗导致布局混乱的问题,为什么又有一个 deddicated window 和 window parameters 这类解决方案呢?其实它们是不同的机制,侧重于从不同方面解决。display-buffer 和 pop-to-buffer 讲究的是这次应该将buffer放在哪。它侧重于将一个内容显示在窗口的某个位置,如果这个位置有了窗口,则重用这个窗口。Dedicated Window 和 Window Parameters 解决的是“这个窗口本身有什么规则”例如我们上面那个布局,display-buffer 和 pop-to-buffer 虽然可以靠算法精确的控制它显示的区域,但是无法保证我们主要的代码区域永远属于显示代码的buffer,设置专有窗口之后,这个窗口就专门用来显示这个buffer的内容。每个窗口都有一个 dedicated 的标志位,当设置这个标志之后,Emacs会尽量避免在本窗口中显示其他内容,例如 switch-buffer、pop-to-buffer、display-buffer 会尽量避免在对应窗口中显示新内容。我们可以通过 set-window-dedicated-p 将一个窗口设置为专属窗口,它接受两个参数:WINDOW 表示设置哪个窗口flag 为 t 时设置专属窗口,为 nil 解除该设置通过 window-dedicated-p 函数可以查看窗口的专属状态。但是仅仅设置 dedicated 标签并不意味着可以精细的管理窗口。我们可以通过设置窗口参数来更细致的控制窗口。Emacs对每个窗口都维护着一个参数的 alist 列表,可以控制窗口的一些行为。窗口参数实际是一推窗口的标签,有些标签Emacs自己就可以利用来改变窗口行为,我们也可以自定这些标签,然后在我们的程序中通过这些标签控制窗口行为和显示。也就是说窗口参数给我们扩展窗口带来了极大的便利设置窗口属性可以使用 set-window-parameter 设置属性,通过 window-parameters 获取对应的属性练习我们来实现一个小例子,来体会一下该如何使用窗口自定义参数改变窗口行为。我们实现一个通过自定义参数 this-window-height 和 this-window-width 参数来修改光标所在窗口大小的函数我们先封装一些获取和设置窗口大小的基本函数(defun my/set-window-height(height) (let ((window (selected-window))) (set-window-parameter window 'this-window-height height) (window-resize window (- height (window-total-height window))))) (defun my/set-window-width(width) (let ((window (selected-window))) (set-window-parameter window 'this-window-width width) (window-resize window (- width (window-total-width window)) t))) (defun my/get-window-height() (window-height (selected-window))) (defun my/get-window-width() (window-width (selected-window)))然后提供一个根据当前参数中存储的大小数据来更新窗口实际大小的函数(defun my/update-window-size() (let* ((params (window-parameters (selected-window))) (height (cdr (assq 'this-window-height params))) (width (cdr (assq 'this-window-width params)))) (if (not) (setq height (my/get-window-height))) (if (not width) (setq width (my/get-window-width))) (my/set-window-height height) (my/set-window-width width)))最终我们在此基础上封装一套修改大小的函数:(defun my/width_zoomin() (interactive) (let* ((params (window-parameters (selected-window))) (width (cdr (assq 'this-window-width params)))) (if (not width) (setq width (my/get-window-width))) (setq width (- 10 width)) (my/set-window-width width))) (defun my/width_zoomout() (interactive) (let* ((params (window-parameters (selected-window))) (width (cdr (assq 'this-window-width params)))) (if (not width) (setq width (my/get-window-width))) (setq width (+ 10 width)) (my/set-window-width width))) (defun my/height_zoomin() (interactive) (let* ((params (window-parameters (selected-window))) (height (cdr (assq 'this-window-height params)))) (if (not height) (setq height (my/get-window-height))) (setq height (- 5 height)) (my/set-window-height height))) (defun my/height_zoomout() (interactive) (let* ((params (window-parameters (selected-window))) (height (cdr (assq 'this-window-height params)))) (if (not height) (setq height (my/get-window-height))) (setq height (+ 5 height)) (my/set-window-height height)))小结本节我们介绍了专属窗口和窗口属性。本节有关的函数有:set-window-dedicated-pwindow-dedicated-pset-window-parameterwindow-parameter
2026年08月31日
6 阅读
0 评论
0 点赞
2026-08-30
读《战国歧途》
本书是接着 《失败的春秋》 继续聊战国的书。春秋战国一般以三家分晋或者以田氏代齐开始。因为春秋时期并没有大夫取代国君的先例。过去大夫可能也权倾朝野,但是仅仅是主持国君的废立,真实的国君仍然是最开始国君的血亲。但是从韩赵魏三家分晋以及田氏带齐开始,开辟了一个异姓大夫取代国君的先河。诡异的是这种明显的僭越却得到周天子的认可,这意味着天子的权威已经荡然无存了。有这个先例之后,各诸侯国的国君开始明白,异姓大夫也开始不可靠了。分封制是天子把天下分割成若干块,王畿之外就分封给诸侯,诸侯国里的事,天子一般不好过问。诸侯再把国分割成若干块,分封给大夫,大夫统治的地方,就叫作“家”。大夫家里的事,诸侯也一般不好过问。与周天子类似的,周天子自己的地盘越分越小,诸侯国也是这样,最终总有几个大的家族可以左右朝政。因为封地导致国君势力慢慢削弱,国君找到了一条新的思路:拥有自己土地、人口、军队的大夫们,总归是靠不住的。从国君的角度考虑,要把权力垄断在自己手里,最好是培养、提拔这样一批人:他能帮我治理国家,但是他没地,没人,没枪,也许有钱—不过我随时可以叫他没钱。找到这批人在当时的社会上并不是不可能,从春秋末期有一个孔子,交学生学习礼、乐、射、御、书、数之类的贵族技能,结果是,孔子那里毕业的高材生,虽然很多是泥腿子穷光蛋,但是贵族们的事,他们都懂,甚至干起来,可能比贵族还精通。当时干这个事情的不止孔子一个人,只是孔子比较出名而已,这些民办院校的师生是在诸国之间流动奔波、焦虑求职的蚁族,这也就给国君们提供了取代贵族的人才库。这些泥腿子出身的人眼界并不广,对国君的要求也不是封地,他们仅仅要求有一份稳定的工作,有一份稳定的收入(一般多发点儿粮食)就足够了,这类人在国君眼里便宜又好用,每年分的那点粮食国君可以随时收走,即使给再大的权力,再高的俸禄他们也掀不起大的浪花。所以,他们和国君一拍即合。这批人的社会身份,叫“士”;而他们扮演的政治角色,用今天仍然高频使用的一个词说,叫“官僚”。士有些特权,只有士,才有资格进政府当公务员。但是你的级别是限死了的,干得再好,指望变成大夫,也是不可能的。即使官位再高国君也不可能给封地。士向上的通道锁死了,但是大夫干的不好家道中落是有可能变回士的。就这样士的上升通道被堵死,而大夫又可能变成士来抢士的饭碗。因为大夫和士的阶层并没有那么死,所以也有士大夫的说法。另一方面,庶人里的优秀分子,又在民办学校(乡校)里受了教育,也可以得到提拔的机会。士就更难过了,一根筋变成两头堵。战国社会阶级间的壁垒打破,士作为大夫和庶人之间的一个阶层,成了上下对流交汇的所在。传统上,国内的大贵族,当然是君主的“朋友”,但国君现在担心的,就是大贵族们想并且有能力抢夺自己的宝座。为了对付这些大贵族,国君们提拔、任用士阶层的人。但其间有一个问题:本国的士,和贵族们之间有千丝万缕的联系(论血缘关系,他跟大夫们往往比跟国君更近)。你怎么能保证,提拔他之后,他就真心站在自己一边,而不是反而和大夫们私通款曲?相反,外国来的士,就可靠多了。他在我国毫无根基,他的权力,都来自我的赐予,一旦失去了我的支持,他就一无所有。他怎么能不全心全意地站在我一边?他当然是我最可信任的人。国内的贵族们自然看外来户是不顺眼的,相应地,外来户打击起贵族来,也最不会心慈手软。士越来越多、国家的岗位又有限、而本国的士又受到猜疑得不到重用,因此战国时期的士阶级流动十分平常,著名的有商鞅、伍子胥、乐毅、苏秦、张仪、吴起这些人。因此战国时期实际上是君主通过士完成打击本国贵族完成中央集权的变革,在这里面最著名的就是商鞅变法了。商鞅变法首先要打击的就是贵族,著名的规定“宗室非有军功论,不得为属籍”,你再是国君的亲戚,如果没有在战场上立功,这个身份是得不到承认的。这是逼贵族和平民站在同一条起跑线上。更根本的政策如推行县制,越发摆明了就是国君要加强自己对地方的控制,而取消贵族被封建的特权。对于普通民众来说,商鞅建立了一整套户籍制度。国境之内,每一个人都要进行户口登记,没有那张身份证(当时术语叫“验”),很多事情都做不成。商鞅又用加税的办法,迫使兄弟分家(不分家就“倍其赋”),把原来的大家族拆分成一个个小家庭,再把小家庭重新编排成新的社群,然后规定,“民无得擅徙”。这就使每个人都处身于国家安排的社群之中,彼此互相监督。对民众来说变法摧毁了人民的自由迁徙权,把普通人固定在土地上变成了国家机器上生产粮食的零件。对于商人,打击商人中比较富有的平民。其次是儒生和辩士,就是平民中较有文化或才智的人。这两类人在商鞅眼中是不事生产,也无法为国君打仗的人,国家必然逐步削弱以至于灭亡。另外一方面经商、和通过自身能力实现阶级跃迁容易在平民中间形成不好的榜样,动动嘴皮子、捣鼓一些货物就能获得财富,不用上战场,也不用面朝黄土背朝天的苦干,长此以往动摇民心。然后是良民,就是任何具有一般伦理道德感的人。商鞅断言,“以良民治,必乱至削;以奸民治,必治至强”。用良民,那么老百姓家庭和睦;用奸民,那么老百姓就觉得爹亲娘亲不如秦国的制度亲。与人为善,喜欢掩盖别人的错误的,这是良民;自私自利而喜欢窥探监视别人的,这是奸民。表彰良民,就会导致过错都被隐瞒;任用奸民,犯罪的人就都能杀掉。前者是民意胜过王法,后者是王法胜过民意。民意胜过王法,国家就乱了;王法胜过民意,军队就强了。商鞅变法的主题,实质上是尊君权。从上面的一些变革来看,商鞅实际上要把人民变成国家的机器,他本身不关注民众权力,凡此种种,都表明了一个简单的事实:人民有钱,有智慧,有独立的人格,都可能导致一个后果,就是不够听话。他需要的不是独立的国民,而是一群听话的绵羊。民众不要有任何想法,乖乖的做国家的耗材就好了。人民,卑贱了就会在乎爵位,弱小了就会尊敬官员,穷光蛋就会贪图赏金。君主要有效地控制人民,无非是两手:一手是刑,一手是赏。要让赏这一手效果发挥到最好,那就得让民辱,让民弱,让民贫。但秦国再强大,秦国人民的生活,六国人绝对不羡慕。前面已经说过,当时人民本普遍没有什么爱国心,除了秦国搞非常严厉的户籍制度,人口迁移基本由政府决定外,其余各国的人,觉得哪个国家好就搬家到哪国去乃是常态。但没谁想去秦国,相反一旦有置身秦国统治下的可能,大家就都很恐慌。因此虽然最后秦短暂的得到天下,结果在这套高压制度上也就二世而亡。因此可以看出打天下不妨使用暴力,而维持稳定才需要民心。战国时期各国其实都有变法,其他各国的变法也是加强君权打击传统贵族。只是其他国家没有像秦国这样彻底把人民当耗材。秦国最终能统一的原因一个是变法,一个是地理位置优秀。处于东部平原的六国,虽然交通便利,经济也相对发达,但是平原属于难守易攻,而秦国位于西边,中国的地理形势,由高往低三级阶梯,六国的核心地区(首都及其周边)都在最低一级阶梯的大平原上。彼此之间,并没什么难以逾越的障碍。这就导致了两个后果:一是秦国打六国很容易;二是六国之间,谁打谁也都很容易。所以,六国都很没有安全感。也是个连锁反应:要想不挨打,就要建立足够强大的军队;要建立足够强大的军队,就得同意君主增加税额和抓壮丁的权力;而其他六国变法没有秦国那么彻底,征兆的壮丁自然有限。秦国老百姓阶级跃迁主要靠军功,因此在参与战争方面秦国是要强于其他六国。况且六国要耗费巨资维持大量的军队,导致经济最终崩溃。这似乎是一个死局,秦国从一开始的地理位置就已经优于其他六国,即使长时间消耗也能消耗,而且又通过变法将民众变为国家机器上的零件。虽然各国都有强势的时期但是在大势面前也无能为力。
2026年08月30日
7 阅读
0 评论
0 点赞
2026-08-30
《elisp 每日一练》——Window 树与布局状态恢复
前面的文章中,我们介绍了可以通过 split-window 来创建窗口,也介绍了通过 walk-windows 遍历窗口并且通过 overlay 机制实现了一个简单、直观的快速跳转窗口的功能。今天我们来深入了解背后的原理背景知识各位读者如果观察过Emacs执行 split-window 的效果,应该会发现它只会将当前窗口的区域进行划分,将当前窗口区域分为两个部分。这样我们很容易想到一个树的结构,光标所在的位置是树的一个叶子节点,在新建窗口时将该叶子节点作为新的根节点,在下方创建两个新的叶子节点。其实Emacs底层正是这样做的。我们可以通过 window-tree 来获取当前frame下窗口布局的树结构。我们用一个简单的例子来描述一下返回的具体结构, 假设我们有如下一个窗口布局+------------+-------------+ | | Window | | Window | B | | A +-------------+ | | Window | | | C | +------------+-------------+我们调用 window-tree 得到的结果可能是这样的( (nil (0 3 84 38) #<window 3 on *scratch*> (t (42 3 84 38) #<window 8 on *scratch*> #<window 10 on *scratch*>)) #<window 4 on *Minibuf-0*>)从整体上看它分为两个大的部分,第一个部分是一个复杂的结构,而第二个部分是一个window对象,它实际上是 mini-buffer 的窗口。我们再来看看前面一个大的结构。我们将它转换成一个树状结构├── (nil (0 3 84 38) │ ├── #<window 3 on *scratch*> │ └── (t (42 3 84 38) │ ├── #<window 8 on *scratch*> │ ├── #<window 10 on *scratch*> └── #<window 4 on *Minibuf-0*>通过这个我们发现,一个根节点实际是当前区域的分割方式和区域的大小,第一个参数为 nil 表示它是水平分割,而 t 表示垂直分割。各位读者请注意,这里的水平和数值的理解,这里并不是在窗口上按水平或者竖直方向画一条线将窗口进行划分,而是表示将新窗口按照水平或者竖直方向进行排列。所以上述结构真正含义其实是:先通过 nil 将窗口区域划分为水平方向的两个窗口,也就是左右两个其中一个为window3将右边区域一分为二,通过 t 表明它应该是竖直方向的两个窗口,也就是在中间水平位置画一条线进行区分在过去如果想要保存和恢复这个窗口布局,我们需要通过 window-tree 函数保存当前的树结构,恢复时解析每个节点并且调用 split-window 重新根据节点进行。但是从Emacs 24.1 之后提供了两个函数,可以方便的进行保存和后续的恢复: window-state-get 和 window-state-put(window-state-get &optional WINDOW WRITABLE)它可以返回指定窗口的布局状态,默认就是root 窗口,也就是当前frame 的主视图区域。我们可以在关闭时通过 window-state-get 获取root 的布局信息,保存到一个文件中,再下一次启动Emacs 时通过window-state-put 进行恢复窗口。如果我们只是临时性的保存和恢复当前窗口布局,可以使用 current-window-configuration 和 set-window-configuration 。它们的作用与前面介绍的 save-excursion、save-restriction 和 with-current-buffer 类似,如果包含临时性的窗口操作,为了让用户没有察觉或者不改变当前窗口布局,可以使用练习好了,我们来实现一个保存和恢复窗口布局的例子,因为我们还没有接触过Emacs文件的操作,所以这里简单的实现一个在Emacs启动期间执行保存和恢复的功能,后续学习了如何读取和保存文件之后我们可以继续完善因为不涉及到持久化的保存,所以我们可以简单的使用 current-window-configuration 和 set-window-configuration 函数。(defvar current-window-state nil) (defun my/save-current-window-state() (interactive) (setq current-window-state (current-window-configuration))) (defun my/restore-current-window-state() (interactive) (set-window-configuration current-window-state))我们的练习比较简单,如果读者觉得难度不过瘾,可以考虑通过保存和遍历 window-tree 的方式实现这个功能小结本篇我们介绍的函数有:window-treecurrent-window-configuration 和 set-window-configurationget-window-state 和 put-window-state
2026年08月30日
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 点赞
1
2
3
4
...
45