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.lua

local function nui_lsp_rename()
  -- ... the implementation from above
end

return {
  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 muniftanjim.dev on July 18, 2021.

22