Creating a Custom Command Bar in Neovim

Written — Updated

Command bars are a useful addition to many applications that let you search for and execute commands. Neovim's Telescope plugin provides the pieces to create your own with just a bit of Lua programming.

vim-command-bar.png

Telescope can be installed using any Neovim plugin manager, and then you're ready to dive in. Even if you haven't used Lua before, this is fairly straightforward.

First, we'll start by importing some modules.

local telescope = require 'telescope'
local pickers = require 'telescope.pickers'
local finders = require 'telescope.finders'

The main modules to be aware of here are pickers and finders. A picker is the actual Telescope window that pops up, and the finder provides the items to show in the picker.

Defining the Commands

Next, we'll define our commands. This may look a bit weird if you're not used to Lua, but objects and arrays are sort of the same thing here, so they both use braces. Lua calls them both "tables."

The actual structure of the commands can look however you want, since you'll convert them to the format that Telescope uses later. Here I have a name, a category, and a function to run when the command is selected, but you can add additional information or functions if you want.

local commands = {
  { 
    name = 'Organize imports', 
    category = "LS", 
    action = function() 
      vim.fn.CocActionAsync('runCommand', 'editor.action.organizeImport')
    end
  },
  { 
    name = 'Format document',
    category = "LS",
    action = function()
      vim.fn.CocActionAsync('runCommand', 'editor.action.formatDocument')
    end
  },
  { 
    name = 'Git Difftool',
    category = "Git", 
    action = function () vim.cmd('Git difftool') end
  },
}

Note that just using CocAction will cause the command to be run to completion before the picker closes. This can cause issues with certain commands and so CocActionAsync works much better.

Formatting

The picker in the screenshot above has two columns: the command's name and category. We can use the entry_display module to help with this formatting. It does not autosize columns though, so first we need to find our column widths.

This code snippet figures out the longest command name. (The # operator returns the length of a string.)

local longest_command_name = 0
for _, command in ipairs(commands) do
  if #command.name > longest_command_name then
    longest_command_name = #command.name
  end
end

Next, we can create our formatter.

local entry_display = require 'telescope.pickers.entry_display'
local displayer = entry_display.create {
  separator = " ",
  items = {
    { width = longest_command_name + 2 },
    -- The final column can be set to fill the remaining space
    { remaining = true }, 
  },
}

The Finder

With your commands set up, you can create a finder to hold the items. Telescope's finders support a lot of functionality, but here we just have a fixed set of commands in a table. The finders.new_table method creates a simple finder that can populate its results from this table.

local finder = finders.new_table {
  results = commands,
  entry_maker = function(entry)
    return {
      value = entry,
      display = function(ent)
        return displayer {
          ent.value.name,
          { ent.value.category, "TelescopeResultsComment" }
        }
      end,
      ordinal = entry.name,
    }
  end,
}
The entry_maker function above returns information for the picker to use.
  • The value can be any arbitrary data. In this case we just use the entry itself.
  • display is a function that formats the item. We use our displayer from above to format the name and the category, with a highlight group applied to the category.
  • ordinal is the input to the picker's sorting algorithm. Here we're just sorting by name.

Showing the Window

Now we're ready to actually show our picker.

local showCommandBar = function(opts)
  opts = opts or {}
  
  local conf = require 'telescope.config'.values
  local actions = require 'telescope.actions'
  local action_state = require 'telescope.actions.state'
  
  pickers.new(opts, {
    prompt_title = 'Common commands',
    finder = finder,
    -- Use the default sorter
    sorter = conf.generic_sorter(opts),
    attach_mappings = function(prompt_bufnr, map)
      map('i', '<CR>', function()
        actions.close(prompt_bufnr)
        local selection = action_state.get_selected_entry(prompt_bufnr)
        selection.value.action()
      end)
      return true
    end,
  }):find()

end

vim.keymap.set("n", "<leader>k". showCommandBar)

Here we use the default sorter from the Telescope configuration. The attach_mappings argument lets us add special actions into the picker, and we add one action so that the Enter key will close the picker, get the selected item, and run its action.

Finally, we created a key mapping that would run the function. And that's it!

Next Steps

We covered just the basics here, but there's a lot of opportunity for more advanced functionality.

By moving more of this code inside the showCommandBar function, you could customize the commands based on aspects of the current buffer, such as showing certain commands only for certain file types.

My current full implementation of this technique can be found on Github.

If you build any other cool functionality around this, I'd love to hear it!


Thanks for reading! If you have any questions or comments, please send me a note on Twitter.