Experimental multibuffers API for neovim. Expect breaking changes and some instability. This repository aims to be strictly an API for creating and managing multibuffers. What is a multibuffer? A multibuffer is a single buffer that contains editable regions of other buffers.
Strategy:
multibufferfiletype assigned to all multibuffers- reads and writes handled through
BufReadCmdandBufWriteCmd - customizable virtual text above each section in multibuffer
- line numbers of each region through signcolumn
- live highlight projection from source buffers (1:1 Treesitter/LSP parity)
This plugin comes with no defaults. You create multibuffers using the provided API and manage their appearance and behavior using standard Neovim features like autocommands and ftplugins.
Using lazy.nvim:
{
"zaucy/multibuffer.nvim",
opts = {},
}Since multibuffers are assigned the multibuffer filetype, you can add keymaps using a FileType autocommand. This ensures they are set only once per buffer.
vim.api.nvim_create_autocmd("FileType", {
pattern = "multibuffer",
callback = function(args)
-- Navigation: <CR> to jump to source line
vim.keymap.set("n", "<cr>", function()
local api = require('multibuffer')
local buf, line = api.multibuf_get_origin_at_cursor(0)
if buf then
vim.api.nvim_set_current_buf(buf)
vim.api.nvim_win_set_cursor(0, { line, 0 })
end
end, { buffer = args.buf, desc = "Jump to source" })
end,
})Alternatively, you can place your configuration in after/ftplugin/multibuffer.lua.
Disable standard line numbers to avoid confusion with the multibuffer's own line numbers in the signcolumn:
-- using BufWinEnter so that if the multibuf window changes to a different buffer it will reset
-- NOTE: this assumes you're setting your window options in another autocmd
vim.api.nvim_create_autocmd("BufWinEnter", {
callback = function(args)
if vim.bo[args.buf].filetype ~= "multibuffer" then
return
end
local winid = vim.api.nvim_get_current_win()
vim.api.nvim_set_option_value("number", false, { scope = "local", win = winid })
vim.api.nvim_set_option_value("relativenumber", false, { scope = "local", win = winid })
-- Ensure enough room for multibuf line numbers (e.g. 4 digits)
vim.api.nvim_set_option_value("signcolumn", "yes:3", { scope = "local", win = winid })
end,
})
vim.api.nvim_create_autocmd({ "BufEnter", "BufNew", "BufWinEnter", "TermOpen" }, {
callback = function()
if vim.bo.filetype == "multibuffer" then return end
-- restore your window options
end,
})In setup options you may pass a function called render_multibuf_title that returns a list to be rendered in the virtual text. Here is an example that uses nvim-web-devicons and a rounded border.
-- setup options
{
render_multibuf_title = function(bufnr)
local icons = require("nvim-web-devicons")
local buf_name = vim.api.nvim_buf_get_name(bufnr)
local icon, icon_hl_group = icons.get_icon(buf_name)
local nice_buf_name = vim.fn.fnamemodify(buf_name, ":~:.")
nice_buf_name = string.gsub(nice_buf_name, "\\", "/")
icon = icon or ""
icon_hl_group = icon_hl_group or "DevIconDefault"
local title = { { " " }, { icon, icon_hl_group }, { " ", "" }, { nice_buf_name, "MultibufferTitleName" }, { " " } }
local title_text_length = 0
for _, part in ipairs(title) do
title_text_length = title_text_length + string.len(part[1])
end
local top_text = "╭" .. string.rep("─", title_text_length - 2) .. "╮"
local bottom_text = "╰" .. string.rep("─", title_text_length - 2) .. "╯"
table.insert(title, 1, { "│", "MultibufferTitleBorder" })
table.insert(title, { "│", "MultibufferTitleBorder" })
return {
{ { top_text, "MultibufferTitleBorder" } },
title,
{ { bottom_text, "MultibufferTitleBorder" } },
}
end,
}Create a multibuffer from all results in a Telescope picker:
-- Inside your telescope setup mappings
mappings = {
i = {
["<C-a>"] = function(prompt_bufnr)
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
local multibuffer = require("multibuffer")
local picker = action_state.get_current_picker(prompt_bufnr)
local selections = {}
for entry in picker.manager:iter() do
local bufnr = entry.bufnr
if bufnr == nil then
bufnr = vim.fn.bufadd(entry.filename)
vim.fn.bufload(bufnr)
end
table.insert(selections, {
filename = entry.filename,
bufnr = bufnr,
start_row = (entry.lnum or 1) - 1,
})
end
actions.close(prompt_bufnr)
local multibuf = multibuffer.create_multibuf()
--- @type table<number, MultibufAddBufOptions>
local add_opts_by_buf = {}
for _, selection in ipairs(selections) do
if add_opts_by_buf[selection.bufnr] == nil then
add_opts_by_buf[selection.bufnr] = {
buf = selection.bufnr,
regions = {},
}
end
table.insert(add_opts_by_buf[selection.bufnr].regions, {
start_row = selection.start_row - telescope_multibuffer_expand,
end_row = selection.start_row + telescope_multibuffer_expand,
})
end
--- @type MultibufAddBufOptions[]
local add_buf_opts = {}
for _, add_opts in pairs(add_opts_by_buf) do
table.insert(add_buf_opts, add_opts)
end
multibuffer.multibuf_add_bufs(multibuf, add_buf_opts)
multibuffer.win_set_multibuf(0, multibuf)
end,
},
},