IDE-like breadcrumbs, out of the box
A polished, IDE-like, highly-customizable winbar for Neovim with drop-down menus and multiple backends.
For more information see :h dropbar
.
dropbar-demo-light.mp4
- Opening drop-down menus or go to definition with a single mouse click
- Pick mode for quickly selecting a component in the winbar with shortcuts
- Automatically truncating long components
- Better truncation when winbar is still too long after shortening all components
- Multiple backends that support fall-backs
dropbar.nvim
comes with five builtin sources:- lsp: gets symbols from language servers using nvim's builtin LSP framework
- markdown: a custom incremental parser that gets symbol information about markdown headings
- path: gets current file path
- treesitter: gets symbols from treesitter parsers using nvim's builtin treesitter integration
- terminal: easily switch terminal buffers using the dropdown menu To make a new source yourself, see making a new source. For source fall-backs support, see bar options.
- Zero config & Zero dependency
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. - Drop-down menu components and winbar symbols that response to
mouse/cursor hovering:
- This features requires
:h mousemoveevent
to be enabled.
- This features requires
- Preview symbols in their source windows when hovering over them in the
drop-down menu
- Reorient the source window on previewing or after jumping to a symbol
- Add scrollbar to the menu when the symbol list is too long
- Neovim >= 0.11.0
- Optional
- nvim-web-devicons, if you want to see icons for different filetypes
- telescope-fzf-native, if you want fuzzy search support
- Working language server installation for the lsp source to work
- Working treesitter parser installation for the treesitter source to work
-
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.
- Basics
- Moves the cursor around and see the winbar reflects your current context
- Mouse support
- Click on a component in the winbar to open a drop-down menu of its siblings
- Click on an entry in the drop-down menu to go to its location
- Click on the indicator in the drop-down menu to open a sub-menu of its children
- Pick mode
- Use
require('dropbar.api').pick()
to enter interactive pick mode orrequire('dropbar.api').pick(<idx>)
to directly select a component atidx
. - Inside interactive pick mode, press the corresponding pivot shown before each component to select it
- Use
- Fuzzy finder
- Use
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
- Use
- Default keymaps in drop-down menu
<LeftMouse>
: call theon_click
callback of the symbol at the mouse click<CR>
: find the first clickable symbol in the current drop-down menu entry and call itson_click
callbacki
: enter fzf mode from the menuq
/<Esc>
: close current menu- To disable, remap or add new keymaps in the drop-down menu, see menu options
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
- Controls whether to attach dropbar to the current buffer and window
- If a function is provided, it will be called with the current bufnr and winid and should return a boolean
- Default:
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[]
- Controls when to evaluate the
enable()
function and attach the plugin to corresponding buffer or window - Default:
{ 'OptionSet', 'BufWinEnter', 'BufWritePost', }
- Controls when to evaluate the
opts.bar.update_debounce
:number
- Wait for a short time before updating the winbar, if another update
request is received within this time, the previous request will be
cancelled, this improves the performance when the user is holding
down a key (e.g.
'j'
) to scroll the window - If you encounter performance issues when scrolling the window, try
setting this option to a number slightly larger than
1000 / key_repeat_rate
- Default:
32
- Wait for a short time before updating the winbar, if another update
request is received within this time, the previous request will be
cancelled, this improves the performance when the user is holding
down a key (e.g.
opts.bar.update_events.win
:string[]
- List of events that should trigger an update on the dropbar attached to a single window
- Default:
{ 'CursorMoved', 'WinEnter', 'WinResized', }
opts.bar.update_events.buf
:string[]
- List of events that should trigger an update on all dropbars attached to a buffer
- Default:
{ 'BufModifiedSet', 'FileChangedShellPost', 'TextChanged', 'ModeChanged', }
opts.bar.update_events.global
:string[]
- List of events that should trigger an update of all dropbars in current nvim session
- Default:
{ 'DirChanged', 'VimResized', }
opts.bar.hover
:boolean
- Whether to highlight the symbol under the cursor
- This feature requires
'mousemoveevent'
to be enabled - Default:
true
opts.bar.sources
:dropbar_source_t[]|fun(buf: integer, win: integer): dropbar_source_t[]
- List of sources to show in the winbar
- If a function is provided, it will be called with the current bufnr and winid and should return a list of sources
- Default:
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
- For more information about sources, see
dropbar_source_t
.
opts.bar.padding
:{ left: number, right: number }
- Padding to use between the winbar and the window border
- Default:
{ left = 1, right = 1 }
opts.bar.pick.pivots
:string
- Pivots to use in pick mode
- Default:
'abcdefghijklmnopqrstuvwxyz'
opts.bar.truncate
:boolean
- Whether to truncate the winbar if it doesn't fit in the window
- Default:
true
These options live under opts.menu
and are used to control the behavior of the
menu:
-
opts.menu.quick_navigation
:boolean
- When on, automatically set the cursor to the closest previous/next
clickable component in the direction of cursor movement on
CursorMoved
- Default:
true
- When on, automatically set the cursor to the closest previous/next
clickable component in the direction of cursor movement on
-
opts.menu.entry.padding
:{ left: number, right: number }
- Padding to use between the menu entry and the menu border
- Default:
{ left = 1, right = 1 }
-
opts.menu.preview
:boolean
- Whether to enable previewing for menu entries
- Default:
true
-
opts.menu.hover
:boolean
- Whether to highlight the symbol under the cursor
- This feature requires
'mousemoveevent'
to be enabled - Default:
true
-
opts.menu.keymaps
:table<string, function|string|table<string, function>|table<string, string>>
- Buffer-local keymaps in the menu
- Use
<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. - Default:
{ ['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>
- Scrollbar configuration for the menu.
- Default:
{ 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>
- Window configurations for the menu, see
:h nvim_open_win()
- Each config key in
opts.menu.win_configs
accepts either a plain value which will be passes directly tonvim_open_win()
, or a function that takes the current menu (seedropbar_menu_t
) as an argument and returns a value to be passed tonvim_open_win()
. - Default:
{ 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 menu.prev_menu then if menu.prev_menu.scrollbar and menu.prev_menu.scrollbar.thumb then return vim.api.nvim_win_get_config(menu.prev_menu.scrollbar.thumb).zindex end return vim.api.nvim_win_get_config(menu.prev_win).zindex end end, }
- Window configurations for the menu, see
These options live under opts.fzf
and are used to control the behavior and
appearance of the fuzzy finder interface.
-
opts.fzf.keymaps
- The keymaps that will apply in insert mode, in the fzf prompt buffer
- Same config as opts.menu.keymaps
- Default:
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
- Options passed to
:h nvim_open_win
. The fuzzy finder will use its parent window's config by default, but options set here will override those. - Same config as opts.menu.win_configs
- Default:
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, },
- Options passed to
-
opts.fzf.prompt
- Prompt string that will be displayed in the statuscolumn of the fzf input window.
- Can include highlight groups
- Default:
prompt = '%#htmlTag# '
-
opts.fzf.char_pattern
- Default:
char_pattern = '[%w%p]'
- Default:
-
opts.fzf.retain_inner_spaces
- Default:
retain_inner_spaces = true
- Default:
-
opts.fzf.fuzzy_find_on_click
- When opening an entry with a submenu via the fuzzy finder, open the submenu in fuzzy finder mode.
- Default:
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
- Whether to enable icons
- Default:
true
opts.icons.kinds.dir_icon
:fun(path: string): string, string?|string?
- Directory icon and highlighting getter, set to empty string to disable
- Default:
function(_) return M.opts.icons.kinds.symbols.Folder, 'DropBarIconKindFolder' end
opts.icons.kinds.file_icon
:fun(path: string): string, string?|string?
- File icon and highlighting getter, set to empty string to disable
- Default:
function(path) return M.opts.icons.kinds.symbols.File, 'DropBarIconKindFile' end
opts.icons.kinds.symbols
:table<string, string>
- Table mapping the different kinds of symbols to their corresponding icons
- Default:
{ 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>
- Controls the icons used in the winbar UI
- Default:
{ separator = ' ', extends = '…', }
opts.icons.ui.menu
:table<string, string>
- Controls the icons used in the menu UI
- Default:
{ 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?
- Default function called when clicking or pressing
<CR>
on the symbol - Default:
function(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,
- Default function called when clicking or pressing
opts.symbol.preview.reorient
:fun(win: integer, range: {start: {line: integer, character: integer}, end: {line: integer, character: integer}})
- Function to reorient the source window when previewing symbol given
the source window
win
and the range of the symbolrange
- Default:
function() end
- Function to reorient the source window when previewing symbol given
the source window
opts.symbol.jump.reorient
:fun(win: integer, range: {start: {line: integer, character: integer}, end: {line: integer, character: integer}})
- Function to reorient the source window after jumping to symbol given
the source window
win
and the range of the symbolrange
- Default:
function() end
- Function to reorient the source window after jumping to symbol given
the source window
These options live under opts.sources
and are used to control the behavior of
each sources.
opts.sources.path.max_depth
:integer
- Maximum number of symbols to return
- A smaller number can help to improve performance in deeply nested paths
- Default:
16
opts.sources.path.relative_to
:string|fun(buf: integer, win: integer): string
- The path to use as the root of the relative path
- If a function is provided, it will be called with the current buffer number and window id as arguments and should return a string to be used as the root of the relative path
- Notice: currently does not support
..
relative paths - Default:
function(_, 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
- A function that takes a file name and returns whether to include it in the results shown in the drop-down menu
- Default:
function(_) return true end
opts.sources.path.modified
:function(sym: dropbar_symbol_t): dropbar_symbol_t
- A function that takes the last
symbol
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 modified - Default:
function(sym) return sym end
- To set a different icon, name, or highlights when the buffer is modified,
you can change the corresponding fields in the returned
symbol
dropbar_symbol_t
function(sym) return sym:merge({ name = sym.name .. '[+]', icon = ' ', name_hl = 'DiffAdded', icon_hl = 'DiffAdded', -- ... }) end
- A function that takes the last
symbol
opts.sources.path.preview
:boolean|fun(path: string): boolean?|nil
- A boolean or a function that takes a file path and returns whether to preview the file under cursor
- Default:
true
opts.sources.path.min_widths
:integer[]
- Minimum width of each symbols when truncated, in reverse order
(e.g.
{10}
forces the last symbol has width >= 10) - Default:
{}
- Minimum width of each symbols when truncated, in reverse order
(e.g.
opts.sources.treesitter.max_depth
:integer
- Maximum number of symbols to return
- A smaller number can help to improve performance in deeply nested trees (e.g. in big nested json files)
- Default:
16
opts.sources.treesitter.name_regex
:string
- Vim regex used to extract a short name from the node text
- Default:
[=[[#~!@\*&.]*[[:keyword:]]\+!\?\(\(\(->\)\+\|-\+\|\.\+\|:\+\|\s\+\)\?[#~!@\*&.]*[[:keyword:]]\+!\?\)*]=]
opts.sources.treesitter.valid_types:
string[]
- A list of treesitter node types to include in the results
- Default:
{ '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[]
- Minimum width of each symbols when truncated, in reverse order
(e.g.
{10}
forces the last symbol has width >= 10) - Default:
{}
- Minimum width of each symbols when truncated, in reverse order
(e.g.
opts.sources.lsp.max_depth
:integer
- Maximum number of symbols to return
- A smaller number can help to improve performance when the language server returns huge list of nested symbols
- Default:
16
opts.sources.lsp.valid_symbols:
string[]
- A list of LSP document symbols to include in the results
- Default:
{ '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
- Number of times to retry a request before giving up
- Default:
60
opts.sources.lsp.request.interval
:number
- Number of milliseconds to wait between retries
- Default:
1000
opts.sources.lsp.min_widths
:integer[]
- Minimum width of each symbols when truncated, in reverse order
(e.g.
{10}
forces the last symbol has width >= 10) - Default:
{}
- Minimum width of each symbols when truncated, in reverse order
(e.g.
opts.sources.markdown.max_depth
:integer
- Maximum number of symbols to return
- Default:
6
opts.sources.markdown.parse.look_ahead
:number
- Number of lines to update when cursor moves out of the parsed range
- Default:
200
opts.sources.markdown.min_widths
:integer[]
- Minimum width of each symbols when truncated, in reverse order
(e.g.
{10}
forces the last symbol has width >= 10) - Default:
{}
- Minimum width of each symbols when truncated, in reverse order
(e.g.
Thanks @willothy for implementing this.
-
opts.sources.terminal.icon
:string|fun(buf: integer): string
- Icon to show before terminal names
- Default:
function(_) return M.opts.icons.kinds.symbols.Terminal or ' ' end
-
opts.sources.terminal.name
:string|fun(buf: integer): string
- Default:
vim.api.nvim_buf_get_name
- Easy to integrate with other plugins (for example, toggleterm.nvim):
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
- Default:
-
opts.sources.terminal.show_current: boolean
- Show the current terminal buffer in the menu
- Default:
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,
}
dropbar_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
.