Buffer name as ghost text in the top-right corner of every window. Persistent extmark refreshed via autocommands, so it survives terminal scroll optimisations without any forced redraws.
Most buffer-name overlays for Neovim (incline.nvim, dropbar, the various winbar configurations) place a floating window over the buffer. Floats are heavy: they leave artifacts at window borders, lag on scroll, fight with completion popups, and have to be torn down and rebuilt when windows change.
wisp.nvim uses a single right-aligned extmark per buffer, attached to the topmost visible line and refreshed via autocommands. The mark is part of the buffer itself, so terminal scroll regions carry it along naturally and the rendering survives across scrolls without any forced redraws.
- Active vs inactive highlight groups, switched automatically based on which window has focus.
- Path truncation that keeps the filename and the start of the path, eliding only the middle:
nvim/lua/scratch/plugins/lsp/lspconfig.luabecomesnvim/lua/scratch/…/lspconfig.luain a tight window.- Absolute paths keep their leading
/so/path/fileandpath/filestay distinct. - Width is bounded by a percentage of the window width or by an absolute column count.
- Cursor-aware placement. When the cursor sits on the topmost visible line, the ghost text shifts one line down so it does not cover the line you are editing.
- No floating windows, no forced redraws, no fight with terminal scroll optimisations.
The extmark is buffer-scoped. If the same buffer is shown in several windows at the same time, the active window's text and highlight win — the apply for the active window runs last and overwrites the per-buffer mark. The text appears in inactive windows too but with the active window's content and colour.
If you want truly per-window labels for shared buffers, you need a different rendering primitive (a floating window per window). Wisp deliberately trades that case for being lightweight and rock-solid on every other one.
- Neovim >= 0.10.
{
"reybits/wisp.nvim",
event = "VeryLazy",
opts = {
-- see "Configuration" below; defaults work out of the box.
},
}If you want the ghost text on the very first buffer (before VeryLazy fires), use lazy = false or event = "BufReadPre".
Defaults:
require("wisp").setup({
enabled = true,
max_width = 0.5, -- <= 1.0: fraction of window width; > 1: absolute columns.
prefix = " ", -- string prepended to the rendered name.
suffix = " ", -- string appended to the rendered name.
hl_active = "WispActive", -- highlight group for the focused window.
hl_inactive = "WispInactive",-- highlight group for unfocused windows.
skip_filetypes = { -- filetypes for which nothing is rendered.
NeogitLogView = true,
NeogitStatus = true,
TelescopePrompt = true,
checkhealth = true,
dashboard = true,
help = true,
lazy = true,
lspinfo = true,
mason = true,
oil = true,
qf = true,
query = true,
startuptime = true,
trouble = true,
},
should_show = nil, -- fun(buf): boolean. Extra per-buffer predicate.
format = nil, -- fun(parts, is_abs, limit): string. Override the truncation.
})Set vim.b.wisp_disable = true on a buffer to hide the ghost text for it. Useful for scratch flows where you do not want the overlay.
WispActive links to Comment and WispInactive links to NonText by default. Override either:
vim.api.nvim_set_hl(0, "WispActive", { fg = "#a0a0a0" })
vim.api.nvim_set_hl(0, "WispInactive", { fg = "#404040" })If you want a different rendering (basename only, icons, project-relative prefixes), supply format:
require("wisp").setup({
format = function(parts, is_abs, limit)
-- Just the basename, with a leading "/" for absolute paths.
local name = parts[#parts]
return is_abs and ("/" .. name) or name
end,
})parts is a list of path components (basename last), is_abs is true when the path starts with /, limit is the byte budget for the body (prefix and suffix sit outside it). Return the string to render.
should_show runs after the built-in checks (buftype, filetype, empty name, vim.b.wisp_disable) and lets you opt out further. Return false to suppress rendering, anything else to allow.
require("wisp").setup({
should_show = function(buf)
return vim.api.nvim_buf_line_count(buf) > 10
end,
})require("wisp").toggle() -- flip on/off
require("wisp").toggle(false) -- explicit state:checkhealth wispVerifies Neovim version, the configured highlight groups, the enabled state, and the basic config sanity (positive max_width, with note whether it is interpreted as a fraction or as absolute columns).
- On require the plugin installs
WispActiveandWispInactivehighlight groups (default = true, so colorscheme overrides take precedence) and registers a single autocmd group. - Events that can affect the rendering (window focus changes, scroll, resize, cursor movement, buffer switches, directory change, filetype detection) all enqueue a refresh through
vim.schedule. Bursts in the same tick collapse to one refresh. - The refresh clears the namespace from every buffer, then iterates visible windows: inactive ones first, the active one last. Each window writes a right-aligned
virt_textextmark withid = 1on the topmost visible line of its buffer (shifted down one row when the cursor sits on the top line). - Because the extmark is attached to a buffer line, terminal scroll regions move it together with the line. There is no decoration provider involved and no need to force a redraw.
MIT