Vim 8 中 C/C++ 符號索引:從 GTags 到 LanguageServer

符號索引是個重要功能,不論閱讀新項目,還是開發複雜點的大項目,符號索引都能幫你迅速掌握項目脈絡,加快開發進度。傳統 ctags 系統雖和 vim 結合緊密,但只能查定義無法查引用,cscope 能查引用,但只支持 C 語言,C++都不支持,況且常年不更新。ctags 由於使用文本格式存儲數據,雖用了二分查找,但打開 Linux Kernel 這樣的大項目時,查詢會有卡頓的感覺。

本文將從討論 gtags 和 LanguageServer 開始,一步步教你 DIY 一套超越市面上任何編輯器(vscode,emacs,vscode)體驗的最強符號索引系統。請注意二者是正交關係,不是說介紹一大堆gtags,然後後面用 Language Server 代替之,所以我們先從 gtags 開始。

GTags (或者叫做 GNU GLOBAL)比起 ctags 來說,有幾個主要的優點:

  1. 不但能查定義,還能查引用
  2. 原生支持 6 種語言(C,C++,Java,PHP4,Yacc,彙編)
  3. 擴展支持 50+ 種語言(包括 go/rust/scala 等,基本覆蓋所有主流語言)
  4. 使用性能更好的本地資料庫存儲符號,而不是 ctags 那種普通文本文件
  5. 支持增量更新,每次只索引改變過的文件
  6. 多種輸出格式,能更好的同編輯器相集成

曾經用過 gtags 的人或許會問,gtags 我都用過好幾年了,也沒見用出朵花來啊? 現在不管是 vscode 還是 sublime text 或者 emacs ,不都有 gtags 插件了么,要用簡單得很,還有比較好的用戶體驗,你在 vim 下配置半天圖啥呢?

答案是,如果還停留在這些傳統體驗上,那我也沒必要寫這篇文章了。現實中,大部分人都沒有用對 gtags,如果你能夠在 Vim 下正確使用 gtags,不但能極大的方便你開發複雜項目或者閱讀新項目代碼,還能獲得比上面所有編輯器更好的體驗。

Vim中的符號索引,他真能玩出花來,接下來我們就 DIY 這樣一個體驗超過任何其他編輯器的符號索引系統。

正確安裝 GTags

請首先安裝最新版本 gtags,目前版本是 6.6.2,Windows 下可到 [這裡] 下載可執行,Linux 下請自行編譯最新版(Debian / Ubuntu 自帶的都太老了),Mac 下檢查下 brew 安裝的版本至少不要低於 6.6.0 ,否則請自己編譯。

只寫 C/C++/Java 的話,那麼到這裡就夠了,gtags 原生支持。如想要更多語言,那麼 gtags 是支持使用 ctags/universal-ctags 或者 pygments 來作為分析前端支持 50+ 種語言。使用 ctags/universal-ctags 作為前端只能生成定義索引不能生成引用索引,因此我們要安裝 pygments ,保證你的 $PATH 裡面有 python,接著:

pip install pygments

保證 Vim 里要設置過兩個環境變數才能正常工作:

let $GTAGSLABEL = native-pygmentslet $GTAGSCONF = /path/to/share/gtags/gtags.conf

第一個 GTAGSLABEL 告訴 gtags 默認 C/C++/Java 等六種原生支持的代碼直接使用 gtags 本地分析器,而其他語言使用 pygments 模塊。

第二個環境變數必須設置,否則會找不到 native-pygments 和 language map 的定義, Windows 下面在 gtags/share/gtags/gtags.conf,Linux 下要到 /usr/local/share/gtags 里找,也可以把它拷貝成 ~/.globalrc ,Vim 配置的時候方便點。

實際使用 pygments 時,gtags 會啟動 python 運行名為 pygments_parser.py 的腳本,通過管道和它通信,完成源代碼分析,故需保證 gtags 能在 $PATH 里調用 python,且這個 python 安裝了 pygments 模塊。

自動生成 Gtags

VSCode 中的 C++ Intellisense 插件就是使用 Gtags 來提供 intellisense 的,但是它有兩個非常不好用的地方:

  • 代碼修改了需要自己手動去運行 gtags ,更新符號索引
  • 會在代碼目錄下生成:GTAGS,GRTAGS,GPATH 三個文件,污染我的項目目錄

第一條是我過去幾次使用 gtags 最頭疼的一個問題;第二條也蛋疼,礙眼不說,有時不小心就把三個文件提交到代碼倉庫里了,極端討厭。

那麼 Vim 8 下有無更優雅的方式,自動打點好 gtags 三個文件,放到一個統一的地方,並且文件更新了自動幫我更新數據,讓我根本體驗不倒 gtags 的這些負擔,完全流暢的使用 gtags 的各種功能呢?

當然有,使用我在《韋易笑:如何在 Linux 下利用 Vim 搭建 C/C++ 開發環境?》中介紹過的 gutentags 插件來打理,它不但能根據文件改動自動生成 ctags 數據,還能幫我們自動更新 gtags 數據,稍微擴充一下上文的配置,讓 gutentags 同時支持 ctags/gtags:

" gutentags 搜索工程目錄的標誌,當前文件路徑向上遞歸直到碰到這些文件/目錄名let g:gutentags_project_root = [.root, .svn, .git, .hg, .project]" 所生成的數據文件的名稱let g:gutentags_ctags_tagfile = .tags" 同時開啟 ctags 和 gtags 支持:let g:gutentags_modules = []if executable(ctags) let g:gutentags_modules += [ctags]endifif executable(gtags-cscope) && executable(gtags) let g:gutentags_modules += [gtags_cscope]endif" 將自動生成的 ctags/gtags 文件全部放入 ~/.cache/tags 目錄中,避免污染工程目錄let g:gutentags_cache_dir = expand(~/.cache/tags)" 配置 ctags 的參數let g:gutentags_ctags_extra_args = [--fields=+niazS, --extra=+q]let g:gutentags_ctags_extra_args += [--c++-kinds=+px]let g:gutentags_ctags_extra_args += [--c-kinds=+px]" 如果使用 universal ctags 需要增加下面一行let g:gutentags_ctags_extra_args += [--output-format=e-ctags]" 禁用 gutentags 自動載入 gtags 資料庫的行為let g:gutentags_auto_add_gtags_cscope = 0

通過上面的配置,可以在後台自動打理 ctags 和 gtags 資料庫,檢測文件改動,並更新到 ~/.cache/tags 目錄中,避免污染你的項目目錄。

上面定義了項目標誌文件(.git, .svn, .root, .project, .hg),gutentags 需要確定當前文件所屬的項目目錄,會從當前文件所在目錄開始向父目錄遞歸,直到找到這些標誌文件。如果沒有,則 gutentags 認為該文件是個野文件,不會幫它生成 ctags/gtags 數據,這也很合理,所以如果你的項目不在 svn/git/hg 倉庫中的話,可以在項目根目錄 touch 一個空的名為 .root 的文件即可。

現在我們在 Vim 中隨便編輯文件,gtags 資料庫就會默默的幫我們生成好了,如果你使用 airline ,還能再 airline 上看到生成索引的狀態。gtags 程序包里有個 gtags-cscope 的程序,可用 cscope 的介面來為 Vim 提供 cscope 的所有操作,只需要再 vim 中修改一下 cscopeprg 指向這個 gtags-cscope 程序,就可以 cs add 添加 gtags 資料庫,然後像 cscope一樣的使用 gtags 了。

Vim 里原有的 cscope 機制可以設定好數據文件後,啟動一個 cscope 進程並用管道和其鏈接,通過管道命令實現定義和引用的查詢,你修改了 cscopeprg 指向 gtags-cscope 後,就可以在 Vim 中用 :cs add path 命令啟動 gtags-cscope 這個子進程,鏈接 gtags 的資料庫,然後提供全套 cscope 類似的操作。

gtags-cscope 還有一個優點就是我後台更新了 gtags 資料庫,不需要像 cscope 一樣調用 cs reset 重啟 cscope 子進程,gtags-cscope 一旦連上永遠不用重啟,不管你啥時候更新資料庫,gtags-cscope 進程都能隨時查找最新的符號。

那麼最後臨門一腳,我們將要想辦法避免這個手工 cs add 的過程。

自動載入 gtags

gutentags 可以為我們自動 cs add 命令添加當前更新好的 gtags 資料庫到 vim ,但是你編輯一個項目還好,如果你同時編輯兩個以上的項目,gutentags 會把兩個資料庫都連接到 vim 里,於是你搜索一個符號,兩個項目的結果都會同時出現,基本沒法用了。

所以上面的配置中禁用了 gutentags 自動載入,我們可以每次查詢單獨執行一遍外部的 gtags-cscope 工具,將結果放到 quickfix。這樣做可以避免項目之間結果混在一起,啟動前配好項目目錄和資料庫目錄,查詢完就退出,稍微封裝下即可,唯一問題就是用起來有點慢。

更好的方法是繼續使用 vim 自帶 cscope 系統,並解決好資料庫鏈接斷開問題:首先要能找到當前文件所屬項目的 gtags 資料庫被 gutentags 放到哪裡了,其次一開始用不著 cs add 載入任何 gtags 資料庫,只有在真正查詢前增加個檢測:

  1. 如果當前項目資料庫已經添加過,就繼續開始查詢工作。
  2. 沒有添加的話就斷開其他所有項目的 gtags 資料庫,再添加本項目資料庫。

過程說起來很複雜,用起來卻很簡單,我寫了個 gutentags_plus.vim 的小腳本做這個事,下載該文件並放入你的 ~/.vim/plugin 目錄,或者你自己 dotfiles 的 plugin 目錄(記得 dotfiles 項目要添加到 runtimepath中):

直接用裡面的 GscopeFind 命令,像 cs find 一樣用就行了,搭配 gutentags,這個腳本在你每次 GscopeFind 前幫你處理資料庫載入問題,已經載入過的資料庫不會重複載入,非本項目的資料庫會得到即時清理,所以你根本感覺不到 gtags 的存在,只管始用 GscopeFind g 命令查找定義,GscopeFind s 命令查找引用,既不用 care gtags 資料庫載入問題更不用關心何時更新,你只管寫你的代碼,打開你要閱讀的項目,隨時都能通過 GscopeFind 查詢最新結果,並放入 quickfix 窗口中:

這個小腳本末尾還還定義了一系列快捷鍵:

  • <leader>cg - 查看游標下符號的定義
  • <leader>cs - 查看游標下符號的引用
  • <leader>cc - 查看有哪些函數調用了該函數
  • <leader>cf - 查找游標下的文件
  • <leader>ci - 查找哪些文件 include 了本文件

比如打開 Linux 代碼樹,memory.c 游標停留在 core_initcall 函數名上面,然後 <leader>cc,下面 quickfix 窗口立馬就列出了調用過該函數的位置。

得益於 gtags 的數據存儲格式,再大的項目,也能給你瞬間完成查詢,得益於 gtags-cscope 的介面,vim中可以對同一個項目持續服用相同的 gtags-cscope 子進程,採用管道通信,避免同項目多次查詢不斷的啟動新進程,查詢毫無卡頓。

到此為止,我們在 vim 中 DIY 了一個比 vscode 流暢得多的符號索引體驗,無縫結合 gtags 的程度超過以往任何編輯器,讓你象在 IDE 里一樣毫無負擔的查找定義和引用,而IDE 只支持一兩種語言,咱們起步就覆蓋 50+ 種語言。

快速預覽

我們從新項目倉庫里查詢了一個符號的引用,gtags噼里啪啦的給了你二十多個結果,那麼多結果順著一個個打開,查看,關閉,再打開很蛋疼,可使用 vim-preview 插件高效的在 quickfix 中先快速預覽所有結果,再有針對性的打開必要文件:

按照插件文檔配置 keymap,就可以在quickfix中對應結果那一行,按 p鍵在右邊打開預覽窗口查看文件,多次按 p預覽多個文件都會固定在右側的預覽窗口顯示,不會打開新窗口或tab,更不會切走當前文件,也不用你因為預覽新結果而要在文件窗口和 quickfix 窗口中來回切換,即便你想上下滾動預覽窗口裡的代碼,也可以不用離開quickfix窗口,直接 alt+U/D 就可以在 quickfix 中遙控 preview 窗口上下滾屏了。

當你閱讀完預覽內容可以用大寫 P 關閉預覽窗口,然後正常用回車在新窗口或者tab中打開想要具體操作的文件了,這依賴 switchbuf 設置可以看vim幫助文檔,不想看了 F10 關閉 quickfix 窗口就是。

搭配前文介紹過的 vim-unimpaired 插件,你還可以在不操作 quickfix窗口的情況下,使用快捷鍵進行上下結果跳轉,Vim的好處在於有比較多的標準基礎組件,比如 quickfix,emacs 就沒有這樣的基礎設施,雖然 elisp 都可以實現,每個插件各自實現了一個差不多的 quickfix 窗口,碎片化嚴重,無法像vim那樣一些插件往 quickfix里填充數據,一些插件提供 quickfix 方便的預覽和跳轉,還有一些插件可以根據quickfix里的結果內容做進一步的處理和響應,他們搭配在一起能夠形成合力,這在碎片化嚴重的 emacs 里是看不到的。

通過上面的一系列 DIY,我們陸續解決了:按需自動索引,資料庫自動連接以及結果快速預覽等以往使用 gtags 的痛點問題,反觀其他編輯器,符號索引功能或多或少都有這樣那樣不如意的地方。

所以如果你想得到這樣一個其他編輯器從沒達到過的IDE級別的符號索引系統,又能支持比IDE更多語言,那麼花點時間DIY 一下也是值得的。

接下來我們談 Language Server

(待續)

推薦閱讀:

移動端內容編輯器(鍵盤)的設計參考
如何從頭打造一個Markdown編輯器(序章)
收集漂亮的 Vim 主題
Confluence 6 在編輯器中控制參數的顯示

TAG:Vim | 文本編輯器 | Emacs |