520 lines
16 KiB
Markdown
520 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
|