Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Neovim's answer to the mouse 🦘

License

NotificationsYou must be signed in to change notification settings

ggandor/leap.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

leap.nvim

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.

showcase

How to use it (TL;DR)

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.

Why is this method cool?

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.

Getting started

Status

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.

Requirements

  • Neovim >= 0.10.0 stable, or latest nightly

Dependencies

Installation

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.

Extras

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.

Next steps

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.

Design considerations in detail

The ideal

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.

How do we measure up?

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 andtworks, 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

FAQ

Defaults

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.

Features

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.

Configuration

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ïīíìîı','','','sṣšß','tṭ','uúûüűū','zẓ'}

Miscellaneous

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.

Extending Leap

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

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp