主要记录相关软件和环境配置过程的问题以及解决方案
在日常的工作生活中有各种各样的琐事，如果不及时记录下来很可能后面就忘了。或者在日常软件开发中有很多想法想要实现但是时间一长自己就忘了。这种情况下我们需要一个系统来记录收集想法并且后续需要追踪甚至回顾。我们需要一套适合的管理系统让大脑摆脱待办事项的纠缠，让大脑专注于当前要做的事物。目前我采取的方式是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来编写文档至少也显得赏心悦目了。
之前我介绍了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
周末结束过来上班的时候发现公司电脑上安装的wsl2 中的arch和Ubuntu 都无法打开了，表现出来的现象是通过终端打开系统时长时间无法进入系统，等待大概半小时左右会出现提示：Wsl/0x80080005通过网络上查找，这种一般是系统坏了，或者是服务没启动，又或者是网络协议栈出现问题。对应的办法就是重置Linux系统(这种是最坏的情况，一般留到最后实在没办法了再尝试)、启动 LxxManager 服务，重置网络协议。通过尝试我发现我的属于网络协议栈问题，通过powershell 命令(需要管理员权限)netsh winsock reset # 重置网络套接字 netsh int ip reset # 重置 IP 协议然后重启电脑问题就解决啦!
在介绍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的功能并没有配置完，后面我计划加上自动编译运行、调试等功能，这些就在后面的文章中给出
在前面的章节中，介绍了 vertico 体系的补全，已经实现了在各个buffer中的补全功能，但是作为程序员，使用Emacs主要用来编程，对于编程来说上述的补全体系仍然不够完整，我们还需要基于lsp的补全以及基于代码片段模板的补全，这里先介绍代码片段。在之前写vim的配置时已经介绍过代码片段的概念以及基于vim 的配置，感兴趣的读者可以自行在我往期的博客中寻找，这里就不对它进行详细的介绍了。直接来使用它。在Emacs中，我们使用插件 yasnippet 来支持这个功能(use-package yasnippet :ensure t :hook ((after-init . yas-reload-all) ((prog-mode LaTeX-mode org-mode) . yas-minor-mode)) :config (setq yas-snippet-dirs '("~/.emacs.d/snippets")))我们通过上面的代码就能把它启用起来，yas-reload-all 表示在Emacs启动时重新加载所有已启动的代码片段；yas-minor-mode代表启动代码片段补全的功能。但是我们做了限制，只在编写代码、LaTeX和org-mode 文件时启动。如果各位读者希望在markdown 中也启动的话，也可以加上 markdown-mode 的选项。但是既然使用了Emacs，当然要用比 markdown 更加强大的 org-mode 了现在Emacs已经支持了代码片段，它的快捷键与vim中的一致都是使用tab 来进行补全。那么接下来就是通过代码片段文件来提高我们编程的效率了。添加代码片段文件我们可以使用已经写好的，也可以自己根据自己的习惯来定制。下载通用的代码片段官方虽然提供了一些代码片段，但是数量较少，使用起来并不那么方便，我们可以下载网络上的一些公共库来丰富我们的代码片段。这里我介绍一下 Doom Emacs Snippets Library。它是专门为doom emacs 用户打造的代码片段库，使用者不少。其他的代码片段库的安装与使用与它类似，读者可以依葫芦画瓢来安装。 首先找到官方的代码仓库，然后将它克隆下来,我将它放到 ~/.emacs.d/emacs-snippets 目录中。然后通过(use-package doom-snippets :ensure nil :load-path "~/.emacs.d/doom-snippets" :after yasnippet)来加载它。之后需要 Yasnippet 插件能找到这个目录，所以我们修改一下上面的配置(use-package yasnippet :ensure t :hook ((after-init . yas-reload-all) ((prog-mode LaTeX-mode org-mode) . yas-minor-mode)) :config (setq yas-snippet-dirs '("~/.emacs.d/snippets" "~/.emacs.d/doom-snippets"))) ;; 添加一行代码片段的加载路径此时再编写代码发现多了一些代码片段自定义代码片段与 doom emacs snippets library 代码片段类似，我们也需要按照具体的mode来组织代码片段的文件目录。每一个关键字一个文件例如~/.emacs.d/snippets/ ├── python-mode/ │ ├── for ; for 循环模板 │ └── class ; 类定义模板 └── c++-mode/ └── foreach ; for-each 循环模板有了这些准备的前提条件，我们可以自行创建自己的代码片段了。在emacs 中执行 yas-new-snippet 可以打开一个新的buffer，并显示代码片段的模板# -*- mode: snippet -*- # name: # key: # -- name后面跟着的是代码片段的名称，在补全时会以补全项显示这个名称；key代表关键字，表示用哪个缩写来触发。例如我们定义一个 prt 来展开成 printf 语句，那么就可以写成 name:prt key: prt尝试生成第一个代码片段后面的写具体展开的代码，例如我们编写一个自动生成hello world的模板# -*- mode: snippet -*- # name: create-hello # key: hello # -- #include <stdio.h> int main(int argc, char* argv[]) { printf("hello world\n"); return 0; } 在编写完成之后使用快捷键 C-c C-c 来保存，Emacs在保存时会让你输入对应的 mode 并且给出需要保存的位置。当我们修改完适用的 mode 和 保存的位置之后，我们发现它在对应位置生成了文件。重启Emacs或者 使用命令 yas-reload-all 来重新加载所有的代码片段。之后输入hello就可以看到它为我们生成了 c 版本的 hello world 程序占位符我们希望的代码大体上不变但是关键地方由我们自己来填充，这个时候可以使用占位符。占位符的格式为 ${} 大括号中填入数字序号。或者使用 ${1: default} 来表示这个位置默认填写default字符。下面是一个具体的例子# -*- mode: snippet -*- # name: qt-class # key: qcls # -- class ${1} : public ${2:QObject} { Q_OBJECT public: ${1}(${2}* parent = nullptr); ${1}(const ${1}& other); ${1}& operator=(const ${1}& other); ~${1}(); signals: slots: protected: }上面我们定义了一个快速生成继承自 QObject 类的子类的模板，上面有两个占位符，第一个占位符表示名称，它没有默认值，一旦确定了它的也就是类型，它会自动生成类的无参构造、拷贝构造、赋值运算符重载和析构函数的定义。第二个占位符指出了它的父类，它的默认值是 QObject 类，它的效果如下使用elisp代码生成动态信息我们可以使用 `` 来包裹elisp代码，生成动态信息。一般在C/C++ 代码的头文件中会包含一些注释信息，用来说明文件的用途包含的接口信息等等。我们希望生成这么一段注释的代码片段# -*- mode: snippet -*- # name: Custom File Header # key: hdoc # -- /** * @file `(file-name-nondirectory (buffer-file-name))` * @bref ${1:brief description} * @details ${2:detailed explanation} * @date `(format-time-string "%Y-%m-%d")` * @commit history: * \t v${3:1.0}: ${4:Initial version} */ $0上面我们使用 elisp代码 (file-name-nondirectory (buffer-file-name)) 来获取对应的文件名，通过 (format-time-string "%Y-%m-%d") 来获取时间。并且最后使用 $0 来将光标定位到这个位置，这个位置将会在我们使用tab处理过所有tab之后自动跳转。本文到此就结束了。本文主要介绍了在Emacs中使用代码片段，并且介绍了编写代码片段的一些方式。有了这些在编写代码时会更加得心应手。
Emacs 号称一个伪装成文本编辑器的操作系统，你几乎可以在Emacs中干任何事情。在Emacs中运行终端自然是小菜一碟。Emacs中有许多其他类型的shell，例如 eshell、term、vetrmterm 终端其中 term 是Emacs中自带的shell，它最终是调用系统中安装的其他shell环境。我们可以直接使用 M-x 调用 term命令，来选择启用一个系统终端。我们可以在这个终端上进行一些操作。但是还有一些执行细节需要优化。控制term窗口的显示首先我们看到在启动终端之后，它占用了一整个窗口，这对于我来说并不友好。一般情况下我只是希望在编写代码之后临时的调用终端执行一些命令，例如编译或者运行代码之类的操作。因此我希望它可以放到下方并且可以快速的关闭退出我们可以通过 shackle 插件来控制窗口的行为。主要有：方向、大小和弹出方式等等。配置 shackle 插件最重要的是变量 shackle-rules 。它的构成如下: CONDITION(:regexp) :select :inhibit-window-quit :size+:align|:other :same|:popupCONDITION 是相关的条件，表示我们要定义哪一类窗口的行为。这个条件可以是正则表达式，可以是字符串，可以是modeselect 控制弹出窗口后是否选中size: 0到1间的数字，控制窗口宽度和高度占整个窗口区域的百分比inhibit-window-quit: 按q退出时不删除这个缓冲区align: 弹出窗口的对齐方式，取值有 ’left, ‘right, ‘below, ‘aboveother: 如果当前 frame 有多个 window，是否复用另外一个 windowpopup: 弹出一个新的 window，而不是复用当前 windowsame: 不弹出 window，复用当前 windowignore: 禁止显示该窗口我们当前的配置可以这么写(use-package shackle :ensure t :hook (after-init . shackle-mode) :custom (shackle-default-size 0.5) (shackle-default-alignment 'below) :config (setq shackle-rules '((term-mode :regexp t :select t :size 0.3 :align t :popup t :quit t))))这里表示 term 窗口在弹出之后占据下方30%的区域进行显示，并且退出后直接关闭窗口。这样我们可以看到使用 term 之后窗口显示在下方，并且像普通buffer那样可以使用vim的命令 :q 来退出快速切换显示和隐藏状态对于终端这种临时窗口，我们一般不会让它长时间驻留在buffer中。想要的时候随时调用、不要的时候随时关闭或者隐藏。对于这种快速打开关闭的需求，我们可以使用 popper 插件。它通过赋予任意缓冲区“弹出”状态，让这些缓冲区不打扰你的核心工作区，只需一键即可调用或隐藏，完美适用于那些需要即时访问但又希望不影响视线的场景。popper-toggle、popper-cycle 和 popper-toggle-type 是三个核心命令。popper-toggle: 快速切换最新一个弹出缓冲区的显示/隐藏状态popper-cycle: 循环切换所有标记为弹出的缓冲区popper-toggle-type: 切换特定类型缓冲区的显示/隐藏状态另外它需要通过 popper-reference-buffers 来定义特定类型的缓冲区，插件会将符合这些规则的缓冲区标记为 POP并且进行管理。popper-reference-buffers 支持正则表达式来匹配缓冲区名。我们可以使用下面简单的配置(use-package popper :ensure t :hook (after-init . popper-mode) :init (setq popper-reference-buffers '("\\*Messages\\*" "\\*Async Shell Command\\*" term-mode help-mode helpful-mode "^\\*eshell.*\\*$" eshell-mode "^\\*shell.*\\*$" shell-mode ("\\*corfu\\*" . hide))) :config (my-leader-def "/" #'popper-toggle "t" #'popper-cycle "T" #'popper-toggle-type) )最终它的效果如下:eshelleshell 与 term 最大的不同在于eshell 是Emacs 自己通过elisp实现的一个仿制的终端，而 term 是真实的系统终端，它调用系统中的shell环境。目前来说使用 term 来打开系统终端能应付大部分的情况，但是学习Emacs，eshell似乎是一个绕不开的坎，并且eshell配置好了，同样好用。eshell里面实现了普通shell里面常用的命令，例如 ls、cd、cp 等等。同时也可以执行elisp代码,例如可以通过find-file 打开一个文件。或者输入简单的算术表达式像 (+ 1 1) 来进行一些数学计算。封装shell命令到eshelleshell里面实现了常见的shell 命令，但是我们在后续会经常性的安装一些好用的工具，来简化我们的工作流程，所以在使用eshell的第一件事就是将shell命令封装到eshell这里我以 autojump 为例。autojump 是一个快速跳转到指定目录的工具，当你通过cd进入过的目录会被记录下来，下一次通过 autojump 不再需要输入全路径，而是直接模糊匹配进行跳转，例如我们多次进入了 ~/.emacs.d/lisp 目录，下一次不管当前目录在哪里只需要输入 j lisp 即可进入。我们希望复刻这种行为到eshell中。定义eshell 中的命令可以通过定义一个以 eshell/ 开头的函数来实现，函数名后面的名称就是可以输入eshell的命令名称，所以这里我们要定义一个名为 eshell/j 的函数。同理，我们想要使用eshll中定义好的命令例如 cd ,可以直接调用 eshell/cd 函数。我们还是使用 use-package 来管理关于 eshell 的配置，最终实现的代码如下:(use-package eshell :ensure nil :config (defun eshell/j (&rest args) (let* ((argstr (mapconcat 'identity args " ")) (target (shell-command-to-string (concat "autojump " argstr)))) (setq target (replace-regexp-in-string "\n" "" target)) (if (file-directory-p target) (eshell/cd target) (error "目录不存在: %s" target)))))上述代码主要通过 autojump 命令来根据输入的目录名称来匹配一个实际对应的路径全名，然后通过 eshell/cd 来完成跳转操作。为什么不直接执行 j 来实现跳转呢？eshell是emacs模拟的shell并不是真实的shell，它是与Emacs绑定的，外部的shell命令无法改变 eshell的环境包括它当前所在的目录。因此我们需要调用eshell自身的命令来改变它的环境。最终的效果如下eshell 的 alias在普通的shell中，我们可以通过 alias 定义命令的别名，例如常用的 ll 或者我喜欢将 vi 和 vim 改成 nvim。eshell中也可以设置别名eshell 中使用 alias 来设置别名，它的语法如下:alias 别名 '命令 [参数] $@*'参数部分传入 $1、$2 等等，代表传入的第一个、第二个参数，或者直接使用 $@* 表示接受输入的所有参数，例如我们定义一下 ll 这个别名alias ll 'ls -l $@*' ; 输入 `ll` 等价于 `ls -l`在 eshell 中输入的 alias 命令仅仅在当前 eshell 环境中生效，想要永久生效可以将上述命令写入到 ~/.emacs/eshell/alias 文件中。eshell 会通过函数 eshell-read-aliases-list 加载对应文件中定义的别名，保存别名的文件路径被保存在 eshell-aliases-file 中，它默认的值为 ~/.emacs.d/eshell/aliaseshell history普通shell中有一个 history 命令可以查看保存的历史，并且可以使用方向键的上和下来输入上一条或者下一条命令。eshell 虽然也能这么干，但是eshell 因为是elisp模拟的终端，背靠Emacs这座大山，它可以配合Emacs的相关插件更高效的利用history命令。这里我们可以配合 consult-history + orderless 来模糊匹配命令(use-package em-history :ensure nil :defer t :custom (eshell-history-size 1024) (eshell-his-ignoredups t) (eshell-save-history-on-exit t)) (use-package esh-mode :ensure nil :hook (eshell-mode . (lambda () (local-set-key (kbd "C-r") #'consult-history))) :bind (:map eshell-mode-map ("C-r" . consult-history)) :config (with-eval-after-load 'evil (evil-define-key '(normal insert) eshell-mode-map (kbd "C-r") #'consult-history))) eshell 直接使用 term 命令上面我们提到，想要调用shell命令，可以通过编写lisp代码封装进行封装，能否直接使用shell命令呢？答案是可以的，eshell中有 eshell-visual-commands、eshell-visual-subcommands、eshell-visual-options 这么三个变量来共同决定哪些命令需要启用term 终端.eshell-visual-commands 接受一个字符串的列表，我们输入的命令在这个字符串列表中，那么就会启用term终端来执行命令eshell-visual-subcommands 来处理带子命令的复杂命令，在特定子命令下才需终端模拟eshell-visual-options 根据命令选项动态启用终端模拟。某些命令仅在特定选项（如 -i 交互式模式）出现时才需终端支持三者优先级：eshell-visual-commands > eshell-visual-subcommands > eshell-visual-options。Eshell 按此顺序检查，匹配任意条件即触发终端模拟。例如我们常见的 git、man、lazygit 等命令无法使用 eshell 中的 elisp 进行模拟，我们可以通过上述函数来封装这些命令。(use-package em-term :ensure nil :defer t :custom (eshell-visual-commands '("top" "htop" "less" "more" "lazygit")) (eshell-visual-subcommands '(("git" "help" "lg" "log" "diff" "show"))) (eshell-visual-options '(("git" "--help" "--paginate"))) (eshell-destroy-buffer-when-process-dies t))结合上述对这些函数的说明，来分析一下这段代码。首先是 eshell-visual-command 中定义的 top、htop、less、more 这些命令需要完成的term终端支持。当运行这些命令时，Eshell 自动切换到term终端，确保交互式界面正常显示（如分页、高亮等）eshell-visual-subcommands 为特定命令（如 git）的子命令启用图形终端，也就是说我们执行 git help 时会触发切换到term终端的操作，而执行 git status 时不会eshell-visual-options 当命令携带特定选项时会切换到term 终端。下面是在eshell 中执行 lazygit 的界面eshell 美化上面介绍了eshell的一些基本用法，现在来介绍如何将eshell进行简单的美化。首先介绍的是 eshell-git-prompt 。它提供了好多好看的主题在安装完插件后，可以在eshell中输入 use-theme 来列出它支持的所有主题，或者输入 use-theme name 来设定一个主题。想要持久化，可以通过函数 eshell-git-prompt-use-theme 来指定一个主题(use-package eshell-git-prompt :ensure t :after esh-mode :config (eshell-git-prompt-use-theme 'powerline))接着介绍 eshell-syntax-highlighting，它是eshell中语法高亮的插件，它是继承自 zsh 中的 zsh-syntax-highlighting。(use-package eshell-syntax-highlighting :after eshell-mode :ensure t ;; Install if not already installed. :config ;; Enable in all Eshell buffers. (eshell-syntax-highlighting-global-mode +1))接下来要介绍的插件是 capf-autosuggest 。它是一个自动补全的插件(use-package capf-autosuggest :ensure t :hook ((eshell-mode comint-mod) . capf-autosuggest-mode) )这样基本上就把我在终端上常用的一些插件和操作习惯给挪到eshell中了。当然我常用的还有一个名为 autojump 的插件。对应的 eshell 中也有一个名为 eshell-autojump 的插件。但是我已经通过别名简单的实现了一个eshell 上的 j 命令，这个插件的实现原理也是这样的，这里就不介绍这个插件了。
本节我们将介绍如何在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中有关窗口的配置和快捷键。