現代的專案開發,很少在從無到有打造,大部分都是基於現有的程式之上繼續開發或維護,進入專案之後,通常第一件事就是大量閱讀程式碼理清專案的整個脈絡,才開始著手寫 code,所以好的程式碼導覽技巧將會帶你上天堂。

我將大部分的程式碼導覽情境大致拆成了三種情況:

  • 基本的跳轉:
    • #if #endif 中間的程式碼太長,想要跳轉到成對的 #if #endif
    • cursor 在 {…} block 裡, 這個 block 又臭又長,想快速跳轉 block 的開頭和結尾
    • 註解長篇大論,想快速跳到註解的開頭或結尾
  • 尋找專案下檔案 (可透過設定 path option 解決)
    • 跳轉到標頭檔
    • 跳轉到指定檔案
  • 使用 source code tagging system 快速挑轉到 function 定義及特定 symbol 等等: 專案相當大的時候,source code tagging system 相當好用,可以快速找到定位,也不會花掉太多的資源,比起 LSP 的跳轉定義,是較輕量的選擇。

本篇依據上述三種情境,一一講述。

常用跳轉操作

  • %

    • 跳到成對的括號
    • 如果註解是 /*...*/,跳到成對的註解符號
    • 在成對的 #if, #ifdef, #else, #elif, #endif 之中跳轉,這相當重要,如果 code 很長, if else macro 又是巢狀的,這招可以讓你少迷路好幾次
  • [{ ]}

    • [{:若 cursor 在 {…} 區塊中,跳到此區塊的 {
    • ]}:若 cursor 在 {…} 區塊中,跳到此區塊的 }
  • [* ]*

    • [*:找到上一個 /*
    • ]*:找到下一個 */
  • [# ]# 用法和 [{ ]} 類似,但是針對的是 # 在 line2 區塊 [{ 跳到 line1, ]} 跳到 line3 在 line4 區塊 [{ 跳到 line3, ]} 跳到 line5 在 line6 區塊 [{ 跳到 line5, ]} 跳到 line7

    #ifdef TEST
    // line2
    #elif TEST2
    // line4
    #else
    // line6
    #endif
    

    若 cursor 在 #else…#endif 區塊中,跳到 #else

透過 path 設定,快速查找相關檔案

看看以下圖片,將 cursor 移動到程式碼標頭檔的位置 (stdio.h),按下 gf,bang! 你跳到 stdio.h 標頭檔了

再來看看 :find 指令,我想找出 stdlib.h 標頭檔,使用 :find stdlib.h, 就跳到 stdlib.h 啦

我們來看看 vim 到底怎麼實現這個魔法的: 其實 vim 是藉由 path 這個 option 去搜尋的 (詳見 :help 'path'),path 這個值由許多路徑組成並由逗號隔開,在 nix 系統裡 vim 對 path 預設的值是

.,/usr/include,,

可以看到三個值:

  • .: 當前檔案所在的目錄
  • /usr/include: 很顯然找到 gf 找到的 stdio.h 和 :find 找到的 stdlib.h 是由這個目錄找出來的
  • empty string:工作目錄 (current work directory, 通常是 vim 開啟的位置,如果你沒用 :cd 的話)

所以可以把自己常用的標頭檔所在目錄加入 path ! 舉個例子 /usr/local/include 也是很常放標頭檔的路徑,所以我把它加入到 path

:set path+=/usr/local/include

註: . 和 empty string 的差別請看:is-vims-default-path-option-redundant

** 算是一個蠻好用的值,它會對路徑做 recursive 的解析,你可能會使用到它,詳細的規則可以看 :help starstar

Source code tagging system 和 vim

2020 年 Linux kernel 原始碼規模已經達到 2780 萬行,規模如此之龐大,Linux 的程式開發人員到底是怎麼開發的,隨便想找一個 function 定義都相當吃力,總不能用 grep 慢慢去找吧!這時候 source code tagging system 就派上用場了,它可以對專案所有的 symbol 產生索引檔,需要查找目標時就藉由這些索引檔快速查找位置。概念其實就是字典的索引,字典這麼大一本不會有人從頭掃到尾,都是直接翻到字典最後面的索引快速找到目標。

這些 source code tagging system,通常會產生索引檔並且提供一個介面,讓編輯器使用它們快速在程式碼內定位。

接下來將逐一介紹 cscope, gtags, ctags 等等三個 source code tagging system 在 vim 裡的基本使用方式。

Cscope

Cscope 簡介

cscope 官網聲稱可以做到以下事情:

  • 查詢所有使用到這個 symbol 的地方
  • 查詢全域變數
  • 查詢 function 在哪被呼叫
  • 查詢這個 function 呼叫了誰
  • 使用字串查詢找到目標
  • 使用 egrep pattern 尋找目標
  • 快速尋找專案下檔案
  • 查看哪個檔案 include 當前檔案

cscope 指令常用選項

  • b:只產生索引檔案,不進入互動查詢介面
  • q:產生 cscope.in.out 和 cscope.po.out文件,加快 cscope 的索引速度
  • k:產生索引檔案时,不搜尋 /usr/include
  • R:遞迴搜尋子目錄

Cscope 和 vim

在專案目錄產生索引檔案:

$ cscope -bkqR

會產生三個檔案 cscope.in.out cscope.out cscope.po.out

進入 vim 之後可以透過 cs 查看 cscope 在 vim 裡的使用方式

讓 vim 知道 cscope 的路徑

:set cscopeprg=/path/to/cscope

指定 cscope 索引檔案,讓 vim 知道要用哪個索引檔案查詢:

:cs add cscope.out

接下來就可以使用以下方式查詢 (xxx 為想搜尋的目標):

  • :cs find a xxx 查尋 xxx 被賦值的地方
  • :cs find c xxx 查尋所有呼叫 xxx function 的地方
  • :cs find d xxx 查尋 xxx function 呼叫的所有函數
  • :cs find e xxx 使用 egrep pattern 查詢
  • :cs find f xxx 找出 xxx 檔案在哪裡
  • :cs find g xxx 查尋 xxx 所定義的地方 (通常最常用的是這個)
  • :cs find i xxx 查尋所有 inlcude xxx 檔案的地方
  • :cs find s xxx 查尋所有 xxx symbol 出現的地方
  • :cs find t xxx 查詢 xxx 字串出現的地方,其實搜尋效果就類似 grep

記得查詢完的結果可以用 :cw, :copen 在 quickfix window 列出:

GNU GLOBAL (gtags)

由 GNU GLOBAL 由 GNU 所開發的 soure code tagging system,本身支持四種語言,透過 Pygments 和 Universal Ctags 的 parser 可以支持其他 25 種語言,支援 incremental update,原始碼更動後,只會對改變的地方索引,不會重新索引,這對龐大的專案有很大的幫助。另外 gtags 提供了 cscope 相容的互動介面,叫做 gtags-cscope,讓你用起來像 cscope,但背後其實是 gtags 的索引檔案。

在專案目錄產生索引檔案:

$ gtags

讓 vim 知道 gtags-cscope 路徑:

:set cscopeprg=/path/to/gtsgs-cscope

指定 gtags 索引檔案,讓 vim 知道要用哪個索引檔案查詢:

:cs add GTAGS

借下來就可以用和 cscope 一樣的方式查詢,除了 :cs find d,其他皆支援

Ctags

Ctags 簡介

最早的 Ctag 在 BSD 上實現,版本變革的歷史脈絡我不是相當清楚,查不太到資料。 現在普及的版本是 Exuberant Ctags,一開始和 vim 是一包軟體,一起發布,但在 vim6 之後獨立成兩個專案,從這裏可看出它一定和 vim 兼容良好,但此版本在 2009 年之後已不再維護。建議使用Exuberant Ctags 所 fork 的版本 Universal Ctags,目前還在維護和開發當中。

Ctags 和 vim

在專案下生成索引檔案,可以看到目錄裡有 tags 檔案

$ ctags -R *

指定 tags 索引檔案,讓 vim 知道要用哪個索引檔案查詢:

:set tags=tags

再來就可以使用:

  • Ctrl + ]: 跳到定義,如果有多個匹配則會跳到第一個
  • Ctrl + T: 跳回原本的地方

每次用 ctrl + ] 跳轉到定義,vim 就會把這位置推到 tag stack 裡,每次 ctrl + T 跳回原來的地方,vim 就會從 tag stack 裡 pop 位置出來,我們可以使用 :tags 看到 tag stack 裡的內容。

Reference