Make a Neovim Plugin – Syntax Highlighted Cursor

影片中看到安東尼哥使用的文字編輯器,游標背景色竟然會隨著 Syntax Highlight 變化!覺得超酷,Google 了一下貌似沒有類似的外掛,於是乎——效仿安東尼哥自己寫了一套文字編輯器 babi ——就自己寫了。

順便寫個簡單版 Neovim 的 Lua 語言外掛開發教學。

Lua 是啥?

Lua 可以想成是一種超輕量 Python,也是直譯式語言,用來寫一些簡單腳本。因為 Vimscript 語法不慎友善(絕對不是我懶得學),所以 Neovim 開始引入 Lua 支援。

來介紹一些重點 Lua 語法。

Table(陣列、字典)

Lua 只有 Table。陣列(Array)、字典都是用 Table 實現,差別在於 Key 是否存在。

字典

-- example of dictionary
local foo = {
    bar = 1,   -- usually without quotes, but you can if you want
    ["key with space"] = 2  -- if key contains space, use ["xxx"] syntax
}
print(foo.bar)  -- 1, you can also use foo['bar']
print(foo["key with space"])  -- 2, in this case you can only get value with [] 

留意若 Key 包含空白,需要額外的中括號跟引號。

陣列(Array)

-- example of list
local foo = {1, 2, "123"}
print(foo[1])  -- 1, yes! index starts at 1
print(foo[#foo])  -- "123", the way you get size of array is by prefixing #

你可以大雜燴,放入各種型別的內容。特別注意第一個 Index 從 1 開始。井字號返回陣列大小。

其他詳細 Syntax 可以參考 Lua 語法教學

開發流程

初步了解 Lua 之後就可以開始開發囉!

開發流程——我預設你有使用 Plugin 管理外掛——大致為:

  1. 在你的 Neovim 的 Plugin Directory 中新增資料夾,以收集所有程式碼。
    • 這個位置會依據 Plugin 管理外掛不同而改變。
    • 以我在 MacOS 上使用 Lazy.nvim 為例,這個位置在 ~/.local/share/nvim/lazy
    • 若你使用 Vim Plug 的預設位置,則會在 ~/.vim/plugged
  2. 放入空白 Lua 檔案。
    • 這個檔案我是會塞入一個 print("hi") 以確保外掛有掛上。
  3. 確認你的 Plugin 管理外掛設定有載入你的外掛。
  4. 啟動 Neovim,檢查功能。
    • 此時我就可以確認是否有印出 hi
  5. 修改新外掛原始碼,重複步驟 4。直到滿意。

空白 Lua 外掛

一個最簡單的 Lua 外掛資料夾結構如下:

./
└── lua/
    └── module-name/
        └── init.lua

特別留意 module-name 資料夾名稱,這決定了你要用什麼名稱來引入此 Lua 模組。

現在在 init.lua 中加入以下基本框架。

local function _setup(params)
    print(1)  -- test code to see if plugin is successfully loaded
end

return {
    setup = _setup,
}

Neovim 慣例上使用 setup 函式做外掛初始化。

與 Vim 互通有無

Neovim 提供 vim 模組,不用呼叫 require 內建就有引入,讓你可以使用 Lua 語法來操控 Neovim 功能。如果你想在 Neovim 視窗中嘗試,可以在 Normal Mode 下鍵入 :lua vim.cmd.echo(123)。你就可以用 Lua 呼叫 Vim 的 echo 指令來印出 123

回到我們的外掛。如果你想印出 Table 的內容,請善用 vim.inspect(foo)。關於 Vim 模組的用法,可以參考〈在 neovim 中使用 Lua〉這篇文章。

Syntax Highlighted Cursor

由於安東尼哥的編輯器是自己寫的,想怎麼寫都可以。但我不是 Neovim 核心開發者,所以最簡單又快速的方式就是外掛套件上去。實現步驟如下:

  1. 偵測目前游標所在位置 Highlight Group
  2. 取得該 Highlight Group 的顏色值
  3. 將游標背景顏色改為 Highlight Group 的前景色
  4. 游標移動時,重複步驟 1

大部分功能只要 Google 就可以查到方法,以下簡單介紹。

取得 Cursor 底下的 Highlight Group

local posInfo = vim.inspect_pos()  -- get position info at current cursor
local syntax_groups = posInfo.syntax
local treesitter_groups = posInfo.treesitter

Highlight Group 返回陣列,表示所有符合的樣式,若返回 nil 則表示沒有該規則下符合的 Highlight。而 Neovim 會使用最後一個來決定其顏色。

取得 Highlight Group 顏色

local cursorHi = vim.api.nvim_command_output("hi Cursor")

這步就是呼叫 Vim 原生語法,獲取 Cursor 這個 Group 的顏色設定,並將輸出字串存至變數 cursorHi

修改游標顏色

這裡使用 guicursor 設定。MacOS 內建的 Terminal 不知道為何改不了顏色。我另外安裝的 nvim-qt 就可以,所以才有本篇文章,不然可能就會永遠以為不能改顏色 XD

先設定 guicursor 使用 Cursor 這個 Highlight Group。(這個設定我是用 Vimscript 直接寫的。當然也可以呼叫 vim.cmd 寫在 Lua 那邊)

set guicursor=n-v-c:block-Cursor-blinkon0
set guicursor+=i:ver30-Cursor

再修改 Cursor 這個 Highlight Group 的顏色。這裡使用 Vim API,第一個參數 0 代表目前的 Buffer。

vim.api.nvim_set_hl(0, "Cursor", { fg=colors.guibg, bg=colors.guifg, })

游標移動時更新

這功能使用 Vim 的 Auto Command 來實現。我包了一個函式 augroup,以幫你產生 Auto Command Group。

function augroup (group_name, autocmds)
    -- autocmds = list of {
    --     events = {},
    --     opts = {
    --         pattern = {"*"},
    --         desc = "",
    --         command = "",
    --     },
    -- }
    local group = vim.api.nvim_create_augroup(group_name, {
        clear = true,
    })
    for _, autocmd in pairs(autocmds) do
        local opts = autocmd["opts"]
        opts["group"] = group
        vim.api.nvim_create_autocmd(autocmd["events"], opts)
    end
end

這樣就可以呼叫 augroup 來新增 Auto Command。每當游標移動後就執行 callback

augroup("SyntaxColorCursor", {
    {
        events = {"CursorMoved"},
        opts = {
            pattern = {"*"},
            desc = "SyntaxColorCursor",
            callback = function()
                if updapte_cursor_color() then
                    if not vim.o.modifiable or vim.o.readonly then
                        return
                    end

                    -- HACK: to update cursor color immediately
                    -- just go to insert mode than back to normal mode
                    vim.api.nvim_feedkeys(t'a', 'm', false)
                    vim.api.nvim_feedkeys(t'<esc>','m', false)
                end
            end,
        },
    },
})

最 Hack 的地方來了。在我呼叫 updapte_cursor_color 改完 Cursor 顏色之後,做了一件神秘的事。

因為我發現改完 Cursor 顏色之後不會馬上套用,GUI 應該是有快取,所以還是沿用原本顏色。但不能馬上更新的話體驗就很差。推測要想辦法觸發 GUI 重新繪製——至少游標那小塊——才可以。網路上查不太到,試過 redrawupdatetime 等等都無法解決此問題。直到我發現按下冒號進入 Command 模式、或切換 Insert Mode 之後再次回到 Normal Mode。賓果!顏色就更新了。

由於最終還是找不到單獨觸發重新繪製游標的 API,於是乎,就決定 Hack 了。一開始嘗試透過 API 模擬按下 : 進入命令列的方法,發現游標跳來跳去一度放棄了改用按下按鍵 a<Esc> 來瞬間進入 Insert Mode 一下再回到 Normal Mode。後來發現這種方式會讓游標很不穩定,和其他套件的相容性很差。最終還是改成進入命令列的方式,但加上了 silent 關鍵字,以避免游標跳動。搞定!但畢竟頻繁進出命令列還是怪怪的,也會影響其他外掛使用。所以額外加入許多條件排除觸發更新的時機。

雖然寫法多少有點 Hack,但成果出奇讚。歡迎大家安裝來玩看看。

ukyouz/syntax-highlighted-cursor.nvim

 

References

  1. Lua – Neovim docs
  2. Api – Neovim docs