Emacs 折腾日记(二十九)—— 打造C++ IDE

在介绍vim配置的时候介绍过lsp的相关基础知识。简单来说lsp是一个协议,它以C/S架构的形式进行组织,lsp负责分析语法,给出具体的语法单元,完成跳转等功能的核心实现。而客户端则负责接收用户的操作请求并呈现具体结果。这样做的好处是将核心服务和客户端显示分离出来,核心部分重用,客户端则可以由各个编辑器自己实现,使各种编辑器都有相同的核心功能体验。

对于Emacs来说,由lsp-mode 提供核心客户端库,管理服务器生命周期、消息路由及基础功能。另外也有 lsp-ui 这种增强UI模块,提供实时信息侧边栏(lsp-ui-sideline)、代码透镜(Code Lens)、悬浮文档等

下面来介绍如何使用它们配置一个基础的lsp功能

lsp-mode

根据官方给出的配置,我们可以组一个基础的配置

emacs-lisp 复制代码
(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 这个插件

emacs-lisp 复制代码
(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 的插件来进行项目管理

emacs-lisp 复制代码
(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 结合进行搜索

语法检查

语法检查方面,目前主流的插件是 flycheckflymake,但是好像 flycheck 使用的人多一些,所以这里我也采用 flycheck

emacs-lisp 复制代码
(use-package flycheck
 :ensure t
 :config
 (setq truncate-lines nil) ; 如果单行信息很长会自动换行
 :hook
 (prog-mode . flycheck-mode))

这里我们仅在编程的时候开启语法检查。

同样的, flycheck 是一个前端,用来显示结果的,具体检查的核心功能是通过后端来实现的。后端的程序可以在官方网站上找到。

状态栏显示相关的lsp信息

现在是时候来修改以下状态栏的显示了,根据配置vim的经验,我还是希望主要显示当前编辑的模式、文件名、文件编码、lsp服务器、语法错误信息等。这里我使用 doom-modeline 来完成这个功能

emacs-lisp 复制代码
(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构建的项目

shell 复制代码
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1

另外我希望每种语言对应的lsp配置放入到不同的配置文件中,做到模块化处理,因此我在配置中新建了一个lsp目录,并且按编程语言每种语言一个配置文件。目前c++语言的配置如下:

emacs-lisp 复制代码
(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 的值。

txt 复制代码
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 位置

emacs-lisp 复制代码
(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

emacs-lisp 复制代码
(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,所以需要手动下载然后加载

emacs-lisp 复制代码
(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

emacs-lisp 复制代码
(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的功能并没有配置完,后面我计划加上自动编译运行、调试等功能,这些就在后面的文章中给出

相关推荐
2301_7850381822 分钟前
c++初学day1(类比C语言进行举例,具体原理等到学到更深层的东西再进行解析)
c语言·c++·算法
Dream it possible!1 小时前
LeetCode 面试经典 150_数组/字符串_加油站(14_134_C++_中等)(贪心算法)
c++·leetcode·面试
EnzoRay2 小时前
C++(一)
c++
啊阿狸不会拉杆3 小时前
《算法导论》第 12 章 - 二叉搜索树
数据结构·c++·算法·排序算法
zoujiahui_201810 小时前
vscode中创建python虚拟环境的方法
ide·vscode·python
岁忧10 小时前
(nice!!!)(LeetCode 每日一题) 3363. 最多可收集的水果数目 (深度优先搜索dfs)
java·c++·算法·leetcode·go·深度优先
略无慕艳意12 小时前
Notes of Effective CMake
c++·c·cmake
ze言12 小时前
为什么现代 C++ (C++11 及以后) 推荐使用 constexpr和模板 (Templates) 作为宏 (#define) 的替代品?
开发语言·c++
岁忧17 小时前
(LeetCode 面试经典 150 题) 82. 删除排序链表中的重复元素 II (链表)
java·c++·leetcode·链表·面试·go