I have written a previous article How to configure Vim’s Golang development environment. Go language jump completions use NeoVim’s built-in lsp functionality, so there is no need to integrate additional plugins. However, lsp does not seem to support adding or modifying tags to structs. My colleagues in the group started tinkering with it, and they found a small plugin developed in lua and introduced it to me. I thought about it, this function is not complicated, so I debugged it on site on the student’s computer, and finally wrote a function with only 13 lines, which solved the problem perfectly. Today, I’ll share it with you.

First, we need to install gomodifytags this tool:

1
go install github.com/fatih/gomodifytags

This is a special tool for modifying go struct tag, support for adding, deleting and other operations. It is also very simple to use.

1
2
3
4
# 添加 tag
gomodifytags -add-tags json -file 文件路径 -range 10,20 -w
# 删除 tag
gomodifytags -remove-tags json -file 文件路径 -range 10,20 -w

gomodifytags itself has a number of parameters, so check out its help file gomodifytags -h. We use four main parameters.

  • -add-tags/-remove-tags specifies the tag name, such as json, etc.
  • -file specifies the path to the source file
  • -range specifies the line number at the beginning and end of the struct.
  • -w means to save the modified content to the original file (by default, only output to stdout)

The effect we want to achieve is also very simple.

  • If the cursor moves inside a struct, running :GoAddTags json will add json tags to all fields
  • If you select a few lines first and then run :GoAddTags json, only the selected fields will have json tags added to them.

Let’s break it down from top to bottom. First, we need to declare a new command, which uses vim’s command syntax.

1
command GoAddTags call gomodifytags()

This way we will call the gomodifytags() function when we execute :GoAddTags.

We want GoAddTags to be able to add json tags by specifying a specific tag name as an argument to :GoAddTags json. So, the command syntax declaration needs to be modified to

1
command -nargs=* GoAddTags call gomodifytags()

-nargs=* tells command to accept multiple command arguments. How do you pass the arguments to a later function? This requires the special syntax <f-args>, and there is a lot of special syntax in VimScript. So the command statement needs to be changed to.

1
command -nargs=* GoAddTags call gomodifytags(<f-args>)

Now we write the skeleton of a gomodifytags() function.

1
2
3
function gomodifytags(...)
	echo a:000
endfunction

The three dots here indicate that the number of function arguments is indeterminate and any number of arguments can be passed in. a:000 indicates the full number of arguments to the function call. At this point, we execute :GoAddTags json 1 and see vim output ['json', '1'], indicating that our passing of parameters has taken effect.

We said earlier that we want to support modifying only the selected fields. How do we achieve this? Again, we need to use Vim’s hack. If we select a few lines and press :GoAddTags, Vim will actually display :'<,'>GoAddTags. The preceding '< and '> are special Vim syntax that indicates the beginning and end line numbers of the selected area. So the question is, how do we get this line number in the gomodifytags function? The answer is <line1>, <line2>, <count>, which is analogous to <f-args>. We need to change the command statement to.

1
command -nargs=* -range GoAddTags call lv#gomodifytags(<line1>, <line2>, <count>, '-add-tags', <f-args>)

Then change the gomodifytags function to

1
2
3
function gomodifytags(line1, line2, count, ...)
	echo a:000
endfunction

Here line1 and line2 represent the first and last line of the selected area. count is not clear what it means. But through practice, if there is no selected area, the value of the count field is -1. It is enough to know this point.

The above is the command mapping part, next we will talk about the specific implementation of gomodifytags.

Our goal is to extract the parameters needed by gomodifytags.

  • The file path can be obtained by expand('%p')
  • There are two cases for modifying the range
    • If count is positive, then we have already selected a region, so we can use line1 and line2 directly.
    • Otherwise, we need to determine the range of the current struct ourselves

How to determine the range of the current struct? Vim supports quickly selecting or modifying the contents of a pair of equal numbers. For example, if the Go language uses curly brackets for structs, we can press va{ to select the entire contents of the struct (including the curly brackets themselves), and Vim automatically saves the start and end of the last selected area, which we can read with line("'<") and line("'>").

But there is another problem, how do we execute va{ in gomodifytags? This requires the execute directive.

1
execute 'normal va{^['

Here ^[ needs to be edited by pressing ctrl+v and then Esc, direct copy has no effect.

The last thing you need to do is to execute the gomodifytags command, which uses the system function. Just stitch the command together and pass it to it. So the complete function code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

At this point, executing :GoAddTags json will add json tags to all fields of the current struct.

If you do it yourself, you’ll see the problem: Vim doesn’t show the newly added tags. Because we modified the source code directly using gomodifytags, an external command, and Vim did not load the modified content. So how do we get Vim to load it automatically? The easiest way is to execute the e command at the end of the function, which means edit.

If you try it again, you’ll see that you can indeed display the updated tag content 😂 However, Vim’s screen will flicker a bit and the position of the content will jump. What can I do? This is where Vim’s view save and restore feature comes in.

The core logic is to save the current view before calling the command and restore it after execution.

1
2
3
let v = winsaveview()
e
call winrestview(v)

The above is the entire code of the plugin. The core logic is to extract gomodifytags and then execute the corresponding command, very simple. So, don’t think that Vim plugins are too difficult or too high level. Everyone can develop their own plugins. If you want to be good at what you do, you must first be good at what you do. Others are not as good as their own custom hand.