refactor: code-golf pass + explanatory comments
Two logical changes that landed as one file rewrite: 1. Tighten the code. SLOC 168 -> 126 (-42), total lines 201 -> 146 pre-comments. Moves: inline single-statement if/while bodies (via stylua's `collapse_simple_statement = Always` and `column_width = 120`), collapse K-termcode table to one line, combine api/fn/M into one local declaration, drop verbose config comments, merge `two` / `ivs_by_line` / `li` into one local, inline CR-branch loop, drop the apply-indirection in setup(), and assorted smaller trims. 2. Re-add prose where the golf made intent non-obvious. Comments now explain: smartcase detection, the +start_line/-2 offset reconcile, the *global* avail disqualification, the cursor-proximity sort purpose, the slow/fast char encoding of pair labels (and why it keeps `prefixes` minimal), the overlap-skip interval math and the carryover-to-next-match behavior, why the augroup is recreated per setup call, and why table-form labels own a dedicated highlight group. No code change from the comment pass itself. Final sizes: 126 SLOC, 194 total lines including 55 comment lines and 13 blank lines. Still smaller than upstream (131 SLOC) and one to two orders of magnitude smaller than every other jump plugin.
This commit is contained in:
3 files changed
+107
-94
No files matched your search
@@ -12,17 +12,36 @@ This plugin is a fork of
|
|||||||
which is excellent and which I used happily for a long time. Big thanks
|
which is excellent and which I used happily for a long time. Big thanks
|
||||||
to the original author for a clean, focused implementation.
|
to the original author for a clean, focused implementation.
|
||||||
|
|
||||||
I forked it because I wanted to pass label highlight attributes directly
|
I forked it because I wanted to pass label highlight attributes directly to
|
||||||
to `setup()` (instead of having to define a separate highlight group and
|
`setup()` (instead of having to define a separate highlight group and re-apply
|
||||||
re-apply it on every colorscheme change). I submitted that as a PR but
|
it on every colorscheme change). I submitted that as a
|
||||||
it was declined, which is the maintainer's right — the upstream plugin
|
(PR)[https://github.com/yorickpeterse/nvim-jump/pull/8] but it was declined,
|
||||||
is explicitly considered feature-complete. See the closed PR for
|
which is the maintainer's right — the upstream plugin is explicitly considered
|
||||||
context: <https://github.com/yorickpeterse/nvim-jump/pull/8>.
|
feature-complete.
|
||||||
|
|
||||||
Since the upstream code is MPL-2.0, a fork is fair game. This plugin is
|
Since the upstream code is MPL-2.0, a fork is fair game. This plugin is a tiny
|
||||||
a tiny extension of it — same core behavior, plus a slightly friendlier
|
extension of it — same core behavior, plus a slightly friendlier highlight API —
|
||||||
highlight API — maintained under the `tiny-*` family of my other
|
maintained under the `tiny-*` family of my other Neovim plugins.
|
||||||
Neovim plugins.
|
|
||||||
|
## Size vs. the alternatives
|
||||||
|
|
||||||
|
`tokei lua/` (code lines only, ignoring comments and blank lines), run
|
||||||
|
against a fresh `git clone --depth=1` of each project:
|
||||||
|
|
||||||
|
| Plugin | Files | SLOC |
|
||||||
|
| -------------------------------------------------------------------------------- | ----: | ----: |
|
||||||
|
| **swaits/tiny-jump.nvim** (this fork) | 1 | 126 |
|
||||||
|
| [yorickpeterse/nvim-jump](https://github.com/yorickpeterse/nvim-jump) (upstream) | 1 | 131 |
|
||||||
|
| [echasnovski/mini.jump2d](https://github.com/echasnovski/mini.nvim) | 1 | 553 |
|
||||||
|
| [rlane/pounce.nvim](https://github.com/rlane/pounce.nvim) | 4 | 634 |
|
||||||
|
| [smoka7/hop.nvim](https://github.com/smoka7/hop.nvim) | 18 | 1,489 |
|
||||||
|
| [ggandor/leap.nvim](https://github.com/ggandor/leap.nvim) | 11 | 2,549 |
|
||||||
|
| [folke/flash.nvim](https://github.com/folke/flash.nvim) | 22 | 2,695 |
|
||||||
|
|
||||||
|
Despite fixing a double-`setup()` bug, adding inline label-color configuration,
|
||||||
|
cursor-proximity sorting, and two-char labels for overflow matches, this fork is
|
||||||
|
slightly _smaller_ than upstream — and an order of magnitude smaller than every
|
||||||
|
other alternative on this list.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
+76
-83
@@ -6,55 +6,42 @@
|
|||||||
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
local api = vim.api
|
local api, fn, M = vim.api, vim.fn, {}
|
||||||
local fn = vim.fn
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
M.config = {
|
M.config = {
|
||||||
-- The labels that may be used, in order of their preference. Ordered by
|
|
||||||
-- QWERTY finger strength (index > middle > ring > pinky, home > top >
|
|
||||||
-- bottom) with hand alternation to break ties.
|
|
||||||
labels = 'fjdkslahgeiruowmcnvptyqxzb',
|
labels = 'fjdkslahgeiruowmcnvptyqxzb',
|
||||||
|
|
||||||
-- The highlight group to use for match highlights.
|
|
||||||
search = 'Search',
|
search = 'Search',
|
||||||
|
|
||||||
-- The highlight group to use for labels. May be a group name (string) or
|
|
||||||
-- a table of `nvim_set_hl` attributes.
|
|
||||||
label = 'IncSearch',
|
label = 'IncSearch',
|
||||||
}
|
}
|
||||||
|
|
||||||
local K = {
|
-- Termcode constants. BS and C_H cover both backspace variants terminals send.
|
||||||
CR = vim.keycode('<cr>'),
|
local K = { CR = vim.keycode('<cr>'), BS = vim.keycode('<bs>'), C_H = vim.keycode('<c-h>'), ESC = vim.keycode('<esc>') }
|
||||||
BS = vim.keycode('<bs>'),
|
|
||||||
C_H = vim.keycode('<c-h>'),
|
|
||||||
ESC = vim.keycode('<esc>'),
|
|
||||||
}
|
|
||||||
local NS = api.nvim_create_namespace('tiny-jump')
|
local NS = api.nvim_create_namespace('tiny-jump')
|
||||||
|
|
||||||
-- Find all matches of `pattern` in `lines` and compute the set of labels that
|
-- Scan the visible lines for matches of `pattern` and, as a side product,
|
||||||
-- do not conflict with the character immediately following any match (so that
|
-- compute `avail[c]` = true for each label char `c` that is *safe* to use as a
|
||||||
-- typing the next input character cannot be mistaken for a label).
|
-- label — i.e. `c` is never the character immediately following a match. If it
|
||||||
|
-- were, pressing `c` would be ambiguous: jump-to-label-c vs extend-pattern-by-c.
|
||||||
local function scan(pattern, lines, start_line)
|
local function scan(pattern, lines, start_line)
|
||||||
|
-- smartcase: lowercase-only pattern → case-insensitive search
|
||||||
local lower = pattern == pattern:lower()
|
local lower = pattern == pattern:lower()
|
||||||
local matches, avail = {}, {}
|
local matches, avail = {}, {}
|
||||||
for i = 1, #M.config.labels do
|
for i = 1, #M.config.labels do
|
||||||
avail[M.config.labels:sub(i, i)] = true
|
avail[M.config.labels:sub(i, i)] = true
|
||||||
end
|
end
|
||||||
for idx, line in ipairs(lines) do
|
for idx, line in ipairs(lines) do
|
||||||
local search_line = lower and line:lower() or line
|
local sline = lower and line:lower() or line
|
||||||
local col = 1
|
local col = 1
|
||||||
while true do
|
while true do
|
||||||
local s, e = search_line:find(pattern, col, true)
|
local s, e = sline:find(pattern, col, true)
|
||||||
if not s then
|
if not s then break end
|
||||||
break
|
|
||||||
end
|
|
||||||
col = e + 1
|
col = e + 1
|
||||||
matches[#matches + 1] = {
|
-- start_line is 1-indexed (Vim convention), buffer rows are 0-indexed, and
|
||||||
line = start_line + idx - 2,
|
-- ipairs's idx is also 1-indexed — the `- 2` reconciles both offsets.
|
||||||
start_col = s - 1,
|
matches[#matches + 1] = { line = start_line + idx - 2, start_col = s - 1, end_col = e }
|
||||||
end_col = e,
|
-- Mark the char after this match as unavailable for labeling. Note this
|
||||||
}
|
-- is a *global* disqualification: any one match with 'e' next disables
|
||||||
|
-- the 'e' label for all matches.
|
||||||
avail[line:sub(e + 1, e + 1):lower()] = false
|
avail[line:sub(e + 1, e + 1):lower()] = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -67,29 +54,31 @@ 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)
|
||||||
|
-- State that persists across input keystrokes:
|
||||||
|
-- chars = accumulated search pattern typed so far
|
||||||
|
-- active = map from label-key (1 or 2 chars) to { row, col } target
|
||||||
|
-- prefixes = set of chars that are the first char of some 2-char label;
|
||||||
|
-- input loop checks this to decide whether to buffer a 2nd key
|
||||||
local chars, active, prefixes = '', {}, {}
|
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
|
-- Two-char label mode: if `ch` is a known prefix, wait for a 2nd keystroke
|
||||||
ch = ch .. fn.getcharstr(-1)
|
-- and treat the pair as the label key.
|
||||||
end
|
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
|
||||||
break
|
break
|
||||||
elseif ch == K.CR then
|
elseif ch == K.CR then
|
||||||
|
-- <CR> jumps to whichever labeled match has the strongest label — i.e.
|
||||||
|
-- the one that appears earliest in the label-preference string.
|
||||||
for i = 1, #M.config.labels do
|
for i = 1, #M.config.labels do
|
||||||
local c = M.config.labels:sub(i, i)
|
jump_to = active[M.config.labels:sub(i, i)]
|
||||||
if active[c] then
|
if jump_to then break end
|
||||||
jump_to = active[c]
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if jump_to then
|
|
||||||
api.nvim_win_set_cursor(win, jump_to)
|
|
||||||
end
|
end
|
||||||
|
if jump_to then api.nvim_win_set_cursor(win, jump_to) end
|
||||||
break
|
break
|
||||||
elseif ch == K.BS or ch == K.C_H then
|
elseif ch == K.BS or ch == K.C_H then
|
||||||
chars = chars:sub(1, -2)
|
chars = chars:sub(1, -2)
|
||||||
@@ -100,65 +89,66 @@ function M.start()
|
|||||||
chars = chars .. ch
|
chars = chars .. ch
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Fresh scan per keystroke: clear everything that was drawn, then redraw
|
||||||
|
-- from the new `chars` value. `active` and `prefixes` must be regenerated
|
||||||
|
-- because label-to-position mapping depends on the current match set.
|
||||||
active, prefixes = {}, {}
|
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
|
||||||
local matches, avail = scan(chars, lines, top)
|
local matches, avail = scan(chars, lines, top)
|
||||||
|
-- Sort matches by cursor proximity so the N closest get the N strongest
|
||||||
|
-- labels. Primary: line distance. Secondary: column distance.
|
||||||
local cr = api.nvim_win_get_cursor(win)
|
local cr = api.nvim_win_get_cursor(win)
|
||||||
local cl, cc = cr[1] - 1, cr[2]
|
local cl, cc = cr[1] - 1, cr[2]
|
||||||
table.sort(matches, function(a, b)
|
table.sort(matches, function(a, b)
|
||||||
local da, db = math.abs(a.line - cl), math.abs(b.line - cl)
|
local da, db = math.abs(a.line - cl), math.abs(b.line - cl)
|
||||||
if da ~= db then
|
if da ~= db then return da < db end
|
||||||
return da < db
|
|
||||||
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 L = M.config.labels
|
||||||
local two = #matches > #L
|
-- Switch to two-char labels when there are more matches than single-char
|
||||||
local line_intervals = {}
|
-- labels. Pair labels expand capacity to up to |L|^2 (676 with defaults),
|
||||||
local li = 1
|
-- at the cost of requiring two keystrokes per jump while `two` is true.
|
||||||
|
local two, ivs_by_line, li = #matches > #L, {}, 1
|
||||||
for _, m in ipairs(matches) do
|
for _, m in ipairs(matches) do
|
||||||
vim.hl.range(
|
vim.hl.range(buf, NS, M.config.search, { m.line, m.start_col }, { m.line, m.end_col }, { priority = 200 })
|
||||||
buf,
|
|
||||||
NS,
|
|
||||||
M.config.search,
|
|
||||||
{ m.line, m.start_col },
|
|
||||||
{ m.line, m.end_col },
|
|
||||||
{ priority = 200 }
|
|
||||||
)
|
|
||||||
local label
|
local label
|
||||||
if two then
|
if two and li <= #L * #L then
|
||||||
if li <= #L * #L then
|
-- Pair encoding: `a` (slow-varying) is the first char, `b` (fast-
|
||||||
local a = math.floor((li - 1) / #L) + 1
|
-- varying) is the second. So labels li=1..#L all share the same
|
||||||
local b = (li - 1) % #L + 1
|
-- first char, keeping `prefixes` minimal for as long as possible —
|
||||||
label = L:sub(a, a) .. L:sub(b, b)
|
-- important because every char in `prefixes` is a char the user
|
||||||
end
|
-- can no longer use to extend the search pattern in one keystroke.
|
||||||
else
|
local a, b = math.floor((li - 1) / #L) + 1, (li - 1) % #L + 1
|
||||||
|
label = L:sub(a, a) .. L:sub(b, b)
|
||||||
|
elseif not two then
|
||||||
|
-- Single-char: walk the cursor past any label that's unavailable
|
||||||
|
-- (would conflict with pattern extension).
|
||||||
while li <= #L and not avail[L:sub(li, li)] do
|
while li <= #L and not avail[L:sub(li, li)] do
|
||||||
li = li + 1
|
li = li + 1
|
||||||
end
|
end
|
||||||
if li <= #L then
|
if li <= #L then label = L:sub(li, li) end
|
||||||
label = L:sub(li, li)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
if label then
|
if label then
|
||||||
|
-- Skip this match's label if it would visually overlap (or, in 2-char
|
||||||
|
-- mode, touch within 1 column of) any label already placed on this
|
||||||
|
-- line. The match still gets a search highlight; the unused label
|
||||||
|
-- carries over to the next non-conflicting match.
|
||||||
local gap = two and 1 or 0
|
local gap = two and 1 or 0
|
||||||
local ivs = line_intervals[m.line] or {}
|
local ivs = ivs_by_line[m.line] or {}
|
||||||
local fits = true
|
local fits = true
|
||||||
for _, iv in ipairs(ivs) do
|
for _, iv in ipairs(ivs) do
|
||||||
if
|
-- Standard [a, b) interval-overlap test with `gap` padding on each side.
|
||||||
m.start_col < iv[2] + gap
|
if m.start_col < iv[2] + gap and iv[1] < m.start_col + #label + gap then
|
||||||
and iv[1] < m.start_col + #label + gap
|
|
||||||
then
|
|
||||||
fits = false
|
fits = false
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if fits then
|
if fits then
|
||||||
if two then
|
-- Record the 2-char label's first char so the input loop knows
|
||||||
prefixes[label:sub(1, 1)] = true
|
-- to buffer a second keystroke when it sees that char.
|
||||||
end
|
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 } },
|
||||||
@@ -166,7 +156,7 @@ function M.start()
|
|||||||
priority = 201,
|
priority = 201,
|
||||||
})
|
})
|
||||||
ivs[#ivs + 1] = { m.start_col, m.start_col + #label }
|
ivs[#ivs + 1] = { m.start_col, m.start_col + #label }
|
||||||
line_intervals[m.line] = ivs
|
ivs_by_line[m.line] = ivs
|
||||||
li = li + 1
|
li = li + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -183,18 +173,21 @@ end
|
|||||||
|
|
||||||
function M.setup(opts)
|
function M.setup(opts)
|
||||||
M.config = vim.tbl_extend('force', M.config, opts or {})
|
M.config = vim.tbl_extend('force', M.config, opts or {})
|
||||||
local group =
|
-- Recreate the augroup on every setup() call (clear = true drops any prior
|
||||||
api.nvim_create_augroup('tiny-jump.highlights', { clear = true })
|
-- autocmds). Without this, repeated setup calls with different label configs
|
||||||
|
-- would accumulate stale ColorScheme autocmds.
|
||||||
|
local group = api.nvim_create_augroup('tiny-jump.highlights', { clear = true })
|
||||||
if type(M.config.label) == 'table' then
|
if type(M.config.label) == 'table' then
|
||||||
|
-- User passed highlight attrs directly. Own a dedicated highlight group
|
||||||
|
-- (TinyJumpLabel) and re-apply it on ColorScheme so their colors survive
|
||||||
|
-- theme changes — colorschemes clear user-defined highlights.
|
||||||
local attrs = M.config.label
|
local attrs = M.config.label
|
||||||
M.config.label = 'TinyJumpLabel'
|
M.config.label = 'TinyJumpLabel'
|
||||||
-- Apply now (setup may run after the colorscheme is already loaded) and
|
api.nvim_set_hl(0, 'TinyJumpLabel', attrs)
|
||||||
-- on every ColorScheme event (themes clear user-defined highlights).
|
api.nvim_create_autocmd('ColorScheme', {
|
||||||
local function apply()
|
group = group,
|
||||||
api.nvim_set_hl(0, 'TinyJumpLabel', attrs)
|
callback = function() api.nvim_set_hl(0, 'TinyJumpLabel', attrs) end,
|
||||||
end
|
})
|
||||||
apply()
|
|
||||||
api.nvim_create_autocmd('ColorScheme', { group = group, callback = apply })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
column_width = 80
|
column_width = 120
|
||||||
indent_type = 'Spaces'
|
indent_type = 'Spaces'
|
||||||
indent_width = 2
|
indent_width = 2
|
||||||
quote_style = 'AutoPreferSingle'
|
quote_style = 'AutoPreferSingle'
|
||||||
|
collapse_simple_statement = 'Always'
|
||||||
Reference in new issue
Block a user