首页
归档
友情链接
关于
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-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日
6 阅读
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日
3 阅读
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日
7 阅读
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日
6 阅读
0 评论
0 点赞
2026-08-14
《elisp 每日一练》—— overlay 机制
在之前的文章中,我们利用 walk-windows 和 completing-read 实现了一个可以跳转到任意窗口的程序。但是我们通过 buffer + window 的方式进行显示,如果buffer不重名还好,用户可以根据buffer名称来选择,而当buffer有重名,用户没办法直观的看出哪个项对应哪个窗口。如果读者尝试过配置自己的Emacs,在管理窗口方面一定绕不开 ace-window这个插件,它提供了一个非常优雅高效的方式。今天将介绍overlay机制,并且利用该机制实现一个类似的跳转功能。背景知识介绍对于buffer的显示效果,Emacs提供了两种改变的机制,一种是通过设置 Text properties 来改变文本的字号、字体大小等属性,是 buffer 文本的一部分,会随文本一起复制、保存。另一个就是 Overlay 机制,它只影响显示,不进入 undo 列表,也不随文本复制。对于 Overlay 官方给它的定位是:You can use overlays to alter the appearance of a buffer’s text on the screen, for the sake of presentation features. An overlay is an object that belongs to a particular buffer, and has a specified beginning and end.你可以将它想象成一个贴在buffer上的便利贴。如果要使用的话,我们首先要创建一个便利贴、然后决定便利贴的位置,最后在不需要时销毁它。并且Emacs还提供了一些操作它的函数。下面我们来列举一些重要的函数。创建可以使用函数 make-overlay,它的原形如下:(make-overlay BEG END &optional BUFFER FRONT-ADVANCE REAR-ADVANCE)函数的相关参数如下:BEG 和 END : 指定overlay起始位置和终点位置BUFFER : 指定叠加层属于哪个缓冲区, 默认为当前缓冲区FRONT-ADVANCE 和 REAR-ADVANCE 表示当开头或者结尾被插入新字符时,overlay 该如何变化我们来了解一下最后两个参数的含义。我们还是将它想象成一个便利贴。假设便利贴我们从头到尾写满了文字,这个时候我们想要在便利贴开始位置或者结束位置写一个新的内容,该怎么办? FRONT-ADVANCE 和 REAR-ADVANCE 就是来处理这种情况的。一种办法是丢失这些内容,纸张的大小不变,这个时候对应着 FRONT-ADVANCE 为非 nil 而 REAR-ADVANCE 为 nil。还有一种办法就是纸张大小改变,我们在前面和后面插入内容,此时对应着 FRONT-ADVANCE 为 nil 而 REAR-ADVANCE 为非 nil关于属性,我们有 overlay-put 和 overlay-get 来设置或者获取属性。面对一个陌生的,不是我们创建的 overlay,我们可以通过 overlay-start 和 overlay-end 来获取它的起始和结束位置另外我们可以通过 overlays-at 和 overlays-in 来获取区域内的所有 overlay。我们常用的属性有:face 设置样式,包括背景色、前景色、字体等display : 替换overlay中的的显示内容priority : 重叠时的 z序window : 限定只在指定窗口生效overlay 实际是对于 buffer 的修改,所以默认情况下,如果多个窗口都显示同一个buffer,那么每个窗口都会显示这样的内容,我们可以通过 window 属性,限定只在指定的窗口中显示例如我们可以尝试写下如下代码,来创造一个显示内容为 "标记" 的黄低红色的标记,并且通过 window 属性指定只在当前窗口显示(let ((overlay (make-overlay (point-min) (+ 10 (point-min))))) (overlay-put overlay 'window (selected-window)) (overlay-put overlay 'face '(:background "red" :foreground "yellow")) (overlay-put overlay 'display "A"))练习到此我们差不多已经掌握了 overlay 的绝大部分用法了。可以开始我们的改造行动了。我们的思路仍然是先遍历,然后给每个窗口打上标签,并且记录到一个列表中,列表中每项的内容为 (LABLE . window) 后续根据用户输入的字母或者数字来找到窗口并跳转。(defvar my/all-window-list '() "保存窗口字典的列表") (defun my/show-overlay(label win) "在左上角创建显示字母的 overlay" (with-selected-window win (let* ((pos (window-start)) (ol (make-overlay pos (+ 10 pos) (window-buffer win)))) (overlay-put ol 'display label) (overlay-put ol 'face '(:foreground "red" :weight bold :background "yellow")) (overlay-put ol 'window win) ol))) ;; 返回 ol 用于清理 (defun my/jump-to-selected-window() "根据用户按下的字母标签进行跳转" (interactive) (let*((chars (mapcar #'identity "abcdefghijklmnopqrstuvwxyz")) ;; 构建一个字母的字典 (overlays '()) ;; 保存所有的overlay 方便后面删除 (index 0)) ;; 存储当前使用的字母的索引 (walk-windows (lambda(win) (when (< index (length chars)) (let* ((ch (nth index chars)) (ol (my/show-overlay (string ch) win))) (when ol (push ol overlays) (push (cons ch win) my/all-window-list))) (setq index (1+ index))))) ;; 读取用户按键并跳转 (unwind-protect (let* ((char (read-char "选择窗口: ")) (win (cdr (assoc char my/all-window-list)))) (if win (select-window win) (message "没有选择对应的窗口"))) ;;清理资源 (mapc #'delete-overlay overlays) (setq overlays nil))))上述代码中使用了一个新的宏 unwind-protect。它的作用与try...catch 中的 finally 类似,它的定义如下:(unwind-protect body-form cleanup-forms...)它主要执行body-form 中的内容,并且保证会执行 cleanup-forms 中的清理代码。总结本节我们学习了如何给buffer 加上 overlay ,用来展示一些信息。本节介绍的函数有make-overlayoverlay-put 和 overlay-getoverlay-start 和 overlay-endoverlays-at 和 overlays-in
2026年08月14日
5 阅读
0 评论
0 点赞
2026-08-13
《elisp 每日一练》——显示buffer的新姿势
上一篇提到,可以使用 split-window 来创建新的窗口,也区分了buffer和窗口的概念,同时也知道如何获取、修改窗口展示的buffer。split-window 创建的窗口有一个特点:它需要我们自己指定新窗口的摆放位置,窗口位置由我们自己维护。一般情况下,在编程或者做工程化的项目时引入 workspace 的概念,我们规定如何划分窗口区间是有利的,但是有些窗口像 *Help*、*Messages* 、Shell 这类窗口我们经常看一眼或者临时需要打开,如果将它纳入自主的窗口管理就不合适了。本节我们来解决这个问题知识铺垫Emacs 中提供了两个函数 display-buffer 和 pop-to-buffer。我们先从 display-buffer 开始。它的函数原形为(display-buffer BUFFER-OR-NAME &optional ACTION FRAME)它会根据我们已有的窗口布局来选择合适的窗口显示对应的buffer内容,也就是说它不一定会创建新的窗口,它只是根据用户需求选择合适的窗口来显示。如果没有合适的则创建,有则利用已有的窗口来显示。我们可以通过 ACTION 参数来定义它的行为。它只负责显示buffer,而不会跳转光标,这种一般用于设置查看日志但是不操作buffer内容的场合。如果我们希望在显示buffer之后光标也跟着跳转,可以使用函数 pop-to-buffer。它们显示buffer的行为完全一样,但是 pop-to-buffer 会移动光标到新的buffer所所在窗口中。(pop-to-buffer BUFFER-OR-NAME &optional ACTION NORECORD)我们看到它的函数参数中,也有一个 ACTION 参数。我们可以通过这个参数来实现自定义显示行为。一个标准的ACTION是 '(FUNCTIONS . ALIST) 的结构。其中 FUNCTION 表示函数或者函数列表,Emacs 在显示时会Emacs 会从左到右依次尝试这些函数,直到其中有一个成功找到或创建了窗口为止。ALIST 是一个由 (属性 . 值) 构建的关联列表。用来微调上述函数执行时的行为(如窗口高度、分屏方向、是否专有等)。各位读者看到这些结构也不必惊慌,Emacs为我们提供了一些常用的通用封装。对于function来说,有以下常用的预定义函数:display-buffer-reuse-window : 重用已有窗口, 如果该 Buffer 已经在某个窗口显示了,直接使用该窗口。display-buffer-same-window : 占用当前窗口, 直接在当前光标所在的窗口中显示该 Buffer。display-buffer-pop-up-window : 切分新窗口, 根据 split-height-threshold 或 split-width-threshold 在周围切出一个新窗口。display-buffer-below-selected : 在下方切分, 在当前窗口的正下方新建或重用一个窗口。display-buffer-at-bottom : 底部全宽弹出, 在当前 Frame(或屏幕)的最底部创建一个跨越整宽的窗口。display-buffer-in-side-window : 侧边栏/侧边窗口, 在 Frame 的四周(left/right/top/bottom)作为 Sidebar 弹出(通常搭配专属属性)。display-buffer-pop-up-frame : 弹出新 Frame , 直接新建一个独立的 OS 窗口(Frame)。对于 ACTION 中的 ALIST 也有一些预定义的属性(window-height . N) : 设置弹出窗口的高度。整数(如 15)表示固定行数;浮点数(如 0.3)表示占当前/全屏高度的比例。(window-width . N) : 设置弹出窗口的宽度。整数表示列数;浮点数表示宽度比例。(side . left/right/top/bottom) : 指定侧边栏弹出的位置,可以与上述 display-buffer-in-side-window 搭配使用(slot . N) : 侧边栏的位置,用于同侧有多个窗口的情况,(reusable-frames . val) : 寻找可重用窗口时的范围:t(所有 Frame)、visible(所有可见 Frame)、0(当前及当前可见 Frame)、nil(仅当前 Frame)。(inhibit-same-window . t): 明确要求绝对不能占用当前窗口。(dedicated . t) : 将新创建的窗口标记为专有(Dedicated)。专有窗口在用户切换其他 Buffer 时不会被轻易替换。有了这些知识,我们可以对显示messages buffer给出简单的定义,例如我希望message buffer永远只出现在最底部,并且占用整个底部。就可以有下面的代码:(defun my/show-messages-buffer() (interactive) (display-buffer "*Messages*" ;; 底部全宽显示 '((display-buffer-at-bottom) ;;ALIST 参数, 高度为30,强制不覆盖当前窗口 (window-height 30) (inhibit-same-window . t))))实际上除了在 ACTION 参数中设置这些属性外,还可以通过全局变量 display-buffer-alist 来设置这里的 ACTION 参数。它与这里通过参数设置的方式相比,优势在于它可以通过正则表达式来匹配一次性定义一堆buffer的显示方式。它的数据结构为:((CONDITION . ACTION) (CONDITION . ACTION) ...)上一节我们提到,可以使用 quit-window 和 delete-window 删除窗口,但是没有讲它们的区别,因为那个时候还没有引入这种显示buffer的新姿势。现在有了这些知识就可以来说说它们的区别了。今天的新姿势对比之前来讲,最大的区别就是不一定要创建新窗口,它可以重用现在的窗口来显示新内容。quit-window 是一种温和的删除窗口的方式,对于这种重用的状态,它只是不显示新buffer的内容但是会酌情保留原来的窗口。而 delete-window 来说就比较不讲情面了,它不管你还需不需要用这个窗口,直接删除。练习今天的练习,我们来实现一个查看message、log这种buffer的功能,我们希望通过 display-buffer-alist 设置全局的,对于这种窗口我们采用底部显示的方式,取底部35%的区域进行显示。(add-to-list 'display-buffer-alist '("\\*.*\\(?:log\\|messages\\).*\\*" (display-buffer-reuse-window display-buffer-at-bottom);; 优先重用已打开的对应窗口;若未打开,则在当前 Frame 的最底部弹出 (window-height . 0.35) ;; 占用25%的空间 (reusable-frames . visible))) ;; 允许在当前可见的其他Frame中搜寻 (display-buffer "*Messages*")总结本篇我们了解了另一种通过window 显示buffer的方式,主要的函数有:display-bufferpop-to-buffer
2026年08月13日
11 阅读
0 评论
0 点赞
2026-08-12
《elisp 每日一练》——窗口操作
经过之前两篇,我们差不多把buffer相关的内容都了解了,主要是buffer的创建、删除、获取以及buffer属性的变动。本篇我们将开始讨论关于窗口的话题背景知识首先需要了解的是窗口和buffer不是一个概念,窗口是用来显示buffer内容的,而buffer只是保存文本信息。并且多个窗口可以显示同一个buffer的内容,但是不同的buffer不能由同一个Window显示。创建一个窗口可以通过函数 split-window ,默认它会在光标所在窗口的右侧新建一个窗口,而新老窗口显示的是同一个buffer。它的函数原形如下:(split-window &optional WINDOW SIZE SIDE PIXELWISE)WINDOW 具体要分隔的是哪个窗口,默认不填的话就是当前窗口SIZE 新窗口的大小(行数或列数), 如果为正数则表示新窗口的宽或者高,负数则表示留给原窗口的宽或者高SIDE 分割方向below 或 t 在下方创建新窗口above 在上方创建新窗口right 在右侧创建新窗口left 在左侧创建新窗口PIXELWISE 表示 SIZE 参数的单位, 若为非 nil,SIZE 的单位将从字符行列数变为像素单位它返回新创建的窗口对象。因为它默认只能显示当前buffer,一般来讲我们会在创建的时候指定它要显示的buffer,修改窗口显示的buffer使用函数 set-window-buffer 它需要接收至少两个参数,分别是窗口和buffer(或者buffer的名称)而我们如果希望获取window中显示的buffer可以使用函数 window-buffer, 它返回当前窗口中显示的buffer 对象。一般在日常使用中将它们搭配使用(defun my/show-message-buffer() (interactive) (let ((new-window (split-window nil 20 'below))) ;; 在当前窗口下方新建一个高度为20行的新窗口 (set-window-buffer new-window "*Messages*"))) ;; 新窗口中显示 *Messages* buffer上述例子执行之后我们发现,光标仍然停留在之前的窗口上,想要修改光标所在的窗口可以使用 select-window,它接收一个window对象表示要将光标移动到哪个窗口,或者说当前要激活哪个窗口。因此我们可以改进一下上述函数,做到立即跳转光标到新窗口。这里就不给代码了,读者可以自行尝试。如果我们想遍历当前窗口可以使用 walk-windows ,它主要接受一个函数,每当遍历一个窗口时调用该函数。最后再介绍一下删除窗口的操作。删除窗口操作可以使用 delete-window 和 quit-window, 一般而言quit-window 较为温和,而 delete-window 粗暴一些。 quit-window 用于删除一些临时性的或者弹出式窗口,而delete-window 用于删除其他窗口。练习本次的练习,我们结合 walk-windows 和 completing-read 函数做一个遍历所有窗口并根据用户显示来将光标跳转到指定窗口的小功能。(defun my/jump-to-selected-window () "遍历所有窗口并通过补全进行跳转(支持同名 Buffer 区分)。" (interactive) (let ((window-collections '())) (walk-windows (lambda (win) (let* ((buffer (window-buffer win)) (bname (buffer-name buffer)) ;; 生成格式如: "*scratch* [win #<window 3 on *scratch*>]" (label (format "%s [%s]" bname win))) (push (cons label win) window-collections)))) (let ((selected (completing-read "请选择要跳转的窗口: " window-collections nil t))) (when (and selected (assoc selected window-collections)) (select-window (cdr (assoc selected window-collections)))))))上述代码我们考虑了多个window显示同一个buffer的问题,以buffer和window对象的字符串作为主键,后续根据用户选择来执行 select-window 操作。总结本篇我们介绍了基础的窗口操作,本篇介绍的函数如下:split-window 创建新窗口set-window-buffer 设置窗口显示的bufferwindow-buffer 获取窗口显示的buffer对象select-window 激活指定的窗口对象walk-windows 遍历当前所有窗口delete-window 和 quit-window 关闭窗口
2026年08月12日
8 阅读
0 评论
0 点赞
2026-08-11
《elisp 每日一练》——buffer属性
前面我们介绍了buffer相关的操作,主要有buffer的获取、创建、删除、设置当前buffer。我们再最开始的时候为了方便理解buffer的概念,让大家将它类比成 char* const 类型,主要用来保存文本数据的缓冲。这条对也不对,buffer是Emacs维护的一个复杂的结构,可不单单是有个缓冲区的指针那么简单。它除了保存了文本的缓冲,也有各种属性。本篇我们将来介绍一些常用的属性背景知识铺垫首先是buffer的名称。我们在创建的时候都会给一个名称,后续也可以根据名称来获取buffer对象。可以说名称似乎就是获取buffer的 primary key。获取名称可以使用 buffer-name ,而修改名称可以使用 rename-buffer。rename-buffer 有一个名为 UNIQUE的可选参数,表示是否允许名称重复,如果允许的话,在重名的情况下会在buffer名称后加上数字后缀,如果不允许也就是值为 nil 的时候,给出重复名称时会报错。buffer-name 可以传入buffer 对象来获取不同buffer的名称,而 rename-buffer 只能修改当前buffer的名称。第二个属性是它关联的文件。一般来讲如果我们希望修改的内容能持久化的保存,需要将buffer与磁盘上的物理文件关联,并在Emacs退出前进行保存。buffer-file-name 可以帮助我们获取当前buffer关联的文件的物理路径。如果我们要修改/设置某个buffer关联的物理文件,可以使用 set-visited-file-name。第三个属性是修改状态。当Emacs打开一个文件时,会将文件的内容加载到buffer中,并将二者进行关联。此时buffer处于未修改或者说干净的状态。如果我们执行操作修改了文件内容,那么此时buffer会变成修改但未保存到文件的状态,或者说是脏页状态。我们手动执行保存操作之后buffer又会恢复干净的状态。如果我们关闭Emacs时,Emacs默认会遍历当前打开的buffer,对于哪些处于修改状态的buffer会弹出提示框询问我们是否需要保存。但是对于那些没有关联到文件上的则省去了询问的过程。我们可以通过 buffer-modified-p 来查看当前buffer 是否被修改了。也可以通过 set-buffer-modified-p 来设置它的状态。第四个属性是只读属性。如果尝试修改它会报错。我们可以通过 buffer-read-only 这个变量来确定当前buffer是否是只读的。例如我们可以写一个简单的函数来判断指定buffer是只读状态(defun my/buffer-read-only-p (bname) (interactive "B输入对应的buffer:") (let ((buffer (get-buffer bname))) (if buffer (with-current-buffer buffer (if buffer-read-only (message "buffer %s 是只读的" bname) (message "buffer %s 不是只读" bname))) (message "未找到指定名称的buffer"))))我们可以通过设置当前buffer的 inhibit-read-only 变量为t来临时的取消buffer的只读状态,例如我们可以尝试通过以下代码清空 Messages buffer(defun my/clear-messages-buffer() (interactive) (let ((message-buffer (get-buffer "*Messages*"))) (if message-buffer (with-current-buffer message-buffer (let ((inhibit-read-only t)) (erase-buffer))))))请注意我们这里使用的是 let 设置的变量值,而没有使用 setq。我们这里实际上创建的是同名的临时变量,用临时变量覆盖了buffer的“全局变量”。在临时变量存在的时候Emacs读取的是这个临时变量的值,此举会短暂性的将buffer修改为可写状态。在代码跳出let的作用域之后临时变量被销毁,inhibit-read-only 又恢复了初始值。而setq 就达不到这个效果。在 Emacs Lisp 官方开发规范中,临时修改只读 Buffer 最标准的做法就是利用 let 绑定 inhibit-read-only练习有了这些基础,我们可以写一个批量保存的小工具。我们可以遍历当前所有打开的buffer,判断buffer是否是修改状态,如果被修改并且绑定到了具体的物理文件则执行保存操作。保存buffer到文件可以使用 save-buffer 函数(defun my/save-all-buffers() (interactive) (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (and (buffer-modified-p) (buffer-file-name)) (save-buffer)))))总结本篇我们介绍了,Emacs中关于buffer的一些常用属性,以及修改它们的函数或者变量。下面我将其列举出来buffer-name 和 rename-bufferbuffer-file-name 和 set-visited-file-namebuffer-modified-p 和 set-buffer-modified-pbuffer-read-only 和 inhibit-read-only
2026年08月11日
12 阅读
0 评论
0 点赞
2026-08-10
《elisp 每日一练》——buffer 操作
在前面关于buffer 的介绍中,我们都是在操作当前buffer。但是Emacs里面有许多buffer,同时也允许我们创建不同的buffer。本篇我们将进入buffer操作的环节。看看如何在Emacs中管理buffer。背景知识buffer的操作与文本操作类似,无外乎也就是增删改查四个大的内容。我们按照这个顺序来介绍。首先是buffer的创建,可以使用 get-buffer-create 来完成,它接收一个buffer 名称的字符串作为参数,如果当前名称的buffer已经存在则直接获取该buffer的对象,否则创建一个新的buffer。如果我们无论如何都要创建新buffer,而不管名称是否存在,可以使用 generate-new-buffer,该函数会无条件的创建新的buffer,如果当前名称的buffer已经存在,它会创建一个带数字后缀名称的新buffer。而如果我们仅仅获取已经存在的buffer,可以通过函数 get-buffer,当buffer不存在时会返回nil删除一个buffer 可以使用 kill-buffer,它接收一个buffer对象或者buffer名称的字符串作为参数,来删除一个buffer。修改buffer,实际上我个人认为分成两个层面,一个是修改buffer的内容,也就是前面我们提到的goto-char、delete-region 之类的文本操作。而另一个就是修改buffer的属性。其中有一个就是修改当前buffer,在这个层面可以使用函数 set-buffer。这里可能要稍微补充一下什么是当前缓冲,用面向对象的角度来讲,我们调用操作对象的方法总的有一个被操作的对象,而C/C++ 中这个对象实际上通过类函数中的this 指针进行绑定。而我们调用 goto-char、insert、delete-region 等函数,操作的是当前buffer,如果我们希望这些函数操作另外的buffer,那么就需要通过 set-buffer 修改默认buffer。一般来讲,我们通过 set-buffer 切换到别的buffer中进行操作实际上是一个临时性的动作,因为用户此时的注意力完全在当前显示的buffer上,我们只会短暂的切换,而后续又会切换回来。因此这里涉及到一个关于buffer的保存和恢复现场的概念。保存和恢复buffer的现场,用的是 with-current-buffer。witch-current-buffer 会切换到一个已存在的buffer,而事后并不会主动清理无效的buffer。也是说我们在使用前要先确保当前buffer存在,然后使用。但是有时候我们仅仅只需要一个临时的buffer,用后直接删除,此时可以使用 with-temp-buffer到此为止我们已经有4个保存和恢复现场的函数了。我们一般的顺序是 with-current-buffer、save-excursion、save-restriction,至于 save-match-data ,用的不如前面一个广泛。查询buffer,我们可以通过 buffer-name 获取指定buffer的名称,它接收一个buffer对象,或者默认获取当前buffer的名称。我们也可以通过 buffer-list 来获取所有buffer 的列表。最后我们判断一个buffer是否有效时可以使用 buffer-live-p练习buffer的内容可能没有之前介绍文本操作时那么详细,因为我觉得buffer比较简单,实际上我们只要理解buffer是Emacs存放文本的一个内存缓冲区,我们的这些操作实际上就是在修改保留这块内存的变量的属性。而且如果读者对一些文本编辑器有一定的了解,会发现大多数文本编辑器都有buffer的概念。本次的练习我们来尝试将用户选定的buffer中的文本拷贝到当前buffer最后面。它经历的流程如下:通过交互式命令让用户输入对应的buffer名称通过名称获取buffer对象,如果对象不存在则输出错误信息切换当前buffer直接通过 buffer-substring 获取整个buffer的拷贝切换回当前buffer调用insert插入对应的文本最终我实现的代码如下:(defun my/append-string-to-buffer(bname) (interactive "B您要拷贝哪个buffer的内容:") (let ((src-buffer (get-buffer bname))) (if src-buffer ;; buffer有效 (let (src-string) (with-current-buffer src-buffer (save-restriction (widen) (setq src-string (buffer-substring (point-min) (point-max))))) (push-mark) (goto-char (point-max)) (newline) (insert src-string)) (message "无效的buffer名称"))))当前例子中的代码还是有一些问题的,例如我们没有判断当前所在的buffer是否是只读类型的。只读类型只是buffer的一个属性,关于buffer中的相关属性将在后面的内容进行讨论。本篇我们介绍的函数如下:get-buffer-creategenerate-new-bufferget-bufferkill-bufferset-bufferwith-current-bufferwith-temp-bufferbuffer-namebuffer-listbuffer-live-p
2026年08月10日
29 阅读
0 评论
0 点赞
2026-08-09
《elisp 每日一练》——阶段性总结与练习
在之前的一系列文章中,我们已经详细介绍了关于文本和光标的一些操作以及对应的API。我们先回顾一下之前介绍的内容前期内容回顾我们先介绍了有关光标位置和移动光标的一些函数:point-min 和 point-maxline-beginning-position 和 line-end-positiongoto-charforward-line以及拷贝对应区域文本的函数 buffer-substring接着又介绍了有关恢复光标位置相关的API函数markpush-mark 和 pop-marksave-excursion而Emacs内部维护 mark 的方式被称为 mark-ring,有关 mark-ring 的函数与变量如下:mark-ringmarker-positionline-number-at-posmarker-buffermark 与 Point 能构成一个 Region,关于 Region 的函数如下:region-beginning 和 region-endactivate-markuse-region-p因为 region 本身不具备区域保护的功能,它无法限制我们仅仅操作某个区间的文本,所以Emacs又提供了 Narrowing 和 Widen 机制。narrow-to-regionwidensave-restriction之后我们正式进入了对文本的增删改查的学习。相关API内容如下:insertdelete-regionsearch-forward 和 re-search-forward 以及反向的 search-backward 和 re-search-backwardmatch-stringsave-match-datastring-matchstring-match-pmatch-beginning 和 match-endreplace-match综合练习本次练习,我们结合之前介绍的相关API来写一个综合性的功能。我们平时在写代码时经常会写各种各样的注释,有些注释有一些固定的写法, 例如我经常写 TODO 表示这部分功能没有加,后面加上, FIXME 这部分实现的比较粗糙,后面在进行重构或者精细化处理。虽然未来大概率就不了了之了,但是留下这些注释显得比较专业(不是)我们现在要做的就是收集 TODO 和 FIXME 开头的这些注释,然后将它们加入到minibuffer 中,显示,在用户选择对应的注释之后跳转到buffer对应的位置(defvar my/todo-mark-list '() "保存全局的todo列表,格式为(SUMMARY-TEXT . marker)") ;; 收集todo/fixme 数据 (defun my/code-todo-list-collect() (interactive) (setq my/todo-mark-list '()) (save-excursion (save-restriction (widen) (goto-char (point-min)) ;;遍历并收集数据 (while (re-search-forward "//\\s-*\\(todo\\|fixme\\):?\\s-*\\(.*\\)$" nil t) (let* ((raw-content (match-string 2)) ;; 准备数据 (m (copy-marker (match-beginning 0)))) (push (cons raw-content m) my/todo-mark-list)))))) (defun my/jump-to-next-todo() (interactive) (save-restriction (widen) (if (null my/todo-mark-list) (message "当前没有待修正的任务") (let* ((selected (completing-read "请选择要跳转的位置: " my/todo-mark-list nil t))) (when selected (let ((target (cdr (assoc selected my/todo-mark-list)))) (push-mark) (switch-to-buffer (marker-buffer target)) (goto-char target)))))))本次的练习,我们分了两个函数,一个用于收集信息,另一个用于在minibuffer 进行交互。在收集信息时我们仍然采用之前的写法,将string 放在CELL的最外层,这样 completing-read 可以正常读取出对应的字符串提示信息并且显示。而后面我们仍然是通过二者的关联性快速执行跳转。虽然我将它称之为综合练习,但是目前我们还有有些内容没有涉及到,比如说修改注释,我们可以做的更复杂一些,将这种注释修改为更加标准的内容。后面我们学习了buffer、file相关的操作之后就可以做的更复杂一些,例如对搜索的结果进行持久化,又或者搜集整个工程中的待办事项。总结到此位置关于操作文本相关的内容我们都已经涉及到了,后面我们会开始进入有关 buffer 和 window 专题讨论相关的操作
2026年08月09日
10 阅读
0 评论
0 点赞
1
...
3
4
5
...
8