Neovim - Build UI using nui.nvim
I’ve been using Neovim almost everyday, for years now. I play around with my config files often and recently been trying out the built-in LSP features that come with Neovim 0.5+.
For my work stuffs I still use coc.nvim
and I'm quite used to the nice and simple UIs it provides to interact with the LSP client. So I wanted something similar for the built-in LSP client too.
And I found some excellent projects that provide UIs for LSP. For example:
Surely, I could’ve used one of them and called it a day. But this blog post wouldn’t be here if I did that! 😝
Instead I thought, what if I try to make those UIs myself? So the next logical (🤔) thing was to search for plugin/library for building Neovim UIs quickly. Here’s what I found so far:
Most of the Neovim Lua plugins I saw share some version or variant of codes from these two plugins (because all of them are trying to do the same thing). All these duplication of efforts needs to end. But, how?
Initially I thought about contributing to popup.nvim
or neovim-ui
project. But those projects have different goals than what I had in mind. What I want is to have a high-level API with a lot of sugar for doing repetitive things. Those projects are focused more on low-level stuffs.
So… Alright people let’s do this one last time! 🤟
Enter nui.nvim
The UI component library for Neovim that I’ve been working on for the past few weeks.
Features and Goals:
- Customizable UI
- High-level API to build UI elements
- Sugar for doing repetitive things, e.g. autocmd, keymap etc.
It’s still in early stage of development, but it’s quite usable right now. If you’re interested in using it, have ideas, feature requests, want to contribute or just appreciate the project — please do check the GitHub repo MunifTanjim/nui.nvim 😃
Now, it’s demo time…
UI for LSP Rename using nui.nvim
Let’s try to build the UI for LSP textDocument/rename
operation using nui.nvim
.
First of all, let’s get the identifier name under cursor:
local curr_name = vim.fn.expand("<cword>")
To send request to the LSP server for renaming this identifier, we need the exact position of it. Neovim got us covered:
local params = vim.lsp.util.make_position_params()
Now, what happens when we get the new name for the identifier? Let’s write a on_submit
callback function that is expected to be called with the new name:
local function on_submit(new_name)
if not new_name or #new_name == 0 or curr_name == new_name then
-- do nothing if `new_name` is empty or not changed.
return
end -- add `newName` property to `params`.
-- this is needed for making `textDocument/rename` request.
params.newName = new_name -- send the `textDocument/rename` request to LSP server
vim.lsp.buf_request(0, "textDocument/rename", params, function(_, result, _, _)
if not result then
-- do nothing if server returns empty result
return
end -- the `result` contains all the places we need to update the
-- name of the identifier. so we apply those edits.
vim.lsp.util.apply_workspace_edit(result) -- after the edits are applied, the files are not saved automatically.
-- let's remind ourselves to save those...
local total_files = vim.tbl_count(result.changes)
print(
string.format(
"Changed %s file%s. To save them run ':wa'",
total_files,
total_files > 1 and "s" or ""
)
)
end)
end
Now that all those are done, let’s focus on creating the UI for getting the new name for the identifier:
local Input = require("nui.input")local popup_options = {
-- border for the window
border = {
style = "rounded",
text = {
top = "[Rename]",
top_align = "left"
},
},
-- highlight for the window.
highlight = "Normal:Normal",
-- place the popup window relative to the
-- buffer position of the identifier
relative = {
type = "buf",
position = {
-- this is the same `params` we got earlier
row = params.position.line,
col = params.position.character,
}
},
-- position the popup window on the line below identifier
position = {
row = 1,
col = 0,
},
-- 25 cells wide, should be enough for most identifier names
size = {
width = 25,
height = 1,
},
}local input = Input(popup_options, {
-- set the default value to current name
default_value = curr_name,
-- pass the `on_submit` callback function we wrote earlier
on_submit = on_submit,
prompt = "",
})
Believe it or not, the UI is ready. Now we just need to render it:
input:mount()
That’s it! The input popup window will show up.
It’ll initially be in INSERT mode. You can press Esc
to exit INSERT mode or CTRL-c
to close the popup window. If you press Enter
, the on_submit
callback function will be invoked with the input value and the popup window will be closed.
Let’s add some additional interactions. If Esc
is pressed in NORMAL mode, let’s close the popup window:
-- close on <esc> in normal mode
input:map("n", "<esc>", input.input_props.on_close, { noremap = true })
Also close it when the cursor leaves the popup window:
local event = require("nui.utils.autocmd").event-- close when cursor leaves the buffer
input:on(event.BufLeave, input.input_props.on_close, { once = true })
Now all you need to do is put all these inside a function nui_lsp_rename
in a separate file and map your keys to start the renaming.
For example, put the file at ~/.config/nvim/lua/config/nui_lsp.lua
:
-- file: ~/.config/nvim/lua/config/nui_lsp.lualocal function nui_lsp_rename()
-- ... the implementation from above
endreturn {
lsp_rename = nui_lsp_rename,
}
And map your keys in your Neovim config file:
-- file: ~/.config/nvim/init.lua-- you probably want to put this inside the `on_attach` callback function
-- of your lsp server config and use `vim.api.nvim_buf_set_keymap` instead.
vim.api.nvim_set_keymap(
"n",
"<Leader>rn",
"<cmd>lua require('config.nui_lsp').lsp_rename()<CR>",
{ noremap = true, silent = true }
)
You can find the whole snippet here Gist: Neovim LSP Rename with nui.nvim.
That’s it for today!
If you have any queries, questions or thoughts on nui.nvim
, let's Discuss at GitHub.
Have a great day! 😄
Originally published at https://muniftanjim.dev on July 18, 2021.