Treating Go types as objects in Vim

Treating Go types as objects in Vim
Photo by https://unsplash.com/samuelzeller

Vim users know that they can edit text in a very different way, with what we
call as “text objects” or “text motions”. For example words are defined with
the character “w” or paragraphs with “p”. Instead of selecting words
manually and change them (or delete) you select “words” and operate on
them. So you can replace words, delete them, copy them and so on. These actions
are called operators in Vim and there are many of them, such as “d” for
delete, “c” for change, “y” for yank, etc… How do they work?

For example this simple action below means “delete a word”:

dw

Or the following action means “copy four words”:

4yw

This is a very powerful concept that enables Vim users to edit their content in a very different mindset.

So I thought about these “text objects” and “text motions” a little bit. It’s very useful to have the “w” object that defines how to move forward to a “word”. Combining this with an operator changes what we want to do with the word. But there are many other objects that are not useful when you edit a **Go source code. **Such as “p” for paragraphs, or “s” for sentences, “t” for XML tags, and so on.

What if we could define Go specific text objects? For example what if we define a function with the character “f” ? Or what if we could move to the third function in our source code? These are questions that opens a whole different mindset of editing your source code.

Current Problems

Initially vim-go had already some primitive text objects, such as “af” and “if”, which defines “a function” and “inner function”. But these were based on searching backwards via a regex statement for a func keyword and doing the necessary action based on the operator.

This approach (based on regex) was very flawed and had problems:

  1. It doesn’t select function literals
  2. It doesn’t know what a function is (no knowledge of the AST).
  3. It’s based on regex, maintaining it is not easy and also not worth it. It’s fundamentally broken because it doesn’t has any kind of knowledge what the source code is.
  4. Using regex meant also that we can’t use more advanced cases, such as selecting one-line function declarations from the same line, selecting function declarations from the comment itself, etc…

Regex is not the solution.

Fortunately Go has very fundamental and great packages (the parser family: go/{parser,token,scanner,ast}) that enables us to parse Go code and get a sense how a Go code is constructed. These packages are already used as a foundation to many other tools. Why don’t we use this information?

Introducing Motion

Motion is the name I gave to the whole project that combines the parser and CLI tool, vim implementation and the idea behind this story.

motion is the tool which is doing the heavy lifting. It’s a CLI tool that can be run in multiple modes and outputs different outputs based on the given mode (_just like _guru)

Under the hood it uses the go parser family to parse the given file or director and then returns a consumable output. Currently it outputs in vim and json format. The output changes based on the given mode. For example one of the modes is called enclosing, and this mode returns the enclosed function information for a given offset. What does the information contains? Anything we might be interested! Below is an example output:

{
 "mode": "enclosing",
 "func": {
  "sig": {
   "full": "func Bar() (string, error)",
   "recv": "",
   "name": "Bar",
   "in": "",
   "out": "string, error"
  },
  "func": {
   "filename": "testdata/main.go",
   "offset": 174,
   "line": 15,
   "col": 1
  },
  "lbrace": {
   "filename": "testdata/main.go",
   "offset": 201,
   "line": 15,
   "col": 28
  },
  "rbrace": {
   "filename": "testdata/main.go",
   "offset": 225,
   "line": 17,
   "col": 1
  }
 }
}

This information is just a simple representation of a given function declaration. For example suppose we have the information above. Now for any given cursor position, we can easily know if the cursor is inside a function, is on top of a comment documentation or is outside a function.

The new implementation in vim-go is using
motion to retrieve the necessary information
and then using that information to perform the operator actions on the if and
af objects.

Here is a video of showing how powerful these objects are now:

Many details are now better compared to the regex implementation:

  • Anonymous functions are supported
  • One liner functions can be selected easier
  • Cursor position can be anywhere as long as it makes sense
  • Comments are treated as a part of the function declaration

Relying on the AST opens a whole another world of ways implementing new features. This is only one part, in my next blog post (I’ve posted it, go ahead and read it!) I’m going to show another new feature in vim-go, which is used to jump to a generic declaration both on file and package level.

Text object support will be released soon with other additional improvements and new features.

It’s still in alpha mode, but if you have any feedback and ideas how we can even improve it even further, please don’t hesitate! I’m looking forward to improve it and extend it with possible other features.