Leap is a general-purpose motion plugin for Neovim, building and improving primarily on vim-sneak, with the ultimate goal of establishing a new standard interface for moving around in the visible area in Vim-like modal editors. It allows you to reach any target in a very fast, uniform way, and minimizes the required focus level while executing a jump.
Leap's default motions allow you to jump to any position in the visible editor area by entering a 2-character search pattern, and then potentially a label character to pick your target from multiple matches, similar to Sneak. The main novel idea in Leap is that you get a preview of the target labels - you can see which key you will need to press before you actually need to do that.
s
) or in the other windows
(S
). (Note: you can use a single key for the whole tab page, if you are
okay with the trade-offs.){char1}{char2}
).{char1}{?}
pairs. You cannot use the labels yet - they only get active
after finishing the pattern.{char2}
. If the pair was not labeled, then voilà, you're already
there. You can safely ignore the remaining labels, and continue editing -
those are guaranteed non-conflicting letters, disappearing on the next
keypress.<space>
and
<backspace>
.Character pairs give you full coverage of the screen:
s{char}<space>
jumps to the last character on a line.s<space><space>
jumps to actual end-of-line characters, including empty
lines.At any stage, <enter>
consistently jumps to the next available target
(<backspace>
steps back):
s<enter>...
repeats the previous search.s{char}<enter>...
can be used as a multiline substitute for fFtT
motions.It is ridiculously fast: not counting the trigger key, leaping to literally anywhere on the screen rarely takes more than 3 keystrokes in total, that can be typed in one go. Often 2 is enough.
At the same time, it reduces mental effort to almost zero:
You don't have to weigh alternatives: a single universal motion type can be used in all non-trivial situations.
You don't have to compose motions in your head: one command achieves one logical movement.
You don't have to be aware of the context: the eyes can keep focusing on the target the whole time.
You don't have to make decisions on the fly: the sequence you should type is fixed from the start.
You don't have to pause in the middle: if typing at a moderate speed, at each step you already know what the immediate next keypress should be, and your mind can process the rest in the background.
The plugin is not 100% stable yet, but don't let that stop you - the usage basics are extremely unlikely to change. To follow breaking changes, subscribe to the corresponding issue.
.
) to
workUse your preferred method or plugin manager. No extra steps needed besides
defining keybindings - to use the default ones, put the following into your
config (overrides s
in all modes, and S
in Normal mode):
require('leap').set_default_mappings()
(init.lua)
lua require('leap').set_default_mappings()
(init.vim)
Calling require('leap').set_default_mappings()
is equivalent to:
vim.keymap.set({'n', 'x', 'o'}, 's', '<Plug>(leap)')
vim.keymap.set('n', 'S', '<Plug>(leap-from-window)')
Jump to anywhere in Normal mode with one key:
vim.keymap.set('n', 's', '<Plug>(leap-anywhere)')
vim.keymap.set({'x', 'o'}, 's', '<Plug>(leap)')
Trade-off: if you have multiple windows open on the tab page, you will almost never get an automatic jump, except if all targets are in the same window. (This is an intentional restriction: it would be too disorienting if the cursor could jump in/to a different window than your goal, right before selecting the target.)
Sneak-style:
vim.keymap.set({'n', 'x', 'o'}, 's', '<Plug>(leap-forward)')
vim.keymap.set({'n', 'x', 'o'}, 'S', '<Plug>(leap-backward)')
vim.keymap.set({'n', 'x', 'o'}, 'gs', '<Plug>(leap-from-window)')
See :h leap-custom-mappings
for more.
Define a preview filter (:h leap.opts.preview_filter
):
-- Skip the middle of alphabetic words:
-- foobar[quux]
-- ^----^^^--^^
require('leap').opts.preview_filter =
function (ch0, ch1, ch2)
return not (
ch1:match('%s') or
ch0:match('%a') and ch1:match('%a') and ch2:match('%a')
)
end
Define equivalence classes for brackets and quotes, in addition to the default whitespace group:
require('leap').opts.equivalence_classes = { ' \t\r\n', '([{', ')]}', '\'"`' }
Use the traversal keys to repeat the previous motion without explicitly invoking Leap:
require('leap.user').set_repeat_keys('<enter>', '<backspace>')
...is all the rage now, but doing it via your plugin manager is unnecessary, as
Leap lazy loads itself. Using the keys
feature of lazy.nvim might even cause
problems.
Experimental features, APIs might be subject to change.
Inspired by leap-spooky.nvim, and flash.nvim's similar feature.
This function allows you to perform an action in a remote location: it forgets the current mode or pending operator, lets you leap with the cursor (to anywhere on the tab page), then continues where it left off. Once an operation or insertion is finished, it moves the cursor back to the original position, as if you had operated from the distance.
-- If using the default mappings (`gs` for multi-window mode), you can
-- map e.g. `gS` here.
vim.keymap.set({'n', 'x', 'o'}, 'gs', function ()
require('leap.remote').action()
end)
Example: gs{leap}yap
, vgs{leap}apy
, or ygs{leap}ap
yank the paragraph at
the position specified by {leap}
.
Tips
Swapping regions becomes moderately simple, without needing a custom
plugin: d{region1} gs{leap} v{region2} pP
. Example (swapping two
words): diwgs{leap}viwpP
.
As the remote mode is active until returning to Normal mode again (by
any means), <ctrl-o>
becomes your friend in Insert mode, or when
doing change operations.
Icing on the cake, no. 1 - giving input ahead of time
The input
parameter lets you feed keystrokes automatically after the jump:
-- Trigger visual selection right away, so that you can `gs{leap}apy`:
vim.keymap.set({'n', 'o'}, 'gs', function ()
require('leap.remote').action { input = 'v' }
end)
-- Forced linewise version:
vim.keymap.set({'n', 'o'}, 'gS', function ()
require('leap.remote').action { input = 'V' }
end)
-- Remote K:
vim.keymap.set('n', 'gK', function ()
require('leap.remote').action { input = 'K' }
end)
By feeding text objects as input
, you can create remote text objects, for
an even more intuitive workflow (yarp{leap}
- "yank a remote paragraph
at..."):
-- Create remote versions of all a/i text objects by inserting `r`
-- into the middle (`iw` becomes `irw`, etc.).
-- A trick to avoid having to create separate hardcoded mappings for
-- each text object: when entering `ar`/`ir`, consume the next
-- character, and create the input from that character concatenated to
-- `a`/`i`.
do
local remote_text_object = function (prefix)
local ok, ch = pcall(vim.fn.getcharstr) -- pcall for handling <C-c>
if not ok or ch == vim.keycode('<esc>') then return end
require('leap.remote').action { input = prefix .. ch }
end
vim.keymap.set({'x', 'o'}, 'ar', function () remote_text_object('a') end)
vim.keymap.set({'x', 'o'}, 'ir', function () remote_text_object('i') end)
end
A very handy custom mapping - remote line(s), with optional count
(yaa{leap}
, y3aa{leap}
):
vim.keymap.set({'x', 'o'}, 'aa', function ()
-- Force linewise selection.
local V = vim.fn.mode(true):match('V') and '' or 'V'
-- In any case, move horizontally, to trigger operations.
local input = vim.v.count > 1 and (vim.v.count - 1 .. 'j') or 'hl'
-- With `count=false` you can skip feeding count to the command
-- automatically (we need -1 here, see above).
require('leap.remote').action { input = V .. input, count = false }
end)
Icing on the cake, no. 2 - automatic paste after yanking
With this, you can clone text objects or regions in the blink of an eye, even
from another window (yarp{leap}
, and voilà, the remote paragraph appears
there):
vim.api.nvim_create_autocmd('User', {
pattern = 'RemoteOperationDone',
group = vim.api.nvim_create_augroup('LeapRemote', {}),
callback = function (event)
-- Do not paste if some special register was in use.
if vim.v.operator == 'y' and event.data.register == '"' then
vim.cmd('normal! p')
end
end,
})
vim.keymap.set({'x', 'o'}, 'R', function ()
require('leap.treesitter').select()
end)
Besides choosing a label (R{label}
), in Normal/Visual mode you can also use
the traversal keys for incremental selection (;
and ,
are automatically
added to the default keys). The labels are forced to be safe, so you can
operate on the current selection right away (R;;y
).
Tips
It is worth using linewise mode (VR
, yVR
), as redundant nodes are
filtered out (only the outermost are kept in a given line range), making the
selection much more efficient.
Traversal can "wrap around" backwards, so you can select the root node right
away (R,
), instead of going forward (R;;;;;
).
To increase/decrease the selection in a
clever-f-like manner (RRRRrr
instead of R;;;,,
), set the trigger key (or the suffix of it) and its
inverted case as temporary traversal keys for this specific call (select()
can take an opts
argument, just like leap()
- see :h leap.leap()
):
-- "clever-R"
vim.keymap.set({'n', 'x', 'o'}, 'R', function ()
local sk = vim.deepcopy(require'leap'.opts.special_keys)
-- The items in `special_keys` can be both strings or tables - the
-- shortest workaround might be the below one:
sk.next_target = vim.fn.flatten(vim.list_extend({'R'}, {sk.next_target}))
sk.prev_target = vim.fn.flatten(vim.list_extend({'r'}, {sk.prev_target}))
-- Remove the temporary traversal keys from `safe_labels`.
local sl = {}
for _, label in ipairs(vim.deepcopy(require'leap'.opts.safe_labels)) do
if label ~= 'R' and label ~= 'r' then table.insert(sl, label) end
end
require('leap.treesitter').select {
opts = { special_keys = sk, safe_labels = sl }
}
end)
Help files are not exactly page-turners, but I suggest at least skimming
:help leap
, even if you don't have a specific question yet
(if nothing else: :h leap-usage
, :h leap-config
, :h leap-events
). While
Leap has deeply thought-through, opinionated defaults, its small(ish) but
comprehensive API makes it pretty flexible.
Premise: jumping from point A to B on the screen should not be some exciting puzzle, for which you should train yourself; it should be a non-issue. An ideal keyboard-driven interface would impose almost no more cognitive burden than using a mouse, without the constant context-switching required by the latter.
That is, you do not want to think about
All the while using as few keystrokes as possible, and getting distracted by as little incidental visual noise as possible.
It is obviously impossible to achieve all of the above at the same time, without
some trade-offs at least; but in our opinion Leap comes pretty close, occupying
a sweet spot in the design space. (The worst remaining offender might be visual
noise, but clever filtering in the preview phase can help - see :h leap.opts.preview_filter
.)
The one-step shift between perception and action is the big idea that cuts the Gordian knot: a fixed pattern length combined with previewing labels can eliminate the surprise factor from the search-based method (which is the only viable approach - see "jetpack" above). Fortunately, a 2-character pattern - the shortest one with which we can play this trick - is also long enough to sufficiently narrow down the matches in the vast majority of cases.
Fixed pattern length also makes (safe) automatic jump to the first target
possible. You cannot improve on jumping directly, just like how f
and t
works, not having to read a label at all, and not having to accept the match
with <enter>
either. However, we can do this in a smart way: if there are
many targets (more than 15-20), we stay put, so we can use a bigger, "unsafe"
label set - getting the best of both worlds. The non-determinism we're
introducing is less of an issue here, since the outcome is known in advance.
In sum, compared to other methods based on labeling targets, Leap's approach is unique in that it
offers a smoother experience, by (somewhat) eliminating the pause before typing the label
feels natural to use for both distant and close targets
For Neovim versions < 0.10 (https://github.com/neovim/neovim/issues/20793):
-- Hide the (real) cursor when leaping, and restore it afterwards.
vim.api.nvim_create_autocmd('User', { pattern = 'LeapEnter',
callback = function()
vim.cmd.hi('Cursor', 'blend=100')
vim.opt.guicursor:append { 'a:Cursor/lCursor' }
end,
}
)
vim.api.nvim_create_autocmd('User', { pattern = 'LeapLeave',
callback = function()
vim.cmd.hi('Cursor', 'blend=0')
vim.opt.guicursor:remove { 'a:Cursor/lCursor' }
end,
}
)
Caveat: If you experience any problems after using the above snippet, check #70 and #143 to tweak it.
Common operations should use the fewest keystrokes and the most comfortable keys, so it makes sense to take those over by Leap, especially given that both native commands have synonyms:
Normal mode
s
= cl
(or xi
)S
= cc
Visual mode
s
= c
S
= Vc
, or c
if already in linewise modeIf you are not convinced, just head to :h leap-custom-mappings
.
The preview phase, unfortunately, makes them impossible, by design: for a potential match, we might need to show two different labels (corresponding to two different futures) at the same time. (1, 2, 3)
Basic template:
local function remote_action ()
require('leap').leap {
target_windows = require('leap.user').get_focusable_windows(),
action = function (target)
local winid = target.wininfo.winid
local lnum, col = unpack(target.pos) -- 1/1-based indexing!
-- ... do something at the given position ...
end,
}
end
See Extending Leap for more.
require('leap').opts.safe_labels = {}
require('leap').opts.preview_filter = function () return false end
-- Or just set to grey directly, e.g. { fg = '#777777' },
-- if Comment is saturated.
vim.api.nvim_set_hl(0, 'LeapBackdrop', { link = 'Comment' })
If a language-mapping
('keymap'
) is active,
Leap waits for keymapped sequences as needed and searches for the keymapped
result as expected.
Also check out opts.equivalence_classes
, that lets you group certain
characters together as mutual aliases, e.g.:
{
' \t\r\n', 'aäàáâãā', 'dḍ', 'eëéèêē', 'gǧğ', 'hḥḫ',
'iïīíìîı', 'nñ', 'oō', 'sṣšß', 'tṭ', 'uúûüűū', 'zẓ'
}
To paraphrase Steve Jobs about their logo and Turing's poison apple, I wish it were, but it is a coincidence. "Leap" is just another synonym for "jump", that happens to rhyme with Sneak. That said, in some respects you can indeed think of leap.nvim as a spiritual successor to Raskin's work, and thus the name as a little tribute to the great pioneer of interface design, even though embracing the modal paradigm is a fundamental difference in our approach.
There are lots of ways you can extend the plugin and bend it to your will - see
:h leap.leap()
and :h leap-events
. Besides tweaking the basic parameters of
the function (search scope, jump offset, etc.), you can:
Some practical examples:
local function get_line_starts(winid, skip_range)
local wininfo = vim.fn.getwininfo(winid)[1]
local cur_line = vim.fn.line('.')
-- Skip lines close to the cursor.
local skip_range = skip_range or 2
-- Get targets.
local targets = {}
local lnum = wininfo.topline
while lnum <= wininfo.botline do
local fold_end = vim.fn.foldclosedend(lnum)
-- Skip folded ranges.
if fold_end ~= -1 then
lnum = fold_end + 1
else
if (lnum < cur_line - skip_range) or (lnum > cur_line + skip_range) then
table.insert(targets, { pos = { lnum, 1 } })
end
lnum = lnum + 1
end
end
-- Sort them by vertical screen distance from cursor.
local cur_screen_row = vim.fn.screenpos(winid, cur_line, 1)['row']
local function screen_rows_from_cur(t)
local t_screen_row = vim.fn.screenpos(winid, t.pos[1], t.pos[2])['row']
return math.abs(cur_screen_row - t_screen_row)
end
table.sort(targets, function (t1, t2)
return screen_rows_from_cur(t1) < screen_rows_from_cur(t2)
end)
if #targets >= 1 then
return targets
end
end
-- You can pass an argument to specify a range to be skipped
-- before/after the cursor (default is +/-2).
function leap_line_start(skip_range)
local winid = vim.api.nvim_get_current_win()
require('leap').leap {
target_windows = { winid },
targets = get_line_starts(winid, skip_range),
}
end
-- For maximum comfort, force linewise selection in the mappings:
vim.keymap.set({'x', 'o'}, '|', function ()
-- Only force V if not already in it (otherwise it would exit Visual mode).
if vim.fn.mode(1) ~= 'V' then vim.cmd('normal! V') end
leap_line_start()
end)
-- NOTE: If you try to use this before entering any input, an error is thrown.
-- (Help would be appreciated, if someone knows a fix.)
local function get_targets (buf)
local pick = require('telescope.actions.state').get_current_picker(buf)
local scroller = require('telescope.pickers.scroller')
local wininfo = vim.fn.getwininfo(pick.results_win)[1]
local top = math.max(
scroller.top(pick.sorting_strategy, pick.max_results, pick.manager:num_results()),
wininfo.topline - 1
)
local bottom = wininfo.botline - 2 -- skip the current row
local targets = {}
for lnum = bottom, top, -1 do -- start labeling from the closest (bottom) row
table.insert(targets, { wininfo = wininfo, pos = { lnum + 1, 1 }, pick = pick, })
end
return targets
end
local function pick_with_leap (buf)
require('leap').leap {
targets = function () return get_targets(buf) end,
action = function (target)
target.pick:set_selection(target.pos[1] - 1)
require('telescope.actions').select_default(buf)
end,
}
end
require('telescope').setup {
defaults = {
mappings = {
i = { ['<a-p>'] = pick_with_leap },
}
}
}
See flit.nvim. Note that this is not a proper extension plugin, as it uses undocumented API too.