好几年前,我使用 Vim Script 实现过一个悬浮侧栏插件 scrollbar.vim, 前段时间该插件使用 Lua 进行了重写并改名称为 scrollbar.nvim, 重写后的插件只支持 Neovim。
scrollbar.nvim 会在当前窗口的右侧使用浮窗绘制一个滚动条,其位置依据当前窗口显示的内容在整个文件中所在的行数, 并且随着鼠标移动、滚屏等操作上下移动。
使用 nvim-plug 进行安装:
require('plug').add({
{
'wsdjeg/scrollbar.nvim'
}
})
或者使用 luarocks 进行安装:
luarocks install scrollbar.nvim
require('scrollbar').setup({
max_size = 10,
min_size = 5,
width = 1,
right_offset = 1,
excluded_filetypes = {
'startify',
'git-commit',
'leaderf',
'NvimTree',
'tagbar',
'defx',
'neo-tree',
'qf',
},
shape = {
head = '▲',
body = '█',
tail = '▼',
},
highlight = {
head = 'Normal',
body = 'Normal',
tail = 'Normal',
},
debug = false,
})
前面再阅读一些插件源码时,发现一个问题,很多插件的使用了 ftplugin 这个目录,其内的脚本文件中直接使用了 setlocal xx=xx 这样的语法。
在早期的 Neovim 或者 Vim 版本中这样确实没有问题,但是随着 Neovim 功能特性增加。这样写就会容易出错。
实际上,直到目前为止 Neovim 和 Vim 的官方文档 :h ftplugin 内的示例还是:
" Only do this when not done yet for this buffer
if exists("b:did_ftplugin")
finish
endif
let b:did_ftplugin = 1
setlocal textwidth=70
Neovim 插件的 ftplugin 目录是一个特殊的文件夹,其内的文件会在 FileType 事件触发是被载入。
看一下 Neovim 的源码,ftplugin 目录下的文件是如何被载入的。
augroup filetypeplugin
au FileType * call s:LoadFTPlugin()
func! s:LoadFTPlugin()
if exists("b:undo_ftplugin")
exe b:undo_ftplugin
unlet! b:undo_ftplugin b:did_ftplugin
endif
let s = expand("<amatch>")
if s != ""
if &cpo =~# "S" && exists("b:did_ftplugin")
" In compatible mode options are reset to the global values, need to
" set the local values also when a plugin was already used.
unlet b:did_ftplugin
endif
" When there is a dot it is used to separate filetype names. Thus for
" "aaa.bbb" load "aaa" and then "bbb".
for name in split(s, '\.')
" Load Lua ftplugins after Vim ftplugins _per directory_
" TODO(clason): use nvim__get_runtime when supports globs and modeline
" XXX: "[.]" in the first pattern makes it a wildcard on Windows
exe $'runtime! ftplugin/{name}[.] ftplugin/{name}_*. ftplugin/{name}/*.'
endfor
endif
endfunc
augroup END
以上内容不难看出,Neovim 实际上是监听了 FileType 这个事件,然后根据 expand('<amatch>') 的值来执行 :runtime 命令。
但是,随着 Neovim 和 Vim 增加了设置非当前 buffer 的 option 这一功能后。就会出现这样问题,当 FileType 事件触发时,触发的 buffer 并非是当前 buffer。
那么在 ftplugin 内如果使用了 setlocal 这样的命令,有可能会设置错了缓冲区。
test_ft.lua
local log = require("logger").derive("ft")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("-----------------------------------------------------")
local real_current_win = vim.api.nvim_get_current_win()
local newbuf = vim.api.nvim_create_buf(true, false)
local events = {}
for _, v in ipairs(vim.fn.getcompletion("", "event")) do
if not vim.endswith(v, "Cmd") then
table.insert(events, v)
end
end
local id = vim.api.nvim_create_autocmd(events, {
group = vim.api.nvim_create_augroup("test_ft", { clear = true }),
pattern = { "*" },
callback = function(ev)
log.info("-----------------------------------------------------")
log.info("event is " .. ev.event)
log.info("ev.buf is " .. ev.buf)
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("real_current_win's buf is" .. vim.api.nvim_win_get_buf(real_current_win))
end,
})
vim.api.nvim_open_win(newbuf, false, { split = "above" })
vim.api.nvim_set_option_value("filetype", "test123", { buf = newbuf })
vim.api.nvim_del_autocmd(id)
log.info("-----------------------------------------------------")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
[ 23:50:19:932 ] [ Info ] [ ft ] nvim_get_current_buf() is 7
[ 23:50:19:932 ] [ Info ] [ ft ] nvim_get_current_win() is 1000
[ 23:50:19:932 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:50:19:933 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:50:19:933 ] [ Info ] [ ft ] event is WinNew
[ 23:50:19:933 ] [ Info ] [ ft ] ev.buf is 7
[ 23:50:19:933 ] [ Info ] [ ft ] nvim_get_current_buf() is 7
[ 23:50:19:933 ] [ Info ] [ ft ] nvim_get_current_win() is 1008
[ 23:50:19:933 ] [ Info ] [ ft ] real_current_win's buf is7
[ 23:50:19:934 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:50:19:934 ] [ Info ] [ ft ] event is BufWinEnter
[ 23:50:19:934 ] [ Info ] [ ft ] ev.buf is 9
[ 23:50:19:934 ] [ Info ] [ ft ] nvim_get_current_buf() is 9
[ 23:50:19:934 ] [ Info ] [ ft ] nvim_get_current_win() is 1008
[ 23:50:19:934 ] [ Info ] [ ft ] real_current_win's buf is7
[ 23:50:19:953 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:50:19:953 ] [ Info ] [ ft ] event is Syntax
[ 23:50:19:953 ] [ Info ] [ ft ] ev.buf is 9
[ 23:50:19:953 ] [ Info ] [ ft ] nvim_get_current_buf() is 9
[ 23:50:19:953 ] [ Info ] [ ft ] nvim_get_current_win() is 1008
[ 23:50:19:953 ] [ Info ] [ ft ] real_current_win's buf is7
[ 23:50:19:954 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:50:19:954 ] [ Info ] [ ft ] event is FileType
[ 23:50:19:954 ] [ Info ] [ ft ] ev.buf is 9
[ 23:50:19:954 ] [ Info ] [ ft ] nvim_get_current_buf() is 9
[ 23:50:19:954 ] [ Info ] [ ft ] nvim_get_current_win() is 1008
[ 23:50:19:954 ] [ Info ] [ ft ] real_current_win's buf is7
[ 23:50:19:954 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:50:19:954 ] [ Info ] [ ft ] event is OptionSet
[ 23:50:19:954 ] [ Info ] [ ft ] ev.buf is 0
[ 23:50:19:954 ] [ Info ] [ ft ] nvim_get_current_buf() is 9
[ 23:50:19:954 ] [ Info ] [ ft ] nvim_get_current_win() is 1008
[ 23:50:19:954 ] [ Info ] [ ft ] real_current_win's buf is7
[ 23:50:19:954 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:50:19:954 ] [ Info ] [ ft ] nvim_get_current_buf() is 7
[ 23:50:19:954 ] [ Info ] [ ft ] nvim_get_current_win() is 1000
可以看到,在 event 触发 callback 函数内 nvim_get_current_win 和 nvim_get_current_buf 都临时被修改了。
测试一下,不开窗口效果呢?
local log = require("logger").derive("ft")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("-----------------------------------------------------")
local real_current_win = vim.api.nvim_get_current_win()
local newbuf = vim.api.nvim_create_buf(true, false)
local events = {}
for _, v in ipairs(vim.fn.getcompletion("", "event")) do
if not vim.endswith(v, "Cmd") then
table.insert(events, v)
end
end
local id = vim.api.nvim_create_autocmd(events, {
group = vim.api.nvim_create_augroup("test_ft", { clear = true }),
pattern = { "*" },
callback = function(ev)
log.info("-----------------------------------------------------")
log.info("event is " .. ev.event)
log.info("ev.buf is " .. ev.buf)
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("real_current_win's buf is" .. vim.api.nvim_win_get_buf(real_current_win))
end,
})
-- vim.api.nvim_open_win(newbuf, false, { split = "above" })
vim.api.nvim_set_option_value("filetype", "test123", { buf = newbuf })
vim.api.nvim_del_autocmd(id)
log.info("-----------------------------------------------------")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
[ 23:53:49:058 ] [ Info ] [ ft ] nvim_get_current_buf() is 10
[ 23:53:49:058 ] [ Info ] [ ft ] nvim_get_current_win() is 1000
[ 23:53:49:058 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:53:49:078 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:53:49:078 ] [ Info ] [ ft ] event is Syntax
[ 23:53:49:078 ] [ Info ] [ ft ] ev.buf is 12
[ 23:53:49:078 ] [ Info ] [ ft ] nvim_get_current_buf() is 12
[ 23:53:49:078 ] [ Info ] [ ft ] nvim_get_current_win() is 1001
[ 23:53:49:078 ] [ Info ] [ ft ] real_current_win's buf is10
[ 23:53:49:079 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:53:49:079 ] [ Info ] [ ft ] event is FileType
[ 23:53:49:079 ] [ Info ] [ ft ] ev.buf is 12
[ 23:53:49:079 ] [ Info ] [ ft ] nvim_get_current_buf() is 12
[ 23:53:49:079 ] [ Info ] [ ft ] nvim_get_current_win() is 1001
[ 23:53:49:079 ] [ Info ] [ ft ] real_current_win's buf is10
[ 23:53:49:079 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:53:49:079 ] [ Info ] [ ft ] event is OptionSet
[ 23:53:49:079 ] [ Info ] [ ft ] ev.buf is 0
[ 23:53:49:079 ] [ Info ] [ ft ] nvim_get_current_buf() is 12
[ 23:53:49:079 ] [ Info ] [ ft ] nvim_get_current_win() is 1001
[ 23:53:49:079 ] [ Info ] [ ft ] real_current_win's buf is10
[ 23:53:49:079 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:53:49:079 ] [ Info ] [ ft ] nvim_get_current_buf() is 10
[ 23:53:49:079 ] [ Info ] [ ft ] nvim_get_current_win() is 1000
这窗口 1001 是什么鬼?临时隐藏窗口?
local log = require("logger").derive("ft")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("-----------------------------------------------------")
local real_current_win = vim.api.nvim_get_current_win()
local newbuf = vim.api.nvim_create_buf(true, false)
local events = {}
for _, v in ipairs(vim.fn.getcompletion("", "event")) do
if not vim.endswith(v, "Cmd") then
table.insert(events, v)
end
end
local id = vim.api.nvim_create_autocmd(events, {
group = vim.api.nvim_create_augroup("test_ft", { clear = true }),
pattern = { "*" },
callback = function(ev)
log.info("-----------------------------------------------------")
log.info("event is " .. ev.event)
log.info("ev.buf is " .. ev.buf)
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info('win count is ' .. vim.fn.winnr('$'))
log.info('winconfig is ' .. vim.inspect(vim.api.nvim_win_get_config(vim.api.nvim_get_current_win())))
log.info("real_current_win's buf is" .. vim.api.nvim_win_get_buf(real_current_win))
end,
})
-- vim.api.nvim_open_win(newbuf, false, { split = "above" })
vim.api.nvim_set_option_value("filetype", "test123", { buf = newbuf })
vim.api.nvim_del_autocmd(id)
log.info("-----------------------------------------------------")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
[ 23:57:49:249 ] [ Info ] [ ft ] nvim_get_current_buf() is 9
[ 23:57:49:249 ] [ Info ] [ ft ] nvim_get_current_win() is 1000
[ 23:57:49:249 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:57:49:268 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:57:49:268 ] [ Info ] [ ft ] event is Syntax
[ 23:57:49:268 ] [ Info ] [ ft ] ev.buf is 13
[ 23:57:49:268 ] [ Info ] [ ft ] nvim_get_current_buf() is 13
[ 23:57:49:268 ] [ Info ] [ ft ] nvim_get_current_win() is 1001
[ 23:57:49:268 ] [ Info ] [ ft ] win count is 2
[ 23:57:49:268 ] [ Info ] [ ft ] winconfig is {
anchor = "NW",
col = 0,
external = false,
focusable = false,
height = 5,
hide = false,
mouse = false,
relative = "editor",
row = 0,
width = 168,
zindex = 50
}
[ 23:57:49:268 ] [ Info ] [ ft ] real_current_win's buf is9
[ 23:57:49:269 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:57:49:269 ] [ Info ] [ ft ] event is FileType
[ 23:57:49:269 ] [ Info ] [ ft ] ev.buf is 13
[ 23:57:49:269 ] [ Info ] [ ft ] nvim_get_current_buf() is 13
[ 23:57:49:269 ] [ Info ] [ ft ] nvim_get_current_win() is 1001
[ 23:57:49:269 ] [ Info ] [ ft ] win count is 2
[ 23:57:49:270 ] [ Info ] [ ft ] winconfig is {
anchor = "NW",
col = 0,
external = false,
focusable = false,
height = 5,
hide = false,
mouse = false,
relative = "editor",
row = 0,
width = 168,
zindex = 50
}
[ 23:57:49:270 ] [ Info ] [ ft ] real_current_win's buf is9
[ 23:57:49:270 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:57:49:270 ] [ Info ] [ ft ] event is OptionSet
[ 23:57:49:270 ] [ Info ] [ ft ] ev.buf is 0
[ 23:57:49:270 ] [ Info ] [ ft ] nvim_get_current_buf() is 13
[ 23:57:49:270 ] [ Info ] [ ft ] nvim_get_current_win() is 1001
[ 23:57:49:270 ] [ Info ] [ ft ] win count is 2
[ 23:57:49:270 ] [ Info ] [ ft ] winconfig is {
anchor = "NW",
col = 0,
external = false,
focusable = false,
height = 5,
hide = false,
mouse = false,
relative = "editor",
row = 0,
width = 168,
zindex = 50
}
[ 23:57:49:270 ] [ Info ] [ ft ] real_current_win's buf is9
[ 23:57:49:270 ] [ Info ] [ ft ] -----------------------------------------------------
[ 23:57:49:270 ] [ Info ] [ ft ] nvim_get_current_buf() is 9
[ 23:57:49:270 ] [ Info ] [ ft ] nvim_get_current_win() is 1000
这里说明一下,即便是 nvim_open_win 没有执行,Neovim 也会新建一个 autocmd windows,使用 win_gettype() 函数可以获取值为 “autocmd”。
有这样一个需求,要给自己多个 Github 仓库增加多个仓库 secrets,以便于 Action 中使用。 起初我是手动在每个仓库的设置页面逐一复制粘贴来添加的。但是这样效率明显太低,而且容易出错,此外后期若需要修改, 还得要重新操作。
于是我就想着是否可以使用脚本访问 Github API 来更改,查阅了 GitHub secrets API 文档,也找到了相关信息。 这篇文章就是我使用 Neovim 编辑并批量更新 Github 仓库 secrets 的完整方案。
github.nvim 是一个 GitHub REST API 的 Lua 实现,用于在 Neovim 中访问 Github API。 早些年,我使用 Vim Script 写过类似的 Vim 插件 GitHub.vim, 而 github.nvim 算是 github.vim 的 Lua 重新实现版本,目前也仅仅实现了部分 API,会更具个人使用需要,陆续再实现其他的 API。
可以使用任意 Neovim 插件管理器安装,比如 nvim-plug,
require('plug').add({
'wsdjeg/github.nvim',
})
当然了,你也可以使用 luarocks 安装:
luarocks install github.nvim
由于 Github 的 secrts API 访问时,传输的数据都是要使用 libsodium 加密,因此需要安装 libsodium,以及对应的 Lua 模块。 这里记录一下 Windows 下安装这两个工具遇到的坑:
从官网下载 libsodium-1.0.20-stable-msvc.zip,解压后,
需要将 libsodium\x64\Release\v143\dynamic 目录加入到环境变量 PATH 内。
vim.env.PATH = vim.env.PATH
.. ';'
.. [[D:\Downloads\libsodium-1.0.20-stable-msvc\libsodium\x64\Release\v143\dynamic]]
此时再使用 luarocks 安装 luasodium,需要指定 libsodium 的安装目录:
luarocks install luasodium SODIUM_INCDIR=D:\Downloads\libsodium-1.0.20-stable-msvc\libsodium\include SODIUM_DIR=D:\Downloads\libsodium-1.0.20-stable-msvc\libsodium\x64\Release\v143\dynamic
如果提示安装成功后,在 Neovim 内如果使用以下 Lua 脚本,只有最后一个会报错。
local luasodium_ffi = require'luasodium' -- ok
local luasodium_ffi = require'luasodium.ffi' -- uses the FFI API (in a C module) ok
local luasodium_c = require'luasodium.core' -- uses the C API ok
local luasodium_pureffi = require'luasodium.pureffi' -- 失败,因为他使用 `require('ffi').load('sodium')`, 应该是 libsodium
最后一个失败的原因时因为 luasodium 源码文件 lib_loader.lua 中使用了 require(‘ffi’).load(‘sodium’)
– 在luasodium 修复之前,可以临时如下操作 – 将 dynamic 目录里面的 libsodium.dll 改名为 sodium.dll
我也给 luasodium 提交了一个 PR 来解决这个问题。
确保上述 libsodium、luasodium、github.nvim 都安装好了之后,就可以使用以下脚本了:
local secrts = {
{
name = 'DOCKER_API_KEY',
value = '12jdksjdiiwkdjsskkdj',
},
{
name = 'LUAROCKS_API_KEY',
value = 'ijnuhbygvtfcrdxesz',
},
}
local repos = {
'picker.nvim',
'format.nvim',
'tasks.nvim',
}
for _, repo in ipairs(repos) do
for _, secrt in ipairs(secrts) do
require('github.secrets').update_repository_secret('wsdjeg', repo, secrt)
end
end
通过上述脚本,就给批量给自己的 Github 仓库设定 secrets,后期如果 API_KEY 修改了, 只需要修改脚本后再执行一次即可。
在使用 Git 过程中,有时候会遇到这样的情况:某个以前删除的文件,不记得当时删除的 commit message 内容了, 也不记得完整的文件路径,只记得文件部分名称,想要找到是哪一个 commit 删除的,发现比较难。
实际上可以通过下方的命令来时间这一功能,首先使用下面的命令搜索删掉的文件路径包含 src 的文件列表:
git log --diff-filter=D --summary | rg delete | rg src
如果要使用 code-runner.nvim,执行并输出,需要设定 ps 的 runner:
require('code-runner').setup({
runners = {
lua = { exe = 'nvim', opt = { '-l', '-' }, usestdin = true },
ps = { exe = 'powershell.exe', opt = { '-Command', '-' }, usestdin = true },
},
})
快捷键调用 code-runner 输出:
[Running] powershell.exe -Command - STDIN
--------------------
delete mode 100644 src/test/hello.c
delete mode 100644 src/example/delete.c
[Done] exited with code=0, single=0 in 0.916431 seconds
获取到上述文件列表后,通过 git.nvim 执行以下命令,就可以打开 log 列表,列表内回车会分屏展示 commit diff。
:Git log -1 -- src/test/hello.c
最近,给 git.nvim 增加了一个 git-ghosts 的 picker.nvim 拓展。其实现的原理就是参考上述的命令执行顺序。 先获取已删除文件的列表,回车获取改文件的最后一次 commit 的哈希值,然后执行:
:Git show <commit_hash>
luarocks 是 lua 常用的包管理器,类似于 python 的 pip。前面使用 Lua 实现了一个 Neovim 的插件管理器,而目前我自己正在维护的插件也都是使用 Lua 来实现的。 因此使用 luarocks 来管理插件,同时又让插件管理器 nvim-plug 支持 luarocks 就显得很有必要了。看了下目前主流的插件管理器 lazy.nvim、rocks.nvim 也是支持 luarcoks 的。
在 Windows 下可以使用 scoop 命令进行安装:
scoop install luarocks
这里需要注意一下,上述命令会自动安装 lua 5.4,但是 Neovim 内默认使用的是 luajit 兼容的 lua 5.1。因此为了避免下载的 rocks 不兼容。可以再执行以下命令:
scoop uninstall lua
scoop install lua51
安装完成后检查一下:
luarocks config | rg deploy
输出内容:
deploy_bin_dir = "D:\\Scoop\\apps\\luarocks\\current\\rocks\\bin"
deploy_lib_dir = "D:\\Scoop\\apps\\luarocks\\current\\rocks\\lib\\lua\\5.1"
deploy_lua_dir = "D:\\Scoop\\apps\\luarocks\\current\\rocks\\share\\lua\\5.1"
在 Neovim 内使用 :lua 命令或者使用 lua 开发 Neovim 插件时,
若想要使用 luarocks 安装的包,其原理就是将 luarocks 所安装的包位置加入到
package.path 和 package.cpath:
nvim-plug 中实现这一步骤的逻辑如下:
function M.enable()
if enabled then
return
end
local ok, _ = pcall(function()
local luarocks_config = vim.json.decode(
vim.system({ 'luarocks', 'config', '--json' }):wait().stdout
)
package.path = package.path
.. ';'
.. luarocks_config.deploy_lua_dir
.. [[\?.lua]]
.. ';'
.. luarocks_config.deploy_lua_dir
.. [[\?\init.lua]]
.. ';'
package.cpath = package.cpath
.. ';'
.. luarocks_config.deploy_lib_dir
.. '\\?.'
.. luarocks_config.external_lib_extension
-- 此处,还可以将 luarcoks bin 目录加入到 PATH
vim.env.PATH = vim.env.PATH .. ';' .. luarocks_config.deploy_bin_dir
end)
if ok then
enabled = true
end
end
可以在添加插件时,指定 type = 'rocks',比如:
return {
'wsdjeg/mru.nvim',
events = { 'UIEnter' },
opts = {
enable_cache = true,
ignore_path_regexs = {
'/.git/',
'/nvim/runtime/doc/',
'.mp3$',
'.mp4$',
'.png$',
'.jpg$',
'.exe$',
'nvim-mru.json$',
'tags$',
},
enable_logger = true,
sort_by = 'lastenter',
},
type = 'rocks',
desc = 'mru(most recently used) files',
}
参考以上方式添加插件后,nvim-plug 在安装插件时会自动调用 luarocks install plugin_name 这一命令。
在实现完上述功能后,才发现 luarocks 这个包管理器似乎还有一些限制。比如:
不支持同时安装多个插件。
因为 nvim-plug 是使用异步 job 调用外部命令的,因此支持多线程。 但是起初实现后发现,当同时执行多个 luarocks install 命令时, 只有第一个是成功的,后续的命令都有会报这一错误:Error: command ‘install’ requires exclusive write access。
解决的办法是为 luarocks 实现单独的 tasks 序列,逐一执行,这样的话插件的安装会非常慢。一个是单线程,一个是 16 线程 (max_processes = 16)。
无法根据 plugSpec 获取的 rtp 目录位置
一个最简单 plugSpec 比如 { 'wsdjeg/mru.nvim' },
默认 type 是 git,我是可以获取到该插件默认的 runtimepath 值为
plug.config.bundle_dir .. '/' .. 'wsdjeg/mru.nvim',
此时就可以根据这个目录是否存在来判断插件是否已安装。
但是,这样一个 plugSpec:
return {
'wsdjeg/mru.nvim',
type = 'rocks',
}
将无法获取到默认的 runtimepath 目录位置,因为他的格式是
D:/Scoop/apps/luarocks/current/rocks/lib/luarocks/rocks-5.1/mru.nvim/1.4.0-1
最后面这个版本号,除非是 plugSpec 内指定,否则是无法判断到默认的值的。
最终的解决方案是分析 luarocks list 命令的输出内容,返回一个类似与这样的 lua table:
return {
['mru.nvim'] = {
rtp = 'D:/Scoop/apps/luarocks/current/rocks/lib/luarocks/rocks-5.1/mru.nvim/1.4.0-1',
},
['rooter.nvim'] = {
rtp = 'D:/Scoop/apps/luarocks/current/rocks/lib/luarocks/rocks-5.1/rooter.nvim/1.3.0-1',
},
}
这里主要使用到两个 Github actions:
使用 googleapis/release-please-action 来自动打 tag 并且新建 GitHub release,可以参考之前的文章《Github 仓库自动 release》。
使用 nvim-neorocks/luarocks-tag-release GitHub action 自动将 tag 上传到 luarocks.org。
在仓库根目录新建文件 .github/workflows/luarocks.yml:
name: Push to Luarocks
on:
push:
tags: # Will upload to luarocks.org when a tag is pushed
- "*"
pull_request: # Will test a local install without uploading to luarocks.org
workflow_dispatch:
jobs:
luarocks-upload:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: LuaRocks Upload
uses: nvim-neorocks/luarocks-tag-release@v7
env:
LUAROCKS_API_KEY: $
存在一个问题,luarocks-tag-release 使用 copy_directories 来复制打包插件的其他文件, 但是 luarocks 一旦使用了 copy_directories 这一设置,就不会复制默认的 doc 文件了。
相关 issue : luarocks/issues/1266
代码逻辑 src/luarocks/build.lua#L344-L363
neovim 中直接使用 rocks 似乎还有问题,dll 文件载入时会报错,估计跟 luarocks 的包编译方式有关
D:\wsdjeg\my-blog>luarocks list | rg file -A 2
luafilesystem
1.8.0-1 (installed) - D:\Scoop\apps\luarocks\current\rocks\lib\luarocks\rocks-5.1
然后在 Neovim 内执行 :lua require('lfs') 时,报错:
E5108: Error executing lua error loading module 'lfs' from file 'D:\Scoop\apps\luarocks\current\rocks\lib\lua\5.1\lfs.dll':
找不到指定的模块。
stack traceback:
[C]: at 0x7ff83ac1bdb0
[C]: in function 'require'
[string ":lua"]:1: in main chunk
实际上这个 dll 文件是存在的:
D:\Scoop\apps\luarocks\current\rocks\lib\lua\5.1>ls
lfs.dll
使用 scoop 安装 dependencies,
scoop install dependencies
打开 lfs.dll 文件,发现确实是依赖问题:
在 lua51 的安装目录里:
D:\Scoop\apps\lua51\current>ls
Microsoft.VC80.CRT install.json lua5.1.dll.manifest lua51.dll manifest.json
bin2c5.1.exe liblua5.1.a lua5.1.exe lua51.dll.manifest wlua5.1.exe
include lua5.1.dll lua5.1.exe.manifest luac5.1.exe wlua5.1.exe.manifest
而 Neovim 中 :lua 调用的是:
D:\Scoop\apps\neovim\current\bin>ls
dbghelp.dll lua51.dll nvim.exe platforms win32yank.exe xxd.exe
使用 scoop 安装 luajit:
scoop install luajit
看下 luajit 的目录结构
D:\Scoop\apps\luajit/..
2.1.1762795099-1
current ➛ 2.1.1762795099-1
bin
lua51.dll
luajit
luajit-2.1.1762795099.exe
luajit.exe
include/luajit-2.1
lauxlib.h
lua.h
lua.hpp
luaconf.h
luajit.h
lualib.h
lib
share
install.json
manifest.json
修改 D:\Scoop\apps\luarocks\current\config.lua 为:
lua_interpreter = "D:/Scoop/apps/luajit/current/bin/luajit.exe"
lua_version = "5.1"
rocks_trees = {
"D:/Scoop/apps/luarocks/current/rocks"
}
variables = {
LUA = "D:/Scoop/apps/luajit/current/bin/luajit.exe",
LUA_BINDIR = "D:/Scoop/apps/luajit/current/bin",
LUA_INCDIR = "D:/Scoop/apps/luajit/current/include/luajit-2.1",
LUA_DIR = "D:/Scoop/apps/luajit/current/bin"
}
重新安装 luafilesystem:
luarocks install luafilesystem --force
此时再使用 Dependencies 查看 lfs.dll:
此时在 Neovim 中执行 :=require('lfs') 就会看到:
{
_COPYRIGHT = "Copyright (C) 2003-2017 Kepler Project",
_DESCRIPTION = "LuaFileSystem is a Lua library developed to complement the set of functions related to file systems offered by the standard Lua distribution",
_VERSION = "LuaFileSystem 1.8.0",
attributes = <function 1>,
chdir = <function 2>,
currentdir = <function 3>,
dir = <function 4>,
link = <function 5>,
lock = <function 6>,
lock_dir = <function 7>,
mkdir = <function 8>,
rmdir = <function 9>,
setmode = <function 10>,
symlinkattributes = <function 11>,
touch = <function 12>,
unlock = <function 13>
}
为了能在 Neovim 内置终端中使用 lua,luajit,luarocks 等,给 nvim-plug 增加这样一个 patch:
diff --git a/lua/plug/rocks/init.lua b/lua/plug/rocks/init.lua
index e336791..58f391d 100644
--- a/lua/plug/rocks/init.lua
+++ b/lua/plug/rocks/init.lua
@@ -71,6 +71,8 @@ function M.enable()
.. luarocks_config.deploy_lib_dir
.. '\\?.'
.. luarocks_config.external_lib_extension
+ vim.env.LUA_PATH = package.path
+ vim.env.LUA_CPATH = package.cpath
end)
if ok then
enabled = true
这样在 Neovim 内置终端内使用 lua 命令,或者 luajit 命令,就会自动读取这两个变量值。
D:\wsdjeg\my-blog>lua
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
> print(require('lfs'))
table: 00000000004DCAC0
>
D:\wsdjeg\my-blog>luajit
LuaJIT 2.1.1762795099 -- Copyright (C) 2005-2025 Mike Pall. https://luajit.org/
JIT: ON SSE3 SSE4.1 BMI2 fold cse dce fwd dse narrow loop abc sink fuse
> print(require('lfs'))
table: 0x01f72937bc70
>
D:\wsdjeg\my-blog>echo print(require("lfs")) | nvim -l -
table: 0x01dcba84a148
最近在修改 notify.nvim 插件源码的时候有这么一段:
local win_config = {
-- .....
}
if not vim.api.nvim_buf_is_valid(buf) then
buf = vim.api.nvim_create_buf(false, true)
end
if not vim.api.nvim_win_is_valid(win) then
win_config.noautocmd = true
win = vim.api.nvim_open_win(buf, false, win_config)
else
vim.api.nvim_win_set_config(win, win_config)
end
我把 win_config 内容省略了,主要想表达的意思是,
本来我想通过一个固定的窗口配置来打开或者设置窗口的参数。
但是 Neovim 的 nvim_open_win 和 nvim_win_set_config
两个函数所接受的 win_opt 选项是有区别的,已存在的窗口使用后者设置时不能够传入 noautocmd 选项(neovim#36409)。
'noautocmd' cannot be used with existing windows
于是只能分开写,在调用 nvim_open_win 时传入 noautocmd 参数。
那么这个 noautocmd 到底禁用了哪些事件,以及禁用的时机时什么呢?
以下为测试脚本:
local buf = vim.api.nvim_create_buf(true, false)
local log = require('logger').derive('t_no')
local aug = vim.api.nvim_create_augroup('test_noautocmd', { clear = true })
vim.api.nvim_create_autocmd(
{ 'WinEnter', 'BufWinEnter', 'BufEnter', 'WinLeave', 'TextChangedI' },
{
pattern = { '*' },
group = aug,
callback = function(ev)
log.info(ev.event)
end,
}
)
vim.api.nvim_open_win(buf, true, { split = 'above', noautocmd = true })
-- [ 20:43:20:664 ] [ Info ] [ t_no ] TextChangedI
-- [ 20:43:23:092 ] [ Info ] [ t_no ] WinLeave
-- [ 20:43:23:093 ] [ Info ] [ t_no ] WinEnter
-- [ 20:43:23:094 ] [ Info ] [ t_no ] BufEnter
local buf = vim.api.nvim_create_buf(true, false)
local log = require('logger').derive('t_no')
local aug = vim.api.nvim_create_augroup('test_noautocmd', { clear = true })
vim.api.nvim_create_autocmd(
{ 'WinEnter', 'BufWinEnter', 'BufEnter', 'WinLeave', 'TextChangedI' },
{
pattern = { '*' },
group = aug,
callback = function(ev)
log.info(ev.event)
end,
}
)
vim.api.nvim_open_win(buf, true, { split = 'above', noautocmd = false })
-- [ 20:44:50:454 ] [ Info ] [ t_no ] WinLeave
-- [ 20:44:50:455 ] [ Info ] [ t_no ] WinEnter
-- [ 20:44:50:456 ] [ Info ] [ t_no ] BufEnter
-- [ 20:44:50:456 ] [ Info ] [ t_no ] BufWinEnter
-- [ 20:44:51:279 ] [ Info ] [ t_no ] TextChangedI
-- [ 20:44:52:045 ] [ Info ] [ t_no ] WinLeave
-- [ 20:44:52:046 ] [ Info ] [ t_no ] WinEnter
-- [ 20:44:52:048 ] [ Info ] [ t_no ] BufEnter
如果去看 API 的源码,neovim 这段 nvim_open_win api 的源码。
Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Error *err)
FUNC_API_SINCE(6) FUNC_API_TEXTLOCK_ALLOW_CMDWIN
{
#define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key)
buf_T *buf = find_buffer_by_handle(buffer, err);
if (!buf) {
return 0;
}
if ((cmdwin_type != 0 && enter) || buf == cmdwin_buf) {
api_set_error(err, kErrorTypeException, "%s", e_cmdwin);
return 0;
}
WinConfig fconfig = WIN_CONFIG_INIT;
if (!parse_win_config(NULL, config, &fconfig, false, err)) {
return 0;
}
bool is_split = HAS_KEY_X(config, split) || HAS_KEY_X(config, vertical);
Window rv = 0;
if (fconfig.noautocmd) {
block_autocmds();
}
win_T *wp = NULL;
tabpage_T *tp = curtab;
assert(curwin != NULL);
win_T *parent = config->win == 0 ? curwin : NULL;
if (config->win > 0) {
parent = find_window_by_handle(fconfig.window, err);
if (!parent) {
// find_window_by_handle has already set the error
goto cleanup;
} else if (is_split && parent->w_floating) {
api_set_error(err, kErrorTypeException, "Cannot split a floating window");
goto cleanup;
}
tp = win_find_tabpage(parent);
}
if (is_split) {
if (!check_split_disallowed_err(parent ? parent : curwin, err)) {
goto cleanup; // error already set
}
if (HAS_KEY_X(config, vertical) && !HAS_KEY_X(config, split)) {
if (config->vertical) {
fconfig.split = p_spr ? kWinSplitRight : kWinSplitLeft;
} else {
fconfig.split = p_sb ? kWinSplitBelow : kWinSplitAbove;
}
}
int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER;
int size = (flags & WSP_VERT) ? fconfig.width : fconfig.height;
TRY_WRAP(err, {
if (parent == NULL || parent == curwin) {
wp = win_split_ins(size, flags, NULL, 0, NULL);
} else {
switchwin_T switchwin;
// `parent` is valid in `tp`, so switch_win should not fail.
const int result = switch_win(&switchwin, parent, tp, true);
assert(result == OK);
(void)result;
wp = win_split_ins(size, flags, NULL, 0, NULL);
restore_win(&switchwin, true);
}
});
if (wp) {
wp->w_config = fconfig;
if (size > 0) {
// Without room for the requested size, window sizes may have been equalized instead.
// If the size differs from what was requested, try to set it again now.
if ((flags & WSP_VERT) && wp->w_width != size) {
win_setwidth_win(size, wp);
} else if (!(flags & WSP_VERT) && wp->w_height != size) {
win_setheight_win(size, wp);
}
}
}
} else {
if (!check_split_disallowed_err(curwin, err)) {
goto cleanup; // error already set
}
wp = win_new_float(NULL, false, fconfig, err);
}
if (!wp) {
if (!ERROR_SET(err)) {
api_set_error(err, kErrorTypeException, "Failed to create window");
}
goto cleanup;
}
if (fconfig._cmdline_offset < INT_MAX) {
cmdline_win = wp;
}
// Autocommands may close `wp` or move it to another tabpage, so update and check `tp` after each
// event. In each case, `wp` should already be valid in `tp`, so switch_win should not fail.
// Also, autocommands may free the `buf` to switch to, so store a bufref to check.
bufref_T bufref;
set_bufref(&bufref, buf);
if (!fconfig.noautocmd) {
switchwin_T switchwin;
const int result = switch_win_noblock(&switchwin, wp, tp, true);
assert(result == OK);
(void)result;
if (apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf)) {
tp = win_find_tabpage(wp);
}
restore_win_noblock(&switchwin, true);
}
if (tp && enter) {
goto_tabpage_win(tp, wp);
tp = win_find_tabpage(wp);
}
if (tp && bufref_valid(&bufref) && buf != wp->w_buffer) {
// win_set_buf temporarily makes `wp` the curwin to set the buffer.
// If not entering `wp`, block Enter and Leave events. (cringe)
const bool au_no_enter_leave = curwin != wp && !fconfig.noautocmd;
if (au_no_enter_leave) {
autocmd_no_enter++;
autocmd_no_leave++;
}
win_set_buf(wp, buf, err);
if (!fconfig.noautocmd) {
tp = win_find_tabpage(wp);
}
if (au_no_enter_leave) {
autocmd_no_enter--;
autocmd_no_leave--;
}
}
if (!tp) {
api_set_error(err, kErrorTypeException, "Window was closed immediately");
goto cleanup;
}
if (fconfig.style == kWinStyleMinimal) {
win_set_minimal_style(wp);
didset_window_options(wp, true);
}
rv = wp->handle;
cleanup:
if (fconfig.noautocmd) {
unblock_autocmds();
}
return rv;
#undef HAS_KEY_X
}
从源码中不难看出,这个 noautocmd 选项只是在 nvim_open_win 这个函数调用内起作用,在最后的时候使用 unblock_autocmds 又恢复的事件的响应。