feat: two-char jump labels for overflow matches

When there are more matches than the single-char label pool (>|L|,
i.e. > 26 with the default), issue two-char labels so matches past
the single-char ceiling are still reachable.

Tactics:

- Cap: 2 * |L| = 52 labels total in two-char mode. The full |L|^2 =
  676 space produces visually unusable density on packed buffers;
  52 is plenty once the cursor-proximity sort lands them near the
  cursor.

- Label order: first char varies slow (26 labels starting with 'f',
  next 26 starting with 'j') so only 'f' and 'j' become prefixes.
  The remaining 24 letters stay usable for pattern extension in
  two-char mode.

- Per-line readability spacing: a 2-char label requires one column
  of original text between itself and the previous 2-char label on
  the same line. Prevents visual bleed when matches are adjacent
  (e.g. '0' search on timestamp-heavy text). Single-char mode
  unchanged.

- No global mode flag: a 'prefixes' set, populated while assigning
  labels, tells the input loop when to buffer a second keystroke.
  The set resets each iteration with 'active'.
This commit is contained in:
swaits committed 2026-04-13 20:41:00 -06:00
1 parent f8e0dfe54c
commit d9788571ec
1 file changed
+29 -9
+29 -9
View File
@@ -67,11 +67,14 @@ function M.start()
local info = fn.getwininfo(win)[1] local info = fn.getwininfo(win)[1]
local top = info.topline local top = info.topline
local lines = api.nvim_buf_get_lines(buf, top - 1, info.botline, true) local lines = api.nvim_buf_get_lines(buf, top - 1, info.botline, true)
local chars, active = '', {} local chars, active, prefixes = '', {}, {}
while true do while true do
api.nvim_echo({ { '/' .. chars, '' } }, false, {}) api.nvim_echo({ { '/' .. chars, '' } }, false, {})
local ch = fn.getcharstr(-1) local ch = fn.getcharstr(-1)
if prefixes[ch] then
ch = ch .. fn.getcharstr(-1)
end
local jump_to = active[ch] local jump_to = active[ch]
if ch == K.ESC then if ch == K.ESC then
@@ -97,7 +100,7 @@ function M.start()
chars = chars .. ch chars = chars .. ch
end end
active = {} active, prefixes = {}, {}
api.nvim_buf_clear_namespace(buf, NS, 0, -1) api.nvim_buf_clear_namespace(buf, NS, 0, -1)
if #chars > 0 then if #chars > 0 then
@@ -111,6 +114,9 @@ function M.start()
end end
return math.abs(a.start_col - cc) < math.abs(b.start_col - cc) return math.abs(a.start_col - cc) < math.abs(b.start_col - cc)
end) end)
local L = M.config.labels
local two = #matches > #L
local line_end = {}
local li = 1 local li = 1
for _, m in ipairs(matches) do for _, m in ipairs(matches) do
vim.hl.range( vim.hl.range(
@@ -121,20 +127,34 @@ function M.start()
{ m.line, m.end_col }, { m.line, m.end_col },
{ priority = 200 } { priority = 200 }
) )
while local label
li <= #M.config.labels and not avail[M.config.labels:sub(li, li)] if two then
do if li <= 2 * #L then
li = li + 1 local a = math.floor((li - 1) / #L) + 1
local b = (li - 1) % #L + 1
label = L:sub(a, a) .. L:sub(b, b)
end
else
while li <= #L and not avail[L:sub(li, li)] do
li = li + 1
end
if li <= #L then
label = L:sub(li, li)
end
end end
if li <= #M.config.labels then local prev = line_end[m.line]
local label = M.config.labels:sub(li, li) if label and (not prev or m.start_col >= prev) then
li = li + 1 if two then
prefixes[label:sub(1, 1)] = true
end
active[label] = { m.line + 1, m.start_col } active[label] = { m.line + 1, m.start_col }
api.nvim_buf_set_extmark(buf, NS, m.line, m.start_col, { api.nvim_buf_set_extmark(buf, NS, m.line, m.start_col, {
virt_text = { { label, M.config.label } }, virt_text = { { label, M.config.label } },
virt_text_pos = 'overlay', virt_text_pos = 'overlay',
priority = 201, priority = 201,
}) })
line_end[m.line] = m.start_col + #label + (two and 1 or 0)
li = li + 1
end end
end end
end end