Neovim Configuration — bugabinga
Working directory (/home/me/Workspace/dotfiles/neovim) is symlinked to
~/.config/nvim. Tests run against the live config at ~/.config/nvim/lua/.
Project Structure
neovim/
├── init.lua # Main entry point; Neovim 0.11+ required
├── justfile # Build/lint/dev commands (Nushell, not bash)
├── lua/
│ ├── std/ # Standard library (no external deps)
│ │ ├── auto.lua # vim.api.nvim_create_autocmd wrapper
│ │ ├── const.lua # OS/app constants (win32, wsl, os_background)
│ │ ├── dbg/ # Debug logging submodule directory
│ │ │ ├── init.lua # Debug facade (dbg(), dbg.toggle(), dbg.kind())
│ │ │ ├── viewer.lua # Special buffer UI for browsing debug events
│ │ │ ├── file.lua # JSONL file read/write + context detection
│ │ │ ├── format.lua # Format string parser ({N}, {["key"]} placeholders)
│ │ │ ├── kinds.lua # DEBUG/TRACE/ERROR kind definitions (icon, label, color)
│ │ │ └── encode.lua # JSON encoder for debug data
│ │ ├── icon.lua # Nerd font icon map (LSP/completion icons)
│ │ ├── map.lua # Fluent keymap DSL (builder pattern)
│ │ ├── prequire.lua # Protected require with nil-safe proxy
│ │ ├── project.lua # Project root detection (weighted markers)
│ │ ├── puk.lua # Plugin loader (vim.schedule_wrap defer)
│ │ ├── table/ # Table util wrappers (join)
│ │ └── user_command.lua # Fluent user command DSL
│ ├── bugabinga/ # Configuration modules (std + plugins only)
│ │ ├── lsp.lua # LSP config loader (lsp/*.lua auto-discovery)
│ │ ├── format.lua # conform.nvim setup + format keymap
│ │ ├── options/ # Neovim options (keymaps, core, behaviour)
│ │ └── nugu/ # Color theme (palette, dark/light variants)
│ └── patches/ # Built-in Lua function overrides
├── after/
│ ├── ftplugin/ # File-type specific config
│ ├── queries/ # Treesitter query overrides
│ └── syntax/ # Syntax highlight tweaks
├── colors/
│ └── nugu.lua # Color scheme definition
├── lsp/ # LSP server configs (lua_ls, gopls, etc.)
├── tests/ # PlenaryBusted test suite
│ ├── minimal_init.lua # Test harness (preloads plenary, sets package.path)
│ ├── std/ # Tests for std lib (mirror lua/std structure)
│ └── bugabinga/ # Tests for bugabinga modules
└── test/ # Scratch/test artifacts (d2, svg, etc.)
Commands
All commands run via just (Nushell, not bash):
| Command | Description |
|---|---|
just |
List all recipes |
just test |
Run full test suite (PlenaryBusted) |
just test-file <path> |
Run single test file, e.g. just test-file tests/std/const_spec.lua |
just refac |
Run ast-grep refactoring (interactive) |
just repro |
Reproduce issue with nvim -u repro.lua |
just nuk |
Delete all Neovim state (cache, data, state) |
just puke |
Update plugins from puk.plugins.nuon |
Testing
- Framework: PlenaryBusted
(
nvim --headless -c "packadd plenary" -c "PlenaryBustedDirectory tests/" +q) - Test file naming:
*_spec.luasuffix, mirror source path undertests/ - Test harness:
tests/minimal_init.luapreloads plenary and setspackage.path - Module cache: Invalidate with
package.loaded['module'] = nilbeforerequire - Example test:
before_each(function() package.loaded['std.const'] = nil end)
Architecture Rules
std/— May only depend on itself and Neovim APIs. No plugins.lua/bugabinga/— May depend onstd, plugins, and Neovim APIs.lua/patches/— Overrides to built-in Neovim Lua functions (loaded first).- Plugin config — Lives in
lua/bugabinga/alongside native config.
Module Export Pattern
Modules expose functionality two ways:
Side-effect modules (most common): The module runs setup code on require
and returns nothing. Use for plugins, autocmd registration, keymap setup, etc.
-- Module: sets up autocmds, registers keymaps, etc.
-- Test: require triggers side effects
require 'bugabinga.some_plugin'
When a side-effect module needs to expose test-only or external APIs, it
creates a _G global with capitalized name:
| Module path | Global name |
|---|---|
bugabinga.statusline |
BugabingaStatusline |
bugabinga.cmdline |
BugabingaCmdline |
bugabinga.cycle |
BugabingaCycle |
bugabinga.d2 |
BugabingaD2 |
bugabinga.treesitter |
BugabingaTreesitter |
The naming convention: drop the lua/ prefix, replace dots with the module name
parts capitalized, e.g. bugabinga.foo_bar → BugabingaFooBar.
The module returns its global table, so
require('bugabinga.d2') === BugabingaD2 always holds — same table identity.
Use _G globals only when needed — for test assertions or when external callers
(internally or via :BugabingaXxx commands) need to invoke module functions.
Library modules that are properly required and used by other modules do not
need this pattern.
Key stdlib APIs
std.auto(group_name)
Creates autocmd group; returns function to register autocmds.
local auto = require 'std.auto'
auto 'my_group' {
events = 'VimEnter',
command = function() print 'hello' end,
}
std.map
Fluent keymap builder; modes are chainable.
local map = require 'std.map'
map.normal { keys = 'gd', command = vim.lsp.buf.definition }
map.insert { keys = '<c-s>', command = vim.lsp.buf.signature_help }
std.user_command
Fluent user command builder.
local user_command = require 'std.user_command'
user_command.MyCommand 'Does something' (function() print 'ran' end)
std.puk
Plugin loader with defer for lazy loading.
local puk = require 'std.puk'
puk.defer 'mini.pick' (function(pick) pick.setup {} end)
-- Direct require for eager loading:
-- local plugin = puk 'plugin_name'
std.prequire
Protected require; returns nil-safe proxy on failure.
local prequire = require 'std.prequire'
prequire('missing', function(foo) foo.setup() end) -- no crash
std.dbg
Debug logging via vim.g.bugabinga_debug_mode. All events written to
stdpath('state')/bugabinga_config_debug.jsonl (JSONL).
local dbg = require 'std.dbg'
dbg 'hello {1}' { 'world' } -- DEBUG event
dbg.kind('ERROR', 'something broke') -- ERROR event
dbg.toggle() -- toggle debug mode on/off
dbg.get() -- returns boolean
Placeholder Syntax
| Format | Meaning |
|---|---|
{N} |
Positional argument N (1-indexed) |
{["key"]} |
Table key (bracket notation for special chars) |
Examples:
dbg('sum: {1}', a + b)— positionaldbg('name: {1}, value: {2}', name, value)— indexeddbg('user: {["user.name"]}', user)— bracket notation
User Commands
| Command | Description |
|---|---|
:DbgModeToggle |
Toggle vim.g.bugabinga_debug_mode |
:DbgViewToggle |
Open or close the debug event viewer |
:DbgOpenFile |
Open the raw JSONL log file |
Gotcha
{1}is the first positional argument.{}is NOT valid — format parsing will error.
Code Style
Formatting (.editorconfig defaults)
- Indent: 2 spaces (no tabs)
- Quotes: Single quotes (
'string') - Line length: Max 120 characters
- End of line: LF (not CRLF)
- Trailing commas: Smart (keep in tables)
- Final newline: Yes
Lua Conventions
- No comments unless explicitly requested by user
- Module return: Return the value directly — no
local M = {}intermediate variable. Collect fields at the bottom and return the table. - Module patterns:
- Table:
return { foo = foo, bar = bar } - setmetatable callable:
return setmetatable({...}, {__call = func}) - Function:
return function(...) end - Side-effect (plugin setup): no return needed; if test/external access is
needed, create a
_G.CapitalizedNameglobal (see Module Export Pattern above)
- Table:
- Avoid:
local M = {}/function M.xxx()/return M— uselocal function xxx()and return the table at the end - Global table shadowing: Modules do
local table = require 'std.table'(nottable = require...) - Input validation: Use
vim.validateat function entry points - Error handling: Prefer
pcall+ proxy objects (std.prequirepattern) over rawrequire - Metatable DSLs: Builder/method-chaining patterns in
std.map,std.user_command,std.auto
Naming Conventions
- Files: snake_case (
statusline.lua,lua_ls.lua) - Functions/variables: snake_case (
find_root,git_branch) - Modules: snake_case (
require 'std.project') - Test files:
*_spec.luasuffix, mirror source path - Constants: SCREAMING_SNAKE_CASE for true constants (
MAX_TRAVERSAL_COUNT) - Private locals: Prefix with
_(_internal_helper)
Plugin Management
- Plugin list:
puk.plugins.nuon(Nushell data notation, declarative) - Plugin manager:
puk.nu(custom, out-of-band) - Lazy loading:
puk.defer 'plugin'schedules require onvim.schedule_wrap - Plugin modules: Each plugin gets a
require 'bugabinga.<plugin>'entry in init.lua
LSP Configuration
- Server files: Each in
lsp/<name>.lua(auto-discovered viavim.api.nvim_get_runtime_file 'lsp/*.lua') - LSP enable:
vim.lsp.enable(servers)loads all configs - Lua LS: Configured with LuaJIT runtime,
vimglobal, luv library - Attach:
LspAttachautocmd callslsp_attach()for buffer-local keymaps
Gotchas
justrequires Nushell (nu), not bash or zsh- Version gate:
init.luaenforces Neovim 0.11; early return on mismatch - Debug mode: Set
vim.g.bugabinga_debug_mode = trueorNVIM_PROFILE=1for profiling std.mapmodes: Must chain modes or provide full mode table; empty modes defaults tonormalpuk.deferis async: The callback runs onvim.schedule_wrap— not immediatestd.projectcaching: Results cached; clearrequire('std.project').cacheif neededlua_lsdiagnostics globals:vimis a declared global (not an error)- Conform formatexpr: Set via
vim.o.formatexprinformat.lua; format keymap is= - Test isolation: Each test file should handle
package.loadedcleanup inbefore_each/after_each - Windows WSL:
const.luaforcesos_background = 'light'on Win/WSL2 dbg()placeholder syntax:{}is NOT valid; use{1}for first positional argument (empty{}will error in format parsing)