TL;DR
A context-aware snippet for Go error handling code, returning the right types, with the default values.
EDIT(2024-02-23): TJ DeVries made a video explaining an improved version of this snippet. You may still find the discussion on Go errors in this post interesting.
Introduction
Golang’s error handling is notoriously verbose. It was also the top pain point in the Go Developer Survey Q2 2022. Numerous proposals to simplify error handling have been written, but at the time of writing, none have been accepted.
So we have to live with the current state of things. It’s ok, Neovim with Treesitter has a good “understanding” of code, so we can use it to generate the error handling code.
Treesitter
One of Neovim 0.5 most exciting features was the introduction of Treesitter. This new parsing system gives the editor a basic understanding of the code at hand: the editor “knows” what the function name is, where a variable is defined… It enables great plugins, showing the context around some code, improved selection, better renaming and navigation and more! In this post, we will look into how Treesitter can be combined with LuaSnip for smarter snippets.
Error Handling in Go
Feel free to skip this section if you already know about Go error handling.
In Golang, errors are handled by returning a value of type error
, sometimes along the types returned when everything goes well. For instance, the time.Parse
function has the following signature:
func Parse(layout, value string) (Time, error)
time.Parse
will try to parse a string with the given format.
If it’s successful, it’ll return a value of type Time
and the error will be nil
.
If a date can’t be parsed, then the error won’t be nil
.
And so it’s common in Go to check for errors like so:
timeStr := "Aug 6, 2022 @ 7:54pm"
t, err := time.Parse("Jan 2, 2006 @ 3:04pm", timeStr)
if err != nil {
log.Fatal(err)
}
// Now, we can safely use t
In the example above, we just log the error and panic. But if we are in a function, we will likely return an error to the caller, wrapping the error returned by time.Parse
. For instance:
1func parseLong(timeStr string) (Time, error) {
2 t, err := time.Parse("Jan 2, 2006 @ 3:04pm", timeStr)
3 if err != nil {
4 return t, fmt.Errorf("couldn’t parse %v: %v", timeStr, err)
5 }
6 return t, nil
7}
The key takeaway here is that the return
line in the error case (line 4 above) depends on the return type of the function and so can vary significantly.
This is covered in more details in this post on the Go blog.
Clever Snippets with LuaSnip and Treesitter
As we have seen, error handling code varies depending on the number and type of the values returned by the parent function. So we are very often writing variations of the following in our Go code:
func f(arg1 string, arg2 string) (…, error) {
val, err := someFunction(arg1, arg2)
if err != nil {
return …, err
}
…
}
Or with 3 return values:
func f(arg string) (…, …, error) {
val, err := someFunction(arg)
if err != nil {
return …, …, err
}
…
}
It would be handy to have a snippet that would adjust the return
’s shape depending on the return type of the function that contains it.
Demo
Here is a short asciicast demonstration1 of the snippet we are going to build. It is called smart_err
:
The snippet inserted (line 11 in the asciicast) is
myVal, myErr := anotherFunc(arg1, arg2)
if myErr != nil {
return false, fmt.Errorf("anotherFunc: %v", myErr)
}
when the return type of the function was (bool, error)
.
And
val, err := f()
if err != nil {
return false, fmt.Errorf("f: %v", err), 0, ""
}
when the return type was (bool, error, int, string)
.
And it even works in nested functions:
func myFunc() (*MyStruct, error) {
// ...
a := func() (bool, error, int, string) {
// The snippet detected that we need 4 values here,
// and not 2 as in the outer function
return v, nil, 0, ""
}
Also note that we can choose to wrap the containing error, or not, with a keystroke (bound to require("luasnip").change_choice(1)
). The snippet switches between the following 3 forms (line 18 in the asciicast):
return false, err
return false, fmt.Errorf("f: %v", err)
return false, errors.Wrap(err, "f")
That’s pretty cool! Let’s see how to add it to your Neovim configuration.
Code
The code to define this snippet was initially written by TJ DeVries2. I’ve only slightly adapted it so that it fits in one file. I believe the author permits this, as the readme states3:
This is TJ’s configuration repo. Feel free to use whatever you would like from it! It’d be great if you mentioned where it came from if you think it’s cool.
Copy this alongside your other LuaSnip snippets, for instance in ~/.config/nvim/snippets/go.lua
:
1local ls = require "luasnip"
2local f = ls.function_node
3local s = ls.s
4local i = ls.insert_node
5local t = ls.text_node
6local d = ls.dynamic_node
7local c = ls.choice_node
8local snippet_from_nodes = ls.sn
9
10local ts_locals = require "nvim-treesitter.locals"
11local ts_utils = require "nvim-treesitter.ts_utils"
12local get_node_text = vim.treesitter.get_node_text
13
14-- Adapted from https://github.com/tjdevries/config_manager/blob/1a93f03dfe254b5332b176ae8ec926e69a5d9805/xdg_config/nvim/lua/tj/snips/ft/go.lua
15local function same(index)
16 return f(function(args)
17 return args[1]
18 end, { index })
19end
20
21-- Adapted from https://github.com/tjdevries/config_manager/blob/1a93f03dfe254b5332b176ae8ec926e69a5d9805/xdg_config/nvim/lua/tj/snips/ft/go.lua
22vim.treesitter.query.set(
23 "go",
24 "LuaSnip_Result",
25 [[ [
26 (method_declaration result: (_) @id)
27 (function_declaration result: (_) @id)
28 (func_literal result: (_) @id)
29 ] ]]
30)
31
32-- Adapted from https://github.com/tjdevries/config_manager/blob/1a93f03dfe254b5332b176ae8ec926e69a5d9805/xdg_config/nvim/lua/tj/snips/ft/go.lua
33local transform = function(text, info)
34 if text == "int" then
35 return t "0"
36 elseif text == "error" then
37 if info then
38 info.index = info.index + 1
39
40 return c(info.index, {
41 t(string.format('fmt.Errorf("%s: %%v", %s)', info.func_name, info.err_name)),
42 t(info.err_name),
43 -- Be cautious with wrapping, it makes the error part of the API of the
44 -- function, see https://go.dev/blog/go1.13-errors#whether-to-wrap
45 t(string.format('fmt.Errorf("%s: %%w", %s)', info.func_name, info.err_name)),
46 -- Old style (pre 1.13, see https://go.dev/blog/go1.13-errors), using
47 -- https://github.com/pkg/errors
48 t(string.format('errors.Wrap(%s, "%s")', info.err_name, info.func_name)),
49 })
50 else
51 return t "err"
52 end
53 elseif text == "bool" then
54 return t "false"
55 elseif text == "string" then
56 return t '""'
57 elseif string.find(text, "*", 1, true) then
58 return t "nil"
59 end
60
61 return t(text)
62end
63
64local handlers = {
65 ["parameter_list"] = function(node, info)
66 local result = {}
67
68 local count = node:named_child_count()
69 for idx = 0, count - 1 do
70 table.insert(result, transform(get_node_text(node:named_child(idx), 0), info))
71 if idx ~= count - 1 then
72 table.insert(result, t { ", " })
73 end
74 end
75
76 return result
77 end,
78
79 ["type_identifier"] = function(node, info)
80 local text = get_node_text(node, 0)
81 return { transform(text, info) }
82 end,
83}
84
85-- Adapted from https://github.com/tjdevries/config_manager/blob/1a93f03dfe254b5332b176ae8ec926e69a5d9805/xdg_config/nvim/lua/tj/snips/ft/go.lua
86local function go_result_type(info)
87 local cursor_node = ts_utils.get_node_at_cursor()
88 local scope = ts_locals.get_scope_tree(cursor_node, 0)
89
90 local function_node
91 for _, v in ipairs(scope) do
92 if
93 v:type() == "function_declaration"
94 or v:type() == "method_declaration"
95 or v:type() == "func_literal"
96 then
97 function_node = v
98 break
99 end
100 end
101
102 local query = vim.treesitter.query.get("go", "LuaSnip_Result")
103 for _, node in query:iter_captures(function_node, 0) do
104 if handlers[node:type()] then
105 return handlers[node:type()](node, info)
106 end
107 end
108
109 return { t "nil" }
110end
111
112-- Adapted from https://github.com/tjdevries/config_manager/blob/1a93f03dfe254b5332b176ae8ec926e69a5d9805/xdg_config/nvim/lua/tj/snips/ft/go.lua
113local go_ret_vals = function(args)
114 return snippet_from_nodes(
115 nil,
116 go_result_type {
117 index = 0,
118 err_name = args[1][1],
119 func_name = args[2][1],
120 }
121 )
122end
123
124return {
125 -- Adapted from https://github.com/tjdevries/config_manager/blob/1a93f03dfe254b5332b176ae8ec926e69a5d9805/xdg_config/nvim/lua/tj/snips/ft/go.lua
126 s("smart_err", {
127 i(1, { "val" }),
128 t ", ",
129 i(2, { "err" }),
130 t " := ",
131 i(3, { "f" }),
132 t "(",
133 i(4),
134 t ")",
135 t { "", "if " },
136 same(2),
137 t { " != nil {", "\treturn " },
138 d(5, go_ret_vals, { 2, 3 }),
139 t { "", "}" },
140 i(0),
141 }),
142}
We start by importing and aliasing some LuaSnip functions. The snippet itself is defined at the end (line 126). Most of those functions were covered in the previous post, see there for calls to s
, i
and t
. The clever part of this snippet is the dynamic node d
on line 138. There, we call a function that queries (line 22 and 102) Treesitter to find the return types of the current function in Go code. Depending on the types found, default values for the type are used (line 33).
Treesitter Query
The Treesitter query is where the magic happens, so let’s take a deeper look.
Treesitter parses code and builds a tree with node for various parts of the code. Each node can have children, like a function definition has a name, a body… This tree can be queried in a Lisp-like language (s-expressions). For instance, in our snippet definition, we use this query:
1[
2 (method_declaration result: (_) @id)
3 (function_declaration result: (_) @id)
4 (func_literal result: (_) @id)
5]
The bracket syntax returns the nodes that match any of the patterns inside the brackets. So it’s effectively the union of all the nodes matched by any of the 3 patterns (on lines 2 to 4). Let’s take a concrete example and put the Go code in the Treesitter playground, with a trimmed down version of the query:
The tree built by Treesitter is at the bottom, with the corresponding code at the top and the query in the middle. The function_declaration
node is highlighted in gray in the tree, like the corresponding part of the code (from line 13 to line 22). This node has a number of children, with the function name
, the parameters
of the function, a result
node for the return type and body
for the body of the function between curly braces.
Taking the first pattern on the playground, slightly modified:
(function_declaration result: (_) @id2)
The part it captures is highlighted in blue in the Go code: (*MyStruct, error)
. It makes sense, since the function_declaration
(the part on the gray background) has a result
child, that’s captured by @id
.
Now, on to the second pattern:
(func_literal result: (_) @id3)
Similarly, further down in the body
of that function, the return type (bool, error, int, string)
of the anonymous function is matched.
Conclusion
I started writing this post over a year ago. The snippet has been very stable and kept working as without changes, which is a testament to the stability of the LuaSnip (and Treesitter) APIs. I’m still using it when I write Go code, even if some corner cases (like custom structs) aren’t handled. The snippet is quite complicated and niche, so I’m not sure that I would have invested the time to write it myself. I probably don’t write enough Go code to justify the investment, but finding it shared by someone buried in their config file made it worth it. I hope you have found this port useful, either to copy this particular snippet or as an example of advanced snippets.
This is the last post in a series on LuaSnip.
Resources
If you want to dig deeper:
- Working with Errors in Go 1.13
- LuaSnip documentation
- Explore the Treesitter tree for any Neovim buffer where it is active with
:lua vim.treesitter.inspect_tree()
- Treesitter’s Pattern Matching with Queries
By the way, he has a very interesting Twitch channel ↩︎
At the time of writing. See perma.cc and the wayback machine for a snapshot. ↩︎
Liked this post? Subscribe:
Discussions
This blog does not host comments, but you can reply via email or participate in one of the discussions below: