在使用 Neovim 打开某个文件时,我希望 Neovim 自动把当前目录切换到该文件所在的项目根目录。 其实,能实现这一功能的有不少的插件,我最早使用的是 vim-rooter,但是后来因为切换到了 Neovim, 因此使用 Lua 重写了该功能,这个功能早期是 SpaceVim 的内置的, 在 SpaceVim 项目停止维护后独立成单独的插件:rooter.nvim。
可以使用任意插件管理器进行安装,比如 nvim-plug
require('plug').add({
{
'wsdjeg/rooter.nvim',
config = function()
require('rooter').setup({})
end,
},
})
以下是默认的配置:
require('rooter').setup({
root_patterns = { '.git/' },
outermost = true,
enable_cache = true,
project_non_root = '', -- this can be '', 'home' or 'current'
enable_logger = true, -- enable runtime log via logger.nvim
})
project_non_root
: 配置打开非项目文件时的行为outermost
: 若设为 true
,那么通过 root_patterns
检索到的多个目录时,取最外层目录。rooter.nvim 自带 telescope.nvim 拓展,可以使用 :Telescope project
列出过往打开过的项目。
在插件运行过程中产生的日志信息,可以使用 logger.nvim 进行查看。 如果有这一需求,那么在安装 rooter.nvim 时,需要添加相应的依赖插件。
require('plug').add({
{
'wsdjeg/rooter.nvim',
config = function()
require('rooter').setup({})
end,
depends = {
{
'wsdjeg/logger.nvim',
},
},
},
})
通过 reg_callback
可以设置 callback 函数,该函数会在项目切换时被调用。
local function update_ctags_option()
local project_root = vim.fn.getcwd()
local dir = require('util').unify_path(require('tags').cache_dir)
.. require('util').path_to_fname(project_root)
table.insert(tags, dir .. '/tags')
vim.o.tags = table.concat(tags, ',')
end
require('rooter').reg_callback(update_gtags_option)
logger.nvim
提供了一个基础的日志框架,不同的插件可以共用一个日志系统。
和安装其他插件一样,可以使用nvim-plug安装:
require('plug').add({
'wsdjeg/logger.nvim',
config = function()
require('logger').setup({
-- the level only can be:
-- 0 : log debug, info, warn, error messages
-- 1 : log info, warn, error messages
-- 2 : log warn, error messages
-- 3 : log error messages
level = 0,
})
end,
})
比如新建了一个插件 fyz.nvim
,此时可以添加一个文件 lua/fyz/log.lua
:
local M = {}
local logger
function M.info(msg)
if not logger then
pcall(function()
logger = require('logger').derive('fyz')
logger.info('hello world')
end)
else
logger.info('hello world')
end
end
return M
在自己的插件中就可以使用:
local log = require('fyz.log')
log.info('this is log from fyz.nvim')
可以使用 logger.viewRuntimeLog()
查看所有的日志输出,其中就会有如下一行:
[ fyz ] [23:22:50:576] [ Info ] this is log from fyz.nvim
关于任务(Tasks)管理,实际上早在 2020 年的时候就已经给 SpaceVim 增加了 Tasks 支持,参考的是 Vscode Tasks Manager 的实现。 最早的版本使用 Vim Script 实现的,大约在 2023 年的时候增加了 Lua 实现版本, 不过这些都是在 SpaceVim 内置的插件。
现在,SpaceVim 已经不再维护,而这些常用的功能,我也会陆续剥离出来单独形成插件,这篇文章主要介绍 tasks.nvim
可以使用任意插件管理器进行安装,这里以 nvim-plug 为例:
require('plug').add({
{
'wsdjeg/tasks.nvim',
depends = {
{
'wsdjeg/code-runner.nvim',
},
},
config = function()
require('tasks').setup({
global_tasks = '~/.tasks.toml',
local_tasks = '.tasks.toml',
provider = { 'npm' },
})
end,
},
})
tasks.nvim
提供了三个常用命令:
:TasksEdit
:用于打开 tasks 配置文件,默认打开的是项目配置文件,加上感叹号(:TasksEdit!
)则打开全局配置文件。:TasksList
:使用分屏列出所有 tasks:TasksSelect
:选择某个 task 并执行当然,如果你也安装了 telescope.nvim
那么,可以使用 :Telescope tasks
模糊搜索可用的 tasks.
Vscode 有一个非常出名的插件,叫做 Code Runner,我曾经也给 SpaceVim 添加了这么一个功能。
现在将这一功能剥离出来形成一个单独独立的 neovim 插件:code-runner.nvim
可以使用任意插件管理器进行安装,这里以 nvim-plug 为例:
require('plug').add({
{
'wsdjeg/code-runner.nvim',
config = function()
require('code-runner').setup({
runners = {
lua = { exe = 'lua', opt = { '-' }, usestdin = true },
},
enter_win = false,
})
end,
},
})
这篇文章给大家介绍一下我开发的新的 Neovim 插件管理器 nvim-plug。
不管是 Neovim 还是 Vim,已经有太多太多的插件管理器了,
最早期时候刚接触 Vim 的时候,从刚开始编辑 vimrc 时候开始,
只是简单的 set rtp+=path/to/plugdir
,到后来接触到了 Bundle,后来这个插件改名为 Vundle,
仓库的 README 中文版还是我翻译的。
我的插件管理器使用经历: 直接设置rtp -> Bundle(Vundle) -> vim-plug -> neobundle.vim -> dein.vim
越往后,插件管理器增加的功能越多,但实际上本质并未改变, 我前面写过一篇插件管理器的运行机制《Neovim 和 Vim 插件管理器的实现逻辑》 。
我有很长很长一段时间在使用的 dein.vim,包括现在 Vim 下还是使用 dein,。 实际上 dein.vim 已经实现了我所需要的所有的插件管理器的核心逻辑功能,但是,目前我大部分情况使用的是 Neovim, 因为 dein 还是 vimscript 写的,因此速度上在插件很多的时候,还是有些慢的, 因此使用 Lua 来实现一个 Neovim 插件管理器:nvim-plug
目前已实现的功能包括:
config
、config_before
、config_after
三种函数,分别在不同时机执行。其实关于插件管理器的界面,因为历史原因,一直习惯了 Vundle 的界面模式,包括 Vim 下知名插件 vim-plug 也是使用这样类似的插件管理器界面。 而我之前使用的 dein.vim 没有提供默认的界面,为此我还写过一个插件dein-ui.vim。那么在设计 nvim-plug 这个插件管理器的时候, 自然而然就实现了一个类似于 Vundle 的可视化界面,但是考虑到有可能会有其他操作界面模式的需求,因此 nvim-plug 这个插件管理器的操作界面是设计成界面和逻辑分离的模式。
如果兴趣,也可以实现一个新的插件列表界面,可以通过如下模式进行修改:
--- your custom UI
local function on_ui_update(name, data)
-- logic
end
require('plug').setup({
bundle_dir = 'D:/bundle_dir',
max_processes = 5, -- max number of processes used for nvim-plug job
base_url = 'https://github.com',
ui = on_ui_update, -- default ui is notify, use `default` for split window UI
})
上述的 on_ui_update
函数会在插件下载、更新、build 等命令执行过程种被调用,函数调用时被传入两个参数:插件名称、界面更新数据 plugUiData。
The plugUiData is table with following keys:
plugUiData
是一个 Lua table,其键值如下:
键值 | 描述 |
---|---|
clone_done |
boolead, is true when clone successfully |
command |
string, clone, pull or build |
clone_process |
string, git clone progress, such as 16% (160/1000) |
clone_done |
boolean, git clone exit status |
building |
boolean |
build_done |
boolean |
pull_done |
boolean |
pull_process |
string |
Lua 在 Neovim 中已经被当作了一等公民,目前火热的 Neovim 插件、配置基本上都是使用 Lua 来开发的。而我也是从 2016 年就开始使用 Neovim 了。 这篇文章整理了一些从 VimScript 切换到 Lua 配置一些 Tips。
本文写作环境:
在前面我写过两篇文字,主要是来比较 Lua(luajit)与 VimScipt 以及 Vim9Script 的运行速度。
比较的结果显而易见,Luajit 的速度比起 VimScipt 以及 Vim9Script 快很多,而我的配置还是比较重的,因此选择 Lua 将会大大提升使用体验。
Lua 是一门语法非常简单的编程语言,可以查看《学习 Lua 脚本语言》。
Neovim 的初始化文件是 ~\AppData\Local\nvim\init.lua
,Linux 系统是 ~/.config/nvim/init.lua
。Neovim 启动时会自动读取并执行该文件内容。
当打开一个文件,Neovim 通常会自动识别文件类型并且自动设定 filetype
。比如,打开 Test.java
文件,此时 ftplugin/java.lua
就会被调用执行。
Neovim 提供了多种方式设置 Neovim Option。
使用 Neovim API:
Neovim 提供了设置 Option 的 API 函数:nvim_set_option_value({name}, {value}, {opts})
,opts
是一个 Lua table,支持的 key 包括:
grobal
或者 local
,类似于 :setglobal
和 :setlocal
使用 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}