Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

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

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 forward (s) or backward (S) direction, or inthe other windows (gs). (Note: you can use a single key for the currentwindow or even the whole tab page, 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 ofthe{char1}{?} pairs. You cannot use the labels yet - they only get activeafter finishing the pattern.
  • Enter{char2}. If the pair was not labeled, then voilà, you're alreadythere. You can safely ignore the remaining labels, and continue editing -those are guaranteed non-conflicting letters, disappearing on the nextkeypress.
  • 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>.

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 emptylines.

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 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 in your head: one command achieves one logicalmovement.

  • 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 enteris determined from the very beginning.

  • 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.9.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,S andgs in all modes):

require('leap').create_default_mappings() (init.lua)

lua require('leap').create_default_mappings() (init.vim)

Alternative key mappings and arrangements (bidirectional jump,etc.)

Callingrequire('leap').create_default_mappings() is equivalent to:

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)')

Bidirectionals for Normal and Visual mode:

vim.keymap.set({'n','x'},'s','<Plug>(leap)')vim.keymap.set('n','S','<Plug>(leap-from-window)')vim.keymap.set('o','s','<Plug>(leap-forward)')vim.keymap.set('o','S','<Plug>(leap-backward)')

Trade-off: Compared to using separate keys for the two directions, you willonly get half as many autojumps on average.

Jump to anywhere in Normal mode with one key:

vim.keymap.set('n','s','<Plug>(leap-anywhere)')vim.keymap.set('x','s','<Plug>(leap)')vim.keymap.set('o','s','<Plug>(leap-forward)')vim.keymap.set('o','S','<Plug>(leap-backward)')

Trade-off: if you have multiple windows open on the tab page, you will almostnever get an autojump, except if all targets are in the same window. (This isan intentional restriction: it would be too disorienting if the cursor couldjump in/to a different window than your goal, right before selecting thetarget.)

Note that when searching bidirectionally in the current window, Leap sortsmatches by euclidean (beeline) distance from the cursor, with the exceptionthat the current line you're on, and on that line, forward direction isprioritized. That is, you can always be sure that the targets right in front ofyou will be the first ones.

Bidirectional search is not recommended for Operator-pending mode, asdot-repeat cannot be used if the search is non-directional. Also worth notingthat in Normal and Visual mode you cannot traverse through the matches anymore(:h leap-traversal), although invoking repeat right away (:h leap-repeat)can substitute for that.

See:h leap-custom-mappings for more.

Suggested additional tweaks
-- 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>')-- Define a preview filter (skip the middle of alphanumeric words):require('leap').opts.preview_filter=function (ch0,ch1,ch2)returnnot (ch1:match('%s')orch0:match('%w')andch1:match('%w')andch2:match('%w')    )end
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 features, APIs might be subject to change.

Remote operations ("spooky actions at a distance")

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.

-- 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, orygs{leap}ap yank the paragraph atthe position specified by{leap}.

Tips

  • Swapping regions becomes moderately simple, without needing a customplugin:d{region1} gs{leap} v{region2} pP. Example (swapping twowords):diwgs{leap}viwpP.

  • As the remote mode is active until returning to Normal mode again (byany means),<ctrl-o> becomes your friend in Insert mode, or whendoing change operations.

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)-- 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)-- Remote gx:vim.keymap.set('n','gX',function ()require('leap.remote').action {input='gx'}end)

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>ifnotokorch==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, do some movement, to trigger operations in O-p mode.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_augroup('LeapRemote', {})vim.api.nvim_create_autocmd('User', {pattern='RemoteOperationDone',group='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
vim.keymap.set({'n','x','o'},'ga',function ()require('leap.treesitter').select()end)-- Linewise.vim.keymap.set({'n','x','o'},'gA','V<cmd>lua require("leap.treesitter").select()<cr>')

Besides choosing a label (ga{label}), in Normal/Visual mode you can also usethe traversal keys for incremental selection (; and, are automaticallyadded to the default keys). The labels are forced to be safe, so you canoperate on the current selection right away (ga;;y).

Tips

  • The traversal can "wrap around" backwards, so you can select the root noderight away (ga,), instead of going forward (ga;;;...).

  • Linewise mode skips the current line, and redundant nodes are also filteredout (only the outermost are kept among the ones that span the same lineranges).

  • To increase/decrease the selection in aclever-f-like manner (gaaaAA...instead ofga;;,,), set the trigger key (or the suffix of it) and itsinverted case as temporary traversal keys for this specific call (select()can take anopts argument, just likeleap() - see:h leap.leap()):

    -- "clever-a"vim.keymap.set({'n','x','o'},'ga',function ()localsk=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({'a'}, {sk.next_target}))sk.prev_target=vim.fn.flatten(vim.list_extend({'A'}, {sk.prev_target}))require('leap.treesitter').select {opts= {special_keys=sk } }end)

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

Bugs

Workaround for the duplicate cursor bug when autojumping

For Neovim versions < 0.10 (neovim/neovim#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#70and#143 to tweak it.

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
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, in some respects you can indeed thinkof leap.nvim as a spiritual successor to Raskin's work, and thus the name as alittle tribute to the great pioneer of interface design, even though embracingthe modal paradigm is a fundamental difference in our 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:

Linewise motions
localfunctionget_line_starts(winid,skip_range)localwininfo=vim.fn.getwininfo(winid)[1]localcur_line=vim.fn.line('.')-- Skip lines close to the cursor.localskip_range=skip_rangeor2-- Get targets.localtargets= {}locallnum=wininfo.toplinewhilelnum<=wininfo.botlinedolocalfold_end=vim.fn.foldclosedend(lnum)-- Skip folded ranges.iffold_end~=-1thenlnum=fold_end+1elseif (lnum<cur_line-skip_range)or (lnum>cur_line+skip_range)thentable.insert(targets, {pos= {lnum,1 } })endlnum=lnum+1endend-- Sort them by vertical screen distance from cursor.localcur_screen_row=vim.fn.screenpos(winid,cur_line,1)['row']localfunctionscreen_rows_from_cur(t)localt_screen_row=vim.fn.screenpos(winid,t.pos[1],t.pos[2])['row']returnmath.abs(cur_screen_row-t_screen_row)endtable.sort(targets,function (t1,t2)returnscreen_rows_from_cur(t1)<screen_rows_from_cur(t2)end)if#targets>=1thenreturntargetsendend-- You can pass an argument to specify a range to be skipped-- before/after the cursor (default is +/-2).functionleap_line_start(skip_range)localwinid=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).ifvim.fn.mode(1)~='V'thenvim.cmd('normal! V')endleap_line_start()end)vim.keymap.set('o','|',"V<cmd>lua leap_line_start()<cr>")
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 },    }  }}
Enhanced f/t motions

Seeflit.nvim. Note that this is not aproper extension plugin, as it uses undocumented API too.

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp