这篇文章给大家介绍一下我开发的新的 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}
无意间阅读到一篇文章 《震惊!竟然有人在 GitHub 上冒充我的身份!》, 大致看了下 Github 网站对于 Commit 的归属的判断规则。
只需要 Commit 的邮箱在某人的账号设置的邮箱列表内。那么就会将此次提交显示为是某人的归属。这就意味着,任何一个人都可以使用 git config user.email "YOUR_EMAIL"
这一命令设置成别人的邮箱,伪装成他人进行提交。
如果只需要设置邮箱就可以伪装成用户进行提交,那不是乱套了?因此大多数软件维护者都会要求 Commit 签名。Github 其实支持多种方式签名,这里大致记录一下自己设置 GPG 签名的方案。
Windows 系统下,我使用的 scoop 包管理器,通过 scoop install gpg
就可以安装好了。 使用 gpg --version
看一下版本信息。
D:\wsdjeg>gpg --version
gpg (GnuPG) 2.4.7
libgcrypt 1.11.0
Copyright (C) 2024 g10 Code GmbH
License GNU GPL-3.0-or-later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Home: D:\Scoop\apps\gpg\current\home
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2
参考 Github 的文档生成并上传 GPG 密钥, 上传完成后使用 https:/github.com/{username}.gpg
格式链接就可以访问你的公钥。
例如我的:wsdjeg.gpg
使用 where gpg
查看 gpg 命令的绝对路径:
d:\wsdjeg>where gpg
D:\Scoop\apps\gpg\current\bin\gpg.exe
清除之前的设置:
git config --global --unset gpg.format
之前导入已备份的私钥后,使用 git commit -S
一直提示没有私钥,找了很久原因才知道,原来需要设置 Git 的签名方式及程序,可能是原来设置过。
git config --global gpg.program "D:\Scoop\apps\gpg\current\bin\gpg.exe"
使用 gpg --list-secret-keys --keyid-format=long
列出 GPG 密钥:
d:\wsdjeg\my-blog>gpg --list-secret-keys --keyid-format=long
D:\Scoop\apps\gpg\current\home\pubring.kbx
------------------------------------------
sec rsa2048/41BB7053E835C848 2021-09-21 [SC]
9C957B574786F570AC69625041BB7053E835C848
uid [ unknown] Shidong Wang (Shidong's GPG key) <[email protected]>
ssb rsa2048/D3E3902EF4E8074C 2021-09-21 [E]
设置 Git 签名的 ID:
git config --global user.signingkey 41BB7053E835C848
设置 Git 默认启用 Commit 签名:
git config --global commit.gpgsign true
当初,Neovim 刚刚推出异步 job 特性的时候,我就使用 Vim Script 写过一个实时检索的插件 FlyGrep.vim。
最开始的实现是使用 :split
命令分屏展示搜索结果,使用 :echo
命令配合 while true getchar()
在 cmdline 内模拟输入框。
但是 :split
命令分屏时,总是带动整个界面其他窗口内容的移动。随着 Neovim 增加悬浮窗口这一特性,
我把 Flygrep 搜索结果窗口及底部提示状态都使用浮窗来实现,这就不会受到原先窗口界面布局的影响了。但是目前 Flygrep 的浮窗还是底部半屏窗口。
现在大多数浮窗插件都是在屏幕中间打开窗口,下面就从零开始一步一步实现一个简单的实时代码检索插件。
最终效果图如下:
首先时窗口界面,整体界面占据窗口中间 80% 区域,分上下两部分,下面窗口仅有一行,作为一个输入窗口,上面窗口作为搜索结果展示窗口。
-- 窗口位置
-- 宽度: columns 的 80%
local screen_width = math.floor(vim.o.columns * 0.8)
-- 起始位位置: lines * 10%, columns * 10%
local start_col = math.floor(vim.o.columns * 0.1)
local start_row = math.floor(vim.o.lines * 0.1)
-- 整体高度:lines 的 80%
local screen_height = math.floor(vim.o.lines * 0.8)
local prompt_bufid = vim.api.nvim_create_buf(false, true)
local prompt_winid = vim.api.nvim_open_win(prompt_bufid, true, {
relative = 'editor',
width = screen_width,
height = 1,
col = start_col,
row = start_row + screen_height - 3,
focusable = true,
border = 'rounded',
title = 'Input',
title_pos = 'center',
-- noautocmd = true,
})
local result_bufid = vim.api.nvim_create_buf(false, true)
local result_winid = vim.api.nvim_open_win(result_bufid, false, {
relative = 'editor',
width = screen_width,
height = screen_height - 5,
col = start_col,
row = start_row,
focusable = false,
border = 'rounded',
title = 'Result',
title_pos = 'center',
-- noautocmd = true,
})
在底部窗口输入内容时,后台自动执行搜索命令,并在搜索结果窗口实时展示。这里需要监控 TextChangeI
这一事件,在事件 callback 函数内调用搜索命令。
local job = require('spacevim.api.job')
local augroup = vim.api.nvim_create_augroup('floatgrep', {
clear = true,
})
vim.api.nvim_create_autocmd({ 'TextChangedI' }, {
group = augroup,
buffer = prompt_bufid,
callback = function(ev)
local text = vim.api.nvim_buf_get_lines(prompt_bufid, 0, 1, false)[1]
if text ~= '' then
local grep_cmd = {
'rg',
'--no-heading',
'--color=never',
'--with-filename',
'--line-number',
'--column',
'-g',
'!.git',
'-e',
text,
'.',
}
job.start(grep_cmd, {
on_stdout = function(id, data)
if vim.fn.getbufline(result_bufid, 1)[1] == '' then
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, data)
else
vim.api.nvim_buf_set_lines(result_bufid, -1, -1, false, data)
end
end,
})
else
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, {})
end
end,
})
上述代码中,我使用了 spacevim job API,其实,我也考虑过使用 vim.system()
函数,但是异步搜索完全不调用,
可能是写法有误, 使用 vim.system()
写法如下(无效):
vim.api.nvim_create_autocmd({ 'TextChangedI' }, {
group = augroup,
buffer = prompt_bufid,
callback = function(ev)
local text = vim.api.nvim_buf_get_lines(prompt_bufid, 0, 1, false)[1]
if text ~= '' then
local grep_cmd = {
'rg',
'--no-heading',
'--color=never',
'--with-filename',
'--line-number',
'--column',
'-g',
'!.git',
'-e',
text,
'.',
}
vim.system(grep_cmd, {
stdout = function(err, data)
vim.api.nvim_buf_set_lines(result_bufid, 0, -1, false, vim.split(data, '\n'))
end,
})
end
end,
})
因为输入框只有一行,因此避免回车键换行,同时增加在搜索结果窗口内上下移动两个快捷键。
-- 使用 Esc 关闭整个界面
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 })
-- 搜索结果行转换成文件名、光标位置
local function get_file_pos(line)
local filename = vim.fn.fnameescape(vim.fn.split(line, [[:\d\+:]])[1])
local linenr =
vim.fn.str2nr(string.sub(vim.fn.matchstr(line, [[:\d\+:]]), 2, -2))
local colum = vim.fn.str2nr(
string.sub(vim.fn.matchstr(line, [[\(:\d\+\)\@<=:\d\+:]]), 2, -2)
)
return filename, linenr, colum
end
-- 使用回车键打开光标所在的搜索结果,同时关闭界面
vim.keymap.set('i', '<Enter>', function()
vim.cmd('noautocmd stopinsert')
-- 获取搜索结果光表行
local line_number = vim.api.nvim_win_get_cursor(result_winid)[1]
local filename, linenr, colum = get_file_pos(
vim.api.nvim_buf_get_lines(
result_bufid,
line_number - 1,
line_number,
false
)[1]
)
vim.api.nvim_win_close(prompt_winid, true)
vim.api.nvim_win_close(result_winid, true)
vim.cmd('edit ' .. filename)
vim.api.nvim_win_set_cursor(0, { linenr, colum })
end, { buffer = prompt_bufid })
-- 使用 Tab/Shift-Tab 上下移动搜素结果
vim.keymap.set('i', '<Tab>', function()
local line_number = vim.api.nvim_win_get_cursor(result_winid)[1]
vim.api.nvim_win_set_cursor(result_winid, { line_number + 1, 0 })
end, { buffer = prompt_bufid })
vim.keymap.set('i', '<S-Tab>', function()
local line_number = vim.api.nvim_win_get_cursor(result_winid)[1]
vim.api.nvim_win_set_cursor(result_winid, { line_number - 1, 0 })
end, { buffer = prompt_bufid })
-- 高亮文件名及位置
vim.fn.matchadd(
'Comment',
[[\([A-Z]:\)\?[^:]*:\d\+:\(\d\+:\)\?]],
10,
-1,
{ window = result_winid }
)
可以使用 extmarks 美化输入框,添加一个 >
符号,禁用行号。
vim.api.nvim_set_option_value('number', false, { win = prompt_winid })
vim.api.nvim_set_option_value('relativenumber', false, { win = prompt_winid })
local extns = vim.api.nvim_create_namespace('floatgrep_ext')
vim.api.nvim_buf_set_extmark(prompt_bufid, extns, 0, 0, {
sign_text = '>',
sign_hl_group = 'Error',
})
以上代码仅供参考,实际上还有很多细节并未完全考虑到, 比如 callback 函数内对于未执行完成的 job 的处理、 比如增加输入与搜索之间的延迟减少快速输入过程中不必要的额外执行搜索命令。
完整的代码可以看:simple_float_grep
Neovim 一直在迭代更新,本文以 Neovim v0.10.0 版本为基础进行功能演示。
按照 :h extmark
描述 Extended marks (extmarks) 是跟踪缓冲区文本变化的的特定位置的注释。位置从 0 开始,位于第一个字符的前方。
f o o b a r line contents
0 1 2 3 4 5 character positions (0-based)
0 1 2 3 4 5 6 extmark positions (0-based)
与 Neovim Extended marks 相关的函数有两个:nvim_buf_get_extmark
和 nvim_buf_set_extmark
。
nvim_buf_set_extmark
nvim_buf_set_extmark
的函数签名是:
nvim_buf_set_extmarks({buffer}, {ns_id}, {start}, {col}, {opts})
可以看到函数调用的参数包括 buffer
、ns_id
、start
、col
、opts
。
buffer
指的是设置 extmarks 对应的缓冲区 ID,ns_id
全称是 name space id, 可以由 nvim_create_namespace
新建。
其中 opts
是一个 Lua table,比如:
vim.api.nvim_buf_set_extmark(
0,
vim.api.nvim_create_namespace('test_extmark'),
28,
3,
{ end_row = 28, end_col = 5, hl_group = 'TODO' }
)
opts
支持的 Key 值及其含义包括:
id
如果是新建一个 extmark 可以缺省,如果是修改某个已存在的 extmark,需要指定 id.
end_row
和 end_col
指定 extmark 结束的行和列,这里的列指的是行首、行尾或者字符之间,都是从 0 开始。
hi_group
hl_eol
这是一个布尔值,控制 extmark 覆盖的行尾无字符区域是否需要高亮。
virt_text
这是一个列表,其中每一个元素结构是 [text, highlight]
。默认是添加在 extmark 开始的行行尾。
示例代码:
vim.api.nvim_buf_set_extmark(4, vim.api.nvim_create_namespace("test_extmark"), 47, 3, {
end_row = 47,
end_col = 5,
hl_group = "TODO",
virt_text = { { "This is ", "Comment" }, { "hello", "Number" }, { " extmarks", "TODO" } },
})
virt_text_pos
设置虚拟文本的位置,可以设定的值为:
eol
:行尾最后一个字符右边overlay
: 在 extmark 起始位置显示虚拟文本,覆盖的字符不右移right_align
: 在窗口最右侧显示inline
: 在 extmark 起始位置显示虚拟文本,覆盖的字符右移virt_text_win_col
将 virtual text 展示在 fixed screen line,屏幕中可见的第一行为 0,往下依次加1。
virt_text_hide
hl_mode
设置高亮颜色的模式:
replace
: 默认,只显示 virtual text 颜色combine
: combine with background text colorblend
: blend with background text colorvirt_lins
设置虚拟多行文本,每一行结构是多个 [text, highlight]
组成的列表。
virt_lines_above
当设置虚拟多行文本时,默认是在 extmark 起始行下方,该选项设为 true
,可以在上方显示。
virt_lines_leftcol
虚拟多行文本左对齐,绕过 sign 和 行号列
right_gravity
和 end_right_gravity
控制在 extmark 左右侧末端添加字符时 extmark 的位置扩展行为
winbar
Neovim 0.8.0 就增加了 'winbar'
这一选项。winbar 实际上类似于状态栏(statusline),只不过 winbar 是显示在每一个窗口的顶部。
它的设置格式与状态栏也完全一致。
这里做了一个简单的示例效果图:
" 使用全局状态栏,状态栏只在底部显示,水平分割窗口将不再显示状态栏。
set laststatus=3
" 隐藏顶部标签栏
set showtabline=0
前面也提到了 Neovim 实际上早就增加了 winbar,我是因为习惯了现有的状态栏和标签栏,不太像改变操作习惯。 最近正好想尝试使用一下 winbar 特性,因为之前写过自定义的标签栏、状态栏,因此配置起来还算顺手。
详细代码可以看我的 Github 仓库:wsdjeg/winbar.nvim
实现的逻辑也比较简单,监控指定的 Neovim 事件,在 callback 函数内调用 redraw_winbar 函数。
local augroup = vim.api.nvim_create_augroup('winbar.nvim', {
clear = true,
})
vim.api.nvim_create_autocmd({ 'BufWinEnter' }, {
group = augroup,
pattern = '*',
callback = function(e)
redraw_winbar()
end,
})
我看过 fgheng/winbar.nvim
插件的代码,最后一次更新时间是 2022 年七月,在他的插件里 setup 函数直接这样写:
function M.setup(opts)
config.set_options(opts)
local winbar = require('winbar.winbar')
winbar.init()
if opts.enabled == true then
vim.api.nvim_create_autocmd({ 'DirChanged', 'CursorMoved', 'BufWinEnter', 'BufFilePost', 'InsertEnter', 'BufWritePost' }, {
callback = function()
winbar.show_winbar()
end
})
end
end
这会存在一个非常严重问题,就是如果使用者不小心调用了两次甚至多次 setup 函数,那么就会发现,实际上定义了多个重复的 autocmd。 因此不管是使用 Lua 来创建 autocmd 还是早期 Vim 下使用 Vim Script 创建 autocmd,都建议使用 augroup。
redraw_winbar
函数实现简单到极致的实现:
local function redraw_winbar()
local file_name = vim.fn.expand('%:t')
if file_name == '' then
return
end
local value = '%#SpaceVim_winbar#' .. file_name .. ' %#SpaceVim_winbar_Normal#' .. default_conf.winbar_seperator .. '%#Normal#'
vim.api.nvim_set_option_value('winbar', value, { scope = 'local' })
end
这里面其实有很多功能可以去做的,比如:
local file_name = vim.fn.expand('%:t')
local ft = vim.filetype.match({ filename = file_name })
for _, v in ipairs(excluded_filetypes) do
if v == ft then
return
end
end
local excluded_regex = [[\.txt$]]
local re = vim.regex(excluded_regex)
local file_name = vim.fn.expand('%:t')
for _, v in ipairs(excluded_filetypes) do
if re:match(file_name) then
return
end
end