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 positions 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, in a manner similar to Sneak. The main novel idea in Leap is that you get a live preview of the target labels - Leap shows you which key you will need to press before you actually need to do that.
- Initiate the search in the forward (
s
) or backward (S
) direction, or in the other windows (gs
). (Note: you can configure the plugin to merge these cases, using two keys instead, or even just one, if you are okay with the trade-offs.) - Start typing a 2-character pattern (
{char1}{char2}
). - After typing the first character, you see "labels" appearing next to some of
the
{char1}{?}
pairs. You cannot use the labels yet. - Enter
{char2}
. If the pair was not labeled, then voilà, you're already there. No need to be bothered by remaining labels - those are guaranteed "safe" letters -, just continue editing. - Else: type the label character. If there are too many matches (more than
~50), you might need to switch to the desired group first, using
<space>
(step back with<tab>
, if needed).
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 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 enter is determined from the very beginning.
-
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.
Type
s{char}<space>
to jump to a character before the end of the line.s<space><space>
to jump to an empty line (or any EOL position if Visual mode orvirtualedit
allows it)s{char}<enter>
to jump to the first{char}{?}
pair right away.s<enter>
to repeat the last search.s<enter><enter>...
ors{char}<enter><enter>...
to traverse through the matches.
You can also
- search bidirectionally in the whole window, or bind only one key to Leap, and search in all windows (see FAQ).
- map keys to repeat motions without explicitly invoking Leap, similar to how
;
and,
works (see:h leap-repeat-keys
).
This was just a teaser - mind that while Leap has deeply thought-through, opinionated defaults, its small(ish) but comprehensive API makes it flexible: you can configure it to resemble other similar plugins, extend it with custom targeting methods, and even do arbitrary actions with the selected target - read on to dig deeper.
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
- the command: we need one fundamental targeting method that can bring you anywhere: a "jetpack" instead of a "railway network" (↔ EasyMotion and its derivatives)
- the context: it should be enough to look at the target, and nothing else (↔ vanilla Vim motion combinations using relative line numbers and/or repeats)
- the steps: the motion should be atomic (↔ Vim motion combos), and ideally
you should be able to type the whole sequence in one go, always knowing the
next step in advance (↔ any kind of "just-in-time" labeling method; note that
the "
/
on steroids" approach by Pounce and Flash, where the pattern length is not fixed, and thus the labels appear at an unknown time, makes this last goal impossible)
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.)
The one-step shift between perception and action is the big idea that cuts the Gordian knot: a fixed pattern length combined with ahead-of-time labeling 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. With ahead-of-time labeling, however, we can do this in
a smart way - disabling autojump and switching back to a bigger, "unsafe" label
set beyond a certain number of targets. The non-determinism is not much of an
issue here, since the outcome is known in advance.
Optimize for the common case
A good example is using strictly one-character labels and switching between groups, which can become awkward beyond, say, 200 targets, but makes a whole bunch of edge cases and UI problems nonexistent.
Sharpen the saw
Build on Vim's native features, aim for synergy, and don't reinvent the wheel
(dot-repeat (.
), inclusive/exclusive toggle (v
),
keymap
support, autocommands via User
events, <Plug>
keys, etc.).
(http://vimcasts.org/blog/2012/08/on-sharpening-the-saw/)
Mechanisms instead of policies
Complement the small and opinionated core by extension points, keeping the plugin flexible and future-proof.
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.
- Neovim >= 0.7.0 stable, or latest nightly
- repeat.vim, for dot-repeats (
.
) to work
Use 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
, S
, and gs
in all modes):
require('leap').create_default_mappings()
(init.lua)
lua require('leap').create_default_mappings()
(init.vim)
To set custom mappings instead, see :h leap-custom-mappings
.
Workaround for the duplicate cursor bug when autojumping
Until neovim/neovim#20793 is fixed:
-- 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.
Lazy loading
...is all the rage now, but doing it manually or via some plugin manager is completely redundant, as Leap takes care of it itself. Nothing unnecessary is loaded until you actually trigger a motion.
Permalink to the example file, if you want to follow along.
The search is invoked with s
in the forward direction, S
in the backward
direction, and gs
in the other windows. Let's target some word containing
ol
. After entering the letter o
, the plugin processes all character pairs
starting with it, and from here on, you have all the visual information you
need to reach your specific target. (The highlighting of unlabeled matches -
green underlined on the screenshots - is opt-in, turned on for clarity here.)
Let's finish the pattern, i.e., type l
. Leap now jumps to the first match
(the unlabeled one) automatically - if you aimed for that, you are good to go,
just continue your work! (The labels for the subsequent matches of ol
will
remain visible until the next keypress, but they are carefully chosen "safe"
letters, guaranteed to not interfere with your following editing command.)
Otherwise, type the label character next to your target match, and move on to
that.
Note that Leap only jumps to the first match if the remaining matches can be
covered by the limited set of safe target labels, but stays in place, and
switches to an extended label set otherwise. For fine-tuning or disabling this
behaviour, see :h leap-config
(labels
and safe_labels
).
To show the last important feature, let's go back to the start position, and
start a new jump - we will target the struct member fr_height
on line 1100,
near the bottom (available = oldwin->w_frame->fr_height;
), using the pattern
fr
. Press s
, and then f
:
The blue labels indicate a secondary group of matches, where we start to reuse
the available labels. You can reach those by pressing <space>
first, which
switches to the subsequent match group. To jump to our target (the blue j
),
you should now press r
(to finish the pattern), and then <space>j
.
In very rare cases, if the large number of matches cannot be covered even by
two label groups, you might need to press <space>
multiple times, until you
see the target label, first in blue, and then in green. (Substitute "green" and
"blue" with the actual colors in the current theme.)
<enter>
(special_keys.next_target
) is a very special key: at any stage, it
initiates "traversal" mode, moving on to the next match on each subsequent
keypress. If you press it right after invoking a Leap motion (e.g. s<enter>
),
it uses the previous search pattern. In case you overshoot your target, <tab>
(special_keys.prev_target
) can revert the previous jump(s). Note that if the
safe label set is in use, the labels will remain available the whole time!
You can make next_target
and prev_target
behave like like ;
and ,
, that
is, repeat the last motion without explicitly invoking Leap (see :h leap-repeat-keys
).
Traversal mode can be used as a substitute for fFtT
motions.
s{char}<enter><enter>
is the same as f{char};
, or ds{char}<enter>
as
dt{char}
, but they work over multiple lines.
In case of cross-window search (gs
), you cannot traverse (since there's no
direction to follow), but the search can be repeated, and you can also accept
the first (presumably only) match with <enter>
, even after one input.
Jumping to the end of the line and to empty lines
A character at the end of a line can be targeted by pressing <space>
after it.
There is no special mechanism behind this: <space>
is simply an alias for the
newline character, defined in opts.equivalence_classes
by default.
Empty lines or EOL positions can also be targeted, by pressing the newline
alias twice (<space><space>
). This latter is a slightly more magical feature,
but fulfills the principle that any visible position you can move to with the
cursor should be reachable by Leap too.
Concealed labels
A special character might replace the label in two cases:
-
"Here be dragons" (conflict marker in phase one):
- the label is on top of another label (possible when the target is right next to EOL or the window edge, and the label needs to be shifted left)
- the label immediately follows an unlabeled match (like above)
- the label is on top of an unlabeled match
In the latter two cases, the match highlight is removed, even if enabled in
opts
. -
Two-phase processing is enabled, and unlabeled targets are not highlighted (i.e., the default settings). In this case, targets beyond the secondary group need to have some kind of label next to them, to signal that they are not unlabeled, that is, not directly reachable.
Leap automatically uses either space (if both primary and secondary labels have a background in the current color scheme) or a middle dot (U+00B7).
Below is a list of all configurable values in the opts
table, with their
defaults. Set them like: require('leap').opts.<key> = <value>
. For details on
the particular fields, see :h leap-config
.
case_sensitive = false
equivalence_classes = { ' \t\r\n', }
max_phase_one_targets = nil
highlight_unlabeled_phase_one_targets = false
max_highlighted_traversal_targets = 10
substitute_chars = {}
safe_labels = 'sfnut/SFNLHMUGTZ?'
labels = 'sfnjklhodweimbuyvrgtaqpcxz/SFNJKLHODWEIMBUYVRGTAQPCXZ?'
special_keys = {
next_target = '<enter>',
prev_target = '<tab>',
next_group = '<space>',
prev_group = '<tab>',
}
See :h leap-default-mappings
. To define alternative mappings, you can use the
<Plug>
keys listed at :h leap-custom-mappings
. There is also an
alternative, "fFtT"-style key set for in-window motions, including or excluding
the whole 2-character match in Visual and Operator-pending-mode.
To create custom motions with behaviours different from the predefined ones,
see :h leap.leap()
.
To set repeat keys that work like ;
and ,
that is, repeat the last motion
without explicitly invoking Leap, see :h leap-repeat-keys
.
For customizing the highlight colors, see :h leap-highlight
.
In case you - as a user - are not happy with a certain colorscheme's
integration, you could force reloading the default settings by calling
leap.init_highlight(true)
. The call can even be wrapped in an
autocommand to automatically re-init on every colorscheme change:
autocmd ColorScheme * lua require('leap').init_highlight(true)
This can be tweaked further, you could e.g. check the actual colorscheme, and only execute for certain ones, etc.
Leap triggers User
events on entering/exiting (with patterns LeapEnter
and
LeapLeave
), so that you can set up autocommands, e.g. to change the values of
some editor options while the plugin is active (:h leap-events
).
Workaround for the duplicate cursor bug when autojumping
Until neovim/neovim#20793 is fixed:
-- 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.
Why remap `s`/`S`?
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
(orxi
)S
=cc
Visual mode
s
=c
S
=Vc
, orc
if already in linewise mode
If you are not convinced, just head to :h leap-custom-mappings
.
Bidirectional search in the current window
Simply initiate multi-window mode (that is, call leap()
directly, giving it a
target_windows
argument) with the current window as the only target. Not
recommended for Operator-pending mode, as dot-repeat cannot be used if the
search is non-directional. Another caveat is that you cannot traverse through
the matches. Needless to say, you will also get a lot more visual noise, less
comfortable labels, and half as many auto-jumps on average.
vim.keymap.set('n', 's', function ()
require('leap').leap { target_windows = { vim.api.nvim_get_current_win() } }
end)
Search in all windows
The same caveats as above apply here, obviously even more so.
vim.keymap.set('n', 's', function ()
local focusable_windows = vim.tbl_filter(
function (win) return vim.api.nvim_win_get_config(win).focusable end,
vim.api.nvim_tabpage_list_wins(0)
)
require('leap').leap { target_windows = focusable_windows }
end)
Smart case sensitivity, wildcard characters (one-way aliases)
Ahead-of-time labeling, unfortunately, makes them impossible, by design: for a potential match in phase one, we might need to show two different labels (corresponding to two different futures) at the same time. (1, 2, 3)
Arbitrary remote actions instead of jumping
Basic template:
local function remote_action ()
local focusable_windows = vim.tbl_filter(
function (win) return vim.api.nvim_win_get_config(win).focusable end,
vim.api.nvim_tabpage_list_wins(0)
)
require('leap').leap {
target_windows = 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.
Other supernatural powers besides clairvoyance?
You might be interested in telekinesis.
Disable auto-jumping to the first match
require('leap').opts.safe_labels = {}
Greying out the search area
-- Or just set to grey directly, e.g. { fg = '#777777' },
-- if Comment is saturated.
vim.api.nvim_set_hl(0, 'LeapBackdrop', { link = 'Comment' })
Hiding secondary labels
You can hide the letters, and show emtpy boxes by tweaking the
LeapLabelSecondary
highlight group (that way you keep a visual indication
that the target is labeled):
vim.api.nvim_create_autocmd('ColorScheme', {
callback = function ()
local bg = vim.api.nvim_get_hl(0, {name = 'LeapLabelSecondary'}).bg
vim.api.nvim_set_hl(0, 'LeapLabelSecondary',{ fg = bg, bg = bg, })
end
})
Lightspeed-style highlighting
-- The below settings make Leap's highlighting closer to what you've been
-- used to in Lightspeed.
vim.api.nvim_set_hl(0, 'LeapBackdrop', { link = 'Comment' }) -- or some grey
vim.api.nvim_set_hl(0, 'LeapMatch', {
-- For light themes, set to 'black' or similar.
fg = 'white', bold = true, nocombine = true,
})
-- Lightspeed colors
-- primary labels: bg = "#f02077" (light theme) or "#ff2f87" (dark theme)
-- secondary labels: bg = "#399d9f" (light theme) or "#99ddff" (dark theme)
-- shortcuts: bg = "#f00077", fg = "white"
-- You might want to use either the primary label or the shortcut colors
-- for Leap primary labels, depending on your taste.
vim.api.nvim_set_hl(0, 'LeapLabelPrimary', {
fg = 'red', bold = true, nocombine = true,
})
vim.api.nvim_set_hl(0, 'LeapLabelSecondary', {
fg = 'blue', bold = true, nocombine = true,
})
-- Try it without this setting first, you might find you don't even miss it.
require('leap').opts.highlight_unlabeled_phase_one_targets = true
Working with non-English text
Check out opts.equivalence_classes
. For example, you can group accented
vowels together: { 'aá', 'eé', 'ií', ... }
.
Was the name inspired by Jef Raskin's Leap?
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 is more to Leap than meets the eye. On a general level, you should think
of it as less of a motion plugin and more of an engine for selecting visible
targets on the screen (acquired by arbitrary means), and doing arbitrary things
with them. See :h leap.leap()
.
There are lots of ways you can extend the plugin and bend it to your will, and the combinations of them give you almost infinite possibilities. Some practical examples:
Linewise motions
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', '|', 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)
vim.keymap.set('o', '|', "V<cmd>lua leap_line_start()<cr>")
Select Tree-sitter nodes
Not as sophisticated as flash.nvim's implementation, but totally usable, in 50 lines:
local api = vim.api
local ts = vim.treesitter
local function get_ts_nodes()
if not pcall(ts.get_parser) then return end
local wininfo = vim.fn.getwininfo(api.nvim_get_current_win())[1]
-- Get current node, and then its parent nodes recursively.
local cur_node = ts.get_node()
if not cur_node then return end
local nodes = { cur_node }
local parent = cur_node:parent()
while parent do
table.insert(nodes, parent)
parent = parent:parent()
end
-- Create Leap targets from TS nodes.
local targets = {}
local startline, startcol
for _, node in ipairs(nodes) do
startline, startcol, endline, endcol = node:range() -- (0,0)
local startpos = { startline + 1, startcol + 1 }
local endpos = { endline + 1, endcol + 1 }
-- Add both ends of the node.
if startline + 1 >= wininfo.topline then
table.insert(targets, { pos = startpos, altpos = endpos })
end
if endline + 1 <= wininfo.botline then
table.insert(targets, { pos = endpos, altpos = startpos })
end
end
if #targets >= 1 then return targets end
end
local function select_node_range(target)
local mode = api.nvim_get_mode().mode
-- Force going back to Normal from Visual mode.
if not mode:match('no?') then vim.cmd('normal! ' .. mode) end
vim.fn.cursor(unpack(target.pos))
local v = mode:match('V') and 'V' or mode:match('�') and '�' or 'v'
vim.cmd('normal! ' .. v)
vim.fn.cursor(unpack(target.altpos))
end
local function leap_ts()
require('leap').leap {
target_windows = { api.nvim_get_current_win() },
targets = get_ts_nodes,
action = select_node_range,
}
end
vim.keymap.set({'x', 'o'}, '\\', leap_ts)
Remote text objects
See leap-spooky.nvim.
<<<<<<< HEAD
Enhanced f/t motions
======= Leap can also be used by external plugins, for example in `telescope.nvim` to allow for quick selection between search results. The below config maps (normal-mode) `s` within a Telescope picker to use Leap to select an entry and perform the default action (i.e. ``) on it, and (normal-mode) `S` to only select the entry (so you can perform a different action ot it).Example: select a Telescope entry
local telescope = require "telescope"
local actions = require "telescope.actions"
local action_state = require "telescope.actions.state"
local function get_telescope_targets(prompt_bufnr)
local pick = action_state.get_current_picker(prompt_bufnr)
local scroller = require "telescope.pickers.scroller"
local wininfo = vim.fn.getwininfo(pick.results_win)
-- restrict targets to visible range of entries
local first = math.max(scroller.top(pick.sorting_strategy, pick.max_results, pick.manager:num_results()), wininfo[1].topline - 1)
local last = wininfo[1].botline - 1
local targets = {}
for row=last,first,-1 do
local target = {
wininfo = wininfo[1],
pos = {row + 1, 1},
row = row,
pick = pick
}
table.insert(targets, target)
end
return targets
end
telescope.setup {
defaults = {
-- ....
mappings = {
n = {
-- set the current selected entry using Leap
["S"] = function (prompt_bufnr)
require('leap').leap {
targets = get_telescope_targets(prompt_bufnr),
action = function (target)
target.pick:set_selection(target.row)
end
}
end,
-- perform the default action (i.e. <CR>) on the entry selected using Leap
["s"] = function (prompt_bufnr)
require('leap').leap {
targets = get_telescope_targets(prompt_bufnr),
action = function (target)
target.pick:set_selection(target.row)
actions.select_default(prompt_bufnr)
end
}
end
}
}
},
}
(see also telescope-hop.nvim).
origin/telescope-integration
See flit.nvim.