When asked to add a feature, start with adding a failing test, then the feature.
mise run ci- Run lint, format check, and testsmise run lint- Typecheck with emmylua_checkmise run fmt- Format code with styluamise run fmt:check- Check code formattingmise run test- Run tests with busted
Single test: Use busted directly: busted --verbose --filter='"<FULL TEST NAME HERE>"'
Key Understanding: TextChanged autocmds don't fire in headless neovim due to typeahead/operator pending behavior:
TextChanged/TextChangedI/TextChangedPautocmds: Do NOT fire in headless mode, regardless of whether changes are programmatic (nvim_buf_set_lines) or simulated user input (feedkeys,vim.cmd.normal). According to:help TextChanged: "Not triggered when there is typeahead or when an operator is pending."nvim_buf_attachwithon_bytescallback: DOES fire reliably in all scenarios (headless, interactive, programmatic, user input).changedtick: Does increment for all buffer changes, providing a way to detect that changes occurred.
This is why tests require manual vim.cmd.doautocmd 'TextChanged' calls - the current implementation uses TextChanged autocmd to batch on_bytes events, but this autocmd never fires in headless test environments.
Current workaround in tests: Call vim.cmd.doautocmd 'TextChanged' manually after programmatic buffer changes to simulate the autocmd that would fire with real user input.
- Buffer cleanup: Tests use
with_buf()wrapper that explicitly callsvim.cmd.bdelete { bang = true }, which fires all deletion events (BufUnload,BufDelete,BufWipeout) regardless ofbufhiddensettings - Event testing: To test buffer deletion events, use explicit
vim.api.nvim_buf_delete(bufnr, { force = true })rather than Vim commands like:enewor:tabclose, which have inconsistent behavior depending onbufhidden
Note: Interactive nvim commands with input (like nvim --headless -c "..." where commands expect user input) will hang. Always use non-interactive commands or scripts.
- LuaJIT, do NOT use goto
- Use stylua with 2-space indentation, 100 char column width
- Prefer single quotes, auto-prefer single quotes
- No call parentheses for simple statements
- Collapse simple statements always
- Sort requires automatically
- Use EmmyLua type annotations (
--- @param,--- @return,--- @type) - Follow patterns in existing code:
morph.Ctx<Props, State> - Component functions should annotate props and state types
- Components: PascalCase (e.g.,
Counter,TodoList) - Functions/variables: snake_case
- Constants: UPPER_SNAKE_CASE
- Local variables: concise but descriptive
- Use
require 'module'(single quotes, no parentheses) - Group imports at top of file
- Use local aliases:
local Morph = require 'morph'
- Use pcall for error boundaries in tests
- Return empty string from event handlers to consume keypress
- Validate inputs in component functions
- Use context object (
ctx) for state management - Initialize state in
ctx.phase == 'mount'condition - Use
ctx:update(new_state)to trigger re-renders (ctx:refresh()is short-hand forctx:update(ctx.state)) - Return arrays/tables of elements, not strings with concatenation