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还有其他一些我引用了但是忘记了具体链接的博客或者教程。
在上一篇，已经介绍了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来编写文档至少也显得赏心悦目了。
之前我介绍了Emacs相关配置以及对应的知识，主要包括：vim模式、编辑的优化、补全、代码跳转、调试等等，旨在将它打造成另一个vscode、也许是我技术能力有限，不仅在使用体验上赶不上vscode，在配置的简便化以及开箱即用的程度上都比不上。如果是使用Emacs仅仅因为它复杂一般人不那么容易学会，那么就有点装x的嫌疑了。我使用Emacs的原因主要有三点：高度自由、elisp语言的魅力以及org mode。可以说没有org mode我可能不会考虑Emacs。我认为 org mode是Emacs的灵魂也是目前市面上没有任何一款软件能替代的。什么是org mode、它与markdown相比有什么优势呢？本文将对org mode做一个入门的介绍什么是org mode实际上 Org mode 是一种轻量级标记语言，它与markdown、RST类似，但是功能上要比它们强得多，不仅可以用来完成日常的文章编写，还可以进行任务管理、项目规划、笔记收集整理等各种操作。并且配合elisp编程可以千变万化。作为一门标记语言org mode实际是十分容易的，它保存的文档以 .org 作为扩展名基本使用大纲大纲可以理解为多级的标题，了解markdown的肯定知道这个意思。一个 * 代表一级大纲，两个代表二级大纲，然后依次类推。对于大纲我们可以手动输入 *，可以使用Emacs提供的快捷键快捷键功能备注在后面创建一个与光标所在位置同级的大纲如果没有大纲则默认创建一个新的一级大纲M-降低当前大纲的等级 M-升高当前大纲的等级 M-将当前大纲及其内容整体向上移动 M-将当前大纲以及内容整体向下移动 大纲这部分有点像思维导图，我们可以将某些知识点以这种大纲的形式组织起来。有些思维导图软件可以点击某个模块将它的子模块隐藏起来，与这类似的，org mode也可以在大纲上按tab键来显示或者隐藏大纲底下的内容。我们可以定义和切换整个org 文档的大纲显示样式，它主要有三种Folded: 对于当前的大纲只显示大纲本身不显示其子大纲以及内容children: 只显示当前大纲的子大纲，不显示更下一级的内容subtree: 完全展开整个大纲代码块代码块放在 #+BEGIN_SRC 和 #+END_SRC 之间，在Emacs中有一个快捷键 <s <TAB> 来快速输入这个标识. 另外也可以在 后面接上 语言类型，例如下面是一段对应的c++代码#+BEGIN_SRC c++ int main(int argc, char* argv[]) { return 0; } #+END_SRC当我们进入到代码块中编写代码时它会开启对应语言的major mode，前面我们配置了c++ lsp 补全相关的内容也可以在编辑代码块时使用超链接超链接我们可以采用 [[ ][<text>]] 的语法，例如 [[https://www.baidu.com][百度一下，你就知道]]图片本地图片可以使用 file:/path/to/image.png 的形式来写上本地图片的位置，然后开启 iimage-mode 显示图片字体样式字体样式主要有:粗体：我们可以使用 ** 来包裹文字以便显示为粗体斜体: 可以使用 // 来包裹文字显示斜体下划线: 可以使用 一前一后两个 _ _来包裹文字将它显示为带下划线的样式删除线: 可以使用 + 前后包裹来显示为删除线其实org mode 与markdown一样都非常简单，日常写博客或者写文档的话，我觉得这些基本知识差不多就够用了。
上一篇博客中，我完成了C++ IDE初步工作，包括代码的高亮、折叠、跳转以及补全等工作。但是作为IDE来说功能还有点不够，就我个人而言作为IDE来说它还需要具备一键编译运行和调试功能。这篇文章就来记录我是如何实现上述功能的编译运行我使用的演示项目比较简单，它的文件结构如下:. ├── include │ └── head.h └── src ├── add.cpp ├── div.cpp ├── main.cpp ├── mult.cpp └── sub.cpp它分为两个目录分别保存头文件和源文件。其中头文件只有一个定义各个接口函数，而接口函数的实现就放到各自定义的cpp文件中。这里使用加减乘除的四则运算的实现来作为演示。这里我分别演示一下Make文件和CMake构建的项目是如何实现一键编译运行的。Make构建的项目针对上前面介绍的简单项目，我们可以写出如下的Makefile# 编译器设置 # 定义项目根目录 ROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) CXX := g++ CXXFLAGS := -Iinclude -Wall -Wextra -pedantic -std=c++11 -MMD -MP LDFLAGS := EXE_OUTPUT := $(ROOT_DIR)bin TARGET := $(EXE_OUTPUT)/app $(info TARGET = $(TARGET)) # 源文件和对象文件设置 SRC_DIR := src SRCS := $(wildcard $(SRC_DIR)/*.cpp) OBJ_DIR := $(ROOT_DIR)build/obj OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS)) DEPS := $(OBJS:.o=.d) # 默认目标（第一个目标） all: $(TARGET) # 链接生成可执行文件 $(TARGET): $(OBJS) @mkdir -p $(@D) $(CXX) $(LDFLAGS) $^ -o $@ # 编译源文件并生成依赖 $(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR) $(CXX) $(CXXFLAGS) -c $< -o $@ # 创建对象文件目录 $(OBJ_DIR): mkdir -p $@ # 包含自动生成的依赖关系 -include $(DEPS) # 清理生成的文件 clean: rm -rf $(TARGET) build .PHONY: all clean 上面我们定义了头文件路径为include 目录，并且规定了中间文件生成在 build/obj 中，最后定义了生成可执行程序在 bin/app 中对于编译来说，Emacs内置了 compile 命令，它会自动执行 make -k 命令，但是如果我在使用Emacs的过程中切换到了其他目录的话，还需要特别指定Makefile 所在的路径，对我来说我希望能在尽可能少输入参数的情况下完成同样的操作，不太希望每次都指定项目根目录，好在之前配置的projectile 插件帮助我们识别出来了项目的根目录。所以这里可以使用 projectile-compile-project 来自动指定根目录并编译。从上面的截图可以看到，flycheck 提示了几个错误，这是因为项目没有生成 compile_commands.json文件，所以lsp服务器无法跨文件分析，导致找不到头文件。原始的make 命令并不支持生成 compile_commands.json 文件，我们可以通过 bear 命令来完成这个工作，它的用法比较简单，只需要使用 bear -- <your-build-command> 即可, 对于使用make编译的项目来说 <your-build-command> 代表的就是 make 命令。我们需要考虑的一个问题是，如何将bear加入到编译命令中，也就是将它自动生成的 make -k 给替换掉，第二个问题是如果当前目录在其他目录下，如何保证 compile_commands.json 永远生成在根目录下Emacs中有一个变量 compile-command 保存了编译的命令，如果我们使用Emacs自带的compile来编译可以通过修改它来实现，而 projectile-compile-project 则是通过变量 projectile-project-compilation-cmd 来保存编译命令，默认是nil，对于使用 projectile 我们通过修改这个变量的值从而修改编译时使用的命令。另外既然 projectile 可以得到项目的根目录，我们就可以利用这个插件来获取项目的根目录，有了这些信息通过一个函数就可以重新生成一个编译命令(defun my/general-compile-command() (concat "bear --output " projectile-project-root "compile_commands.json" " -- make -k"))这个函数的代码非常简单，通过 projectile-project-root 来获取项目的根目录，然后通过字符串拼接的方式来得到编译命令生成 compile_commands.json 成功之后，我们重启 lsp 服务后可以看到错误都消失了，只有两个警告了了解了编译的一些情况，下面来看看如何在Emacs中执行生成的可执行程序。Emacs中可以使用 shell-command 来执行可执行 shell 命令。例如我们可以在项目的根目录下执行 shell-command ./bin/app。很明显如果每次都指定程序的路径是非常麻烦的事，我希望能有一个命令或者函数来自动执行可执行程序。但是Makefile构建的项目比较古老也灵活，Makefile中没有一个固定的方式或者写法来指定可执行程序的生成路径，也就是说没有一个通用的方式来根据Makefile获取可执行文件的路径。一种折中的方案就是针对每个项目都定义一个 execuable-path 的变量来指定可执行程序的路径，然后再通过elisp代码来根据这个变量执行程序(defun my/run-program() (interactive) (shell-command (concat projectile-project-root executable-path)))我们可以针对每个项目单独设置一个 executable-path 变量。Emacs会读取项目根目录中的 .dir-locals.el 文件，并且将文件中定义的变量作为项目的局部变量，所以我们只需要在该文件中定义好 executable-path 就可以了。我们可以通过命令 add-dir-local-variable 来往该文件中添加一个局部变量，也可以自己手写该文件实现这一操作添加完变量之后，项目根目录中的 .dir-locals.el 文件内容如下;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") ((c++-mode . ((executable-path . "bin/app"))))在重启Emacs之后，执行这个函数就可以做到一键运行了有了这些，我希望能将它们有机的组合起来，也就是说按下某个快捷键，这里我暂时定义为 <F7>。它直接同时执行编译和运行的操作。通过 C-<F7> 来完成重编译的操作。;; 重新编译 (defun my/rebuild-program () (interactive) (let ((root (file-name-as-directory (projectile-project-root)))) (shell-command (concat "make clean -C " root)) (setq compile-command (concat "bear --output " root "compile_commands.json" " -- make -k -C " root)) (compile compile-command))) ;; 绑定快捷键 (setq compilation-read-command nil) ;; 取消编译时确定命令行 (evil-define-key 'normal c++-mode-map (kbd "<f7>") #'projectile-compile-project) (evil-define-key 'normal c++-mode-map (kbd "C-<f7>") #'my/rebuild-program))) 这里的代码比较简单，对于编译来说只需要将之前执行的 projectile-compile-project 绑定到对应的快捷键；对于重编译，我通过函数 my/rebuild-program 来完成。这个函数主要操作是先执行 make clean 命令然后重新执行 make。在正式绑定快捷键之前，有一句 (setq compilation-read-command nil) 。projectile-compile-project 和 compile 命令都是交互式命令，执行时会首先显示对应的编译命令，需要用户手动执行回车确认命令，这句代码的意思是不取消它们需要确认的步骤，直接执行命令。本来我打算在重编译函数中也采用 projectile-compile-project 但是它这个交互式我一直取消不了，所以这里我直接采用 compile 指定根目录的方式来完成这个操作。如果想要绑定一键运行的操作也可以采用这个思路，将快捷键绑定到 my/run-program 函数中，这个函数也可以添加一个编译命令确保执行的是最新代码生成的可执行程序CMake工程CMakeLists.txt 文件内容如下:cmake_minimum_required(VERSION 3.15) set(CMAKE_CXX_STANDARD 11) project(test) # aux_source_directory(${PROJECT_SOURCE_DIR} source) file(GLOB source ${CMAKE_SOURCE_DIR}/src/*.cpp) include_directories(${CMAKE_SOURCE_DIR}/include) set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) add_executable(app ${source})这个CMakeLists.txt 文件中主要定义了编译使用到的源文件、头文件目录路径和生成的exe路径emacs 中有一个名为 cmake-ide 的包，它用于读取cmake配置中的各项参数并将参数传递到对应的包中，虽然用它可以很方便的针对cmake配置，但是它依赖rtags，并且没有支持lsp-mode。所以这里就淘汰它，还是想办法自己实现针对cmake来说，要生成 compile_commands.json 比较简单，我们可以在命令行中使用cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1也可以在cmake配置文件中，project命令之后添加set (CMAKE_EXPORT_COMPILE_COMMANDS ON)这里我采用将命令写到cmake文件中的方式。对于cmake 编译的过程主要由两个部分组成，首先是cmake构建项目生成Makefile，然后使用make 命令编译项目。我们要实现自动编译也需要模拟这两个命令。与上面类似，这里我只需要将 my/general-compile-command 函数做少许改动即可(defun my/cmake-general-compile-command () (concat "cmake -B " (projectile-project-root) "build -DCMAKE_BUILD_TYPE=Debug " (projectile-project-root) " && ln -sf " (projectile-project-root) "build/compile_commands.json " (projectile-project-root) "compile_commands.json" " && cmake --build " (projectile-project-root) "build --config Debug"))这个函数生成的命令主要完成三个工作，将构建编译生成的临时文件放到 build 目录下；因为生成的 compile_commands.json 文件也一起放在了 build 目录中，所以我加一个软链接到项目根目录的操作；最后就是执行编译操作了。至于重编译则于上面的步骤相似，cmake一般我习惯删除存放临时文件的build目录然后重新执行cmake构建。所以这里还是模拟这个过程(defun my/cmake-rebuild-program () (interactive) (let ((root (file-name-as-directory (projectile-project-root)))) (shell-command (concat "rm -rf " root "build")) (setq compile-command (my/cmake-general-compile-command)) (compile compile-command)))至于运行程序，我们还是可以采用上面介绍的指定程序生成路径的方式。也就是不管使用cmake或者Makefile 构建的工程都可以使用上面定义的 my/run-program 函数来运行程序调试作为IDE的一个重要或者说基础的功能，调试功能是必不可少的。emacs 自身支持使用gdb进行调试，我们可以执行 M-x gdb 来启动一个调试示例，这个时候我们一边通过gdb的调试命令来控制程序语句的执行一边观看代码的上下文。但是目前流行的方式是使用 dap 来调试程序，至于什么是dap，我在配置vim的时候已经介绍过了，这里就不再赘述了emacs 中有一个名为dap-mode 的插件通过这个插件可以实现dap相关的功能。因为在介绍vim配置的时候我使用的是vscode中的 cpptools插件，这里我打算也使用它来作为dap的调试后端，可以通过cpptools官方仓库 进行下载接着需要安装lldb-vscode，它是针对vscode的一个插件，我们可以在 官方仓库 中找到对应的下载包。下载完成之后可以直接解压到对应目录，这里我解压到 ~/.emacs.d/cpptools 目录中。此时对应的调试后端程序为 ~/.emacs.d/cpptools/extension/debugAdapters/bin/OpenDebugAD7。我们需要赋予它可执行的权限。在这些工作都做好之后，可以使用下面的代码来安装dap-mode(use-package dap-mode :ensure t :after (lsp-mode) :config (dap-auto-configure-mode) ; 可选：启用自动配置 (setq dap-cpptools-debug-program '("~/.emacs.d/cpptools/extension/debugAdapters/bin/OpenDebugAD7")) )我们可以通过命令 dap-debug-edit-template 创建一个调试的模板。对给出的模板做一些简单的修改(dap-register-debug-template "cpptools::Run Configuration" (list :type "cppdbg" :request "launch" :name "cpptools::Run Configuration" :MIMode "gdb" :program "${workspaceFolder}/bin/hello" :cwd "${workspaceFolder}" :environment [] :miDebuggerPath "/usr/bin/gdb")) 我们执行一下这个代码就会向Emacs注册一个调试的模板。接着直接调用 dap-debug 即可启动调试。虽然我们可以将上述注册的代码放到主配置文件中，但是其中的一些关键的字段，例如程序的位置，使用的环境变量，以及对应的调试参数都无法做到所有程序都统一，所以这里我觉得还是需要的时候直接修改就好了。dap-mode 的一些命令如下:dap-debug 和 dap-continue : 启动调试或者运行到下一个断点处dap-next : 执行下一句代码dap-step-in : 执行下一行代码并进入函数内部dap-step-out : 执行到函数返回dap-breakpoint-toggle : 创建或者删除端点我们可以对这些命令进行键位绑定(use-package dap-mode :ensure t :after (lsp-mode) :config (dap-auto-configure-mode) ; 可选：启用自动配置 (require 'dap-cpptools) (setq dap-cpptools-debug-program '("~/.emacs.d/cpptools/extension/debugAdapters/bin/OpenDebugAD7")) (evil-define-key 'normal dap-mode-map (kbd "<f10>") #'dap-next) (evil-define-key 'normal dap-mode-map (kbd "<f9>") #'dap-breakpoint-toggle) (evil-define-key 'normal dap-mode-map (kbd "<f5>") #'dap-debug) )这样我们可以使用上述快捷键来进行调试操作总结这篇文章花了好长时间才弄出来，主要是我对于emacs和lisp语言不太熟悉，中间在尝试编写一键运行和配置dap时耗费了大量的时候。最终我还是成功了，至少我完成我当初想要的一些ide的基本功能，当然在使用上还是比不过vscode，但是在折腾中总能找到一丝乐趣。本文中的配置仅仅经过我自己机器的检验，本来想弄的更加灵活更加接近vscode的体验，有一些我自己想要的功能还没加上，仅仅做了一个可用的玩具。但是我没有想到什么办法，而且这篇文章已经憋了好久了，再不写点东西出来我感觉马上就要放弃了，我想先弄点东西出来给自己一个激励，让我有动力继续深入学习一下Emacs的其他内容。等我多学了一点Emacs多写了一点elisp代码之后可能会对调试和编译方面的代码做一个大的更新。最后如果有读者觉得这篇文章写的有那么一点帮助，那将是我的荣幸，感谢读者在百忙之中能读完本文。
因为公司的编译环境使用的是Ubuntu 20.04 搭配 gcc5.4 和 cmake3.16。所以我也在自己的开发机上安装了配套的环境Ubuntu 就使用 WSL2 直接安装就行，编译环境因为Ubuntu 20.04 默认使用的是gcc 9的版本，所以配置起来还是需要稍微费点功夫的。这里记录一下有效的步骤防止后续又需要配置这套环境的时候满世界找因为gcc5.4 相对来说比较老了，这一版Ubuntu 的官方软件源仓库中没有，需要添加额外的软件源仓库我们可以选择先备份原始的软件源，然后将下列源添加到 /etc/apt/sources.list 文件的末尾deb http://archive.ubuntu.com/ubuntu xenial main universe然后执行sudo apt update && sudo apt upgrade更新软件源接着安装gcc 5.4 以及对应的32位环境支持sudo apt install -y gcc-5 g++-5 gcc-5-multilib g++-5-multilib sudo apt install -y libc6-dev-i386 lib32stdc++6 lib32gcc1默认情况下，ubuntu 20.04 中安装的gcc版本是9.0，如果我们自行安装了 gcc5.4，系统中会有多个gcc版本有时候会导致冲突，我们可以使用下面的命令来管理多个gcc版本# 配置备选版本 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 50 sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-5 50 # 切换版本（选 5.4 对应的编号） sudo update-alternatives --config gcc sudo update-alternatives --config g++此时使用 gcc --version 将会看到当前gcc的版本是5.4Ubuntu 20.04 官方的软件源中cmake的版本就是 3.16，所以对于cmake直接使用sudo apt install cmake
在介绍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的功能并没有配置完，后面我计划加上自动编译运行、调试等功能，这些就在后面的文章中给出
本节我们将介绍如何在Emacs中的buffer与窗口管理，目标是快速管理窗口，以及快速在不同buffer中进行切换基本概念介绍Emacs与vim相比的一个特点是，Emacs是一个窗口程序，或者说是一个gui程序。而vim是一个终端字符界面程序(当然Emacs也可以启用终端模式)，那么与vim相比，Emacs多了了一个frame的概念。有时候有些初学者(包括我自己)总是将frame当作窗口。frame：Emacs整个程序，包括标题栏、工具栏、显示文本的界面等等部分window: 真正用来显示文本的区域被称之为window。我们经常说的分屏就是创建了一个window，frame里可以包含多个windowbuffer: Emacs 从磁盘中读取的文本保存在buffer中，buffer不一定都在window上显示。也不是所有buffer都对应一个文件针对frame来说，虽然也有相关的函数可以控制，但是一般我不太喜欢在多个frame之间进行切换，也不习惯创建多个frame。所以这里先略过window 管理关于 window。在之前介绍了window相关的函数，包括 split-window、selected-window、delete-window、以及跟窗口设置相关的 current-window-configuration、set-window-configuration。基于evil插件，我们可以使用vim的窗口创建命令。例如可以使用 :vsp 来将窗口进行纵向分割。但是我发现 :sp 和 :vsp 都是纵向分割的，这个跟vim就不太一样了。好在evil提供了一个名为 evil-ex-define-cmd 函数用来修改ex命令。所以这里我们在evil的配置中进行修改，添加下面的代码(evil-ex-define-cmd "sp" 'split-window-below) (evil-ex-define-cmd "vsp" 'split-window-right)窗口创建完成之后，我们可以使用vim 的方式来关闭窗口，例如使用 :q 命令来关闭当前窗口，或者使用 :on (或者它的全称 :only) 命令来关闭其他所有窗口。在窗口之间的移动，当初我在介绍vim配置的时候介绍过我习惯使用 leader + 'h/j/k/l' 的方式来在各个窗口间移动。但是在Emacs中，我们有比这更高效的移动方式。ace-window 是一个在各个窗口之间进行快速跳转的插件，在激活ace-window 之后，它会在各个窗口上标记一个数字，我们可以按下这个数字快速跳转到对应的窗口(use-package ace-window :ensure t :after general :hook (dired-mode . (lambda () (setq-local aw-ignore-on t))) ;; dired-mode 下禁用 ace-window :config (my-leader-def :states '(normal visual) ";" #'ace-window "l" #'evil-window-left "h" #'evil-window-right "j" #'evil-window-down "k" #'evil-window-up) )这里我们绑定 leader + ; 来调用 ace-window 执行窗口的跳转buffer 管理在介绍neovim 的配置的时候，介绍过一个名为 buffer-line 的插件，它可以很方便的将 buffer 以 tab 的形式给列出来，并且可以根据当前打开的buffer上的编号来进行跳转，我们绑定了 leader + 1~9 的数字来分别跳转到编号为 1~9 的buffer。Emacs中我也希望实现这样的功能，可以将vim上的操作无缝的转移到Emacs上来。要实现这个功能，我们可以安装 awesome-tab 包。它并没有被包含在Emacs 的官方源中，我们需要自行下载git clone --depth=1 https://github.com/manateelazycat/awesome-tab.git将它下载到对应的位置之后，如果要加载它，则需要将它所在的路径放到 load-path 中如果使用 use-package 来加载可以使用如下的代码(use-package awesome-tab :load-path "path/to/your/awesome-tab" :config (awesome-tab-mode t))启用之后，当我们多开了几个buffer之后，它们被显示到不同的tab上了。如果希望能像 buffer-line 那样根据编号跳转，首先可以通过 awesome-tab-show-tab-index 来显示tab上的编号，然后绑定数字键到 awesome-tab-select-visible-tab 进行跳转。解决了这个问题，剩下的函数官方文档上都有介绍，所以我们还是沿用vim上的快捷键，最终的配置如下:(use-package awesome-tab :load-path "~/.emacs.d/awesome-tab" :after general :custom (awesome-tab-show-tab-index 1) :config (awesome-tab-mode t) (my-leader-def :states '(normal) "gb" #'awesome-tab-ace-jump "gT" #'awesome-tab-backward-tab "gt" #'awesome-tab-forward-tab "1" #'awesome-tab-select-visible-tab "2" #'awesome-tab-select-visible-tab "3" #'awesome-tab-select-visible-tab "4" #'awesome-tab-select-visible-tab "5" #'awesome-tab-select-visible-tab "6" #'awesome-tab-select-visible-tab "7" #'awesome-tab-select-visible-tab "8" #'awesome-tab-select-visible-tab "9" #'awesome-tab-select-visible-tab))awesome-tab还可以针对不同的buffer进行分组，然后对分组的buffer进行统一的操作，这里就不介绍了，有兴趣的读者可以自行阅读相关的文档。本节到此就结束了，本节通过 ace-window 和 awesome-tab 插件基本复刻了vim中有关窗口的配置和快捷键。
在之前的文章中，分了几篇着重介绍了Emacs编辑方面的功能改造。作为一个文本编辑器，要想坚持用下去首先应该改造成自己熟悉的编辑方式。本节我们来介绍Emacs的目录管理Dired ModeEmacs自带一个名为 Dired (Directory Editor) 的插件，它负责对目录进行操作。默认的，我们可以使用 C-x d 来进入Dired Mode。启动后它会等待用户输入想进入的目录，默认是当前目录。Dired Mode 是提供了一种类似操作文本的方式来操作目录。我们可以像操作文本那样来对目录进行类似于拷贝剪切粘贴删除创建跳转查找重命名批量操作得益于前面的篇章配置的一些插件，可以很方便的使用Dired Modedired 美化在正式介绍dired 使用之前，先稍微对它进行一些美化，原始的界面太素了，看着不太好看。首先介绍 diredfl ，原始的dired 只能使用两种颜色来区别文件和目录，而 diredfl 可以使用多种颜色，让dired显示的更加漂亮(use-package diredfl :ensure t :hook (dired-mode . diredfl-mode))接着我们再使用 all-the-icons-dired 来给dired显示的前面加上一个图标。这个插件依赖 all-the-icons 插件。(use-package all-the-icons :ensure t :when (display-graphic-p) :commands all-the-icons-install-fonts) ;; 安装完成之后需要执行 all-the-icons-install-fonts 命令安装对应字体 (use-package all-the-icons-dired :ensure t :hook (dired-mode . all-the-icons-dired-mode))我们也可以通过安装 all-the-icons-completion 插件，给minibuffer中的补全系统也加上图标(use-package all-the-icons-completion :ensure t :hook ((after-init . all-the-icons-completion-mode) (marginalia-mode . all-the-icons-completion-mode)))dirvish 增强direddirvish 是在dired 基础之上的文件管理增强插件。相对与dired 它提供快速跳转、实时预览、并且能兼容对dired的一些扩展。(use-package dirvish :ensure t :hook (after-init . dirvish-override-dired-mode) :bind( ("C-x d" . dirvish) )) 我们使用命令 dirvish 就能打开对应的窗口，或者像上面那样绑定快捷键来打开对应的窗口使用vim的方式来操作目录这里的使用vim的方式来操作多少有点标题党的意思。我无法做到完全按照vim编辑文本那样来编辑目录，但是这里我可以修改以下对应的快捷键已达到某些操作可以使用vim的快捷键。(use-package dirvish :ensure t :hook (after-init . dirvish-override-dired-mode) :bind(:map dired-mode-map ("C-x d" . dirvish) ("y" . dired-do-copy) ;; 拷贝粘贴 ("d" . dired-do-delete) ;; 删除 ("r" . dired-do-rename) ;; 重命名 ("a" . dired-create-empty-file) ;; 创建空文件 ("+" . dired-create-directory) ;; 创建文件 ("SPC" . nil) ;; 取消空格键的绑定 ) :config (with-eval-after-load 'evil (evil-define-key 'normal dired-mode-map "r" 'dired-do-rename)) ;; 排除evil模式下默认键的覆盖 (my-leader-def "j" #'dired-goto-file) )在上述的配置中，我绑定的快捷键如下快捷键功能C-x d打开diredy拷贝d删除r重命名a创建空文件+创建空目录SPC j跳转到指定文件需要注意的是，在 config 中调用了这样的语句 (with-eval-after-load 'evil (evil-define-key 'normal dired-mode-map "r" 'dired-do-rename))with-eval-after-load 表示在某个插件加载之后，这句代码的意思是，在evil插件加载后，在normal模式下，我们定义dired mode 下快捷键 r 绑定到 dired-do-rename,也就是重命名这个功能。之所以要这么做是因为在evil插件加载后会覆盖我们定义的快捷键。另外为了正常使用 leader键，这里特意取消了空格键在dired 中的定义，原本它被定义为跳转到下一行。这些功能都比较简单，所以这里就不演示了。搜索文件并跳转正常情况下，当项目文件和代码量上来之后，再一个目录一个目录的找就不太现实了。常见的是根据代码中函数定义来找文件或者根据文件来找函数定义。前者我们通过consult-rg 已经实现了。这里介绍以下如何通过文件名来查找并快速打开文件我们可以使用 consult-locate 来搜索文件，结合前面介绍的orderless，我们只需要对文件有一个相对的映像就可以找到。想要使用 consult-locate 需要安装 locate 程序，在Arch Linux 中可以使用下面的命令安装sudo pacman -S mlocate sudo updatedb ;; 更新数据库我们以打开配置文件中的 init-completion.el 为例。我们可以直接使用 M-x 输入命令 consult-locate并回车 , 接着在命令提示符后输入一个大概的内容，然后在minibuffer的候选项中找到对应的文件即可我们也可以结合 embark-act 命令来做到跳转到文件所在的 dired 中，这里我们在 embark 的配置中添加一个快捷键定义(use-package embark :ensure t :after consult :bind (("C-e" . embark-export) ("C-;" . embark-act))) ;; 添加 embark-act 快捷键这样在定位到文件之后，可以直接使用 C-; 调出对应的动作，最后使用 j 来完成进入dired的动作有了 consult-locate 和 embark-act，前面介绍的 dired中的跳转操作的实用性就大大降低了，如果我们记得文件的全路径，直接使用 find-file 打开就好了。如果只有一个模糊的映像，那么使用 consult-locate 配合 orderless，比进入dired 然后执行跳转要快的多到这里，Emacs中关于目录管理的部分就介绍完了。使用dired配合键盘操作能极大的提升的效率
Emacs 折腾日记(二十四)——帮助信息的一些优化
Emacs 是一个自文档的系统，任何有关Emacs的信息都可以通过Emacs自身查阅。包括基础的入门手册、elisp手册、以及各种插件的相关说明信息。一般遇到不懂的变量直接使用 C-h v。查看它的说明。使用 C-h f 查阅相关函数、使用 C-h k 来查找对应快捷键绑定的函数。一般来说阅读官方一手的文档比从任何论坛或者搜索引擎来的更快更准确，而且有不少Emacs高手都推荐多多阅读Emacs的官方文档。本节就着重介绍一下我了解的关于阅读文档的一些姿势。查找相关定义最开始介绍过使用 C-h f 之类的查看相关文档，Emacs所谓的子文档不光指的是查看文档，而且还可以查看相关的源代码。有些我们可以通过文档中给出的源码链接点进去看，但是还是不如直接查看源码。直接查看源码可以使用 find-function、find-variable、find-function-on-key。它们分别对应着查找函数、变量、以及快捷键绑定的函数定义。我们可以绑定一些快捷键(global-set-key (kbd "C-h C-f") 'find-function) (global-set-key (kbd "C-h C-v") 'find-variable) (global-set-key (kbd "C-h C-k") 'find-function-on-key)对默认文档显示的优化我觉得官方的文档显得比较素，比较原始。为了提高阅读的体验我们可以对文档进行美化。这里可以使用 helpful来增强一下。(use-package helpful :ensure t :bind (("C-h f" . helpful-callable) ("C-h v" . helpful-variable) ("C-h k" . helpful-key) ("C-h s" . helpful-symbol) ))我们针对 C-h k 这个快捷键来对比一下原始的文档和 helpful 增强之后的文档显示效果对比发现，helpful 不光对显示效果进行了一些改进，而且显示的内容更加丰富。特别是它已经可以直接显示出相关定义的源代码，在某种程度上替换默认的 find-function 等函数。which-key在介绍配置 neovim 的时候，介绍了 which-key 插件，它可以根据用户输入的快捷键前缀显示所有可能的匹配，使我们记忆快捷键的负担减轻。Emacs上也有类似的插件。在Emacs 30以后内置了 which-key 插件，我们只需要启用(use-package which-key :ensure t :hook (after-init . which-key-mode) :custom (which-key-idle-delay 0.7))
Emacs 折腾日记(二十三)——进一步提升编辑效率
在前面的几篇，我们完成了Emacs的vim模拟器、中文输入、多行编辑以及基本的补全功能的添加。这一篇没有具体的提升哪一方面的能力，这一篇我想整合我在其他教程中看到的我认为对我比较有用的用法和插件，算是对前期功能的一个总结。让Emacs记住一些信息一般的编辑器都会在下次打开时记住上次的一些信息，例如记住之前打开过的文件，执行过的命令，或者记住上次的窗口布局。记住上次执行的命令我们每次使用 M-x 执行命令时，minibuffer中显示的提示都是一样的，那些常用命令要么不在上面要么太靠下了，我们希望能记住某些命令，以便能快速找到它。记住上次执行的命令可以使用 savehist 插件。它是一个Emacs自带的插件，默认是关闭的状态，我们可以通过将 use-package 来加载它，但是因为是自带的，不需要从镜像中下载所以它的 :ensure 项应该设置为 nil(use-package savehist :ensure nil :hook (after-init . savehist-mode) :custom (savehist-file (locate-user-emacs-file "custom/savehist")) ;; 设置保存文件的位置 (savehist-additional-variables '(kill-rings shell-command-history)) ;; 额外保存剪切板和shell命令行历史 (savehist-ignored-variables '(message-history)) ;; 不保存消息历史 (history-delete-duplicates t) ;; 自动去重 (history-length 1000) ;; 保存历史数据条目 )在执行一些操作关掉Emacs之后，我们会发现它在 ~/.emacs.d/custom 生成了一个名为 savehist 的文件，它记录了之前在minibuffer中执行的命令。为了保持git工程的干净，我打算将这种历史文件排除在git管理之外，所以单独将它放到custom目录，并忽略它其实该插件不光可以保留执行的命令，minibuffer中的许多信息它都可以保存和持久化。minibuffer-history (所有 minibuffer 输入历史)command-history (执行过的命令)search-ring (搜索历史)regexp-search-ring (正则搜索历史)extended-command-history (M-x 命令历史)file-name-history (文件路径历史)记住上次打开的文件一般的编辑器都可以记录上次打开的文件，并列出来。Emacs也有一个类似的内置插件—— recentf(use-package recentf :ensure nil :hook (after-init . recentf-mode) :custom (recentf-max-menu-item 10) ;; 最多只记录10条历史记录 (recentf-save-file (locate-user-emacs-file "custom/.recentf")) ;; 定义保存历史记录的临时文件路径 )搜索功能的增强实现全局搜索我们可以依赖Linux上的命令行工具 grep 和最近(也不算近了)的 ripgrep。之前在介绍vim的时候，vim内部集成了 grep。但是我们使用更为强大的 ripgrep。在Emacs中可以配合插件 consult 和 ripgrep，调用 consult-ripgrep 来进行全局搜索。它会自动搜索当前项目下的所有目录。我们对之前 consult 插件的配置代码进行一些修改，并添加 ripgrep 的配置(use-package consult :ensure t) (use-package ripgrep :ensure t :after consult :bind (("C-s" . consult-ripgrep)) )这里我们将 C-s 绑定的快捷键修改为 consult-ripgrep。神奇的是，配合之前安装的orderless，我们只需要按照一个模糊的记忆来匹配想要的内容。同时它也能支持输入中文批量替换批量替换这个功能，根据我找到的教程，它需要依赖 embark、consult、和 wgrep 这么三个插件。其中 consult 用来进行搜索，而 embark 可以为不同场景下的文本/候选项（如搜索结果、补全列表、文件路径等）提供动态的快捷操作菜单。简化了minibuffer上的一些操作。而 wgrep 则是其中的核心插件，用来批量修改内容并保存(use-package embark :ensure t :after consult :bind (("C-e" . embark-export))) (use-package embark-consult :ensure t :after embark) (use-package wgrep :ensure t :custom (wgrep-auto-save-buffer t) ;; 自动保存修改 )这里我们使用 :after 来保证插件的加载顺序依次为 consult、embark、embark-consult，特别是 embark-consult，它依赖 consult 和 embark，一定要将它放到后面加载。下面来演示如何进行批量替换，这里我们将配置中所有 use-package 修改为 package-install，修改之前记得使用git等版本管理工具进行备份首先，使用 C-s 搜索 use-package 关键字接着使用 C-e, 也就是上面绑定的快捷键来将结果从 minibuffer 导出到 buffer然后使用 C-c C-p 调用 wgrep-change-to-wgrep-mode 将 buffer 的mode由 grep-mode 修改为 wgrep-mode接着使用 M-% 调用 query-replace 进行替换，这个时候它需要输入被替换的字符和替换后的字符确定后，对于每个待替换的位置使用 y 或者 n 来表示替换或者不替换。也可以使用 ! 替换所有最后使用 C-c C-c 调用 wgrep-finish-edit 来结束编辑，配置之前设置的自动保存，此时修改内容已经被保存了修改之后如何不满意，可以使用 C-c C-k 撤销修改小节这应该是最后一篇关于Emacs自身编辑功能的增强了，在这一块我没有使用太多的Emacs经验。倒是在vim上有点经验，所以很多东西我不自觉地就往vim上面靠，总想着vim在编辑上有些功能Emacs上还没有，该如何进行添加，这几篇就显得比较分散，总是想到什么功能就往上面堆。为此造成各位读者阅读体验不佳，我表示道歉。谢谢各位读者的支持和鼓励！