关于任务(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,
},
})
借助 rooter.nvim 插件,可以在切换项目时,读取 .clang
文件内容,并且更新 c 语言的 runner。具体代码实现:
local c_runner = {
exe = 'gcc',
targetopt = '-o',
usestdin = true,
opt = { '-std=c11', '-xc', '-' },
}
require('code-runner').setup({
runners = {
c = { c_runner, '#TEMP#' },
},
})
vim.keymap.set(
'n',
'<leader>lr',
'<cmd>lua require("code-runner").open()<cr>',
{ silent = true }
)
-- make sure rooter.nvim plugin is loaded before code-runner
local function update_clang_flag()
if vim.fn.filereadable('.clang') == 1 then
local flags = vim.fn.readfile('.clang')
local opt = { '-std=c11' }
for _, v in ipairs(flags) do
table.insert(opt, v)
end
table.insert(opt, '-xc')
table.insert(opt, '-')
c_runner.opt = opt
end
end
require('rooter').reg_callback(update_clang_flag)
这篇文章给大家介绍一下我开发的新的 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}
vim.g
访问全局变量的弊端如果看了 vim.g
的源码,
不难发现,实际上是通过 vim._getvar
和 vim._setvar
来分别映射 __index
和 __newindex
。
可以理解为,每一次访问 __index
时,调用一个 vim._getvar
获取 viml 变量,并转化为一个新的 Lua 变量。最简单的测试:
let g:wsd = { 'a' : 'a' }
print(vim.g.wsd)
print(vim.g.wsd)
-- 执行两次,打印的结果不一样
-- table: 0x0215fa040d28
-- table: 0x0215fb557f10
这就意味着,如果 VimL 字典变量发生了内部变化,之前转化的 Lua 变量并不会发生变化:
vim.cmd([[
let g:wsd = {'a' : 'a'}
]])
local a = vim.g.wsd
vim.cmd([[
let g:wsd.a = 'b'
]])
vim.print(a.a) -- 任然时 a
同样的,Lua 这边通过 vim.g
获取到的字典变量的变化,也只有在 __newindex
函数被执行时,才会同步到 VimL 的变量。
vim.g.wsd = { a = 'a' }
vim.cmd('echo g:wsd') -- {'a' : 'a'}
vim.g.wsd.a = 'b'
vim.cmd('echo g:wsd') -- 任然是 {'a' : 'a'}
无意间阅读到一篇文章 《震惊!竟然有人在 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