在前面我们介绍了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 恢复调用之前的可见区间
在关于保存和恢复现场的例子中，我们了解了 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 获取指定事物所在的区间
在第二天里面，我们介绍了光标移动和行索引区间的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。一般我们叫它选区，也就是在编辑器中我们看到的用户选中区域。这部分内容将在后面进行介绍。
在第一天，我们知道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函数。并且实现了一个简单的复制当前行到下一行的函数。但是这个例子不太完美，因为我们发现执行玩之后光标停留在下一行的行尾位置，如果我们不希望改变光标位置，让它停留在当期行该怎么办呢？下一篇我们将解决这个问题，来讨论如何恢复现场
之前我根据自己学习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。读者可以提前考虑一下怎么通过这两个函数完成复制当前行的内容
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还有其他一些我引用了但是忘记了具体链接的博客或者教程。
在上一篇文章中，我们简单介绍了 gtd 的一些理念，并且也通过org capture 完成了 gtd 中收集的操作。gtd分为收集任务、整理、执行、回顾。本篇我想通过org todo 来聚焦整理和执行这两个步骤整理上一篇文章中，我们通过org capture 收集到了一些任务，针对这些任务我们还需要给每个任务安排优先级并且根据优先级来确定开始执行的时间。显示待办事项我们在前面的模板中，待办任务前面都是以 TODO 开头，这里的TODO就是org 中待办事项的一个标识。我们可以通过 org-agenda 来查看相关的任务。打开该页面之后，它列举出了所有的支持的一些快捷键。例如使用 t 显示所有的待办事项，使用 m 来根据 PROP、TODO、TAGS 来查找任务。但是第一次使用时会发现自己兴冲冲的咔咔一顿添加任务，结果到头来通过 org-ageda 的 t 来列举之前添加的待办任务毛都没看到，顿时好奇兴奋的心情一下跌倒谷底。别着急这是因为我们没有告诉 org-agenda 该去哪里找这些待办事项。我们可以通过 org-agenda-files 来告诉org-agenda 这个信息就像名称显示的那样 org-agenda-files 是一个列表成员，我们指定它去这些文件中查找待办事项； (use-package org-agenda :ensure nil :custom (org-agenda-files (list "~/org/reading.org" "~/org/blog.org" "~/org/working.org")))这里我设置了三个 org 文档，这三个文档分别记录待读书目、需要完成的博客以及工作任务。需要注意的是我们需要这几个文件真实存在否则就会报错。设置好了这个变量之后效果如下:设定优先级列举出所有任务之后，针对每个任务我们可以考虑如何安排这些任务，首先需要的就是考虑给它从那四个维度安排一个优先级。上面说到我们可以使用A、B、C、D 做一个标识，这里我规定优先级的顺序是:A > B > C > D为了达到这个需求，首先我们要解决的是优先级的定义问题，因为Emacs默认只支持A/B/C这三种优先级，我需要它能支持4种。Emacs种有两个变量分别控制优先级最大值和优先级最小值，它们是 org-highest-priority 和 org-lowest-priority。另外还有一个变量可以设置默认的优先级 org-default-priority。理论上它可以设置优先级从A到Z，但是过多的优先级并不适合。我们可以在上面的 :custom 下使用下列的语句进行设置 (org-highest-priority ?A) (org-lowest-priority ?D) (org-default-priority ?D)设置完成之后可以使用 S-UP (Shift + 上箭头)或者 S-Down(Shift+下箭头)来调整优先级设置计划执行时间和结束时间安排好了优先级，我们就需要为每个任务安排时间了，这里可以安排计划开始时间和结束时间。开始时间和结束时间在 org agenda 中分别是命令 org-agenda-schedule 和 org-agenda-deadline。它们被绑定到了快捷键 C-c C-s 和 C-c C-d上面。当我们通过快捷键或者 M-x 调用这两个命令的时候，Emacs会弹出日历菜单供我们选择日期，默认提供了三个月的日期可供选择。如果不够的化，日历下方两个各有一个箭头可以向上或者向下再显示一个月。另外可以在下方 mini-buffer 处输入 +1d 这样的字符表示在当前时间的1天以后，根据这个规律我们可以输入 + 后面跟任意的数字，然后后面加 d/w/m/y 来表示天、周、月、年。设置任务属性和标签有些任务，特别是工作中的任务我们自己可能无法单独完成，例如某个bug需要前端或者后端一起完成又或者这个bug不光前端要改后端也需要改，这个时候我们修改完了自己的部分，这个时候可以显示我们将任务派发给了其他同事。又或者这个bug牵扯到其他bug，又或者有时候需要开会或者整理需求文档，这个时候我们需要记录该任务的场地例如对于会议可以记录一下在 D-01 会议室。这些可以通过设置属性和标签来描述这些信息。我个人认为它们二者在记录任务的额外信息时没有什么大的区别。主要区别在显示方面，标签一般于TODO 信息展示在一行，而属性默认是被折叠起来的，所以对于一些简要的信息，例如任务目前是谁在处理、开会的地点这类信息我习惯放置到标签上、而属性放置一些不那么重要的信息。对于标签，我们可以在设置任务时，在任务后面使用 :TAG1:TAG2: 这种方式添加多个标签。在前面介绍 org-capture 的文章中我给出了一个创建工作任务的模板 * TODO %^{任务描述} :%^{任务类型|dev|bugfix|env|doc|meeting}:\n SCHEDULED: %^t\n PRIORITY: %^{优先级|A|B|C|D}\n %?\n %i"。任务类型部分就是一个标签,这里我只设置了一个标签。我们可以事先使用 org-tag-alist 来规定一组标签以及打上这个标签所需要的快捷键。这个变量是一个列表类型，每个列表包含一个cell，cell的第一个元素是一个标签的名称，第二个元素是对应的快捷键。同时它也可以通过 :groupstart 和 :groupend 来定义一组互斥的标签项。例如针对任务我们可以定义如下的标签 (org-tag-alist `((:startgroup) ("dev" . ?d) ("bugfix" . ?B) ("env" . ?E) ("doc" . ?D) ("meeting" . ?M) ("reading" . ?R) (:endgroup) ("Sendto Tom" . ?T) ("Sendto Jerry" . ?J) ))需要注意这里标签过多的话，我们需要避免出现快捷键重复的情况。设置了标签，我们可以使用 C-c C-q属性是放到 :PROPERTIES: 下的一组键值对，默认情况下是被折叠的。属性相对于标签来说更加灵活，能显示的信息也更加多样化。我们可以使用 C-c C-x p 来设置属性设置子任务有时候一个大的任务分为几个小任务，例如某个bug可能包含几个方面的问题甚至需要不同组的开发人员进行协调。这个时候我们可以将一个任务分解为几个子任务org mode 中对于子任务的表现比较简单。子任务就是任务下的一个次级的 headline。我们可以在主任务后面加上 [/] 或者 [%] 来根据子任务的完成情况自动更新主任务的完成进度从上面的截图上看，关于子任务还有两个问题没有解决，首先在将任务状态由TODO 切换到DONE时，checkbox的状态应该是勾选的。第二个问题就是当所有的子任务都完成之后应该将主任务的状态改为done。这些我暂时没有找到好的解决方案，所以不展开说明了。任务状态切换上面我们看到可以通过 C-c C-t 来切换状态，但是我们只能在TODO|DONE之间进行切换。以我个人的习惯来说，我还需要几个关键字:WAIT: 等待，一般是任务完成需要等待其他人的配合，例如bug的产生可能是多个模块共同作用，需要等待其他人修改完ABORT：终止DOING: 正在执行这里我说的是关键字，但是它并不代表org todo 中任务实际的状态。事实上org todo中只有两种状态，TODO和DONE表示未完成和终结，我们无法在中间插入新的状态，能做的也只有在两种状态之间插入一些新的关键字。插入新的关键字可以通过设置变量 org-todo-keywords。根据Emacs的文档显示，org-todo-keywords 原始的值为 ((sequence "TODO" "DONE"))。因为这里需要一个有序列表，也就是说Emacs会保证状态切换时按照我们定义的顺序进行切换，并且切换到最后一个关键字时会认定任务已经处于终止状态，后续可以针对这个终止状态来进行一些操作。所以这里需要通过 sequence 函数来保证设置的是一个有序的列表这里我们针对终止状态设置了两个关键字DONE和ABORT，我们可以通过在中间插入 | 的方式告知Emacs，| 之前的关键字是未完成的状态，| 之后的是终止的状态。我们可以在 :custom 下加入 (org-todo-keywords '( (sequence "TODO(t)" "DOING(i)" "WAIT(w@/!)" "|" "DONE(d!)" "ABORT(a@)") ))) 这里后面括号中的字母表示可以在 C-c C-t 之后按字母快捷键快速选择状态，@表示需要由用户填写理由，例如针对等待的任务可以写明需要等待某个子模块修改完成。而后面的 !表示会插入一条包括时间戳的记录。至于 / 则是中间的一个分隔符现在它的效果如下:接入番茄工作法前面介绍了关于任务的定义和关键字的切换，现在我们的流程可能就变成了:通过 org-capture 收集任务将分析任务确定任务的优先级以及计划执行和结束时间通过org-agenda 查看当天需要执行的任务执行任务任务完成之后切换状态在执行任务的过程中我们可以采用番茄工作法来保证时间的高效利用。Emacs中可以嵌入有关番茄工作法的插件来做到。这里推荐使用 org-pomodoro 插件(use-package org-pomodoro :ensure t :custom (org-prmodoro-length 25) ;每个番茄钟25分钟 (org-pomodoro-short-break-length 5) ;; 短休5分钟 (org-pomodoro-long-break-length) ;; 长休15分钟 (每4个番茄钟之后) (org-pomodoro-play-sounds-t) ;; 启用提示音 )在使用时我们可以将鼠标放置到需要执行的任务上，然后执行命令 org-pomodoro。如果出现打扰的情况，例如有同事突然找到你，希望你提供帮助或者外卖电话到了要去拿外卖这个时候可以通过再次执行 org-pomodoro 停止计时。我们可以看到在番茄钟启动之后下方的 minibuffer 中有番茄钟的倒计时。到此为止，我们现在自建的任务管理系统已经可以到执行这步了，这个系统后续只剩下如何进行进行归档与回顾了。关于这部分将在下篇文章中给出我的一些实践，谢谢各位读者的关注
在日常的工作生活中有各种各样的琐事，如果不及时记录下来很可能后面就忘了。或者在日常软件开发中有很多想法想要实现但是时间一长自己就忘了。这种情况下我们需要一个系统来记录收集想法并且后续需要追踪甚至回顾。我们需要一套适合的管理系统让大脑摆脱待办事项的纠缠，让大脑专注于当前要做的事物。目前我采取的方式是GTD+ 番茄工作法。GTD 简介GTD（Getting Things Done），是由戴维·艾伦于2002年提出的时间管理方法。其核心在于通过收集、整理、组织、回顾、执行五个步骤，将任务从大脑中移出，减轻心理压力以提升效率，强调两分钟内可完成事项立即处理的原则。用我的理解，我们需要事先准备几个速记的笔记本，一旦脑海中出现了想法就将它记录下来，这步叫做收集。在一天中的空闲时间例如下班前或者睡觉前打开看看之前记录的内容，将内容按照一定的规则重新组织例如分为工作、生活、学习之类的想法，将它们转化成可以执行的任务。并且为每个事项定一个紧急程度，这个就是整理、组织。在每天开始的时候或者结束的时候可以根据上面的分类安排一下当前或者明天的任务，这个是执行任务。在每周或者每天的时候回忆一下当前的任务看看哪些完成了、哪些未完成、从任务中有何感想，有哪些可以记录下来作为重要的日志或者日后博客的灵感来源，这个就是最后的回顾总结过程。番茄工作法再完美的系统，如果不执行或者不认真执行也是白搭的。对我来说GTD主要解决的是要做哪些事，如何安排时间的问题。至于如何高效的执行任务专注到任务上，我采用的番茄工作法。番茄工作法的简单理解就是将任务的时间拆分成几个不可分割的最小单元，在这个时间片单元内一心一意、专心到任务上。一旦分心就需要终止当前番茄钟，并记录分心的原因，如果是有新来的新任务就需要采用记录的方式记录，如果是紧急且重要的任务可能还要修改当前的任务计划。当然番茄工作法有一整套的收集、整理、执行、归纳总结的流程，但是对我来说我喜欢GTD的流程和番茄工作法的时间管理方式，所以我采用的是它们二者的结合。但是主要用在工作中，日常生活中的场景都是一些简单的琐事，例如收拾卫生、做饭、对我来说不合适这种形式。org capture目前来说市面上没有完全符合我心目中最佳时间管理理论的工具，但是Emacs中的org 是十分强大的，完全可以稍加改造成符合自己心意的时间管理系统。首先要解决的就是如何收集想法的问题。这个功能可以使用 org-capture，顾名思义它就是使用org的格式来快速记录当前的想法。我们使用 org-capture 命令可以看到下面的界面下方提示我们可以使用前面的字母符号来选择一个模板，默认只提供了一个名为 Task 的模板。我们可以使用 org-capture 中 来实现GTD中的分类记录的功能。在我们选定需要使用的模板之后，Emacs会调用 org-capture-select-template 函数来选中模板并且根据模板来创建默认的org 模板。默认的Task 模板如下：("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n %u\n %a")它们的含义如下t 标识选择模板时的快捷键。Task 模板的描述，让用户了解当前模板作用的说明文字。后面一个部分就是新增的内容新增的内容比较复杂，我们一个个的说。第一个部分是新增模板的type。type 可以是下面的内容type描述entry带有 headline 的一个 Org mode 节点item一个列表项checkitem一个 checkbox 列表项table-line一个表格行plain普通文本type 的不同，后面跟的内容也不一样。例如如果当前是 entry 的话，后面需要添加 "* headline" 作为标题行的形式，如果是 item 后面需要跟上 - item 作为一个项。如果是 checkitem 需要跟 [ ] item 作为一个checkbox项。如果使用 table-line，需要跟 | colum 1 | colum 2 | colum3 | 作为一个表格项目。如果是 plain 则没有特殊要求。接下来看看 (file+headline "" "Tasks") 中各个部分的含义，前面的 file+headline 表示将记录的内容保存到哪个位置。除了这两个标识还有其他的一些常用标识标识含义file文件id某个特定id的headlinefile+headline某个文件中的某个headlinefile+olp文件中某个headline的路径file+regexp文件以及被正则匹配的headlinefile+datetree文件中当日所在的datetreefile+datetree+prompt文件中的datetree 会弹出日期选择file+weektree文件中当日所属的 weektreefile+weektree+prompt文件中 weektree 会弹出日期选择file+function文件中被函数匹配的headlineclock当前正在计时中的任务所在位置function自定义函数的所定义的位置后面两个参数根据前面的内容来决定，例如如果使用 file+headline 的话，第二个参数就是文件路径，第三个参数就是具体的 headline 的名称。如果文件名参数为空的话，那么会使用 org-default-notes-file 所对应的文件路径为 ~/.notes 这些内容如果觉得有些比较难以理解的，可以查看 org-capture-tempetes 变量的详细说明"* TODO %?\n %u\n %a" 部分都是一些格式字符串和占位符，在选中模板之后会自动生成我们想要的模板文字。这里的占位符如果写过代码应该很容易理解，首先占位符都是 以%开头，后面根据不同的字符表示不同的含义时间相关的占位符占位符含义%<>表示自定义时间的时间戳，两个<>之间可以使用字符表示不同的时间格式，例如 %<%Y-%m-%d> 表示年月日的格式，年是4位字符的形式%t当前仅包含日期的时间戳%T当前包含日期时间的时间戳%u包含当前未激活的时间戳%U表示当前未激活的包含日期时间的时间戳%^t类似于 %t，但是会弹出日历供用户选择%^T类似于%T，但是会弹出日期和时间供用户选择%^u与上面的类似%^U与上面类似这里所谓的激活，在Emacs的文档中使用的词是 interactive 我不知道翻译为激活是否准确。未激活的时间戳日后不会出现在 org-agenda 中，至于什么是 org-agenda 请继续往下看剪切板相关的占位符占位符含义%c当前 kill ring 中的第一条内容%x当前系统剪贴板中的内容%^C交互式地选择 kill ring 或剪贴板中的内容%^L类似 %^C，但是将选中的内容作为链接插入后面还有更多更复杂的占位符，但是目前来说我用不到那么多，也就不一一列举了，各位读者如果感兴趣可以查看 org-capture-templetes 来查阅更多类型的占位符，至于 %?它什么也不往文件里面添加，它会在模板文本添加成功后将光标移动到它所在位置，方便用于自己输入内容除此以外，它还支持自定义函数的形式来插入内容，它的形式为 %(sexp) 括号中间就是lisp函数，假设我们有一个 custom-placeholder 那么这里就可以写成 %(custom-placeholder) 有了这些内容我们就可以定义自己的各种任务的模板个人日常任务模板对我个人来说，日常任务主要有：阅读清单、博客计划、工作任务。下面我希望通过这三大类任务来定制一些模板阅读清单首先是阅读清单，阅读清单主要记录一些我通过各种渠道了解到的并计划在后续需要读的书。根据我之前读的《如何有效阅读一本书：超实用笔记读书法》 的说法，记录要读的书，可以记录下面几个内容:书名作者出版社其他(可以是ISBN号)如果只根据书名可能会有书名重复的现象，所以根据这几条信息就可以唯一确认一本书因为Emacs提供的默认模板我不太喜欢，所以这里我先使用 (setq org-capture-templates nil) 来清空模板，后续由我自己实现各种模板(use-package org-capture :ensure nil :custom (org-capture-templates nil) (org-capture-templates '(("r" "Reading List" entry (file+olp "~/org/reading.org" "Reading" "Book") "*** TODO 《%^{书名}》\n作者:%^{作者}\n出版社:%^{出版社}:\n备注:%^{备注}\n添加时间:%U\n" :clock-int t :clock-resume t))))我们按照之前分析的模板中各个部分的组成来看看它是什么意思。首先第一个部分，我们定义通过快捷键 r 来选中并生成这个模板，Reading List 表示在模板展示时显示的内容。后面的entry 表示我需要添加一个带有 headline 的 org mode 节点，它后面跟着的子列表里面使用 file+olp 表示我需要在文件中的某个headline 下添加这条记录作为它的子内容部分。后面跟的就是文件路径、以及需要插入到当前文件下Reading 里面的 Book 下，作为子节点。最后定义的就是具体的内容了, 因为Reading 作为一级标题，它下面的 Book 作为二级标题，所以这里添加的阅读清单记录就是三级标题，所以我在它前面加了一个 *** 作为三级标题。前面的TODO是关键词，表示待做。后面就是输入的常规的内容了，包括出版社、作者等信息。它的运行效果如下:新建博客文章org-mode实际上是支持转换成markdown并通过hugo之类的静态网站生成程序来生成博客的。对于hugo 来说如果希望将markdown格式的内容转化成博客，我们需要在markdown文件头位置加入这些内容title: "Test" date: categories: blog tags: blog draft: true根据这些信息我们可以很容易的搭建基础的模板。针对这种场景，我希望它能在固定的位置生成一个以时间命名的 .md文档，这个位置一般位于hugo安装目录下的 posts 目录下。我们可以写下这样的配置 (add-to-list 'org-capture-templates `("b" "Blog" plain (file ,(concat "~/org/" (format-time-string "%Y-%m-%d.md"))) ,(concat "---\n" "tile: %^{标题}\n" "date: %U\n" "categories: %^{分类}\n" "tags: %^{标签}\n" "draft: %^{草稿|true|false}\n" "---\n" "%?")))它的效果如下:最终在我们保存之后它会在~/org 目录下生成一个以时间命名的markdown文件这种模式我个人不太喜欢，因为对我而言org-capture 仅仅只是一个收集的动作，我们应该快速记录想要写的博客以及博客的主要论点，而不应该开始编写具体的内容。至于如何使用org-mode来写博客，后面我想单独写一篇博客来介绍创建工作计划作为一个程序员来说，日常工作总会与程序打交道，比如开发新功能、写接口文档、修改bug、搭建开发测试环境、开会等等。有时候需要一个人负责多个多个项目。对于工作任务我们还需要分一个轻重缓急，我一般习惯于将它们分为：重要且紧急、重要但不紧急、紧急但不重要、不重要不紧急。那么对于工作任务的需求就很明确了。首先任务可以归属到不同的项目中，日后可以根据任务进行筛选任务可以细分为开发新功能、修复bug、搭建环境、写文档、开会等等任务可以划分轻重缓急日后通过todolist可以看到任务的轻重缓急，目前使用重要且紧急、重要但不紧急、不重要但紧急、不紧急不重要我们采用下面的代码 (add-to-list 'org-capture-templates `("w" "Work Task" entry (file+headline "~/org/working.org" "Work") "* TODO %^{任务描述} :%^{任务类型|dev|bugfix|env|doc|meeting}:\n SCHEDULED: %^t\n PRIORITY: %^{优先级|A|B|C|D}\n %?\n %i")))它的效果如下:到目前为止，介绍了关于org capture的相关内容，并演示了如何利用capture 实现一个任务的捕捉功能。gtd 后面的流程还有任务的分配、执行、记录等等功能，这些内容将在后面介绍。感谢各位读者的支持
在上一篇，已经介绍了org mode的基础知识，它与markdown非常相似，并且也十分容易上手，但是它的可扩展性比markdown要强很多。如果将来打算重度使用org mode，那么此时可以对它进行一些基本的配置和美化基本配置org mode 的配置可以通过Emacs自带的org 包来进行管理，可以配置一些标签显示的图形以及一些特殊语句块的高亮(use-package org :ensure nil :mode ("\\.org\\'" . org-mode) :hook ((org-mode . visual-line-mode) (org-mode . my/org-prettify-symbols)) :commands (org-find-exact-headline-in-buffer org-set-tags) :custom-face ;; 设置org mode标题以及美级标题行的大小 (org-document-title ((t (:height 1.75 :weight bold)))) (org-level-1 ((t (:height 1.4 :weight bold)))) (org-level-2 ((t (:height 1.35 :weight bold)))) (org-level-3 ((t (:height 1.3 :weight bold)))) (org-level-4 ((t (:height 1.25 :weight bold)))) (org-level-5 ((t (:height 1.2 :weight bold)))) (org-level-6 ((t (:height 1.15 :weight bold)))) (org-level-7 ((t (:height 1.1 :weight bold)))) (org-level-8 ((t (:height 1.05 :weight bold)))) (org-level-9 ((t (:height 1.0 :weight bold)))) ;; 设置代码块用上下边线包裹 (org-block-begin-line ((t (:underline t :background unspecified)))) (org-block-end-line ((t (:overline t :underline nil :background unspecified)))) :config ;; 设置org mode中某些标签的显示字符 (defun my/org-prettify-symbols() (setq prettify-symbols-alist '(("[ ]" . 9744) ;; ☐ ("[x]" . 9745) ;; ☑ ("[-]" . 8863) ;; ⊟ ("#+begin_src" . 9998) ;; ✎ ("#+end_src" . 9633) ;; □ ("#+results:" . 9776) ;; ☰ ("#+attr_latex:" . "🄛") ("#+attr_html:" . "🄗") ("#+attr_org:" . "🄞") ("#+name:" . "🄝") ("#+caption:" . "🄒") ("#+date:" . 128197) ;; 📅 ("#+author:" . 128100) ;; 💁 ("#+setupfile:" . 128221) ;;📝 ("#+email:" . 128231) ;;📧 ("#+startup" . 10034) ;; ✲ ("#+options:" . 9965) ;; ⛭ ("#+title:" . 10162) ;; ➲ ("#+subtitle:" . 11146) ;; ⮊ ("#+downloaded" . 8650) ;; ⇊ ("#+language:" . 128441) ;;🖹 ("#+begin_quote" . 187) ;; » ("#+end_quote" . 171) ;; « ("#+begin_results" . 8943) ;; ⋯ ("#+end_results" . 8943) ;; ⋯ )) (setq prettify-symbols-unprettify-at-point t) (prettify-symbols-mode 1)) :custom (org-fontify-whole-heading-line t) ;; 设置折叠符号 (org-ellipsis " ▾") ) 上述配置比较简单，核心部分就是我们使用 prettify-symbols-alist 来使将这些特定的 property 字符串替换成更加美观的图标。它是一个列表，列表种的每个子元素又是一个cell，用cell的两个元素来表示替换关系。配置之后，一个org文件大致的效果如下:org-modern 美化为了使文档的显示效果更好，我们需要依靠一个名为 org-modern 的插件，它是一个为Emacs Org模式提供现代化视觉美化的项目，它通过精心设计的样式和布局，能够显著的提升Org文档的可读性和美观度。我们可以使用 use-package 直接安装(use-package org-modern :ensure t :hook (after-init . (lambda () (setq org-modern-hide-stars 'leading) (global-org-modern-mode t))) :config ;; 定义各级标题行字符 (setq org-modern-star ["◉" "○" "✸" "✳" "◈" "◇" "✿" "❀" "✜"]) (setq-default line-spacing 0.1) (setq org-modern-label-border 1) (setq org-modern-table-vectical 2) (setq org-modern-table-horizontal 0) ;; 复选框美化 (setq org-modern-checkbox '((?X . #("▢✓" 0 2 (composition ((2))))) (?- . #("▢–" 0 2 (composition ((2))))) (?\s . #("▢" 0 1 (composition ((1))))))) ;; 列表符号美化 (setq org-modern-list '((?- . "•") (?+ . "◦") (?* . "▹"))) ;; 代码块左边加上一条竖边线 (setq org-modern-block-fringe t) ;; 属性标签使用上述定义的符号，不由 org-modern 定义 (setq org-modern-block-name nil) (setq org-modern-keyword nil) )现在org 文档显示的就更加漂亮了到此我们对org-mode 显示的效果进行了初步的美化，现在的文档看起来比原始的要好看多了，用org来编写文档至少也显得赏心悦目了。
在介绍vim配置的时候介绍过lsp的相关基础知识。简单来说lsp是一个协议，它以C/S架构的形式进行组织，lsp负责分析语法，给出具体的语法单元，完成跳转等功能的核心实现。而客户端则负责接收用户的操作请求并呈现具体结果。这样做的好处是将核心服务和客户端显示分离出来，核心部分重用，客户端则可以由各个编辑器自己实现，使各种编辑器都有相同的核心功能体验。对于Emacs来说，由lsp-mode 提供核心客户端库，管理服务器生命周期、消息路由及基础功能。另外也有 lsp-ui 这种增强UI模块，提供实时信息侧边栏（lsp-ui-sideline）、代码透镜（Code Lens）、悬浮文档等下面来介绍如何使用它们配置一个基础的lsp功能lsp-mode根据官方给出的配置，我们可以组一个基础的配置(use-package lsp-mode :ensure t :init ;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l") (setq lsp-keymap-prefix "C-c l") :hook ( ;; if you want which-key integration (lsp-mode . lsp-enable-which-key-integration)) :commands (lsp lsp-deferred))这里我们只是安装了一个客户端，想要真正实现lsp的功能，还需要针对具体的语言下载一个服务端，可以通过 lsp-install-server 来下载如果我们希望能像vscode那样，以悬浮窗口的形式显示符号的定义、声明或者注释文档，那么我们需要使用 lsp-ui 这个插件(use-package lsp-ui :ensure t :after (lsp-mode) :config (setq lsp-ui-doc-position 'top))这里我通过 lsp-ui-doc-position 来定义显示的窗口在上方，一般的编辑器默认是显示在光标所在的位置，但是我觉得显示在光标位置会影响我阅读后续的代码，所以我将它显示在上面，如果各位读者希望它像其他编辑器那样显示在光标位置可以修改参数为 at-point项目管理上述的lsp配置完之后，它只能使用当前buffer中的内容进行语法补全提示，也就是说我在其他的位置定义的函数和类在当前buffer中是无法识别到的。我们需要结合项目一起来使用。我们可以使用名为 projectile 的插件来进行项目管理(use-package projectile :ensure t :init (setq projectile-project-search-path '("~/projects/" "~/work/" "~/playground")) :config (define-key projectile-mode-map (kbd "C-c C-p") 'projectile-command-map) (global-set-key (kbd "C-c p") 'projectile-command-map) (projectile-mode +1)) (use-package counsel-projectile :ensure t :after(projectile) :init (counsel-projectile-mode))这里我们额外安装了 counsel-projectile 用来与 counsel 结合进行搜索语法检查语法检查方面，目前主流的插件是 flycheck 和 flymake，但是好像 flycheck 使用的人多一些，所以这里我也采用 flycheck。(use-package flycheck :ensure t :config (setq truncate-lines nil) ; 如果单行信息很长会自动换行 :hook (prog-mode . flycheck-mode))这里我们仅在编程的时候开启语法检查。同样的， flycheck 是一个前端，用来显示结果的，具体检查的核心功能是通过后端来实现的。后端的程序可以在官方网站上找到。状态栏显示相关的lsp信息现在是时候来修改以下状态栏的显示了，根据配置vim的经验，我还是希望主要显示当前编辑的模式、文件名、文件编码、lsp服务器、语法错误信息等。这里我使用 doom-modeline 来完成这个功能(use-package doom-modeline :ensure t :init (doom-modeline-mode 1) :config (setq doom-modeline-height 30) (setq doom-modeline-bar-width 5) (setq doom-modeline-icon t) (setq doom-modeline-lsp t) (setq doom-modeline-major-mode-icon t) (setq doom-modeline-buffer-state-icon t) (setq doom-modeline-buffer-file-name-style 'truncate-with-project) (setq doom-modeline-check-simple-format t) )通过上述代码来简单的配置一下，就可以有丰富的显示信息每种语言都对应了一个lsp的后端程序，lsp-mode官方网站 给出了每种语言对应的lsp后端程序，我们可以使用 lsp-install-server 来安装,这里我准备安装 clangd 这个后端在安装了lsp之后，再次打开一个cpp文件，可以发现它的界面如下:上面我们完成了lsp配置的基础准备工作，下面将要来针对具体的语言探索一下实际的使用方式和使用体验状态栏下分别显示了当前的模式(N代表normal模式)、文件类型、文件名称、当前编码方式等等，最后一个是flycheck 语法检查的结果，这里的代码比较简单，所以它没有检测出来任何问题，最后以绿色圆圈中的一个勾来显示。需要注意的是状态栏中的小火箭表示lsp服务已启用，如果没有这个标志可以手动的执行 lsp 命令选中项目的根目录C++ 项目实例C/C++ 项目需要明确的编译信息，clangd 通过 compile_commands.json 文件获取这些信息。对于cmake构建的项目cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1另外我希望每种语言对应的lsp配置放入到不同的配置文件中，做到模块化处理，因此我在配置中新建了一个lsp目录，并且按编程语言每种语言一个配置文件。目前c++语言的配置如下:(require 'lsp-mode) (add-hook 'c++-mode-hook (lambda () (setq lsp-project-identification-methods '(:root ("compile_commands.json" ".git" ".clangd" "CMakeLists.txt" "Makefile"))) (setq lsp-clients-clangd-args '("-background-index" "--clang-tidy" "--completion-style=detailed")) (setq-local completion-at-point-functions '( cape-file cape-keyword cape-dabbrev)) (lsp-deferred))) (provide 'cpp)为了实现不同的语言加载不同的配置，这里通过对应模式的hook来实现。一般每种模式都有一个对应的hook变量，它里面保存了一个函数，当启用该模式时会执行对应的代码，这里c++-mode 对应的是 c++-mode-hook。在这个hook中，主要做了四件事：第一，我们定义了项目根目录的标识；第二，定义了clangd启动的一些参数；最后也是最关键的一点，我们重新定义了补全端的函数。在配置Emacs的补全时，提供了一大堆补全方式，很多在编程时是用不到的，而且Emacs在补全时会依次根据 completion-at-point-functions 列表中保存的补全函数来查找补全项，一旦前一个函数返回了补全项，那么就不再往后查找。在编程的过程中前面定义的很多补全功能并没有lsp提供的补全好用，而且还会影响lsp的补全导致它出不来，所以我们去掉一些没用的，仅仅保留关键的部分。lsp-mode 会提供一个名为 lsp-completion-at-point 的补全函数注册到 completion-at-point-functions 中，因此这里我们不需要单独的写出来，当然写出来也没问题。我们可以通过 describe-variable 命令来查看 completion-at-point-functions 的值。Its value is (lsp-completion-at-point cape-file cape-keyword cape-dabbrev) Local in buffer main.cpp; global value is (cape-line cape-elisp-symbol cape-dict cape-ispell cape-keyword cape-file cape-dabbrev tags-completion-at-point-function) 最后一件事就是调用 lsp-deferred 来延迟加载lsp。因为 lsp-deferred 需要等到缓冲区完全加载完成之后才加载，所以将它放到最后面代码跳转在vim中，有几个关于跳转的关键的快捷键，gd 表示 goto declaration 跳转到定义，gD 表示 goto definition 跳转到定义，使用 gf 来跳转到头文件，跳转之后可以使用 ` 再次回来，这些快捷键 evil` 也为我们保留了，我们不需要额外的配置只要lsp能加载起来就能使用。显示符号文档信息vim配置中定义了 gh 显示符号的文档信息，我们可以使用 define-key 来定义快捷键。既然显示文档的功能由 lsp-ui 来提供，这里就将代码放入到 lsp-ui 位置(define-key lsp-ui-mode-map (kbd "gh") #'lsp-ui-doc-glance)tree-sitter在配置vim的时候提到过tree-sitter 是轻量级的语法分析器，而lsp是进行语义分析的重量级工具，tree-sitter 是对 lsp 的一个补充，并且我们用tree-sitter 进行了语法高亮和代码片段的折叠，这里尝试在Emacs中使用tree-sitter(use-package tree-sitter :ensure t :hook (prog-mode . tree-sitter-hl-mode) ;;启用语法高亮 :config (global-tree-sitter-mode)) (use-package tree-sitter-langs :ensure t :after tree-sitter)安装完成之后，我们通过命令 tree-sitter-langs-install-grammars 来安装对应的语言支持，默认是安装所有语言的支持，这里我就使用默认的就好下面尝试启用在vim中 zc、和 zo的功能。它们主要是用来折叠和展开代码块。我们来安装一个名为 ts-fold 的插件，该插件没有被放入到 elpa，所以需要手动下载然后加载(use-package ts-fold :load-path "~/.emacs.d/ts-fold" :config (global-ts-fold-mode 1) (with-eval-after-load 'evil (evil-define-key 'normal 'global "zc" #'ts-fold-close "zo" #'ts-fold-open)))最终的效果如下:基于tree-sitter，我们还可以实现一个非常重要的功能——增量选择代码块。在vim配置中我使用回车来扩大选区，使用退格键来减小选区，这里仍然沿用这种配置增量选择的功能可以使用 expand-region(use-package expand-region :ensure t ; 从ELPA自动安装 :bind ("C-=" . er/expand-region) :config (defun my/incremental-expand-region() (interactive) (if (region-active-p) (er/expand-region 1) (er/mark-word)) (setq deactivate-mark nil)) (defun my/contract-region() (interactive) (if (region-active-p) (er/contract-region 1) (call-interactively 'evil-backward-char))) (with-eval-after-load 'evil (dolist (state '(normal visual motion)) (evil-define-key state 'global (kbd "RET") nil) (evil-define-key state 'global (kbd "<backspace>") nil)) (evil-define-key 'normal 'global (kbd "RET") #'my/incremental-expand-region) (evil-define-key 'normal 'global (kbd "<backspace>") #'my/contract-region)))原版的增量选择，如果没有选中某个部分无法执行增量，所以对它进行稍微的改造，如果当前没有选中任何部分，先选中当前光标所在的单词。然后继续进行增量选择。与增加选区类似的，减少选区也是先判断如果已经没有选中区域的话还是延续vim中退格键的功能。本篇到此就先结束了，但是我们的IED的功能并没有配置完，后面我计划加上自动编译运行、调试等功能，这些就在后面的文章中给出