- Notifications
You must be signed in to change notification settings - Fork47
Neovim's answer to the mouse 🦘
License
ggandor/leap.nvim
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Leap is a general-purpose motion plugin for Neovim, building and improvingprimarily onvim-sneak, with theultimate goal of establishing a new standard interface for moving around in thevisible area in Vim-like modal editors. It allows you to reach any target in avery fast, uniform way, and minimizes the required focus level while executinga jump.
Leap's default motions allow you to jump to any position in the visible editorarea by entering a 2-character search pattern, and then potentially a labelcharacter to pick your target from multiple matches, similar to Sneak. The mainnovel idea in Leap is thatyou get a preview of the target labels - you cansee which key you will need to press before you actually need to do that.
- Initiate the search in the current window (
s
) or in the other windows(S
). (Note: you can use a single key for the whole tab page, if you areokay with the trade-offs.) - Start typing a 2-character pattern (
{char1}{char2}
). - After typing the first character, you see "labels" appearing next to some ofthe
{char1}{?}
pairs. You cannot use them yet - they only get active afterfinishing the pattern. - Enter
{char2}
. If the pair was not labeled, you automatically jump there.You can safely ignore the remaining labels, and continue editing - those areguaranteed non-conflicting letters, disappearing on the next keypress. - Else: type the label character, that is now active. If there are more matchesthan available labels, you can switch between groups, using
<space>
and<backspace>
.
Every visible position is targetable:
s{char}<space>
jumps to the last character on a line.s<space><space>
jumps to end-of-line characters, including empty lines.
At any stage,<enter>
consistently jumps to the next/closest available target(<backspace>
steps back):
s<enter>...
repeats the previous search.s{char}<enter>...
can be used as a multiline substitute forfFtT
motions.
It is ridiculously fast: not counting the trigger key, leaping to literallyanywhere on the screen rarely takes more than 3 keystrokes in total, that can betyped in one go. Often 2 is enough.
At the same time, it reduces mental effort to almost zero:
Youdon't have to weigh alternatives: a single universal motion type can beused in all non-trivial situations.
Youdon't have to compose motions in your head: one command achieves onelogical movement.
Youdon't have to be aware of the context: the eyes can keep focusing on thetarget the whole time.
Youdon't have to make decisions on the fly: the sequence you should typeis fixed from the start.
Youdon't have to pause in the middle: if typing at a moderate speed, ateach step you already know what the immediate next keypress should be, andyour mind can process the rest in the background.
The plugin is not 100% stable yet, but don't let that stop you - the usagebasics are extremely unlikely to change. To follow breaking changes, subscribeto the correspondingissue.
- Neovim >= 0.10.0 stable, or latest nightly
- repeat.vim, for dot-repeats (
.
) towork
Use your preferred method or plugin manager. No extra steps needed besidesdefining keybindings - to use the default ones, put the following into yourconfig (overridess
in all modes, andS
in Normal mode):
require('leap').set_default_mappings()
Alternative key mappings and arrangements
Callingrequire('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 almostnever 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 cursorcould jump in/to a different window than your goal, right before selecting thetarget.)
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.
Suggested additional tweaks
Highly recommended: define a preview filter to reduce visual noise and theblinking effect after the first keypress (:h leap.opts.preview_filter
). Youcan still target any visible positions if needed, but you can define what isconsidered an exceptional case ("don't bother me with preview for them").
-- Exclude whitespace and the middle of alphabetic words from preview:-- foobar[baaz] = quux-- ^----^^^--^^-^-^--^require('leap').opts.preview_filter=function (ch0,ch1,ch2)returnnot (ch1:match('%s')orch0:match('%a')andch1:match('%a')andch2:match('%a') )end
Define equivalence classes for brackets and quotes, in addition to the defaultwhitespace group:
require('leap').opts.equivalence_classes= {'\t\r\n','([{',')]}','\'"`'}
Use the traversal keys to repeat the previous motion without explicitlyinvoking Leap:
require('leap.user').set_repeat_keys('<enter>','<backspace>')
Lazy loading
...is all the rage now, but doing it via your plugin manager is unnecessary, asLeap lazy loads itself. Using thekeys
feature of lazy.nvim might even causeproblems.
Experimental modules, might be moved out, and APIs are subject to change.
Remote actions
Inspired byleap-spooky.nvim,andflash.nvim's similar feature.
This function allows you to perform an action in a remote location: itforgets the current mode or pending operator, lets you leap with thecursor (to anywhere on the tab page), then continues where it left off.Once an operation or insertion is finished, it moves the cursor back tothe original position, as if you had operated from the distance.
vim.keymap.set({'n','x','o'},'gs',function ()require('leap.remote').action()end)
Example:gs{leap}yap
,vgs{leap}apy
, orygs{leap}ap
yank the paragraph atthe position specified by{leap}
.
Tip: As the remote mode is active until returning to Normal mode again (by anymeans),<ctrl-o>
becomes your friend in Insert mode, or when doing changeoperations.
Swapping regions
Exchanging two regions of text becomes moderately simple, without needing acustom plugin:d{region1} gs{leap}v{region2}p P
. Example (swapping twowords):diw gs{leap}viwp P
.
With remote text objects (see below), the swap is even simpler, almost on parwithvim-exchange:diw virw{leap}p P
.
Using remote text objectsand combining them with an exchange operator ispretty much text editing at the speed of thought:cxiw cxirw{leap}
.
Icing on the cake, no. 1 - giving input ahead of time
Theinput
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)-- Other ideas: `V` (forced linewise), `K`, `gx`, etc.
By feeding text objects asinput
, you can createremote text objects, foran even more intuitive workflow (yarp{leap}
- "yank a remote paragraphat..."):
-- 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`.dolocalremote_text_object=function (prefix)localok,ch=pcall(vim.fn.getcharstr)-- pcall for handling <C-c>ifnotokor (ch==vim.keycode('<esc>'))thenreturnendrequire('leap.remote').action {input=prefix..ch }endvim.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 optionalcount
(yaa{leap}
,y3aa{leap}
):
vim.keymap.set({'x','o'},'aa',function ()-- Force linewise selection.localV=vim.fn.mode(true):match('V')and''or'V'-- In any case, move horizontally, to trigger operations.localinput=vim.v.count>1and (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, evenfrom another window (yarp{leap}
, and voilà, the remote paragraph appearsthere):
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.ifvim.v.operator=='y'andevent.data.register=='"'thenvim.cmd('normal! p')endend,})
Incremental treesitter node selection
Besides choosing a label (R{label}
), in Normal/Visual mode you can also usethe traversal keys for incremental selection. The labels are forced to be safe,so you can operate on the selection right away (RRRy
). Traversal can also"wrap around" backwards (Rr
selects the root node).
vim.keymap.set({'x','o'},'R',function ()require('leap.treesitter').select {-- To increase/decrease the selection in a clever-f-like manner,-- with the trigger key itself (vRRRRrr...). The default keys-- (<enter>/<backspace>) also work, so feel free to skip this.opts=require('leap.user').with_traversal_keys('R','r') }end)
Note that it is worth using (forced) linewise mode (VRRR...
,yVR
), asredundant nodes are filtered out (only the outermost are kept in a given linerange), making the selection much more efficient.
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
). WhileLeap has deeply thought-through, opinionated defaults, its small(ish) butcomprehensive API makes it pretty flexible.
Premise: jumping from point A to B on the screen should not be someexcitingpuzzle, for which you should train yourself; itshould be a non-issue. An ideal keyboard-driven interface would impose almost nomore cognitive burden than using a mouse, without the constant context-switchingrequired by the latter.
That is,you do not want to think about
- the command: we need one fundamental targeting method that can bring youanywhere: a jetpack on the back, instead of airline routes (↔EasyMotion and itsderivatives)
- the context: it should be enough to look at the target, and nothing else(↔ vanilla Vim motion combinations using relative line numbers and/orrepeats)
- the steps: the motion should be atomic (↔ Vim motion combos), and ideallyyou should be able to type the whole sequence in one go, on more or lessautopilot (↔ any kind of "just-in-time" labeling method; note that the"search command on steroids" approach byPounce andFlash, where the labels appear at anunknown time by design, makes this last goal impossible)
All the while usingas few keystrokes as possible, and getting distracted byas little incidental visual noise as possible.
It is obviously impossible to achieve all of the above at the same time, withoutsome trade-offs at least; but in our opinion Leap comes pretty close, occupyinga sweet spot in the design space. (The worst remaining offender might be visualnoise, but clever filtering in the preview phase can help - see:h leap.opts.preview_filter
.)
Theone-step shift between perception and action is the big idea that cutsthe Gordian knot: a fixed pattern length combined with previewing labels caneliminate the surprise factor from the search-based method (which is the onlyviable approach - see "jetpack" above). Fortunately, a 2-character pattern -the shortest one with which we can play this trick - is also long enough tosufficiently narrow down the matches in the vast majority of cases.
Fixed pattern length also makes(safe) automatic jump to the first targetpossible. You cannot improve on jumping directly, just like howf
andt
works, not having to read a label at all, and not having to accept the matchwith<enter>
either. However, we can do this in a smart way: if there aremany 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'reintroducing 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 isunique in that it
offers a smoother experience, by (somewhat) eliminating the pause beforetyping the label
feels natural to use for both distantand close targets
Why remap `s`/`S`?
Common operations should use the fewest keystrokes and the most comfortablekeys, so it makes sense to take those over by Leap, especially given that bothnative 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
.
Smart case sensitivity, wildcard characters (one-wayaliases)
The preview phase, unfortunately, makes them impossible, by design: for apotential match, we might need to show two different labels (corresponding totwo different futures) at the same time.(1,2,3)
Arbitrary remote actions instead of jumping
Basic template:
localfunctionremote_action ()require('leap').leap {target_windows=require('leap.user').get_focusable_windows(),action=function (target)localwinid=target.wininfo.winidlocallnum,col=unpack(target.pos)-- 1/1-based indexing!-- ... do something at the given position ...end, }end
SeeExtending Leap for more.
Disable auto-jumping to the first match
require('leap').opts.safe_labels= {}
Disable previewing labels
require('leap').opts.preview_filter=function ()returnfalseend
Always show labels at the beginning of the match
Note:on_beacons
is an experimental escape hatch, and this workaround dependson implementation details.
-- `on_beacons` hooks into `beacons.light_up_beacons`, the function-- responsible for displaying stuff.require('leap').opts.on_beacons=function (targets,_,_)for_,tinipairs(targets)do-- Overwrite the `offset` value in all beacons.-- target.beacon looks like: { <offset>, <extmark_opts> }ift.labelandt.beaconthent.beacon[1]=0endend-- Returning `true` tells `light_up_beacons` to continue as usual-- (`false` would short-circuit).returntrueend
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'})
Working with non-English text
If alanguage-mapping
('keymap'
) is active,Leap waits for keymapped sequences as needed and searches for the keymappedresult as expected.
Also check outopts.equivalence_classes
, that lets you group certaincharacters together as mutual aliases, e.g.:
{'\t\r\n','aäàáâãā','dḍ','eëéèêē','gǧğ','hḥḫ','iïīíìîı','nñ','oō','sṣšß','tṭ','uúûüűū','zẓ'}
Was the name inspired by Jef Raskin's Leap?
To paraphrase Steve Jobs about their logo and Turing's poison apple, I wish itwere, but it is a coincidence. "Leap" is just another synonym for "jump", thathappens to rhyme with Sneak. That said, you can think of the name as alittle tribute to the great pioneer of interface design, even though embracingthe modal paradigm is a fundamental difference in Vim's 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 ofthe function (search scope, jump offset, etc.), you can:
- give it a customaction to perform, instead of jumping
- feed it with customtargets, and only use it as labeler/selector
- customize its behavior on a per-call basis viaautocommands
Some practical examples:
1-character search (enhanced f/t motions)
Note:inpulen
is an experimental feature at the moment, subject to change orremoval.
dolocalfunctionft_args (key_specific_args)localcommon_args= {inputlen=1,inclusive_op=true,opts= {case_sensitive=true,labels= {},-- Match the modes here for which you don't want to use labels.safe_labels=vim.fn.mode(1):match('o')and {}ornil, }, }returnvim.tbl_deep_extend('keep',common_args,key_specific_args)endlocalleap=require('leap').leap-- This helper function makes it easier to set "clever-f"-like-- functionality (https://github.com/rhysd/clever-f.vim), returning-- an `opts` table, where:-- * the given keys are set as `next_target` and `prev_target`-- * `prev_target` is removed from `safe_labels` (if appears there)-- * `next_target` is used as the first labellocalwith_traversal_keys=require('leap.user').with_traversal_keyslocalf_opts=with_traversal_keys('f','F')localt_opts=with_traversal_keys('t','T')-- You can of course set ;/, for both instead:-- local ft_opts = with_traversal_keys(';', ',')vim.keymap.set({'n','x','o'},'f',function ()leap(ft_args({opts=f_opts, }))end)vim.keymap.set({'n','x','o'},'F',function ()leap(ft_args({opts=f_opts,backward=true }))end)vim.keymap.set({'n','x','o'},'t',function ()leap(ft_args({opts=t_opts,offset=-1 }))end)vim.keymap.set({'n','x','o'},'T',function ()leap(ft_args({opts=t_opts,backward=true,offset=-1 }))end)end
Jump to lines
Note:pattern
is an experimental feature at the moment, subject toremoval.
localfunctionleap_linewise ()local_,l,c=unpack(vim.fn.getpos('.'))localpattern='\\v'-- Skip 3-3 lines around the cursor...'(%<'..(math.max(1,l-3))..'l|%>'..(l+3)..'l)'-- Cursor column or the last one (if we're beyond that)...'(%'..c..'v|%<'..c..'v$)'require('leap').leap {pattern=pattern,target_windows= {vim.fn.win_getid() },opts= {safe_labels=''} }end-- For maximum comfort, force linewise selection in-- the mappings:vim.keymap.set({'n','x','o'},'|',function ()localmode=vim.fn.mode(1)-- Only force V if not already in it (otherwise it exits Visual mode).ifnotmode:match('n$')andnotmode:match('V')thenvim.cmd('normal! V')endleap_linewise()end)
Shortcuts to Telescope results
-- NOTE: If you try to use this before entering any input, an error is thrown.-- (Help would be appreciated, if someone knows a fix.)localfunctionget_targets (buf)localpick=require('telescope.actions.state').get_current_picker(buf)localscroller=require('telescope.pickers.scroller')localwininfo=vim.fn.getwininfo(pick.results_win)[1]localtop=math.max(scroller.top(pick.sorting_strategy,pick.max_results,pick.manager:num_results()),wininfo.topline-1 )localbottom=wininfo.botline-2-- skip the current rowlocaltargets= {}forlnum=bottom,top,-1do-- start labeling from the closest (bottom) rowtable.insert(targets, {wininfo=wininfo,pos= {lnum+1,1 },pick=pick, })endreturntargetsendlocalfunctionpick_with_leap (buf)require('leap').leap {targets=function ()returnget_targets(buf)end,action=function (target)target.pick:set_selection(target.pos[1]-1)require('telescope.actions').select_default(buf)end, }endrequire('telescope').setup {defaults= {mappings= {i= { ['<a-p>']=pick_with_leap }, } }}
About
Neovim's answer to the mouse 🦘
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.