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
在 Neovim 或者 Vim 中,QuickFix 窗口内的内容默认的格式是:
<filename>|<lnum> col <col>|<text>
最近有需求修改 QuickFix 窗口内容的格式,使其更加美观一些,修改前,QuickFix 窗口打开后内容如下:
而修改后最终展示的效果如下:
下面展示了两种修改方法,一个是在老旧的 Vim 内实现的方法,另外一个是借助 Vim 8.2 的 qftf
选项实现的方式。
早在 Vim 7.4 之前,可以使用 autocmd 来手动修改,其实现的逻辑是监控 BufReadPost
事件
function! QuickFixFormat()
let qflist = map(getqflist(),
\ 'extend(v:val, {"filename" : bufname(v:val.bufnr)})')
let prefix_len = 2 + max(map(copy(qflist),
\ 'strchars(v:val.filename . v:val.lnum)'))
let fmt = '%-' . prefix_len . 's' . '%s'
setlocal modifiable
call setline('1', map(qflist,
\ 'printf(fmt, v:val.filename . ":" . v:val.lnum, "| " . v:val.text)'))
setlocal nomodifiable nomodified
endfunction
augroup QuickFixFormat
autocmd!
autocmd BufReadPost quickfix call QuickFixFormat()
augroup END
上述代码中,还可以把分隔符 |
修改成占据整行的竖线 │
,以实现完整分割线效果。
完成窗口内容修改后,还存在一个问题,QuickFix 窗口里内容的高亮是通过 Vim 默认的 syntax/qf.vim
文件实现的,在这里需要覆盖默认的 qf
FileType 的语法高亮:
Vim 可以新建 ~/.vim/syntax/qf.vim
文件,Neovim 是 ~/.config/nvim/syntax/qf.vim
。
if exists('b:current_syntax')
finish
endif
syn match qfFileName "^[^│]*" contains=qfLineNr
syn match qfSeparator "│"
syn match qfLineNr ":\d*" contained
" The default highlighting.
hi def link qfFileName Directory
hi def link qfLineNr LineNr
hi def link qfSeparator VertSplit
let b:current_syntax = 'qf'
quickfixtextfunc
上面的方法是强制修改 QuickFix Buffer 内容实现的格式化,Vim 8.2 提供了 quickfixtextfunc
选项,
可以通过设定这一选项来实现格式化 QuickFix 窗口,修改第一段代码为:
function! QuickFixFormat(info)
let qflist = getqflist({'id' : a:info.id, 'items' : 1}).items
let qflist = map(qflist,
\ 'extend(v:val, {"filename" : bufname(v:val.bufnr)})')
let prefix_len = 2 + max(map(copy(qflist),
\ 'strchars(v:val.filename . v:val.lnum)'))
let fmt = '%-' . prefix_len . 's' . '%s'
return map(qflist,
\ 'printf(fmt, v:val.filename . ":" . v:val.lnum, "│ " . v:val.text)')
endfunction
set quickfixtextfunc=QuickFixFormat
可以通过 if exists('&quickfixtextfunc')
来检测当前 Vim 或者 Neovim 是否支持 quickfixtextfunc
选项。
结束了一天的工作,晚饭后去酒店旁边的电影院看了《误杀3》。
看完重生,再来开这个确实觉得剧情太容易猜到剧情的发展了,看就没太大悬念了。不同于重生里面以毒品为话题,这部电影是以拐卖儿童这一敏感话题展开的,先不谈拍的好坏。起码让这个话题被更多的人关注到加以防范也算是起到一丁点的好处,让社会跟相关部门更多地关注到这个问题那就更好了。 回过头来再想想整部电影的剧情,感觉其中还是处处提供了线索,看的过程中还是能感觉出来一些人物的心理活动的。
郑炳睿
一开始确实没想到他原来居然是人贩子,求生其实是生命的本能。人类虽然是高等动物,通过思想可以客服本能,但是还是会有人没办法克服,会依靠本能去做一些决定。 为了能活下去而参与贩卖人口等等,后来还是为了能活下去选择按下女儿那边炸弹的遥控器。虽然几个镜头也极力体现他对女儿的疼爱,比如看到女儿被伤害,哭的眼泪口水哒哒的。 关于这个角色心理的选择,我觉得有一个大的转变,从一开始怕死,而选择按下女儿那边炸弹遥控器,到后来奋不顾身地为女儿挡枪。
但是上面图片中的介绍真是扯淡, 郑炳睿出手爆杀人贩???他才是人贩子。图片剧情简介也完全是跟剧情实际情况截然相反。
李慧萍
电影开头第一眼,这女孩丢失八成跟她有关系,肯定是人贩子。后来证实下来确实是她参与的,但是没想到的是她原来也是是为了复仇。前后出镜截然两种不同风格人物,演员在演演员。
张景贤
误解了,一开始福利院收娃的长头发我以为是他。加上半夜出来杀郑炳睿,我还以为他是达蒙的人。后来看到原来他的妻子是梁素娥,才知道他也是要复仇的。
雅英
我脸盲,看了好几次才把她跟丢失娃的母亲对上号。
吴达毅
最后杀他的是谁?
施福安和Tinaya
没啥感触,死得其所。
阿卢
第一个镜头,就看到仇恨的眼光,审判室里的大笑也没看懂,直到知道他也是丢失儿童的家属,一切才显得那么理所当然。
达蒙
最后啥他的人是谁?
门多萨
一开始以为他是一无是处的领导。
梁素娥
最有感触的还是梁素娥的剧情,感觉词穷了,不知道该怎么写。第一次出镜的时候,感觉挺好看,一张混血儿脸,心善,“我也是孩子母亲,我一定会找到真相”,台词可能我记错了,但是我想她所想表达的意思以及她所坚持的事情一定是这样的,也为此付出了生命,也包括还没出生的孩子。
阿佐
开第一枪的男警察?
阿婕
不记得谁了,女警察?
郑婷婷
最后开枪的是谁?
Neovim 的 quickfix 窗口还是比较常用的功能,在这里,我实现了一些 quickfix 窗口内常用的快捷键。
在 quickfix 窗口内,默认情况下 Normal 模式下的 dd
和 Visual 模式下的 d
快捷键是不可用的,会提示:
E21: Cannot make changes, 'modifiable' is off
但是有时候,会感觉 quickfix 展示的结果太多了,有些内容不是我所需要的。使用如下代码实现了 Normal 模式下 dd
删除光标下的结果,Visual 模式下 d
删除选中行的结果。
local group = vim.api.nvim_create_augroup('quickfix_mapping', {
clear = true,
})
vim.api.nvim_create_autocmd({ 'FileType' }, {
pattern = 'qf',
group = group,
callback = function(ev)
vim.keymap.set('n', 'dd', function()
local qflist = vim.fn.getqflist()
local line = vim.fn.line('.')
table.remove(qflist, line)
vim.fn.setqflist(qflist)
vim.cmd(tostring(line))
end, { buffer = ev.buf })
end,
})
local function table_remove(t, from, to)
local rst = {}
for i = 1, #t, 1 do
if i < from or i > to then
rst[#rst + 1] = t[i]
-- else
-- i = to + 1
end
end
return rst
end
vim.api.nvim_create_autocmd({ 'FileType' }, {
pattern = 'qf',
group = group,
callback = function(ev)
vim.keymap.set('v', 'd', function()
local esc = vim.api.nvim_replace_termcodes('<esc>', true, false, true)
vim.api.nvim_feedkeys(esc, 'x', false)
local qflist = vim.fn.getqflist()
local from = vim.fn.getpos("'<")[2]
local to = vim.fn.getpos("'>")[2]
if from > to then
from, to = to, from
end
qflist = table_remove(qflist, from, to)
vim.fn.setqflist(qflist)
vim.cmd(tostring(from))
end, { buffer = ev.buf })
end,
})
这里主要是为了实现一个功能,移除文件路径匹配输入表达式的结果。
local group = vim.api.nvim_create_augroup('quickfix_mapping', {
clear = true,
})
vim.api.nvim_create_autocmd({ 'FileType' }, {
pattern = 'qf',
group = group,
callback = function(ev)
vim.keymap.set('n', 'c', function()
local input_pattern = vim.fn.input('filter pattern:')
-- vim.cmd('noautocmd normal! :')
local re = vim.regex(input_pattern)
local qf = {}
for _, item in ipairs(vim.fn.getqflist()) do
if not re:match_str(vim.fn.bufname(item.bufnr)) then
table.insert(qf, item)
end
end
vim.fn.setqflist(qf)
end, { buffer = ev.buf })
end,
})
上述代码,设定的快捷键为 c
, 按下快捷键后会弹出一个出入提示,输入一段字符串,可以是 Vim 正则,回车后会移除 quickfix 窗口中文件名称匹配字符串的结果。
如果需要保留文件名匹配字符串的结果,可以增加一个新的快捷键,比如 C
,代码如下:
local group = vim.api.nvim_create_augroup('quickfix_mapping', {
clear = true,
})
vim.api.nvim_create_autocmd({ 'FileType' }, {
pattern = 'qf',
group = group,
callback = function(ev)
vim.keymap.set('n', 'C', function()
local input_pattern = vim.fn.input('filter pattern:')
-- vim.cmd('noautocmd normal! :')
local re = vim.regex(input_pattern)
local qf = {}
for _, item in ipairs(vim.fn.getqflist()) do
if re:match_str(vim.fn.bufname(item.bufnr)) then
table.insert(qf, item)
end
end
vim.fn.setqflist(qf)
end, { buffer = ev.buf })
end,
})
上述功能汇总为插件 quickfix.nvim.
2018 年 8 月 28 日,我在 CSDN 博客上面发表过一篇文章:《Vim 中使用正则表达式》。 随着 CSDN 账号的注销,这篇文章已经无法在更新,于是将文章迁移至个人博客,并加以更新,增加 Neovim 相关内容。
经常在网上看到有人抱怨 Vim 的正则表达式太奇怪,无法接受。我倒是觉得 Vim 的正则表达式比较容易理解。 可能是因为我最早接触的正则表达式就是 Vim 的正则表达式吧,正好借此机会整理下 Vim 的正则表达式相关的内容。
首先,在哪些情况下会用到正则表达式?
使用正则表达式的命令最常见的就是 /
和 ?
命令。其格式如下:
/正则表达式
?正则表达式
另一个很有用的命令就是 :s[ubstitute]
(替换)命令,将第一个//
之间的正则表达式替换成第二个//
之间的字符串。
:s/正则表达式/替换字符串/选项
在学习正则表达式时可以利用 /
命令来练习。
元字符 | 说明 |
---|---|
. |
匹配任意字符 |
[abc] |
匹配方括号中的任意一个字符。可以使用 - 表示字符范围,如[a-z0-9] 匹 配小写字母和阿拉伯数字。 |
\d |
匹配阿拉伯数字,等同于[0-9] 。 |
[^abc] |
在方括号内开头使用^符号,表示匹配除方括号中字符之外的任意字符。 |
\d |
匹配阿拉伯数字,等同于[0-9]。 |
\D |
匹配阿拉伯数字之外的任意字符,等同于[^0-9]。 |
\x |
匹配十六进制数字,等同于[0-9A-Fa-f]。 |
\X |
匹配十六进制数字之外的任意字符,等同于[^0-9A-Fa-f]。 |
\w |
匹配单词字母,等同于[0-9A-Za-z_]。 |
\W |
匹配单词字母之外的任意字符,等同于[^0-9A-Za-z_]。 |
\t |
匹配字符。 |
\s |
匹配空白字符,等同于[ \t]。 |
\S |
匹配非空白字符,等同于[^ \t]。 |
如果需要查找一些特殊字符,如 *
、.
、/
等,可以在这些字符前面添加 \
,表示这些不是元字符,而是普通字符。比如:\/d
匹配的是 /d
这两个字符,而不是匹配任意数字。
表示数量的元字符
元字符 | 说明 |
---|---|
* |
匹配0-任意个 |
\+ |
匹配1-任意个 |
\? |
匹配0-1个 |
\{n,m} |
匹配n-m个 |
\{n} |
匹配n个 |
\{n,} |
匹配n-任意个 |
\{,m} |
匹配0-m个 |
表示位置的符号
元字符 | 说明 |
---|---|
$ |
匹配行尾 |
^ |
匹配行首 |
\< |
匹配单词词首 |
\> |
匹配单词词尾 |
使用示例
命令 | 描述 |
---|---|
/char\s\+[A-Za-z_]\w*; |
查找所有以char开头,之后是一个以上的空白,最后是一个标识符和分号 |
/\d\d:\d\d:\d\d |
查找如 17:37:01 格式的时间字符 |
:g/^\s*$/d |
删除只有空白的行 |
:s/\<four\>/4/g |
将所有的 four 替换成 4,但是 fourteen 中的 four 不替换 |
在正规表达式中使用 \(
和 \)
符号括起正规表达式,即可在后面使用\1
、\2
等变量来访问 \(
和 \)
中的内容。
使用示例
命令 | 描述 |
---|---|
/\(a\+\)[^a]\+\1 |
查找开头和结尾处a的个数相同的字符串,如 aabbbaa,aaacccaaa,但是不匹配 abbbaa |
:s/\(http:\/\/[-a-z\._~\+%\/]\+\)/<a href="\1">\1<\/a>/ |
将 url 替换为http://url的格式 |
:s/\(\w\+\)\s\+\(\w\+\)/\2\t\1 |
将 data1 data2 修改为 data2 data1 |
在替换命令 :s/{pattern}/{string}/[flags]
中可以使用函数表达式来书写替换内容,格式为
:s/替换字符串/\=函数式
在函数式中可以使用 submatch(1)
、submatch(2)
等来引用 \1
、\2
等的内容,而submatch(0)
可以引用匹配的整个内容。
使用例
:%s/\<id\>/\=line(".")
将各行的 id 字符串替换为行号
:%s/^\<\w\+\>/\=(line(".")-10) .".". submatch(1)
将每行开头的单词替换为 (行号-10).单词 的格式,如第11行的 word 替换成 1. word
Vim语法 | Perl语法 | 含义 |
---|---|---|
\+ |
+ |
1-任意个 |
\? |
? |
0-1个 |
\{n,m} |
{n,m} |
n-m个 |
\( 和 \) |
( 和 ) |
分组 |
在 Vim 里,默认是贪婪模式,即 a.*b
会尽可能多滴匹配字符,在 ahdbjkbkls
中匹配 ahdbjkb
而不是 ahdb
。
如果是非贪婪的,可以使用 \{-}
代替 *
,即 a.\{-}b
匹配 ahdb
而不是 ahdbjkb
。
Neovim 目前主要的配置语言是 Lua,并且 Lua 并没有 VimL 的 =~#
和 =~
比较符。但是 Neovim 提供了一个 vim.regex
Lua 模块。
vim.regex
模块常见的使用方式:
if vim.regex('http[s]*://'):match_str(str) then
-- do something
end
熟悉 Vim 或者 Neovim 的用户对于其状态栏(statusline)和标签栏(tabline)应该也不陌生,对于这两个 UI 组件上面的点击事件, 早期是只有 tabline 上面支持点击操控 Vim 的标签页。
很早以前,就有用户想在 vim-airline 插件的标签栏实现 Buffer 切换的功能, 但是似乎直到现在,Vim 都未增加该功能。
2016 年,Neovim 增加了 tablineat 特性,使得用户可以在自定义状态栏和标签栏时,
指定某个区域设定对应的点击事件回调函数,以此实现监控状态栏和标签栏的点击事件。可以使用 has('tablineat')
检测当前 Neovim 是否支持该特性。
以下内容摘自 :h statusline
@ N Start of execute function label. Use %X or %T to end the label,
e.g.: %10@[email protected]%X. Clicking this label runs the
specified function: in the example when clicking once using left
mouse button on "foo.c", a `SwitchBuffer(10, 1, 'l', ' ')`
expression will be run. The specified function receives the
following arguments in order:
1. minwid field value or zero if no N was specified
2. number of mouse clicks to detect multiple clicks
3. mouse button used: "l", "r" or "m" for left, right or middle
button respectively; one should not rely on third argument
being only "l", "r" or "m": any other non-empty string value
that contains only ASCII lower case letters may be expected
for other mouse buttons
4. modifiers pressed: string which contains "s" if shift
modifier was pressed, "c" for control, "a" for alt and "m"
for meta; currently if modifier is not pressed string
contains space instead, but one should not rely on presence
of spaces or specific order of modifiers: use |stridx()| to
test whether some modifier is present; string is guaranteed
to contain only ASCII letters and spaces, one letter per
modifier; "?" modifier may also be present, but its presence
is a bug that denotes that new mouse button recognition was
added without modifying code that reacts on mouse clicks on
this label.
Use |getmousepos()|.winid in the specified function to get the
corresponding window id of the clicked item.
按照前面的帮助文档描述的内容,可以使用格式 %N@function_name@text%X
来指定区域监控点击事件,
回调函数的名称为两个 @
标记之间的字符串,%
与 @
之间的数字作为第一个参数。
写一个测试函数,看一看点击事件的具体执行效果:
function! OnClick(a, b, c, d) abort
echom a:a
echom a:b
echom a:c
echom '>' . a:d . '<'
endfunction
set tabline=xxxxxxx%1@OnClick@11111%X%2@OnClick@22222%X
将以上文件保存为 vim 文件并执行 :so %
后,状态栏变成了
xxxxxxx1111122222
, 鼠标左键单击 11111
区域,看到如下输出。
1
1
l
> <
上述四行分别对应函数的四个参数:
%
与 @
之间的数字l
)、右键(r
)或者中键(m
)c
)、Shift 键(s
)、Alt 键(a
)、Meta 键(m
)shift/alt/ctrl
。在使用前面的测试脚本测试的过程中,发现第二个参数返回的是点击的次数,其结果不是完全可预期的。为了更加方便测试,将测试的脚本修改如下:
function! OnClick(a, b, c, d) abort
echom '点击次数:' .. a:b
endfunction
set tabline=xxxxxxx%1@OnClick@11111%X%2@OnClick@22222%X
多次点击后,其结果如下:
点击次数:1
点击次数:2
点击次数:3
点击次数:4
点击次数:1
点击次数:2
点击次数:3
点击次数:4
点击次数:1
点击次数:2
点击次数:1
点击次数:2
点击次数:3
点击次数:4
点击次数:1
点击次数:2
上面的输出可以看到,当连续点击时,回调函数会在每次点击时被调用。并且点击次数会增加,
当间隔时间超过 mousetime
时,点击次数计数归零,
间隔时间可以查阅 :h mousetime
, 默认为 500 毫秒。