From e360f0c9d5c39ebe149e4ac71ae82f47ee02cf38 Mon Sep 17 00:00:00 2001 From: Wang Shidong Date: Wed, 27 Apr 2022 22:13:32 +0800 Subject: [PATCH] feat(hop): add hop.nvim for neovim v0.6.0 --- .github/workflows/lint.yml | 4 + autoload/SpaceVim/layers/core.vim | 65 -- autoload/SpaceVim/layers/edit.vim | 138 ++- bundle/hop.nvim/.gitignore | 1 + bundle/hop.nvim/CONTRIBUTING.md | 192 ++++ bundle/hop.nvim/LICENSE | 30 + bundle/hop.nvim/README.md | 287 ++++++ bundle/hop.nvim/doc/hop.txt | 887 ++++++++++++++++++ .../lua/hop-extension-hello-world/init.lua | 32 + bundle/hop.nvim/lua/hop/defaults.lua | 17 + bundle/hop.nvim/lua/hop/health.lua | 36 + bundle/hop.nvim/lua/hop/highlight.lua | 31 + bundle/hop.nvim/lua/hop/hint.lua | 122 +++ bundle/hop.nvim/lua/hop/init.lua | 476 ++++++++++ bundle/hop.nvim/lua/hop/jump_target.lua | 406 ++++++++ bundle/hop.nvim/lua/hop/perm.lua | 163 ++++ bundle/hop.nvim/lua/hop/priority.lua | 14 + bundle/hop.nvim/lua/hop/window.lua | 140 +++ bundle/hop.nvim/plugin/hop.vim | 63 ++ .../rfcs/0001-hop-general-hint-modes.md | 260 +++++ config/plugins/hop.vim | 1 + doc/SpaceVim.txt | 23 + 22 files changed, 3321 insertions(+), 67 deletions(-) create mode 100644 bundle/hop.nvim/.gitignore create mode 100644 bundle/hop.nvim/CONTRIBUTING.md create mode 100644 bundle/hop.nvim/LICENSE create mode 100644 bundle/hop.nvim/README.md create mode 100644 bundle/hop.nvim/doc/hop.txt create mode 100644 bundle/hop.nvim/examples/hop-extension-hello-world/lua/hop-extension-hello-world/init.lua create mode 100644 bundle/hop.nvim/lua/hop/defaults.lua create mode 100644 bundle/hop.nvim/lua/hop/health.lua create mode 100644 bundle/hop.nvim/lua/hop/highlight.lua create mode 100644 bundle/hop.nvim/lua/hop/hint.lua create mode 100644 bundle/hop.nvim/lua/hop/init.lua create mode 100644 bundle/hop.nvim/lua/hop/jump_target.lua create mode 100644 bundle/hop.nvim/lua/hop/perm.lua create mode 100644 bundle/hop.nvim/lua/hop/priority.lua create mode 100644 bundle/hop.nvim/lua/hop/window.lua create mode 100644 bundle/hop.nvim/plugin/hop.vim create mode 100644 bundle/hop.nvim/rfcs/0001-hop-general-hint-modes.md create mode 100644 config/plugins/hop.vim diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f3d0fb1b4..47680e0cd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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: diff --git a/autoload/SpaceVim/layers/core.vim b/autoload/SpaceVim/layers/core.vim index 6472a2cfc..10052ca7d 100644 --- a/autoload/SpaceVim/layers/core.vim +++ b/autoload/SpaceVim/layers/core.vim @@ -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'], '(easymotion-overwin-f)', 'jump to a character', 0) - call SpaceVim#mapping#space#def('nmap', ['j', 'j'], '(better-easymotion-overwin-f)', 'jump-or-select-to-a-character', 0, 1) - nnoremap (better-easymotion-overwin-f) :call better_easymotion_overwin_f(0) - xnoremap (better-easymotion-overwin-f) :call better_easymotion_overwin_f(1) - call SpaceVim#mapping#space#def('nmap', ['j', 'J'], '(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'], '(easymotion-overwin-line)', 'jump to a line', 0) - call SpaceVim#mapping#space#def('nmap', ['j', 'l'], '(better-easymotion-overwin-line)', 'jump-or-select-to-a-line', 0, 1) - nnoremap (better-easymotion-overwin-line) :call better_easymotion_overwin_line(0) - xnoremap (better-easymotion-overwin-line) :call better_easymotion_overwin_line(1) - call SpaceVim#mapping#space#def('nmap', ['j', 'v'], '(easymotion-overwin-line)', 'jump-to-a-line', 0) - call SpaceVim#mapping#space#def('nmap', ['j', 'w'], '(easymotion-overwin-w)', 'jump-to-a-word', 0) - call SpaceVim#mapping#space#def('nmap', ['j', 'q'], '(easymotion-overwin-line)', 'jump-to-a-line', 0) call SpaceVim#mapping#space#def('nnoremap', ['j', 'n'], "i\\", '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 ', '', ':wincmd p', 'Switch to previous window or tab','wincmd p') call SpaceVim#mapping#space#def('nnoremap', [''], 'try | b# | catch | endtry', 'last-buffer', 1) let lnum = expand('') + 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("\(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("\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 diff --git a/autoload/SpaceVim/layers/edit.vim b/autoload/SpaceVim/layers/edit.vim index 06cb739c2..57d94d010 100644 --- a/autoload/SpaceVim/layers/edit.vim +++ b/autoload/SpaceVim/layers/edit.vim @@ -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' : '(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' : '(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'], '(easymotion-overwin-f)', 'jump to a character', 0) + call SpaceVim#mapping#space#def('nmap', ['j', 'j'], '(better-easymotion-overwin-f)', 'jump-or-select-to-a-character', 0, 1) + nnoremap (better-easymotion-overwin-f) :call better_easymotion_overwin_f(0) + xnoremap (better-easymotion-overwin-f) :call better_easymotion_overwin_f(1) + call SpaceVim#mapping#space#def('nmap', ['j', 'J'], '(easymotion-overwin-f2)', 'jump-to-suite-of-two-characters', 0) + " call SpaceVim#mapping#space#def('nmap', ['j', 'l'], '(easymotion-overwin-line)', 'jump to a line', 0) + call SpaceVim#mapping#space#def('nmap', ['j', 'l'], '(better-easymotion-overwin-line)', 'jump-or-select-to-a-line', 0, 1) + nnoremap (better-easymotion-overwin-line) :call better_easymotion_overwin_line(0) + xnoremap (better-easymotion-overwin-line) :call better_easymotion_overwin_line(1) + call SpaceVim#mapping#space#def('nmap', ['j', 'v'], '(easymotion-overwin-line)', 'jump-to-a-line', 0) + call SpaceVim#mapping#space#def('nmap', ['j', 'w'], '(easymotion-overwin-w)', 'jump-to-a-word', 0) + call SpaceVim#mapping#space#def('nmap', ['j', 'q'], '(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("\(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 diff --git a/bundle/hop.nvim/.gitignore b/bundle/hop.nvim/.gitignore new file mode 100644 index 000000000..0a56e3fc5 --- /dev/null +++ b/bundle/hop.nvim/.gitignore @@ -0,0 +1 @@ +/doc/tags diff --git a/bundle/hop.nvim/CONTRIBUTING.md b/bundle/hop.nvim/CONTRIBUTING.md new file mode 100644 index 000000000..c32e5987c --- /dev/null +++ b/bundle/hop.nvim/CONTRIBUTING.md @@ -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 +doesn’t comply to this document. + + + +* [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) + + + +# 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 don’t follow this document, for instance — are +going to require me more work than was involved into making the actual change. It’s even worse when the contribution +actually solves a bug or add a new feature. + +So please read this document; it’s not hard and the few rules here are easy to respect. You might already do +everything in this list anyway, but reading it won’t hurt you. For more junior / less-experienced developers, it’s +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 + don’t open an issue first and work on something that is not in the scope of the project, or already being + made by someone else, you’ll be working for nothing. Also, keep in mind that if your change doesn’t 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 — it’s greatly appreciated if the commit messages, code and PR’s content already + contains this information so that people don’t 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, + you’re advised to name your branch according to the _Git Flow_ naming convention: + - `fix/your-bug-here`: if you’re fixing a bug, name your branch. + - `feature/new-feature-here`: if you’re 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 you’re 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, I’m 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 don’t 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. + +**I’m very strict on git messages as I use them to write `CHANGELOG.md` files. Don’t 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, here’s 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 linter’s 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, it’s 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 won’t be _too picky_ about it, but if I judge that you should split a commit into two or fixup two commits, + please don’t take it too personal. :) + +If you don’t 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, it’s 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 +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 +they’re 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 don’t 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: + +> -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: + +> -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 doesn’t unlock any privilege people who don’t donate wouldn’t 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 don’t. This is a _free and open-source_ project, everybody is welcome to contribute, with or without + money. + +[@phaazon]: https://github.com/phaazon diff --git a/bundle/hop.nvim/LICENSE b/bundle/hop.nvim/LICENSE new file mode 100644 index 000000000..4194aab7e --- /dev/null +++ b/bundle/hop.nvim/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2021, Dimitri Sabadie + +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 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. diff --git a/bundle/hop.nvim/README.md b/bundle/hop.nvim/README.md new file mode 100644 index 000000000..80d6a4959 --- /dev/null +++ b/bundle/hop.nvim/README.md @@ -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. + + + +* [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) + + + +# 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 / 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 it’s 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 doesn’t 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 doesn’t 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', "lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })", {}) +vim.api.nvim_set_keymap('n', 'F', "lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })", {}) +vim.api.nvim_set_keymap('o', 'f', "lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true, inclusive_jump = true })", {}) +vim.api.nvim_set_keymap('o', 'F', "lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true, inclusive_jump = true })", {}) +vim.api.nvim_set_keymap('', 't', "lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true })", {}) +vim.api.nvim_set_keymap('', 'T', "lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true })", {}) +vim.api.nvim_set_keymap('n', 'e', " lua require'hop'.hint_words({ hint_position = require'hop.hint'.HintPosition.END })", {}) +vim.api.nvim_set_keymap('v', 'e', " lua require'hop'.hint_words({ hint_position = require'hop.hint'.HintPosition.END })", {}) +vim.api.nvim_set_keymap('o', 'e', " lua require'hop'.hint_words({ hint_position = require'hop.hint'.HintPosition.END, inclusive_jump = true })", {}) +``` + +# 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 it’s 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 diff --git a/bundle/hop.nvim/doc/hop.txt b/bundle/hop.nvim/doc/hop.txt new file mode 100644 index 000000000..2007bb6a8 --- /dev/null +++ b/bundle/hop.nvim/doc/hop.txt @@ -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 shouldn’t 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 + shouldn’t 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 don’t 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 shouldn’t 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 + 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 '', pressing 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 = ''` + +`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 haven’t + 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 + `` or ``, 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, it’s 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 doesn’t 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 + +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 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: diff --git a/bundle/hop.nvim/examples/hop-extension-hello-world/lua/hop-extension-hello-world/init.lua b/bundle/hop.nvim/examples/hop-extension-hello-world/lua/hop-extension-hello-world/init.lua new file mode 100644 index 000000000..79d02a9b8 --- /dev/null +++ b/bundle/hop.nvim/examples/hop-extension-hello-world/lua/hop-extension-hello-world/init.lua @@ -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 diff --git a/bundle/hop.nvim/lua/hop/defaults.lua b/bundle/hop.nvim/lua/hop/defaults.lua new file mode 100644 index 000000000..f24de4817 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/defaults.lua @@ -0,0 +1,17 @@ +local M = {} + +M.keys = 'asdghklqwertyuiopzxcvbnmfj' +M.quit_key = '' +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 diff --git a/bundle/hop.nvim/lua/hop/health.lua b/bundle/hop.nvim/lua/hop/health.lua new file mode 100644 index 000000000..6653087c1 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/health.lua @@ -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 diff --git a/bundle/hop.nvim/lua/hop/highlight.lua b/bundle/hop.nvim/lua/hop/highlight.lua new file mode 100644 index 000000000..2f597540a --- /dev/null +++ b/bundle/hop.nvim/lua/hop/highlight.lua @@ -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 diff --git a/bundle/hop.nvim/lua/hop/hint.lua b/bundle/hop.nvim/lua/hop/hint.lua new file mode 100644 index 000000000..5abfb6a66 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/hint.lua @@ -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 diff --git a/bundle/hop.nvim/lua/hop/init.lua b/bundle/hop.nvim/lua/hop/init.lua new file mode 100644 index 000000000..d5543bcb4 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/init.lua @@ -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 don’t 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 (it’s 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 don’t 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 doesn’s 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(' -> there’s 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 wasn’t 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 diff --git a/bundle/hop.nvim/lua/hop/jump_target.lua b/bundle/hop.nvim/lua/hop/jump_target.lua new file mode 100644 index 000000000..91a8e1b02 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/jump_target.lua @@ -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 it’s a space, we assume it’s not uppercase, even though Lua doesn’t 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 diff --git a/bundle/hop.nvim/lua/hop/perm.lua b/bundle/hop.nvim/lua/hop/perm.lua new file mode 100644 index 000000000..27b2c0072 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/perm.lua @@ -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 diff --git a/bundle/hop.nvim/lua/hop/priority.lua b/bundle/hop.nvim/lua/hop/priority.lua new file mode 100644 index 000000000..a41321fa7 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/priority.lua @@ -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 diff --git a/bundle/hop.nvim/lua/hop/window.lua b/bundle/hop.nvim/lua/hop/window.lua new file mode 100644 index 000000000..a93e2bf85 --- /dev/null +++ b/bundle/hop.nvim/lua/hop/window.lua @@ -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 can’t rely on this; for this reason, we have to implement a weird hack that + -- is going to disable the signs while hop is running (I’m 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 = , +-- { -- windows list that display the same buffer +-- hwin = , +-- ... +-- }, +-- ... +-- }, +-- ... +-- } +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 diff --git a/bundle/hop.nvim/plugin/hop.vim b/bundle/hop.nvim/plugin/hop.vim new file mode 100644 index 000000000..f51358de2 --- /dev/null +++ b/bundle/hop.nvim/plugin/hop.vim @@ -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 }) diff --git a/bundle/hop.nvim/rfcs/0001-hop-general-hint-modes.md b/bundle/hop.nvim/rfcs/0001-hop-general-hint-modes.md new file mode 100644 index 000000000..c3fe61dc9 --- /dev/null +++ b/bundle/hop.nvim/rfcs/0001-hop-general-hint-modes.md @@ -0,0 +1,260 @@ +# Hop hint modes refined: an extensible model + +This document is a design document presenting a redesign of Hop’s « hint modes » to allow for a better customization +experience for people using Hop. + + + + +* [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) + + +# 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 it’s + `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 wasn’t reviewed + until late and had conflicting changes. Also, this PR changed too many things, refactoring things that don’t 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 that’s 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 doesn’t 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`. I’m not a huge fan of the terminology, it doesn’t 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. I’m mostly okay with this, but we need to change the name of the function so + that it’s 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 it’s + 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 +it’s 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 don’t have to worry too much about this. It’s 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 won’t 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. I’m 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). diff --git a/config/plugins/hop.vim b/config/plugins/hop.vim new file mode 100644 index 000000000..3fc485e2e --- /dev/null +++ b/config/plugins/hop.vim @@ -0,0 +1 @@ +lua require('hop').setup() diff --git a/doc/SpaceVim.txt b/doc/SpaceVim.txt index 8635bda00..9c60cb36c 100644 --- a/doc/SpaceVim.txt +++ b/doc/SpaceVim.txt @@ -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*