A polished, IDE-like, highly-customizable winbar for Neovim with drop-down menus and multiple backends.
For more information see :h dropbar
.
https://github.com/Bekaboo/dropbar.nvim/assets/76579810/e8c1ac26-0321-4762-9975-b20fc3098c5a
dropbar.nvim
comes with five builtin sources:dropbar.nvim
does not require nvim-lspconfig, nvim-treesitter
or any third-party UI libraries to work.
As long as the language server or the treesitter parser is installed,
it should work just fine.
Optionally, you can install telescope-fzf-native
to add fuzzy search support to dropbar menus.:h mousemoveevent
to be enabled.Using lazy.nvim
require('lazy').setup({
{
'Bekaboo/dropbar.nvim',
-- optional, but required for fuzzy finder support
dependencies = {
'nvim-telescope/telescope-fzf-native.nvim',
build = 'make'
},
config = function()
local dropbar_api = require('dropbar.api')
vim.keymap.set('n', '<Leader>;', dropbar_api.pick, { desc = 'Pick symbols in winbar' })
vim.keymap.set('n', '[;', dropbar_api.goto_context_start, { desc = 'Go to start of current context' })
vim.keymap.set('n', '];', dropbar_api.select_next_context, { desc = 'Select next context' })
end
}
})
Using packer.nvim
require('packer').startup(function(use)
use({
'Bekaboo/dropbar.nvim',
requires = {
'nvim-telescope/telescope-fzf-native.nvim',
run = 'make'
},
config = function ()
local dropbar_api = require('dropbar.api')
vim.keymap.set('n', '<Leader>;', dropbar_api.pick, { desc = 'Pick symbols in winbar' })
vim.keymap.set('n', '[;', dropbar_api.goto_context_start, { desc = 'Go to start of current context' })
vim.keymap.set('n', '];', dropbar_api.select_next_context, { desc = 'Select next context' })
end
})
end)
Using native package manager
mkdir -p ~/.local/share/nvim/site/pack/packages/
git clone https://github.com/Bekaboo/dropbar.nvim ~/.local/share/nvim/site/pack/packages/start/dropbar.nvim
Lazy-loading is unneeded as it is already done in plugin/dropbar.lua.
require('dropbar.api').pick()
to enter interactive pick mode or
require('dropbar.api').pick(<idx>)
to directly select a component at
idx
.dropbar_menu_t:fuzzy_find_open()
to interactively
filter, select and preview entries using fzf<Esc>
: exit fzf mode<Up>/<Down>
: move the cursor in fzf mode<CR>
: call the on_click callback of the symbol under the cursor<LeftMouse>
: call the on_click
callback of the symbol at the mouse
click<CR>
: find the first clickable symbol in the current drop-down menu
entry and call its on_click
callbacki
: enter fzf mode from the menuq
/ <Esc>
: close current menuvim.ui.select
Dropbar can be used as a drop-in replacement for Neovim's builtin vim.ui.select
menu.
To enable this functionality, simply replace vim.ui.select
with dropbar.utils.menu.select
:
vim.ui.select = require('dropbar.utils.menu').select
For all available options and their default values, see lua/dropbar/configs.lua.
Below are the detailed explanation of the options.
These options live under opts.bar
and are used to control the behavior of the
winbar:
opts.bar.enable
: boolean|fun(buf: integer?, win: integer?, info: table?): boolean
function(buf, win, _)
if
not vim.api.nvim_buf_is_valid(buf)
or not vim.api.nvim_win_is_valid(win)
or vim.fn.win_gettype(win) ~= ''
or vim.wo[win].winbar ~= ''
or vim.bo[buf].ft == 'help'
then
return false
end
local stat = vim.uv.fs_stat(vim.api.nvim_buf_get_name(buf))
if stat and stat.size > 1024 * 1024 then
return false
end
return vim.bo[buf].ft == 'markdown'
or pcall(vim.treesitter.get_parser, buf)
or not vim.tbl_isempty(vim.lsp.get_clients({
bufnr = buf,
method = 'textDocument/documentSymbol',
}))
end,
opts.bar.attach_events
: string[]
enable()
function and attach the plugin
to corresponding buffer or window{
'OptionSet',
'BufWinEnter',
'BufWritePost',
}
opts.bar.update_debounce
: number
'j'
) to scroll the window1000 / key_repeat_rate
32
opts.bar.update_events.win
: string[]
{
'CursorMoved',
'WinEnter',
'WinResized',
}
opts.bar.update_events.buf
: string[]
{
'BufModifiedSet',
'FileChangedShellPost',
'TextChanged',
'ModeChanged',
}
opts.bar.update_events.global
: string[]
{
'DirChanged',
'VimResized',
}
opts.bar.hover
: boolean
'mousemoveevent'
to be enabledtrue
opts.bar.sources
: dropbar_source_t[]|fun(buf: integer, win: integer): dropbar_source_t[]
function(buf, _)
local sources = require('dropbar.sources')
local utils = require('dropbar.utils')
if vim.bo[buf].ft == 'markdown' then
return {
sources.path,
sources.markdown,
}
end
if vim.bo[buf].buftype == 'terminal' then
return {
sources.terminal,
}
end
return {
sources.path,
utils.source.fallback({
sources.lsp,
sources.treesitter,
}),
}
end
dropbar_source_t
.opts.bar.padding
: { left: number, right: number }
{ left = 1, right = 1 }
opts.bar.pick.pivots
: string
'abcdefghijklmnopqrstuvwxyz'
opts.bar.truncate
: boolean
true
These options live under opts.menu
and are used to control the behavior of the
menu:
opts.menu.quick_navigation
: boolean
CursorMoved
true
opts.menu.entry.padding
: { left: number, right: number }
{ left = 1, right = 1 }
opts.menu.preview
: boolean
true
opts.menu.hover
: boolean
'mousemoveevent'
to be enabledtrue
opts.menu.keymaps
: table<string, function|string|table<string, function>|table<string, string>>
<key> = <function|string>
to map a key in normal mode in the menu
buffer, or use <key> = table<mode, function|string>
to map
a key in specific modes.{
['q'] = '<C-w>q',
['<Esc>'] = '<C-w>q',
['<LeftMouse>'] = function()
local menu = utils.menu.get_current()
if not menu then
return
end
local mouse = vim.fn.getmousepos()
local clicked_menu = utils.menu.get({ win = mouse.winid })
-- If clicked on a menu, invoke the corresponding click action,
-- else close all menus and set the cursor to the clicked window
if clicked_menu then
clicked_menu:click_at({ mouse.line, mouse.column - 1 }, nil, 1, 'l')
return
end
utils.menu.exec('close')
utils.bar.exec('update_current_context_hl')
if vim.api.nvim_win_is_valid(mouse.winid) then
vim.api.nvim_set_current_win(mouse.winid)
end
end,
['<CR>'] = function()
local menu = utils.menu.get_current()
if not menu then
return
end
local cursor = vim.api.nvim_win_get_cursor(menu.win)
local component = menu.entries[cursor[1]]:first_clickable(cursor[2])
if component then
menu:click_on(component, nil, 1, 'l')
end
end,
['<MouseMove>'] = function()
local menu = utils.menu.get_current()
if not menu then
return
end
local mouse = vim.fn.getmousepos()
if M.opts.menu.hover then
utils.menu.update_hover_hl(mouse)
end
if M.opts.menu.preview then
utils.menu.update_preview(mouse)
end
end,
['i'] = function()
local menu = utils.menu.get_current()
if not menu then
return
end
menu:fuzzy_find_open()
end,
},
opts.menu.scrollbar
: table<string, boolean>
{
enable = true,
-- if false, only the scrollbar thumb will be shown
background = true
}
opts.menu.win_configs
: table<string, dropbar_menu_win_config_opts_t>
:h nvim_open_win()
opts.menu.win_configs
accepts either a plain value
which will be passes directly to nvim_open_win()
, or a function that
takes the current menu (see dropbar_menu_t
) as an
argument and returns a value to be passed to nvim_open_win()
.{
style = 'minimal',
row = function(menu)
return menu.prev_menu
and menu.prev_menu.clicked_at
and menu.prev_menu.clicked_at[1] - vim.fn.line('w0')
or 0
end,
---@param menu dropbar_menu_t
col = function(menu)
if menu.prev_menu then
return menu.prev_menu._win_configs.width
+ (menu.prev_menu.scrollbar and 1 or 0)
end
local mouse = vim.fn.getmousepos()
local bar = require('dropbar.api').get_dropbar(
vim.api.nvim_win_get_buf(menu.prev_win),
menu.prev_win
)
if not bar then
return mouse.wincol
end
local _, range = bar:get_component_at(math.max(0, mouse.wincol - 1))
return range and range.start or mouse.wincol
end,
relative = 'win',
win = function(menu)
return menu.prev_menu and menu.prev_menu.win
or vim.fn.getmousepos().winid
end,
height = function(menu)
return math.max(
1,
math.min(
#menu.entries,
vim.go.pumheight ~= 0 and vim.go.pumheight
or math.ceil(vim.go.lines / 4)
)
)
end,
width = function(menu)
local min_width = vim.go.pumwidth ~= 0 and vim.go.pumwidth or 8
if vim.tbl_isempty(menu.entries) then
return min_width
end
return math.max(
min_width,
math.max(unpack(vim.tbl_map(function(entry)
return entry:displaywidth()
end, menu.entries)))
)
end,
zindex = function(menu)
if not menu.prev_menu then
return
end
return menu.prev_menu.scrollbar
and menu.prev_menu.scrollbar.thumb
and vim.api.nvim_win_get_config(menu.prev_menu.scrollbar.thumb).zindex
or vim.api.nvim_win_get_config(menu.prev_win).zindex
end,
}
These options live under opts.fzf
and are used to control the behavior and
appearance of the fuzzy finder interface.
opts.fzf.keymaps
keymaps = {
['<LeftMouse>'] = function()
---@type dropbar_menu_t
local menu = utils.menu.get_current()
if not menu then
return
end
local mouse = vim.fn.getmousepos()
if not mouse then
return
end
if mouse.winid ~= menu.win then
local default_func = M.opts.menu.keymaps['<LeftMouse>']
if type(default_func) == 'function' then
default_func()
end
menu:fuzzy_find_close(false)
return
elseif mouse.winrow > vim.api.nvim_buf_line_count(menu.buf) then
return
end
vim.api.nvim_win_set_cursor(menu.win, { mouse.line, mouse.column - 1 })
menu:fuzzy_find_click_on_entry(function(entry)
return entry:get_component_at(mouse.column - 1, true)
end)
end,
['<MouseMove>'] = function()
---@type dropbar_menu_t
local menu = utils.menu.get_current()
if not menu then
return
end
local mouse = vim.fn.getmousepos()
if not mouse then
return
end
-- If mouse is not in the menu window or on the border, end preview
-- and clear hover highlights
if
mouse.winid ~= menu.win
or mouse.line <= 0
or mouse.column <= 0
or mouse.winrow > #menu.entries
then
menu = menu:root() --[[@as dropbar_menu_t]]
if menu then
menu:finish_preview(true)
if M.opts.menu.hover then
menu:update_hover_hl()
end
end
return
end
if M.opts.menu.preview then
menu:preview_symbol_at({ mouse.line, mouse.column - 1 }, true)
end
if M.opts.menu.hover then
menu:update_hover_hl({ mouse.line, mouse.column - 1 })
end
end,
['<Up>'] = api.fuzzy_find_prev,
['<Down>'] = api.fuzzy_find_next,
['<C-k>'] = api.fuzzy_find_prev,
['<C-j>'] = api.fuzzy_find_next,
['<C-p>'] = api.fuzzy_find_prev,
['<C-n>'] = api.fuzzy_find_next,
['<CR>'] = api.fuzzy_find_click,
['<S-Enter>'] = function()
api.fuzzy_find_click(-1)
end,
}
opts.fzf.win_configs
:h nvim_open_win
. The fuzzy finder will use its
parent window's config by default, but options set here will override those.win_configs = {
relative = 'win',
anchor = 'NW',
height = 1,
win = function(menu)
return menu.win
end,
width = function(menu)
local function border_width(border)
if not border then
border = vim.go.winborder
end
if type(border) == 'string' then
if border == '' or border == 'none' or border == 'shadow' then
return 0
end
return 2 -- left and right border
end
local left, right = 1, 1
if
(#border == 1 and border[1] == '')
or (#border == 4 and border[4] == '')
or (#border == 8 and border[8] == '')
then
left = 0
end
if
(#border == 1 and border[1] == '')
or (#border == 4 and border[4] == '')
or (#border == 8 and border[4] == '')
then
right = 0
end
return left + right
end
local menu_width = menu._win_configs.width
+ border_width(menu._win_configs.border)
local self_width = menu._win_configs.width
local self_border = border_width(
(
M.opts.fzf.win_configs
and M.eval(M.opts.fzf.win_configs.border, menu)
)
or (menu.fzf_win_configs and M.eval(
menu.fzf_win_configs.border,
menu
))
or menu._win_configs.border
)
if self_width + self_border > menu_width then
return self_width - self_border
else
return menu_width - self_border
end
end,
row = function(menu)
local menu_border = menu._win_configs.border or vim.go.border
if
type(menu_border) == 'string'
and menu_border ~= 'shadow'
and menu_border ~= 'none'
and menu_border ~= ''
then
return menu._win_configs.height + 1
elseif menu_border == 'none' or menu_border == '' then
return menu._win_configs.height
end
local len_menu_border = #menu_border
if
len_menu_border == 1 and menu_border[1] ~= ''
or (len_menu_border == 2 or len_menu_border == 4) and menu_border[2] ~= ''
or len_menu_border == 8 and menu_border[8] ~= ''
then
return menu._win_configs.height + 1
else
return menu._win_configs.height
end
end,
col = function(menu)
local menu_border = menu._win_configs.border or vim.go.border
if
type(menu_border) == 'string'
and menu_border ~= 'shadow'
and menu_border ~= 'none'
and menu_border ~= ''
then
return -1
end
if
type(menu_border) == 'table' and menu_border[#menu_border] ~= ''
then
return -1
end
return 0
end,
},
opts.fzf.prompt
prompt = '%#htmlTag# '
opts.fzf.char_pattern
char_pattern = '[%w%p]'
opts.fzf.retain_inner_spaces
retain_inner_spaces = true
opts.fzf.fuzzy_find_on_click
fuzzy_find_on_click = true
These options live under opts.icons
and are used to configure the icons
used by the plugin:
opts.icons.enable
: boolean
true
opts.icons.kinds.dir_icon
: fun(path: string): string, string?|string?
function(_)
return M.opts.icons.kinds.symbols.Folder, 'DropBarIconKindFolder'
end
opts.icons.kinds.file_icon
: fun(path: string): string, string?|string?
function(path)
return M.opts.icons.kinds.symbols.File, 'DropBarIconKindFile'
end
opts.icons.kinds.symbols
: table<string, string>
{
Array = ' ',
BlockMappingPair = ' ',
Boolean = ' ',
BreakStatement = ' ',
Call = ' ',
CaseStatement = ' ',
Class = ' ',
Color = ' ',
Constant = ' ',
Constructor = ' ',
ContinueStatement = '→ ',
Copilot = ' ',
Declaration = ' ',
Delete = ' ',
DoStatement = ' ',
Element = ' ',
Enum = ' ',
EnumMember = ' ',
Event = ' ',
Field = ' ',
File = ' ',
Folder = ' ',
ForStatement = ' ',
Function = ' ',
GotoStatement = ' ',
Identifier = ' ',
IfStatement = ' ',
Interface = ' ',
Keyword = ' ',
List = ' ',
Log = ' ',
Lsp = ' ',
Macro = ' ',
MarkdownH1 = ' ',
MarkdownH2 = ' ',
MarkdownH3 = ' ',
MarkdownH4 = ' ',
MarkdownH5 = ' ',
MarkdownH6 = ' ',
Method = ' ',
Module = ' ',
Namespace = ' ',
Null = ' ',
Number = ' ',
Object = ' ',
Operator = ' ',
Package = ' ',
Pair = ' ',
Property = ' ',
Reference = ' ',
Regex = ' ',
Repeat = ' ',
Return = ' ',
RuleSet = ' ',
Scope = ' ',
Section = ' ',
Snippet = ' ',
Specifier = ' ',
Statement = ' ',
String = ' ',
Struct = ' ',
SwitchStatement = ' ',
Table = ' ',
Terminal = ' ',
Text = ' ',
Type = ' ',
TypeParameter = ' ',
Unit = ' ',
Value = ' ',
Variable = ' ',
WhileStatement = ' ',
}
opts.icons.ui.bar
: table<string, string>
{
separator = ' ',
extends = '…',
}
opts.icons.ui.menu
: table<string, string>
{
separator = ' ',
indicator = ' ',
}
These options live under opts.symbol
and are used to control the behavior of
the symbols:
opts.symbol.on_click()
: fun(symbol: dropbar_symbol_t, min_width: integer?, n_clicks: integer?, button: string?, modifiers: string?)|false?
<CR>
on the symbolfunction(symbol)
-- Update current context highlights if the symbol
-- is shown inside a menu
if symbol.entry and symbol.entry.menu then
symbol.entry.menu:update_current_context_hl(symbol.entry.idx)
elseif symbol.bar then
symbol.bar:update_current_context_hl(symbol.bar_idx)
end
-- Determine menu configs
local prev_win = nil ---@type integer?
local entries_source = nil ---@type dropbar_symbol_t[]?
local init_cursor = nil ---@type integer[]?
local win_configs = {}
if symbol.bar then -- If symbol inside a dropbar
prev_win = symbol.bar.win
entries_source = symbol.opts.siblings
init_cursor = symbol.opts.sibling_idx
and { symbol.opts.sibling_idx, 0 }
if symbol.bar.in_pick_mode then
---@param tbl number[]
local function tbl_sum(tbl)
local sum = 0
for _, v in ipairs(tbl) do
sum = sum + v
end
return sum
end
win_configs.relative = 'win'
win_configs.win = vim.api.nvim_get_current_win()
win_configs.row = 0
win_configs.col = symbol.bar.padding.left
+ tbl_sum(vim.tbl_map(
function(component)
return component:displaywidth()
+ symbol.bar.separator:displaywidth()
end,
vim.tbl_filter(function(component)
return component.bar_idx < symbol.bar_idx
end, symbol.bar.components)
))
end
elseif symbol.entry and symbol.entry.menu then -- If inside a menu
prev_win = symbol.entry.menu.win
entries_source = symbol.opts.children
end
-- Toggle existing menu
if symbol.menu then
symbol.menu:toggle({
prev_win = prev_win,
win_configs = win_configs,
})
return
end
-- Create a new menu for the symbol
if not entries_source or vim.tbl_isempty(entries_source) then
return
end
local menu = require('dropbar.menu')
local configs = require('dropbar.configs')
symbol.menu = menu.dropbar_menu_t:new({
prev_win = prev_win,
cursor = init_cursor,
win_configs = win_configs,
---@param sym dropbar_symbol_t
entries = vim.tbl_map(function(sym)
local menu_indicator_icon = configs.opts.icons.ui.menu.indicator
local menu_indicator_on_click = nil
if not sym.children or vim.tbl_isempty(sym.children) then
menu_indicator_icon =
string.rep(' ', vim.fn.strdisplaywidth(menu_indicator_icon))
menu_indicator_on_click = false
end
return menu.dropbar_menu_entry_t:new({
components = {
sym:merge({
name = '',
icon = menu_indicator_icon,
icon_hl = 'dropbarIconUIIndicator',
on_click = menu_indicator_on_click,
}),
sym:merge({
on_click = function()
local root_menu = symbol.menu and symbol.menu:root()
if root_menu then
root_menu:close(false)
end
if current_menu then
current_menu:close(false)
end
sym:jump()
end,
}),
},
})
end, entries_source),
})
symbol.menu:toggle()
end,
opts.symbol.preview.reorient
: fun(win: integer, range: {start: {line: integer, character: integer}, end: {line: integer, character: integer}})
win
and the range of the symbol range
function() end
opts.symbol.jump.reorient
: fun(win: integer, range: {start: {line: integer, character: integer}, end: {line: integer, character: integer}})
win
and the range of the symbol range
function() end
These options live under opts.sources
and are used to control the behavior of
each sources.
opts.sources.path.max_depth
: integer
16
opts.sources.path.relative_to
: string|fun(buf: integer, win: integer): string
..
relative pathsfunction(_, win)
-- Workaround for Vim:E5002: Cannot find window number
local ok, cwd = pcall(vim.fn.getcwd, win)
return ok and cwd or vim.fn.getcwd()
end
opts.sources.path.filter
: function(name: string): boolean
function(_)
return true
end
opts.sources.path.modified
: function(sym: dropbar_symbol_t): dropbar_symbol_t
dropbar_symbol_t
in the result got
from the path source and returns an alternative
symboldropbar_symbol_t
to show if the
current buffer is modifiedfunction(sym)
return sym
end
dropbar_symbol_t
function(sym)
return sym:merge({
name = sym.name .. '[+]',
icon = ' ',
name_hl = 'DiffAdded',
icon_hl = 'DiffAdded',
-- ...
})
end
opts.sources.path.preview
: boolean|fun(path: string): boolean?|nil
true
opts.sources.path.min_widths
: integer[]
{10}
forces the last symbol has width >= 10){}
opts.sources.treesitter.max_depth
: integer
16
opts.sources.treesitter.name_regex
: string
[=[[#~!@\*&.]*[[:keyword:]]\+!\?\(\(\(->\)\+\|-\+\|\.\+\|:\+\|\s\+\)\?[#~!@\*&.]*[[:keyword:]]\+!\?\)*]=]
opts.sources.treesitter.valid_types:
string[]
{
'block_mapping_pair',
'array',
'boolean',
'break_statement',
'call',
'case_statement',
'class',
'constant',
'constructor',
'continue_statement',
'delete',
'do_statement',
'element',
'enum',
'enum_member',
'event',
'for_statement',
'function',
'goto_statement',
'if_statement',
'interface',
'keyword',
'macro',
'method',
'namespace',
'null',
'number',
'operator',
'package',
'pair',
'property',
'reference',
'repeat',
'return_statement',
'rule_set',
'scope',
'section',
'specifier',
'struct',
'switch_statement',
'table',
'type',
'type_parameter',
'unit',
'value',
'variable',
'while_statement',
'declaration',
'field',
'identifier',
'object',
'statement',
}
opts.sources.treesitter.min_widths
: integer[]
{10}
forces the last symbol has width >= 10){}
opts.sources.lsp.max_depth
: integer
16
opts.sources.lsp.valid_symbols:
string[]
{
'File',
'Module',
'Namespace',
'Package',
'Class',
'Method',
'Property',
'Field',
'Constructor',
'Enum',
'Interface',
'Function',
'Variable',
'Constant',
'String',
'Number',
'Boolean',
'Array',
'Object',
'Keyword',
'Null',
'EnumMember',
'Struct',
'Event',
'Operator',
'TypeParameter',
}
opts.sources.lsp.request.ttl_init
: number
60
opts.sources.lsp.request.interval
: number
1000
opts.sources.lsp.min_widths
: integer[]
{10}
forces the last symbol has width >= 10){}
opts.sources.markdown.max_depth
: integer
6
opts.sources.markdown.parse.look_ahead
: number
200
opts.sources.markdown.min_widths
: integer[]
{10}
forces the last symbol has width >= 10){}
Thanks @willothy for implementing this.
opts.sources.terminal.icon
: string|fun(buf: integer): string
function(_)
return M.opts.icons.kinds.symbols.Terminal or ' '
end
opts.sources.terminal.name
: string|fun(buf: integer): string
vim.api.nvim_buf_get_name
name = function(buf)
local name = vim.api.nvim_buf_get_name(buf)
-- the second result val is the terminal object
local term = select(2, require("toggleterm.terminal").indentify(name))
if term then
return term.display_name or term.name
else
return name
end
end
opts.sources.terminal.show_current: boolean
true
dropbar.nvim
defines sets of highlight groups. Override them in your
colorscheme to change the appearance of the drop-down menu:
Highlight group | Description | Attributes |
---|---|---|
DropBarCurrentContext | Background of selected/clicked symbol in dropbar | { link = 'Visual' } |
DropBarCurrentContextIcon | Highlight for selected/clicked symbol's icon in dropbar | { link = 'DropBarCurrentContext' } |
DropBarCurrentContextName | Highlight for selected/clicked symbol's name in dropbar | { link = 'DropBarCurrentContext' } |
DropBarFzfMatch | Fzf fuzzy search matches | { link = 'Special' } |
DropBarHover | Background of the dropbar symbol when the mouse is hovering over it | { link = 'Visual' } |
DropBarIconKindDefault | Default highlight for dropbar icons | { link = 'Special' } |
DropBarIconKindDefaultNC | Default highlight for dropbar icons in non-current windows | { link = 'WinBarNC' } |
DropBarIconKind... | Highlights of corresponding symbol kind icons | { link = 'Repeat' } |
DropBarIconKind...NC | Highlights of corresponding symbol kind icons in non-current windows | { link = 'DropBarIconKindDefaultNC' } |
DropBarIconUIIndicator | Shortcuts before entries in utils.menu.select() |
{ link = 'SpecialChar' } |
DropBarIconUIPickPivot | Shortcuts shown before each symbol after entering pick mode | { link = 'Error' } |
DropBarIconUISeparator | Separator between each symbol in dropbar | { link = 'Comment' } |
DropBarIconUISeparatorMenu | Separator between each symbol in dropbar menus | { link = 'DropBarIconUISeparator' } |
DropBarMenuCurrentContext | Background of current line in dropbar menus | { link = 'PmenuSel' } |
DropBarMenuFloatBorder | Border of dropbar menus | { link = 'FloatBorder' } |
DropBarMenuHoverEntry | Background of hovered line in dropbar menus | { link = 'IncSearch' } |
DropBarMenuHoverIcon | Background of hovered symbol icon in dropbar menus | { reverse = true } |
DropBarMenuHoverSymbol | Background of hovered symbol name in dropbar menus | { bold = true } |
DropBarMenuNormalFloat | Normal text in dropbar menus | { link = 'NormalFloat' } |
DropBarMenuSbar | Scrollbar background of dropbar menus | { link = 'PmenuSbar' } |
DropBarMenuThumb | Scrollbar thumb of dropbar menus | { link = 'PmenuThumb' } |
DropBarPreview | Range of the symbol under the cursor in source code | { link = 'Visual' } |
DropBarKind... | Highlights of corresponding symbol kind names | undefined |
DropBarKind...NC | Highlights of corresponding symbol kind names in non-current windows | undefined |
This configuration highlights filenames from path source with custom highlight
group DropBarFileName
.
local dropbar = require('dropbar')
local sources = require('dropbar.sources')
local utils = require('dropbar.utils')
vim.api.nvim_set_hl(0, 'DropBarFileName', { fg = '#FFFFFF', italic = true })
local custom_path = {
get_symbols = function(buff, win, cursor)
local symbols = sources.path.get_symbols(buff, win, cursor)
symbols[#symbols].name_hl = 'DropBarFileName'
if vim.bo[buff].modified then
symbols[#symbols].name = symbols[#symbols].name .. ' [+]'
symbols[#symbols].name_hl = 'DiffAdded'
end
return symbols
end,
}
dropbar.setup({
bar = {
sources = function(buf, _)
if vim.bo[buf].ft == 'markdown' then
return {
custom_path,
sources.markdown,
}
end
if vim.bo[buf].buftype == 'terminal' then
return {
sources.terminal,
}
end
return {
custom_path,
utils.source.fallback {
sources.lsp,
sources.treesitter,
},
}
end,
},
})
Some plugins, e.g. oil and
fugitive, have buffers with file path
confusing for dropbar.nvim. This is because their buffers names start with
things like oil://
or fugitive://
.
This configuration should addresses the issue:
require('dropbar').setup({
bar = {
enable = function(buf, win, _)
if
not vim.api.nvim_buf_is_valid(buf)
or not vim.api.nvim_win_is_valid(win)
or vim.fn.win_gettype(win) ~= ''
or vim.wo[win].winbar ~= ''
or vim.bo[buf].ft == 'help'
then
return false
end
local stat = vim.uv.fs_stat(vim.api.nvim_buf_get_name(buf))
if stat and stat.size > 1024 * 1024 then
return false
end
return vim.bo[buf].ft == 'markdown'
or vim.bo[buf].ft == 'oil' -- enable in oil buffers
or vim.bo[buf].ft == 'fugitive' -- enable in fugitive buffers
or pcall(vim.treesitter.get_parser, buf)
or not vim.tbl_isempty(vim.lsp.get_clients({
bufnr = buf,
method = 'textDocument/documentSymbol',
}))
end,
},
sources = {
path = {
relative_to = function(buf, win)
-- Show full path in oil or fugitive buffers
local bufname = vim.api.nvim_buf_get_name(buf)
if
vim.startswith(bufname, 'oil://')
or vim.startswith(bufname, 'fugitive://')
then
local root = bufname:gsub('^%S+://', '', 1)
while root and root ~= vim.fs.dirname(root) do
root = vim.fs.dirname(root)
end
return root
end
local ok, cwd = pcall(vim.fn.getcwd, win)
return ok and cwd or vim.fn.getcwd()
end,
},
},
})
The flow chart below should well illustrate what does dropbar
do user moves
around in their window or clicks at a symbol in the winbar:
┌──────────────────┐
│winbar at win 1000│ {k}th symbol clicked
│ contaning buf 1 ├──────────────────────┐
└───────┬─▲────────┘ │
▼ │ │
_G.dropbar() │
│ ▲ │
┌──────────────┐ ┌──────▼─┴──────┐ │
│sources │ │_G.dropbar.bars│ │
│ ┌───┐ │ └──────┬─▲──────┘ │
│ │lsp│ │ ┌───────┬──▼─┴──┬───────┐ │
│ └───┘ │ ┌─▼─┐ ┌─┴─┐ ┌─┴─┐ ... │
│ ┌──────────┐ │ │[1]│ │[2]│ │[3]│ │
│ │treesitter│ │ └─┬─┘ └─┬─┘ └─┬─┘ │
│ └──────────┘ │ │ ... ... │
│ ... │ └──┬─▲─────────────┬──────┐ │
└─────┬─▲──────┘ ┌─▼─┴──┐ ┌──┴───┐ ... │
│ │ │[1000]│ │[1015]│ │
│ │ └─┬─▲──┘ └──────┘ │
│ │ __call() │ │ return string cache │
│ │ ┌───▼─┴───┐ ┌──────────────▼──────────────┐
│ │ │dropbar_t├────────────────────▶ _G.dropbar.callbacks │
│ │ On update events └───┬─▲───┘ register symbol └──────────────┬──────────────┘
│ │ get_symbols(1, 1000, <cursor>) │ │ on_click() callbacks │
│ └─────────────────────────────────┘ │ ┌──────────┬────▼─────┬─────────┐
└─────────────────────────────────────┘ ┌───▼────┐ ┌───┴────┐ ┌───┴────┐ ...
each source returns dropbar_symbol_t[] │['buf1']│ │['buf2']│ │['buf3']│
dropbar_t adds symbols as its components └───┬────┘ └───┬────┘ └───┬────┘
dropbar_t flushes string cache │ ... ...
└────────┬───────────────┬─────────┐
┌─────▼─────┐ ┌─────┴─────┐ ...
│['win1000']│ │['win1015']│
└─────┬─────┘ └─────┬─────┘
│ ...
┌─────────┬────▼────┬─────────┐
┌───┴───┐ ... ┌────┴────┐ ...
│['fn1']│ │['fn{k}']│
└───────┘ └────┬────┘
▼
invoke _G.dropbar.bars[1][1000].components[k]:on_click()
│
▼
open drop-down menu, goto symbol, etc
A dropbar_source_t
instance is just a table with
get_symbols
field set to a function that returns an array of
dropbar_symbol_t
instances given the buffer number, the
window id, and the cursor position.
We have seen a simple example of a custom source in the default config of
opts.bar.sources
where the second source is set to a combination
of lsp/treesitter/markdown sources using the utils.source.fallback()
factory
function, which simply returns a table containing a get_symbols()
function
where each source passed to utils.source.fallback()
is queried and the first
non-empty result get from the sources is returned as the result of the combined
source.
Here is another example of a custom source that will always return two symbols
saying 'Hello' and 'dropbar' with highlights 'hl-Keyword'
and 'hl-Title'
and a smiling face shown in 'hl-WarningMsg'
at the start of the first symbol;
clicking on the first symbol will show a notification message saying 'Have you
smiled today?', followed by the smiling face icon used in the in dropbar symbol:
local bar = require('dropbar.bar')
local custom_source = {
get_symbols = function(_, _, _)
return {
bar.dropbar_symbol_t:new({
icon = ' ',
icon_hl = 'WarningMsg',
name = 'Hello',
name_hl = 'Keyword',
on_click = function(self)
vim.notify('Have you smiled today? ' .. self.icon)
end,
}),
bar.dropbar_symbol_t:new({
name = 'dropbar',
name_hl = 'Title',
}),
}
end,
}
Add this source to opts.bar.sources
table to see it in action:
require('dropbar').setup({
bar = {
sources = {
custom_source,
},
},
})
The following example shows how to make a source that returns two symbols with the first symbol having a drop-down menu with a single entry saying 'World':
local bar = require('dropbar.bar')
local menu = require('dropbar.menu')
local custom_source = {
get_symbols = function(_, _, _)
return {
bar.dropbar_symbol_t:new({
icon = ' ',
icon_hl = 'WarningMsg',
name = 'Hello',
name_hl = 'Keyword',
on_click = function(self)
self.menu = menu.dropbar_menu_t:new({
entries = {
menu.dropbar_menu_entry_t:new({
components = {
bar.dropbar_symbol_t:new({
icon = ' ',
icon_hl = 'WarningMsg',
name = 'World',
name_hl = 'Keyword',
on_click = function(sym)
vim.notify('Have you smiled today? ' .. sym.icon)
end,
}),
},
}),
},
})
self.menu:toggle()
end,
}),
bar.dropbar_symbol_t:new({
name = 'dropbar',
icon = ' ',
name_hl = 'Special',
icon_hl = 'Error',
}),
}
end,
}
on_click()
Callbackdropbar_symbol_t:new()
defines a default on_click()
callback if non is provided.
The default on_click()
callback will look for these fields in the symbol
instance and create a drop-down menu accordingly on click, for more information
about these fields see dropbar_symbol_t
.
For creating the drop-down menu:
dropbar_symbol_t.siblings
dropbar_symbol_t.sibling_idx
dropbar_symbol_t.children
For jumping to the symbol or previewing it:
dropbar_symbol_t.range
dropbar_symbol_t.win
dropbar_symbol_t.buf
The following example shows a source that utilizes the default on_click()
callback:
local bar = require('dropbar.bar')
local custom_source = {
get_symbols = function(buf, win, _)
return {
bar.dropbar_symbol_t:new({
name = 'Section 1',
name_hl = 'Keyword',
siblings = {
bar.dropbar_symbol_t:new({
name = 'Section 2',
name_hl = 'WarningMsg',
}),
bar.dropbar_symbol_t:new({
name = 'Section 3',
name_hl = 'Error',
}),
bar.dropbar_symbol_t:new({
name = 'Section 4',
name_hl = 'String',
children = {
bar.dropbar_symbol_t:new({
buf = buf,
win = win,
name = 'Section 4.1',
name_hl = 'String',
-- Will jump to line 3, col 4 (0-indexed) when clicked in the
-- menu
range = {
start = { line = 3, character = 4 },
['end'] = { line = 5, character = 6 },
}
}),
},
}),
},
}),
}
end,
}
To see this source in action add it to opts.bar.sources
table:
require('dropbar').setup({
bar = {
sources = {
custom_source,
},
},
})
If the symbol fields siblings
or children
are expensive to compute, you can
use meta-tables to lazy-load them, so that they are only computed when a menu
is opened:
local bar = require('dropbar.bar')
local custom_source = {
get_symbols = function(_, _, _)
return {
bar.dropbar_symbol_t:new(setmetatable({
name = 'Section 1',
name_hl = 'Keyword',
}, {
__index = function(self, key)
if key == 'siblings' then
self[siblings] = -- [[ compute siblings ]]
return self[siblings]
end
if key == 'children' then
self[children] = -- [[ compute children ]]
return self[children]
end
-- ...
end,
})),
}
end,
}
To see concrete examples of lazy-loading see
lua/dropbar/sources
.