Nvim Plugins for Dummies

For Dummies is an extensive series of instructional reference books which are intended to present non-intimidating guides for readers new to the various topics covered

from Wikipedia

cover

For Dummies

You want to do your toy project, do it for fun. You want some result-guaranteed experience: no stress, no frustration, no excessive complexity.

You have an idea. You have years of software engineering experience. However, you want to explore some new territory. What can go wrong?

You need a non-intimidating guide to do it, beginner-friendly materials.

I was needed to move between functions in Go for my nvim + lsp + gopls setup. I decided to make a plugin for it and basically just exported functionality :GoDecls from vim-go plugin with a few tweaks. Plugin has three main parts:

  • parset to work with syntax tree
  • UI to represent data
  • plugin body itself which unserstands context, calls parser and representation data in vim UI

I used vimscript to write plugin itself, motion to work with syntax tree and fzf#run which is basic wrapper function for fzf.

I expected simplicity of using Nespresso machine: you put a capsule and get a cup of coffee. Instead I got a cold shower.

So I was looking for new, more robust tools for that. Somehow it feels easier for me to have a deal with nvim and Lua then vim/vimscript. I have got emotionally rewarding experience contributing to opensource project neotest-go. I set up my lsp support for Lua, wrote a few unit tests, got familiar with tree-sitter. So I decided to redo it. So have rewritten from scratch in Lua.

I replaced motion written in Go with tree-sitter which is natively supported by nvim.

Finally, fzf was replaced with Telescope. plugin

Tree-sitter

What is it for?

Tree-sitter allows you to build a concrete syntax tree for a source file. Then you can run a query against this syntax tree and nodes that match the query will be captured. Sounds intimidating, doesn’t it. Fortunately, tree-sitter has a nice documentation, and, more importantly, a plaground.

Query

What I was need is a query to select all functions and methods from current source file:

1
2
3
4
5
6
;;query
(function_declaration
  name: (identifier) @func.name)

(method_declaration
  name: (field_identifier) @func.name)

This query provides you all matches that are captured as @func.name with actual name of function/method and a postion of it in a source file.

Let’s say we have a Go file

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

func main() {
}

func a() {
}

func b() {
}

func c() {
}

type dummy struct {
}

func (d dummy) doDummy() {
}

and the query result could be formatted to function name, line number, column number (line and column numbers start with 0)

1
2
3
4
5
main 2 5
a 5 5
b 8 5
c 11 5
doDummy 17 15

Lua function that returns list of formatted query results base on passed nvim buffer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local get_nodes = function(bufnr)
	local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype')
	local language_tree = vim.treesitter.get_parser(bufnr, filetype)
	local syntax_tree = language_tree:parse()
	local root = syntax_tree[1]:root()

	local query = vim.treesitter.parse_query(filetype, [[
    ;;query
	(function_declaration
      name: (identifier) @func.name)
	(method_declaration
      name: (field_identifier) @func.name)
]])
	local captured_nodes = {}
	local idx = 1
	for _, captures in query:iter_matches(root, bufnr) do
		local func_name = q.get_node_text(captures[1], bufnr)
		local ln, col = captures[1]:range()
		local pos = {}
		pos["line"] = ln
		pos["col"] = col
		captured_nodes[idx] = { func_name, pos }
		idx = idx + 1
	end
	return captured_nodes

end

We will use this later for our telescope extension.

Role of Playground

It takes usually huge amount of time to set up a plaground for yourself with a new language/technology. For example you want to learn Java and you go to the official website to get some help. You get some tutorials which are made in old-fashioned, developer-unfriendly style. You need to set up a software, tools, go through tons of error on that way until can start execute steps from tutorials.

From the other hand tree-sitter provide you a playground immediatlely. What difference does it make? It gives you a safe environment in which to explore and get an experiental context. You start learning not by documentation analysis but by synthesis your own examples. You will go to the documentation when you need to clarify something, to go deeper into the topic, until you are making a bigger picture.

I am pretty sure that success of Go language and Kubernetes is based on resources they provide including playground.

Telescope

telescope.nvim is a highly extendable fuzzy finder over lists. Built on the latest awesome features from neovim core. Telescope is centered around modularity, allowing for easy customization.

Telescope doesn’t have a playground, but it has nice example which you can write and run from nvim immediately.

I have just slightly customized it for my needs.

Getting started with telescope:

You can find more about info about architecture and primary components :h telescope.nvim, but simplified version looks next:

  • picker: central UI dedicated to varying use cases(finding files, grepping, diagnostics, etc.), the most highlevel object I worked with
  • finder: pipe or interactively generates results to pick over, represents results of tree-sitter query in specific form
  • actions: functions that are useful for people creating their own mappings, allows us to process selected entry and act respectively

Finder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
finder = finders.new_table {
    results = get_nodes(vim.api.nvim_get_current_buf()),
    entry_maker = function(entry)
        return {
            value = entry,
            display = entry[1],
            ordinal = entry[1],
        }
    end
},

where get_nodes is a function we discussed above.

Actions

We use action mapping to set cursor to the position of a func we selected from finder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
attach_mappings = function(prompt_bufnr, map)
    actions.select_default:replace(function()
        actions.close(prompt_bufnr)
        local selection = action_state.get_selected_entry()
        local line = selection.value[2].line + 1
        local col = selection.value[2].col + 1
        vim.api.nvim_win_set_cursor(0, { line, col })
    end)
    return true
end,

Lua

Last but not least, Lua gives much better developer experience than vimscript. First of all, you can set up Lua LSP support(scaping configuration from here). Also it has bigger community and built in nvim lua guide(:help lua-guide).

Full code. Repo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
local pickers = require "telescope.pickers"
local finders = require "telescope.finders"
local conf = require("telescope.config").values
local actions = require "telescope.actions"
local action_state = require "telescope.actions.state"
local q = require "vim.treesitter.query"

local get_nodes = function(bufnr)
	local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype')
	local language_tree = vim.treesitter.get_parser(bufnr, filetype)
	local syntax_tree = language_tree:parse()
	local root = syntax_tree[1]:root()

	local query = vim.treesitter.parse_query(filetype, [[
    ;;query
	(function_declaration
      name: (identifier) @func.name)
	(method_declaration
      name: (field_identifier) @func.name)
]])
	local captured_nodes = {}
	local idx = 1
	for _, captures in query:iter_matches(root, bufnr) do
		local func_name = q.get_node_text(captures[1], bufnr)
		local ln, col = captures[1]:range()
		local pos = {}
		pos["line"] = ln
		pos["col"] = col
		captured_nodes[idx] = { func_name, pos }
		idx = idx + 1
	end
	return captured_nodes

end

-- our picker function: funkmotion
local funkmotion = function(opts)
	opts = opts or {}
	pickers.new(opts, {
		prompt_title = "functions",
		finder = finders.new_table {
			results = get_nodes(vim.api.nvim_get_current_buf()),
			entry_maker = function(entry)
				return {
					value = entry,
					display = entry[1],
					ordinal = entry[1],
				}
			end
		},
		sorter = conf.generic_sorter(opts),
		attach_mappings = function(prompt_bufnr, map)
			actions.select_default:replace(function()
				actions.close(prompt_bufnr)
				local selection = action_state.get_selected_entry()
				local line = selection.value[2].line + 1
				local col = selection.value[2].col + 1
				vim.api.nvim_win_set_cursor(0, { line, col })
			end)
			return true
		end,
	}):find()
end

return require("telescope").register_extension {
	exports = {
		funkmotion = funkmotion
	},
}

Demo

asciicast