原文 https://maskray.me/blog/2017-12-03-c++-language-server-cquery
C++代码索引工具现状
Tag system 流派
- Universal Ctags很精妙,用正则表达式跳转,因此文档编辑后仍能使用。https://github.com/universal-ctags/ctags/blob/master/parsers/c.c 但不带引用,Vim 会用二分查找,大约会有 log2(size/4096)次 seek。
- Cscope似乎已经荒废了。
- ID Utils
- GNU GLOBAL
libparser/C.c。用 Berkeley DB 存储 definition/reference/path name。带有插件系统可以使用 ctags idutils 的 parser。对于 Emacs/Vim 用户来说,可能是 tag 流派中最好用的工具了。辅以一些 heuristics 和 ripgrep 等,很多用户不觉得自己生活在水深火热中…… - Elixir Cross Referencer
- OpenGrok
clang 流派
- clang-tags荒废。
- YouCompleteMe不够好用,因为只处理单一 translation unit,无法查找引用。
- clangd最有前景,有大厂大项目愿意采用,Xcode 使用。但目前尚无存储系统,因此无法处理多 translation units。作为clang-tools-extra一部分,而 clang+llvm 构建 /贡献门槛高([https://reviews.llvm.org/])。对于这类工具类应用,贡献难易程度是个重要因素。目前有尝试引入存储模型(MarkZ3),但目前设计较为复杂,而实际上不带 garbage collection 的
std::vector(cquery 风格)足够应对大部分使用场景。很担心他们走上歧路。 - Google Kythe,(mostly) language-agnostic,概念复杂,配置困难。不重视 language server protocol,当前仅提供
ReferencesProvider,HoverProvider,DefinitionProvider,且交互使用可能有极大延迟。大多数人并不在意 C++ Haskell Python 代码间无缝跳转。 - rtags可以查找引用,但每个 translation unit 6 个文件
info,symbols,symnames,targets,tokens,usrs(过多),没有使用 in-memory 索引,查找引用请求会读项目所有 translation units 的文件。导致性能低下https://github.com/Andersbakken/rtags/issues/1007。 - cquery现阶段的妥协。主要数据结构为不带 garbage collection(变量 /函数 /类型等的 id 不会回收)的
std::vector(src/indexer.h)。有一些 Emacs 用户积极贡献code navigation 功能。
IDE(Any sufficiently complicated IDE contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of C++.)
cquery 安装、配置
https://github.com/jacobdufault/cquery。Arch Linux 可用aur/cquery-git。
- 构建 language server 可执行文件
- 编辑器安装 language client 插件
- 配置 language client,打开 C++文件时运行 language server 可执行文件,通过 stdio 用JSON-RPC 2.0通信
-
- 光标移动时向 language server 发送
textDocument/hover请求,language server 返回变量 /函数声明信息 - 查找定义发送
textDocument/definition请求 - 查找引用发送
textDocument/references请求 - 查找当前文档定义的符号(通常是顶层的 otline)发送
textDocument/documentSymbol请求 - 查找项目定义的符号(只查找 outline 的也很有用)发送
workspace/symbol请求 - 补全
textDocument/completion - 文档编辑操作发送
textDocument/didChange
- 光标移动时向 language server 发送
Emacs
安装lsp-mode,参照https://github.com/jacobdufault/cquery/wiki/Emacs配置。lsp-mode 会设置xref-backend-functions,把以下函数
xref-find-definitions(默认M-.),对应textDocument/definitionxref-find-references(默认M-?),对应textDocument/referencesxref-find-apropos(默认M-?),对应workspace/symbol- lsp-imenu 对应
textDocument/documentSymbol - LSP 生态系统解决的一大痛点是以前对于不同语言,要使用不同工具,设置不同快捷键。用了 language client 就可以统一了。
定向到 lsp-mode 中的处理函数,发送textDocument/definition等请求并渲染。
如果使用helm,可以考虑安装helm-xref,xref-show-xrefs-function会使用helm-xref-show-xrefs。但目前有两个 issues 影响了我的使用体验:
- https://github.com/brotzeit/helm-xref/6
- https://github.com/brotzeit/helm-xref/5 变通方案是把
xref-prompt-for-identifier添加到xref-prompt-for-identifier,https://github.com/emacs-lsp/lsp-mode/issues/194。
注意协议中定义返回结果为Location[] | null,只含位置信息,不包含代码行内容,如果要显示行内容。若文件在某个 buffer 内,则显示该 buffer 相应行;若未打开则需要打开该文件。
Emacs Lisp dynamic scoping 使得我们可以很容易复用已有代码。可以基于 evil-jumps 做一个用于 xref 的 jump list。我喜欢 xref jump list 和正常 jump list 分离。因为查找定义 /引用后会进行一些局部跳转,喜欢有快捷键回到定义 /引用跳转前的位置。并且需要能双向移动,不能只是 jump stack,xref.el用 ring buffer 实现的是 stack。
(defmacro my-xref//with-evil-jumps (&rest body) "Make `evil-jumps.el' commands work on `my-xref--jumps'." (declare (indent 1)) `(let ((evil--jumps-window-jumps ,my-xref--jumps)) ,@body)) (with-eval-after-load 'evil-jumps (evil-define-motion my-xref/evil-jump-backward (count) (my-xref//with-evil-jumps (evil--jump-backward count) (run-hooks 'xref-after-return-hook))) (evil-define-motion my-xref/evil-jump-forward (count) (my-xref//with-evil-jumps (evil--jump-forward count) (run-hooks 'xref-after-return-hook)))) 最近的新包https://github.com/emacs-lsp/lsp-ui包含 code lens、flycheck 等功能,以及一个基于quick-peek的find-{definitions,references,apropos}。
https://github.com/MaskRay/Config/blob/master/home/.emacs.d/layers/%2Bmy/my-code/funcs.el
- 希望spacemacs支持 LSP。
reference-handler(类似于跳转到定义的jump-handler)也很有用: https://github.com/syl20bnr/spacemacs/pull/9911 - lsp-mode 和ggtags都会
(setq-local eldoc-documentation-function ...),对于这类 minor-mode 冲突问题,如果能设置优先级就能优雅解决。
Neovim
参照https://github.com/autozimu/LanguageClient-neovim/wiki/cquery
nn <leader>ji :Denite documentSymbol<cr> nn <leader>jI :Denite workspaceSymbol<cr> " 终端限制,<C-,>不可用。ord(`,`) & 64 为 0 无法表示 nn <M-,> :Denite references<cr> nn <silent> <C-j> :MarkPush<cr>:call LanguageClient_textDocument_definition()<cr> 不清楚怎么把定义 /引用改造成使用可双向移动的 jump list。
生成compile_commands.json
CMake
% mkdir build % (cd build; cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=YES ..) % ln -s build/compile_commands.json Build EAR
Bear is a tool that generates a compilation database for clang tooling. It can be used for any project based on Makefile.
bear make # generates compile_commands.json Ninja
ninja -t compdb rule_names... > compile_commands.json 问题
- Task lists https://github.com/jacobdufault/cquery/issues/30 Polish before publishing (to GitHub Marketplace)
- 需要一个妥善的 on-disk storage。很多轻量级数据库不支持或有较难处理的问题(如果有需求把现在 in-memory+JSON 改成有其他存储模型)。注记,
SQLITE_ENABLE_LOCKING_STYLE、flock 很难。
其他
索引 Linux kernel
wget 'https://git.archlinux.org/svntogit/packages.git/plain/trunk/config?h=packages/linux' -O .config yes '' | make config bear make -j bzImage modules 生成 3GiB 文件。
索引 llvm,du -sh => 1.1GB,索引完内存占用 2G。
查看 LSP requests/responses
sudo sysdig -As999 --unbuffered -p '%evt.type %evt.buffer' "proc.pid=$(pgrep -fn build/app) and fd.type=pipe" | egrep -v '^Content|^$' 生成compile_commands.json,参考https://github.com/jacobdufault/cquery/wiki。
希望有朝一日 Debug Protocol 也能获得重视,https://github.com/Microsoft/vscode-debugadapter-node/blob/master/protocol/src/debugProtocol.ts,让realgud轻松一点。
和 YouCompleteMe 等项目一样,cquery 默认下载prebuilt clang+llvm,即.h .so .a。用户不需要编译完整的 llvm,开发门槛比 clangd 低。
感谢 ngkaho1234。
