在之前的文章中，分了几篇着重介绍了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的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上还没有，该如何进行添加，这几篇就显得比较分散，总是想到什么功能就往上面堆。为此造成各位读者阅读体验不佳，我表示道歉。谢谢各位读者的支持和鼓励！
本文是参考 emacs lisp 简明教程 写的，很多东西都是照搬里面的内容，如果各位读者觉得本文没有这篇教程优秀或者有抄袭嫌疑、又或者觉得我更新比较慢、再或者其他什么原因，请直接阅读上述链接中的教程。上一篇我们讲了elisp中的流程控制结构相关的内容，下面就该进入到对应数据结构的学习了。elisp中主要的数据结构有：整数、浮点数、列表、符号、向量、散列表等等类型。下面我们先从最简单的类型——整数和浮点数说起数字类型与C/C++对比起来，elisp数字类型少的多，C/C++ 整数类型就有好几种，包括有符号、无符号、int、short、long之类的。elisp不区分这些，它仅仅有整数和浮点数。而且elisp 中只有浮点数这一种小数类型，不像C/C++还有双精度浮点数和单精度浮点数之分。elisp的整数范围与具体的机器有关，它的范围可以通过变量 most-positive-fixnum 和 most-negative-fixnum 来得到。例如在我的机器上它们的值如下most-positive-fixnum ;; 2305843009213693951 most-negative-fixnum ;; -2305843009213693952在给变量使用数字类型赋值的时候，我们可以使用10进制或者其他任意进制的形式。例如#b101100 => 44 ; 二进制 #o54 => 44 ; 八进制 #x2c => 44 ; 十进制 #24r1k => 44 ; 二十四进制因为26个英文字母+10个数字的原因，我们最大只能使用36进制来表示一个数字，但是基本不用到这么大的。日常最多也就用用10进制、二进制、16进制、8进制都算用的少。浮点数的表达遵循 IEEE 标准，也就是可以使用带小数点的数字来表示，或者带上 e 来使用科学计数法，例如3.14 1.0e-10数字类型的测试作为动态类型的语言，在代码执行阶段，变量的类型是会发生变化的。我们无法仅通过变量名或者变量的初始化值来判断变量类型。emacs的变量在执行阶段都知道自己的类型，但是它无法主动向我们报告，我们需要使用一些函数来进行判断，关于数字类型，提供了下列的函数integerp floatp numberp从字面上能理解它们分别判断是否是整形、浮点数、以及数字类型。elisp 测试函数一般都是用 p 来结尾，p 是 predicate 的第一个字母。如果函数名是一个单词，通常只是在这个单词后加一个 p，如果是多个单词，一般是加 -p数的比较与C/C++ 类似，数字的比较一般有 >、<、>=、<= 。但是也有不同的地方，因为elisp中都是使用 setq 来进行赋值的，所以它采用 = 来表示数学意义上的相等。还有一个不同的地方因为elisp中没有 += 、-=、/= 、*= 这样的运算符，所以它使用 /= 来作为不等的判断符号与其他语言类似的，浮点数直接使用等于或者不等于来判断并不准确，需要在一定范围内忽略误差。在C/C++中，我们常见的写法是给定一个误差值，然后二者差的绝对值在这个误差值范围内则认为它们相等。我们将这个算法使用elisp改写一下就得到下面的代码(defun approx-equal (x y) (let ((fuzz-factor 1.0e-6)) (< (abs (- x y)) fuzz-factor))) (approx-equal 1.000001 1.00000000000000001) ;; => t上述的写法并不严谨，在一定误差范围内，它是对的，但是在某些情况下它就不对了，例如 1.0e-7 和 1.0e-12 。它们本身并不相等，但是它们都超过了这个误差范围，相减之后的值小于这个误差范围。但是我们看到其实它们直接的差距还是挺大的，间隔1.0e5 的数量积。我们可以将上述算法进行一些改进(defun approx-equal(x y) (let ((fuzz-factor 1.0e-6)) (or (and (= x 0) (= y 0)) (< (/ (abs (- x y)) (max (abs x) (abs y))) fuzz-factor)))) (approx-equal 1.0e-7 1.0e-12) ;; => t这段代码采用的是比较相对差距的办法。因为涉及到除法，所以先把二者等于0的情况排除了，避免发生除0的问题。上述代码改造成对应的C代码就是#define FUZZ_FACTOR 1.0e-6 // 定义误差范围 bool approx_equal(double x, double y) { // 处理特殊情况：如果两个数都是 0 if (x == 0 && y == 0) { return true; } // 计算相对差并进行比较 double relative_difference = fabs(x - y) / fmax(fabs(x), fabs(y)); return relative_difference < FUZZ_FACTOR; }另外 elisp 中有 eql 函数来判断两个数是否相等(eql 1 1.0) ;; => nil (eql 1.0e-7 1.0e-12) ;; => nileql 在判断数字时不光判断值，也判断类型。第一条语句，因为二者类型不同，第二条语句二者都是float属于同类型，但是二者的值不同，因此两个结果都是假。数字的转换elisp 中可以进行 整形和float型数字的相互转换。在C/C++ 中，整形可以通过隐式转换自动转换成float，而float转换成int时会丢失小数位，比如哪怕是 1.9 在转换为整数时也会是 1。在elisp中，可以通过float将整数转化为浮点数。例如(floatp 1) ; ⇒ nil (floatp (float 1)) ; ⇒ t (eql (float 1) 1.0) ; ⇒ t而浮点数转化成整数有下面几个函数truncate: 抹除小数位，也就是C/C++语言中float转int的操作floor: 类似于C/C++ 中的floor 函数，返回小于等于该数的最大整数ceiling: 类似于 C/C++ 中的 ceil 函数，返回大于等于该数的最小整数round: 类似于 C/C++ 中的 round 函数，返回四舍五入后的整数数的运算一般的语言，数的运算无外乎 +、-、*、/ 取整、取模。elisp 中同样有这些操作，前面的加减乘除跟其他语言一致，没什么特别的。C/C++ 以及 elisp 中的除法都不是纯粹数学意义上的除法，它会将结果抹掉小数位转换成整数。我们如果将除数或者被除数转换为float类型的话，那么就得到数学意义上的除法结果 (当然也不全是，毕竟float数据有表达数据的限制)但是python 不一样，它就是纯粹数学意义上的除法。这个设计我也不知道算是好还是不好，毕竟它与其他语言不一致增加了记忆的负担。(/ 3 2) ; ⇒ 1 (/ (float 3) 2) ; ⇒ 1.5 (/ 3.0 2) ; ⇒ 1.5C/C++ 中有 ++ 、 -- 操作，而且还分 前++ 和 后++ 。在 elisp 中没有这两个操作，也没有类似于 += 的操作。elisp的赋值一直是用的 setq。而且它提供了 1+ 1- 这两个符号来表示 ++ 和 --。至于是前 ++ 还是 后++ 呢？两个都不是，C/C++中的 ++ 本身具有改变变量值的作用，它们的区别在于是返回值之前改变还是之后改变。而elisp 主要使用 setq 来改变变量的值， 1+ 这个操作无法改变变量的，它仅仅改变这条语句返回的值。例如可以使用下面的代码来测试(defun inc (num) (1+ num)) (setq foo (inc 3)) ;; ⇒ 4这里将传入的参数加了1，但是其实函数中 num 的值并没有变化，我们可以对函数做一下修改来验证这一点(defun inc (num) (progn (1+ num) num)) (setq foo (inc 3)) ; ⇒ 3要改变变量的值需要使用 setq 来进行赋值，这个函数可以做一下修改(defun inc (num) (progn (setq num (1+ num)) num)) (setq foo (inc 3)) ; ⇒ 4取模的操作，elisp 中提供了两个方式 % 和 mod 函数，其中 % 与其他语言类似，它要求除数与被除数都是整数，而 mod 则没有这个要求。我们查看mod函数，发现它是被写在C代码里面的。它虽然也是取余，但是它与数学意义上取余的结果并不一致，例如(mod -10 3) ;; ⇒ 2 (mod 10 -3) ;; => -2 (% -10 3) ;; ⇒ -1 (% 10 -3) ;; ⇒ 1% 单纯的就是数学意义上的取模的操作，首先找到商，然后根据商来决定模而 mod 则不同，mod 中首先一个原则就是余数和除数的符号相同。所以第一个的结果应该是正数 也就是 -3 * 4 + 2 = 10，余数是2。第二个结果应该是 - 3 * (-4) - 2 = 10 mod 还有一个原则，那就是商的结果应该是整数。利用这两个原则我们就可以大概的还原一下计算的过程(mod 3.5 2) ;; ⇒ 1.5 (mod -3.5 2) ;; ⇒ 0.5 (mod 3.5 -2) ;; ⇒ -0.5根据上面两个原则，那么它们分别可以还原为1 * 2 + 1.5 = 3.5-2 * 2 + 0.5 = -3.5-2 * (-2) + 0.5 = 3.5另外还有一些其他数学上的操作，对于学习后面写配置的话，大多数应该是用不到的。后续需要使用的话再查询就好了，这里就不在多啰嗦了。到此为止我们已经介绍完了elisp中数的常见操作。后续将陆续介绍其他数据类型，敬请期待。
Emacs本身的使用并不复杂，利用帮助文档，差不多半小时左右就能把一些常见的操作方式和快捷键过一遍，剩下的就是慢慢使用并且熟悉了。Emacs真正有价值的是它高度的客制化。任何人都可以利用elisp代码将Emacs改造成只属于自己的编辑器。会elisp 的不一定是高手，但是高手没有一个是不会elisp的。学习Emacs也绕不开elisp。下面我们就来简单的学点elisp一个简单的 Hello Word(message "hello world")这是一个简单的elisp版本的hello world程序。麻雀虽小但是五脏俱全。从这个简单的程序来说我们可以看出lisp的最大特点，就是以括号作为作为一个完整的表达式。曾今有一个段子说苏联的特工冒死偷到了美国阿波罗计划代码的最后一页，结果回来一看全是括号。这也鲜明的表达了一个lisp的特点，那就是大量的括号我们简单的解析一下上面的代码，上面的代码调用了一个函数，函数的参数就是一个字符串的 hello world。我们可以打开Emacs，进入 scratch buffer，输入上述代码。之后可以将光标移动到代码尾部，按下快捷键 C-x C-e 来执行代码，或者使用 M-x 输入命令 eval-buffer 来看效果。我们可以看到，mini-buffer 位置出现了 "hello world" 的字符串变量我们可以使用 setq 来定义变量，它类似于C/C++ 中的= ，用来给变量赋值或者定义并初始化一个变量。例如我们可以改一下上述的代码(setq name "Emacs") (message "hello, %s" name)将上述代码输入到scratch buffer之后就需要依次在每行的最后执行 C-x C-e 或者直接执行eval-buffer 命令，这样就可以看到效果了上面我们说，elisp的表达式是使用括号来表示的，但是如果在上面代码的基础上加上一句(name)此时就会报错，显示的错误为 void-function name 。我们的本意是想让解释器返回name的值，但是解释器将它作为了一个函数。通过这个错误我们能了解到，elisp基本的表达式中函数需要用括号括起来，但是变量自己本身被作为一个完整的表达式。除了使用setq我们还可以使用 defvar 来定义变量，defvar 的使用如下(defvar variable-name value "variable document")例如(defvar name "Emacs" "a defvar demo name") name ;; ==> "Emacs"我们将光标放到name上，按下 C-h v 可以看到关于name的说明文档。需要注意的是，defvar与setq 除了defvar可以指定变量的说明文档外，还有一个区别就是defvar在定义变量前，这个变量已经有值的话，defvar不会改变变量的值例如下面的例子(setq foo "foo") (defvar foo "this is foo" "document for variable foo") (defvar bar "this is bar" "document for variable bar") foo ;; =>"foo" bar ;; =>"this is bar"C-x C-e(eval-last-sexp) treatsdefvarexpressions specially. Normally, evaluating adefvarexpression does nothing if the variable it defines already has a value. But this command unconditionally resets the variable to the initial value specified by thedefvar; this is convenient for debugging Emacs Lisp programs.defcustomanddeffaceexpressions are treated similarly. Note the other commands documented in this section, excepteval-defun, do not have this special feature.上述英文翻译过来就是 eval-last-sexp 对 defvar 做了特殊处理，默认情况下 defvar 在变量有值的情况下不做任何操作，但是在这个命令中，defvar 会无条件的将变量值重置为它指定的值。主要是为了方便调试代码。同时 defcustom 和 deface 做了同样的操作。请注意在本节中记录的其他命令(eval-defun 除外)没有做这样的处理这就解释了为什么我们使用 C-x C-e 执行的时候 foo 的值发生了改变函数函数的定义与使用作为一门函数式编程语言，函数是elisp的一等公民。如何使用一个函数我们已经在前面的hello world程序中见识过了，那么如何定义一个函数呢？定义一个函数使用 defun 关键字。它的语法如下(defun func-name(args) "document string" body)第一行代表一个函数名和函数的参数列表，第二行表示可以使用一个双引号包含函数的说明文档，Emacs是一个自文档的系统，后续我们可以查看这里写的文档。最后一行是函数的主体内容，例如下面的例子(defun say-hello(name) "say hello to define user" (message "hello, %s" name)) (say-hello "emacs") ;; => "hello, emacs"执行将会输出对应的信息。我们将光标移动到say-hello这个函数上，执行 C-h f 默认回车将会得到我们针对函数写的文档lambda 表达式其实像C++、Java、Python 之类的语言也有lambda表达式，它就是一个匿名函数。一般我们使用函数都是先定义同时给函数取一个名字，但是有时候我们仅仅需要一个临时函数作为参数或者仅仅只会在某些地方调用一次，这个时候就可以使用匿名函数。lambda表达式的形式与defun类似，它的使用规则如下(lambda (args) "document string" body)除了关键字变了，就是不用写函数名称了。我们使用 funcall 来调用一个lambda表达式(funcall (lambda (name) (message "hello, %s" name)) "emacs")我们执行它将会在mini-buffer中看到显示的字符串信息另外我们也可以将一个lambda表达式赋值给一个变量，最后通过funcall 来调用(setq say-hello (lambda (name) (message "hello, %s" name))) (funcall say-hello "emacs") (say-hello "emacs") ;; error, void-function say-hello知乎的大牛指出，这里原本有一个错误，在这里更正：defun 会把函数值绑定到符号的 function-cell 上，setq 会绑定到符号的 value-cell 上上述的说法，我查过 funcall 、value-cell 以及 function-cell 相关的文档，里面涉及到的一些知识点比较复杂，目前我还没有完全搞明白，而且把它贴出来作为入门来讲有点过于复杂了。变量作用域elisp默认使用 setq 定义的变量不管是在函数内还是函数外全都是全局变量，它们的作用域是全局作用域，例如(defun say-hello () (setq name "Emacs") (message "hello, %s" name)) (say-hello) ;;需要执行一下函数解释器才能执行到定义`name`变量的位置 (message name)一般来说代码如果都是全局变量的话，会给代码的编写和维护带来很大的不便。elisp中同样支持定义局部变量，我们可以使用 let 和 let* 它们的用法类似(let (bindings) body)其中的 bindings 可以是单个值，也可以是括号包裹的键值对。如果是单个值，则默认赋值nil，也就是空。如果是键值对，则将值赋值给对应的键。let定义的变量只能作用在let语句块内，例如下面一个计算圆面积的函数，这里知乎的大牛告诉我，pi 是emacs中的内置变量，我采用PI来定义圆周率(defun circle-area (radix) (let ((PI 3.1415926) area) (setq area (* PI radix radix)) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3) (message "%f" area) ;; error void-variable area其中我们在let语句块中定义了两个变量，pi初始化为3.1415926，area 初始化为 nil同样的，可以使用 let* 改写上面的程序(defun circle-area (radix) (let* ((PI 3.1415926) area) (setq area (* PI radix radix)) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3)let* 与 let 的区别在于，let* 可以在binding时候使用前面已经定义过的变量。例如上面的代码可以改写成(defun circle-area (radix) (let* ((PI 3.1415926) (area (* PI radix radix))) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3)我们使用 let 来改写一下这个程序，发现它会报错(defun circle-area (radix) (let ((PI 3.1415926) (area (* PI radix radix))) (message "半径为 %.2f 的圆的面积是 %.2f" radix area))) (circle-area 3) ;; error void-variable PI好了本篇也该结束了，本篇主要了解了基本的elisp语法，下面进行一下总结:使用 defvar 和 setq 来定义全局变量，其中defvar可以给变量设置一个说明文档，我们使用 C-h v 来查看这个文档使用 let 和 let 来定义变量，其中 let 可以在变量定义的时候使用前面定义过的变量使用 defun 来定义函数使用lambda 来定义一个lambda表达式，使用funcall 来调用lambda，lambda可以赋值给变量，后续使用funcall来调用
初次知道emacs这个东西是在《程序员的呐喊》这本书。书中的作者提倡学习编译原理，推崇emacs。现在距离我知道emacs已经过去了快8年，期间不断的重复学习——放弃——学习的路子。与过去学习vim类似，vim我也经历过放弃到学习，最后有项目需要使用Vim在Linux上开发，没办法慢慢学会了它的操作，以及一些简单的配置。后来我写了vim的操作和配置，我发现这样做很有用首先就是在写vim操作的时候，相关的概念和操作技巧我又重新梳理了一遍，加深了它的印象，过去一些不怎么用的操作我发现在之后我用的越来越顺手。其次就是我目前使用的配置在出错或者需要做修改的时候根据之前的文章我又梳理了一遍，很方便我回顾之前的思路能快速定位到每个模块当初的思路以及一些被遗忘的知识点。既然之前的教程对我有这么大的帮助，那么我想这次学习emacs就采用这种策略。首先日常记笔记，记录工作的日志之类的操作就从markdown转化到emacs的org-mode中以便熟悉emacs的日常操作。另外通过写博客的方式记笔记，记录自己做的一些配置以及一些操作技巧。日后用来回顾也好或者仅仅为了加深印象也好都可以。另外我想这一系列既然是折腾日记，而且我还是emacs的小白，那么这一系列的文章自然是不成体系的，基本上是想到什么就写什么。如果看这一系列的文章的读者存在问题我可能也没有能力解答。在这里给读者说一声抱歉。emacs 是什么任何技术或者工具我都喜欢从这几个方面先了解一下，再考虑是不是值得学。首先一个问题就是“是是什么”，其次是“为什么”，最后是“怎么做”。我想现在就来说说我对这几个问题的理解首先Emacs是什么呢？Emacs 其实并不特指某一款编辑器，而是一个文本编辑器大家族的统称。最初由理查德·斯托曼于1975年在MIT协同盖伊·史提尔二世共同完成。这一创意的灵感来源于TECO宏编辑器TECMAC和TMACS。这里我们提到Emacs主要特指 GNU EMacs，它是由理查德·斯托曼于1984年开始开发，至今已经有40多年的历史。它的核心理念官网上是这么介绍的An extensible, customizable, free/libre text editor — and more它是一个 可扩展、可定制、自由的文本编辑器，并且远不止于此。EMacs 是自由软件，它没有对用户做出任何限制，包括内核代码完全是开放的。你可以在此基础之上进行任何改动。同时它提供了一个完成的lisp解释器，随着这些年的发展，它的插件系统已经特别丰富了，利用插件几乎可以实现任何功能。有人戏称Emacs是一个伪装成文本编辑器的操作系统，在Emacs上几乎无所不能。emacs 是否值得学我个人认为Emacs还是很值的学习的。说说我学习Emacs的理由：它是一个与vim齐名的编辑器，因为它的插件丰富，结构设计良好，它几乎可以做到vim能做的所有事情，包括vim那一套操作逻辑它使用lisp作为扩展语言，我目前掌握了面向过程的语言C，面向对象的语言 C++/Python，但是还从来没有完整的学习过函数式编程语言，而且《黑客与画家》的作者也推荐lisp语言。平时写普通的脚本我都是用shell 或者 python 就搞定了，没什么几乎接触到lisp的实战，借这个机会学习一下lisp并且用于实战，也是一个不错的选择emacs 的 orgmode很吸引我，它的功能比markdown要丰富，借助Emacs这个平台能提供非常棒的体验，包括但不限于写博客，做计划，管理日程，甚至能在里面运行代码如何学下面是一张网上流传的图，各个编辑器的难易程度虽然有点夸张，但是Emacs的学习过程并不算简单。目前我也没有好的学习路径，而且这一系列的文章只是我的尝试。目前我也没有掌握Emacs，甚至是一个小白。我想以写文章的方式来慢慢学习关于Emac的内容网上有很多Emacs的教程，最经典应该要数这篇 一年成为Emacs高手但是我之前尝试过使用高手的配置，总是用着不是那么舒服，总想要折腾出一套属于自己的配置，后面慢慢的就放弃了。这次我想慢慢的根据其他人的教程折腾出一套属于自己的配置，然后通过使用 org-mode 来写博客、记笔记、并且进行日程管理，后续慢慢的将写脚本之类的简单开发工作转移到Emacs上，通过折腾配置并且多用Emacs来学习。祝我好运吧。
很抱歉我决定结束这个系列的内容了。原本我打算介绍markdown、orgmode相关的配置，甚至还打算介绍如何在vim 中使用 emacs 的 org-agenda 来进行日常的任务管理。但是出于一些原因我打算放弃了。首先如果将markdown 理解为另一种类似于HTML 的标记语言的话，我们在介绍LSP 的时候已经介绍过该如何新增新的编程语言的支持，再另外介绍Markdown 的配置就显得多余了。而且本系列也并不打算事无巨细的带领大家从零开始配置一套完整的配置，我仅仅希望通过这一系列的内容介绍一下vimscript 或者lua 接口以及vim 的一些特性，让大家看完之后又能力自行动手弄出一套属于自己的配置。至于orgmode 的内容，我发现目前还没有任何插件能完美的模拟emacs 的orgmode 功能。vim 上的插件也仅仅能做到渲染样式，语法高亮而已。也就没有必要单独介绍了。如果后续我能掌握 emacs 的话，再来介绍也不迟总之就是本系列到此结束了。一些建议不知道各位小伙伴在跟着我这一系列文章尝试自己配置vim 的时候有什么感觉？我当初在整理这些配置的时候发现它越来越像vs code ，甚至最近几年新推出的LSP以及 DAP 的一些插件几乎都是原生的用于 vscode 上的或者从它上面移植过来的。有些主题也是照搬 vscode 的。我们发现自己费劲心力终于将vim 变成的 vscode 。有没有觉得在做无用功？既然要将它变成 vscode 那为何不直接使用 vscode 呢？可能有人会说， vscode 对于vim的一些模式和 ex 命令的支持并不好。我想这就是我们使用 vim 的理由，也是vim 比其他编辑器强的地方。我们仅仅是在使用工具而已，哪个工具好用，哪个工具能帮助我们快速完成工作，那就用哪个。工具本身没有高低贵贱之分，只有合适与否的差异。作为程序员要拥抱新技术，千万不要抱着某个技术某个工具不放。也不要觉得用vim 的比用 vscode 或者其他编辑器的高级，就高人一等。vim自身也在吸收其他技术不断的成长，例如它从 vscode 那边学来了LSP 和 DAP 。这就有点像武侠小说中的吸功大法，集万物所长为我所用。另外一条建议就是千万不要拿我给出的配置直接来进行使用。这一套配置仅仅是为了教学使用，很多地方没有进行深度定制，并且基本采用白话的写法，完全不考虑封装性和程序设计，另外我也没有考虑通用性，很多小伙伴评论出现了各种各样的问题，最后就是它的效率也不算高。我也不希望自己的文章仅仅给各位小伙伴提供了一套配置。我更希望小伙伴们能通过这一系列文章学到一点东西，从这套配置中衍生出一套适合自己的内容。若干年以后，各位小伙伴在对vim有更深的理解回过头来看到这套配置时可能发出这样的声音：“这是什么破烂配置，连 xxx 的支持都没有；有些功能有时候会报错，我看看把它改好；启动时间咋这么慢，我能把它优化到xx毫秒；现在还在用xx技术早就落伍了，看我把它改成用xx技术”。（我自认为本系列最有价值的是开始配置之前，vim相关特性的介绍）最后的一条建议就是，如果各位小伙伴未来将长时间使用vim 进行代码的编写和日常的开发。那么我推荐使用一些社区比较活跃的第三方通用配置，例如我最近在使用的lunarVim。使用这类的配置有一些好处：不用费力折腾配置，节约时间学习高手的配置，提升自己对编辑器的审美。就像没学习vim之前我一直觉得使用编辑器用鼠标选中文本是天经地义的事，我习惯了它，甚至习惯了用鼠标翻页等操作，完全不知道这样有多么的浪费时间。通过高手配置可能能使你重新审视自己使用编辑器的习惯，从而找到一套真正适合自己的高效的文本操作术。社区活跃的话，除了问题不用自己死磕，可能有人能帮忙解决PS: 如果各位觉得我的教程不好或者有些内容没有提到，各位可以去看看lunarVim作者的另一个项目，Neovim-from-scratch 该项目也是从0开始配置vim，并且在油管上有对应的教学视频。后面的学习通过本系列的学习相信各位小伙伴已经有能力能看懂各种第三方配置的代码，能在此基础之上衍生出一套属于自己的配置。甚至能完全抛弃第三方配置独立弄出一套自己的配置。所以后面我推荐的学习路线就是：不断阅读vim官方手册熟练使用某一个第三方配置在熟练的基础之上根据自己的习惯来定制一些只属于自己的功能形成一套只属于自己的科学的、高效的文本操作习惯根据这套习惯尝试定制自己的配置在其他编辑器中通过一定的配置尝试复刻这一套科学而又高效的操作习惯目前我正在第三部分努力。希望本系列文章能带领大家真正入门vim ，不会再出现因为觉得难而中途放弃。最后祝愿各位小伙伴在vim的使用中能收获快乐，并坚持下去！
很抱歉又拖更了这么久了，在这个新公司我想快速度过试用期，所以大部分的精力主要花在日常工作上面。但是这个系列还是得更新下去，平时只能抽有限的业余时间来准备。这就导致我写这些文章就慢了一些。废话不多说，咱们正式开始有关git相关的配置。这些配置都是根据我自身使用习惯来定义的，不一定符合各位的习惯，各位可以根据自身的习惯来调整gitsigns第一个要推荐的插件是 gitsigns。就像它的名字一样，该插件可以将最近的更改以标签的形式展现出来方便我们查看。我们可以使用这样的代码进行安装 use {'lewis6991/gitsigns.nvim' }。需要注意最新的版本需要 neovim 的版本在0.7 以上。安装完成之后我们通过配置 require('gitsigns').setup()来使用它。这样我们就可以通过 Gitsigns toggle_signs来打开或者关闭符号显示了。除了采用最基本的符号显示以外，它还可以对改变位置的行号进行标记以及高亮显示变更的行。这两个功能可以通过 Gitsigns toggle_numl和 Gitsigns toggle_linel来打开，打开之后显示如下：从图中可以看到，更改行行号被用绿色显示了出来并且更改行进行了高亮显示另外它还有其他的显示效果：它主要的一些显示功能主要有下面几个：toggle_signs: 显示变更记录toggle_numl: 显示变更行号toggle_linel：高亮变更的行toggle_delete: 显示被删除的行，以红色背景高亮显示toggle_word_diff: 在两行分别显示修改前和修改后的内容toggle_current_line_blame: 在对应行后面显示提交记录我们将所有的这些功能都打开将得到这么一个效果是不是看着有点乱？这是我修改了一处的，一旦修改多了看着会更混乱。所以我自己的经验告诉我在这个buffer里面最好是只打开 signs、numl、linel、current_line_blame的功能，其他的都关掉。也就是在当前buffer中只显示现在的代码，然后辅助以简单的符号来显示哪行是新加的，哪行被删除了，哪行被修改了，至于修改前是什么样子的，我可以通过其他方式来查阅。所有内容都在一个buffer 中显示会比较乱，不利于阅读代码。我们可以通过配置将我们要显示的内容进行定义，也可以定义使用何种图标来表示修改记录。我们采用如下的配置gitsigns.setup({ signs = { add = {hl = 'GitSignsAdd' , text = '+', numhl='GitSignsAddNr' , linehl='GitSignsAddLn'}, change = {hl = 'GitSignsChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn'}, delete = {hl = 'GitSignsDelete', text = '-', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn'}, topdelete = {hl = 'GitSignsDelete', text = '-', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn'}, untracked = {hl = 'GitSignsAdd', text = '+', numhl='GitSignsAddNr' , linehl='GitSignsAddLn'}, changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn'}, }, signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` numhl = true, -- Toggle with `:Gitsigns toggle_numhl` linehl = true, -- Toggle with `:Gitsigns toggle_linehl` word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` watch_gitdir = { interval = 1000, follow_files = true }, attach_to_untracked = true, current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` current_line_blame_opts = { virt_text = true, virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' delay = 1000, ignore_whitespace = false, }, current_line_blame_formatter = '<author>, <author_time:%Y-%m-%d> - ', sign_priority = 6, update_debounce = 100, status_formatter = nil, -- Use default max_file_length = 40000, -- Disable if file is longer than this (in lines) preview_config = { -- Options passed to nvim_open_win border = 'single', style = 'minimal', relative = 'cursor', row = 0, col = 1 }, yadm = { enable = false }, })其中大部分都是官方给出的默认配置，我只是在此基础之上改了 signs在各种状态下显示的图标，以及显示哪些内容。主要是使用 + 来表示新增、| 来表示修改、- 表示删除。除了显示以外它有一个重要的功能就是在各种修改状态之间跳转，例如调用 next_hunk来跳转到下一个更改位置。并且它也集成了一些git的操作。我们对常用的操作定义一些快捷键on_attach = function() vim.api.nvim_set_keymap("n", "<leader>gj", "<cmd>Gitsigns next_hunk<CR>", {silent = true, noremap = true}) vim.api.nvim_set_keymap("n", "<leader>gk", "<Cmd>Gitsigns prev_hhunk<CR>", {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hs', ':Gitsigns stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('v', '<leader>hs', ':Gitsigns stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hr', ':Gitsigns reset_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('v', '<leader>hr', ':Gitsigns reset_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hS', '<cmd>Gitsigns stage_buffer<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hu', '<cmd>Gitsigns undo_stage_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hR', '<cmd>Gitsigns reset_buffer<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hp', '<cmd>Gitsigns preview_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hb', '<cmd>lua require"gitsigns".blame_line{full=true}<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>tb', '<cmd>Gitsigns toggle_current_line_blame<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hd', '<cmd>Gitsigns diffthis<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>hD', '<cmd>lua require"gitsigns".diffthis("~")<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('n', '<leader>td', '<cmd>Gitsigns toggle_deleted<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('o', 'ih', ':<C-U>Gitsigns select_hunk<CR>', {silent = true, noremap = true}) vim.api.nvim_set_keymap('x', 'ih', ':<C-U>Gitsigns select_hunk<CR>', {silent = true, noremap = true}) end这里是我将官方给出的配置直接粘贴了过来，这堆快捷键定义的操作中我最常用的就是 next_hunk和 prev_hunk来在各种修改之间进行跳转。虽然偶尔也用用 diffthis来显示差异，但这部分我更喜欢使用我接下来介绍的插件diffview这个插件从名字上看就知道是专门用来查看版本差异的插件。与前面介绍的 gitsigns插件相比它有下面几个优点：它是专门用来显示差异的，与gitsigns相比，显示的更加明显它可以在文件树中显示有变更的文件它可以做到任意版本之间的差异对比它可以显示单个文件的版本提交记录它还有另外的功能，可以由各位小伙伴根据官方文档自行了解。它的使用方式如下：:DiffviewOpen显示当前与上一个版本之间的差异:DiffviewOpen + 版本号 可以显示当期与某一个特定版本的差异，例如 :DiffviewOpen HEAD~2或者:DiffviewOpen 906ddac317来查看版本差异● :DiffviewFileHistory + 文件名 来查看某个文件的版本差异因为它比较简单，具体的用法就不在这里演示了。关于它的配置，我自己认为平时很少使用，而且默认的配置已经够用了，也不太需要花精力为它优化快捷键了。lazygitlazygit是一个非常好用的git客户端，可以方便的进行提交、回滚、查看变更等git操作。这里我不推荐什么插件，因为它本身已经很强大了，而且脱离vim它也可以很好的工作。想来想去只有配置一个快捷键来快速打开 lazygit 的终端不知道各位小伙伴是否还记得之间介绍的 toggleterm 插件，我们将要依赖它来快速启动local lazygitterm = Terminal:new({ cmd = 'lazygit', direction = 'float' }) function lazygit_toggle() lazygitterm:toggle() end vim.api.nvim_set_keymap("n", "<leader>lg", "<Cmd>lua lazygit_toggle()<CR>", {noremap = true, silent = true})通过这样简单的配置我们已经可以使用 <leader>lg来快速启动 lazygit的客户端了。至此我们关于git的配置就完成了。一般我的使用习惯是使用 gitsigns来在更改中进行跳转，用于提交前或者合并分支前的代码审查，做到提交和合并都心中有数。在发生bug要回溯代码并且查看当前与没有问题的版本之间的差异会用到 diffview插件。在进行提交、合并、回溯等git相关操作时会使用到 lazygit。各位小伙伴也可以根据自己的使用习惯来定制这一部分的配置。
很抱歉这么久才来更新这一系列，主要是来新公司还在试用期，我希望在试用期干出点事来，所以摸鱼的时间就少了。加上前面自己阳了休息了一段时间。在想起来更新就过去一个多月了。废话不多说了，让我们开始进入正题。在前一章，我们谈论了如何在 neovim 中使用cpptools 这个DAP 的适配器对代码进行调试，目前针对编译型和解释型语言来说我们都有了对应的方法来配置调试器对其进行调试。本节将要介绍关于dap的其他一些功能，主要包括 repl窗口和 gdb的集成repl 窗口什么是 repl 呢？它的全称是 Read Eval Print Loop 中文一般翻译为交互式解析器，可能看到这你还是一脸懵逼，你可以想想 python 或者nodejs，在控制台输入python 就可以进入到它的交互式解析器中，随着我们输入python 的语句，它会实时的给出运行的结果。交互式解析器就是这么一个东西，输入命令，它给你一个实时的结果。在调试中使用交互式解析器还是很有用的，比如我想显示当前某个变量的值，当前执行到哪个语句了等等。nvim-dap已经提供了一个内置的 repl 窗口，我们每次启动调试的时候都会看到它每次都会创建一个新的名为 dap-repl的buffer。之前我们没怎么关注它主要还是因为那个时候的重点在于如何进行调试，现在我们将要来优化这部分的显示和功能。首先我们发现每次调试结束的时候这个buffer 都会被遗留，需要我们手动的进行关闭，除了针对buffer通用的 :q 或者 :bd命令进行关闭，还可以使用 :lua require('dap').repl.close()。当然也可以配套使用 :lua require('dap').repl.open()来打开一个 repl的窗口，既然每次都会自动新建，那么这里我们就不需要进行新建，主要用于想办法关闭就可以了。还记得之前介绍 nvim-dapui 插件的时候介绍的那两个监听函数吗，同样的我们要在监听调试结束的函数中添加代码来关闭repl 窗口，函数的整个代码如下dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close({}) dap.repl.close() end dap.listeners.before.event_exited["dapui_config"] = function() dapui.close({}) dap.repl.close() end再启动调试的时候发现它已经可以在调试结束的时候自动关闭了。还有另外一个问题就是我不太喜欢现在这样在最下角显示 repl。我希望它能够在最下方以整行的形式显示。或者可以方便的不显示，只有在需要的时候显示。要达成这个目的我们需要修改 dapui 的配置。dapui 的配置主要以 element为基础，每个 element 代表一个提供对应功能的窗口。我们先来根据它默认的配置来讲解每部分的含义require("dapui").setup({ icons = { expanded = "", collapsed = "", current_frame = "" }, mappings = { -- Use a table to apply multiple mappings expand = { "<CR>", "<2-LeftMouse>" }, open = "o", remove = "d", edit = "e", repl = "r", toggle = "t", }, -- Use this to override mappings for specific elements element_mappings = { -- Example: -- stacks = { -- open = "<CR>", -- expand = "o", -- } }, -- Expand lines larger than the window -- Requires >= 0.7 expand_lines = vim.fn.has("nvim-0.7") == 1, -- Layouts define sections of the screen to place windows. -- The position can be "left", "right", "top" or "bottom". -- The size specifies the height/width depending on position. It can be an Int -- or a Float. Integer specifies height/width directly (i.e. 20 lines/columns) while -- Float value specifies percentage (i.e. 0.3 - 30% of available lines/columns) -- Elements are the elements shown in the layout (in order). -- Layouts are opened in order so that earlier layouts take priority in window sizing. layouts = { { elements = { -- Elements can be strings or table with id and size keys. { id = "scopes", size = 0.25 }, "breakpoints", "stacks", "watches", }, size = 40, -- 40 columns position = "left", }, { elements = { "repl", "console", }, size = 0.25, -- 25% of total lines position = "bottom", }, }, controls = { -- Requires Neovim nightly (or 0.8 when released) enabled = true, -- Display controls in this element element = "repl", icons = { pause = "", play = "", step_into = "", step_over = "", step_out = "", step_back = "", run_last = "", terminate = "", }, }, floating = { max_height = nil, -- These can be integers or a float between 0 and 1. max_width = nil, -- Floats will be treated as percentage of your screen. border = "single", -- Border style. Can be "single", "double" or "rounded" mappings = { close = { "q", "<Esc>" }, }, }, windows = { indent = 1 }, render = { max_type_length = nil, -- Can be integer or nil. max_value_lines = 100, -- Can be integer or nil. } })首先最上面的 icons 表示，各个部分显示的图标，这里分别定义了展开的，合并的以及当前位置的图标信息，我们可以观察一下变量栏或者调用栈显示信息的左侧就可以看到这里定义的图标。mappings 代表的是部分窗口动作定义的快捷键。例如上面定义的 expand = { "\<CR>", "" }表示可以在待展开项上按下回车或者鼠标左键双击来展开。element_mappings表示的是我们为某些窗口特意定制的一些快捷键。例如上面注释的是针对调用栈定义的快捷键。layouts代表布局，每个布局都有一个子table，而每个子table主要由 elements、size、position组成，它们分别代表采取该种布局的元素(也可以说是窗口)，窗口大小以及窗口的位置。窗口一般通过id来描述，每种窗口都有固定的ID，根据官方文档的描述，它支持这么几种窗口:scopes显示全局或者当前局部变量，它支持的操作主要是 edit编辑变量的值、expand展开结构化的变量、repl将变量拷贝到repl窗口stacks显示当前正在运行的线程以及它们对应的调用栈，它主要支持的操作是 open :运行代码到当前被选中的位置， toggle:打开或者关闭该窗口watches显示我们需要追踪的变量，它支持的主要操作是 edit: 输入想要追踪的变量或者给对应的变量赋值。expand: 展开结构化的变量，remove:删除当前监视的变量，repl:将变量拷贝到repl窗口breakpoints显示当前激活的断点。它支持的主要操作有 open:执行代码到当前选中的断点处, toggle :激活或者使当前断点无效repl显示repl窗口console显示控制台窗口这些窗口的这些操作的快捷键我们已经通过上方的 mappings做了定义了，只要保持光标在对应窗口然后按下快捷键就可以执行对应的窗口命令了。如果想要单独对窗口进行快捷键定义可以在element_mappings 中被注释的代码controls 部分配置的是在repl窗口上方显示的那一堆调试按钮。由于我们定义了一些快捷键，这些按钮没太大的作用。这里我不需要它显示调试用的按键，所以我就在 controls 项中设置 enabled = false 禁用它。floating、window、render则定义的是悬浮窗口样式和普通窗口的一些样式。这里就不深究了。下面是改造之后的配置dapui.setup({ icons = { expanded = "", collapsed = "", current_frame = "" }, mappings = { -- Use a table to apply multiple mappings expand = { "<CR>", "<2-LeftMouse>" }, open = "o", remove = "d", edit = "e", repl = "r", toggle = "t", }, layouts = { { elements = { { id = 'scopes', size = 0.35 }, {id = "stacks", size = 0.35}, {id = "watches", size = 0.15}, {id = "breakpoints", size = 0.15}, }, size = 40, position = "left", }, { elements = { "repl" }, size = 5, position = "bottom", } }, controls = {enabled = false}, floating = { max_height = nil, -- These can be integers or a float between 0 and 1. max_width = nil, -- Floats will be treated as percentage of your screen. border = "single", -- Border style. Can be "single", "double" or "rounded" mappings = { close = { "q", "<Esc>" }, }, }, windows = { indent = 1 }, })repl 窗口的主要命令如下:.exit: 退出/关闭一个 repl 窗口.c/.continue: 继续执行代码.n/.next: 执行下一行代码.into: 跳转到函数中继续执行.out: 跳出函数.scopes: 打印当前栈的一些变量信息.threads: 打印线程信息.frames: 打印当前线程的调用栈.capabilities: 打印当前适配器实现的一些功能.p 暂停当前运行的程序更多的命令可以通过在 repl窗口中输入 .help查看看了这么多无聊的文字描述，不知道小伙伴们有没有觉得头晕眼花呢？我们来通过实际的例子来看看如何应用这些内容来进行调试。示例1：调试单线程死循环假设有一段程序在不知不觉中被写成死循环了，程序无法正常执行下面的操作，我们以下面的程序为例#include <stdio.h> #include <unistd.h> void loop_forever(){ for(unsigned int i = 10; i >= 0; --i){ // do something sleep(0.1); } } int main (int argc, char *argv[]) { // do something loop_forever(); printf("do other things\n"); return 0; }本来我们想的是它执行完函数 loop_forever之后会执行接下来的操作，但是我们死活看不到它执行后面的操作，这个时候我们意识到它可能在某个地方陷入死循环，无法出来了，假设前后都有大断的代码，无法快速定位到死循环的位置，该如何处理这种情况呢？我们先通过<F5>来执行操作，然后在 repl 中输入 i 进入插入模式，然后执行.p 中断当前程序执行。此时程序已经断了下来，接着我们输入.frames 查看当前调用栈信息。我们发现此时程序停留在loop_forever 函数的 sleep 中，我们在栈中找到 sleep 的位置并按下回车，这个时候我们发现程序执行到了 sleep() 函数处了。这个时候我们在这里按下<F9>下一个断点，接着使用 <F5> 继续运行到断点位置停止，这个时候我们通过实时显示的 i值已经发现问题所在了。原来i 递减到0之后，继续递减，因为它是无符号数所以永远无法达成小于0 的条件。示例2：调试多线程死锁我们以下面一个多线程程序为例#include <cstdio> #include <thread> #include <vector> using namespace std; void func(int thread_id){ int var = thread_id; while(true); } int main (int argc, char *argv[]) { vector<thread> vec; for(int i = 0; i < 5; i++){ vec.push_back(thread(func, i)); } // join for(int i = 0; i < 5; i++){ vec[i].join(); } return 0; }因为使用了c++ 相关的内容和 thread 库，因此编译的时候需要使用 g++ 并且指定链接 pthread 库，我们采用 g++ main.cpp -g -o main -lpthread 来编译它我们还是按照之前的步骤，先按下 .p来暂停程序，这个时候我们发现它会提示我们需要暂停哪个线程，遗憾的是根据线程的id还没法判断具体哪个是子线程哪个是主线程。这里我们随便选一个暂停。然后执行 .threads查看当前线程信息，在某个线程下使用回车键可以看到调用的函数栈。我们发现子线程卡在while 这句话，我们还是一样在卡主的位置按下回车跳转到对应代码位置，在此处下一个断点。然后我们在对应线程位置按下 o 命令来继续执行之前暂停的线程。这样就完美的找到了线程卡死的位置了。后面可以使用 .c 来继续执行所有被中断的线程nvim-gdb 插件该插件提供了一种方式，可以直接在neovim中进入gdb的session。例如我们可以通过命令 :GdbStart gdb -q a.out来启动一个gdb会话，并且关联了一个 a.out 的程序。后续可以直接使用gdb相关的命令来启动调试这个程序。我们可以使用如下的代码来安装它use {"sakhnik/nvim-gdb", run="./install.sh"}我们先来试试效果，直接使用快捷键 <leader>dd 来加载一个程序进行调试。进入到gdb会话之后可以使用gdb 的命令。例如我们使用 b main来在 main函数的位置打一个断点，然后通过r来启动程序运行到断点处。接着可以使用 n来执行下一步或者使用 c来直接运行到下一个断点。最后可以使用 q退出基础配置我们发现使用 nvim-gdb 插件的时候会在对应代码位置显示断点或者当前执行行。这里我们对它做一些配置，先统一使用nvim-gdb和 nvim-dap这两种情况下的显示信息。先创建一个新的配置文件为 nvimgdb.lua作为它的配置文件。因为它暂时还不支持lua的配置所以这里我们使用vim原生的写法。vim.cmd([[ let g:nvimgdb_config_override = { \ 'sign_breakpoint': [''], \ 'sign_current_line': '', \ } ]])在这里定义了断点和执行到当前行的配置，这里只能统一图标，还无法做到完全像使用 dap那样显示。另外我们可以在 nvimgdb_config_override 这个变量中定义如下快捷键以保证在调试时拥有同样的体验 \ 'key_next': '<F10>', \ 'key_step': '<F11>', \ 'key_continue': '<F5>',最后我们发现当我们输入文件名进行调试的时候，它会一直闪屏。这是因为它会不断根据我们输入的内容在文件系统中匹配合适的可执行文件，为了解决闪屏问题我们屏蔽它的这个特性，我们可以使用如下的配置来解决这个问题 let g:nvimgdb_use_find_executables = 0 let g:nvimgdb_use_cmake_to_find_executables = 0显示窗口的配置定义了显示形式，我们来定义显示的窗口，这里我让它显示常用的像调用栈，变量，以及watch窗口。我们先使用如下代码来将窗口分割为左右两个部分 let w:nvimgdb_termwin_command = "rightbelow vnew" let w:nvimgdb_codewin_command = "vnew"根据官方文档的描述，nvimgdb_termwin_command 是终端窗口，用来显示repl 相关信息并且与用户交互，后面我们可以对它进行切割用于显示其他信息。nvimgdb_codewin_command 是源代码窗口。这两句代码可以形成一个左右分屏的界面，左侧显示代码，右侧显示repl窗口。在gdb成功加载之后，我们可以使用命令 :GdbCreateWatch info locals来创建一个显示当前变量的窗口。默认是新创建一个窗口来显示，但是我们可以在命令前加上 belowright来指定在右侧继续分屏，它会在右下角新建窗口来显示变量。需要查看其它窗口可以对应传入不同的参数，例如传入 breakpoints来显示所有断点信息。传入的参数就是gdb中接收的对应参数。有了这些基础我们就可以对其进行配置了，我们要实现的目标就是当gdb成功加载的时候自动加载这些窗口。在vim中要实现自动化我们目前知道有两种方式，第一种使用自动命令，第二种使用插件配置中提供的回调函数。遗憾的是在这个插件中我没有找到回调函数，因此我们只能采用自动命令这种方法。根据官方的文档，我们主要使用这么两个事件——NvimGdbStart和 NvimGdbCleanup。它们一个是成功加载gdb的时候触发，一个是关闭gdb会话的时候触发。vim.cmd(augroup GdbSession autocmd! autocmd User NvimGdbStart :lua StartGdbSession() autocmd User NvimGdbCleanup :lua EndGdbSession() augroup END)在 StartGdbSession 函数中写入如下代码来完成窗口的配置StartGdbSession = function() vim.api.nvim_command(":belowright GdbCreateWatch backtrace") vim.api.nvim_command(":wincmd h") vim.api.nvim_command(":belowright GdbCreateWatch info locals") vim.api.nvim_command(":set wrap") vim.api.nvim_command(":wincmd k") end不知道各位小伙伴能不能理解这段代码是如何在分屏的。首先启动gdb的时候会将整个屏幕纵向分为两个部分，左侧为 code右侧为 repl窗口接着我们执行 :belowright GdbCreateWatch backtrace 它会在右下方创建一个窗口用来展示调用栈然后执行 :wincmd h来将光标移动到代码窗口上继续执行 :belowright GdbCreateWatch info locals它会在代码窗口的下方新增一个窗口用于显示变量的信息这样就将窗口分为4个部分了，左上部分显示代码，左下部分显示变量信息。右上部分是repl窗口，右下部分显示变量信息。最后我们通过 :set wrap设置窗口中自动换行，不然有些内容显示在一行不容易查看。通过 :wincmd k移动光标到 repl窗口。方便后续调试启动之后他的效果如下最后我们在结束gdb的时候做一些收尾工作，关闭我们创建的窗口EndGdbSession = function() vim.api.nvim_command(":bdelete! backtrace info locals") end这里我是根据buffer的名称来进行删除。最后的效果如下到此我们已经介绍了关于dap 的所有配置，至于其他语言相信各位小伙伴根据官方给出的示例可以独立完成配置，这里就不一一介绍了。
首先给大家说一声抱歉，前段时间一直在忙换工作的事，包括但不限于交接、背面试题准备面试。好在最终找到了工作，也顺利入职了。期间也有朋友在催更，在这里我对关注本系列的朋友表示感谢。多的就不说了，我们正式进入vim 的配置吧上一节通过配置 python 的调试环境，我们大概了解了配置 dap 的基本步骤。首先需要一个 dap 的客户端负责在编辑器上显示各种调试信息，并且与用户进行交互。然后需要一个服务端，与客户端通信并完成调试的实际步骤。然后需要配置两个东西， dap.adapters 用来配置如何启动调试器，dap.configurations用来配置如何将当前项目加载到调试器上。本篇我们进一步配置 dap。让它变得更好用，并且介绍编译型语言(C/C++)调试的配置。优化界面回顾一下上一篇中在演示图片里面看到的效果。默认界面在断点位置以 B 来标识，当前运行的代码以 -> 来标识。看起来不那么的直观，我们先对它进行优化，我们采用 Visual Code 的调试图标来进行标识我们采用以下代码进行配置local dap_breakpoint_color = { breakpoint = { ctermbg=0, fg='#993939', bg='#31353f', }, logpoing = { ctermbg=0, fg='#61afef', bg='#31353f', }, stopped = { ctermbg=0, fg='#98c379', bg='#31353f' }, } vim.api.nvim_set_hl(0, 'DapBreakpoint', dap_breakpoint_color.breakpoint) vim.api.nvim_set_hl(0, 'DapLogPoint', dap_breakpoint_color.logpoing) vim.api.nvim_set_hl(0, 'DapStopped', dap_breakpoint_color.stopped) local dap_breakpoint = { error = { text = "", texthl = "DapBreakpoint", linehl = "DapBreakpoint", numhl = "DapBreakpoint", }, condition = { text = 'ﳁ', texthl = 'DapBreakpoint', linehl = 'DapBreakpoint', numhl = 'DapBreakpoint', }, rejected = { text = "", texthl = "DapBreakpint", linehl = "DapBreakpoint", numhl = "DapBreakpoint", }, logpoint = { text = '', texthl = 'DapLogPoint', linehl = 'DapLogPoint', numhl = 'DapLogPoint', }, stopped = { text = '', texthl = 'DapStopped', linehl = 'DapStopped', numhl = 'DapStopped', }, } vim.fn.sign_define('DapBreakpoint', dap_breakpoint.error) vim.fn.sign_define('DapBreakpointCondition', dap_breakpoint.condition) vim.fn.sign_define('DapBreakpointRejected', dap_breakpoint.rejected) vim.fn.sign_define('DapLogPoint', dap_breakpoint.logpoint) vim.fn.sign_define('DapStopped', dap_breakpoint.stopped)上面的代码主要配置了显示的颜色和图标。最终调试的效果如下图所示然后我们需要提供一个可用的界面用来显示调试过程中的各种信息，包括变量值和调用栈。完成这个工作的是插件 nvim-dap-ui 。我们使用如下的代码进行安装use { "rcarriga/nvim-dap-ui", requires = {"mfussenegger/nvim-dap"} }这个插件里面包装了很多调试相关的窗口，例如变量监控、调用栈等等。我们可以对他进行配置，让这些窗口元素出现在我们希望它出现的位置。为了加载这个插件我们还是按照之前的惯例，为它准备一个单独的配置文件，并且加载它。local dapui = require("dapui") dapui.setup({})我们可以使用该插件中的函数 toggle() 开打开或者关闭这些调试窗口。最终的效果就像这样每次都输入这个函数来打开和关闭调试窗口比较麻烦，因此我们这里可以使用以下代码来实现自动加载和关闭local dapui = require("dapui") dapui.setup({}) local dap = require("dap") dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open({}) end dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close({}) end dap.listeners.before.event_exited["dapui_config"] = function() dapui.close({}) end这段代码在 dap 的事件中注册了几个回调函数，当对应的事件发生时会调用对应的函数，我们在 dap 的调试启动时打开调试窗口，在结束时关闭调试窗口最后关于界面方面的优化再来推荐一个插件——nvim-dap-virtual-text 它的作用是在调试过程中，在变量附近事实显示变量的值。我们可以在 dap-ui 的配置文件中对他进行配置require("nvim-dap-virtual-text").setup({ enabled = true, enable_commands = true, highlight_changed_variables = true, highlight_new_as_changed = false, show_stop_reason = true, commented = false, only_first_definition = true, all_references = false, filter_references_pattern = '<module', virt_text_pos = 'eol', all_frames = false, virt_lines = false, virt_text_win_col = nil })上述的配置是官方给出的，我原封不动的复制过来了。它的效果如下图所示：配置c++基础调试环境终于到了本文最重要的环节了，就是配置 c/c++ 的调试环境，上一篇我们讲解了 Python 的配置，它代表了脚本类解释型语言的调试配置，C/C++ 代表了编译型语言的调试配置。针对 C/C++ 的调试我们选用 cpptools 作为 dap 的服务端。首先通过 MasonInstall cpptools 来下载安装它，也可以通过 :Mason 命令在图形化的界面上进行安装。然后我们还是按照之前的顺序来对他进行配置，首先配置它的加载方式local dap = require("dap") dap.adapters.cppdbg = { id = "cppdbg", type = 'executable', command = "~/.local/share/nvim/mason/bin/OpenDebugAD7", }这里我们设置它以 executable 的方式启动(在客户端调试时启动)。然后指定可执行程序的路径，如果这里报找不到 OpenDebugAD7 这种错误，可以将 ~ 改为 /home/user 这样的具体目录。然后我们配置一下客户端与服务器通信相关的内容dap.configurations.cpp = { { name = "Launch file", type = "cppdbg", request = "launch", program = function() return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file") end, cwd = "${workspaceFolder}", stopAtEntry = true, }, } dap.configurations.c = dap.configurations.cpp最后我们通过一个 dap.configurations.c= dap.configurations.cpp 让c++和 c使用同一个配置。因为 C/C++ 是编译运行的，在调试的时候其实调试的是它生成的可执行程序，所以这里每次在调试的时候需要手工指定要调试的可执行程序。最后别忘了在 ftplugin/cpp.lua 中加载它另外需要注意，因为可执行程序运行时是不依赖源代码的，但是调试的时候想让调试器能够准确的知道当前在源码的位置并且能够显示当前变量的值，这个时候需要在可执行程序中打包符号表，对于linux 的 C/C++ 程序来说，只需要在编译的时候给gcc/g++ 传递 -s 参数即可。我们写一个简单的 C程序来进行实验#include <stdio.h> int main (int argc, char *argv[]) { printf("hello world\n"); for (size_t i = 0; i < 10; i++) { printf("i = %ld\n", i); } return 0; }注意: 这里我们使用的调试器仍然是gdb, cpptools 只是在上层进行了一层封装。因此这里能调试的前提是安装了gdb 调试器到此我们将关于 dap 调试的部分都基本介绍完了。其实 dap 也并没有想象中那么难，目前从安装到配置使用，都有大量的插件来方便我们使用，而且官网上基本都有配置的介绍，没有特殊需求只需要将标准配置原样拷贝粘贴即可。下一篇我们将补充一些关于 dap 的其他内容，并介绍 neovim + gdb 的组合，敬请期待！
从零开始配置vim(28)——代码的编译、运行与调试
在前面几个章节，我们逐渐为 Vim 配置了语法高亮、代码的跳转和自动补全功能。现在的 Vim 已经可以作为代码编辑器来使用了。但是想将它作为日常发开的主力编辑器来用还需要很长一段路要走，其中一个就是要为它配置代码的一键编译与运行功能。这里我们仍然以 C 和 Python 为例。一个是需要编译运行的一个是直接就可以运行的，这两个语言应该能代表大多数语言的情况。自动运行C 语言的配置在之前 vim 入门的一系列教程中我们介绍过 vim 自带 make 命令的运行机制以及如何进行自定义。对于其他语言要实现这个自动编译运行的效果我们核心的操作就是在修改 make 命令。而 C/C++ 本身采用 make 命令来进行编译和运行，所以这里 C/C++ 我们直接采用 vim 自带的 :make 命令在之前 vim 入门的一系列教程中我们介绍过 vim 自带 make 命令的运行机制以及如何进行自定义。对于其他语言要实现这个自动编译运行的效果我们核心的操作就是在修改 make 命令。而 C/C++ 本身采用 make 命令来进行编译和运行，所以这里 C/C++ 我们直接采用 vim 自带的 :make 命令我们先创建一个 C 的工程。让后使用上一节的生成 hello world 的代码片段生成一个基本的程序。然后提供一个供 :make 命令使用的 Makefile 文件main.out: main.o gcc main.o -o main.out main.o: main.c gcc -c main.c clean: rm -rf *.o *.out run: ./main.out然后我们执行 make 用来编译。如果出错了，可以使用 quickfix 相关命令跳转到对应位置。我们一般的流程是 :make 进行编译，然后使用 :make run 来进行运行。把命令搞清楚了，下面就考虑如何加快这个流程，做到一键编译运行。我们的思路还是绑定快捷键。每种语言虽然定义相同的快捷键但是运行的命令不同，我们需要根据不同的语言类型绑定对应的命令。这个时候最好的办法就是在 filetype 的机制上完成绑定的操作。我们在 lua/lsp/cpp.lua 中绑定快捷键。local on_attach = function(client, bufnr) lsp_set_keymap.set_keymap(bufnr) -- 编译 vim.api.nvim_buf_set_keymap(bufnr, "n", "<F7>", "<cmd>make<CR>", {silent = true, noremap = true}) -- 编译运行 vim.api.nvim_buf_set_keymap(bufnr, "n", "<F5>", "<cmd>make run<CR>", {silent = true, noremap = true}) end到此我们关于 C/C++ 的配置就完成了。可能显的有些简单但是已经初步可用了，小伙伴可以根据自己的需求来进一步修改这个配置。使用这个配置的前提是 C/C++ 的工程中有已经定义好的 Makefile 文件Python的配置之前我们在讲解命令的模式的提到过可以使用 % 来代表当前 buffer 所对应的文件。所以 python 的配置就比较简单了。因为 Python 不需要编译，所以这里直接绑定 <F5> 来运行vim.api.nvim_buf_set_keymap(bufnr, "n", "<F5>", "<cmd>!python %<CR>", {silent = true, noremap = true})dap 配置我们经常看到有人配置 neovim 或者 vim 的时候会介绍到 dap ，那么什么是 dap 呢? dap 的全称是 Debug Adapter Protocol 从名称上看它又是一个协议。它为多种调试器提供了一层统一的适配抽象层。有点类似于前面的介绍的 lsp。只要在适配层提供接口的实现，那么在客户端，也就是代码编辑器这端可以不做任何修改的集成不同调试联想到 lsp 的配置，我们配置dap 首先需要的是有一个 dap 的客户端，用来向调试器发送各种命令，例如下断点、显示变量名等等。另外想要能够调试也需要有具体的调试器，用来接收处理这些命令。现在思路有了，我们 这里先以 Python 为例来介绍 dap 的基本配置。首先是需要一个客户端，用于通过 neovim下发各种调试命令并实时显示调试信息。截止到 0.7 版本 NeoVim 并没有在内部集成 dap 客户端的功能，需要我们单独安装相关插件来实现这部分的功能。这里我们使用的客户端是 nvim-dap 插件。我们先使用 use {'mfussenegger/nvim-dap'} 来安装它。接着我们来定义一下相关的快捷键，这里我喜欢使用 Visual Studio 的快捷键。各位小伙伴可以自行选择自己喜欢的快捷键。这里我希望在插入模式和选择中也可以使用这些快捷键，由于 vim.api.nvim_set_keymap 函数第一个参数只能有一个模式字符串，如果采用这个函数来定义快捷键，这里同样的代码我要写三次，为了简化代码，这里介绍一个新的函数 vim.keymap.set。它与 vim.api.nvim_set_keymap 函数支持的参数相同，只是它第一个表示模式的参数可以支持用字典来一次绑定到多个模式中。这样就简化了绑定快捷键的代码量。vim.keymap.set({"i", "n", "v"}, "<F5>", "<cmd>lua require'dap'.continue()<CR>", {silent = true, noremap = true, buffer = bufnr}) vim.keymap.set({"i", "n", "v"}, "<F10>", "<cmd>lua require'dap'.step_over()<CR>", {silent = true, noremap = true, buffer = bufnr}) vim.keymap.set({"i", "n", "v"}, "<F11>", "<cmd>lua require'dap'.step_into()<CR>", {silent = true, noremap = true, buffer = bufnr}) vim.keymap.set({"i", "n", "v"}, "<F12>", "<cmd>lua require'dap'.step_over()<CR>", {silent = true, noremap = true, buffer = bufnr}) vim.keymap.set({"i", "n", "v"}, "<F9>", "<cmd>lua require'dap'.toggle_breakpoint()<CR>", {silent = true, noremap = true, buffer = bufnr})这个函数是 0.7 以后的版本引进的，如果你的是0.7之前的版本，还是老老实实的多写几遍。另外我们这里绑定了 <F5> 快捷键，因此之前我们在 Python 中，绑定的直接运行的 <F5> 键的代码需要注释一下。我们想要真正实现调试，还需要配合调试器使用。前面说 dap 只是一层协议，需要客户端服务器按照这一层协议来实现相关功能，某些调试器可能自身支持这个协议，而某些可能不支持，这样就需要额外的配置来使调试器也能支持该协议。下面我们以 Python 为例先把整个调试环境搭建起来，先跑起来再说Lsp 在安装 Server 的时候有 nvim-lsp-installer 这样的插件来专门安装 LSP server 的，那么 dap 有没有类似的插件来安装 dap 调试器相关的服务呢？有！我们使用 mason 来管理 dap 的调试器。use { "williamboman/mason.nvim" }当初我推荐过 nvim-lsp-installer 插件作为下载、管理 lsp server 的工具。后来只是知道作者发布了新的管理工具，因为比较新怕出问题就没怎么关注，后来有好多小伙伴在评论区推荐，我仔细看了一下发现它已经支持 dap 服务的管理了。那还是使用它吧 ^_^。我们可以使用 Mason 打开一个带界面的 Lsp 和 DAP 的服务管理窗口，可以使用 数字键在上面进行跳转，找到想要的服务之后直接使用 i 来安装也可以使用 MasonInstall 来安装想要的服务。我们先在插件配置中删除与 nvim-lsp-installer 相关的配置，包括 packer 中对它的引用和 plugins-config 目录中的配置。下一步就是配置 dap 的客户端与 服务端的联动，这需要配置 nvim-dap 插件，根据官方的描述我们主要配置两个部分，第一个部分叫做适配器，主要配置我们加载哪个调试器，以及如何加载调试器。这一步需要提供如下的配置框架local dap = require('dap') dap.adapters.language = { }language 是具体的调试器例如 debugpy 这里的 language 就是 python了。既然与语言相关，我们自然的想到要用 ftplugin目录。为了方便管理，这里与 lsp 配置的组织形式类似，我们将所有关于 dap 的配置都放到 lua/dap目录中。并且按语言名称来命名，例如关于 python 的 dap 配置我们放到 lua/dap/python.lua 中。然后在 ftplugin/python.lua 中加载这个配置文件即。require("dap/python")然后在 python.lua 文件中写入以下配置local dap = require('dap') dap.adapters.python = { type = 'executable'; command = '/usr/bin/python3.8'; args = { '-m', 'debugpy.adapter' }; }其中各个参数的含义如下:type : 表示启动调试器的方式， executable 表示由客户端自行启动调试器； server 表示 调试器已经单独启动了，后续客户端只需要将调试请求发送到服务器即可。command: 表示启动调试器的命令args: 表示启动调试器的命令行参数由于 python 调试工具 debugpy 是一个 Python 的第三方模块，因此这里我们使用 python -m debugpy.adapter 来启动这个调试器。接着我们需要针对语言来配置如何进行调试。它的配置都放在一个名为 dap.configurations.language 的 字典中。language 代表的是当前文件的文件类型名, 所以针对 Python 来说这里需要填写的是 dap.configurations.python。它的配置如下所示:dap.configurations.python = { { type = "python"; request = "launch"; name = "launch file"; program = "${file}"; pythonPath = function () return "/usr/bin/python3.8" end }, }各参数的含义如下:name : 是一个字符串它表示当前配置的名称，你可以理解为一个idtype: 使用哪个调试器，跟我们之前配置的 dap.adapters相关request：调试的方式，支持 attach 附加到一个已有的进程或者 launch 启动一个新进程。由于在上一步我们指定由客户端来启动调试器，因此这里应该选择 launch 来启动一个新调试进程program: 需要调试的代码， ${file} 表示当前 buffer 所对应文件pythonPath： 执行该文件需要使用的 python 解析器路径这样我们在某一个打开的文件上按下 <F5> 的时候，它会通过 pythonPath 指定的解析器来执行脚本，并且会按照配置中 request 指定的方式来打开一个新的调试器进程。并且将对应的调试命令发送到调试器完成调试工作。好了，到此为止我们配置了最基本、最简单的dap 调试。现在只是有一个勉强能用的调试工具，距离好用还差的很远，下一篇里面我们首先会对 dap 功能进行增强，美化，并讨论如何针对 C/C++ 这种编译型的语言进行调试。最后感谢各位给我提意见的小伙伴，谢谢大家!