1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-23 07:20:04 +08:00

feat(hop): add hop.nvim for neovim v0.6.0

This commit is contained in:
Wang Shidong 2022-04-27 22:13:32 +08:00 committed by GitHub
parent 550fd06375
commit e360f0c9d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 3321 additions and 67 deletions

View File

@ -8,6 +8,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@master
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install git
- name: Run vint with reviewdog
uses: reviewdog/action-vint@v1.0.1
with:

View File

@ -188,19 +188,6 @@ function! SpaceVim#layers#core#config() abort
\ . string(s:_function('s:explore_current_dir')) . ', [1])',
\ 'split-explore-current-directory', 1)
" call SpaceVim#mapping#space#def('nmap', ['j', 'j'], '<Plug>(easymotion-overwin-f)', 'jump to a character', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'j'], '<Plug>(better-easymotion-overwin-f)', 'jump-or-select-to-a-character', 0, 1)
nnoremap <silent> <Plug>(better-easymotion-overwin-f) :call <SID>better_easymotion_overwin_f(0)<Cr>
xnoremap <silent> <Plug>(better-easymotion-overwin-f) :<C-U>call <SID>better_easymotion_overwin_f(1)<Cr>
call SpaceVim#mapping#space#def('nmap', ['j', 'J'], '<Plug>(easymotion-overwin-f2)', 'jump-to-suite-of-two-characters', 0)
call SpaceVim#mapping#space#def('nnoremap', ['j', 'k'], 'j==', 'goto-next-line-and-indent', 0)
" call SpaceVim#mapping#space#def('nmap', ['j', 'l'], '<Plug>(easymotion-overwin-line)', 'jump to a line', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'l'], '<Plug>(better-easymotion-overwin-line)', 'jump-or-select-to-a-line', 0, 1)
nnoremap <silent> <Plug>(better-easymotion-overwin-line) :call <SID>better_easymotion_overwin_line(0)<Cr>
xnoremap <silent> <Plug>(better-easymotion-overwin-line) :<C-U>call <SID>better_easymotion_overwin_line(1)<Cr>
call SpaceVim#mapping#space#def('nmap', ['j', 'v'], '<Plug>(easymotion-overwin-line)', 'jump-to-a-line', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'w'], '<Plug>(easymotion-overwin-w)', 'jump-to-a-word', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'q'], '<Plug>(easymotion-overwin-line)', 'jump-to-a-line', 0)
call SpaceVim#mapping#space#def('nnoremap', ['j', 'n'], "i\<cr>\<esc>", 'sp-newline', 0)
call SpaceVim#mapping#space#def('nnoremap', ['j', 'c'], 'call call('
\ . string(s:_function('s:jump_last_change')) . ', [])',
@ -220,9 +207,6 @@ function! SpaceVim#layers#core#config() abort
call SpaceVim#mapping#space#def('nnoremap', ['w', 'R'], 'call call('
\ . string(s:_function('s:previous_window')) . ', [])',
\ 'rotate-windows-backward', 1)
call SpaceVim#mapping#space#def('nnoremap', ['j', 'u'], 'call call('
\ . string(s:_function('s:jump_to_url')) . ', [])',
\ 'jump-to-url', 1)
call SpaceVim#mapping#def('nnoremap <silent>', '<S-Tab>', ':wincmd p<CR>', 'Switch to previous window or tab','wincmd p')
call SpaceVim#mapping#space#def('nnoremap', ['<Tab>'], 'try | b# | catch | endtry', 'last-buffer', 1)
let lnum = expand('<slnum>') + s:lnum - 1
@ -610,11 +594,6 @@ else
endfunction
endif
function! s:jump_to_url() abort
let g:EasyMotion_re_anywhere = 'http[s]*://'
call feedkeys("\<Plug>(easymotion-jumptoanywhere)")
endfunction
function! s:safe_erase_buffer() abort
if s:MESSAGE.confirm('Erase content of buffer ' . expand('%:t'))
normal! ggdG
@ -857,50 +836,6 @@ function! s:comment_invert_yank(visual) range abort
call feedkeys("\<Plug>NERDCommenterInvert")
endfunction
function! s:better_easymotion_overwin_line(is_visual) abort
let current_line = line('.')
try
if a:is_visual
call EasyMotion#Sol(0, 2)
else
call EasyMotion#overwin#line()
endif
" clear cmd line
noautocmd normal! :
if a:is_visual
let last_line = line('.')
exe current_line
if last_line > current_line
exe 'normal! V' . (last_line - current_line) . 'j'
else
exe 'normal! V' . (current_line - last_line) . 'k'
endif
endif
catch /^Vim\%((\a\+)\)\=:E117/
endtry
endfunction
function! s:better_easymotion_overwin_f(is_visual) abort
let [current_line, current_col] = getpos('.')[1:2]
try
call EasyMotion#OverwinF(1)
" clear cmd line
noautocmd normal! :
if a:is_visual
let last_line = line('.')
let [last_line, last_col] = getpos('.')[1:2]
call cursor(current_line, current_col)
if last_line > current_line
exe 'normal! v' . (last_line - current_line) . 'j0' . last_col . '|'
else
exe 'normal! v' . (current_line - last_line) . 'k0' . last_col . '|'
endif
endif
catch /^Vim\%((\a\+)\)\=:E117/
endtry
endfunction
function! s:comment_paragraphs(invert) abort
if a:invert

View File

@ -48,6 +48,29 @@
" >
" autosave_location/path+=to+=filename.ext.backup
" <
" 5. `enable_hop`: by default, spacevim use easymotion plugin. and if you are
" using neovim 0.6.0 or above, hop.nvim will be enabled. You can disabled this
" plugin and still using easymotion.
"
" @subsection key bindings
"
" The `edit` layer also provides many key bindings:
" >
" key binding description
" SPC x c count in the selection region
" <
"
" The following key binding is to jump to targets. The default plugin is
" `easymotion`, and if you are using neovim 0.6.0 or above. The `hop.nvim` will
" be used.
" >
" key binding description
" SPC j j jump or select a character
" SPC j J jump to suite of two characters
" SPC j l jump or select to a line
" SPC j w jump to a word
" SPC j u jump to a url
" <
scriptencoding utf-8
if exists('s:autosave_timeout')
@ -66,6 +89,7 @@ let s:autosave_timeout = 0
let s:autosave_events = []
let s:autosave_all_buffers = 0
let s:autosave_location = ''
let s:enable_hop = 1
function! SpaceVim#layers#edit#health() abort
call SpaceVim#layers#edit#plugins()
@ -85,13 +109,17 @@ function! SpaceVim#layers#edit#plugins() abort
\ [g:_spacevim_root_dir . 'bundle/vim-table-mode'],
\ [g:_spacevim_root_dir . 'bundle/vim-textobj-entire'],
\ [g:_spacevim_root_dir . 'bundle/wildfire.vim',{'on_map' : '<Plug>(wildfire-'}],
\ [g:_spacevim_root_dir . 'bundle/vim-easymotion'],
\ [g:_spacevim_root_dir . 'bundle/vim-easyoperator-line'],
\ [g:_spacevim_root_dir . 'bundle/editorconfig-vim', { 'merged' : 0, 'if' : has('python') || has('python3')}],
\ [g:_spacevim_root_dir . 'bundle/vim-jplus', { 'on_map' : '<Plug>(jplus' }],
\ [g:_spacevim_root_dir . 'bundle/tabular', { 'merged' : 0}],
\ ['andrewradev/splitjoin.vim',{ 'on_cmd':['SplitjoinJoin', 'SplitjoinSplit'],'merged' : 0, 'loadconf' : 1}],
\ ]
if has('nvim-0.6.0')
call add(plugins,[g:_spacevim_root_dir . 'bundle/hop.nvim', { 'merged' : 0, 'loadconf' : 1}])
else
call add(plugins,[g:_spacevim_root_dir . 'bundle/vim-easymotion', { 'merged' : 0}])
call add(plugins,[g:_spacevim_root_dir . 'bundle/vim-easyoperator-line', { 'merged' : 0}])
endif
if executable('fcitx')
call add(plugins,[g:_spacevim_root_dir . 'bundle/fcitx.vim', { 'on_event' : 'InsertEnter'}])
endif
@ -111,6 +139,7 @@ function! SpaceVim#layers#edit#set_variable(var) abort
let s:autosave_events = get(a:var, 'autosave_events', s:autosave_events)
let s:autosave_all_buffers = get(a:var, 'autosave_all_buffers', s:autosave_all_buffers)
let s:autosave_location = get(a:var, 'autosave_location', s:autosave_location)
let s:enable_hop = get(a:var, 'enable_hop', s:enable_hop)
endfunction
function! SpaceVim#layers#edit#get_options() abort
@ -329,8 +358,68 @@ function! SpaceVim#layers#edit#config() abort
\ 'SplitjoinJoin', 'join into a single-line statement', 1)
call SpaceVim#mapping#space#def('nnoremap', ['j', 'm'],
\ 'SplitjoinSplit', 'split a one-liner into multiple lines', 1)
call SpaceVim#mapping#space#def('nnoremap', ['j', 'k'], 'j==', 'goto-next-line-and-indent', 0)
if has('nvim-0.6.0') && s:enable_hop
call SpaceVim#mapping#space#def('nmap', ['j', 'j'], 'HopChar1', 'jump-or-select-to-a-character', 1, 1)
call SpaceVim#mapping#space#def('nmap', ['j', 'J'], 'HopChar2', 'jump-to-suite-of-two-characters', 1, 1)
call SpaceVim#mapping#space#def('nmap', ['j', 'l'], 'HopLine', 'jump-or-select-to-a-line', 1, 1)
call SpaceVim#mapping#space#def('nmap', ['j', 'w'], 'HopWord', 'jump-to-a-word', 1, 1)
else
" call SpaceVim#mapping#space#def('nmap', ['j', 'j'], '<Plug>(easymotion-overwin-f)', 'jump to a character', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'j'], '<Plug>(better-easymotion-overwin-f)', 'jump-or-select-to-a-character', 0, 1)
nnoremap <silent> <Plug>(better-easymotion-overwin-f) :call <SID>better_easymotion_overwin_f(0)<Cr>
xnoremap <silent> <Plug>(better-easymotion-overwin-f) :<C-U>call <SID>better_easymotion_overwin_f(1)<Cr>
call SpaceVim#mapping#space#def('nmap', ['j', 'J'], '<Plug>(easymotion-overwin-f2)', 'jump-to-suite-of-two-characters', 0)
" call SpaceVim#mapping#space#def('nmap', ['j', 'l'], '<Plug>(easymotion-overwin-line)', 'jump to a line', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'l'], '<Plug>(better-easymotion-overwin-line)', 'jump-or-select-to-a-line', 0, 1)
nnoremap <silent> <Plug>(better-easymotion-overwin-line) :call <SID>better_easymotion_overwin_line(0)<Cr>
xnoremap <silent> <Plug>(better-easymotion-overwin-line) :<C-U>call <SID>better_easymotion_overwin_line(1)<Cr>
call SpaceVim#mapping#space#def('nmap', ['j', 'v'], '<Plug>(easymotion-overwin-line)', 'jump-to-a-line', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'w'], '<Plug>(easymotion-overwin-w)', 'jump-to-a-word', 0)
call SpaceVim#mapping#space#def('nmap', ['j', 'q'], '<Plug>(easymotion-overwin-line)', 'jump-to-a-line', 0)
endif
call SpaceVim#mapping#space#def('nnoremap', ['j', 'u'], 'call call('
\ . string(s:_function('s:jump_to_url')) . ', [])',
\ 'jump-to-url', 1)
endfunction
if has('nvim-0.6.0')
" Hop
lua << EOF
-- Like hop.jump_target.regex_by_line_start_skip_whitespace() except it also
-- marks empty or whitespace only lines
function regexLines()
return {
oneshot = true,
match = function(str)
return vim.regex("http[s]*://"):match_str(str)
end
}
end
-- Like :HopLineStart except it also jumps to empty or whitespace only lines
function hintLines(opts)
-- Taken from override_opts()
opts = setmetatable(opts or {}, {__index = require'hop'.opts})
local gen = require'hop.jump_target'.jump_targets_by_scanning_lines
require'hop'.hint_with(gen(regexLines()), opts)
end
EOF
" See `:h forced-motion` for these operator-pending mappings
function! s:jump_to_url() abort
lua hintLines()
endfunction
else
function! s:jump_to_url() abort
let g:EasyMotion_re_anywhere = 'http[s]*://'
call feedkeys("\<Plug>(easymotion-jumptoanywhere)")
endfunction
endif
function! s:transpose_with_previous(type) abort
let l:save_register = @"
if a:type ==# 'line'
@ -381,6 +470,51 @@ function! s:transpose_with_next(type) abort
let @" = l:save_register
endfunction
function! s:better_easymotion_overwin_line(is_visual) abort
let current_line = line('.')
try
if a:is_visual
call EasyMotion#Sol(0, 2)
else
call EasyMotion#overwin#line()
endif
" clear cmd line
noautocmd normal! :
if a:is_visual
let last_line = line('.')
exe current_line
if last_line > current_line
exe 'normal! V' . (last_line - current_line) . 'j'
else
exe 'normal! V' . (current_line - last_line) . 'k'
endif
endif
catch /^Vim\%((\a\+)\)\=:E117/
endtry
endfunction
function! s:better_easymotion_overwin_f(is_visual) abort
let [current_line, current_col] = getpos('.')[1:2]
try
call EasyMotion#OverwinF(1)
" clear cmd line
noautocmd normal! :
if a:is_visual
let last_line = line('.')
let [last_line, last_col] = getpos('.')[1:2]
call cursor(current_line, current_col)
if last_line > current_line
exe 'normal! v' . (last_line - current_line) . 'j0' . last_col . '|'
else
exe 'normal! v' . (current_line - last_line) . 'k0' . last_col . '|'
endif
endif
catch /^Vim\%((\a\+)\)\=:E117/
endtry
endfunction
function! s:move_text_down_transient_state() abort
if line('.') == line('$')
else

1
bundle/hop.nvim/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/doc/tags

View File

@ -0,0 +1,192 @@
# Contributing
This document is the official contribution guide contributors must follow. It will be **greatly appreciated** if you
read it first before contributing. It will also prevent you from losing your time if you open an issue / make a PR that
doesnt comply to this document.
<!-- vim-markdown-toc GFM -->
* [Disclaimer and why this document](#disclaimer-and-why-this-document)
* [How to make a change](#how-to-make-a-change)
* [Process](#process)
* [Conventions](#conventions)
* [Coding](#coding)
* [Git](#git)
* [Git message](#git-message)
* [Commit atomicity](#commit-atomicity)
* [Hygiene](#hygiene)
* [Release process](#release-process)
* [Overall process](#overall-process)
* [Changelogs update](#changelogs-update)
* [Git tag](#git-tag)
* [Support and donation](#support-and-donation)
<!-- vim-markdown-toc -->
# Disclaimer and why this document
People contributing is awesome. The more people contribute to Free & Open-Source software, the better the
world is to me. However, the more people contribute, the more work we have to do on our spare-time. Good
contributions are highly appreciated, especially if they thoroughly follow the conventions and guidelines of
each and every repository. However, bad contributions — that dont follow this document, for instance — are
going to require me more work than was involved into making the actual change. Its even worse when the contribution
actually solves a bug or add a new feature.
So please read this document; its not hard and the few rules here are easy to respect. You might already do
everything in this list anyway, but reading it wont hurt you. For more junior / less-experienced developers, its
very likely you will learn a bit of process that is considered good practice, especially when working with VCS like
Git.
> Thank you!
# How to make a change
## Process
The typical process is to base your work on the `master` branch. The `master` branch must always contain a stable
version of the project. It is possible to make changes by basing your work on other branches but the source
of truth is `master`. If you want to synchronize with other people on other branches, feel free to.
The process is:
1. (optional) Open an issue and discuss what you want to do. This is optional but highly recommended. If you
dont open an issue first and work on something that is not in the scope of the project, or already being
made by someone else, youll be working for nothing. Also, keep in mind that if your change doesnt refer to an
existing issue, I will be wondering what is the context of your change. So prepare to be asked about the motivation
and need behind your changes — its greatly appreciated if the commit messages, code and PRs content already
contains this information so that people dont have to ask.
2. Fork the project.
3. Create a branch starting from `master` or the branch you need to work on. Even though this is not really enforced,
youre advised to name your branch according to the _Git Flow_ naming convention:
- `fix/your-bug-here`: if youre fixing a bug, name your branch.
- `feature/new-feature-here`: if youre adding some work.
- Free for anything else.
- The special `release/*` branch is used to either back-port changes from newer versions to previous
versions, or to release new versions by updating files, changelogs, etc. Normally, contributors should
never have to worry about this kind of brach as their creations is often triggered when wanting to make a release.
4. Make some commits!
5. Once youre ready, open a Pull Request (PR) to merge your work on the target branch. For instance, open a PR for
`master <- feature/something-new`.
6. (optional) Ask someone to review your code in the UI. Normally, Im pretty reactive to notifications but it never
hurts to ask for a review.
7. Discussion and peer-review.
8. Once the CI is all green, someone (likely me [@phaazon]) will merge your code and close your PR.
9. Feel free to delete your branch.
# Conventions
## Coding
N/A
## Git
### Git message
Please format your git messages like so:
> Starting with an uppercase letter, ending with a dot. #343
>
> The #343 after the dot is appreciated to link to issues. Feel free to add, like this message, more context
> and/or precision to your git message. You dont have to put it in the first line of the commit message,
> but if you are fixing a bug or implementing a feature thas has an issue linked, please reference it, so
> that it is easier to generate changelogs when reading the git log.
**Im very strict on git messages as I use them to write `CHANGELOG.md` files. Dont be surprised if I ask you
to edit a commit message. :)**
### Commit atomicity
Your commits should be as atomic as possible. That means that if you make a change that touches two different
concepts / has different scopes, most of the time, you want two commits for instance one commit for the backend code
and one commit for the interface code. There are exceptions, so this is not an absolute rule, but take some time
thinking about whether you should split your commits or not. Commits which add a feature / fix a bug _and_ add tests at
the same time are fine.
However, heres a non-comprehensive list of commits considered bad and that will be refused:
- **Formatting, refactoring, cleaning, linting code in a PR that is not strictly about formatting**. If you open a PR to
fix a bug, implement a feature, change configuration, add metadata to the CI, etc. — pretty much anything — but you
also format some old code that has nothing to do with your PR, apply a linters suggestions (such as `clippy`), remove
old code, etc., then I will refuse your commit(s) and ask you to edit your PR.
- **Too atomic commits**. If two commits are logically connected to one another and are small, its likely that you want
to merge them as a single commit — unless they work on too different parts of your code. This is a bit subjective
topic, so I wont be _too picky_ about it, but if I judge that you should split a commit into two or fixup two commits,
please dont take it too personal. :)
If you dont know how to write your commits in an atomic maneer, think about how one would revert your commits if
something bad happens with your changes — like a big breaking change we need to roll back from very quickly. If your
commits are not atomic enough, rolling them back will also roll back code that has nothing to do with your changes.
### Hygiene
When working on a fix or a feature, its very likely that you will periodically need to update your branch
with the `master` branch. **Do not use merge commits**, as your contributions will be refused if you have
merge commits in them. The only case where merge commits are accepted is when you work with someone else
and are required to merge another branch into your feature branch (and even then, it is even advised to
simply rebase). If you want to synchronize your branch with `master`, please use:
```
git switch <your_branch>
git fetch origin --prune
git rebase origin/master
```
# Release process
## Overall process
Releases occur at arbitrary rates. If something is considered urgent, it is most of the time released immediately
after being merged and tested. Sometimes, several issues are being fixed at the same time (spanning on a few
days at max). Those will be gathered inside a single update.
Feature requests might be delayed a bit to be packed together as well but eventually get released, even if
theyre small. Getting your PR merged means it will be released _soon_, but depending on the urgency of your changes,
it might take a few minutes to a few days.
## Changelogs update
`CHANGELOG.md` files must be updated **before any release**. Especially, they must contain:
- The version of the release.
- The date of the release.
- How to migrate from a minor to the next major.
- Everything that a release has introduced, such as major, minor and patch changes.
Because I dont ask people to maintain changelogs, I have a high esteem of people knowing how to use Git and create
correct commits. Be advised that I will refuse any commit that prevents me from writing the changelog correctly.
## Git tag
Once a new release occurs, a Git tag is created. Git tags are formatted regarding the project they refer to, if several
projects are present in the repository. If only one project is present, tags will refer to this project by the same
naming scheme anyway:
> <project-name>-X.Y.Z
Where `X` is the _major version_, `Y` is the _minor version_ and `Z` is the _patch version_. For instance
`project-0.37.1` is a valid Git tag, so is `project-derive-0.5.3`.
A special kind of tag is also possible:
> <project-name>-X.Y.Z-rc.W
Where `W` is a number starting from `1` and incrementing. This format is for _release candidates_ and occurs
when a new version (most of the time a major one) is to be released but more feedback is required.
# Support and donation
This project is a _free and open-source_ project. It has no financial motivation nor support. I
([@phaazon]) would like to make it very clear that:
- Sponsorship is not available. You cannot pay me to make me do things for you. That includes issues reports,
features requests and such.
- If you still want to donate because you like the project and think I should be rewarded, you are free to
give whatever you want.
- However, keep in mind that donating doesnt unlock any privilege people who dont donate wouldnt already
have. This is very important as it would bias priorities. Donations must remain anonymous.
- For this reason, no _sponsor badge_ will be shown, as it would distinguish people who donate from those
who dont. This is a _free and open-source_ project, everybody is welcome to contribute, with or without
money.
[@phaazon]: https://github.com/phaazon

30
bundle/hop.nvim/LICENSE Normal file
View File

@ -0,0 +1,30 @@
Copyright (c) 2021, Dimitri Sabadie <dimitri.sabadie@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Dimitri Sabadie <dimitri.sabadie@gmail.com> nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

287
bundle/hop.nvim/README.md Normal file
View File

@ -0,0 +1,287 @@
__
/ /_ ____ ____
/ __ \/ __ \/ __ \
/ / / / /_/ / /_/ /
/_/ /_/\____/ .___/
/_/
· Neovim motions on speed! ·
[![](https://img.shields.io/badge/matrix-join%20the%20speed!-blueviolet)](https://matrix.to/#/#hop.nvim:matrix.org)
**Hop** is an [EasyMotion]-like plugin allowing you to jump anywhere in a
document with as few keystrokes as possible. It does so by annotating text in
your buffer with hints, short string sequences for which each character
represents a key to type to jump to the annotated text. Most of the time,
those sequences lengths will be between 1 to 3 characters, making every jump
target in your document reachable in a few keystrokes.
<!-- vim-markdown-toc GFM -->
* [Motivation](#motivation)
* [Features](#features)
* [Word mode (`:HopWord`)](#word-mode-hopword)
* [Line mode (`:HopLine`)](#line-mode-hopline)
* [1-char mode (`:HopChar1`)](#1-char-mode-hopchar1)
* [2-char mode (`:HopChar2`)](#2-char-mode-hopchar2)
* [Pattern mode (`:HopPattern`)](#pattern-mode-hoppattern)
* [Visual extend](#visual-extend)
* [Jump on sole occurrence](#jump-on-sole-occurrence)
* [Use as operator motion](#use-as-operator-motion)
* [Inclusive / exclusive motion](#inclusive--exclusive-motion)
* [Getting started](#getting-started)
* [Installation](#installation)
* [Important note about versioning](#important-note-about-versioning)
* [Using vim-plug](#using-vim-plug)
* [Using packer](#using-packer)
* [Nightly users](#nightly-users)
* [Usage](#usage)
* [Keybindings](#keybindings)
* [Configuration](#configuration)
* [Extension](#extension)
* [Chat](#chat)
<!-- vim-markdown-toc -->
# Motivation
**Hop** is a complete from-scratch rewrite of [EasyMotion], a famous plugin to
enhance the native motions of Vim. Even though [EasyMotion] is usable in
Neovim, it suffers from a few drawbacks making it not comfortable to use with
Neovim version >0.5 at least at the time of writing these lines:
- [EasyMotion] uses an old trick to annotate jump targets by saving the
contents of the buffer, replacing it with the highlighted annotations and
then restoring the initial buffer after jump. This trick is dangerous as it
will change the contents of your buffer. A UI plugin should never do anything
to existing buffers contents.
- Because the contents of buffers will temporarily change, other parts of the
editor and/or plugins relying on buffer change events will react and will go
mad. An example is the internal LSP client implementation of Neovim >0.5 or
its treesitter native implementation. For LSP, it means that the connected
LSP server will receive a buffer with the jump target annotations… not
ideal.
**Hop** is a modern take implementing this concept for the latest versions of
Neovim.
# Features
- [x] Go to any word in the current buffer.
- [x] Go to any character in the current buffer.
- [x] Go to any bigrams in the current buffer.
- [x] Use Hop cross windows with multi-windows support.
- [x] Make an arbitrary search akin to <kbd>/</kbd> and go to any occurrences.
- [x] Go to any line.
- [x] Visual extend mode, which allows you to extend a visual selection by hopping elsewhere in the document.
- [x] Use it with commands like `d`, `c`, `y` to delete/change/yank up to your new cursor position.
- [x] Support a wide variety of user configuration options, among the possibility to alter the behavior of commands
to hint only before or after the cursor, for the current line, change the dictionary keys to use for the labels,
jump on sole occurrence, etc.
- [x] Extensible: provide your own jump targets and create Hop extensions!
## Word mode (`:HopWord`)
This mode highlights all the recognized words in the visible part of the buffer and allows you to jump to any.
![](https://phaazon.net/media/uploads/hop_word_mode.gif)
## Line mode (`:HopLine`)
This mode highlights the beginnings of each line in the visible part of the buffer for quick line hopping.
![](https://phaazon.net/media/uploads/hop_line_mode.gif)
## 1-char mode (`:HopChar1`)
This mode expects the user to type a single character. That character will then be highlighted in the visible part of
the buffer, allowing to jump to any of its occurrence. This mode is especially useful to jump to operators, punctuations
or any symbols not recognized as parts of words.
![](https://phaazon.net/media/uploads/hop_char1_mode.gif)
## 2-char mode (`:HopChar2`)
A variant of the 1-char mode, this mode exacts the user to type two characters, representing a _bigram_ (they follow
each other, in order). The bigram occurrences in the visible part of the buffer will then be highlighted for you to jump
to any.
![](https://phaazon.net/media/uploads/hop_char2_mode.gif)
Note that its possible to _fallback to 1-char mode_ if you hit a special key as second key. This key can be controlled
via the user configuration. `:h hop-config-char2_fallback_key`.
## Pattern mode (`:HopPattern`)
Akin to `/`, this mode prompts you for a pattern (regex) to search. Occurrences will be highlighted, allowing you to
jump to any.
![](https://phaazon.net/media/uploads/hop_pattern_mode.gif)
## Visual extend
If you call any Hop commands / Lua functions from one of the visual modes, the visual selection will be extended.
![](https://phaazon.net/media/uploads/hop_visual_extend.gif)
## Jump on sole occurrence
If only a single occurrence is visible in the buffer, Hop will automatically jump to it without requiring pressing any
extra key.
![](https://phaazon.net/media/uploads/hop_sole_occurrence.gif)
## Use as operator motion
You can use Hop with any command that expects a motion, such as `d`, `y`, `c`, and it does what you would expect:
Delete/yank/change the document up to the new cursor position.
## Inclusive / exclusive motion
By default, Hop will operate in exclusive mode, which is similar to what you get with `t`: deleting from the cursor
position up to the next `)` (without deleting the `)`), which is normally done with `dt)`. However, if you want to be
inclusive (i.e. delete the `)`, which is `df)` in vanilla), you can set the `inclusive_jump` option to `true`.
Some limitations currently exist, requiring `virtualedit` special settings. `:h hop-config-inclusive_jump` for more
information.
# Getting started
This section will guide you through the list of steps you must take to be able to get started with **Hop**.
This plugin was written against Neovim 0.5, which is currently a nightly version. This plugin will not work:
- With a version of Neovim before 0.5.
- On Vim. **No support for Vim is planned.**
## Installation
Whatever solution / package manager you are using, you need to ensure that the `setup` Lua function is called at some
point, otherwise the plugin will not work. If your package manager doesnt support automatic calling of this function,
you can call it manually after your plugin is installed:
```lua
require'hop'.setup()
```
To get a default experience. Feel free to customize later the `setup` invocation (`:h hop.setup`). If you do, then you
will probably want to ensure the configuration is okay by running `:checkhealth`. Various checks will be performed by
Hop to ensure everything is all good.
### Important note about versioning
This plugin implements [SemVer] via git branches and tags. Versions are prefixed with a `v`, and only patch versions
are git tags. Major and minor versions are git branches. You are **very strongly advised** to use a major version
dependency to be sure your config will not break when Hop gets updated.
### Using vim-plug
```vim
Plug 'phaazon/hop.nvim'
```
### Using packer
```lua
use {
'phaazon/hop.nvim',
branch = 'v1', -- optional but strongly recommended
config = function()
-- you can configure Hop the way you like here; see :h hop-config
require'hop'.setup { keys = 'etovxqpdygfblzhckisuran' }
end
}
```
### Nightly users
Hop supports nightly releases of Neovim. However, keep in mind that if you are on a nightly version, you must be **on
the last one**. If you are not, then you are exposed to compatibility issues / breakage.
# Usage
A bunch of vim commands are available to get your fingers wrapped around **Hop** quickly:
- `:HopWord`: hop around by highlighting words.
- `:HopPattern`: hop around by matching against a pattern (as with `/`).
- `:HopChar1`: type a single key and hop to any occurrence of that key in the document.
- `:HopChar2`: type a bigram (two keys) and hop to any occurrence of that bigram in the document.
- `:HopLine`: jump to any visible line in your buffer.
- `:HopLineStart`: jump to any visible first non-whitespace character of each line in your buffer.
Most of these commands have variant to jump before / after the cursor, and on the current line. For instance,
`:HopChar1CurrentLineAC` is a form of `f` (Vim native motion) using Hop.
If you would rather use the Lua API, you can test it via the command prompt:
```vim
:lua require'hop'.hint_words()
```
For a more complete user guide and help pages:
```vim
:help hop
```
# Keybindings
Hop doesnt set any keybindings; you will have to define them by yourself.
If you want to create a key binding from within Lua:
```lua
-- place this in one of your configuration file(s)
vim.api.nvim_set_keymap('n', 'f', "<cmd>lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })<cr>", {})
vim.api.nvim_set_keymap('n', 'F', "<cmd>lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })<cr>", {})
vim.api.nvim_set_keymap('o', 'f', "<cmd>lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true, inclusive_jump = true })<cr>", {})
vim.api.nvim_set_keymap('o', 'F', "<cmd>lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true, inclusive_jump = true })<cr>", {})
vim.api.nvim_set_keymap('', 't', "<cmd>lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })<cr>", {})
vim.api.nvim_set_keymap('', 'T', "<cmd>lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })<cr>", {})
vim.api.nvim_set_keymap('n', '<leader>e', "<cmd> lua require'hop'.hint_words({ hint_position = require'hop.hint'.HintPosition.END })<cr>", {})
vim.api.nvim_set_keymap('v', '<leader>e', "<cmd> lua require'hop'.hint_words({ hint_position = require'hop.hint'.HintPosition.END })<cr>", {})
vim.api.nvim_set_keymap('o', '<leader>e', "<cmd> lua require'hop'.hint_words({ hint_position = require'hop.hint'.HintPosition.END, inclusive_jump = true })<cr>", {})
```
# Configuration
You can configure Hop via several different mechanisms:
- _Global configuration_ uses the Lua `setup` API (`:h hop.setup`). This allows you to setup global options that will be
used by all Hop Lua functions as well as the vim commands (e.g. `:HopWord`). This is the easiest way to configure Hop
on a global scale. You can do this in your `init.lua` or any `.vim` file by using the `lua` vim command.
Example:
```vim
" init.vim
"
" Use better keys for the bépo keyboard layout and set
" a balanced distribution of terminal / sequence keys
lua require'hop'.setup { keys = 'etovxqpdygfblzhckisuran', jump_on_sole_occurrence = false }
```
- _Local configuration overrides_ are available only on the Lua API and are `{opts}` Lua tables passed to the various
Lua functions. Those options have precedence over global options, so they allow to locally override options. Useful if
you want to test a special option for a single Lua function, such as `require'hop'.hint_lines()`. You can test them
inside the command line, such as:
```
:lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
```
- In the case of none of the above are provided, options are automatically read from the _default_ options. See `:h
hop-config` for a list of default values.
# Extension
It is possible to extend Hop by creating *Hop extension plugins*. For more info:
```vim
:h hop-extension
```
> Disclaimer: you may have written a nice Hop extension plugin. You can open an issue to merge it upstream but remember
> that its unlikely to be merged as Hop should remain small and straight-to-the point.
# Chat
Join the discussion on the official [Matrix room](https://matrix.to/#/#hop.nvim:matrix.org)!
[EasyMotion]: https://github.com/easymotion/vim-easymotion
[packer]: https://github.com/wbthomason/packer.nvim
[SemVer]: https://semver.org

887
bundle/hop.nvim/doc/hop.txt Normal file
View File

@ -0,0 +1,887 @@
*hop.txt* For Neovim version 0.5 Last change: 2021 Nov 02
__
/ /_ ____ ____
/ __ \/ __ \/ __ \
/ / / / /_/ / /_/ /
/_/ /_/\____/ .___/
/_/
· Neovim motions on speed! ·
==============================================================================
CONTENTS *hop-contents*
Introduction ·············································· |hop-introduction|
Requirements ·············································· |hop-requirements|
Usage ···························································· |hop-usage|
Commands ···················································· |hop-commands|
Lua API ······················································ |hop-lua-api|
Jump target API ······································ |hop-jump-target-api|
Configuration ··················································· |hop-config|
Extension ···················································· |hop-extension|
Highlights ·················································· |hop-highlights|
License ························································ |hop-license|
==============================================================================
INTRODUCTION *hop* *hop-introduction*
Hop is an “EasyMotion” like plugin allowing you to jump anywhere in a document
with as few keystrokes as possible. It does so by annotating text in your
buffer with |hints|, short string sequences for which each character represents
a key to type to jump to the annotated text. Most of the time, those
sequences lengths will be between 1 to 3 characters, making every jump target
in your document reachable in a few keystrokes.
Hop is a complete from-scratch rewrite of EasyMotion, a famous plugin to
enhance the native motions of Vim. Even though EasyMotion is usable in
Neovim, it suffers from a few drawbacks making it not comfortable to use with
Neovim version >0.5 at least at the time of writing these lines:
- EasyMotion uses an old trick to annotate jump targets by saving the
contents of the buffer, replacing it with the highlighted annotations and
then restoring the initial buffer after jump. This trick is dangerous as it
will change the contents of your buffer. A UI plugin should never do anything
to existing buffers contents.
- Because the contents of buffers will temporarily change, other parts of the
editor and/or plugins relying on buffer change events will react and will go
mad. An example is the internal LSP client implementation of Neovim >0.5 or
its treesitter native implementation. For LSP, it means that the connected
LSP server will receive a buffer with the jump target annotations… not
ideal.
==============================================================================
REQUIREMENTS *hop-requirements*
Hop works only with Neovim and was written with Neovim-0.5, so it is highly
recommended to use Neovim version 0.5+.
Especially, hop uses |api-extended-marks|, which are not available before
Neovim-0.5.
==============================================================================
USAGE *hop-usage*
Before doing anything else, you have to setup the plugin. If you are not using
a package manager or environment doing that automatically for you, you need to
call the |hop.setup| function to correctly initialize the plugin.
For a minimal setup:
For people using init.lua~
In your `init.lua`, add:
>
require'hop'.setup()
<
For people using init.vim~
In your `init.vim`, add:
>
lua << EOF
require'hop'.setup()
EOF
<
You can pass an optional argument to `setup(opts)` in order to pass {opts}.
Have a look at |hop.setup| for further details.
*hop-health*
Healthcheck~
Hop has support for |:checkhealth|. If you find yourself in a situation where
something looks odd or incorrect, do not forget to run this command before
opening an issue.
*hop-commands*
Commands~
You can try those commands by typing them in your command line. By default,
they will use the default options for the configuration of Hop. If you want to
customize how those commands work, have a look at |hop.setup|. Also, something
pretty important to know is that those are Vim commands. Hop tries to expose
as many features as possible via the Vim commands but ultimately, you will
have access to more features by using the Lua API directly. Have a look at
|hop-lua-api| for more documentation.
Some of the commands have a suffix, such as `BC`, `AC` and `MW`. Those are
variations of the commands without the suffix, applying to the visible part of
the buffer before and after the cursor, and multiple windows, respectively.
Another kind of suffix (that can be mixed with `BC` and `AC`) is `CurrentLine`. This
creates a variant of the command that will only run for the current line.
`:HopWord` *:HopWord*
`:HopWordBC` *:HopWordBC*
`:HopWordAC` *:HopWordAC*
`:HopWordCurrentLine` *:HopWordCurrentLine*
`:HopWordCurrentLineBC` *:HopWordCurrentLineBC*
`:HopWordCurrentLineAC` *:HopWordCurrentLineAC*
`:HopWordMW` *:HopWordMW*
Annotate all |word|s with key sequences. Typing a first key will visually
filter the sequences and reduce them. Continue typing key sequences until
you reduce a sequence completely, which will bring your cursor at that
position.
This is akin to calling the |hop.hint_words| Lua function.
`:HopPattern` *:HopPattern*
`:HopPatternBC` *:HopPatternBC*
`:HopPatternAC` *:HopPatternAC*
`:HopPatternCurrentLine` *:HopPatternCurrentLine*
`:HopPatternCurrentLineBC` *:HopPatternCurrentLineBC*
`:HopPatternCurrentLineAC` *:HopPatternCurrentLineAC*
`:HopPatternMW` *:HopPatternMW*
Ask the user for a pattern and hint the document with it.
This is akin to calling the |hop.hint_patterns| Lua function
with no explicit pattern.
`:HopChar1` *:HopChar1*
`:HopChar1BC` *:HopChar1BC*
`:HopChar1AC` *:HopChar1AC*
`:HopChar1CurrentLine` *:HopChar1CurrentLine*
`:HopChar1CurrentLineBC` *:HopChar1CurrentLineBC*
`:HopChar1CurrentLineAC` *:HopChar1CurrentLineAC*
`:HopChar1MW` *:HopChar1MW*
Type a key and immediately hint the document for this key.
This is akin to calling the |hop.hint_char1| Lua Function
`:HopChar2` *:HopChar2*
`:HopChar2BC` *:HopChar2BC*
`:HopChar2AC` *:HopChar2AC*
`:HopChar2CurrentLine` *:HopChar2CurrentLine*
`:HopChar2CurrentLineBC` *:HopChar2CurrentLineBC*
`:HopChar2CurrentLineAC` *:HopChar2CurrentLineAC*
`:HopChar2MW` *:HopChar2MW*
Type two keys and immediately hint the document for this bigram.
This is akin to calling the |hop.hint_char2| Lua Function
`:HopLine` *:HopLine*
`:HopLineBC` *:HopLineBC*
`:HopLineAC` *:HopLineAC*
`:HopLineCurrentLine` *:HopLineCurrentLine*
`:HopLineCurrentLineBC` *:HopLineCurrentLineBC*
`:HopLineCurrentLineAC` *:HopLineCurrentLineAC*
`:HopLineMW` *:HopLineMW*
Jump to the beginning of the line of your choice inside your buffer.
This is akin to calling the |hop.hint_lines| Lua function.
`:HopLineStart` *:HopLineStart*
`:HopLineStartBC` *:HopLineStartBC*
`:HopLineStartAC` *:HopLineStartAC*
`:HopLineStartCurrentLine` *:HopLineStartCurrentLine*
`:HopLineStartCurrentLineBC` *:HopLineStartCurrentLineBC*
`:HopLineStartCurrentLineAC` *:HopLineStartCurrentLineAC*
`:HopLineStartMW` *:HopLineStartMW*
Like `HopLine` but skips leading whitespace on every line. Blank lines are
skipped over.
This is akin to calling the |hop.hint_lines_skip_whitespace| Lua function.
`:HopAnywhere` *:HopAnywhere*
`:HopAnywhereBC` *:HopAnywhereBC*
`:HopAnywhereAC` *:HopAnywhereAC*
`:HopAnywhereCurrentLine` *:HopAnywhereCurrentLine*
`:HopAnywhereCurrentLineBC` *:HopAnywhereCurrentLineBC*
`:HopAnywhereCurrentLineAC` *:HopAnywhereCurrentLineAC*
`:HopAnywhereMW` *:HopAnywhereMW*
Annotate anywhere with key sequences.
This is akin to calling the |hop.hint_anywhere| Lua function.
*hop-lua-api*
Lua API~
The Lua API comprises several modules. Even though those modules might have
more public functions than described here, you are only supposed to use the
functions in this help page. Using one that is not listed here is considered
unstable.
`hop` Entry point and main interface. If you just want to use
Hop from within Lua via keybindings, you shouldnt need to
read any other modules.
`hop.defaults` Default options.
`hop.hint` Various functions to create, update and reduce hints.
`hop.highlight` Highlight functions (creation / autocommands / etc.).
`hop.jump_target` Core module used to create jump targets.
`hop.perm` Permutation functions. Permutations are used as labels for
the hints.
Main API~
Most of the functions and values you need to know about are in `hop`.
`hop.setup(`{opts}`)` *hop.setup*
Setup the library with options.
This function will setup the Lua API and commands in a way that respects
the options you pass. It is mandatory to call that function at some time
if you want to be able to use Hop.
Note:
Some plugins will automatically call the `setup` public function of a
plugin if declared, which is the case with Hop. With such plugins, you
shouldnt have to care too much about `setup` but focus more on the
{opts} you can pass it.
Arguments:~
{opts} List of options. See the |hop-config| section.
`hop.hint_with(`{jump_target_gtr}`,` {opts}`)` *hop.hint_with*
Main entry-point of Hop, this function expects a jump target generator and
will call it with {opts} to get a list of jump targets. This function will
take care of reducing the hints for you automatically and will perform the
actual jumps.
If you would like to use a more general version to implement different
kind of actions instead of jumping, have a look at
|hop.hint_with_callback|.
Arguments:~
{jump_target_gtr} Jump target generator. See |hop-jump-target-api| for
further information.
{opts} User options.
`hop.hint_with_callback(` *hop.hint_with_callback*
{jump_target_gtr}`,`
{opts}`,`
{callback}
`)`
Main entry-point of Hop, this function expects a jump target generator and
will call it with {opts} to get a list of jump targets. This function will
take care of reducing the hints for you automatically. Once a jump target
is reduced completely, this function will call the {callback} with the
selected jump target.
Arguments:~
{jump_target_gtr} Jump target generator. See |hop-jump-target-api| for
further information.
{opts} User options.
`hop.hint_words(`{opts}`)` *hop.hint_words*
Annotate all words in the current window with key sequences. Typing a
first key will visually filter the sequences and reduce them. Continue
typing key sequences until you reduce a sequence completely, which will
bring your cursor at that position. See |hop-config| for a complete list
of the options you can pass as arguments.
Arguments:~
{opts} Hop options.
`hop.hint_patterns(`{opts}`,` {pattern}`)` *hop.hint_patterns*
Annotate all matched patterns in the current window with key sequences.
Arguments:~
{opts} Hop options.
{pattern} (optional) The pattern to search for.
If not set, the user is prompted for the pattern to search.
`hop.hint_char1(`{opts}`)` *hop.hint_char1*
Let the user type a key and immediately hint all of its occurrences.
Arguments:~
{opts} Hop options.
`hop.hint_char2(`{opts}`)` *hop.hint_char2*
Let the user type a bigram (two concatenated keys) and immediately hint
all of its occurrences.
This function can behave like |hop.hint_char1| in some cases. See
|hop-config-char2_fallback_key|.
Arguments:~
{opts} Hop options.
`hop.hint_lines(`{opts}`)` *hop.hint_lines*
Hint the beginning of each lines currently visible in the buffer view and
allow to jump to them.
This works with empty lines as well.
Arguments:~
{opts} Hop options.
`hop.hint_lines_skip_whitespace(`{opts}`)` *hop.hint_lines_skip_whitespace*
Hint the first non-whitespace character of each lines currently visible in
the buffer view and allow to jump to them.
This works with empty lines as well.
Arguments:~
{opts} Hop options.
`hop.hint_anywhere(`{opts}`)` *hop.hint_anywhere*
Annotate anywhere in the current window with key sequences.
Arguments:~
{opts} Hop options.
Hint API~
The hint API provide the `HintDirection` and `HintPosition`
`hop.hint.HintDirection` *hop.hint.HintDirection*
Enumeration for hinting direction.
Use this table as a value for |hop-config-direction|. Setting it to {nil}
makes the command / function act on the whole visible part of the buffer.
Enumeration variants:~
{BEFORE_CURSOR} Create and apply hints before the cursor.
{AFTER_CURSOR} Create and apply hints after the cursor.
`hop.hint.HintPosition` *hop.hint.HintPosition*
Enumeration for hinting position in match.
Use this table as a value for |hop-config-hint_posititon|.
Enumeration variants:~
{BEGIN} Create and apply hints at the beginning of the match.
{MIDDLE} Create and apply hints at the middle of the match.
{END} Create and apply hints at the end of the match.
*hop-jump-target-api*
Jump target API~
The jump target API is probably the most core API of all. It provides some
helper functions to help you extend Hop by providing your own jump targets.
Most importantly, you will want to read this documentation section to know
exactly which kind of format Hop expects for the jump targets when
implementing your own.
Jump targets are locations in buffers where users might jump to. They are
wrapped in a table and provide the required information so that Hop can
associate labels and display the hints. Such a table must have the following
form:
>
{
jump_targets = {},
indirect_jump_targets = {},
}
>
The `jump_targets` field is a list-table of jump targets. A single jump target
is simply a location in a given window. Actually, the location will be set on
the buffer that is being displayed by that window. So you can picture a jump
target as a triple (line, column, window).
>
{
line = 0,
column = 0,
window = 0,
}
<
`indirect_jump_targets` is an optional yet highly recommended table. They
provide an indirect access to `jump_targets` to re-order them. They are for
instance used to be able to distribute hints according to their distance to
the current location of the cursor. Not providing that table (`nil`) will
result in the jump targets being considered fully ordered and will be assigned
sequences based on their index in `jump_targets`.
Indirect jump targets are encoded as a flat list-table of pairs
(index, score). This table allows to quickly score and sort jump targets.
>
{
index = 0,
score = 0,
}
<
The `index` field gives the index in the `jump_targets` list. The `score` is any
number. The rule is that the lower the score is, the less prioritized the
jump target will be.
So for instance, for two jump targets, a jump target generator must return
such a table:
>
{
jump_targets = {
{ line = 1, column = 14, window = 0 },
{ line = 2, column = 1, window = 0 },
},
indirect_jump_targets = {
{ index = 0, score = 14 },
{ index = 1, score = 7 },
},
}
<
If you dont need to change the score, or if the scores are always ascending
with the indices ascending, you can completely omit the
`indirect_jump_targets` table:
>
{
jump_targets = {
{ line = 1, column = 14, window = 0 },
{ line = 2, column = 1, window = 0 },
},
}
<
This module provides several functions, named `jump_targets_*`, which are
called jump target generators. Such functions are to be passed to
|hop.hint_with| or |hop.hint_with_callback|. Most of the Vim commands are already
doing that for you.
`hop.jump_target.jump_targets_by_scanning_line(` *hop.jump_target.jump_targets_by_scanning_line*
{regex}
`)`
Jump target generator that scans lines of the currently visible buffer and
create the jump targets by using the input {regex}.
Arguments:~
{regex} Regex-like table to apply to each line. See the various
`hop.jump_target.regex*` functions below for a list of available
options.
If you want to create your own regex-like table, you need to
provide a table with two fields: `oneshot`, a boolean value,
that is to be set to `true` if the regex should be applied
only once to each line, and `match`, which is the actual
matcher that returns the beginning / end
(inclusive / exclusive) byte indices of the match. You are
advised to use |vim.regex|.
`hop.jump_target.jump_targets_for_current_line(` *hop.jump_target.jump_targets_for_current_line*
{regex}
`)`
Jump target generator that applies {regex} only to the current line of the
currently active window.
Arguments:~
{regex} Regex-like table to apply to each line. See the various
`hop.jump_target.regex*` functions below for a list of available
options.
If you want to create your own regex-like table, you need to
provide a table with two fields: `oneshot`, a boolean value,
that is to be set to `true` if the regex should be applied to
each line only once, and `match`, which is the actual matcher
that returns the beginning / end (inclusive / exclusive) byte
indices of the match. You are advised to use |vim.regex|.
`hop.jump_target.sort_indirect_jump_targets(` *hop.jump_target.sort_indirect_jump_targets*
{indirect_jump_targets}`,`
{opts}
`)`
Sort {indirect_jump_targets} according to their scores.
Arguments:~
{indirect_jump_targets} Indirect jump targets to sort.
{opts} User options.
`hop.jump_target.regex_by_searching(` *hop.jump_target.regex_by_searching*
{pat}`,`
{plain_search}
`)`
Buffer-line based |pattern| hint mode. This mode will highlight the
beginnings of a pattern you will be prompted for in the document and
will make the cursor jump to the one fully reduced.
Arguments:~
{pat} Pattern to search.
{plain_search} Should the pattern by plain-text.
`hop.jump_target.regex_by_case_searching(` *hop.jump_target.regex_by_case_searching*
{pat}`,`
{plain_search}`,`
{opts}
`)`
Similar to |hop.jump_target.regex_by_searching|, but respects the user case
sensitivity set by 'smartcase' and |hop-config-case_insensitive|.
Arguments:~
{pat} Pattern to search.
{plain_search} Should the pattern by plain-text.
{opts} User options.
`hop.jump_target.regex_by_word_start()` *hop.jump_target.regex_by_word_start*
Buffer-line based |word| hint mode. This mode will highlight the beginnings
of all the words in the window and will make the cursor jump to the one
fully reduced.
`hop.jump_target.regex_by_line_start()` *hop.jump_target.regex_by_line_start*
Highlight the beginning of each line in the current window.
`hop.jump_target.regex_by_line_start_skip_whitespace()` *hop.jump_target.regex_by_line_start_skip_whitespace*
Highlight the non-whitespace character of each line in the current window.
`hop.jump_target.regex_by_anywhere()` *hop.jump_target.regex_by_anywhere*
Highlight the anywhere in the current window.
`hop.jump_target.manh_dist(` *hop.jump_target.manh_dist*
{a}`,`
{b}`,`
{x_bias}
`)`
Manhattan distance between two buffer positions. Both {a} and {b} must be
tables containing two values: the first one for the line and the second one
for the column.
{x_bias} is to be used to skew the Manhattan distance in terms of lines.
Arguments:~
{a} First position.
{b} Second position.
{x_bias} Optional bias applied to the line distance. Defaults to 10.
Highlight API~
The highlight API gives two functions to manipulate the highlights Hop uses:
`hop.highlight.insert_highlights()` *hop.highlight.insert_highlights*
Manually insert the highlights by calling the `highlight default` command.
See |hop-highlights| for further details about the list of highlights
currently available.
`hop.highlight.create_autocmd` *hop.highlight.create_autocmd*
Register autocommands for the `ColorScheme` event, calling
|hop.highlight.insert_highlights|. This is called when you require Hop and
you shouldnt need to call this function by yourself.
Permutation API~
The permutation API is the core part of the algorithm used in Hop.
Permutations in Hop are made out of a source sequence of characters, called
a key set and represented with {keys}. A good choice of key set is important
to yield short and concise permutations, allowing to jump to targets with
fewer keystrokes.
`hop.perm.permutations(`{keys}`,` {n}`,` {opts}`)` *hop.perm.permutations*
Get the first {n} permutations out of {keys}.
Arguments:~
{keys} Input key set to use to create permutations.
{n} Number of permutations to generate.
{opts} Hop options.
Return:~
The {n} first permutations for the given {keys} and
|hop-config-perm_method| set in {opts}.
==============================================================================
CONFIGURATION *hop-config*
The configuration can be provided in two different ways:
- Either by explicitly passing an {opts} table to the various Lua
functions.
- Or by passing an {opts} table to the Lua |hop.setup| function.
Not providing any of the two above will use the default values, as described
below.
`keys` *hop-config-keys*
A string representing all the keys that can be part of a permutation.
Every character (key) used in the string will be used as part of a
permutation. The shortest permutation is a permutation of a single
character, and, depending on the content of your buffer, you might end up
with 3-character (or more) permutations in worst situations.
However, it is important to notice that if you decide to provide `keys`,
you have to ensure to use enough characters in the string, otherwise you
might get very long sequences and a not so pleasant experience.
Note:
This is only for the old permutation algorithm only, which is to get
deprecated soon.
Another important aspect is that the order of the characters you put
in your `keys` is important. Depending on the value you use in
|hop-config-term_seq_bias|, the keys will be split in two and the first
part of it will be used for terminal keys and the second part for
sequence keys.
Defaults:~
`keys = 'asdghklqwertyuiopzxcvbnmfj'`
`quit_key` *hop-config-quit_key*
A string representing a key that will quit Hop mode without also feeding
that key into Neovim to be treated as a normal key press.
It is possible to quit hopping by pressing any key that is not present in
|hop-config-keys|; however, when you do this, the key normal function is
also performed. For example if, hopping in |visual-mode|, pressing <Esc>
will quit hopping and also exit |visual-mode|.
If the user presses `quit_key`, Hop will be quit without the key normal
function being performed. For example if hopping in |visual-mode| with
`quit_key` set to '<Esc>', pressing <Esc> will quit hopping without
quitting |visual-mode|.
If you don't want to use a `quit_key`, set `quit_key` to an empty string.
Note:~
`quit_key` should only contain a single key or be an empty string.
Note:~
`quit_key` should not contain a key that is also present in
|hop-config-keys|.
Defaults:~
`quit_key = '<Esc>'`
`perm_method` *hop-config-perm_method*
Permutation method to use.
Permutation methods allow to change the way permutations (i.e. hints
sequence labels) are generated internally. There are currently two
possible options:
Possible algorithms~
`TermSeqBias`
This algorithm splits your input key set (|hop-config-keys|) into
two separate sets, a terminal key set and a sequence key set.
Terminal keys will always be at the end of a sequence and then
typing them will always jump somewhere in your buffer, while
sequence keys will always be part of a prefix sequence before a
terminal key.
Additionally to |hop-config-keys|, this algorithm uses the
|hop-config-term_seq_bias| option to determine how to split
|hop-config-keys|. A good default value is `0.5` or `3 / 4` but
feel free to experiment with values between (exclusive) `0` and
`1`.
Note:
This algorithm will be deprecated very soon. You are strongly
advised to switch to `TrieBacktrackFilling` if you havent
yet.
`TrieBacktrackFilling`
Permutation algorithm based on tries and backtrack filling.
This algorithm uses the full potential of |hop-config-keys| by
using them all to saturate a trie, representing all the
permutations. Once a layer is saturated, this algorithm will
backtrack (from the end of the trie, deepest first) and create a
new layer in the trie, ensuring that the first permutations will
be shorter than the last ones.
Because of the last, deepest trie insertion mechanism and trie
saturation, this algorithm yields a much better distribution
accross your buffer, and you should get 1-sequences and
2-sequences most of the time. Each dimension grows
exponentially, so you get `keys_length²` 2-sequence keys,
`keys_length³` 3-sequence keys, etc in the worst cases.
Default value~
`perm_method = require'hop.perm'.TrieBacktrackFilling`
`reverse_distribution` *hop-config-reverse_distribution*
The default behavior for key sequence distribution in your buffer is to
concentrate shorter sequences near the cursor, grouping 1-character
sequences around. As hints get further from the cursor, the dimension of
the sequences will grow, making the furthest sequences the longest ones
to type.
Set this option to `true` to reverse the density and concentrate the
shortest sequences (1-character) around the furthest words and the longest
sequences around the cursor.
Defaults:~
`reverse_distribution = false`
`teasing` *hop-config-teasing*
Boolean value stating whether Hop should tease you when you do something
you are not supposed to.
If you find this setting annoying, feel free to turn it to `false`.
Defaults:~
`teasing = true`
`jump_on_sole_occurrence` *hop-config-jump_on_sole_occurrence*
Immediately jump without displaying hints if only one occurrence exists.
Defaults:~
`jump_on_sole_occurrence = true`
`case_insensitive` *hop-config-case_insensitive*
Use case-insensitive matching by default for commands requiring user
input.
Defaults:~
`case_insensitive = true`
`create_hl_autocmd` *hop-config-create_hl_autocmd*
Create and set highlight autocommands to automatically apply highlights.
You will want this if you use a theme that clears all highlights before
applying its colorscheme.
Defaults:~
`create_hl_autocmd = true`
`direction` *hop-config-direction*
Direction in which to hint. See |hop.hint.HintDirection| for further
details.
Setting this in the user configuration will make all commands default to
that direction, unless overriden.
Defaults:~
`direction = nil`
`hint_position` *hop-config-hint_position*
Position of hint in match. See |hop.hint.HintPosition| for further
details.
Defaults:~
`hint_position = require'hop.hint'.HintPosition.BEGIN`
`current_line_only` *hop-config-current_line_only*
Apply Hop commands only to the current line.
Note:
Trying to use this option along with |hop-config-multi_windows| is
unsound.
Defaults:~
`current_line_only = false`
`inclusive_jump` *hop-config-inclusive_jump*
Make all motions inclusive; i.e. jumping to a jump target will actually
jump one display cell right to the jump target. Set this option to `true`
if you would like to have the same behavior as with the |f| motion. Set it
to `false` if you would like to have the same behavior as with the |t|
motion.
There is one important limitation if you use `inclusive_jump = true`: if
the jump target you would like to jump to is the last character on a
line, it will not do what you expect; for instance, deleting or yanking
with `d` / `y` will not include the last character by default, unless you
set |virtualedit| to `onemore`.
Defaults:~
`inclusive_jump = false`
`uppercase_labels` *hop-config-uppercase_labels*
Display labels as uppercase. This option only affects the displayed
labels; you still select them by typing the keys on your keyboard.
Defaults:~
`uppercase_labels = false`
`char2_fallback_key` *hop-config-char2_fallback_key*
Enable a special key that breaks |hop.hint_char2| if only one key is
pressed, falling back to the same behavior as |hop.hint_char1|. It is
recommended that, if you set this option, you not use a key that is in
your |hop-config-keys| (because it would make those keys unreachable), and
not a character that you might jump to. A good fallback key could be
`<esc>` or `<cr>`, for instance.
Defaults:~
`char2_fallback_key = nil`
`extensions` *hop-config-extensions*
List-table of extensions to enable (names). As described in |hop-extension|,
extensions for which the name in that list must have a `register(opts)`
function in their public API for Hop to correctly initialized them.
Defaults:~
`extensions = nil`
`multi_windows` *hop-config-multi_windows*
Enable cross-windows support and hint all the currently visible windows.
This behavior allows you to jump around any position in any buffer
currently visible in a window. Although a powerful a feature, remember
that enabling this will also generate many more sequence combinations, so
you could get deeper sequences to type (most of the time it should be good
if you have enough keys in |hop-config-keys|).
Defaults:~
`multi_windows = false`
==============================================================================
EXTENSION *hop-extension*
Hop supports extensions, that can provide ad hoc jump targets that are not
part of the core of Hop. If you want to write a Hop extension, this section
should provide all the required informations.
The first important part is to know how to generate jump targets. You will
then first want to read the |hop-jump-target-api|, that describes this process
in terms of Lua protocol.
The second part is that because you will depend on the Hop API, you need to be
sure that Hop is completely setup before initiating your Hop extension plugin.
In order to do so, your users will have to read the user guide of the package
manager they are using to mark Hop as a dependency of your plugin when
installing. You should ensure such instructions are easily accessible to them,
for instance in your help pages and README.
Finally, Hop extension plugins do not get initialized via the regular `setup`
function — well, its up to you to still provide that function, but keep in
mind that Hop should not be assumed ready if you do so, so really keep setting
up things unrelated to Hop in your `setup` function. In order to setup a Hop
extension plugin, your plugin must provide the `register(opts)` function, that
takes the same {opts} arguments as `setup(opts)`. This function will be called
by Hop automatically.
Once a user has installed both Hop and the extension plugin, they can simply
modify the configuration of Hop and pass the name of your plugin in the
`extensions` user configuration. See |hop-config-extensions| for further
details about that.
==============================================================================
HIGHLIGHTS *hop-highlights*
Anywhere in the hint buffer that doesnt contain a hint, the |hl-EndOfBuffer|
highlight is used. For the rest:
`HopNextKey` *hop-hl-HopNextkey*
Highlight used for the mono-sequence keys (i.e. sequence of 1).
`HopNextKey1` *hop-hl-HopNextKey1*
Highlight used for the first key in a sequence.
`HopNextKey2` *hop-hl-HopNextKey2*
Highlight used for the second and remaining keys in a sequence.
`HopUnmatched` *hop-hl-HopUnmatched*
Highlight used for unmatched part of the buffer when running a Hop command
/ Lua functions.
`HopCursor` *hop-hl-HopCursor*
Highlight used for the fake cursor visible when running a Hop command /
Lua functions.
Highlights are inserted in an augroup, `HopInitHighlight`, and an autocommand
is automatically set when initializing the plugin, unless you set
|hop-config-create_hl_autocmd| to `false`.
==============================================================================
LICENSE *hop-license*
Copyright (c) 2021-2022, Dimitri Sabadie <dimitri.sabadie@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Dimitri Sabadie <dimitri.sabadie@gmail.com> nor the
names of other contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
==============================================================================
vim:tw=78:sw=4:ts=8:ft=help:norl:

View File

@ -0,0 +1,32 @@
local M = {}
M.opts = {}
function M.hint_around_cursor(opts)
-- the jump target generator; we are simply going to retreive the cursor position and hint around it as an example
local jump_targets = function() -- opts ignored
local cursor_pos = require'hop.window'.get_window_context().cursor_pos
local line = cursor_pos[1] - 1
local col = cursor_pos[2] + 1
local jump_targets = {}
-- left
if col > 0 then
jump_targets[#jump_targets + 1] = { line = line, column = col - 1, window = 0 }
end
-- right
jump_targets[#jump_targets + 1] = { line = line, column = col + 1, window = 0 }
return { jump_targets = jump_targets }
end
require'hop'.hint_with(jump_targets, opts)
end
function M.register(opts)
vim.notify('registering the nice extension', 0)
M.opts = opts
end
return M

View File

@ -0,0 +1,17 @@
local M = {}
M.keys = 'asdghklqwertyuiopzxcvbnmfj'
M.quit_key = '<Esc>'
M.perm_method = require'hop.perm'.TrieBacktrackFilling
M.reverse_distribution = false
M.teasing = true
M.jump_on_sole_occurrence = true
M.case_insensitive = true
M.create_hl_autocmd = true
M.current_line_only = false
M.inclusive_jump = false
M.uppercase_labels = false
M.multi_windows = false
M.hint_position = require'hop.hint'.HintPosition.BEGIN
return M

View File

@ -0,0 +1,36 @@
local M = {}
local hop = require'hop'
-- Initialization check.
--
-- This function will perform checks at initialization to ensure everything will work as expected.
function M.check()
local health = require'health'
health.report_start('Ensuring keys are unique')
local existing_keys = {}
local had_errors = false
for i = 0, #hop.opts.keys do
local key = hop.opts.keys:sub(i, i)
if existing_keys[key] then
health.report_error(string.format('key %s appears more than once in opts.keys', key))
had_errors = true
else
existing_keys[key] = true
end
end
if not had_errors then
health.report_ok('Keys are unique')
end
health.report_start('Checking for deprecated features')
had_errors = false
if not had_errors then
health.report_ok('All good')
end
end
return M

View File

@ -0,0 +1,31 @@
-- This module contains everything for highlighting Hop.
local M = {}
-- Insert the highlights that Hop uses.
function M.insert_highlights()
-- Highlight used for the mono-sequence keys (i.e. sequence of 1).
vim.api.nvim_command('highlight default HopNextKey guifg=#ff007c gui=bold ctermfg=198 cterm=bold')
-- Highlight used for the first key in a sequence.
vim.api.nvim_command('highlight default HopNextKey1 guifg=#00dfff gui=bold ctermfg=45 cterm=bold')
-- Highlight used for the second and remaining keys in a sequence.
vim.api.nvim_command('highlight default HopNextKey2 guifg=#2b8db3 ctermfg=33')
-- Highlight used for the unmatched part of the buffer.
-- ctermbg=bg is omitted because it errors if Normal does not have ctermbg set
-- Luckily guibg=bg does not seem to error even if Normal does not have guibg set so it can be used
vim.api.nvim_command('highlight default HopUnmatched guifg=#666666 guibg=bg guisp=#666666 ctermfg=242')
-- Highlight used for the fake cursor visible when hopping.
vim.api.nvim_command('highlight default link HopCursor Cursor')
end
function M.create_autocmd()
vim.api.nvim_command('augroup HopInitHighlight')
vim.api.nvim_command('autocmd!')
vim.api.nvim_command("autocmd ColorScheme * lua require'hop.highlight'.insert_highlights()")
vim.api.nvim_command('augroup end')
end
return M

View File

@ -0,0 +1,122 @@
local perm = require'hop.perm'
local prio = require'hop.priority'
local M = {}
M.HintDirection = {
BEFORE_CURSOR = 1,
AFTER_CURSOR = 2,
}
M.HintPosition = {
BEGIN = 1,
MIDDLE = 2,
END = 3,
}
local function tbl_to_str(label)
local s = ''
for i = 1, #label do
s = s .. label[i]
end
return s
end
-- Reduce a hint.
--
-- This function will remove hints not starting with the input key and will reduce the other ones
-- with one level.
local function reduce_label(label, key)
local snd_idx = vim.fn.byteidx(label, 1)
if label:sub(1, snd_idx) == key then
label = label:sub(snd_idx + 1)
end
if label == '' then
label = nil
end
return label
end
-- Reduce all hints and return the one fully reduced, if any.
function M.reduce_hints(hints, key)
local next_hints = {}
for _, h in pairs(hints) do
local prev_label = h.label
h.label = reduce_label(h.label, key)
if h.label == nil then
return h
elseif h.label ~= prev_label then
next_hints[#next_hints + 1] = h
end
end
return nil, next_hints
end
-- Create hints from jump targets.
--
-- This function associates jump targets with permutations, creating hints. A hint is then a jump target along with a
-- label.
--
-- If `indirect_jump_targets` is `nil`, `jump_targets` is assumed already ordered with all jump target with the same
-- score (0)
function M.create_hints(jump_targets, indirect_jump_targets, opts)
local hints = {}
local perms = perm.permutations(opts.keys, #jump_targets, opts)
-- get or generate indirect_jump_targets
if indirect_jump_targets == nil then
indirect_jump_targets = {}
for i = 1, #jump_targets do
indirect_jump_targets[i] = { index = i, score = 0 }
end
end
for i, indirect in pairs(indirect_jump_targets) do
hints[indirect.index] = {
label = tbl_to_str(perms[i]),
jump_target = jump_targets[indirect.index]
}
end
return hints
end
-- Create the extmarks for per-line hints.
--
-- Passing `opts.uppercase_labels = true` will display the hint as uppercase.
function M.set_hint_extmarks(hl_ns, hints, opts)
for _, hint in pairs(hints) do
local label = hint.label
if opts.uppercase_labels then
label = label:upper()
end
if vim.fn.strdisplaywidth(label) == 1 then
vim.api.nvim_buf_set_extmark(hint.jump_target.buffer or 0, hl_ns, hint.jump_target.line, hint.jump_target.column - 1, {
virt_text = { { label, "HopNextKey" } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
priority = prio.HINT_PRIO
})
else
-- get the byte index of the second hint so that we can slice it correctly
local snd_idx = vim.fn.byteidx(label, 1)
vim.api.nvim_buf_set_extmark(hint.jump_target.buffer or 0, hl_ns, hint.jump_target.line, hint.jump_target.column - 1, { -- HERE
virt_text = { { label:sub(1, snd_idx), "HopNextKey1" }, { label:sub(snd_idx + 1), "HopNextKey2" } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
priority = prio.HINT_PRIO
})
end
end
end
return M

View File

@ -0,0 +1,476 @@
local defaults = require'hop.defaults'
local hint = require'hop.hint'
local jump_target = require'hop.jump_target'
local prio = require'hop.priority'
local window = require'hop.window'
local M = {}
-- Ensure options are sound.
--
-- Some options cannot be used together. For instance, multi_windows and current_line_only dont really make sense used
-- together. This function will notify the user of such ill-formed configurations.
local function check_opts(opts)
if not opts then
return
end
if opts.multi_windows and opts.current_line_only then
vim.notify('Cannot use current_line_only across multiple windows', 3)
end
end
-- Allows to override global options with user local overrides.
local function override_opts(opts)
check_opts(opts)
return setmetatable(opts or {}, {__index = M.opts})
end
-- Display error messages.
local function eprintln(msg, teasing)
if teasing then
vim.api.nvim_echo({{msg, 'Error'}}, true, {})
end
end
-- A hack to prevent #57 by deleting twice the namespace (its super weird).
local function clear_namespace(buf_handle, hl_ns)
vim.api.nvim_buf_clear_namespace(buf_handle, hl_ns, 0, -1)
vim.api.nvim_buf_clear_namespace(buf_handle, hl_ns, 0, -1)
end
-- Dim everything out to prepare the Hop session.
--
-- - hl_ns is the highlight namespace.
-- - top_line is the top line in the buffer to start highlighting at
-- - bottom_line is the bottom line in the buffer to stop highlighting at
local function apply_dimming(buf_handle, hl_ns, top_line, bottom_line, cursor_pos, direction, current_line_only)
local start_line = top_line
local end_line = bottom_line
local start_col = 0
local end_col = nil
if direction == hint.HintDirection.AFTER_CURSOR then
start_col = cursor_pos[2]
elseif direction == hint.HintDirection.BEFORE_CURSOR then
if cursor_pos[2] ~= 0 then
end_col = cursor_pos[2] + 1
end
end
if current_line_only then
if direction == hint.HintDirection.BEFORE_CURSOR then
start_line = cursor_pos[1] - 1
end_line = cursor_pos[1] - 1
else
start_line = cursor_pos[1] - 1
end_line = cursor_pos[1]
end
end
vim.api.nvim_buf_set_extmark(buf_handle, hl_ns, start_line, start_col, {
end_line = end_line,
end_col = end_col,
hl_group = 'HopUnmatched',
hl_eol = true,
priority = prio.DIM_PRIO
})
end
-- Add the virtual cursor, taking care to handle the cases where:
-- - the virtualedit option is being used and the cursor is in a
-- tab character or past the end of the line
-- - the current line is empty
-- - there are multibyte characters on the line
local function add_virt_cur(ns)
local cur_info = vim.fn.getcurpos()
local cur_row = cur_info[2] - 1
local cur_col = cur_info[3] - 1 -- this gives cursor column location, in bytes
local cur_offset = cur_info[4]
local virt_col = cur_info[5] - 1
local cur_line = vim.api.nvim_get_current_line()
-- first check to see if cursor is in a tab char or past end of line
if cur_offset ~= 0 then
vim.api.nvim_buf_set_extmark(0, ns, cur_row, cur_col, {
virt_text = {{'', 'Normal'}},
virt_text_win_col = virt_col,
priority = prio.CURSOR_PRIO
})
-- otherwise check to see if cursor is at end of line or on empty line
elseif #cur_line == cur_col then
vim.api.nvim_buf_set_extmark(0, ns, cur_row, cur_col, {
virt_text = {{'', 'Normal'}},
virt_text_pos = 'overlay',
priority = prio.CURSOR_PRIO
})
else
vim.api.nvim_buf_set_extmark(0, ns, cur_row, cur_col, {
-- end_col must be column of next character, in bytes
end_col = vim.fn.byteidx(cur_line, vim.fn.charidx(cur_line, cur_col) + 1),
hl_group = 'HopCursor',
priority = prio.CURSOR_PRIO
})
end
end
-- Move the cursor at a given location.
--
-- If inclusive is `true`, the jump target will be incremented visually by 1, so that operator-pending motions can
-- correctly take into account the right offset. This is the main difference between motions such as `f` (inclusive)
-- and `t` (exclusive).
--
-- This function will update the jump list.
function M.move_cursor_to(w, line, column, inclusive)
-- If we do not ask for inclusive jump, we dont have to retreive any additional lines because we will jump to the
-- actual jump target. If we do want an inclusive jump, we need to retreive the line the jump target lies in so that
-- we can compute the offset correctly. This is linked to the fact that currently, Neovim doesns have an API to «
-- offset something by 1 visual column. »
if inclusive then
local buf_line = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(w), line - 1, line, false)[1]
column = vim.fn.byteidx(buf_line, column + 1)
end
-- update the jump list
vim.cmd("normal! m'")
vim.api.nvim_set_current_win(w)
vim.api.nvim_win_set_cursor(w, { line, column})
end
function M.hint_with(jump_target_gtr, opts)
if opts == nil then
opts = override_opts(opts)
end
M.hint_with_callback(jump_target_gtr, opts, function(jt)
M.move_cursor_to(jt.window, jt.line + 1, jt.column - 1, opts.inclusive_jump)
end)
end
function M.hint_with_callback(jump_target_gtr, opts, callback)
if opts == nil then
opts = override_opts(opts)
end
if not M.initialized then
vim.notify('Hop is not initialized; please call the setup function', 4)
return
end
local all_ctxs = window.get_window_context(opts.multi_windows)
-- create the highlight groups; the highlight groups will allow us to clean everything at once when Hop quits
local hl_ns = vim.api.nvim_create_namespace('hop_hl')
local dim_ns = vim.api.nvim_create_namespace('')
-- create jump targets
local generated = jump_target_gtr(opts)
local jump_target_count = #generated.jump_targets
local h = nil
if jump_target_count == 0 then
eprintln(' -> theres no such thing we can see…', opts.teasing)
clear_namespace(0, hl_ns)
clear_namespace(0, dim_ns)
return
elseif jump_target_count == 1 and opts.jump_on_sole_occurrence then
local jt = generated.jump_targets[1]
callback(jt)
clear_namespace(0, hl_ns)
clear_namespace(0, dim_ns)
return
end
-- we have at least two targets, so generate hints to display
local hints = hint.create_hints(generated.jump_targets, generated.indirect_jump_targets, opts)
local hint_state = {
hints = hints,
hl_ns = hl_ns,
dim_ns = dim_ns,
}
local buf_list = {}
for _, bctx in ipairs(all_ctxs) do
buf_list[#buf_list + 1] = bctx.hbuf
for _, wctx in ipairs(bctx.contexts) do
window.clip_window_context(wctx, opts.direction)
-- dim everything out, add the virtual cursor and hide diagnostics
apply_dimming(bctx.hbuf, dim_ns, wctx.top_line, wctx.bot_line, wctx.cursor_pos, opts.direction, opts.current_line_only)
end
end
add_virt_cur(hl_ns)
if vim.fn.has("nvim-0.6") == 1 then
hint_state.diag_ns = vim.diagnostic.get_namespaces()
for ns in pairs(hint_state.diag_ns) do vim.diagnostic.show(ns, 0, nil, { virtual_text = false }) end
end
hint.set_hint_extmarks(hl_ns, hints, opts)
vim.cmd('redraw')
while h == nil do
local ok, key = pcall(vim.fn.getchar)
if not ok then
for _, buf in ipairs(buf_list) do
M.quit(buf, hint_state)
end
break
end
local not_special_key = true
-- :h getchar(): "If the result of expr is a single character, it returns a
-- number. Use nr2char() to convert it to a String." Also the result is a
-- special key if it's a string and its first byte is 128.
--
-- Note of caution: Even though the result of `getchar()` might be a single
-- character, that character might still be multiple bytes.
if type(key) == 'number' then
key = vim.fn.nr2char(key)
elseif key:byte() == 128 then
not_special_key = false
end
if not_special_key and opts.keys:find(key, 1, true) then
-- If this is a key used in Hop (via opts.keys), deal with it in Hop
h = M.refine_hints(buf_list, key, hint_state, callback, opts)
vim.cmd('redraw')
else
-- If it's not, quit Hop
for _, buf in ipairs(buf_list) do
M.quit(buf, hint_state)
end
-- If the key captured via getchar() is not the quit_key, pass it through
-- to nvim to be handled normally (including mappings)
if key ~= vim.api.nvim_replace_termcodes(opts.quit_key, true, false, true) then
vim.api.nvim_feedkeys(key, '', true)
end
break
end
end
end
-- Refine hints in the given buffer.
--
-- Refining hints allows to advance the state machine by one step. If a terminal step is reached, this function jumps to
-- the location. Otherwise, it stores the new state machine.
function M.refine_hints(buf_list, key, hint_state, callback, opts)
local h, hints = hint.reduce_hints(hint_state.hints, key)
if h == nil then
if #hints == 0 then
eprintln('no remaining sequence starts with ' .. key, opts.teasing)
return
end
hint_state.hints = hints
for _, buf in ipairs(buf_list) do
clear_namespace(buf, hint_state.hl_ns)
end
hint.set_hint_extmarks(hint_state.hl_ns, hints, opts)
vim.cmd('redraw')
else
for _, buf in ipairs(buf_list) do
M.quit(buf, hint_state)
end
-- prior to jump, register the current position into the jump list
vim.cmd("normal! m'")
callback(h.jump_target)
return h
end
end
-- Quit Hop and delete its resources.
function M.quit(buf_handle, hint_state)
clear_namespace(buf_handle, hint_state.hl_ns)
clear_namespace(buf_handle, hint_state.dim_ns)
if vim.fn.has("nvim-0.6") == 1 then
for ns in pairs(hint_state.diag_ns) do vim.diagnostic.show(ns, buf_handle) end
end
end
function M.hint_words(opts)
opts = override_opts(opts)
local generator
if opts.current_line_only then
generator = jump_target.jump_targets_for_current_line
else
generator = jump_target.jump_targets_by_scanning_lines
end
M.hint_with(
generator(jump_target.regex_by_word_start()),
opts
)
end
function M.hint_patterns(opts, pattern)
opts = override_opts(opts)
-- The pattern to search is either retrieved from the (optional) argument
-- or directly from user input.
if pattern == nil then
vim.fn.inputsave()
local ok
ok, pattern = pcall(vim.fn.input, 'Search: ')
vim.fn.inputrestore()
if not ok then
return
end
end
local generator
if opts.current_line_only then
generator = jump_target.jump_targets_for_current_line
else
generator = jump_target.jump_targets_by_scanning_lines
end
M.hint_with(
generator(jump_target.regex_by_case_searching(pattern, false, opts)),
opts
)
end
function M.hint_char1(opts)
opts = override_opts(opts)
local ok, c = pcall(vim.fn.getchar)
if not ok then
return
end
local generator
if opts.current_line_only then
generator = jump_target.jump_targets_for_current_line
else
generator = jump_target.jump_targets_by_scanning_lines
end
M.hint_with(
generator(jump_target.regex_by_case_searching(vim.fn.nr2char(c), true, opts)),
opts
)
end
function M.hint_char2(opts)
opts = override_opts(opts)
local ok, a = pcall(vim.fn.getchar)
if not ok then
return
end
local ok2, b = pcall(vim.fn.getchar)
if not ok2 then
return
end
local pattern = vim.fn.nr2char(a)
-- if we have a fallback key defined in the opts, if the second character is that key, we then fallback to the same
-- behavior as hint_char1()
if opts.char2_fallback_key == nil or b ~= vim.fn.char2nr(vim.api.nvim_replace_termcodes(opts.char2_fallback_key, true, false, true)) then
pattern = pattern .. vim.fn.nr2char(b)
end
local generator
if opts.current_line_only then
generator = jump_target.jump_targets_for_current_line
else
generator = jump_target.jump_targets_by_scanning_lines
end
M.hint_with(
generator(jump_target.regex_by_case_searching(pattern, true, opts)),
opts
)
end
function M.hint_lines(opts)
opts = override_opts(opts)
local generator
if opts.current_line_only then
generator = jump_target.jump_targets_for_current_line
else
generator = jump_target.jump_targets_by_scanning_lines
end
M.hint_with(
generator(jump_target.regex_by_line_start()),
opts
)
end
function M.hint_lines_skip_whitespace(opts)
opts = override_opts(opts)
local generator
if opts.current_line_only then
generator = jump_target.jump_targets_for_current_line
else
generator = jump_target.jump_targets_by_scanning_lines
end
M.hint_with(
generator(jump_target.regex_by_line_start_skip_whitespace()),
opts
)
end
function M.hint_anywhere(opts)
opts = override_opts(opts)
local generator
if opts.current_line_only then
generator = jump_target.jump_targets_for_current_line
else
generator = jump_target.jump_targets_by_scanning_lines
end
M.hint_with(
generator(jump_target.regex_by_anywhere()),
opts
)
end
-- Setup user settings.
function M.setup(opts)
-- Look up keys in user-defined table with fallback to defaults.
M.opts = setmetatable(opts or {}, {__index = defaults})
M.initialized = true
-- Insert the highlights and register the autocommand if asked to.
local highlight = require'hop.highlight'
highlight.insert_highlights()
if M.opts.create_hl_autocmd then
highlight.create_autocmd()
end
-- register Hop extensions, if any
if M.opts.extensions ~= nil then
for _, ext_name in pairs(opts.extensions) do
local ok, extension = pcall(require, ext_name)
if not ok then
-- 4 is error; thanks Neovim… :(
vim.notify(string.format('extension %s wasnt correctly loaded', ext_name), 4)
else
if extension.register == nil then
vim.notify(string.format('extension %s lacks the register function', ext_name), 4)
else
extension.register(opts)
end
end
end
end
end
return M

View File

@ -0,0 +1,406 @@
-- Jump targets.
--
-- Jump targets are locations in buffers where users might jump to. They are wrapped in a table and provide the
-- required information so that Hop can associate label and display the hints.
--
-- {
-- jump_targets = {},
-- indirect_jump_targets = {},
-- }
--
-- The `jump_targets` field is a list-table of jump targets. A single jump target is simply a location in a given
-- buffer. So you can picture a jump target as a triple (line, column, window).
--
-- {
-- line = 0,
-- column = 0,
-- window = 0,
-- }
--
-- Indirect jump targets are encoded as a flat list-table of pairs (index, score). This table allows to quickly score
-- and sort jump targets. The `index` field gives the index in the `jump_targets` list. The `score` is any number. The
-- rule is that the lower the score is, the less prioritized the jump target will be.
--
-- {
-- index = 0,
-- score = 0,
-- }
--
-- So for instance, for two jump targets, a jump target generator must return such a table:
--
-- {
-- jump_targets = {
-- { line = 1, column = 14, buffer = 0, window = 0 },
-- { line = 2, column = 1, buffer = 0, window = 0 },
-- },
--
-- indirect_jump_targets = {
-- { index = 0, score = 14 },
-- { index = 1, score = 7 },
-- },
-- }
--
-- This is everything you need to know to extend Hop with your own jump targets.
local hint = require'hop.hint'
local window = require'hop.window'
local M = {}
-- Manhattan distance with column and row, weighted on x so that results are more packed on y.
function M.manh_dist(a, b, x_bias)
local bias = x_bias or 10
return bias * math.abs(b[1] - a[1]) + math.abs(b[2] - a[2])
end
-- Mark the current line with jump targets.
--
-- Returns the jump targets as described above.
local function mark_jump_targets_line(buf_handle, win_handle, regex, line_context, col_offset, win_width, direction_mode, hint_position)
local jump_targets = {}
local end_index = nil
if win_width ~= nil then
end_index = col_offset + win_width
else
end_index = vim.fn.strdisplaywidth(line_context.line)
end
local shifted_line = line_context.line:sub(1 + col_offset, vim.fn.byteidx(line_context.line, end_index))
-- modify the shifted line to take the direction mode into account, if any
-- FIXME: we also need to do that for the cursor
local col_bias = 0
if direction_mode ~= nil then
local col = vim.fn.byteidx(line_context.line, direction_mode.cursor_col + 1)
if direction_mode.direction == hint.HintDirection.AFTER_CURSOR then
-- we want to change the start offset so that we ignore everything before the cursor
shifted_line = shifted_line:sub(col - col_offset)
col_bias = col - 1
elseif direction_mode.direction == hint.HintDirection.BEFORE_CURSOR then
-- we want to change the end
shifted_line = shifted_line:sub(1, col - col_offset)
end
end
local col = 1
while true do
local s = shifted_line:sub(col)
local b, e = regex.match(s)
if b == nil or (b == 0 and e == 0) then
break
end
local colp = col + b
if hint_position == hint.HintPosition.MIDDLE then
colp = col + math.floor((b + e) / 2)
elseif hint_position == hint.HintPosition.END then
colp = col + e - 1
end
jump_targets[#jump_targets + 1] = {
line = line_nr,
column = math.max(1, colp + col_offset + col_bias),
line = line_context.line_nr,
column = math.max(1, colp + col_offset + col_bias),
buffer = buf_handle,
window = win_handle,
}
if regex.oneshot then
break
else
col = col + e
end
end
return jump_targets
end
-- Create jump targets for a given indexed line.
--
-- This function creates the jump targets for the current (indexed) line and appends them to the input list of jump
-- targets `jump_targets`.
--
-- Indirect jump targets are used later to sort jump targets by score and create hints.
local function create_jump_targets_for_line(
buf_handle,
win_handle,
jump_targets,
indirect_jump_targets,
regex,
col_offset,
win_width,
cursor_pos,
direction_mode,
hint_position,
line_context
)
-- first, create the jump targets for the ith line
local line_jump_targets = mark_jump_targets_line(
buf_handle,
win_handle,
regex,
line_context,
col_offset,
win_width,
direction_mode,
hint_position
)
-- then, append those to the input jump target list and create the indexed jump targets
local win_bias = math.abs(vim.api.nvim_get_current_win() - win_handle) * 1000
for _, jump_target in pairs(line_jump_targets) do
jump_targets[#jump_targets + 1] = jump_target
indirect_jump_targets[#indirect_jump_targets + 1] = {
index = #jump_targets,
score = M.manh_dist(cursor_pos, { jump_target.line, jump_target.column }) + win_bias
}
end
end
-- Create jump targets by scanning lines in the currently visible buffer.
--
-- This function takes a regex argument, which is an object containing a match function that must return the span
-- (inclusive beginning, exclusive end) of the match item, or nil when no more match is possible. This object also
-- contains the `oneshot` field, a boolean stating whether only the first match of a line should be taken into account.
--
-- This function returns the lined jump targets (an array of N lines, where N is the number of currently visible lines).
-- Lines without jump targets are assigned an empty table ({}). For lines with jump targets, a list-table contains the
-- jump targets as pair of { line, col }.
--
-- In addition the jump targets, this function returns the total number of jump targets (i.e. this is the same thing as
-- traversing the lined jump targets and summing the number of jump targets for all lines) as a courtesy, plus «
-- indirect jump targets. » Indirect jump targets are encoded as a flat list-table containing three values: i, for the
-- ith line, j, for the rank of the jump target, and dist, the score distance of the associated jump target. This list
-- is sorted according to that last dist parameter in order to know how to distribute the jump targets over the buffer.
function M.jump_targets_by_scanning_lines(regex)
return function(opts)
-- get the window context; this is used to know which part of the visible buffer is to hint
local all_ctxs = window.get_window_context(opts.multi_windows)
local jump_targets = {}
local indirect_jump_targets = {}
-- Iterate all buffers
for _, bctx in ipairs(all_ctxs) do
-- Iterate all windows of a same buffer
for _, wctx in ipairs(bctx.contexts) do
window.clip_window_context(wctx, opts.direction)
-- Get all lines' context
local lines = window.get_lines_context(bctx.hbuf, wctx)
-- in the case of a direction, we want to treat the first or last line (according to the direction) differently
if opts.direction == hint.HintDirection.AFTER_CURSOR then
-- the first line is to be checked first
create_jump_targets_for_line(
bctx.hbuf,
wctx.hwin,
jump_targets,
indirect_jump_targets,
regex,
wctx.col_offset,
wctx.win_width,
wctx.cursor_pos,
{ cursor_col = wctx.cursor_pos[2], direction = opts.direction },
opts.hint_position,
lines[1]
)
for i = 2, #lines do
create_jump_targets_for_line(
bctx.hbuf,
wctx.hwin,
jump_targets,
indirect_jump_targets,
regex,
wctx.col_offset,
wctx.win_width,
wctx.cursor_pos,
nil,
opts.hint_position,
lines[i]
)
end
elseif opts.direction == hint.HintDirection.BEFORE_CURSOR then
-- the last line is to be checked last
for i = 1, #lines - 1 do
create_jump_targets_for_line(
bctx.hbuf,
wctx.hwin,
jump_targets,
indirect_jump_targets,
regex,
wctx.col_offset,
wctx.win_width,
wctx.cursor_pos,
nil,
opts.hint_position,
lines[i]
)
end
create_jump_targets_for_line(
bctx.hbuf,
wctx.hwin,
jump_targets,
indirect_jump_targets,
regex,
wctx.col_offset,
wctx.win_width,
wctx.cursor_pos,
{ cursor_col = wctx.cursor_pos[2], direction = opts.direction },
opts.hint_position,
lines[#lines]
)
else
for i = 1, #lines do
create_jump_targets_for_line(
bctx.hbuf,
wctx.hwin,
jump_targets,
indirect_jump_targets,
regex,
wctx.col_offset,
wctx.win_width,
wctx.cursor_pos,
nil,
opts.hint_position,
lines[i]
)
end
end
end
end
M.sort_indirect_jump_targets(indirect_jump_targets, opts)
return { jump_targets = jump_targets, indirect_jump_targets = indirect_jump_targets }
end
end
-- Jump target generator for regex applied only on the cursor line.
function M.jump_targets_for_current_line(regex)
return function(opts)
local context = window.get_window_context(false)[1].contexts[1]
local line_n = context.cursor_pos[1]
local line = vim.api.nvim_buf_get_lines(0, line_n - 1, line_n, false)
local jump_targets = {}
local indirect_jump_targets = {}
create_jump_targets_for_line(
0,
0,
jump_targets,
indirect_jump_targets,
regex,
context.col_offset,
context.win_width,
context.cursor_pos,
{ cursor_col = context.cursor_pos[2], direction = opts.direction },
opts.hint_position,
{ line_nr = line_n - 1, line = line[1] }
)
M.sort_indirect_jump_targets(indirect_jump_targets, opts)
return { jump_targets = jump_targets, indirect_jump_targets = indirect_jump_targets }
end
end
-- Apply a score function based on the Manhattan distance to indirect jump targets.
function M.sort_indirect_jump_targets(indirect_jump_targets, opts)
local score_comparison = nil
if opts.reverse_distribution then
score_comparison = function (a, b) return a.score > b.score end
else
score_comparison = function (a, b) return a.score < b.score end
end
table.sort(indirect_jump_targets, score_comparison)
end
-- Regex modes for the buffer-driven generator.
local function starts_with_uppercase(s)
if #s == 0 then
return false
end
local f = s:sub(1, vim.fn.byteidx(s, 1))
-- if its a space, we assume its not uppercase, even though Lua doesnt agree with us; I mean, Lua is horrible, who
-- would like to argue with that creature, right?
if f == ' ' then
return false
end
return f:upper() == f
end
-- Regex by searching a pattern.
function M.regex_by_searching(pat, plain_search)
if plain_search then
pat = vim.fn.escape(pat, '\\/.$^~[]')
end
return {
oneshot = false,
match = function(s)
return vim.regex(pat):match_str(s)
end
}
end
-- Wrapper over M.regex_by_searching to add support for case sensitivity.
function M.regex_by_case_searching(pat, plain_search, opts)
if plain_search then
pat = vim.fn.escape(pat, '\\/.$^~[]')
end
if vim.o.smartcase then
if not starts_with_uppercase(pat) then
pat = '\\c' .. pat
end
elseif opts.case_insensitive then
pat = '\\c' .. pat
end
return {
oneshot = false,
match = function(s)
return vim.regex(pat):match_str(s)
end
}
end
-- Word regex.
function M.regex_by_word_start()
return M.regex_by_searching('\\k\\+')
end
-- Line regex.
function M.regex_by_line_start()
return {
oneshot = true,
match = function(_)
return 0, 1, false
end
}
end
-- Line regex skipping finding the first non-whitespace character on each line.
function M.regex_by_line_start_skip_whitespace()
local pat = vim.regex("\\S")
return {
oneshot = true,
match = function(s)
return pat:match_str(s)
end
}
end
-- Anywhere regex.
function M.regex_by_anywhere()
return M.regex_by_searching('\\v(<.|^$)|(.>|^$)|(\\l)\\zs(\\u)|(_\\zs.)|(#\\zs.)')
end
return M

View File

@ -0,0 +1,163 @@
local M = {}
-- Get the first key of a key set.
local function first_key(keys)
return keys:sub(1, vim.fn.byteidx(keys, 1))
end
-- Get the next key of the input key in the input key set, if any, or return nil.
local function next_key(keys, key)
local _, e = keys:find(key)
if e == #keys then
return nil
end
local next = keys:sub(e + 1)
local n = next:sub(1, vim.fn.byteidx(next, 1))
return n
end
-- Permutation algorithm based on tries and backtrack filling.
M.TrieBacktrackFilling = {}
-- Get the sequence encoded in a trie by a pointer.
function M.TrieBacktrackFilling:lookup_seq_trie(trie, p)
local seq = {}
local t = trie
for _, i in pairs(p) do
local current_trie = t[i]
seq[#seq + 1] = current_trie.key
t = current_trie.trie
end
seq[#seq + 1] = t[#t].key
return seq
end
-- Add a new permutation to the trie at the current pointer by adding a key.
function M.TrieBacktrackFilling:add_trie_key(trie, p, key)
local seq = {}
local t = trie
-- find the parent trie
for _, i in pairs(p) do
local current_trie = t[i]
seq[#seq + 1] = current_trie.key
t = current_trie.trie
end
t[#t + 1] = { key = key; trie = {} }
return trie
end
-- Maintain a trie pointer of a given dimension.
--
-- If a pointer has components { 4, 1 } and the dimension is 4, this function will automatically complete the missing
-- dimensions by adding the last index, i.e. { 4, 1, X, X }.
local function maintain_deep_pointer(depth, n, p)
local q = vim.deepcopy(p)
for i = #p + 1, depth do
q[i] = n
end
return q
end
-- Generate the next permutation with backtrack filling.
--
-- - `keys` is the input key set.
-- - `trie` is a trie representing all the already generated permutations.
-- - `p` is the current pointer in the trie. It is a list of indices representing the parent layer in which the current
-- sequence occurs in.
--
-- Returns `perms` added with the next permutation.
function M.TrieBacktrackFilling:next_perm(keys, trie, p)
if #trie == 0 then
return { { key = first_key(keys); trie = {} } }, p
end
-- check whether the current sequence can have a next one
local current_seq = self:lookup_seq_trie(trie, p)
local key = next_key(keys, current_seq[#current_seq])
if key ~= nil then
-- we can generate the next permutation by just adding key to the current trie
self:add_trie_key(trie, p, key)
return trie, p
else
-- we have to backtrack; first, decrement the pointer if possible
local max_depth = #p
local keys_len = vim.fn.strwidth(keys)
while #p > 0 do
local last_index = p[#p]
if last_index > 1 then
p[#p] = last_index - 1
p = maintain_deep_pointer(max_depth, keys_len, p)
-- insert the first key at the new pointer after mutating the one already there
self:add_trie_key(trie, p, first_key(keys))
self:add_trie_key(trie, p, next_key(keys, first_key(keys)))
return trie, p
else
-- we have exhausted all the permutations for the current layer; drop the layer index and try again
p[#p] = nil
end
end
-- all layers are completely full everywhere; add a new layer at the end
p = maintain_deep_pointer(max_depth, keys_len, p)
p[#p + 1] = #trie -- new layer
self:add_trie_key(trie, p, first_key(keys))
self:add_trie_key(trie, p, next_key(keys, first_key(keys)))
return trie, p
end
end
function M.TrieBacktrackFilling:trie_to_perms(trie, perm)
local perms = {}
local p = vim.deepcopy(perm)
p[#p + 1] = trie.key
if #trie.trie > 0 then
for _, sub_trie in pairs(trie.trie) do
vim.list_extend(perms, self:trie_to_perms(sub_trie, p))
end
else
perms = { p }
end
return perms
end
function M.TrieBacktrackFilling:permutations(keys, n)
local perms = {}
local trie = {}
local p = {}
for _ = 1, n do
trie, p = self:next_perm(keys, trie, p)
end
for _, sub_trie in pairs(trie) do
vim.list_extend(perms, self:trie_to_perms(sub_trie, {}))
end
return perms
end
function M.permutations(keys, n, opts)
return opts.perm_method:permutations(keys, n, opts)
end
return M

View File

@ -0,0 +1,14 @@
-- Magic constants for highlight priorities;
--
-- Priorities are ranged on 16-bit integers; 0 is the least priority and 2^16 - 1 is the higher.
-- We want Hop to override everything so we use a very high priority for grey (2^16 - 3 = 65533); hint
-- priorities are one level above (2^16 - 2) and the virtual cursor one level higher (2^16 - 1), which
-- is the higher.
local M = {}
M.DIM_PRIO = 65533
M.HINT_PRIO = 65534
M.CURSOR_PRIO = 65535
return M

View File

@ -0,0 +1,140 @@
local hint = require'hop.hint'
local M = {}
local function window_context(win_handle, cursor_pos)
-- get a bunch of information about the window and the cursor
local win_info = vim.fn.getwininfo(win_handle)[1]
local win_view = vim.fn.winsaveview()
local top_line = win_info.topline - 1
local bot_line = win_info.botline
-- NOTE: due to an (unknown yet) bug in neovim, the sign_width is not correctly reported when shifting the window
-- view inside a non-wrap window, so we cant rely on this; for this reason, we have to implement a weird hack that
-- is going to disable the signs while hop is running (Im sorry); the state is restored after jump
-- local left_col_offset = win_info.variables.context.number_width + win_info.variables.context.sign_width
local win_width = nil
-- hack to get the left column offset in nowrap
if not vim.wo.wrap then
vim.api.nvim_win_set_cursor(win_handle, { cursor_pos[1], 0 })
local left_col_offset = vim.fn.wincol() - 1
vim.fn.winrestview(win_view)
win_width = win_info.width - left_col_offset
end
return {
hwin = win_handle,
cursor_pos = cursor_pos,
top_line = top_line,
bot_line = bot_line,
win_width = win_width,
col_offset = win_view.leftcol
}
end
-- Collect all multi-windows's context:
--
-- {
-- { -- context list that each contains one buffer
-- hbuf = <buf-handle>,
-- { -- windows list that display the same buffer
-- hwin = <win-handle>,
-- ...
-- },
-- ...
-- },
-- ...
-- }
function M.get_window_context(multi_windows)
local all_ctxs = {}
-- Generate contexts of windows
local cur_hwin = vim.api.nvim_get_current_win()
local cur_hbuf = vim.api.nvim_win_get_buf(cur_hwin)
all_ctxs[#all_ctxs + 1] = {
hbuf = cur_hbuf,
contexts = { window_context(cur_hwin, vim.api.nvim_win_get_cursor(cur_hwin)) },
}
if not multi_windows then
return all_ctxs
end
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local b = vim.api.nvim_win_get_buf(w)
if w ~= cur_hwin then
-- check duplicated buffers; the way this is done is by accessing all the already known contexts and checking that
-- the buffer we are accessing is already present in; if it is, we then append the window context to that buffer
local bctx = nil
for _, buffer_ctx in ipairs(all_ctxs) do
if b == buffer_ctx.hbuf then
bctx = buffer_ctx.contexts
break
end
end
if bctx then
bctx[#bctx + 1] = window_context(w, vim.api.nvim_win_get_cursor(w))
else
all_ctxs[#all_ctxs + 1] = {
hbuf = b,
contexts = { window_context(w, vim.api.nvim_win_get_cursor(w)) }
}
end
end
end
-- Move cursor back to current window
vim.api.nvim_set_current_win(cur_hwin)
return all_ctxs
end
-- Collect visible and unfold lines of window context
--
-- {
-- { line_nr = 0, line = "" }
-- }
function M.get_lines_context(buf_handle, context)
local lines = {}
local lnr = context.top_line
while lnr < context.bot_line do -- top_line is inclusive and bot_line is exclusive
local fold_end = vim.api.nvim_win_call(context.hwin,
function()
return vim.fn.foldclosedend(lnr + 1) -- `foldclosedend()` use 1-based line number
end)
if fold_end == -1 then
lines[#lines + 1] = {
line_nr = lnr,
line = vim.api.nvim_buf_get_lines(buf_handle, lnr, lnr + 1, false)[1], -- `nvim_buf_get_lines()` use 0-based line index
}
lnr = lnr + 1
else
lines[#lines + 1] = {
line_nr = lnr,
line = "",
}
lnr = fold_end
end
end
return lines
end
-- Clip the window context based on the direction.
--
-- If the direction is HintDirection.BEFORE_CURSOR, then everything after the cursor will be clipped.
-- If the direction is HintDirection.AFTER_CURSOR, then everything before the cursor will be clipped.
function M.clip_window_context(context, direction)
if direction == hint.HintDirection.BEFORE_CURSOR then
context.bot_line = context.cursor_pos[1] - 1
elseif direction == hint.HintDirection.AFTER_CURSOR then
context.top_line = context.cursor_pos[1] - 1
end
end
return M

View File

@ -0,0 +1,63 @@
if !has('nvim-0.5.0')
echohl Error
echom 'This plugin only works with Neovim >= v0.5.0'
echohl clear
finish
endif
" The jump-to-word command.
command! HopWord lua require'hop'.hint_words()
command! HopWordBC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
command! HopWordAC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })
command! HopWordCurrentLine lua require'hop'.hint_words({ current_line_only = true })
command! HopWordCurrentLineBC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })
command! HopWordCurrentLineAC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })
command! HopWordMW lua require'hop'.hint_words({ multi_windows = true })
" The jump-to-pattern command.
command! HopPattern lua require'hop'.hint_patterns()
command! HopPatternBC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
command! HopPatternAC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })
command! HopPatternCurrentLine lua require'hop'.hint_patterns({ current_line_only = true })
command! HopPatternCurrentLineBC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })
command! HopPatternCurrentLineAC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })
command! HopPatternMW lua require'hop'.hint_patterns({ multi_windows = true })
" The jump-to-char-1 command.
command! HopChar1 lua require'hop'.hint_char1()
command! HopChar1BC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
command! HopChar1AC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })
command! HopChar1CurrentLine lua require'hop'.hint_char1({ current_line_only = true })
command! HopChar1CurrentLineBC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })
command! HopChar1CurrentLineAC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })
command! HopChar1MW lua require'hop'.hint_char1({ multi_windows = true })
" The jump-to-char-2 command.
command! HopChar2 lua require'hop'.hint_char2()
command! HopChar2BC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
command! HopChar2AC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })
command! HopChar2CurrentLine lua require'hop'.hint_char2({ current_line_only = true })
command! HopChar2CurrentLineBC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })
command! HopChar2CurrentLineAC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })
command! HopChar2MW lua require'hop'.hint_char2({ multi_windows = true })
" The jump-to-line command.
command! HopLine lua require'hop'.hint_lines()
command! HopLineBC lua require'hop'.hint_lines({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
command! HopLineAC lua require'hop'.hint_lines({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })
command! HopLineMW lua require'hop'.hint_lines({ multi_windows = true })
" The jump-to-line command.
command! HopLineStart lua require'hop'.hint_lines_skip_whitespace()
command! HopLineStartBC lua require'hop'.hint_lines_skip_whitespace({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
command! HopLineStartAC lua require'hop'.hint_lines_skip_whitespace({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })
command! HopLineStartMW lua require'hop'.hint_lines_skip_whitespace({ multi_windows = true })
" The jump-to-anywhere command.
command! HopAnywhere lua require'hop'.hint_anywhere()
command! HopAnywhereBC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })
command! HopAnywhereAC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })
command! HopAnywhereCurrentLine lua require'hop'.hint_anywhere({ current_line_only = true })
command! HopAnywhereCurrentLineBC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })
command! HopAnywhereCurrentLineAC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })
command! HopAnywhereMW lua require'hop'.hint_anywhere({ multi_windows = true })

View File

@ -0,0 +1,260 @@
# Hop hint modes refined: an extensible model
This document is a design document presenting a redesign of Hops « hint modes » to allow for a better customization
experience for people using Hop.
<!-- vim-markdown-toc GFM -->
* [Context](#context)
* [Analysis](#analysis)
* [Prior and on-going work](#prior-and-on-going-work)
* [Solution](#solution)
* [Redesign `HintMode`](#redesign-hintmode)
* [Rewrite the public interface to support already existing modes](#rewrite-the-public-interface-to-support-already-existing-modes)
* [Part of the work that can be taken out of #123](#part-of-the-work-that-can-be-taken-out-of-123)
* [Alternatives](#alternatives)
* [Rationale](#rationale)
* [Future work](#future-work)
<!-- vim-markdown-toc -->
# Context
The current code uses the concept of _hint modes_ to work. Hop goes through all the visible lines and applies the hint
mode on each line, extracting _jump targets_. The jump targets are then associated with permutations, and the sum of
those properties makes a _hint_.
The goal is to be able to abstract away from this representation and create more general jump targets, so that the core
of Hop can be built using this new model, but also dependent users can:
- Build other plugins using the Hop API to create their own jump target and then be able to jump to them.
- Extend the possible hint modes to provide more Hop motion without necessarily having to merge their code upstream.
This is especially true as some needs are not necessarily something that should be maintained in Hop directly, such
as Treesitter targets which are considered not really interesting. Nevertheless, if some users would like to be able
to use Treesitter as a source of targets, Hop should provide a powerful enough API to allow people to do just that.
Currently, there is no way (besides pushing code) to extend Hop features. Because we want to let _programmers_ extend
Hop, there is no question to let _users_ extend it. What that means is that if a new motion is wanted, two possible
options are available:
- The motion is implemented as a local Lua function / Vim command mapped in the user configuration.
- Someone makes a plugin exposing the Lua function / a Vim command and implementing the motion.
- A possible third option that is unlikely but still possible would be that the motion is small and useful enough to
merge it upstream in https://github.com/phaazon/hop.nvim.
# Analysis
In order to understand how the code is currently working, we can have a look at it from a user perspective. They are
likely to use, either:
- The Vim commands, exposed in `pugin/hop.vim`.
- The Lua API public functions, in `lua/hop/init.lua`.
The Lua functions to use start with `hint_`. For instance, `hint_words()` (`:HopWord`). `hint_words` is defined as:
```lua
function M.hint_words(opts)
hint_with(hint.by_word_start, get_command_opts(opts))
end
```
`hint_patterns`, `hint_char1`, etc. are defined in a similar fashion. `hint_with` is the current (local) function used
to build other hint modes. It takes a `HintMode` as argument and the user options, and builds applies the hint mode.
This is the function that needs to be changed. It must, first, be publicly available. Then, the way the hint modes are
applied need to change. For instance, if a user wants to use Hop with their own jump targets (without having to scan
the visible part of the buffer), they should be able to.
The `hint_with` function is a pretty complex function that does a lot of things:
- It extracts a bunch of information about the current visible part of the buffer. This is useful not to create hints
for text that the user cannot see.
- It supports various optinos, such as direction hinting (before cursor, after cursor, current-line-only, etc.).
- It creates the highlight groups.
- Get the buffer lines so that hint modes can be applied on.
- Call the hint modes and reduce hints until a match is found.
- Do the actual jump.
`hint_mode`, the `HintMode` argument assed to `hint_with`, is used like this:
- It contains a `curr_line_only` boolean that allows to know whether the hinting should be restricted to the current
line only.
- It is passed to `hop.hint.create_hints` to create the hints. To do so, it is passed to other functions that will call
the `match` function on it. What it does is to generate a pair of values allowing to pin-point where the jump
targets are. It is not really an iterator as it returns _spans_ value (i.e. beginning / end), so we have to manually
shift lines to know where and when to stop.
- It contains a `oneshot` boolean that is mostly an implementation detail for the `match` loop to work. If its
`oneshot`, the loop breaks at the first iteration. This is useful for line hinting for instance, where only one jump
target should exist on each line.
So a couple of things to change here, obviously.
# Prior and on-going work
Some PRs have been pushed to attempt to solve this problem:
- [#123](https://github.com/phaazon/hop.nvim/pull/123): refactor hint strategies. Unfortunately, this PR wasnt reviewed
until late and had conflicting changes. Also, this PR changed too many things, refactoring things that dont really
have to be at this point (or not making sense to move around). However, the work can probably be partially taken out
and rebased in other commits, so that this work is not lost.
- [#133](https://github.com/phaazon/hop.nvim/pull/133): this one is a bit weird, as its scope could be have been split
into several PRs. The multi-windows support is probably something that will come later once hint modes are refactored.
The dict support to allow to pass a dict of things is interesting but thats also a feature that should be added
later, once the code is refactored.
So clearly, we need to do something about #123 first. #133 will then be rebased and should be smaller.
# Solution
## Redesign `HintMode`
The first thing that needs to be done is to change change `HintMode` so that it doesnt assume to run line-by-line. The
thing is, `hint_with` should be its own hint mode (that iterates over the lines of the currently visible part of the
buffer and extract jump targets). A `HintMode` should then be:
- A function that provides the jump targets. We need to provide some functions to be able to get visible lines for
instance for people who still want to operate on these. The idea is that once this function has run, it must provide a
dict of jump targets by buffer. Something like:
```lua
{
-- jump target for a buffer
{
buffer_handle = 124,
jump_targets = {
{ line = 67, column = 4},
{ line = 67, column = 7},
-- …
},
},
-- another jump target for another buffer…
}
```
- The code that creates hints (`hop.hint.create_hints`) then must only call that function and do the regular, currently
implemented algorithm associating jump targets with permutations to generate the actual hints.
- The hint reduction can occur as it normally does.
This solution removes `oneshot` and `match`, leaving hint modes as a simple generator function providing the list of
jump targets. However, doing this will require to move some code around to help writing those jump target generators.
For instance, the logic that goes line-by-line, extracting word patterns for instance, is not trivial and is very tricky
to implement (multi-byte, virtualedit, etc.). So this must stay around. Something we can probably do here is to provide
a function that will output a `HintMode` going line-by-line and applying the logic passed as argument. Also, I suggest
to change the name `HintMode` to `JumpTargetGenerator`, which makes more sense.
About the actual function generating the list, something that goes to mind: should we make this a fully synchronous
function that will return all the targets at once, or should we make this an actual generator? I.e. calling it will
return the first jump target, then calling it a second time will return the next jump target, etc. I think it can have
interesting use-cases but it will probably slow everything down for probably not something super interesting.
This design seems to be pretty similar to what was planned in #123, so there is probably some commits to extract from
that PR.
## Rewrite the public interface to support already existing modes
Currently, the following modes are available:
- `HopWord`: hint words.
- `HopWordBC`: same as above, but _before cursor_.
- `HopWordAC`: same as above, but _after cursor_.
- `HopPattern`: hint pattern (manually entered by the user with `input`).
- `HopPatternBC`: same as above, but _before cursor_.
- `HopPatternAC`: same as above, but _after cursor_.
- `HopChar1`: hint the current buffer by pressing one character to select which ones to jump to.
- `HopChar1BC`: same as above but _before cursor_.
- `HopChar1AC`: same as above but _after cursor_.
- `HopChar2`: hint the current buffer by pressing two characters to select which ones to jump to.
- `HopChar2BC`: same as above but _before cursor_.
- `HopChar2AC`: same as above but _after cursor_.
- `HopLine`: hint lines (first column).
- `HopLineBC`: same as above but _before cursor_.
- `HopLineAC`: same as above but _after cursor_.
- `HopLineStart`: hint lines (first non whitespace character).
- `HopLineStartBC`: same asbove but _before_cursor_.
- `HopLineStartAC`: same asbove but _after cursor_.
- `HopChar1Line`: same as `HopChar1` but applies only to the current line.
- `HopChar1LineAC`: same asbove but _before_cursor_.
- `HopChar1LineBC`: same asbove but _after cursor_.
All those commands need to be re-implemented with the new `JumpTargetGenerator` design. All the current commands are
based on scanning line-by-line the currently visible part of the buffer, so we will want a function creating a
`JumpTargetGenerator` that implements this logic. Its arguments should allow us to implement all of the commands above.
The important thing to understand is that the `*BC` and `*AC` variations are actually the same mode but applied with the
user configuration (i.e. `direction`). Restricting to the same line should probably also be a user-configuration option
to allow using `HopWord` only on the current line, for instance.
## Part of the work that can be taken out of #123
Given all the work described here, here is a break down view of the PR:
- [5f93a87d](https://github.com/phaazon/hop.nvim/pull/123/commits/5f93a87d57c4926ceb1f71898c96e777c2ff33d6):
refactoring / code hygiene. **Will pick**.
- [bc449524](https://github.com/phaazon/hop.nvim/pull/123/commits/bc449524605317f48aff824f55e8a0e2ed40d87e): move
`HintMode` to a new weird `constants.lua` module. This adds no value. **Will drop**.
- [adbab40e](https://github.com/phaazon/hop.nvim/pull/123/commits/adbab40ef97f1516dd301bb7c9b9e66bc3638c39): move
all the logic of getting the visible buffer part into a `get_window_context` function. This is interesting but will
require a bit of fixup work. **Will pick**.
- [30d35b94](https://github.com/phaazon/hop.nvim/pull/123/commits/30d35b9479a42feaca35707c5176d47ce7a6e58d): introduce
the concept of _aggregate_ to refactor the process of mapping jump targets (`indirect_hints`) with permutations to
yield `hints`. Im not a huge fan of the terminology, it doesnt really convey what the aggregates are for. We need to
change the terminology, but I will probably use that too. **Will pick**.
- [e8c84c11](https://github.com/phaazon/hop.nvim/pull/123/commits/e8c84c11a2a085d8df108772ad0c6d41571a3aa1): move out
associating permutations to jump targets. Im mostly okay with this, but we need to change the name of the function so
that its clear that the function now only creates jump targets, and another function adds the permutations to it.
This function still uses the _aggregate_ concept introduced in the previous commit, for which we need to change the
terminology. **Will pick**.
- [c8fa480c](https://github.com/phaazon/hop.nvim/pull/123/commits/c8fa480ce593296dcf49dd0ff7401ed930bafcf1): change the
semantics of hint modes to use `get_hints` instead of scanning lines by lines. We need to change the name so that its
something like `get_jump_targets` instead. **Will pick**.
- [9fc5d517](https://github.com/phaazon/hop.nvim/pull/123/commits/9fc5d51785a2819ecc2f7ce72fbaf3f8ad092ff9): remove
length from the output of the function creating the hints. This commit might be dangerous, because the reason for
having the length is important (it allows to ensure we cut currently the hints if they overlap / are at the end of a
`wrap` window). **Not sure, probably will drop**.
- [8a482a98](https://github.com/phaazon/hop.nvim/pull/123/commits/8a482a98041433fb924807a0b53f231327db90c2): replace the
concept of _aggregate_ with _hint list_ (should be _jump target list_) and general refactoring. Not sure whether I
will use this as the rest of the design will probably be left to implement regarding this current document. **Not
sure, probably will pick**.
- [82117eab](https://github.com/phaazon/hop.nvim/pull/123/commits/82117eab12f6f3278927667623ed4c267608a22e):
documentation enhancement. **Will pick**.
The actual commits that were picked were not the ones described just above because decisions to refactor / renames
things in a different way that matches more the overall design.
# Alternatives
Besides merging code upstream, there is no real alternative to this problem. We have to expose the jump target retreival
on the public API so that people can extend Hop the way they want.
# Rationale
This redesign should allow to people to extend Hop on their side without having to merge code upstream. Different
motivations exist, among people wanting to use Treesitter-based motions, which will _not_ end up upstream as I think
its not really interesting / useful / out of scope, because hints are more a visual thing than a semantics thing;
people wanting to use Hop in menus / interfaces, etc.
The other good point of this redesign is that we still support the _user configuration_ that is very important to the
author ([@phaazon](https://github.com/phaazon)).
Another interesting aspects of this redesign is to allow people to create Lua plugins that might end up upstream if
needed, but people wanting to create « extensions » plugins will not have to depend on the upstream to have it possible.
This is important for two reasons:
- People can implement their workflow.
- Hop can remain small and thus is much easier to maintain.
In order to help plugin authors to write their Hop extension, we will have to keep the documentation updated and
top of the notch.
# Future work
An important matter while I was writing this design doc: because we are probably going to have people implementing
extensions, they are going to use the public API of Hop, which will probably have deprecations / breaking-change at some
point. I have parallel work on going (i.e. [poesie.nvim](https://github.com/phaazon/poesie.nvim)) but ultimately, I
really want a SemVer API, so that plugin authors dont have to worry too much about this. Its more about the end-users:
I really dislike it when I update something and it breaks because of a deprecation somewhere. People have their lives,
they wont update immediately, so we need SemVer to prevent that kind of problems from occurring. We need to keep that
in mind for later because this Hop extension thing is going to a perfect example about why we need this. Im explicitely
pinging [@mjlbach](https://github.com/mjlbach) as we mentioned that quite a few times lately, and to show that Hop is
going to _really_ need this. I might probably implement a convention in poesie and givin what the core team want to do
regarding plugins (whether their version will be checked in the core or whether poesie / something else should be
responsible for it).

1
config/plugins/hop.vim Normal file
View File

@ -0,0 +1 @@
lua require('hop').setup()

View File

@ -1977,6 +1977,29 @@ automatically. and the format is:
>
autosave_location/path+=to+=filename.ext.backup
<
5. `enable_hop`: by default, spacevim use easymotion plugin. and if you are
using neovim 0.6.0 or above, hop.nvim will be enabled. You can disabled this
plugin and still using easymotion.
KEY BINDINGS
The `edit` layer also provides many key bindings:
>
key binding description
SPC x c count in the selection region
<
The following key binding is to jump to targets. The default plugin is
`easymotion`, and if you are using neovim 0.6.0 or above. The `hop.nvim` will
be used.
>
key binding description
SPC j j jump or select a character
SPC j J jump to suite of two characters
SPC j l jump or select to a line
SPC j w jump to a word
SPC j u jump to a url
<
==============================================================================
EXPRFOLD *SpaceVim-layers-exprfold*