From 2a516e353bd178aa4f0a14f5f3cbc4524a1d8572 Mon Sep 17 00:00:00 2001 From: ray Date: Sun, 18 Jan 2026 13:38:42 +0000 Subject: [PATCH] Enhance navigation with repeat and reverse functionality 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. --- NAVIGATION_MODE_PLAN.md | 565 ++++++++++++++++++++++++++++++---------- 1 file changed, 434 insertions(+), 131 deletions(-) diff --git a/NAVIGATION_MODE_PLAN.md b/NAVIGATION_MODE_PLAN.md index 608f3cd..d0f21db 100644 --- a/NAVIGATION_MODE_PLAN.md +++ b/NAVIGATION_MODE_PLAN.md @@ -1,7 +1,163 @@ -# Navigation Mode Feature Plan +# Navigation Enhancement Plans -## Concept -A custom navigation mode where keypresses are automatically prefixed with `[` or `]`, making it easier to navigate using Neovim's bracket-based navigation pairs without repeatedly typing brackets. +**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', '.', 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' }) +``` + +### 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 @@ -24,28 +180,43 @@ A custom navigation mode where keypresses are automatically prefixed with `[` or - `]` - Switch to `]` prefix (forward navigation) ### Exit -- `` - Exit navigation mode and restore normal mappings +- `` - 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) + " Exit mode +``` ## Technical Implementation -### State Management -```lua -local NavMode = { - active = false, - prefix = '[', -- '[' or ']' -} -``` +### Approach: getchar() Loop (No Remapping) -### Key Remapping Strategy -1. Store original mappings for letters a-z, A-Z -2. On mode entry, create new mappings: `key → prefix .. key` -3. On mode exit, restore original mappings -4. On prefix toggle, update all mappings with new prefix +Instead of remapping keys, use a `while` loop with `vim.fn.getchar()` to read keypresses and manually execute the prefixed commands. -### Keys to Remap -- Lowercase: `a-z` (26 keys) -- Uppercase: `A-Z` (26 keys) -- Total: 52 keys dynamically remapped +**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 `` to exit anytime. ### Common Bracket Navigation Pairs - `[c`/`]c` - Previous/next git hunk (gitsigns) @@ -66,74 +237,53 @@ Create `lua/navigation-mode.lua`: ```lua local M = {} -local state = { - active = false, - prefix = '[', - stored_mappings = {}, -} - -local LETTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - +--- Enter navigation mode with getchar() loop +--- @param prefix string Either '[' or ']' function M.enter(prefix) - if state.active then return end + prefix = prefix or '[' - state.prefix = prefix or '[' - state.active = true - - -- Store original mappings and create prefixed ones - for i = 1, #LETTERS do - local key = LETTERS:sub(i, i) - -- Store original mapping (if exists) - -- Create new mapping: key → prefix .. key - vim.keymap.set('n', key, state.prefix .. key, { noremap = true, silent = true }) + 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 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 - -- Special mappings for [ and ] to toggle prefix - vim.keymap.set('n', '[', function() M.set_prefix('[') end, { noremap = true, silent = true }) - vim.keymap.set('n', ']', function() M.set_prefix(']') end, { noremap = true, silent = true }) - - -- Exit mapping - vim.keymap.set('n', '', function() M.exit() end, { noremap = true, silent = true }) - - -- TODO: Set visual feedback (statusline, notification, etc.) -end - -function M.set_prefix(new_prefix) - if not state.active then return end - state.prefix = new_prefix - - -- Remap all letters with new prefix - for i = 1, #LETTERS do - local key = LETTERS:sub(i, i) - vim.keymap.set('n', key, state.prefix .. key, { noremap = true, silent = true }) - end - - -- TODO: Update visual feedback -end - -function M.exit() - if not state.active then return end - - -- Restore original mappings - for i = 1, #LETTERS do - local key = LETTERS:sub(i, i) - vim.keymap.del('n', key) - -- Restore stored mapping if it existed - end - - -- Clean up special mappings - vim.keymap.del('n', '[') - vim.keymap.del('n', ']') - vim.keymap.del('n', '') - - state.active = false - - -- TODO: Clear visual feedback + -- 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 `` 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 `` (or ``) to exit + ### Integration in keymaps.lua ```lua -- Navigation mode @@ -146,71 +296,224 @@ vim.keymap.set('n', 'z]', function() end, { desc = 'Enter navigation mode (forward)' }) ``` -## Visual Feedback Options (TODO) +## Visual Feedback -Need to decide on one or more: +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 `` -1. **Statusline indicator**: Show `[NAV ←]` or `[NAV →]` in statusline -2. **Notification**: Brief message on mode entry/exit -3. **Command line**: `echo` message showing current prefix -4. **Cursor highlight**: Change cursor color/shape -5. **Virtual text**: Floating indicator in corner of window +No additional statusline integration needed - the command line prompt is clear and non-intrusive. -## Open Questions +## Design Decisions -1. **Mapping conflicts**: How to handle if a letter already has a mapping? - - Overwrite temporarily? - - Skip that letter? - - Warn user? +### Mapping Conflicts: Solved +No mapping conflicts possible - getchar() reads raw input without touching keymaps. -2. **Buffer-local mappings**: Should mode respect buffer-local mappings? - - Store and restore per-buffer? - - Global mode only? +### Buffer-local Mappings: Not Applicable +Loop executes `normal! [key` which uses whatever mappings exist naturally. -3. **Visual feedback**: Which approach is clearest without being intrusive? +### Visual Feedback: Built-in +Command line prompt is sufficient and non-intrusive. -4. **Number keys**: Should `0-9` also be prefixed? - - Useful for some navigation pairs - - But might conflict with counts +### 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) -5. **Operators**: Should `d`, `c`, `y` still work as operators or only as navigation? - - Current plan: They become navigation only while in mode - - Trade-off: Can't delete/change while navigating +Could add special handling if needed: +```lua +if key:match('^%d$') then + -- Handle numbers specially +end +``` -## Implementation Phases +### Operators (d, c, y, etc.) +In navigation mode, `d` executes `[d` (diagnostic navigation), not delete operator. +This is desired behavior - use `` to exit and edit normally. -1. **Core functionality** (essential) - - Mode enter/exit - - Key remapping for a-z - - Prefix toggle - - Basic state management +### Error Handling +If a command doesn't exist (e.g., `[z`), Vim will show an error but mode continues. +User can press `` to exit or try another key. -2. **Visual feedback** (important) - - Choose and implement feedback mechanism - - Ensure clarity when mode is active +### Pros & Cons -3. **Edge cases** (polish) - - Handle existing mappings gracefully - - Buffer-local mapping preservation - - Mode timeout/auto-exit? +**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 -4. **Documentation** (completion) - - Update README.md with keymaps - - Add examples of useful navigation pairs - - Consider adding to help docs +**Cons:** +- ⚠️ Requires mode switching (mental overhead) +- ⚠️ Blocking loop (though this is by design) +- ⚠️ Need to remember entry/exit keys -## Estimated Complexity -- **Core**: ~100-150 lines of Lua -- **Visual feedback**: +20-50 lines depending on approach -- **Edge case handling**: +30-50 lines -- **Total**: 150-250 lines +### Estimated Complexity +- **Total**: ~50-80 lines +- **Time**: 30-60 minutes -## Testing Checklist -- [ ] Enter mode with `z[` and `z]` -- [ ] Verify letter keys are prefixed correctly -- [ ] Toggle between `[` and `]` prefix -- [ ] Exit cleanly with `` -- [ ] No residual mappings after exit +--- + +## 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 ` (9 keys) | +| **Typing (mixed nav)** | `]d ]d ]c ]c` (8 keys) | `z] ddcc ` (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 -- [ ] Visual feedback is clear -- [ ] Common navigation pairs work (c, d, m, f, q) +- [ ] 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 `` → prompt clears, back to normal mode +- [ ] Exit with `` → 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 `` + +4. **Polish** (optional) + - Add help text on first use + - Handle `` gracefully (already done with `pcall`) + - Consider adding common navigation cheatsheet + +5. **Document** + - Update `README.md` with new keymaps + - Add navigation pairs reference