DEV Community

Anton Gubarev
Anton Gubarev

Posted on

Extending NeoVim with Lua

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]', '')
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And now let me show you how it works.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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..'"'
Enter fullscreen mode Exit fullscreen mode

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

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.

Top comments (0)