In the last article, we saw the basics of creating plugins in Lua using floating windows. Now it's time for a more traditional approach. Let's create a simple plugin that will show us last opened files in handy side navigation. As we focus on learning the interface, we will use vim native oldfiles list for this purpose. It will look something like this:
If you didn't read previous article, I highly recommend you to do so, because this article expands on the ideas from the last one and is full of new things in comparison.
Plugin window
Ok, so we should start by writing a function that will create our first window, where the oldfiles
list will be displayed. But first, we will declare three variables in the main scope of our script: buf
and win
that will contain our navigation window and buffer references and start_win
that will remember the position where we opened our navigation. We will be using these often across our plugin functions.
-- It's our main starting function. For now we will only creating navigation window here.
local function oldfiles()
create_win()
end
local function create_win()
-- We save handle to window from which we open the navigation
start_win = vim.api.nvim_get_current_win()
vim.api.nvim_command('botright vnew') -- We open a new vertical window at the far right
win = vim.api.nvim_get_current_win() -- We save our navigation window handle...
buf = vim.api.nvim_get_current_buf() -- ...and it's buffer handle.
-- We should name our buffer. All buffers in vim must have unique names.
-- The easiest solution will be adding buffer handle to it
-- because it is already unique and it's just a number.
vim.api.nvim_buf_set_name(buf, 'Oldfiles #' .. buf)
-- Now we set some options for our buffer.
-- nofile prevent mark buffer as modified so we never get warnings about not saved changes.
-- Also some plugins treat nofile buffers different.
-- For example coc.nvim don't triggers aoutcompletation for these.
vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile')
-- We do not need swapfile for this buffer.
vim.api.nvim_buf_set_option(buf, 'swapfile', false)
-- And we would rather prefer that this buffer will be destroyed when hide.
vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe')
-- It's not necessary but it is good practice to set custom filetype.
-- This allows users to create their own autocommand or colorschemes on filetype.
-- and prevent collisions with other plugins.
vim.api.nvim_buf_set_option(buf, 'filetype', 'nvim-oldfile')
-- For better UX we will turn off line wrap and turn on current line highlight.
vim.api.nvim_win_set_option(win, 'wrap', false)
vim.api.nvim_win_set_option(win, 'cursorline', true)
set_mappings() -- At end we will set mappings for our navigation.
end
Drawing function
Okay, so we have a window, now we need something to display in it. We will use vim oldfiles
special variable, which stores paths to previously opened files. We will take as many items from it, as we can display without scrolling, but of course, you can take as many as you want in your script. We will call this function redraw
because it can be used to refresh navigation content. File paths might be long, so we will try to make them relative to the working directory.
local function redraw()
-- First we allow introduce new changes to buffer. We will block that at end.
vim.api.nvim_buf_set_option(buf, 'modifiable', true)
local items_count = vim.api.nvim_win_get_height(win) - 1 -- get the window height
local list = {}
-- If you using nightly build you can get oldfiles like this
local oldfiles = vim.v.oldfiles
-- In stable version works only that
local oldfiles = vim.api.nvim_get_vvar('oldfiles')
-- Now we populate our list with X last items form oldfiles
for i = #oldfiles, #oldfiles - items_count, -1 do
-- We use build-in vim function fnamemodify to make path relative
-- In nightly we can call vim function like that
local path = vim.fn.fnamemodify(oldfiles[i], ':.')
-- and this is stable version:
local path = vim.api.nvim_call_function('fnamemodify', {oldfiles[i], ':.'})
-- We iterate form end to start, so we should insert items
-- at the end of results list to preserve order
table.insert(list, #list + 1, path)
end
-- We apply results to buffer
vim.api.nvim_buf_set_lines(buf, 0, -1, false, list)
-- And turn off editing
vim.api.nvim_buf_set_option(buf, 'modifiable', false)
end
We can now update our main function. We will also add some code that prevents opening multiple navigation windows. For this purpose, we can use nvim_win_is_valid
which checks if our plugin window already exists.
local function oldfiles()
if win and vim.api.nvim_win_is_valid(win) then
vim.api.nvim_set_current_win(win)
else
create_win()
end
redraw()
end
Openings files
We can now look at our oldfiles, but it would be much handier if we can also open them. We will allow users to open files in 5 different ways! In a new tab, in horizontal or vertical splits, in the current window and in preview mode, which will keep the focus on navigation.
Let's start by opening files in the current window. We should prepare for two scenarios:
1. Opening a file in the window from which the user opens navigation.
2. Closing the starting window, when we will create a new one for opening file.
local function open()
-- We get path from line which user push enter on
local path = vim.api.nvim_get_current_line()
-- if the starting window exists
if vim.api.nvim_win_is_valid(start_win) then
-- we move to it
vim.api.nvim_set_current_win(start_win)
-- and edit chosen file
vim.api.nvim_command('edit ' .. path)
else
-- if there is no starting window we create new from lest side
vim.api.nvim_command('leftabove vsplit ' .. path)
-- and set it as our new starting window
start_win = vim.api.nvim_get_current_win()
end
end
-- After opening desired file user no longer need our navigation
-- so we should create function to closing it.
local function close()
if win and vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
end
-- Ok. Now we are ready to making two first opening functions
local function open_and_close()
open() -- We open new file
close() -- and close navigation
end
local function preview()
open() -- WE open new file
-- but in preview instead of closing navigation
-- we focus back to it
vim.api.nvim_set_current_win(win)
end
-- To making splits we need only one function
local function split(axis)
local path = vim.api.nvim_get_current_line()
-- We still need to handle two scenarios
if vim.api.nvim_win_is_valid(start_win) then
vim.api.nvim_set_current_win(start_win)
-- We pass v in axis argument if we want vertical split
-- or nothing/empty string otherwise.
vim.api.nvim_command(axis ..'split ' .. path)
else
-- if there is no starting window we make new on left
vim.api.nvim_command('leftabove ' .. axis..'split ' .. path)
-- but in this case we do not need to set new starting window
-- because splits always close navigation
end
close()
end
And in the end the simplest opening in new tab.
local function open_in_tab()
local path = vim.api.nvim_get_current_line()
vim.api.nvim_command('tabnew ' .. path)
close()
end
For everything to work, we need to add the key mappings, export all public functions, and add a command to trigger our navigation.
local function set_mappings()
local mappings = {
q = 'close()',
['<cr>'] = 'open_and_close()',
v = 'split("v")',
s = 'split("")',
p = 'preview()',
t = 'open_in_tab()'
}
for k,v in pairs(mappings) do
-- let's assume that our script is in lua/nvim-oldfile.lua file.
vim.api.nvim_buf_set_keymap(buf, 'n', k, ':lua require"nvim-oldfile".'..v..'<cr>', {
nowait = true, noremap = true, silent = true
})
end
end
-- at file end
return {
oldfiles = oldfiles,
close = close,
open_and_close = open_and_close,
preview = preview,
open_in_tab = open_in_tab,
split = split
}
command! Oldfiles lua require'nvim-oldfile'.oldfiles()
And that's it! Have fun and make grate things!
The whole plugin
local buf, win, start_win
local function open()
local path = vim.api.nvim_get_current_line()
if vim.api.nvim_win_is_valid(start_win) then
vim.api.nvim_set_current_win(start_win)
vim.api.nvim_command('edit ' .. path)
else
vim.api.nvim_command('leftabove vsplit ' .. path)
start_win = vim.api.nvim_get_current_win()
end
end
local function close()
if win and vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
end
local function open_and_close()
open()
close()
end
local function preview()
open()
vim.api.nvim_set_current_win(win)
end
local function split(axis)
local path = vim.api.nvim_get_current_line()
if vim.api.nvim_win_is_valid(start_win) then
vim.api.nvim_set_current_win(start_win)
vim.api.nvim_command(axis ..'split ' .. path)
else
vim.api.nvim_command('leftabove ' .. axis..'split ' .. path)
end
close()
end
local function open_in_tab()
local path = vim.api.nvim_get_current_line()
vim.api.nvim_command('tabnew ' .. path)
close()
end
local function redraw()
vim.api.nvim_buf_set_option(buf, 'modifiable', true)
local items_count = vim.api.nvim_win_get_height(win) - 1
local list = {}
local oldfiles = vim.api.nvim_get_vvar('oldfiles')
for i = #oldfiles, #oldfiles - items_count, -1 do
pcall(function()
local path = vim.api.nvim_call_function('fnamemodify', {oldfiles[i], ':.'})
table.insert(list, #list + 1, path)
end)
end
vim.api.nvim_buf_set_lines(buf, 0, -1, false, list)
vim.api.nvim_buf_set_option(buf, 'modifiable', false)
end
local function set_mappings()
local mappings = {
q = 'close()',
['<cr>'] = 'open_and_close()',
v = 'split("v")',
s = 'split("")',
p = 'preview()',
t = 'open_in_tab()'
}
for k,v in pairs(mappings) do
vim.api.nvim_buf_set_keymap(buf, 'n', k, ':lua require"nvim-oldfile".'..v..'<cr>', {
nowait = true, noremap = true, silent = true
})
end
end
local function create_win()
start_win = vim.api.nvim_get_current_win()
vim.api.nvim_command('botright vnew')
win = vim.api.nvim_get_current_win()
buf = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_name(0, 'Oldfiles #' .. buf)
vim.api.nvim_buf_set_option(0, 'buftype', 'nofile')
vim.api.nvim_buf_set_option(0, 'swapfile', false)
vim.api.nvim_buf_set_option(0, 'filetype', 'nvim-oldfile')
vim.api.nvim_buf_set_option(0, 'bufhidden', 'wipe')
vim.api.nvim_command('setlocal nowrap')
vim.api.nvim_command('setlocal cursorline')
set_mappings()
end
local function oldfiles()
if win and vim.api.nvim_win_is_valid(win) then
vim.api.nvim_set_current_win(win)
else
create_win()
end
redraw()
end
return {
oldfiles = oldfiles,
close = close,
open_and_close = open_and_close,
preview = preview,
open_in_tab = open_in_tab,
split = split
}