Extending NeoVim with Lua
Tags: vim, lua
Published: 2022-07-10 13:39:23

I spend a lot of time with NeoVim because it is my main tool for development. It suits me more than others primarily because it has lua and the ability to write your plugins and automation in a convenient and uncomplicated way. Why do I need this when there are already many ready-made solutions for the most common problems? Because sooner or later, there are cases that are not among the ready. And there are also project-specific cases that will never be among the ready. In this article I will show in two examples how it is relatively easy to expand NeoVim and make your work more productive. All examples below can be viewed live in my [dotfile](https://github.com/antgubarev/dotfiles).

## Curl
None of the query UIs I know will give me a Vim-like environment. For example, I need to be able to search and navigate the answer from the API using the same hotkeys that I already have configured in my Vim. Previously, I always had to switch to Postman, write a query there and copy the answer to my editor to be able to analyze it. And all this with a mouse, which is very unusual for someone who used to use a lot of hot keys. 

```lua
local utils = {}

function utils.getCurrentParagraph()
	local row, col = unpack(vim.api.nvim_win_get_cursor(0))
	
	-- search forward 
	local rowi = row
	while true do
		local lastLine = vim.api.nvim_buf_get_lines(0, rowi, rowi+1, false) or {""}
		if lastLine[1] == "" then break end
		if lastLine[1] == nil then break end
		rowi = rowi + 1
	end

	-- search back 
	local rowj = row
	while true do
		local lastLine = vim.api.nvim_buf_get_lines(0, rowj, rowj+1, false) or {""}
		if lastLine[1] == "" then break end
		if lastLine[1] == nil then break end
		rowj = rowj - 1
		if rowj < 1 then break end
	end

	local lines = vim.api.nvim_buf_get_lines(0, rowj+1, rowi, false)
	local result = table.concat(lines, " ")
	result = result:gsub('[%c]', '')
	
	return result
end

return utils
```

This function searches for the first empty line after the cursor position and then the same line until the cursor position. The empty line at the beginning and at the end I consider the border of the paragraph. I will take a step-by-step look at what happens in the function above.

```lua
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
```
So I get the current cursor position in the buffer 0. Buffer 0 is always the current buffer. This API method returns a row and a column, but I’m only interested in a row.

```lua

local rowi = row
while true do
	local lastLine = vim.api.nvim_buf_get_lines(0, rowi, rowi+1, false) or {""}
	if lastLine[1] == "" then break end
	if lastLine[1] == nil then break end
	rowi = rowi + 1
end
```
By getting one line and increasing the count by 1 I check the row that it is empty and that it exists at all (for example if the end of the file is reached). All I need to do is memorize the row number. `False` in function arguments means that you do not want to throw an error if there is no index.
Then I look in the opposite direction and I find the top of the paragraph.
Then all I have to do is get the lines along the lines and merge them into one.

```lua
local lines = vim.api.nvim_buf_get_lines(0, rowj+1, rowi, false)
local result = table.concat(lines, " ")
result = result:gsub('[%c]', '')
```
Here I additionally delete the template `[%c]` The fact is that the buffer will have a large number of different special characters, which when placed in the console breed a lot of errors. Such as

```
line 1 v curl -X POST https://jsonplaceholder.typicode.com/comments
line 2 v ^I-H 'Content-Type: application/json'
line 3 v ^I-d '{
line 4 v ^I^I"postId": 1
line 5 v ^I}'
```

Okay, now I have the current paragraph. 
To allow this function to be imported into other lua scripts I made it as a module and return it when the file is imported. Below in this article in another example I again use this module in another script. And so far I send the command to the terminal. As a terminal emulator I like [toggleterm](https://github.com/akinsho/toggleterm.nvim) because it is flexible enough and has a convenient API.

```lua
local utils = require("apg.utils")

function execCurl()
	local command = utils.getCurrentParagraph()

	local Terminal = require('toggleterm.terminal').Terminal
	local run = Terminal:new({
		cmd = command,
		hidden = true,
		direction = "float",
		close_on_exit = false,
	   on_open = function(term)
		  vim.api.nvim_buf_set_keymap(term.bufnr, "t", "q", "<cmd>close<CR>", {noremap = true, silent = true})
		end,
	})

	run:toggle()

end
```

And now let me show you how it works.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vy2pm527xhvwtukx46s7.gif)

## SQL

To work with SQL databases already there are a huge number of excellent tools. Every IDE I’m familiar with has database plugins. Vim is included. [LSP server](https://github.com/lighttiger2505/sqls) already exists. But sometimes I just need to run a simple query, without getting distracted by going to other tools, looking for the right connection, or the prepared SQL query. Therefore, using the `getCurrentParagraph` function discussed above, I implemented a simple and fast start of SQL queries for PostgreSQL

```lua
local utils = require("apg.utils")

function execPgSql()
	local query = utils.getCurrentParagraph()
	local cfg = vim.api.nvim_buf_get_lines(0, 0, 1, false)
	local command = 'psql '..cfg[1]..' -c "'..query..'"'

	local Terminal = require('toggleterm.terminal').Terminal
	local run = Terminal:new({
		cmd = command,
		hidden = true,
		direction = "float",
		close_on_exit = false,
	   on_open = function(term)
		  vim.api.nvim_buf_set_keymap(term.bufnr, "t", "q", "<cmd>close<CR>", {noremap = true, silent = true})
		end,
	})

	run:toggle()
end
```
Now I have in the project files with prepared SQL queries, which I can reach and run unrealistically fast. Here is an example of such a file

```sql
-h localhost -p 5432 -U postgres -d demo -W

SELECT   a.aircraft_code,
         a.model,
         s.seat_no,
         s.fare_conditions
FROM     aircrafts a
         JOIN seats s ON a.aircraft_code = s.aircraft_code
WHERE    a.model = 'Cessna 208 Caravan'
ORDER BY s.seat_no;

SELECT   s2.aircraft_code,
         string_agg (s2.fare_conditions || '(' || s2.num::text || ')',
                     ', ') as fare_conditions
FROM     (
          SELECT   s.aircraft_code, s.fare_conditions, count(*) as num
          FROM     seats s
          GROUP BY s.aircraft_code, s.fare_conditions
          ORDER BY s.aircraft_code, s.fare_conditions
         ) s2
GROUP BY s2.aircraft_code
ORDER BY s2.aircraft_code;
```
Here you can see the additional first line. It includes the parameters of connection to the database server and is substituted before query

```lua
local cfg = vim.api.nvim_buf_get_lines(0, 0, 1, false)
local command = 'psql '..cfg[1]..' -c "'..query..'"'
```
I would still like to have a separate file for the configuration of the connection to the base and select the desired one before executing the request. But so far, it’s working fine for me. Maybe in the future I’ll work on this script. I’ll show you how it works.

And now let me show you how it works.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vixjcvoelr2fvnz07d4r.gif)

## Conclusion

With these scripts, I speed up my work and make it more pleasant. Because I don’t get distracted by unnecessary actions, like switching to other tools and memorizing their features. I hope my examples have helped you understand the general principle of writing extensions for NeoVim and maybe find new opportunities for yourself.