Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/AGENTS.md

Raw
Rendered preview

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.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:
    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.

-- 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) — 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.<plugin>' entry in init.lua

LSP Configuration

  • Server files: Each in lsp/<name>.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)
# 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.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 = '<c-s>', 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.<plugin>'` entry in
  init.lua

## LSP Configuration

- **Server files**: Each in `lsp/<name>.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)