Niza Toshpulatov

April 22, 2024

Delete All Buffers Except The Current One In NeoVim

Currently, I'm using NeoVim as my primary code editor. Best part of the editor is its endless extensibility. You're encouraged to make it Your editor, tailor it to your specific needs and preferences. NeoVim has an excellent API for achieving virtually any kind of feature you could think of.

Recently, I was having trouble keeping track of my open buffers while working on my personal website. Normally this isn’t an issue, but I was renaming and moving lots of files and some of the buffers were pointing to non-existent paths. At that moment, I wished that I had a key combination or a command to delete all but the current buffer. So, naturally, I did a quick Google search to see if there was an existing solution for my problem.

Doing research revealed that there are some plugins and certain built-in commands that achieved the behavior I was looking for. But I didn’t like any of them. To be fair, I didn’t check any packages that were suggested by people on forums and Reddit, because I’m currently driving this mantra of “no plugins unless absolutely necessary”. And the built-in commands seemed very confusing and limited. To make it a bit more interesting, I decided to write my own custom command!

So, I would like to take you into this small adventure where most of the job is done by Copilot but it's still fun, I promise!

To achieve our goal, we can use NeoVim’s nvim_create_user_command API:

vim.api.nvim_create_user_command(
  "CommandName",
  function()
    -- implementation...
  end,
  {
    nargs = "?",
    desc = "Command description",
  }
)

Note: the command name must be an uppercased single word so it doesn’t conflict with NeoVim’s built-in commands, otherwise you will get an error when starting the editor.

Very straightforward, so let’s define the behavior that we want:
  • Get the list of open buffers.
  • Iterate through them, skip if we match the current buffer.
  • Check if buffer has unsaved changes:
    • If so, prompt on what to do with the buffer:
      • Write and delete
      • Cancel
      • Skip
      • Force delete
  • Delete the buffer if not canceled or skipped in the previous step.
  • Print the message with the amount of deleted buffers.

First, let’s define the command:

vim.api.nvim_create_user_command(
  "DeleteOtherBuffers",
  delete_other_buffers,
  {
    nargs = "?",
    desc = "Delete all buffers except the current one",
    complete = function()
      return { "force" }
    end
  }
)

As you can see, we’re defining a custom user command called DeleteOtherBuffers that accepts 0 or 1 argument which has an autocompletion of force, which indicates whether we want to force delete all of our open buffers.

Now, let’s define the delete_other_buffers function. Let’s start by parsing our arguments to see if we have the force parameter set:

function delete_other_buffers(opts)
  local initial_force = opts.fargs[1] == "force"
  local force = initial_force

We will use the initial_force variable to reset the force option later in the loop.

After that, we can get the current buffer:

  local current_buf = vim.api.nvim_get_current_buf()
  
Let’s initialize the count variable and start iterating over the list of open buffers:

  local count = 0
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do

As mentioned in our spec, we want to skip the buffer if it matches the current one:

    if buf == current_buf then
      goto continue
    end

We will define the continue label at the end of our loop. Now, let’s get the name of the current buffer and see if it has been modified:

    local bufname = vim.api.nvim_buf_get_name(buf)
    local unsaved_changes = vim.api.nvim_buf_get_option(buf, "modified")

If we do have some changes that aren’t written and we don’t have the force option set, we can summon a prompt with possible actions per our spec:  

    if unsaved_changes and not force then
      local choice = vim.fn.confirm(
        "Buffer " .. bufname .. " has unsaved changes, what would you like to do?",
        "&Write and delete\n&Cancel\n&Skip\n&Force delete",
        4
      )

We act based on the choice. If "Write and delete” option is chosen, we switch to the target buffer, write it, and switch back to the current buffer:

      if choice == 1 then
        vim.api.nvim_command("buffer " .. buf)
        vim.api.nvim_command("write")
        vim.api.nvim_command("buffer " .. current_buf)

In case “Cancel” is chosen, we just return from our function. This will cancel the operation altogether, however, any buffers that were written before the cancellation, will not return to their previous state:

      elseif choice == 2 then
        return

If “Skip” is chosen, we just go to the next buffer:

      elseif choice == 3 then
        goto continue

And finally, if “Force delete” is chosen, we simply set the force option to be true:

      elseif choice == 4 then
        force = true
      end
    end


With all possible cases resolved, we proceed to the actual deletion. Obviously, these lines of code will not be reached if “Skip” or “Cancel” options are chosen.
 
    vim.api.nvim_buf_delete(buf, { force = force })
    count = count + 1

    force = initial_force
    ::continue::
  end

Here, we are also resetting the force option so the choice we made in the current iteration doesn't affect the next one.

As the finishing touch, we can print the amount of deleted buffers and end our function: 

  print("Deleted " .. count .. " buffers.")
end

Here is the full, uninterrupted code:

function delete_other_buffers(opts)
  local initial_force = opts.fargs[1] == "force"
  local force = initial_force
  local current_buf = vim.api.nvim_get_current_buf()

  local count = 0
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do
    if buf == current_buf then
      goto continue
    end

    local bufname = vim.api.nvim_buf_get_name(buf)
    local unsaved_changes = vim.api.nvim_buf_get_option(buf, "modified")

    if unsaved_changes and not force then
      local choice = vim.fn.confirm(
        "Buffer " .. bufname .. " has unsaved changes, what would you like to do?",
        "&Write and delete\n&Cancel\n&Skip\n&Force delete",
        4
      )

      if choice == 1 then
        vim.api.nvim_command("buffer " .. buf)
        vim.api.nvim_command("write")
        vim.api.nvim_command("buffer " .. current_buf)
      elseif choice == 2 then
        return
      elseif choice == 3 then
        goto continue
      elseif choice == 4 then
        force = true
      end
    end

    vim.api.nvim_buf_delete(buf, { force = force })
    count = count + 1
    
    force = initial_force

    ::continue::
  end

  print("Deleted " .. count .. " buffers.")
end

I haven’t tested it for more than a couple of runs, but the core logic seems to be correct. The function might be a bit slow if you have hundreds of buffers open, so a coroutine optimization might be considered? Not sure. But for my basic use-case, this is more than sufficient.

This code and the rest of my NeoVim config is available on my GitHub. Check it out if you’re interested.

Niza ✌️

About Niza Toshpulatov

Hi! I'm Niza, a web developer, productivity enthusiast, and a tech nerd. I love tinkering with software and in my free time, I like to compose music and write silly poems. Check out my website if you want to learn more about my work.