Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The context menu

A right-click opens a menu at the pointer, in every session: vim or modeless, the GUI, the browser or the terminal. Before the menu opens, the click moves the cursor to where you clicked. A click inside the selection keeps the selection instead, so the menu acts on it.

In an editable buffer the menu offers Undo and Redo; Cut, Copy, Paste and Select All; the LSP verbs while a language server is attached; and the command palette. A read-only buffer (help, the quickfix list) offers Select All, plus Copy: always in a modeless session, and when text is selected in a vim one.

One click chooses a row, and the row under the pointer is highlighted. A click or right-click anywhere else dismisses the menu, and a right-click opens a new one where it landed. The keys work too: <Down> / <Up> move, <CR> chooses and <Esc> dismisses.

A runnable tour of everything below, with its own spec, is examples/context-menu/:

BEMTVI_CONFIG=examples/context-menu cargo run -p bemtvi -- examples/context-menu/sample.txt

Groups

The menu is built from groups. Each buffer has its own ordered list of group names, and each name refers to a registered group that gives rows. Groups that give no rows are dropped, and a separator goes between the rest.

btv.contextmenu.list(0)   --> { "undo", "clipboard", "lsp", "commands" }
GroupRowsModeless sessionVim session
undoUndo, Redothe modeless verbs:undo / :redo
clipboardCut, Copy, Paste, Select Allthe modeless verbs"+d, "+y, "+P, ggVG
lspGo to Definition, Find References, Rename Symbol, Format Document, Code Actionsbtv.lsp.*btv.lsp.*
commandsCommand Palettethe palettethe palette
  • Helix’s modes are a vim session here, with Helix’s % for Select All and x then y for Copy with nothing selected, which leaves out the line’s end.
  • undo, and Cut and Paste, show only in a buffer that is 'modifiable'.
  • Copy with nothing selected copies the line, in any modeless buffer and in an editable vim one. A read-only buffer in a vim session needs a selection to copy. In a vim session Cut needs a selection too; a modeless Cut takes the line.
  • lsp shows while a server is attached, and greys out a verb no attached server answers.
  • In a modeless session each row shows the key that does the same thing, read from the live layout, so a rebound key shows its new chord. A vim session shows no keys: vim’s keys are verbs plus motions, not one chord.

Changing the menu

A buffer’s list is live: edit it and that buffer’s menu changes, and no other’s.

local list = btv.contextmenu.list(0)
table.insert(list, 1, "commands")       -- the palette first
btv.contextmenu.set(0, { "clipboard" }) -- or replace the list
btv.contextmenu.set(0, {})              -- no menu in this buffer

A buffer gets its list the first time it’s asked for: a copy of btv.contextmenu.default for a 'modifiable' buffer, or of btv.contextmenu.default_readonly otherwise. Edit the defaults in your config to change what every buffer starts with:

table.remove(btv.contextmenu.default, 1) -- no Undo / Redo in new buffers

A group is a table you can change. Add a row, remove one, or hide the whole group:

table.insert(btv.contextmenu.groups.commands.items, {
  label = "Save",
  action = function() btv.cmd("write") end,
})
btv.contextmenu.groups.undo.when = false

Adding a group

A group has items, the rows, and an optional when. items is a list, or a function that returns one, or returns a promise of one:

btv.contextmenu.groups.file = {
  when = function(ctx) return btv.buf.name(ctx.buf) ~= "" end,
  items = function(ctx)
    local path = btv.buf.name(ctx.buf)
    return btv.fs.stat(path):next(function(st)  -- answers later
      return {
        { label = "Copy Path", detail = st.size .. " bytes",
          action = function() return btv.clipboard.write(path) end },
      }
    end)
  end,
}
table.insert(btv.contextmenu.default, "file")

A row has:

Field
labelThe row’s text. Required.
actionfunction(ctx), run when the row is chosen. It may return a promise. Required.
detailThe right-hand hint: a string, or function(ctx) returning one or nil.
whenHide the row unless this is true: a boolean, or function(ctx).
enabledGrey the row out unless this is true: a boolean, or function(ctx).

A group’s when hides the whole group the same way. A field a group or a row doesn’t know about is an error that names the group, so a typo doesn’t silently drop a row. So is a name in a list with no registered group. Lists hold names, so a group registered after a buffer’s list was made shows as soon as it exists.

The menu opens once every group has answered. Rows keep list order, whichever group answers first.

ctx is built once per right-click and passed to every items, when, enabled, detail and action:

{
  buf = 3, win = 1000,        -- the clicked buffer and window
  mouse = btv.getmousepos(),  -- the click
  mode = "n",                 -- the mode once the click has moved the cursor
  selection = false,          -- Visual or Select is active
  modifiable = true,          -- the buffer can be edited
  modeless = false,           -- a modeless session
}

For a modeless verb’s key hint in your own row, use btv.modeless.key_hint:

{ label = "Save", action = btv.modeless.actions.save,
  detail = function(ctx) return ctx.modeless and btv.modeless.key_hint("save") or nil end }

Opening it yourself

btv.contextmenu.open() opens the current buffer’s menu at the pointer, which is what a right-click does. From a key with no click behind it, the menu opens at the text cursor. It returns a promise that settles once the chosen row’s action has run, or once the menu closed. btv.contextmenu.entries(buf) returns a promise of the rows a buffer’s menu would show right now, without showing it.

A menu of your own

Groups are the one way to put rows in the right-click menu. For a buffer that should show only your rows, set its list: btv.contextmenu.set(buf, { "mine" }). To take the right-click over entirely, bind a buffer-local <RightMouse>: a map replaces a key’s default, so no menu opens.

Underneath, a right-click fires the MenuPopup event (see autocmd events), as vim does just before its popup menu shows, and this menu is what opens. Don’t open a second menu from a MenuPopup handler: only one menu shows at a time, so it would replace this one or be replaced by it.