nvim/NAVIGATION_MODE_PLAN.md

16 KiB
Raw Permalink Blame History

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 ;/, 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

" 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 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

-- 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 (cdm)
  • 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

    require('bracket-repeat').setup()
    
  3. 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)
    
  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)

    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