leap.nvim

Neovim's answer to the mouse

leap.nvim is a Neovim plugin implementing a novel strategy for jumping to any point on-screen in 2–3 keystrokes.

Details

How are targets ordered?

NOTE: The following is based primarily on experimentation and observation, rather than reading the source code.

Depends. Forward/backward invocations are ordered by occurrence, whilst whole-screen leaps are sorted by euclidean distance from the cursor! Very cool! There is an exception:

Note: <Plug>(leap) sorts matches by euclidean distance from the cursor, with the exception that the current line, and on the current line, forward direction is prioritized. That is, you can always be sure that the targets right in front of you will be the first ones.

Safe labels

leap.opts.safe_labels is a list of keys that you would never use after a jump. Why is label safety a desirable property? Because, Leap can automatically jump to the first occurring (not nearest, as when sorting by euclidean distance) target. If the user was aiming for a non-initial target, they can simply hit the associated label without another thought; if they were aiming for the first target, they can immediately start editing without accidentally triggering another label!

leap.opts.safe_labels covers the majority of invocations. However, a plan is still required for other cases. When the number of targets is greater than the number of safe labels, an extended set of labels, leap.opts.labels, is used. See What happens when there are too many targets?.

It appears care has been taken to assign easier-to-hit labels to nearer targets. Look at the default values of safe_labels and labels:

{:safe_labels "sfnut/SFNLHMUGTZ?"
 :labels "sfnjklhodweimbuyvrgtaqpcxz/SFNJKLHODWEIMBUYVRGTAQPCXZ?"}

Naïvely, my initial assumption was that safe_labels would be a prefix of labels, to preserve the position-to-label intuition built by the user across invocations. Instead, capital letters and awkward-to-hit keys are shifted to the end. This is likely to be better than my idea.

Labels known ahead of time

Upon typing the first key, a number of labels are immediately visible. How are they known to be correct even when the second key is unknown?

When are labels known?

Enter s m. The first match for m is followed by a. Thus, we can safely assign the label (nth 0 leap.opts.safe_labels). The second match is mb. Once again, it is the first occurrence of that sequence, so it may be assigned (nth 0 leap.opts.safe_labels). Finally, let the third occurrence of m be another ma. It will be assigned (nth 1 leap.opts.safe_labels).

Essentially, for some prefix p, we can take the list of matches, regroup it into a list of identical matches, and zip each one with leap.opts.safe_labels.

labels :: List Char

data Match = MkMatch
  { matchedText :: Text
  , position :: Pos
  }

-- Assume input is sorted by position.
adornLabels :: Pos -> List Match -> List (Char, Match)
adornLabels cursorPos
  = toList
  . fmap (zip labels)
  . sortLabels cursorPos
  . NE.groupAllWith (index 1 . matchedText)

When aren't labels known?

It seems like never... leap.nvim has a few options that intentionally reduce the number of labels for the sake of less visual noise, which may be what I'm observing.

  • preview_filter (default: nil)

  • max_highlighted_traversal_targets (default: 10)

When are labels hidden?

The first occurence of any "chunk" is not labeled, simply because it doesn't need one. Once the second character is entered, the first match is unconditionally jumped to.

This is my screen after inputting s a with my cursor positioned at (1,1). Notice: the ad chunk in the word Reading, and the au chunk in the word author are both unlabeled; they are the first occurrences of those sequences, so they will be jumped to immediately after pressing d or u. On the other hand, the ad chunk in Madeleine is labeled, as it's the second occurrence.

file:leap-nvim-assets/first-match-is-unlabeled.png

What happens when there are too many targets?

file:leap-nvim-assets/many-targets.mkv

  • The first match is not automatically jumped to!

  • The extended, unsafe set of labels, leap.opts.labels, is used.

  • Targets for which the set of labels has been exhausted are labeled with a space. Pressing SPC eliminates the currently-labeled targets, and regenerates the labels. This process may need to be repeated if there are a comical number of targets in scope.

Reusability

Leap presents itself 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." This is accomplished by splitting the primary motion functionality into separate phases for searching and selecting. I.e., pressing s will first prompt for input and read the two search characters, before handing off a collection of targets to the more general entry point, leap.leap, which will auto-jump, request further input for selecting labels, and so on.

What happens when a non-existent label is given?

The leap invocation is aborted, and the input simply passes through.

What does the count do? E.g. 2sli

In precense of a numeric prefix argument, (leap-forward) and (leap-backward) will entirely skip the labeling process, and simply jump to the first occurence of a given two-character search.

The algorithm, as implemented in evil-leap

The described algorithm is split into separate 'search' and 'select' phases. See Reusability.

Search

The first character is given

As soon as the first character is given, to each possible second character, we may associate a single branch. The user shall be presented with all branches at once.

Example

For the following examples, imagine the user has input l:

Example branch le

This branch represents the timeline in which the user's second character is e.

file:leap-nvim-assets/branch-le.png

There are fewer targets than safe labels, so the first target can be auto-jumped to. This means that the first target will be unlabeled.

Example branch li

This branch represents the timeline in which the user's second character is i.

file:leap-nvim-assets/branch-li.png

In this case, we have to resort to the extended set of unsafe labels. Auto-jump is unavailable, so we label even the first target.

The second character is given

As soon as the two-character pattern is first given, we already know the exact input sequence associated with each target.

Select

By this point, we have a collection of positions available.

Determine if safe labels and auto-jump, are available by comparing the number of targets to the number of safe labels.

TODO: I think we should be testing (<= (length targets) (+ 1 (length evil-leap-safe-labels))).

Fewer targets than safe labels; safe labels are available

Let the list of safe labels be equal to (l :: ls). Auto-jump to l, and assign the remaining targets labels ls.

More targets than safe labels; safe labels are not available

Label each target with the corresponding label from the unsafe set, and assign the remaining targets the label \s.

Possible optimisations

Open questions

How well does this work for non-ascii text? non-English? non-Latin?

How could we compare Leap to other options?

Possible benchmark for raw speed

  • Take a bunch of real sample files of code, prose, etc.

  • Scroll to a random position, and choose an arbitrary position to jump to.

  • Go to the given position with Leap, /, ...

  • Compare keystrokes.

One-to-many mappings

Leap allows you to define equivalence classes for search characters:

                                                 *leap.opts.equivalence_classes*
`equivalence_classes = { ' \t\r\n' }`

    A character in search patterns will match any other in its equivalence
    class. The sets can either be defined as strings or tables.

    Example - whitespace, brackets, and quotes: >lua
        { ' \t\r\n', '([{', ')]}', '\'"`' }
<
    Note: If you want to be able to target empty lines, and characters at the
    end of a line, make sure to keep an alias for `\n` and `\r`.
    |leap-to-end-of-line|

    Note: Wildcard characters (non-mutual aliases) are not possible in Leap,
    for the same reason that supporting |smartcase| is not possible: we cannot
    read your mind, and decide on which label to show for |leap-preview|.
    (Generally: having a `?` -> `[bc]` mapping, after pressing `a`, we either
    label an `ab` match as part of the `ab` subset of matches - corresponding
    to pressing `b` next -, or the potentially bigger `a[bc]` subset -
    corresponding to pressing `?` next, the wildcard.)

I don't see why `?` -> `[bc]` mappings aren't possible. I suspect we can merge both branches.

Extensibility

https://github.com/ggandor/leap.nvim/issues/151

Examples to demo the API:

  • Leap to line

  • Leap to tree-sitter object

  • Remote actions