Skip to content

Commit fe82c5e

Browse files
committed
unmount on Buf{Delete,Unload}, too, not just Wipeout
1 parent b2a91b4 commit fe82c5e

File tree

2 files changed

+51
-7
lines changed

2 files changed

+51
-7
lines changed

AGENTS.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,50 @@
1010

1111
**Single test**: Use busted directly: `busted --verbose --filter='"<FULL TEST NAME HERE>"'`
1212

13+
## Test Environment Notes
14+
15+
### TextChanged Autocmds in Headless Mode
16+
17+
**Key Understanding**: `TextChanged` autocmds don't fire in headless neovim due to typeahead/operator pending behavior:
18+
19+
- **`TextChanged`/`TextChangedI`/`TextChangedP` autocmds**: 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."*
20+
- **`nvim_buf_attach` with `on_bytes` callback**: DOES fire reliably in all scenarios (headless, interactive, programmatic, user input).
21+
- **`changedtick`**: Does increment for all buffer changes, providing a way to detect that changes occurred.
22+
23+
This is why tests require manual `r.buf_watcher.fire()` calls - the current implementation uses `TextChanged` autocmd to batch `on_bytes` events, but this autocmd never fires in headless test environments.
24+
25+
**Current workaround in tests**: Call `r.buf_watcher.fire()` manually after programmatic buffer changes to simulate the autocmd that would fire with real user input.
26+
27+
### Buffer Event Testing
28+
29+
- **Buffer cleanup**: Tests use `with_buf()` wrapper that explicitly calls `vim.cmd.bdelete { bang = true }`, which fires all deletion events (`BufUnload`, `BufDelete`, `BufWipeout`) regardless of `bufhidden` settings
30+
- **Event testing**: To test buffer deletion events, use explicit `vim.api.nvim_buf_delete(bufnr, { force = true })` rather than Vim commands like `:enew` or `:tabclose`, which have inconsistent behavior depending on `bufhidden`
31+
32+
### Testing Neovim Behaviors Interactively
33+
34+
When you need to quickly test Neovim behaviors outside of the formal test suite:
35+
36+
1. **Create a test script**: Create a standalone `.lua` file (e.g., `test_behavior.lua`) in the project root
37+
2. **Write the test**: Use standard Lua and Neovim API calls. The script should end with `vim.cmd.qall { bang = true }` to exit
38+
3. **Run the test**: Execute with `nvim --headless -u NONE -c "set rtp+=." -c "luafile test_behavior.lua" 2>&1`
39+
- `--headless`: Run without UI
40+
- `-u NONE`: Don't load user config
41+
- `-c "set rtp+=."`: Add current directory to runtime path so `require 'morph'` works
42+
- `-c "luafile test_behavior.lua"`: Execute your test script
43+
- `2>&1`: Capture all output
44+
4. **Clean up**: Delete test files when done (they should not be committed)
45+
46+
**Example test script**:
47+
```lua
48+
#!/usr/bin/env nvim -l
49+
local Morph = require 'morph'
50+
-- Your test code here
51+
vim.print('Testing something...')
52+
vim.cmd.qall { bang = true }
53+
```
54+
55+
**Note**: Interactive nvim commands with input (like `nvim --headless -c "..."` where commands expect user input) will hang. Always use non-interactive commands or scripts.
56+
1357
## Code Style Guidelines
1458

1559
### Formatting
@@ -20,7 +64,7 @@
2064
- Sort requires automatically
2165

2266
### Type Annotations
23-
- Use EmmyLua type annotations (`---@param`, `---@return`, `---@type`)
67+
- Use EmmyLua type annotations (`--- @param`, `--- @return`, `--- @type`)
2468
- Follow patterns in existing code: `morph.Ctx<Props, State>`
2569
- Component functions should annotate props and state types
2670

lua/morph.lua

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -544,15 +544,15 @@ function Morph.new(bufnr)
544544
)
545545
table.insert(self.cleanup_hooks, self.buf_watcher.cleanup)
546546

547-
local wipeout_id = vim.api.nvim_create_autocmd('BufWipeout', {
547+
local cleanup_autocmd = vim.api.nvim_create_autocmd({ 'BufDelete', 'BufUnload', 'BufWipeout' }, {
548548
buffer = self.bufnr,
549549
callback = function()
550550
for _, cleanup_hook in ipairs(self.cleanup_hooks) do
551551
cleanup_hook()
552552
end
553553
end,
554554
})
555-
table.insert(self.cleanup_hooks, function() vim.api.nvim_del_autocmd(wipeout_id) end)
555+
table.insert(self.cleanup_hooks, function() vim.api.nvim_del_autocmd(cleanup_autocmd) end)
556556

557557
return self
558558
end
@@ -862,16 +862,16 @@ function Morph:mount(tree)
862862
end
863863
end
864864

865-
-- Don't track this autocmd in cleanup_hooks, because the prior BufWipeout
865+
-- Don't track this autocmd in cleanup_hooks, because the prior BufDelete/BufUnload/BufWipeout
866866
-- will take priority, and will delete this autocmd before it even has a
867867
-- chance to run:
868-
local wipeout_id
869-
wipeout_id = vim.api.nvim_create_autocmd('BufWipeout', {
868+
local unmount_autocmd_id
869+
unmount_autocmd_id = vim.api.nvim_create_autocmd({ 'BufDelete', 'BufUnload', 'BufWipeout' }, {
870870
buffer = self.bufnr,
871871
callback = function()
872872
-- Effectively unmount everything:
873873
H2.visit_tree(self.component_tree.old, nil)
874-
vim.api.nvim_del_autocmd(wipeout_id)
874+
vim.api.nvim_del_autocmd(unmount_autocmd_id)
875875
end,
876876
})
877877

0 commit comments

Comments
 (0)