Anton Gubarev
Posted on July 10, 2022
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.
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.
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.
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.
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.
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 because it is flexible enough and has a convenient API.
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.
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 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
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
-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
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.
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.
Posted on July 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.