在 Vim 下快速编辑 Go struct 标签
涛叔我之前写过一篇文章如何配置 Vim 的 Golang 开发环境。Go 语言跳转补全用了 NeoVim 内置的 lsp 功能,也就不需要集成额外的插件。但是 lsp 好像不支持给 struct 添加或者修改 tag 的功能。组里的同事就开始折腾,他们找到了一个 lua 实现的小插件,还介绍给我。我想了一下,这个功能并不复杂,于是在该同学的电脑上现场调试,最终写了一个仅有十三行的 VimL 函数,完美解决这个问题。今天整理出来分享给大家。
首先,我们需要安装 gomodifytags 这个工具:
go install github.com/fatih/gomodifytags
这是一个专门用来修改 go struct tag 的工具,支持添加、删除等操作。使用方法也很简单:
# 添加 tag
gomodifytags -add-tags json -file 文件路径 -range 10,20 -w
# 删除 tag
gomodifytags -remove-tags json -file 文件路径 -range 10,20 -w
gomodifytags 本身有很多参数,大家查阅它的帮助文档gomodifytags -h
。我们主要用到四个参数:
- -add-tags/-remove-tags 指定 tag 名称,比如 json 等
- -file 指定源文件路径
- -range 指定 struct 开始和结束的行号
- -w 表示将修改后的内容保存到原文件(默认只输出到 stdout)
我们要实现的效果也很简单:
- 如果光标移动到某 struct 内部,运行
:GoAddTags json
会给所有字段添加 json tag - 如果先选中某几行,再运行
GoAddTags json
则只会给选中的字段添加 json tag
我们从上到下分析。首先,我们需要声明一条新指令,这要用到 vim 的 command 语法:
command GoAddTags call gomodifytags()
这样我们执行:GoAddTags
的时候就会调用gomodifytags()
函数。
我们希望 GoAddTags 可以通过参数指定具体的 tag 名称,实现:GoAddTags json
添加 json 标签的效果。所以,需要修改 command 语法声明:
command -nargs=* GoAddTags call gomodifytags()
-nargs=*
告诉command
可以接受多个命令参数。如何将参数传给后面函数呢?这就需要用到<f-args>
这一特殊语法。VimScript 里的特殊语法非常多。所以 command 语句需要改成:
command -nargs=* GoAddTags call gomodifytags(<f-args>)
现在我们写一个gomodifytags()
函数的骨架:
function gomodifytags(...)
echo a:000
endfunction
这里的三个点表示函数参数的个数不确定,可以传入任意数量的参数。a:000
表示函数调用的全部参数。这时候,我们执行:GoAddTags json 1
就会看到 vim 输出['json', '1']
,说明我们的传参生效了。
我们前面说过要支持只修改选中的字段。怎么实现呢?这又需要用到 Vim 的黑科技。如果我们选选中几行再按:GoAddTags
,Vim 实际会显示成:'<,'>GoAddTags
。前面的'<
和'>
是Vim的特殊语法,表示选中区域的开始与结束行号。那问题来了,我们在 gomodifytags 函数中要如何拿到这个行号呢?答案是<line1>, <line2>, <count>
,大家可以类比<f-args>
。我们需要把 command 语句改成:
command -nargs=* -range GoAddTags call lv#gomodifytags(<line1>, <line2>, <count>, '-add-tags', <f-args>)
再把 gomodifytags 函数改成:
function gomodifytags(line1, line2, count, ...)
echo a:000
endfunction
这里的 line1 和 line2 表示选中区域的第一行和最后一行。count 没弄明白是什么意思。但通过实践发现,如果没有选中区域,count 字段的值为 -1。知道这一点也就足够了。
以上就是命令映射部分,下面我们讲 gomodifytags 的具体实现。
我们的目的就是提取 gomodifytags 需要的参数:
- 文件路径可以通过
expand('%p')
获取 - 修改范围分两种情况
- 如果 count 为正数,则说明已经选好区域了,直接使用 line1 和 line2
- 否则,我们需要自己确定当前 struct 的范围
如何确定当前 struct 的范围呢?这需要用到 text-objects 的概念。Vim 支持快速选中或者修改成对等号中的内容。比如 Go 语言的 struct 使用大括号,我们可以按va{
就可以选中 struct 大括号内的全部内容(包括大括号本身)。Vim 会自动保存上一次选中区域的开始和结束位置,我们可以通过line("'<")
和line("'>")
读取。
但又有问题了,我们如何在 gomodifytags 执行va{
呢?这需要用到execute指令:
execute 'normal va{^['
这里的^[
需要在编辑的时候先按ctrl+v
再按Esc
输入,直接复制没有效果。
最后就是执行 gomodifytags 命令,这需要用到 system 这个函数。把命令拼接好传给它就行了。所以完整的函数代码如下:
function gomodifytags(s,e,c,cmd,tag,...)
let path = expand('%p')
if a:c < 0
execute 'normal va{^['
let range = line("'<").",".line("'>")
else
let range = a:s.",".a:e
end
call system('gomodifytags '.a:cmd.' '.a:tag.' -file '.path.' -line '.range.' -w '.join(a:000, ' '))
endfunction
这时候,执行:GoAddTags json
就会给当前 struct 所有字段添加 json tag。
如果你自己实践一下就会发现问题:Vim 并没有显示新添加的 tag。这是为什么呢?因为我们是使用 gomodifytags 这个外部命令直接修改了源码,Vim 并没有加载修改后的内容。那怎么让 Vim 自动加载呢?最简单的办法就是在函数的结尾执行e
命令,表示 edit。
如果你又试了一下,就会发现确实可以显示更新后的 tag 内容了😂但是,Vim 的屏幕会闪一下,而且内容的位置会发生跳动。怎么办呢?这就需要用到 Vim 的 view 保存和恢复功能。
核心逻辑是在调用命令前保存当前 view,执行之后再恢复。
let v = winsaveview()
e
call winrestview(v)
以上就是插件的全部代码。核心逻辑就是提取 gomodifytags 然后执行对应的命令,非常简单。所以说,大家不要觉得 Vim 插件有多难或者多高大上。每个人都可以开发自己的插件。工欲善其事,必先利其器。别人的再好也不如自己定制的趁手。