Make a Neovim Plugin (again) – Telescope GTAGS

GNU Global 是一套標籤程式,幫你記錄程式碼中函式等定義的座標——檔名、行號、引用位置。透過其 gtags 指令可以建立索引,然後透過其 global 指令查詢。

原本愛用的 Ctags 索引功能較陽春,雖支援較多程式語言,還完美配合 vim 內建快捷鍵,如: <ctrl-]>)等,但缺少搜尋 Reference 功能,所以就改用 Gtags。

若在 VIM 中想要索引 Gtags 就必須額外安裝套件。例如 LeaderF 就是一套出色的套件,其模糊搜尋速度飛快,原本也用得好好的。但人生就是這個但是。換成 Shift-JIS 編碼桌面系統之後,預覽全部給我顯示問號!氣死。另一個類似的模糊查找套件 Telescope 就可以指定編碼,能正常顯示預覽。但 Telescope 內建不支援 Gtags 啊啊啊啊~~~(Orz

只好自搞。以下正文開始。

Telescope Plugin Development

好在 Telescope 強大且完善的外掛生態,讓我們不必重新製造模糊搜介面的輪子。只需專注於資料搜集,餵給 Telescope 的 Picker 即可。

基本資料夾結構

我們的目的是新增 Gtags 指令。也就是在 vim 中輸入:

:Telescope gtags<CR>

為此,須要的檔案配置如下:

./
└── lua/
    ├── telescope/
    │   └── _extensions/
    │       ├── gtags.lua   (register a telescope extension)
    │       └── ...
    ├── telescope-gtags/  (register a lua module)
    │   └── init.lua     (register lua setup function)
    └── ...

其中:

  • lua/telescope-gtags/init.lua 讓我們能夠在 Lua script 中使用 require("telescope-gtags")
  • lua/telescope/_extensions/gtags.lua 則是用來註冊 Telescope 外掛

寫一些測試內容,以便確認檔案配置 OK。

return {
    setup = function(opts)
        print("Lua OK!")
    end,
}
local test = function(opts)
    print("'Telescope gtags' is run!")
end

return require("telescope").register_extension {
    setup = function(ext_config, cfg)
        -- access extension config and user config
        print("Telescope Ext Setup!")
    end,
    exports = {
        gtags = module.run_symbols_picker,
    },
}

加入 VIM 配置

假如使用 Lazy.vim:

{
    "ukyouz/telescope-gtags",
    dependencies = {
        "nvim-telescope/telescope.nvim",
    },
}

進入 VIM,在 Command mode 中輸入以下指令,確認 Telescope 有認到 Gtags 指令。

:Telescope gtags<CR>

這時候應該可以透過 :messages 依序看到以下輸出:

Lua OK!
Telescope Ext Setup!
'Telescope gtags' is run!

殼有了,接著把功能補齊即可!以下紀錄一些實作上遇到的問題及解法。

加入 Gtags 查找功能

關於 Telescope 的 picker 使用方式,本文會使用到以下兩種:

  • 執行 shell script 一次取得所有候選資料,餵給 picker。
  • 隨使用者每次輸入,重複執行 shell script 取得候選資料。

第一種方法呼叫 finders.new_oneshot_job,寫起來是這樣:

local pickers = require "telescope.pickers"
local finders = require "telescope.finders"

local args = {
    "<your command>",
    ...  -- arguments
}
pickers.new(opts, {
    prompt_title = "xxx",
    finder = finders.new_oneshot_job(args, opts),
}):find()

第二種方法使用 finders.new_job, 寫起來是底下這樣。new_job 的第一個參數,會用來產出每次想要執行的 Shell Script 指令。

local pickers = require "telescope.pickers"
local finders = require "telescope.finders"

pickers.new(opts, {
    prompt_title = "xxx",
    finder = finders.new_job(function(prompt)
        -- Get user input or command argument 'query'

        -- Execute 'global <prompt> --result=ctags' command
        return {"global", prompt}
    end, opts.entry_maker, opts.max_results, opts.cwd),
}):find()

查找 Symbol

串接以下 Shell Script 指令。

$ global -i <query> --result=ctags

其中 -i 代表忽視大小寫,--result=ctags 代表以 Ctags 相容的格式輸出,這樣就可以串接 Telescope 內建的 Ctags Previewer。

不過,若直接拿使用者輸入作為 query 字串去查,會很難用。因為只有當輸入完全相符時,global 指令才找得到該 Tag。因此實作上有動一些手腳,將使用者輸入轉成正規表達式,以允許模糊查找。例如輸入 gud 就會變成 .*g.*u.*d.*,這樣假如有個 function 名稱是 GetUserData 就找得到。轉換正規表達式的 Lua 程式如下:

-- convert prompt string 'gud' to chars ['g','u','d']
local chars = {}
prompt:gsub(".", function(c) table.insert(chars, c) end)

-- join chars with '.*' and add a trailing ".*"
--   so query become 'g.*u.*d.*'
local query = table.concat(chars, ".*") .. ".*"

跳轉References

串接以下 Shell Script 指令。

$ global -r <query> --result=grep

這邊改用 grep 格式輸出,是因為這樣可以預覽結果所在行。由於需要查找 References 的時候,通常 Tag 名稱已經確定,可以直接使用 VIM 內建的 <cword>

word = vim.fn.expand("<cword>")

接著只需要使用 new_oneshot_job 即可搞定。

跳轉 Definition

這裡串接的 global 和查找 Symbol 相同。只是由於這時已經確定 Tag 名稱,所以可以直接使用 VIM 內建的 <cword>。

這項功能困難的地方在於,通常 Definition 只有一個。此時就應該直接跳轉過去,而不是打開 picker 還要使用者再按一次 Enter。但假若真的遇到多重定義,還是想要保留 picker 功能。所以實作上就複雜了一點。

首先自己敲 globa 指令拿取 Definitions 列表。

-- get the word under cursor
local query = vim.fn.expand("<cword>")

-- run 'global <cword> --result=ctags' command and get the stdout as result
local handle = io.popen("global " .. query .. " --result=ctags")
local result = handle:read("*a")
handle:close()

-- loop line by line and insert each line to 'items'
local items = {}
for s in result:gmatch("[^\r\n]+") do
    table.insert(items, s)
end

這樣就可以藉由判斷 items 數量,決定要直接跳轉還是開啟 picker 讓使用者選擇。

if #items == 1 and opts.jump_type ~= "never" then
  -- directly jump the cursor to definition
  ...
else
  -- open telescope picker for user to choose one
  pickers.new(opts, {
      prompt_title = "GTAGS Definitions - " .. query,
      previewer = previewers.ctags.new(opts),
      finder = finders.new_table {
          results = items,
          entry_maker = entry_maker,
      },
  }):find()
end

詳細實作建議直接看 Github 上 Repo,這裡只稍微解說概念。

外掛成果

很多實作其實也都是直接看 Telesope 原始碼照抄。總之終於搞定。歡迎大家安裝來玩看看。

ukyouz/syntax-highlighted-cursor.nvim

 

References