# 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 ` | 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.lua` suffix, mirror source path under `tests/` - **Test harness**: `tests/minimal_init.lua` preloads plenary and sets `package.path` - **Module cache**: Invalidate with `package.loaded['module'] = nil` before `require` - **Example test**: ```lua before_each(function() package.loaded['std.const'] = nil end) ``` ## Architecture Rules 1. **`std/`** — May only depend on itself and Neovim APIs. No plugins. 2. **`lua/bugabinga/`** — May depend on `std`, plugins, and Neovim APIs. 3. **`lua/patches/`** — Overrides to built-in Neovim Lua functions (loaded first). 4. **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. ```lua -- 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 `require`d 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. ```lua local auto = require 'std.auto' auto 'my_group' { events = 'VimEnter', command = function() print 'hello' end, } ``` ### std.map Fluent keymap builder; modes are chainable. ```lua local map = require 'std.map' map.normal { keys = 'gd', command = vim.lsp.buf.definition } map.insert { keys = '', command = vim.lsp.buf.signature_help } ``` ### std.user_command Fluent user command builder. ```lua 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. ```lua 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. ```lua 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). ```lua 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)` — positional - `dbg('name: {1}, value: {2}', name, value)` — indexed - `dbg('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.CapitalizedName` global (see Module Export Pattern above) - **Avoid**: `local M = {}` / `function M.xxx()` / `return M` — use `local function xxx()` and return the table at the end - **Global table shadowing**: Modules do `local table = require 'std.table'` (not `table = require...`) - **Input validation**: Use `vim.validate` at function entry points - **Error handling**: Prefer `pcall` + proxy objects (`std.prequire` pattern) over raw `require` - **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.lua` suffix, 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 on `vim.schedule_wrap` - **Plugin modules**: Each plugin gets a `require 'bugabinga.'` entry in init.lua ## LSP Configuration - **Server files**: Each in `lsp/.lua` (auto-discovered via `vim.api.nvim_get_runtime_file 'lsp/*.lua'`) - **LSP enable**: `vim.lsp.enable(servers)` loads all configs - **Lua LS**: Configured with LuaJIT runtime, `vim` global, luv library - **Attach**: `LspAttach` autocmd calls `lsp_attach()` for buffer-local keymaps ## Gotchas - **`just` requires Nushell** (`nu`), not bash or zsh - **Version gate**: `init.lua` enforces Neovim 0.11; early return on mismatch - **Debug mode**: Set `vim.g.bugabinga_debug_mode = true` or `NVIM_PROFILE=1` for profiling - **`std.map` modes**: Must chain modes or provide full mode table; empty modes defaults to `normal` - **`puk.defer` is async**: The callback runs on `vim.schedule_wrap` — not immediate - **`std.project` caching**: Results cached; clear `require('std.project').cache` if needed - **`lua_ls` diagnostics globals**: `vim` is a declared global (not an error) - **Conform formatexpr**: Set via `vim.o.formatexpr` in `format.lua`; format keymap is `=` - **Test isolation**: Each test file should handle `package.loaded` cleanup in `before_each`/`after_each` - **Windows WSL**: `const.lua` forces `os_background = 'light'` on Win/WSL2 - **`dbg()` placeholder syntax**: `{}` is NOT valid; use `{1}` for first positional argument (empty `{}` will error in format parsing)