Lua 在 Neovim 中已经被当作了一等公民,目前火热的 Neovim 插件、配置基本上都是使用 Lua 来开发的。而我也是从 2016 年就开始使用 Neovim 了。 这篇文章整理了一些从 VimScript 切换到 Lua 配置一些 Tips。
本文写作环境:
- OS:Windows 11
- Neovim:v0.10.0
- Terminal: Windows Terminal
为什么选择 Lua
在前面我写过两篇文字,主要是来比较 Lua(luajit)与 VimScipt 以及 Vim9Script 的运行速度。
比较的结果显而易见,Luajit 的速度比起 VimScipt 以及 Vim9Script 快很多,而我的配置还是比较重的,因此选择 Lua 将会大大提升使用体验。
学习 Lua
Lua 是一门语法非常简单的编程语言,可以查看《学习 Lua 脚本语言》。
配置文件结构
初始化文件
Neovim 的初始化文件是 ~\AppData\Local\nvim\init.lua
,Linux 系统是 ~/.config/nvim/init.lua
。Neovim 启动时会自动读取并执行该文件内容。
ftplugin
当打开一个文件,Neovim 通常会自动识别文件类型并且自动设定 filetype
。比如,打开 Test.java
文件,此时 ftplugin/java.lua
就会被调用执行。
Options
Neovim 提供了多种方式设置 Neovim Option。
使用 Neovim API:
Neovim 提供了设置 Option 的 API 函数:nvim_set_option_value({name}, {value}, {opts})
,opts
是一个 Lua table,支持的 key 包括:
- scope:
grobal
或者local
,类似于:setglobal
和:setlocal
- win: 设置指定 window-ID 的 local option
- buf:设置指定 buffer number 的 local option
使用 vim.o
:
这类似于直接使用 :set
命令,默认是设置 global option。
vim.o.number = false -- 禁用行号,启用行号可以设置为 true
vim.o.relativenumber = false -- 禁用相对行号
使用 vim.bo
、vim.wo
:
这类似于使用 :setlocal
命令,但是还是有些区别的,按照 :h vim.bo
描述,其使用的格式是:vim.bo[{bufnr}].opt_name
,
如果设置的 option 不是 local to buffer 就会报错,比如执行 :lua vim.bo.number = true
就会报错:
E5108: Error executing lua [string ":lua"]:1: 'buf' cannot be passed for window-local option 'number'
stack traceback:
[C]: in function '__newindex'
[string ":lua"]:1: in main chunk
同理,使用 vim.wo
时也会有类似的问题,因此在使用这两个方式设置 option 时,需要判断 option 是 local to window 还是 local to buffer, 一般在 help 文档里面都有。
比如 :h 'number'
:
'number' 'nu' boolean (default off)
local to window
当然,Vim 也提供了函数去判断,nvim_get_option_info2({name}, {opts})
返回值是一个 lua table,其中 scope
key 值就可以用来判断,其值可以是 global
、win
、buf
。
实际上,如果去看 Neovim 的源码,你会发现不管是 vim.o 还是 vim.bo 等 只不是 API 的 wrap 而已:
vim.o = setmetatable({}, {
__index = function(_, k)
return api.nvim_get_option_value(k, {})
end,
__newindex = function(_, k, v)
return api.nvim_set_option_value(k, v, {})
end,
})
因此,如果需要的话,也可以自己设置一个新的函数批量设置 option, 比如,某个插件打开了一个浮窗,浮窗内是对应的插件 buffer。
local function set_local(winid, bufnr, opts)
for k, v in pairs(opts) do
if vim.api.nvim_get_option_info2(k, {}).scope == 'win' then
vim.api.nvim_set_option_value(k, v, { win = winid })
elseif vim.api.nvim_get_option_info2(k, {}).scope == 'buf' then
vim.api.nvim_set_option_value(k, v, { buf = bufnr })
else
-- skip global opt
end
end
end
local winid, bufnr = open_plugin_float_win()
set_local(winid, bufnr, {
number = false,
relativenumber = false,
filetype = 'java',
bufhidden = 'wipe',
})
事件自动命令
使用 VimScipt 定义自动命令(autocmd)的时候,通常我们可以这样写:
augroup test_augroup_vim
autocmd!
autocmd WinEnter * call s:test1()
autocmd WinEnter * call s:test2()
augroup END
此时,如果需要清除上述两个自动命令的其中一个,就无法实现。只能使用如下脚本全部清除:
augroup text_augroup_vim
autocmd!
augroup END
而 Neovim 提供了两个函数 nvim_create_autocmd
和 nvim_del_autocmd
,演示如下:
local function test1()
print(1)
end
local function test2()
print(2)
end
local group =
vim.api.nvim_create_augroup('test_augroup_neovim', { clear = true })
local autocmd_id1 = vim.api.nvim_create_autocmd({ 'WinEnter' }, {
group = group,
pattern = { '*' },
callback = test1,
})
local autocmd_id2 = vim.api.nvim_create_autocmd({ 'WinEnter' }, {
group = group,
pattern = { '*' },
callback = test2,
})
此时如果需要删除第二个自动命令,只需要:
vim.api.nvim_del_autocmd(autocmd_id2)
用户自定义命令
可以使用 vim.api.nvim_create_user_command({name}, {command}, {opts})
API 来新建命令。
这个 API 函数接受三个参数,第一个顾名思义就是新建的命令的具体名称,
第二个 command
可以是一个 string 也可以是一个 lua function。如果是 string,
当执行这个命令时,这段字符串会被直接执行。如果是 Lua function,则会被传入一个 table 参数调用。
第三个参数是设定命令的一些参数,类似于 :h command-attributes
比如:
vim.api.nvim_create_user_command('SayHello', function(opt)
print('hello')
end, {
nargs = '*',
bang = true,
})
在上述函数内,参数 opt 是一个 Lua table,key 值包括:
{ "bang", "reg", "range", "args", "mods", "line1", "smods", "fargs", "line2", "count", "name" }
bang:
如果定义命令时,第三个参数 bang = true,那么可以在执行命令时带上感叹号。
vim.api.nvim_create_user_command('Test', function(opt)
print(opt.bang)
end, {
nargs = '*',
bang = true,
})
上述定义的命令,执行 :Test
时打印 false,执行 :Test!
时打印 true,也可以在函数内判断 if opt.bang
为命令是否带感叹号赋予不一样的意义。
有一些插件的命令支持两个感叹号,是如何实现的呢?比如 vim-gina
Single command. Users do not need to remember tons of commands
:Gina {command}
will execute a gina command or a git raw command asynchronously:Gina! {command}
will execute a git raw command asynchronously:Gina!! {command}
will execute a git raw command in a shell (mainly for:Gina!! add -p
or:Gina!! rebase -i
)
下面是简单的实现代码:
vim.api.nvim_create_user_command('Gina', function(opt)
if not opt.bang then
print('it is Gina')
elseif opt.bang and opt.fargs[1] == '!' then
print('it is Gina!!')
else
print('it is Gina!')
end
end, {
nargs = '*',
bang = true,
})
nargs 可以设置为 0、1、’*‘、’?’、’+’ 五种。
vim.api.nvim_create_user_command('Test1', function(opt)
end, {
nargs = 0,
})
vim.api.nvim_create_user_command('Test2', function(opt)
vim.print(opt.args)
vim.print(opt.fargs)
end, {
nargs = 1,
})
vim.api.nvim_create_user_command('Test3', function(opt)
vim.print(opt.args)
vim.print(opt.fargs)
end, {
nargs = '*',
})
vim.api.nvim_create_user_command('Test4', function(opt)
vim.print(opt.args)
vim.print(opt.fargs)
end, {
nargs = '+',
})
vim.api.nvim_create_user_command('Test5', function(opt)
vim.print(opt.args)
vim.print(opt.fargs)
end, {
nargs = '?',
})
nargs 为 0,执行命令如果带上参数,那么就会报错。比如::Test1 foo
报错 E488: Trailing characters: foo
。
nargs 为 1 时,如果执行 :Test2 foo zaa xss
就会发现输出内容为:
foo zaa xss
{ "foo zaa xss" }
意味着,如果设置为 1,执行命令的后面一整串字符串不管是否有空格间隔,都被当作为一个参数。
nargs 为 *
时,如果执行 :Test3 foo zaa xss
就会发现输出内容为:
foo zaa xss
{ "foo", "zaa", "xss" }
可以看出,args 还是整个字符串,但是 fargs 则是一个列表,包含了三个参数
nargs 为 +
时,如果执行 :Test4 foo zaa xss
就会发现输出内容为:
foo zaa xss
{ "foo", "zaa", "xss" }
不同的是,如果执行 :Test4
不带参数,则会报错:E471: Argument required
nargs 为 ?
时,如果执行 :Test5 foo zaa xss
就会发现输出内容为:
foo zaa xss
{ "foo zaa xss" }
与设置成 1 不同的是,:Test5
可以不带参数直接执行
设置快捷键
Neovim 提供了 vim.keymap.set()
函数来设置快捷键,
如果阅读源码的话,也可以发现,实际上这个函数也不过是包装了 Neovim API 函数 vim.api.nvim_buf_set_keymap
和 vim.api.nvim_set_keymap
。
但是原生的 API nvim_set_keymap
和 nvim_buf_set_keymap
使用起来太麻烦了,对于一些特殊按键还需要使用 nvim_replace_termcodes()
去转换。
而 vim.keymap.set({mode}, {lhs}, {rhs}, {opts})
的优势是:
differences compared to regular set_keymap:
- remap is used as opposite of noremap. By default it's true for <Plug> keymaps and false for others.
- rhs can be lua function.
- mode can be a list of modes.
- replace_keycodes option for lua function expr maps. (Default: true)
- handles buffer specific keymaps
这个 commit 移除了 <Plug> kemaps
检测, 这里是与 Vim 不一致的地方。
vim.keymap.set('i', '<Esc>', function()
vim.cmd('noautocmd stopinsert')
vim.api.nvim_win_close(prompt_winid, true)
vim.api.nvim_win_close(result_winid, true)
end, { buffer = prompt_bufid })
变量类型转换
不管是从 Lua 到 VimScipt 还是从 VimScipt 到 Lua,变量类型转换是都是创建了一个副本,副本做的修改,通常无法在原变量上体现。
这也是我尽量避免使用 vim.fn
原因。
比如以下这段 VimScipt:
let s:a = [1, 2]
call add(s:a, 3)
echo s:a
以上输出是 [1, 2, 3]
。
换成 Lua:
local a = {1, 2}
vim.fn.add(a, 3)
vim.print(a) --- 输出是 {1, 2}
看一下 vim.fn
的源码:
-- vim.fn.{func}(...)
---@nodoc
vim.fn = setmetatable({}, {
--- @param t table<string,function>
--- @param key string
--- @return function
__index = function(t, key)
local _fn --- @type function
if vim.api[key] ~= nil then
_fn = function()
error(string.format('Tried to call API function with vim.fn: use vim.api.%s instead', key))
end
else
_fn = function(...)
return vim.call(key, ...)
end
end
t[key] = _fn
return _fn
end,
})
实际上就是调用的 vim.call(key, ...)
因此应当尽量避免使用 vim.fn
调用 VimScipt 函数操作 Lua 变量,前面的示例应该改为:
local a = { 1, 2 }
table.insert(a, 3)
vim.print(a) --- 输出是 {1, 2, 3}