bkmr-nvim
July 11, 2026 · View on GitHub
A comprehensive Neovim plugin for the bkmr snippet manager. Provides seamless integration with the bkmr LSP server and a rich editing interface for managing snippets directly within Neovim.
Features
- LSP Integration: Automatic setup with bkmr LSP server for snippet completion
- Visual Snippet Management: Browse and select snippets using fzf-lua or builtin selector
- Rich Editing Interface: Edit snippets in vsplit with template format matching
bkmr edit
The bkmr LSP completion system works by:
- Filetype detection: Automatically uses your current buffer's filetype to filter snippets
- Tag-based filtering: Snippets must be tagged with both snip AND the language name (e.g., make)
- LSP activation: The LSP only attaches to buffers with filetypes in the configured list
Example
Make a bkmr snippet target availabe in nvim buffer for Makefile:
- Enable LSP for Makefiles
Add 'make' to the filetypes configuration so the LSP attaches to Makefile buffers.
- Tag
targetcorrectly
Your target snippet needs proper tags to appear in Makefile completion:
- Option A: Tag it as make,snip (Makefile-only)
- Option B: Tag it as universal,snip (appears in all languages with auto-comment translation)
- Option C: Tag it as make,bash,shell,snip (appears in Makefiles and shell scripts)
- Expand it and fill the tab-stops
Say the target snippet stores this body (with LSP snippet tab-stops):
.PHONY: \$1
\$1: ## \$1
\$0
In a Makefile (filetype make), the flow is:
-
Type the snippet title, e.g.
target. The completion item appears in thenvim-cmpmenu (force it with<C-Space>if it hasn't popped up). -
Press
<CR>to confirm. Because the LSP returns the item as a snippet, confirming expands it and places the cursor on the first tab-stop ($1, right after.PHONY:). The recipe line below is indented with a real<Tab>. -
Type the target name once, e.g.
build.$1is mirrored — all three occurrences fill in together as you type:.PHONY: build build: ## build <cursor moves here on next jump> -
Press
<Tab>to jump to the final stop ($0, the indented recipe line) and type the command. Use<S-Tab>to jump back to a previous stop.
The tab-stops ($1, mirrored, then $0) come from the snippet body; the keys
that drive them come from your completion setup, not from bkmr-nvim.
Completion keymaps for this flow
This example assumes Neovim 0.10+ (native vim.snippet) with nvim-cmp.
The relevant mappings — <CR> to expand, <Tab>/<S-Tab> to jump — are:
local cmp = require('cmp')
cmp.setup({
-- Expand LSP snippets with Neovim's built-in engine
snippet = {
expand = function(args) vim.snippet.expand(args.body) end,
},
mapping = cmp.mapping.preset.insert({
['<C-Space>'] = cmp.mapping.complete(), -- force the menu
['<CR>'] = cmp.mapping.confirm({ select = true }), -- confirm = expand
-- <Tab>: navigate the menu when open, else jump to the next snippet stop.
-- The { 'i', 's' } is required: tab-stops are selected in SELECT mode.
['<Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif vim.snippet.active({ direction = 1 }) then
vim.snippet.jump(1)
else
fallback()
end
end, { 'i', 's' }),
['<S-Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif vim.snippet.active({ direction = -1 }) then
vim.snippet.jump(-1)
else
fallback()
end
end, { 'i', 's' }),
}),
sources = cmp.config.sources({ { name = 'nvim_lsp' } }),
})
If you use a different completion plugin (blink.cmp, coq, or LuaSnip-based setups), the trigger and jump keys differ — consult that plugin's docs; bkmr only supplies the snippet completion items over LSP.
Requirements
- Neovim 0.8+
- bkmr 4.24.0+ (with LSP support)
- Optional: fzf-lua for enhanced snippet selection
- Optional: nvim-lspconfig for automatic LSP setup
Development Requirements
- plenary.nvim for running tests
Installation
Using lazy.nvim
{
"sysid/bkmr-nvim",
dependencies = {
"ibhagwan/fzf-lua", -- Optional: for better snippet selection
"neovim/nvim-lspconfig" -- Optional: for automatic LSP setup
},
config = function()
require('bkmr').setup({
debug = false, -- Enable debug logging
ui = {
use_fzf = true, -- Enable fzf-lua integration
}
})
end
}
Configuration
Default configuration:
require('bkmr').setup({
debug = false, -- Enable debug logging
lsp = {
auto_setup = true, -- Auto-configure with lspconfig
cmd = { "bkmr", "lsp" }, -- LSP server command
filetypes = { -- Supported file types (REPLACES defaults when set)
'rust', 'javascript', 'typescript', 'python', 'go', 'java', 'c', 'cpp',
'html', 'css', 'scss', 'ruby', 'php', 'swift', 'kotlin', 'shell', 'sh',
'bash', 'yaml', 'json', 'markdown', 'xml', 'vim', 'lua', 'toml', 'make'
},
extra_filetypes = {}, -- Filetypes ADDED to the defaults (safe, additive)
},
ui = {
split_direction = "vertical", -- "horizontal" | "vertical"
split_size = "50%", -- Split width/height (number or "N%" for percentage)
use_telescope = false, -- Use telescope for selection
use_fzf = true, -- Use fzf-lua for selection
},
edit = {
auto_save = false, -- Auto-save on buffer leave
confirm_delete = true, -- Confirm before deletion
template_header = true, -- Show template header in edit buffer
}
})
Adding filetypes
lsp.filetypes replaces the default list wholesale — if you set it, include
every filetype you want, or the LSP will silently never attach to the ones you
omit (there is no error or warning). To simply add filetypes without re-listing
the defaults, use lsp.extra_filetypes instead:
require('bkmr').setup({
lsp = {
extra_filetypes = { 'nix', 'dockerfile' }, -- added to the defaults, safe
},
})
extra_filetypes is unioned onto whatever filetypes resolves to (defaults or
your override) and de-duplicated, so it can never drop a filetype.
Manual LSP Setup
If you prefer manual LSP configuration or don't have nvim-lspconfig:
require('bkmr').setup({
lsp = {
auto_setup = false -- Disable automatic setup
}
})
-- Then manually configure with nvim-lspconfig:
require('lspconfig').bkmr_lsp.setup({
cmd = { "bkmr", "lsp" },
filetypes = { "rust", "python", "javascript" }, -- your preferred filetypes
})
Usage
Commands
:BkmrEdit [language]- Browse and edit snippets (defaults to current buffer's filetype if not specified):BkmrNew- Create new snippet:BkmrDelete <id>- Delete snippet by ID:BkmrInsertPath- Insert a filepath comment at the cursor
When using :BkmrEdit, you can browse available snippets and select one to edit.
Default Keymaps
Unless vim.g.bkmr_no_default_mappings is set, the plugin defines these normal-mode maps:
<leader>bs- Browse/list snippets (all languages)<leader>bn- Create new snippet<leader>bp- Insert filepath comment
Set vim.g.bkmr_no_default_mappings = true before the plugin loads to disable them.
Snippet Editing
When editing snippets, the interface uses section markers matching bkmr edit:
# Snippet Template
# Section markers (=== SECTION_NAME ===) are required and must not be removed.
=== ID ===
123
=== CONTENT ===
#!/bin/bash
# This comment is part of the snippet and will be preserved
echo "Hello, World!"
echo "This is my snippet content"
=== TITLE ===
My Example Snippet
=== TAGS ===
_snip_,bash,shell
=== COMMENTS ===
This snippet demonstrates the editing format
=== END ===
Note: All content within sections is preserved literally, including lines starting with #.
Only template header comments (outside of sections) are ignored.
LSP Completion
The plugin automatically configures bkmr LSP completion. Snippets will appear in completion menus based on:
- Current buffer filetype (e.g.,
.rsfiles show Rust snippets) - Universal snippets (tagged with appropriate tags)
- Manual completion trigger (varies by completion plugin)
Debug Mode
Enable debug mode to see detailed LSP communication:
require('bkmr').setup({
debug = true
})
Debug features:
- All LSP requests and responses are logged
- Large responses (>10 items) are automatically truncated in logs
- Error messages are properly extracted from various response formats
- Use
:messagesto view debug output
API
The plugin provides a Lua API for integration with other plugins:
local bkmr = require('bkmr')
-- Check if LSP is available
if bkmr.is_lsp_available() then
-- Get current context
local context = bkmr.get_context()
print(context.filetype)
end
-- Programmatically create snippet
bkmr.new_snippet()
-- List snippets with language filter
bkmr.list_snippets("rust")
-- Delete snippet
bkmr.delete_snippet(456)
-- Note: edit_snippet is used internally when selecting from list
Testing
The plugin includes a comprehensive test suite using plenary.nvim. Tests cover configuration management, UI template generation/parsing, and LSP response handling.
Prerequisites
Install plenary.nvim with your package manager:
-- Using lazy.nvim
{ 'nvim-lua/plenary.nvim' }
-- Using packer.nvim
use 'nvim-lua/plenary.nvim'
Running Tests
# Run all tests (28 tests across 3 modules)
make test
# Run tests interactively in Neovim
make test-interactive
# Run specific test file
make test-file FILE=test_ui.lua
# Manual testing with debug scripts
make test-manual
Troubleshooting
LSP Not Starting
- Verify bkmr is installed and in PATH:
which bkmr - Check bkmr version:
bkmr --version(should be 4.24.0+) - Test LSP manually:
bkmr lsp - Enable debug mode to see detailed logs
No Completions
- Ensure snippets exist:
bkmr list - Check LSP client is attached:
:LspInfo - Verify filetype mapping in configuration
- Try browsing snippets:
:BkmrEdit
Snippet Selection Issues
- If fzf-lua isn't working, install it or disable:
use_fzf = false - For telescope users (not yet implemented), set:
use_telescope = true, use_fzf = false - Falls back to builtin vim.ui.select if neither is available
Common Errors
"Failed to create/update snippet: Unknown error"
- The LSP server returns the snippet object directly on success
- This is normal behavior and the snippet is actually saved
"E382: Cannot write, 'buftype' option is set"
- This has been fixed - the buffer type is now 'acwrite'
- Save with
:wshould work properly
"Invalid tag: Tag cannot be empty"
- Empty language filters are now properly handled
- Use
:BkmrEdit(or<leader>bs) to browse snippets
Related Projects
- bkmr - Command-line bookmark and snippet manager
- bkmr-intellij-plugin - IntelliJ Platform integration
License
MIT License - see LICENSE file for details.