首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
270 阅读
2
从零开始配置 vim(15)——状态栏配置
159 阅读
3
nvim番外之将配置的插件管理器更新为lazy
145 阅读
4
2018总结与2019规划
138 阅读
5
PDF标准详解(五)——图形状态
116 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
读书笔记
elisp
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
395
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
117
篇与
的结果
2026-08-07
《elisp 每日一练》——文本搜索与正则表达式匹配
前面我们介绍的都是与光标位置有关的东西,都是建立在光标基础上的操作。很多时候我们在完成相关功能时都要借助光标的移动来完成。相信各位读者看到现在已经可以很熟练的通过光标来完成一些简单的工作了。现在我们正式进入文本操作的范畴,文本也是一种数据,数据的操作无外乎增删改查。我们先从查开始基础知识铺垫Emacs中查找主要通过正则表达式。关于正则表达式的详细内容我就不介绍了,如果你写过代码应该对它的概念不陌生。而Eamcs的正则表达式虽然与主流的编程语言在规则写法上略有不同,不过现在有AI的加持,只要说出需求一般AI可以给出准确的正则表达式Emacs 中常用的搜索函数是 search-forward 和 re-search-forward。它们一个是严格的字符串匹配一个是正则表达式搜索。(search-forward STRING &optional BOUND NOERROR COUNT) (re-search-forward REGEX &optional BOUND NOERROR COUNT)它们的参数都是类似的:STRING/REGEX 是待匹配的子串或者正则表达BOUND 表示搜索的截至位置,如果传入nil表示搜索结束位置为buffer的结尾或者指定的 Narrow 结尾NOERROR: 是否进行错误处理,如果为nil的话会进行错误处理,默认的情况下会直接报错崩溃,一般建议是这里设为 tCOUNT : 表示要跳转到后面第几次匹配的位置, 如果为负数的话会向前查找。对于这两个函数,我们需要注意下面几点:光标移动的“副作用”,它会将光标跳到匹配文本的末尾BOUND 表示的是搜索结尾的位置,那么它的开始位置是在哪呢?它的开始位置是光标当前所在区域。或者是当前Narrow区域的开始位置。COUNT 为负值时光标会停留在匹配文本的开头,类似于调用 search-backward 和 re-earch-backwardsearch-backward 和 re-earch-backward 函数中如果参数为正值,会向上找并将光标放置于匹配文本的开头。为负值的行为与 search-forward 和 re-search-forward 相同匹配到的文字我们可以通过 match-string 来获取。它的原形如下:(match-string NUM &optional STRING)理论上讲,采用精确匹配的情况下,我们已经知道了匹配得到的字符串是什么了,不需要额外的获取,但是Emacs还是在 match-string 函数中给这种情况提供了支持。NUM如果我们使用精确匹配的方式一般填写0就好了,因为0代表获取完整匹配的字符串,而填入1、2、3这样的数字用来获取正则表达式中第 N 个捕获组(括号 ( ... ) 内匹配的内容)。STRING 是一个可选的参数,如果我们上一次匹配是从buffer中进行的,那么这次我们可以在这个位置给 nil 或者默认,如果是在一个字符串中执行了匹配,那么我们需要填写上次匹配的字符串。这个函数还需要注意:该函数不执行匹配操作,永远只获取上一次执行匹配的结果。所以我们在执行匹配之后保存结果之前不能再次执行匹配否则之前的结果会丢失。执行完匹配之后除了可以自己写代码进行保存之外,Emacs还提供了宏 save-match-data 用来保存之前执行匹配的结果。既然我们上面说,match-string 可以获取上次从字符串中执行匹配操作的结果,那么怎么利用字符串执行匹配操作呢?Emacs提供了函数 string-match。(string-match REGEXP STRING &optional START)它的函数原形就简单很多了。这里就不细说了。需要注意,Emacs并没有为精确匹配提供类似的函数。实际上通过正则表达式也可以执行精确匹配。如果你不想获取匹配结果只想知道是否能有匹配结果可以通过函数 string-match-p,与 match-string 相比它少了将匹配结果保存的操作,效率更高一些。匹配相关的函数我们都介绍的差不多了,接着再介绍一下匹配时与光标有关的函数。match-beginning 可以获取匹配项起始位置,它接收一个整数参数,参数的含义与 match-string 中的相同match-end 可以获取匹配项结尾位置的Point,它接收一个整数参数,参数的含义与 match-string 中的相同练习有了这些函数,我们来写一个能循环匹配的查找函数。所谓的循环匹配就是匹配到最后一个之后能跳转回来从开头位置继续匹配。并且我们希望匹配时将光标置于匹配字符串的开头位置。这里为了简单,我使用search-forward 精确匹配作为演示(defun my/search-forward-loop(regexp) (interactive "sSearch regexp (loop):") (save-restriction (widen) ;;全局查找,需要取消之前设置的narrowing 限制 (let ((start-point (point))) ;; 这里需要保留光标移动,只有在未找到时才恢复 (when (looking-at regexp) ;; 如果光标已经在匹配位置的开头,需要移动到下一个,防止一直匹配同一个位置的尴尬情况 (forward-char 1)) (if (search-forward regexp nil t) (goto-char (match-beginning 0)) ;; 未找到,尝试跳转开头,重新查找 (progn (goto-char (point-min)) (if (search-forward regexp nil t) (goto-char (match-beginning 0)) (goto-char start-point)))))))总结本篇不涉及什么Emacs的处理机制,就是简单的一些函数介绍,这些函数我们在其他编程语言中或多或少都见过或者用过。所以本篇实际就是一篇无聊的摘录相关函数的东西。本次介绍的函数如下:search-forward 进行向后的精确匹配search-backward 进行向前的精确匹配re-search-forward 进行正则表达式的向后匹配re-search-forward 进行向后的正则表达式匹配match-string 获取匹配结果string-mach 使用字符串进行匹配string-match-p 判断当前是否有匹配结果match-beginning 获取匹配结果的起始位置match-end 获取匹配结果的结束位置
2026年08月07日
1 阅读
0 评论
0 点赞
2026-08-06
《elisp 每日一练》——Narrowing与Widen
在前面我们介绍了Region的概念,它就是由 marker 和 point 构成的一个区域。Region 是一种极度临时的状态——我们可以通过移动光标随时修改它的区域,或者激活/取消激活。光标可以随意移动出Region所在区域,以后如果基于这个条件进行操作时容易造成误操作Region 可以说只有在激活那一刻才有效,后续如果改变了point 或者 marker 就会造成改变为了应对这些问题,Eamcs中又有了 narrowing和widen 机制。它能将 Buffer 的可视与操作范围强制限制在特定区域内,为你提供一个安全、持久的局部编辑“沙盒”。背景知识铺垫Emacs 中允许我们通过 Narrowing 机制将注意力只集中在 Buffer 的某一部分,而把其余所有的文本暂时“隐身”。这样即使某些操作全局的函数此时也只能操作该范围内的文本。首先,Narrowing 会改变 (point-max) 、(point-min) 的值,在默认没有 Narrwoing 的情况下, point-min 总是返回1,而 point-max 返回buffer中最后一个字符所在的索引。但一旦对 Buffer 施加了 Narrowing, point-min 、point-max 的值就与我们设置的可见区域的区间有关了。我们可以通过 (narrow-to-region START END) 将buffer的可见区域设置为从 START 到 END之间的区域。通过 (widen) 来解除所有之前设置的 narrowing。实际上于 mark 、point 类似,对于 Narrowing 我们也要考虑备份和恢复现场的情况,保存和恢复现场使用的函数是 save-restriction。在一些插件的代码中我们经常看到 save-restriction 和 save-excursion 成对出现。它们的用法也是类似的。既然它们一般是成对出现的,我们不妨考虑一下它们两个谁在外层,谁在内层呢?假如 save-restriction 在外层,而 save-excursion 在内层, 也就是这样;; 假设当前光标位于位置 (Point = 400) (save-restriction (save-excursion (widen) ;; 暂时恢复可见区间为整个buffer (narrow-to-region (point-min) 200) (goto-char (point-max)) ;; 执行其他操作 ))上述例子中,我们通过 narrow-to-region 将可见区域限制在 1 到 200 这个区间。然后执行一些操作。当要恢复时先调用 save-excursion 尝试恢复光标位置到最初的 400时发现它的可见区域最多到200,此时光标位置可能被置于200位置。最后执行 save-restriction 恢复可见区域,但是此时光标已经回不到最初的 400位置了。从上面可以看到最好是将 save-excursion 放在外层。练习本次的例子,我们实现一个简单的统计指定行中单词的个数。这次用到的函数为 count-words , 它的原形如下:(count-words START END &optional TOTALS)它主要接收两个参数:统计的起始位置和结束位置。获取行的区间坐标可以通过 forword-line 移动光标到指定行,然后通过 line-beginning-position 和 line-end-position 获取行首和行尾位置的 point值利用今天所学的内容可以写下如下的代码:(defun my/count-words-in-line(line) ;; 获取行首和行尾位置 (save-excursion (save-restriction (widen) ;; 先取消之前所设置的可见区间恢复整个buffer可见 (goto-char (point-min)) (forward-line (1- line)) ;; 移动到对应行 (narrow-to-region (line-beginning-position) (line-end-position)) (count-words (point-min) (point-max)) )))总结本次我们了解了 Emacs 中的 Narrowing与Widen 机制。后面我们写代码操作文本时可以先直接默认操作整个区域,而在需要关注操作区间时通过 narrow-to-region 设置可见区间。不过不要忘了通过 save-restriction 恢复之前设置的可见区间。本次我们介绍的api函数有:narrow-to-region 设置可见区间widen 取消之前设置的所有可见区间save-restriction 恢复调用之前的可见区间
2026年08月06日
1 阅读
0 评论
0 点赞
2026-08-05
《elisp 每日一练》——Mark Ring
在前面我们了解了 mark 的机制以及通过 mark 和 point 来组成region,这次我们将深入了解 Emacs 是如何 管理 mark 的。背景知识铺垫我们通过 push-mark 和 pop-mark 来管理mark。根据这两个名词我们可能会认为Emacs 是通过栈来管理的,push-mark 表示压栈而 pop-mark 表示出栈。其实Emacs管理mark的真实手段确实遵循后进先出的栈规则,但是pop-mark 并不会从栈结构中删除对应的mark记录。想象一下这个场景:我们之前记录了 A、B位置,现在又记录了C位置,如果我想回到上一次的B位置,要通过 pop-mark 让C位置出栈,但是如果后续我又想回到C位置怎么办呢?如果出栈的时候销毁了数据就回不来了。所以实际上 push-mark 会放入新的mark但是 pop-mark 只会改变指向栈顶的位置。并且可以在A、B、C之间循环也就是可以从C回到A。考虑到这些场景,我们能想到应该是一个环形的链表,或者是一个首尾相连的数组,构成一个环形的数据结构。这也就是 Mark Ring 这个名称的由来。实际上mark ring 并不能无限增长,它有一个大小的上限, 通过变量 mark-ring-max 指定,一般默认的是16。如果 ring 的长度超过了 mark-ring-max , 最旧的一个会被抛弃。我们还是以A、B、C三个位置来举例说明每次 push-mark 和 pop-mark 之后,Emacs 如何组织环。首先调用 (push-mark A) 来入栈A位置,此时栈的值为 '(A)结着调用 (push-mark B) 来入栈B位置,此时栈的值为 '(B A)然后调用 (push-mark C) 来入栈C位置,此时栈的值为 '(C B A)如果要回到C位置,直接调用 (goto-char (mark)) 此时栈不变如果要回到B位置, 先调用 (pop-mark) ,Emacs 会取出C的值,然后放到列表的尾部,此时整个栈会变成 '(B A C)如果要回到A位置,与上面类似,先出栈,Emacs会改变栈数据,变为 '(A C B)所以说我们如果想要回到上一步一个办法是不断调用 pop-mark 使对应位置。另一个办法就是回归 mark-ring 的本质,我们在 C/C++ 语言中可以通过数组的方式来实现,但是在Emacs Lisp中,它就是一个列表。我们可以通过操作列表方式来做到这点。实际上,Emacs 通过 mark-ring 这个变量记录着当前所维护的栈。我们可以通过 describe-variable 来查看变量,并结合push-mark 和 pop-mark 函数来观看每次改变之后具体的变量值来观察上述过程。另外,Emacs 实际维护了两组 mark-ring,一组是与buffer相关的,一组是全局的。这个很好理解,如果我们要在多个buffer中进行跳转,自然使用全局的 global-mark-ring 。但是我们并不需要特别的执行global-push-mark 这样的函数来操作全局,实际上也没有这个函数。当我们执行 push-mark 时Emacs会自己考虑是否要加入 global-mark-ring。也就说我们通过 push-mark 和 pop-mark 任意操作,Emacs会保证我们能正确记录。一个好的习惯是涉及到大范围的光标转移不要直接使用 goto-char 就完事了,而是要考虑后续是否需要回来,也就是提前调用 push-mark 记录一下。练习有了这些知识作为铺垫,我们今天来做一个相对复杂一点的练习。我们写一个函数,它主要功能如下:在minibuffer 中列举 mark-ring 中的值,以buffer名称、行号和 point 值的形式显示用户在选择某一项之后跳转到当前位置本次练习我们用到的函数和变量如下:mark-ring:记录当前所有保存的mark记录marker-position : 获取当前mark记录的point位置line-number-at-pos : 计算当前point 在buffer的第几行marker-buffer : 获取当前 mark 记录的buffer 对象buffer-name: 获取对应buffer 的名称前面几个函数比较简单,我们看名称和用法大概能知道怎么用,下面我们重点介绍一个与minibuffer相关的函数: completing-read它的函数原形如下:(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)它用来弹出一个minibuffer的补全菜单,根据用户选择返回对应选项的字符串。PROMPT 是提示词,用于提示用户这个菜单是干什么用的。COLLECTION 是一个列表,它可以存储字符串或者以字符串为 key 的列表/哈希表/符号表,我们这里可以构建一个每项都是 '(提示词 . mark对象) 的关联CELL,后续根据用户选择所关联的 mark 进行跳转至此我的实现思路如下:遍历 mark-ring 取出其中的每个marker针对 marker 调用函数获取buffer名称、行号、位置等信息根据上述信息组织字符串并组成一个 '(字符串 . mark对象) 的关联CELL将关联CELL传入 completing-read,弹出提示根据用户选择提示,获取返回选中的字符串根据字符串查找关联的 mark 对象最后执行 goto-char 跳转到mark对象所对应的位置以下是我实现的代码:(defun my/browser-mark-ring () (interactive) (if (null mark-ring) (message "mark-ring 为空") (let ((candidates '())) (dolist (m mark-ring) ;; 遍历列表 (when (and (markerp m) (marker-position m) (marker-buffer m)) (let* ((position (marker-position m)) (buf (marker-buffer m)) (line (line-number-at-pos position buf)) (bname (buffer-name buf)) (label (format "%s | Line %d, pos %d" bname line position))) ;; 组织对应数据结构 (label . marker) (push (cons label m) candidates)))) (if (null candidates) (message "没有有效的 Mark 位置") (let* ((selected (completing-read "请选择要跳转的位置: " candidates nil t))) (when selected (let ((target (cdr (assoc selected candidates)))) (push-mark) (switch-to-buffer (marker-buffer target)) (goto-char target))))))))最终它的效果如下:总结本篇我们详细了解了 mark-ring 的机制,它就是一个列表,我们可以通过操作列表的方式操作它。并且Emacs自己为每个buffer维护了专属的 mark-ring 和通用的全局 global-mark-ring另外通过这个例子我们知道下面几个函数:mark-ring:记录当前所有保存的mark记录marker-position : 获取当前mark记录的point位置line-number-at-pos : 计算当前point 在buffer的第几行marker-buffer : 获取当前 mark 记录的buffer 对象buffer-name: 获取对应buffer 的名称completing-read : 负责与minibuffer 交互并返回选中的字符串值
2026年08月05日
1 阅读
0 评论
0 点赞
2026-08-04
《elisp 每日一练》—— Region
在关于保存和恢复现场的例子中,我们了解了 marker 相关的概念。 marker 与 point 都是一个表示位置的整数,不同的是marker 是动态的,由Emacs 内部根据buffer内容变化进行维护,而 point 是静态的。它们两个结合可以构成我们今天要讨论的内容——Region知识铺垫在图形界面编辑器中,我们经常用鼠标拖拽高亮一段文字,这叫“选中文本”。而在 Emacs 中,这个概念叫 Region(选区)。Region 是一个区间,它由 marker 和 point 组成的闭区间。我们可以使用这样一个公式来描述区间的组成: $\text{Region} = [\min(\text{point}, \text{mark}), \max(\text{point}, \text{mark})]$也就是说,起始位置是从 marker 和当前 point 中找到最小值,终点位置是二者的最大值。所以我们需要注意,在编写有关 Region 的代码时绝对不能假设使用 marker 或者 point 作为起点,或者终点。实际上为了解决这个问题,Emacs 提供了两个函数 region-beginning 和 region-end 来获取Region的开始和结束位置我们如何激活一个区域呢?可以通过 activate-mark 函数来激活当前选取。又或者可以通过 push-mark 在创建 marker 的同时进行激活。实际上 push-mark 的函数原形为:(push-mark &optional LOCATION NOMSG ACTIVATE)后面几个参数是可选的,它们分别代表:LOCATION: 表示作为新 marker 位置的 point 值,默认是当前光标所在位置NOMSG : 是否在minibuffer 中显示 Mark set 字样, 默认为 nil 也就是不显示ACTIVATE : 是否激活当前 Region,默认为 nil 不激活我们可以用一个例子测试一下(defun my/select-whole-buffer() (interactive) (goto-char (point-min)) (push-mark (point-max) nil t))上述例子我们将整个buffer的内容进行全选。另外,在操作 Region 时我们经常需要判断当前 Region 是否出于激活状态。因为前面介绍过实际上 marker 和 point 天然就构成一个 Region。如果不进行判断,用户想要进行某些操作时会出现不可预料的情况。例如如果用户想在选中区域进行查找,或者在当前行查找,如果不进行判断,有可能用户没有进行选区操作但是查找时却因为隐藏的region 导致查找发生在region中我们可以通过 use-region-p 来判断用户是否激活了选取练习我们实现一个函数,如果用户选择了一个区域,那么将区域中的所有字母大写,否则将光标所在单词的字母转换为大写。将单个单词大写可以使用函数 upcase-region,而将单词大写可以使用 upcase-word。upcase-region 接收一个起始位置和结束位置,将这个区间内的字母都转化为大写upcase-word 接收一个整数,表示将光标所在位置之后(正数)、或者之前(负数参数)的多少个单词转化为大写upcase-word 仅仅只能将光标所在位置到单词结尾位置的字母大写,我们还需要一个方法来获取单词首部,然后通过 goto-char 的方式移动光标到词首,最后执行 upcase-word。当然有了范围也可以使用 upcase-region获取光标所在单词的范围可以使用 bounds-of-thing-at-point。他的函数原形如下:(bounds-of-thing-at-point THING)它支持的 THING 非常多,有 word 、list、symbol、number、filename 等等。这里我们使用 word 就行。它返回的格式是 (START . END)。结合之前介绍的CELL的操作就可以获得START 的值。我们可以做一个快速选择光标所在单词的函数来看看它的用法(defun my/select-the-word() (interactive) (let ((bound (bounds-of-thing-at-point 'word))) (progn (goto-char (car bound)) (push-mark (cdr bound)) (activate-mark))))这里我们确实需要改变mark栈来和当前光标位置组成一个新的区域,所以不适合使用 save-excursion 来保存和还原现场。另外这里也演示了如何通过 activate-mark 来激活一个选区。有了这些函数的辅助,最终我的实现如下:(defun my/upcase-target() (interactive) (if (use-region-p) ;;用户选中区域,转换区域内文本 (upcase-region (region-beginning) (region-end)) ;;用户未选中文本,直接转换单词 (save-excursion ;; 考虑保存和恢复现场 (let ((bound (bounds-of-thing-at-point 'word))) (if bound (progn (goto-char (car bound)) (upcase-word 1)))))))至于如何使用 upcase-region 来实现将光标所在单词转化为大写的功能,读者可以自行尝试。总结本篇我们了解了 Region 的概念,以及可以使用Region做一些事情。后续很多高级的功能都可以依赖region 来实现,例如在选区中搜索、替换文本。今天介绍的函数有:region-beginning 和 region-end 获取选区的区间activate-mark 激活选区use-region-p 判断选区是否激活upcase-word 将光标所在位置之后的单词转化为大写upcase-region 将对应区间的单词转化为大写bounds-of-thing-at-point 获取指定事物所在的区间
2026年08月04日
1 阅读
0 评论
0 点赞
2026-08-03
《elisp 每日一练》——现场保护与还原
在第二天里面,我们介绍了光标移动和行索引区间的api函数,并且学会了如何移动光标。实际上在操作Emacs进行文本编译的动作时很多时候都在移动光标,只是我们感觉不到。如果我们随意的移动光标就容易带来一个问题,我们执行某个操作之后找不到光标在哪了,如果我们还要花时间找光标在哪,就很浪费效率背景知识铺垫如果我们自己实现一个简单的版本,我们可能会考虑使用 let ((old-point (point))) 这样的语句来先保存最后通过 (goto-char old-point) 来进行设置。这个思路是对的,但是它主要的问题是point记录的是当前位置的索引,如果我们在当前光标位置之前插入了字符串改变了光标所在文本的索引,那么根据之前保存的位置值就是一个错误的值。为了解决上一个问题,Emacs引入了 marker 的概念。与 point 不同,marker 是指向 Buffer 中特定位置的指针。如果在 Marker 之前插入或删除了文本,Emacs 的 C 语言底层会自动更新 Marker 的数值,使其依然指向原本对应的那个文字点。我们可以通过 push-mark 和 pop-mark 来保存和恢复一个mark。从名字上可以看出来它是一个栈操作。需要注意的是,出栈时并不会返回栈顶存储的值,我们可以通过 mark 函数来获取栈顶的值。为了清楚的体现第二个区别,我们可以写这样一个例子(defun my/test-point() (interactive) (let ((old-point (point))) (goto-char (point-min)) (insert "this is a test for point") (goto-char old-point))) (defun my/test-mark() (interactive) (push-mark) (goto-char (point-min)) (insert "this is a test for point") (goto-char (mark)) ;; 通过栈顶记录的marker 值来恢复光标位置 (pop-mark))到这里我们似乎解决问题了。我们可以通过管理marker 栈来实现保存和恢复现场。但是我们没有考虑一种情况:如果在push 之后 pop 恢复之前程序出错了,怎么办?我们要为这段代码加一个异常处理机制。到这里我们就可以请出今天的主角了—— save-excursion 。这是一个用C实现的底层函数,用于保证在任何情况下都能恢复现场。你可以将它理解为底层通过try catch 包装了的 push-mark 和 pop-mark。根据这点我们可以知道它需要注意的点:它只用来还原光标现场,而不会还原中间对文本执行的插入、删除操作避免在大的循环中使用,多次进行 push-mark和 pop-mark 会不断制造和销毁临时的marker资源,影响性能它的定义如下:(save-excursion &rest BODY)我们可以将上面的例子做一个改动(defun my/test-mark() (interactive) (save-excursion (goto-char (point-min)) (insert "this is a test for marker")))有了这个函数,我们能从繁琐的现场保存与恢复中解放出来,将注意力集中在具体功能的实现上。练习这次的练习比较简单,我们就简单的为上一次的复制行的函数加一个恢复现场的功能(defun my/duplicate-current-line() (interactive) (save-excursion (let ((line-next (buffer-substring (line-beginning-position) (line-end-position)))) (goto-char (line-end-position)) (newline) (insert line-next))))总结本篇我们讨论了Emacs中保护和还原现场,提到了 marker 和 point。同时也引入了 save-excursion 来保护和还原现场。既然本篇章提到了 marker,Emacs中 marker 和 point 组成了一个很重要的概念—— region。一般我们叫它选区,也就是在编辑器中我们看到的用户选中区域。这部分内容将在后面进行介绍。
2026年08月03日
3 阅读
0 评论
0 点赞
2026-08-02
《elisp每日一练》——行定位与光标移动
在第一天,我们知道buffer实际上就是一个存储文本信息的缓冲区,而光标所在位置就是缓冲区中当前字符的索引。并且这个缓冲区有一个最大索引和最小索引。除非我们要对整个缓冲区进行清空、全选、移动到开头或者结尾位置等操作,一般来讲我们用到全缓冲区的操作并不多,更多的是快速移动光标然后插入字符或者在行间进行跳转。所以今天我们主要介绍行操作和光标移动背景知识铺垫依靠光标所在位置可以定位当前行所在的索引区间,通过函数 line-beginning-position 和 line-end-position 来获取前行行首或行尾的 Point 位置。但是和它们第一天介绍的 point 等函数类似,只是获取对应的索引位置,并不会改变光标位置。如果要改变光标位置,通过 goto-char 来实现依靠这些知识,我们实现一个移动到下一行或者上一行的函数(defun my/goto-next-line() (interactive) (let* ((next-start (+ 1 (line-end-position))) (next-start (min next-start (point-max)))) (goto-char next-start)))上述实现的思路就是通过 line-end-position 定位到当前行尾的下一个位置。但是需要考虑是否是最后一行了。最后再通过 goto-char 移动光标到对应位置。如果我们通过循环或者递归的方式调用该函数就可以实现连续向下移动N行。但是这个功能Emacs也提供了,它的函数名为 forward-line。如果传入的参数N为正数则往下移动N行,为负数则表示向上移动N行。练习在vim中,我们常用的操作是 yy 拷贝当前行然后 p/P 粘贴到下一行或者上一行。今天的练习我们来尝试实现这么一个功能,为了简单我们就不支持拷贝多行了。我们可能用到的API函数有:line-beginning-position 和 line-end-position 用来定位范围buffer-substring 用来拷贝字符串goto-char 移动光标位置newline 插入新行insert 用来在光标所在位置插入字符有了这些铺垫,我们可以实现这个函数了(defun my/duplicate-current-line() (interactive) (let ((line-next (buffer-substring (line-beginning-position) (line-end-position))) ;拷贝当前行所在字符 ) (goto-char (line-end-position)) (newline) ;; 插入新行 (insert line-next)))总结本篇介绍了行操作有关的api函数。并且实现了一个简单的复制当前行到下一行的函数。但是这个例子不太完美,因为我们发现执行玩之后光标停留在下一行的行尾位置,如果我们不希望改变光标位置,让它停留在当期行该怎么办呢?下一篇我们将解决这个问题,来讨论如何恢复现场
2026年08月02日
6 阅读
0 评论
0 点赞
2026-08-01
《elisp每日一练》——point与buffer-substring
之前我根据自己学习Emacs的经验发布一系列的博客来介绍Emacs、ELisp,并且完成了一套相应的配置。但是说实话那些配置也是我在一些论坛之中抄过来的,有的带有些许的个人色彩的改动,只能满足最基本的要求。如果你要我对这些配置做更细的改动,或者要深入具体的插件,做更细化的改动,我目前是做不到的。基于以上水平,我在懒猫的博客 找到更进一步学习elisp 的方法。简单来说接下来要做的就是一页一页的翻看 Emacs Lisp Reference Manual, 一个 api 一个 api 的练习。现在就像初学C语言时只能写写hello world,打印斐波那契数列,或者99乘法表这样简单的程序。想要写更复杂的程序需要学习操作系统、网络原理、数据库、甚至跨专业的知识。我每天上班摸鱼时看看,目前持续了一个月左右,看的内容并不多但是有些收获,所以我打算开启一个新系列,来跟读者分享我的学习心得。我考虑到,如果我的这个新系列也按照 Emacs Lisp Reference Manual 来组织一来是太细也太繁琐了,二来里面的有些内容很多跟我一个水平的读者已经很熟悉了,再介绍一次有些重复了。所以我按照我个人的理解来组织后续的内容。废话不多数我们正式进入正题。本篇我们从最基础的内容开始介绍前提知识提要首先简单介绍一下 buffer 的概念。如果你写过从文本文件中读取文本的简单程序就会理解,如果我们要读取文本首先要做的就是开辟一块内存来存储读出来的内容。你可以将emacs 中的buffer做同样的类比,它就是一块在内存中存储文本的区域,但是通过窗口将这个区域呈现在我们眼前。用C/C++ 的概念来讲,它就是一个 char const*对于这个 char const* 数组来说,想要访问它主要有三个变量需要记录:当前的index、数组开始的索引和最大元素所在的索引,对应于Emacs中的 buffer ,它提供了三个函数 point 、point-min、point-max 分别获取当前光标所在索引,最大索引位置和最小索引位置。我们通过 (buffer-substring START END) 来提取当前buffer中从 start 到 end 之间的字符串练习下面我们通过一个简单的例子来实践、了解这些函数的作用。我们实现一个 my/get-word-at-point 的函数,它用来取光标位置前后各5个字符的字符串并通过message 函数来显示。我们实现的思路如下:通过 (- point 5) 和 (+ point 5) 来获取光标前后各5个字符的位置。通过 buffer-substring 来提取数据使用message 打印结果(defun my/get-word-at-point () (interactive) (let* ((curr (point)) (start (max (point-min) (- curr 5))) (end (min (point-max) (+ curr 5))) (substring (buffer-substring start end))) (message "%s" substring)))总结本次我们简单介绍了 buffer 的基础概念,同时重点介绍了三个获取位置相关的函数 point 、point-min、point-max。以及用来做练习的 buffer-substring 函数。下一篇我打算介绍行操作有关的内容,行开始的位置和结束的位置可以通过 line-beginning-position、line-end-position。读者可以提前考虑一下怎么通过这两个函数完成复制当前行的内容
2026年08月01日
4 阅读
0 评论
0 点赞
2026-04-26
wsl 迁移
之前我使用Linux的方式主要是在wsl 中。WSL 中的Linux系统默认放在了C盘,随着使用频率的增加,里面会充斥各种环境、代码、以及其他的内容,最终导致占用磁盘飙升。因为最近我发现我C盘已经飘红了,所以我需要将其中Linux系统从C盘移动到其他盘,目前我就D盘还有大量的空间所以这里我将它移动到了D盘。迁移在移动之前我们先要确保系统处于关闭状态。这里使用如下命令确认它们的状态wsl --list --verbose这条命令会输出当前系统中所有的Linux子系统的名称和对应的状态,我们需要保证对应的系统状态为 Stopped如果处于运行状态,我们可以使用wsl --terminate Arch其中最后的 Arch 就是上一步看到的系统名称停止之后,我们可以使用命令wsl --export Arch D:\WSL_Storage\Arch.tar后面两个参数分别是前面显示的系统名称,和需要备份保存的位置之后我们需要删除原先位于C盘的子系统,使用命令wsl --unregister Arch最后我们从之前备份的位置导入并生成一个新的子系统wsl --import Arch D:\WSL_Storage\Arch D:\WSL_Storage\Arch.tar这条第一个参数Arch代表的是新系统的名称。第二个参数表示系统内容存放的目录。第三个参数是从哪里导入系统数据。再次打开wsl的子系统我们会发现它采用的是root账户登录我们需要默认采用之前配置的普通用户。我们可以使用命令Arch.exe config --default-user archArch.exe 是之前子系统的名称,后面是设置该系统默认的登录用户,这里我仍然使用之前的用户名进入系统之后,我发现终端上之前配置的一些Unicode emoji 符号失效了显示乱码。这是因为我们相当于重新建立了一个子系统,在Windows Terminal 中为这个新系统配置的字体是默认的字体,我们需要在Windows Termianl中为这个新系统选择一个Unicode 字体。之后我们会看到一切恢复如初了
2026年04月26日
49 阅读
0 评论
0 点赞
2026-02-01
Emacs折腾日记(三十六)——打造个人笔记系统
在前面我介绍了如何使用 org mode 来实践 gtd 的理念。其实org mode 和其他工具的结合可以打造一个强大的个人笔记系统嵌入 plantuml作为程序员,对 uml 自然不会陌生,虽然时至今日可能有些显老,但是对我来说它仍然是我不可或缺的工具。而 plantuml 是一种将文本转化为图片的工具。我们可以在 plantuml 入门 找到对应的安装步骤。对于archlinux 来说,我需要首先保证java 安装sudo pacman -S jdk21-openjdk根据 plantuml 的官方介绍,只需要jdk8就可以运行,但是我比较喜欢安装最新的版本。plantuml 中某些图需要依赖 graphviz 来生成,所以这里需要再安装一个 graphvizsudo pacman -S graphviz完成了这些基础组件的安装,下面我们就可以下载 程序这个 jar 包放在哪里都可以,既然是Emacs配合,那么我打算将它放置到 ~/.emacs.d/lib 中作为功能的依赖库我们可以使用官网的测试用例来测试一下是否正常@startuml Alice -> Bob: test @enduml将上述文本保存为 test.txt。然后执行 plantuml.jar -jar test.txt。默认在当前目录中生成同名的png图片。如果显示正常,那么我们就可以进行Emacs的改造工作了根据 官方 的文档,针对Emacs,它提供了名为 plantuml-mode 的扩展插件。我们可以通过以下简单的配置来进行org和plantuml 的联动(use-package plantuml-mode :ensure t :mode ("\\.puml\\'" "\\.plantuml\\'") :config (setq plantuml-default-exec-mode 'jar) (setq plantuml-jar-path (expand-file-name "~/.emacs.d/lib/plantuml.jar")) ;; 设置plantuml jar包的位置 ;; 让org代码块能识别plantuml语法 (add-to-list 'org-src-lang-modes '("plantuml" . "plantuml")) (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t))) (setq org-plantuml-jar-path plantuml-jar-path) )我们还是可以用官方给的示例来看看具体的效果#+begin_src plantuml :file demo.png @startuml Alice -> Bob: test @enduml #+end_src这里必须通过 :file 来指定生成图片的链接这里可以为 plantuml 做一个代码片段以便快速进行进入画图的流程。具体细节就不再深入介绍了。各位读者有兴趣可以自行探索。另外关于画图的一些其他技巧和配置,可以参考 面向产品经理的Emacs教程:15. 在Org mode里用纯文本画图构建笔记系统我个人习惯使用双链笔记,简单来说它就像wiki一样随意插入链接,各种知识结构是一个网状的。传统的笔记是树状结构(这里主要是指马克飞象那样的笔记软件对笔记的组织形式),某一条笔记输入某个单元,而这个单元又属于某个父级单元中,就像一本书一样。但我们在学习的过程中,很难在一开始就把知识整理成体系,而是先零散学习,之后随着知识面的增加逐渐形成体系。另外有些跨学科的知识可能会在多个地方被提及到,就像芒格说的跨学科思维。我们无法将某条知识仅仅归于一个大类里面。而当前双链笔记它是没有层级的,它是一个网络结构,任何知识都可以随意引用其他知识。更符合我们的认知习惯,学到新知识了先记下来,未来知识成体系了可以通过链接随意将它放置到任何体系下。在Emacs中可以使用 org-roam 插件来实现(use-package org-roam :ensure t :after org :init (setq org-oram-v2-ack t) :config (org-roam-setup) :custom (org-roam-directory "~/org/roam/") :bind (("C-c n f" . org-roam-node-find) (:map (("C-c n i" . org-roam-node-insert) ("C-c n o" . org-id-get-create) ("C-c n t" . org-roam-tag-add) ("C-c n a" . org-roam-alias-add) ("C-c n l" . org-roam-buffer-toggle)))))在 org-roam 中,一个文件就是一个note,我们可以通过 org-roam-node-find 来打开或者新建一个节点。新建的文件会被保存到我们定义的 org-roam-directory 目录中。在我们编写笔记的时候如果需要关联另一个笔记,可以通过 org-roam-node-insert 在随意位置插入对另一个文件的引用。当我们对知识有了一定的理解之后可以通过 org-roam-tag-add 来添加一些标签方便我们日后查找。另外有些时候我们组织某个知识点时,它下面有一些小的知识点,我将它们作为当前文件中的一个子标题,日后如果希望能链接到这个子标题,我们可以在子标题上使用 org-id-get-create 来创建。请记住在 org-roam中无法直接链接标题和子标题,它实际链接的是一个id,我们在创建新的知识点时使用 org-roam-node-find 本身就完成了创建id的过程。另外我们可以通过 org-roam-ui 来将笔记的节点进行可视化(use-package org-roam-ui :vc (:url "https://github.com/org-roam/org-roam-ui" :rev :newest) :after org-roam :config (setq org-roam-ui-sync-theme t org-roam-ui-follow t org-roam-ui-update-on-save t org-roam-ui-open-on-start t))在Emacs 29及以上版本内置了通过github下载的功能,mepla 本身没有提供org-roam-ui 包,所以这里我使用内置的从GitHub下载的功能。在安装好之后可以通过 org-roam-mode 来开启笔记节点的可视化。它会创建一个web服务并打开浏览器访问 http://127.0.0.1:35901/具体的细节可以查看它的官方文档org-roam-ui因为我的笔记暂时都记录在 obsidian 中,还没有迁移过来,暂时不贴我的截图了。总结到此为止对我来说Emacs已经可以成为日常使用的代码编辑器、笔记管理、日程管理软件了。所以我的折腾就暂时告一段落了。但是这并不意味着这个系列的完结。后续如果当前的配置有问题或者我看到好的点子,又或者自己有什么想法实践之后觉得不错的也会更新到这个系列中。但是这个系列不会像现在这样大规模的更新了。我个人对Emacs的了解并不深入,当前的配置也仅仅是一个可用的状态。但是在编写此系列中仍然受到许多读者的喜爱,在这里感谢各位读者的支持与鼓励。在前面我的博客出现错误或者我有疑惑时也有比我强的读者给出意见,指出我的问题,在这里对他们进行感谢。终于从对Emacs的一知半解到拥有了自己的一套配置,虽然不完美甚至显得幼稚,但是在这个折腾的过程中我收获许多,下一阶段我想实践一下 懒猫说的认真读一读 Elisp reference manual 加深自己的理解。最后列举一下我在这个系列中参考的一些教程专业Emacs入门面向产品经理的Emacs教程21天学会Emacs还有其他一些我引用了但是忘记了具体链接的博客或者教程。
2026年02月01日
29 阅读
0 评论
0 点赞
2026-01-29
lazygit 规范提交记录
背景随着项目的进程,我们经常面临一个问题:发现之前的代码有bug,但是我不知道当初为什么这么写,如果改了会影响哪些?会不会把原来改好的bug又改出来了。我们可以通过 git 的提交记录来查看当初为什么改的。但是 git 提交记录的增长,一个文件提交记录可能有成千上万,要是从头到尾找一遍不知道要找到什么时候。更糟糕的是,好不容易找到了结果提交记录就一条 update at 2026/01/29。这种情况着实令人抓狂。要防止这种情况,我们可以从两个方面着手:要求整个团队规范git 的提交记录在IDE中能快速找到每行代码对应的提交记录规范提交记录git 原版的提交信息模板提交记录我们可以采取国际通用的 Conventional Commits (约定式提交)。它的格式如下:<类型>(影响范围): 一句话总结 <空行> [正文:详细解释为什么这么做,解决了什么痛点] <空行> [脚注:关联的任务单号 ID]正文部分我希望用 Why、How 这两个关键词,也就是为什么要改,如何改。git本身支持自定义 commit 信息的格式,我们可以将一个模板添加到 ~/.gitmessage。然后通过命令git config --global commit.template ~/.gitmessage来指定使用定义的模板,这里我定义的模板如下:<type>(scope): <subject> # --- 为什么修改 (Why) --- # 描述导致问题的现象,或为什么要增加这个功能 # --- 解决方案 (How) --- # 简述核心算法或处理逻辑 # --- 关联单号 --- # Fixes: #这里的 <type> 可以是修改的类型,这个部分是必须的,我一般喜欢定义这么几种类型bugfix (修改bug)feature (添加新功能)doc (更新文档或者注释)forspell (拼写修改)scope 代表的是影响范围,可以根据项目情况灵活的定义,例如在一个前后端分离的项目中,可以定义范围为UI、数据传输、权限等等模块最后的 subject 就是一句话总结,例如"修改普通用户可以访问其他用户隐私文件的bug"后面我可以通过 git commit 来触发模板,后续通过git 默认的编辑器(一般是vi 或者 nano)。lazygit 的配置lazygit 本身也支持自定义配置,它主要通过 config.yml 文件配置,默认的配置文件位置如下:Windows: %LOCALAPPDATA%\lazygit\config.ymlMacOs: ~/Library/Application Support/lazygit/config.ymlLinux: ~/.config/lazygit/config.yml我们可以通过一个命令快捷键触发一个规范化提交的功能。用户自定义命令的模板可以在这里找到。它以 customCommands 作为根节点。后面接 key,command 和 prompts。各个部分的含义如下:key: 用来触发命令的快捷键command: 真实触发的命令prompts: 触发时的行为prompts 是另一个根节点,用于定义详细的行为。它的子元素如下:type: 输入项的类型,有 menu 表示下拉列表框;input代表输入框;menuFromCommand根据用户提供的外部shell命令来生成一个下拉列表框title: 输入框的标题,提示我们这个框用来输入什么信息key: 在command中,需要填入一些数据,我们暂时利用占位符来表示,key代表的是某个具体占位符,需要与占位符对应如果我们的类型是 menu 的话,还需要利用 options 标签来表示具体的选项。最终我的配置如下:customCommands: - key: 'X' command: "git commit -m '{{.Form.Type}}{{.Form.Scope}}: {{.Form.Subject}}' -m 'Why: {{.Form.Why}}' -m 'How: {{.Form.How}}' -m '用例文档或者jira单: {{.Form.TestCase}}'" context: 'files' description: '规范化提交 (Gitmoji + Scope)' prompts: - type: 'menu' title: '选择提交类型 (Type)' key: 'Type' options: - name: '✨ feat (新功能)' value: '✨' - name: '🐛 fix (修复Bug)' value: '🐛' - name: '🚀 更新流水线或者部署脚本' value: '🚀' - name: '📝 docs (文档修改)' value: '📝' - name: '⚡ perf (性能优化)' value: '⚡' - name: '🎨 style (格式/美化)' value: '🎨' - name: '🍎 修复苹果系统上的问题' value: '🍎' - name: '🐧 修复linux 系统上的问题' value: '🐧' - name: '🏁 修复Windows上的问题' value: '🏁' - name: '🤖 修复安卓上的问题' value: '🤖' - name: '⬆️ 升级依赖' value: '️⬆️' - name: '⬇️ 降低依赖' value: '⬇️' - name: '♻️ 代码重构' value: '♻️' - name: '➕ 添加依赖' value: '➕' - name: '➖ 删除依赖' value: '➖' - name: '⏪ 代码回滚' value: '⏪' - name: '🔀 代码合并' value: '🔀' - name: '👽 因外部API改动而更新代码' value: '👽' - type: 'menu' title: '选择影响范围 (Scope)' key: 'Scope' options: - name: 'layout' value: '(layout)' - name: 'render' value: '(render)' - name: 'data' value: '(data)' - name: 'none (无特定范围)' value: '' - type: 'input' title: '简短总结 (Subject)' key: 'Subject' - type: 'input' title: '为什么修改 (Why)' key: 'Why' - type: 'input' title: '具体做法 (How)' key: 'How' - type: 'input' title: '用例文档或者jira单 (TestCase)' key: 'TestCase'在command 中利用git命令来生成一条记录详细提交信息的内容。{{}} 中包裹的都是占位符, .Form.Type 表示这部分内容来自用户后续提交的表单项 Type 中的内容。后续在 prompts 中某一个key的名称需要为 Type 以便进行对应上述提交的内容我仍然采用 Conventional Commits 的格式,首先 type 部分我采用 gitmoji 中规定的符号来表示提交的类型。影响范围我根据我当前的项目模块暂时定了 layout、render、data 等范围。正文部分我提供了三项,即 Why、How、TestCase表示为什么这么改,可以描述一下bug现象,产生的原因。How 表示如何修改的,可以简短的描述一下算法或者具体修改项。最后加上一个用例或者bug管理系统中的单子,因为我公司采用的是jira,所以这里我可以关联上jira单号IDE 中查看提交记录因为我在公司中主要采用 Visual Studio 和 Visual Studio Code,所以这里主要介绍它们上面可以使用的插件,至于我钟爱的NeoVim 和 Emacs,我还没来得及研究,暂时不介绍它们的配置了Visual Studio 中可以使用 Git Line Blame 插件。Visual Studio Code 上可以使用 GitLens 它们的作用都是显示光标所在行对应的提交记录。它们的效果各位读者可以自行到插件官方文档中找到截图。我们在上面记录了测试用例或者bug 单子的另一个好处时可以根据测试用例和bug单快速查找与之相关的提交记录。可以使用下列命令git log --grep jira-111实际上它就是一个 grep 过滤,如果使用管道加 grep ,它只会找到对应的输出无法关联到具体的提交记录,但是通过git log 提供的grep它会显示匹配上的具体的提交记录到此我觉得已经可以解决我个人的问题了,不知道上述内容对各位读者是否有用。各位读者如果有更好的想法可以在评论区留言,欢迎读者给我介绍新的解决思路
2026年01月29日
19 阅读
0 评论
0 点赞
1
2
...
12