refactor: fuse search passes, simplify state, fix double-setup leak
- Fuse search() and available_labels() into a single scan() pass.
Drops the line_index bookkeeping field on match records and one
full traversal of the visible buffer.
- Replace the separate LABELS array with direct indexing into
M.config.labels (a string, ~26 ASCII chars). One less parallel
data structure; no fn.split at setup, no bottom-of-file auto-setup
call needed.
- Label picker switched from nested O(M*L) scan to an O(M+L) cursor
walk. No more double-duty 'avail' set acting as both conflict-set
and consumed-label marker.
- Termcode constants consolidated into a K = { ... } table using
vim.keycode.
- Normalize module state: CONFIG -> M.config.
- Collapse 'if opts then extend end' to 'opts or {}'.
- Fix: hoist create_augroup out of the table-valued label branch so
a second setup() call with a string label clears any stale autocmd
from a prior table-valued call.
- Drop redundant '#line > 0' guard in scan (string.find handles
empty lines correctly).
Net: 186 -> 148 lines. Public API unchanged.
This commit is contained in:
1 file changed
+70
-108
+70
-108
@@ -1,14 +1,8 @@
|
|||||||
local fn = vim.fn
|
|
||||||
local api = vim.api
|
local api = vim.api
|
||||||
|
local fn = vim.fn
|
||||||
local M = {}
|
local M = {}
|
||||||
local NS = api.nvim_create_namespace('tiny-jump')
|
|
||||||
local CR = api.nvim_replace_termcodes('<Cr>', true, true, true)
|
M.config = {
|
||||||
local BS = api.nvim_replace_termcodes('<Bs>', true, true, true)
|
|
||||||
local CTRL_H = api.nvim_replace_termcodes('<C-h>', true, true, true)
|
|
||||||
local ESC = api.nvim_replace_termcodes('<Esc>', true, true, true)
|
|
||||||
local LABELS = {}
|
|
||||||
local CONFIG = {
|
|
||||||
-- The labels that may be used, in order of their preference.
|
-- The labels that may be used, in order of their preference.
|
||||||
labels = 'fdsaghjklrewqtyuiopvcxzbnm',
|
labels = 'fdsaghjklrewqtyuiopvcxzbnm',
|
||||||
|
|
||||||
@@ -20,52 +14,41 @@ local CONFIG = {
|
|||||||
label = 'IncSearch',
|
label = 'IncSearch',
|
||||||
}
|
}
|
||||||
|
|
||||||
local function search(pattern, lines, start_line, matches)
|
local K = {
|
||||||
|
CR = vim.keycode('<cr>'),
|
||||||
|
BS = vim.keycode('<bs>'),
|
||||||
|
C_H = vim.keycode('<c-h>'),
|
||||||
|
ESC = vim.keycode('<esc>'),
|
||||||
|
}
|
||||||
|
local NS = api.nvim_create_namespace('tiny-jump')
|
||||||
|
|
||||||
|
-- Find all matches of `pattern` in `lines` and compute the set of labels that
|
||||||
|
-- do not conflict with the character immediately following any match (so that
|
||||||
|
-- typing the next input character cannot be mistaken for a label).
|
||||||
|
local function scan(pattern, lines, start_line)
|
||||||
local lower = pattern == pattern:lower()
|
local lower = pattern == pattern:lower()
|
||||||
|
local matches, avail = {}, {}
|
||||||
|
for i = 1, #M.config.labels do
|
||||||
|
avail[M.config.labels:sub(i, i)] = true
|
||||||
|
end
|
||||||
for idx, line in ipairs(lines) do
|
for idx, line in ipairs(lines) do
|
||||||
local lnum = start_line + idx - 1
|
local search_line = lower and line:lower() or line
|
||||||
local line = lower and line:lower() or line
|
local col = 1
|
||||||
|
while true do
|
||||||
if #line > 0 then
|
local s, e = search_line:find(pattern, col, true)
|
||||||
local col = 1
|
if not s then
|
||||||
|
break
|
||||||
while true do
|
|
||||||
local start, stop = line:find(pattern, col, true)
|
|
||||||
|
|
||||||
if not start then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
col = stop + 1
|
|
||||||
table.insert(matches, {
|
|
||||||
line = lnum - 1,
|
|
||||||
start_col = start - 1,
|
|
||||||
end_col = stop,
|
|
||||||
line_index = idx,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
col = e + 1
|
||||||
|
matches[#matches + 1] = {
|
||||||
|
line = start_line + idx - 2,
|
||||||
|
start_col = s - 1,
|
||||||
|
end_col = e,
|
||||||
|
}
|
||||||
|
avail[line:sub(e + 1, e + 1):lower()] = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
return matches, avail
|
||||||
|
|
||||||
local function available_labels(lines, matches)
|
|
||||||
local avail = {}
|
|
||||||
|
|
||||||
for _, char in ipairs(LABELS) do
|
|
||||||
avail[char] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Disable all the labels that conflict with any of the characters that may be
|
|
||||||
-- matched by the next input.
|
|
||||||
for _, match in ipairs(matches) do
|
|
||||||
local next_col = match.end_col + 1
|
|
||||||
local next_char = lines[match.line_index]:sub(next_col, next_col):lower()
|
|
||||||
|
|
||||||
avail[next_char] = false
|
|
||||||
end
|
|
||||||
|
|
||||||
return avail
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.start()
|
function M.start()
|
||||||
@@ -73,76 +56,63 @@ function M.start()
|
|||||||
local buf = api.nvim_win_get_buf(win)
|
local buf = api.nvim_win_get_buf(win)
|
||||||
local info = fn.getwininfo(win)[1]
|
local info = fn.getwininfo(win)[1]
|
||||||
local top = info.topline
|
local top = info.topline
|
||||||
local bot = info.botline
|
local lines = api.nvim_buf_get_lines(buf, top - 1, info.botline, true)
|
||||||
local lines = api.nvim_buf_get_lines(buf, top - 1, bot, true)
|
local chars, active = '', {}
|
||||||
local chars = ''
|
|
||||||
local matches = {}
|
|
||||||
local active = {}
|
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
api.nvim_echo({ { '/' .. chars, '' } }, false, {})
|
api.nvim_echo({ { '/' .. chars, '' } }, false, {})
|
||||||
|
local ch = fn.getcharstr(-1)
|
||||||
|
local jump_to = active[ch]
|
||||||
|
|
||||||
local char = fn.getcharstr(-1)
|
if ch == K.ESC then
|
||||||
local jump_to = active[char]
|
|
||||||
|
|
||||||
if char == ESC then
|
|
||||||
break
|
break
|
||||||
elseif char == CR then
|
elseif ch == K.CR then
|
||||||
for _, char in ipairs(LABELS) do
|
for i = 1, #M.config.labels do
|
||||||
jump_to = active[char]
|
local c = M.config.labels:sub(i, i)
|
||||||
|
if active[c] then
|
||||||
if jump_to then
|
jump_to = active[c]
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if jump_to then
|
if jump_to then
|
||||||
api.nvim_win_set_cursor(win, jump_to)
|
api.nvim_win_set_cursor(win, jump_to)
|
||||||
end
|
end
|
||||||
|
|
||||||
break
|
break
|
||||||
elseif char == BS or char == CTRL_H then
|
elseif ch == K.BS or ch == K.C_H then
|
||||||
chars = chars:sub(1, #chars - 1)
|
chars = chars:sub(1, -2)
|
||||||
elseif jump_to then
|
elseif jump_to then
|
||||||
api.nvim_win_set_cursor(win, jump_to)
|
api.nvim_win_set_cursor(win, jump_to)
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
chars = chars .. char
|
chars = chars .. ch
|
||||||
end
|
end
|
||||||
|
|
||||||
matches = {}
|
|
||||||
active = {}
|
active = {}
|
||||||
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
|
||||||
search(chars, lines, top, matches)
|
local matches, avail = scan(chars, lines, top)
|
||||||
|
local li = 1
|
||||||
local avail = available_labels(lines, matches)
|
for _, m in ipairs(matches) do
|
||||||
|
|
||||||
for _, match in ipairs(matches) do
|
|
||||||
local label = nil
|
|
||||||
|
|
||||||
for _, cur in ipairs(LABELS) do
|
|
||||||
if avail[cur] then
|
|
||||||
label = cur
|
|
||||||
avail[cur] = false
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.hl.range(
|
vim.hl.range(
|
||||||
buf,
|
buf,
|
||||||
NS,
|
NS,
|
||||||
CONFIG.search,
|
M.config.search,
|
||||||
{ match.line, match.start_col },
|
{ m.line, m.start_col },
|
||||||
{ match.line, match.end_col },
|
{ m.line, m.end_col },
|
||||||
{ priority = 200 }
|
{ priority = 200 }
|
||||||
)
|
)
|
||||||
|
while
|
||||||
if label then
|
li <= #M.config.labels and not avail[M.config.labels:sub(li, li)]
|
||||||
active[label] = { match.line + 1, match.start_col }
|
do
|
||||||
api.nvim_buf_set_extmark(buf, NS, match.line, match.start_col, {
|
li = li + 1
|
||||||
virt_text = { { label, CONFIG.label } },
|
end
|
||||||
|
if li <= #M.config.labels then
|
||||||
|
local label = M.config.labels:sub(li, li)
|
||||||
|
li = li + 1
|
||||||
|
active[label] = { m.line + 1, m.start_col }
|
||||||
|
api.nvim_buf_set_extmark(buf, NS, m.line, m.start_col, {
|
||||||
|
virt_text = { { label, M.config.label } },
|
||||||
virt_text_pos = 'overlay',
|
virt_text_pos = 'overlay',
|
||||||
priority = 201,
|
priority = 201,
|
||||||
})
|
})
|
||||||
@@ -159,28 +129,20 @@ function M.start()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function M.setup(opts)
|
function M.setup(opts)
|
||||||
if opts then
|
M.config = vim.tbl_extend('force', M.config, opts or {})
|
||||||
CONFIG = vim.tbl_extend('force', CONFIG, opts)
|
local group =
|
||||||
end
|
api.nvim_create_augroup('tiny-jump.highlights', { clear = true })
|
||||||
|
if type(M.config.label) == 'table' then
|
||||||
LABELS = fn.split(CONFIG.labels, '\\zs')
|
local attrs = M.config.label
|
||||||
|
M.config.label = 'TinyJumpLabel'
|
||||||
if type(CONFIG.label) == 'table' then
|
|
||||||
local attrs = CONFIG.label
|
|
||||||
CONFIG.label = 'TinyJumpLabel'
|
|
||||||
-- Apply now (setup may run after the colorscheme is already loaded) and
|
-- Apply now (setup may run after the colorscheme is already loaded) and
|
||||||
-- on every ColorScheme event (themes clear user-defined highlights).
|
-- on every ColorScheme event (themes clear user-defined highlights).
|
||||||
local function apply()
|
local function apply()
|
||||||
api.nvim_set_hl(0, 'TinyJumpLabel', attrs)
|
api.nvim_set_hl(0, 'TinyJumpLabel', attrs)
|
||||||
end
|
end
|
||||||
apply()
|
apply()
|
||||||
api.nvim_create_autocmd('ColorScheme', {
|
api.nvim_create_autocmd('ColorScheme', { group = group, callback = apply })
|
||||||
group = api.nvim_create_augroup('tiny-jump.highlights', { clear = true }),
|
|
||||||
callback = apply,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
M.setup()
|
|
||||||
|
|
||||||
return M
|
return M
|
||||||
Reference in new issue
Block a user