16 KiB
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
" 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
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
-- 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
;/,forf/t) - ✅ Works with natural workflow
- ✅ Easy to add more tracked pairs
Cons:
- ⚠️ Only handles one "type" at a time (can't easily switch between
]dand]c) - ⚠️ Requires calling
.setup()to track pairs - ⚠️ Might want
;/,forf/trepeat (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[cor]c(git hunks) - Press
d→ executes[dor]d(diagnostics) - Press
m→ executes[mor]m(methods) - Press
f→ executes[for]f(functions) - Press
b→ executes[bor]b(buffers) - Press
q→ executes[qor]q(quickfix)
- Press
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:
]qto quickfix, then multiple]cfor hunks (stay in mode, switch keys)
Example Workflow
" 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:
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 gracefullyvim.api.nvim_replace_termcodes()ensures special key sequences work- Visual feedback built into the loop (shows
NAV [ ←:orNAV ] →:) - Press
[or]to toggle prefix without exiting - Press
<Esc>(or<C-c>) to exit
Integration in keymaps.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:
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]chunks → check]ddiagnostic - When you want visual confirmation of direction
- Exploring unfamiliar code (trying different navigation types)
Implementation Strategy
Both can coexist! They solve slightly different problems:
-
Start with Repeat/Reverse (simpler, covers 80% of cases)
- Use for linear scanning (diagnostics, spelling, quickfix)
- Bind to
z.andz,(or;/,if you don't usef/trepeat often)
-
Add Navigation Mode later (optional, for mixed navigation)
- Use when you need to rapidly switch types
- Bind to
z[andz]
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
]dfollowed by.repeats diagnostic navigation]sfollowed by.repeats spelling navigation]qfollowed 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[andz] - Verify visual prompt shows
NAV [ ←:orNAV ] →: - 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 toNAV [ ←: - Toggle with
]→ prompt updates toNAV ] →: - 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:
-
Phase 1: Repeat/Reverse (start here)
- Simpler, faster to implement
- Covers most common use cases
- Get immediate value
-
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)
-
Create
lua/bracket-repeat.lua(~40 lines)- Implement tracking state
- Implement
setup(),repeat_last(),reverse_last()
-
Add to
init.luarequire('bracket-repeat').setup() -
Add keymaps (
lua/keymaps.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) -
Test with diagnostics, spelling, quickfix
-
Document in README.md
For Navigation Mode (Optional Later)
-
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
- Implement
-
Add keymaps (
lua/keymaps.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)' }) -
Test basic functionality
- Enter with
z[/z] - Navigate with
c,d,m,f,q - Toggle prefix with
[/] - Exit with
<Esc>
- Enter with
-
Polish (optional)
- Add help text on first use
- Handle
<C-c>gracefully (already done withpcall) - Consider adding common navigation cheatsheet
-
Document
- Update
README.mdwith new keymaps - Add navigation pairs reference
- Update