Neovim - Build UI using nui.nvim
Tags: neovim, lua, ui, vim
Published: 2021-07-20 09:28:33

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`](https://github.com/neoclide/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:

- [`glepnir/lspsaga.nvim`](https://github.com/glepnir/lspsaga.nvim)
- [`RishabhRD/nvim-lsputils`](https://github.com/RishabhRD/nvim-lsputils)

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! :stuck_out_tongue_closed_eyes:

Instead I thought, what if I try to make those UIs myself? So the next logical (:thinking:) thing was to search for plugin/library for building Neovim UIs quickly. Here's what I found so far:

- [`nvim-lua/popup.nvim`](https://github.com/nvim-lua/popup.nvim)
- [`mjlbach/neovim-ui`](https://github.com/mjlbach/neovim-ui)

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!_ :love_you_gesture:

## Enter [**`nui.nvim`**](https://github.com/MunifTanjim/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](https://github.com/MunifTanjim/nui.nvim) :smiley:

Now, it's demo time...

## UI for LSP Rename using nui.nvim

Let's try to build the UI for LSP [`textDocument/rename`](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_rename) operation using `nui.nvim`.

First of all, let's get the identifier name under cursor:

```lua
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:

```lua
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:

```lua
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:

```lua
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:

```lua
input:mount()
```

That's it! The input popup window will show up.

![UI for LSP Rename using nui.nvim](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6638u3pz5i2ped7tt5gu.png)

It'll initially be in INSERT mode. You can press <kbd>Esc</kbd> to exit INSERT mode or <kbd>CTRL-c</kbd> to close the popup window. If you press <kbd>Enter</kbd>, 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 <kbd>Esc</kbd> is pressed in NORMAL mode, let's close the popup window:

```lua
-- 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:

```lua
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`:

```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:

```lua
-- 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](https://gist.github.com/MunifTanjim/8d9498c096719bdf4234321230fe3dc7).

That's it for today!

If you have any queries, questions or thoughts on `nui.nvim`, let's  [Discuss at GitHub](https://github.com/MunifTanjim/nui.nvim/discussions).

Have a great day! :smile:

---

Originally published at <a href="https://muniftanjim.dev/blog/neovim-build-ui-using-nui-nvim/" rel="canonical">muniftanjim.dev</a> on July 18, 2021.