在 vim 下快速编辑 Go struct 标签

2021-11-17 ⏳3.5分钟(1.4千字)

我之前写过一篇文章如何配置 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。我们主要用到四个参数:

我们要实现的效果也很简单:

我们从上到下分析。首先,我们需要声明一条新指令,这要用到 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 需要的参数:

如何确定当前 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 插件有多难或者多高大上。每个人都可以开发自己的插件。工欲善其事,必先利其器。别人的再好也不如自己定制的趁手。