Add two new commands for bracket navigation: repeat last and reverse last navigation. This reduces repetitive typing and improves user experience when navigating through diagnostics, spelling, and quickfix items.
519 lines
16 KiB
Markdown
519 lines
16 KiB
Markdown
# Navigation Enhancement Plans
|
||
|
||
**Status**: Two complementary approaches being considered (not mutually exclusive)
|
||
|
||
Both approaches aim to reduce repetitive typing when navigating with Neovim's bracket-based pairs (`[c`, `]d`, etc.).
|
||
|
||
---
|
||
|
||
## Option 1: Repeat/Reverse Last Bracket Navigation
|
||
|
||
### Concept
|
||
Two simple commands that remember and replay the last `[x` or `]x` navigation:
|
||
- **Repeat**: Execute the same navigation again (e.g., `]d` → `.` → `.` → `.`)
|
||
- **Reverse**: Execute the opposite direction (e.g., after `]d`, press `,` → `[d`)
|
||
|
||
Similar to `;` and `,` for repeating/reversing `f/F/t/T` motions.
|
||
|
||
### User Experience
|
||
|
||
```vim
|
||
" Example 1: Scanning diagnostics
|
||
]d " Next diagnostic
|
||
. " Next diagnostic (repeat)
|
||
. " Next diagnostic (repeat)
|
||
. " Next diagnostic (repeat)
|
||
, " Previous diagnostic (reverse, oops went too far)
|
||
|
||
" Example 2: Checking spelling
|
||
]s " Next misspelling
|
||
. " Next misspelling
|
||
. " Next misspelling
|
||
|
||
" Example 3: Quickfix workflow
|
||
]q " Next quickfix item
|
||
. " Next quickfix
|
||
. " Next quickfix
|
||
```
|
||
|
||
### When This Works Best
|
||
- Repeatedly navigating **same type**: diagnostics, spelling, quickfix, location list
|
||
- Linear scanning through items of one kind
|
||
- Quick corrections when you overshoot (reverse)
|
||
|
||
### Implementation
|
||
|
||
**Module**: `lua/bracket-repeat.lua`
|
||
|
||
```lua
|
||
local M = {}
|
||
|
||
-- Track last bracket navigation
|
||
local last_nav = {
|
||
prefix = nil, -- '[' or ']'
|
||
key = nil, -- 'c', 'd', 'm', 's', 'q', etc.
|
||
}
|
||
|
||
--- Setup wrapper mappings to track bracket navigation
|
||
function M.setup()
|
||
-- Common bracket pairs to track
|
||
local pairs = {
|
||
'c', -- Git hunks (gitsigns)
|
||
'd', -- Diagnostics
|
||
's', -- Spelling
|
||
'q', -- Quickfix
|
||
'l', -- Location list
|
||
't', -- Tags
|
||
'm', -- Methods (treesitter)
|
||
'f', -- Functions (treesitter)
|
||
'p', -- Parameters (treesitter)
|
||
}
|
||
|
||
for _, key in ipairs(pairs) do
|
||
-- Wrap [x to track
|
||
vim.keymap.set('n', '[' .. key, function()
|
||
last_nav.prefix = '['
|
||
last_nav.key = key
|
||
return '[' .. key
|
||
end, { expr = true, silent = true })
|
||
|
||
-- Wrap ]x to track
|
||
vim.keymap.set('n', ']' .. key, function()
|
||
last_nav.prefix = ']'
|
||
last_nav.key = key
|
||
return ']' .. key
|
||
end, { expr = true, silent = true })
|
||
end
|
||
end
|
||
|
||
--- Repeat last bracket navigation
|
||
function M.repeat_last()
|
||
if not last_nav.prefix or not last_nav.key then
|
||
vim.notify('No bracket navigation to repeat', vim.log.levels.WARN)
|
||
return
|
||
end
|
||
vim.cmd('normal! ' .. last_nav.prefix .. last_nav.key)
|
||
end
|
||
|
||
--- Reverse last bracket navigation (flip direction)
|
||
function M.reverse_last()
|
||
if not last_nav.prefix or not last_nav.key then
|
||
vim.notify('No bracket navigation to reverse', vim.log.levels.WARN)
|
||
return
|
||
end
|
||
local opposite = last_nav.prefix == '[' and ']' or '['
|
||
vim.cmd('normal! ' .. opposite .. last_nav.key)
|
||
-- Update tracking to reflect the reversal
|
||
last_nav.prefix = opposite
|
||
end
|
||
|
||
return M
|
||
```
|
||
|
||
**Integration**: `init.lua` or `lua/keymaps.lua`
|
||
|
||
```lua
|
||
-- Setup tracking
|
||
require('bracket-repeat').setup()
|
||
|
||
-- Keybindings (choose one option)
|
||
|
||
-- Option A: Override ; and , (loses f/F/t/T repeat, but very ergonomic)
|
||
vim.keymap.set('n', ';', function() require('bracket-repeat').repeat_last() end,
|
||
{ desc = 'Repeat bracket navigation' })
|
||
vim.keymap.set('n', ',', function() require('bracket-repeat').reverse_last() end,
|
||
{ desc = 'Reverse bracket navigation' })
|
||
|
||
-- Option B: Use z prefix (keeps ; and , for f/t motions)
|
||
vim.keymap.set('n', 'z.', function() require('bracket-repeat').repeat_last() end,
|
||
{ desc = 'Repeat bracket navigation' })
|
||
vim.keymap.set('n', 'z,', function() require('bracket-repeat').reverse_last() end,
|
||
{ desc = 'Reverse bracket navigation' })
|
||
|
||
-- Option C: Use leader
|
||
vim.keymap.set('n', '<leader>.', function() require('bracket-repeat').repeat_last() end,
|
||
{ desc = 'Repeat bracket navigation' })
|
||
vim.keymap.set('n', '<leader>,', function() require('bracket-repeat').reverse_last() end,
|
||
{ desc = 'Reverse bracket navigation' })
|
||
```
|
||
|
||
### Pros & Cons
|
||
|
||
**Pros:**
|
||
- ✅ Very simple (~40 lines)
|
||
- ✅ No mode switching, stays in normal mode
|
||
- ✅ Familiar pattern (like `;`/`,` for `f`/`t`)
|
||
- ✅ Works with natural workflow
|
||
- ✅ Easy to add more tracked pairs
|
||
|
||
**Cons:**
|
||
- ⚠️ Only handles one "type" at a time (can't easily switch between `]d` and `]c`)
|
||
- ⚠️ Requires calling `.setup()` to track pairs
|
||
- ⚠️ Might want `;`/`,` for `f`/`t` repeat (depends on keybinding choice)
|
||
|
||
### Estimated Complexity
|
||
- **Total**: ~40 lines
|
||
- **Time**: 15-30 minutes
|
||
|
||
---
|
||
|
||
## Option 2: Navigation Mode (getchar Loop)
|
||
|
||
## User Experience
|
||
|
||
### Entry
|
||
- `z[` - Enter navigation mode with `[` prefix active (backward)
|
||
- `z]` - Enter navigation mode with `]` prefix active (forward)
|
||
|
||
### In Mode
|
||
- All letter keys (`a-z`, `A-Z`) get prefixed with current bracket
|
||
- Examples:
|
||
- Press `c` → executes `[c` or `]c` (git hunks)
|
||
- Press `d` → executes `[d` or `]d` (diagnostics)
|
||
- Press `m` → executes `[m` or `]m` (methods)
|
||
- Press `f` → executes `[f` or `]f` (functions)
|
||
- Press `b` → executes `[b` or `]b` (buffers)
|
||
- Press `q` → executes `[q` or `]q` (quickfix)
|
||
|
||
### Toggle Prefix
|
||
- `[` - Switch to `[` prefix (backward navigation)
|
||
- `]` - Switch to `]` prefix (forward navigation)
|
||
|
||
### Exit
|
||
- `<Esc>` - Exit navigation mode and return to normal editing
|
||
|
||
### When This Works Best
|
||
- Switching between **different types** of navigation (`]d` → `]c` → `]m`)
|
||
- Want visual feedback showing current direction
|
||
- Prefer a dedicated "navigation state"
|
||
- Mixed workflow: `]q` to quickfix, then multiple `]c` for hunks (stay in mode, switch keys)
|
||
|
||
### Example Workflow
|
||
|
||
```vim
|
||
" Mixed navigation scenario
|
||
z] " Enter forward navigation mode (shows: NAV ] →:)
|
||
q " Execute ]q (next quickfix)
|
||
c " Execute ]c (next hunk)
|
||
c " Execute ]c (next hunk)
|
||
[ " Toggle to backward (shows: NAV [ ←:)
|
||
c " Execute [c (previous hunk)
|
||
d " Execute [d (previous diagnostic)
|
||
<Esc> " Exit mode
|
||
```
|
||
|
||
## Technical Implementation
|
||
|
||
### Approach: getchar() Loop (No Remapping)
|
||
|
||
Instead of remapping keys, use a `while` loop with `vim.fn.getchar()` to read keypresses and manually execute the prefixed commands.
|
||
|
||
**Why this approach:**
|
||
- ✅ Zero mapping conflicts (never touches existing keymaps)
|
||
- ✅ Simpler implementation (~50-80 lines vs ~150-250)
|
||
- ✅ Built-in visual feedback via `vim.api.nvim_echo()`
|
||
- ✅ Impossible to leak state (no cleanup needed if crashed)
|
||
- ✅ Predictable behavior (each keypress isolated)
|
||
- ✅ Naturally handles special keys
|
||
|
||
**The "blocking" behavior is actually desired** - you're in a focused navigation mode, press `<Esc>` to exit anytime.
|
||
|
||
### Common Bracket Navigation Pairs
|
||
- `[c`/`]c` - Previous/next git hunk (gitsigns)
|
||
- `[d`/`]d` - Previous/next diagnostic
|
||
- `[b`/`]b` - Previous/next buffer (custom mapping)
|
||
- `[q`/`]q` - Previous/next quickfix item
|
||
- `[l`/`]l` - Previous/next location list item
|
||
- `[m`/`]m` - Previous/next method (treesitter textobjects)
|
||
- `[f`/`]f` - Previous/next function (treesitter textobjects)
|
||
- `[p`/`]p` - Previous/next parameter (treesitter textobjects)
|
||
- `[t`/`]t` - Previous/next tag
|
||
|
||
## Implementation Details
|
||
|
||
### Module Structure
|
||
Create `lua/navigation-mode.lua`:
|
||
|
||
```lua
|
||
local M = {}
|
||
|
||
--- Enter navigation mode with getchar() loop
|
||
--- @param prefix string Either '[' or ']'
|
||
function M.enter(prefix)
|
||
prefix = prefix or '['
|
||
|
||
while true do
|
||
-- Display prompt
|
||
local hl = prefix == '[' and 'DiagnosticInfo' or 'DiagnosticHint'
|
||
local arrow = prefix == '[' and '←' or '→'
|
||
vim.api.nvim_echo({{string.format('NAV %s %s: ', prefix, arrow), hl}}, false, {})
|
||
|
||
-- Get next keypress
|
||
local ok, char = pcall(vim.fn.getchar)
|
||
if not ok then break end -- Handle <C-c> gracefully
|
||
|
||
-- Convert to string
|
||
local key = type(char) == 'number' and vim.fn.nr2char(char) or char
|
||
|
||
-- Handle special keys
|
||
if key == '\27' then -- ESC
|
||
break
|
||
elseif key == '[' then
|
||
prefix = '['
|
||
elseif key == ']' then
|
||
prefix = ']'
|
||
else
|
||
-- Execute prefix + key as normal mode command
|
||
local cmd = prefix .. key
|
||
vim.cmd('normal! ' .. vim.api.nvim_replace_termcodes(cmd, true, false, true))
|
||
end
|
||
end
|
||
|
||
-- Clear prompt
|
||
vim.api.nvim_echo({{'', 'Normal'}}, false, {})
|
||
end
|
||
|
||
return M
|
||
```
|
||
|
||
**Key Points:**
|
||
- No state management needed (self-contained loop)
|
||
- `pcall(vim.fn.getchar)` handles `<C-c>` interrupts gracefully
|
||
- `vim.api.nvim_replace_termcodes()` ensures special key sequences work
|
||
- Visual feedback built into the loop (shows `NAV [ ←:` or `NAV ] →:`)
|
||
- Press `[` or `]` to toggle prefix without exiting
|
||
- Press `<Esc>` (or `<C-c>`) to exit
|
||
|
||
### Integration in keymaps.lua
|
||
```lua
|
||
-- Navigation mode
|
||
vim.keymap.set('n', 'z[', function()
|
||
require('navigation-mode').enter('[')
|
||
end, { desc = 'Enter navigation mode (backward)' })
|
||
|
||
vim.keymap.set('n', 'z]', function()
|
||
require('navigation-mode').enter(']')
|
||
end, { desc = 'Enter navigation mode (forward)' })
|
||
```
|
||
|
||
## Visual Feedback
|
||
|
||
Built into the getchar() loop:
|
||
- Shows `NAV [ ←:` (in blue) when in backward mode
|
||
- Shows `NAV ] →:` (in teal) when in forward mode
|
||
- Prompt updates immediately when toggling with `[` or `]`
|
||
- Clears when exiting with `<Esc>`
|
||
|
||
No additional statusline integration needed - the command line prompt is clear and non-intrusive.
|
||
|
||
## Design Decisions
|
||
|
||
### Mapping Conflicts: Solved
|
||
No mapping conflicts possible - getchar() reads raw input without touching keymaps.
|
||
|
||
### Buffer-local Mappings: Not Applicable
|
||
Loop executes `normal! [key` which uses whatever mappings exist naturally.
|
||
|
||
### Visual Feedback: Built-in
|
||
Command line prompt is sufficient and non-intrusive.
|
||
|
||
### Number Keys
|
||
Currently not prefixed - this allows using counts if a prefixed command accepts them.
|
||
Example: `3c` → executes `[3c` or `]3c` (may not be useful, but won't break anything)
|
||
|
||
Could add special handling if needed:
|
||
```lua
|
||
if key:match('^%d$') then
|
||
-- Handle numbers specially
|
||
end
|
||
```
|
||
|
||
### Operators (d, c, y, etc.)
|
||
In navigation mode, `d` executes `[d` (diagnostic navigation), not delete operator.
|
||
This is desired behavior - use `<Esc>` to exit and edit normally.
|
||
|
||
### Error Handling
|
||
If a command doesn't exist (e.g., `[z`), Vim will show an error but mode continues.
|
||
User can press `<Esc>` to exit or try another key.
|
||
|
||
### Pros & Cons
|
||
|
||
**Pros:**
|
||
- ✅ Switch between different navigation types easily (`c` → `d` → `m`)
|
||
- ✅ Visual feedback (command line prompt)
|
||
- ✅ No mapping conflicts
|
||
- ✅ Clean state (nothing to cleanup if interrupted)
|
||
- ✅ Natural for rapid mixed navigation
|
||
|
||
**Cons:**
|
||
- ⚠️ Requires mode switching (mental overhead)
|
||
- ⚠️ Blocking loop (though this is by design)
|
||
- ⚠️ Need to remember entry/exit keys
|
||
|
||
### Estimated Complexity
|
||
- **Total**: ~50-80 lines
|
||
- **Time**: 30-60 minutes
|
||
|
||
---
|
||
|
||
## Comparison & Recommendation
|
||
|
||
| Aspect | Repeat/Reverse | Navigation Mode |
|
||
|--------|---------------|----------------|
|
||
| **Best for** | Same-type scanning | Mixed navigation |
|
||
| **Complexity** | ~40 lines | ~50-80 lines |
|
||
| **Mental model** | Like `;`/`,` | New mode |
|
||
| **Typing (4× same nav)** | `]d ...` (4 keys) | `z] dddd <Esc>` (9 keys) |
|
||
| **Typing (mixed nav)** | `]d ]d ]c ]c` (8 keys) | `z] ddcc <Esc>` (9 keys) |
|
||
| **Mode switching** | No | Yes |
|
||
| **Visual feedback** | No (unless added) | Yes (built-in) |
|
||
|
||
### Use Cases
|
||
|
||
**Repeat/Reverse excels at:**
|
||
- Scanning diagnostics: `]d . . . .`
|
||
- Spell checking: `]s . . . .`
|
||
- Reviewing quickfix: `]q . . . .`
|
||
- Going back: `. . . , , ,`
|
||
|
||
**Navigation Mode excels at:**
|
||
- Mixed workflow: `]q` → multiple `]c` hunks → check `]d` diagnostic
|
||
- When you want visual confirmation of direction
|
||
- Exploring unfamiliar code (trying different navigation types)
|
||
|
||
### Implementation Strategy
|
||
|
||
**Both can coexist!** They solve slightly different problems:
|
||
|
||
1. **Start with Repeat/Reverse** (simpler, covers 80% of cases)
|
||
- Use for linear scanning (diagnostics, spelling, quickfix)
|
||
- Bind to `z.` and `z,` (or `;`/`,` if you don't use `f`/`t` repeat often)
|
||
|
||
2. **Add Navigation Mode later** (optional, for mixed navigation)
|
||
- Use when you need to rapidly switch types
|
||
- Bind to `z[` and `z]`
|
||
|
||
You'll naturally reach for whichever fits the situation better.
|
||
|
||
---
|
||
|
||
## Common Bracket Pairs Reference
|
||
|
||
- `[c`/`]c` - Previous/next git hunk (gitsigns)
|
||
- `[d`/`]d` - Previous/next diagnostic
|
||
- `[s`/`]s` - Previous/next misspelling
|
||
- `[q`/`]q` - Previous/next quickfix item
|
||
- `[l`/`]l` - Previous/next location list item
|
||
- `[m`/`]m` - Previous/next method (treesitter textobjects)
|
||
- `[f`/`]f` - Previous/next function (treesitter textobjects)
|
||
- `[p`/`]p` - Previous/next parameter (treesitter textobjects)
|
||
- `[t`/`]t` - Previous/next tag
|
||
- `[b`/`]b` - Previous/next buffer (if custom mapping exists)
|
||
|
||
---
|
||
|
||
## Testing Checklists
|
||
|
||
### Repeat/Reverse Testing
|
||
- [ ] Setup completes without errors
|
||
- [ ] `]d` followed by `.` repeats diagnostic navigation
|
||
- [ ] `]s` followed by `.` repeats spelling navigation
|
||
- [ ] `]q` followed by `.` repeats quickfix navigation
|
||
- [ ] `,` reverses last navigation direction
|
||
- [ ] Warning shown when no navigation to repeat/reverse
|
||
- [ ] Works across different buffers
|
||
- [ ] Multiple reverses work: `. . . , , ,`
|
||
|
||
### Navigation Mode Testing
|
||
- [ ] Enter mode with `z[` and `z]`
|
||
- [ ] Verify visual prompt shows `NAV [ ←:` or `NAV ] →:`
|
||
- [ ] Press `c` → navigates to previous/next git hunk
|
||
- [ ] Press `d` → navigates to previous/next diagnostic
|
||
- [ ] Press `m` → navigates to previous/next method
|
||
- [ ] Press `f` → navigates to previous/next function
|
||
- [ ] Toggle with `[` → prompt updates to `NAV [ ←:`
|
||
- [ ] Toggle with `]` → prompt updates to `NAV ] →:`
|
||
- [ ] Exit with `<Esc>` → prompt clears, back to normal mode
|
||
- [ ] Exit with `<C-c>` → handles gracefully
|
||
- [ ] Works across different buffers
|
||
- [ ] Invalid keys (e.g., `z`) show error but don't crash
|
||
- [ ] Existing keymaps still work after exit
|
||
|
||
---
|
||
|
||
## Implementation Priority
|
||
|
||
**Recommended order:**
|
||
|
||
1. **Phase 1: Repeat/Reverse** (start here)
|
||
- Simpler, faster to implement
|
||
- Covers most common use cases
|
||
- Get immediate value
|
||
|
||
2. **Phase 2: Navigation Mode** (optional, evaluate need)
|
||
- Implement only if you find yourself wanting mixed navigation
|
||
- Can be added anytime without conflicts
|
||
- Both features work together
|
||
|
||
**Time estimate:**
|
||
- Phase 1: 15-30 minutes
|
||
- Phase 2: 30-60 minutes
|
||
- Total: 45-90 minutes if both implemented
|
||
|
||
---
|
||
|
||
## Implementation Steps
|
||
|
||
### For Repeat/Reverse (Start Here)
|
||
|
||
1. **Create `lua/bracket-repeat.lua`** (~40 lines)
|
||
- Implement tracking state
|
||
- Implement `setup()`, `repeat_last()`, `reverse_last()`
|
||
|
||
2. **Add to `init.lua`**
|
||
```lua
|
||
require('bracket-repeat').setup()
|
||
```
|
||
|
||
3. **Add keymaps** (`lua/keymaps.lua`)
|
||
```lua
|
||
vim.keymap.set('n', 'z.', function() require('bracket-repeat').repeat_last() end)
|
||
vim.keymap.set('n', 'z,', function() require('bracket-repeat').reverse_last() end)
|
||
```
|
||
|
||
4. **Test** with diagnostics, spelling, quickfix
|
||
|
||
5. **Document** in README.md
|
||
|
||
### For Navigation Mode (Optional Later)
|
||
|
||
1. **Create `lua/navigation-mode.lua`** (~50 lines)
|
||
- Implement `M.enter(prefix)` with getchar() loop
|
||
- Handle ESC, `[`, `]`, and general keys
|
||
- Add visual feedback via `nvim_echo`
|
||
|
||
2. **Add keymaps** (`lua/keymaps.lua`)
|
||
```lua
|
||
vim.keymap.set('n', 'z[', function()
|
||
require('navigation-mode').enter('[')
|
||
end, { desc = 'Navigation mode (backward)' })
|
||
|
||
vim.keymap.set('n', 'z]', function()
|
||
require('navigation-mode').enter(']')
|
||
end, { desc = 'Navigation mode (forward)' })
|
||
```
|
||
|
||
3. **Test basic functionality**
|
||
- Enter with `z[` / `z]`
|
||
- Navigate with `c`, `d`, `m`, `f`, `q`
|
||
- Toggle prefix with `[` / `]`
|
||
- Exit with `<Esc>`
|
||
|
||
4. **Polish** (optional)
|
||
- Add help text on first use
|
||
- Handle `<C-c>` gracefully (already done with `pcall`)
|
||
- Consider adding common navigation cheatsheet
|
||
|
||
5. **Document**
|
||
- Update `README.md` with new keymaps
|
||
- Add navigation pairs reference
|