diff --git a/bundle/hop.nvim/LICENSE b/bundle/hop.nvim/LICENSE index 4194aab7e..fbb6f8e87 100644 --- a/bundle/hop.nvim/LICENSE +++ b/bundle/hop.nvim/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2021, Dimitri Sabadie +Copyright (c) 2021-2022, Dimitri Sabadie All rights reserved. diff --git a/bundle/hop.nvim/README.md b/bundle/hop.nvim/README.md index 80d6a4959..1d6a08245 100644 --- a/bundle/hop.nvim/README.md +++ b/bundle/hop.nvim/README.md @@ -6,7 +6,17 @@ /_/ · Neovim motions on speed! · -[![](https://img.shields.io/badge/matrix-join%20the%20speed!-blueviolet)](https://matrix.to/#/#hop.nvim:matrix.org) +

+ + + + + +

+ +

+ Install · Wiki · Screenshots · Discuss +

**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 @@ -15,19 +25,14 @@ 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) @@ -36,8 +41,6 @@ target in your document reachable in a few keystrokes. * [Nightly users](#nightly-users) * [Usage](#usage) * [Keybindings](#keybindings) -* [Configuration](#configuration) -* [Extension](#extension) * [Chat](#chat) @@ -66,83 +69,18 @@ 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. +- Go to any word in the current buffer (`:HopWord`). +- Go to any character in the current buffer (`:HopChar1`). +- Go to any bigrams in the current buffer (`:HopChar2`). +- Make an arbitrary search akin to / and go to any occurrences (`:HopPattern`). +- Go to any line and any line start (`:HopLine`, `:HopLineStart`). +- Go to anywhere (`:HopAnywhere`). +- Use Hop cross windows with multi-windows support (`:Hop*MW`). +- Use it with commands like `v`, `d`, `c`, `y` to visually select/delete/change/yank up to your new cursor position. +- 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 (`:Hop*BC`, `:Hop*AC`), for the current line (`:Hop*CurrentLine`), + change the dictionary keys to use for the labels, jump on sole occurrence, etc. +- Extensible: provide your own jump targets and create Hop extensions! # Getting started @@ -184,7 +122,7 @@ Plug 'phaazon/hop.nvim' ```lua use { 'phaazon/hop.nvim', - branch = 'v1', -- optional but strongly recommended + branch = 'v2', -- optional but strongly recommended config = function() -- you can configure Hop the way you like here; see :h hop-config require'hop'.setup { keys = 'etovxqpdygfblzhckisuran' } @@ -199,29 +137,7 @@ the last one**. If you are not, then you are exposed to compatibility issues / b # 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 -``` +See the [wiki](https://github.com/phaazon/hop.nvim/wiki). # Keybindings @@ -231,53 +147,22 @@ 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 })", {}) +local hop = require('hop') +local directions = require('hop.hint').HintDirection +vim.keymap.set('', 'f', function() + hop.hint_char1({ direction = directions.AFTER_CURSOR, current_line_only = true }) +end, {remap=true}) +vim.keymap.set('', 'F', function() + hop.hint_char1({ direction = directions.BEFORE_CURSOR, current_line_only = true }) +end, {remap=true}) +vim.keymap.set('', 't', function() + hop.hint_char1({ direction = directions.AFTER_CURSOR, current_line_only = true, hint_offset = -1 }) +end, {remap=true}) +vim.keymap.set('', 'T', function() + hop.hint_char1({ direction = directions.BEFORE_CURSOR, current_line_only = true, hint_offset = 1 }) +end, {remap=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)! diff --git a/bundle/hop.nvim/doc/hop.txt b/bundle/hop.nvim/doc/hop.txt index 2007bb6a8..e82cd3d30 100644 --- a/bundle/hop.nvim/doc/hop.txt +++ b/bundle/hop.nvim/doc/hop.txt @@ -1,4 +1,4 @@ -*hop.txt* For Neovim version 0.5 Last change: 2021 Nov 02 +*hop.txt* For Neovim version 0.5 Last change: 2022 Oct 09 __ / /_ ____ ____ @@ -7,6 +7,7 @@ /_/ /_/\____/ .___/ /_/ · Neovim motions on speed! · + v2.0.2 ============================================================================== CONTENTS *hop-contents* @@ -177,6 +178,14 @@ creates a variant of the command that will only run for the current line. This is akin to calling the |hop.hint_lines_skip_whitespace| Lua function. +`:HopVertical` *:HopVertical* +`:HopVerticalBC` *:HopVerticalBC* +`:HopVerticalAC` *:HopVerticalAC* +`:HopVerticalMW` *:HopVerticalMW* + Like `HopLine` but keeps the column position of the cursor. + + This is akin to calling the |hop.hint_vertical| Lua function. + `:HopAnywhere` *:HopAnywhere* `:HopAnywhereBC` *:HopAnywhereBC* `:HopAnywhereAC` *:HopAnywhereAC* @@ -310,6 +319,16 @@ Most of the functions and values you need to know about are in `hop`. Arguments:~ {opts} Hop options. +`hop.hint_vertical(`{opts}`)` *hop.hint_vertical* + Hint the cursor position of each lines currently visible in the buffer + view and allow to jump to them. If the line is shorter than the current + cursor column position, it will default to the end of the line. + + 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. @@ -499,7 +518,7 @@ doing that for you. 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* +`hop.jump_target.by_line_start()` *hop.jump_target.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* @@ -728,6 +747,18 @@ below. Defaults:~ `hint_position = require'hop.hint'.HintPosition.BEGIN` +`hint_offset` *hop-config-hint_offset* + Offset to apply to a jump location. + + If it is non-zero, the jump target will be offset horizontally from the + selected jump position by `hint_offset` character(s). + + This option can be used for emulating the motion commands |t| and |T| where + the cursor is positioned on/before the target position. + + Defaults:~ + `hint_offset = 0` + `current_line_only` *hop-config-current_line_only* Apply Hop commands only to the current line. @@ -738,22 +769,6 @@ below. 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. @@ -845,6 +860,9 @@ highlight is used. For the rest: Highlight used for the fake cursor visible when running a Hop command / Lua functions. +`HopPreview` *hop-hl-HopPreview* + Highlight used for to preview the hint for HopPattern. + 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`. diff --git a/bundle/hop.nvim/lua/hop/defaults.lua b/bundle/hop.nvim/lua/hop/defaults.lua index f24de4817..32842f053 100644 --- a/bundle/hop.nvim/lua/hop/defaults.lua +++ b/bundle/hop.nvim/lua/hop/defaults.lua @@ -9,9 +9,9 @@ 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 +M.hint_offset = 0 return M diff --git a/bundle/hop.nvim/lua/hop/health.lua b/bundle/hop.nvim/lua/hop/health.lua index 6653087c1..887b04969 100644 --- a/bundle/hop.nvim/lua/hop/health.lua +++ b/bundle/hop.nvim/lua/hop/health.lua @@ -5,7 +5,7 @@ local hop = require'hop' -- -- This function will perform checks at initialization to ensure everything will work as expected. function M.check() - local health = require'health' + local health = vim.health or require'health' health.report_start('Ensuring keys are unique') local existing_keys = {} diff --git a/bundle/hop.nvim/lua/hop/highlight.lua b/bundle/hop.nvim/lua/hop/highlight.lua index 2f597540a..50b73413c 100644 --- a/bundle/hop.nvim/lua/hop/highlight.lua +++ b/bundle/hop.nvim/lua/hop/highlight.lua @@ -19,6 +19,9 @@ function M.insert_highlights() -- Highlight used for the fake cursor visible when hopping. vim.api.nvim_command('highlight default link HopCursor Cursor') + + -- Highlight used for preview pattern + vim.api.nvim_command('highlight link HopPreview IncSearch') end function M.create_autocmd() diff --git a/bundle/hop.nvim/lua/hop/hint.lua b/bundle/hop.nvim/lua/hop/hint.lua index 5abfb6a66..b7c645698 100644 --- a/bundle/hop.nvim/lua/hop/hint.lua +++ b/bundle/hop.nvim/lua/hop/hint.lua @@ -99,8 +99,10 @@ function M.set_hint_extmarks(hl_ns, hints, opts) label = label:upper() end + local col = hint.jump_target.column - 1 + 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, { + vim.api.nvim_buf_set_extmark(hint.jump_target.buffer or 0, hl_ns, hint.jump_target.line, col, { virt_text = { { label, "HopNextKey" } }, virt_text_pos = 'overlay', hl_mode = 'combine', @@ -109,7 +111,7 @@ function M.set_hint_extmarks(hl_ns, hints, opts) 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 + vim.api.nvim_buf_set_extmark(hint.jump_target.buffer or 0, hl_ns, hint.jump_target.line, col, { virt_text = { { label:sub(1, snd_idx), "HopNextKey1" }, { label:sub(snd_idx + 1), "HopNextKey2" } }, virt_text_pos = 'overlay', hl_mode = 'combine', @@ -119,4 +121,16 @@ function M.set_hint_extmarks(hl_ns, hints, opts) end end +function M.set_hint_preview(hl_ns, jump_targets) + for _, jt in ipairs(jump_targets) do + vim.api.nvim_buf_set_extmark(jt.buffer, hl_ns, jt.line, jt.column - 1, { + end_row = jt.line, + end_col = jt.column - 1 + jt.length, + hl_group = 'HopPreview', + hl_eol = true, + priority = prio.HINT_PRIO + }) + end +end + return M diff --git a/bundle/hop.nvim/lua/hop/init.lua b/bundle/hop.nvim/lua/hop/init.lua index d5543bcb4..32027e5a4 100644 --- a/bundle/hop.nvim/lua/hop/init.lua +++ b/bundle/hop.nvim/lua/hop/init.lua @@ -1,9 +1,3 @@ -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. @@ -18,6 +12,9 @@ local function check_opts(opts) if opts.multi_windows and opts.current_line_only then vim.notify('Cannot use current_line_only across multiple windows', 3) end + if vim.api.nvim_get_mode().mode ~= 'n' then + opts.multi_windows = false + end end -- Allows to override global options with user local overrides. @@ -33,18 +30,62 @@ local function eprintln(msg, teasing) 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) +-- Create hint state +-- +-- { +-- all_ctxs: All windows's context +-- buf_list: All buffers displayed in all windows +-- _ns: Required namespaces +-- } +local function create_hint_state(opts) + local window = require'hop.window' + + local hint_state = {} + + -- get all window's context and buffer list + hint_state.all_ctxs = window.get_window_context(opts.multi_windows) + hint_state.buf_list = {} + for _, bctx in ipairs(hint_state.all_ctxs) do + hint_state.buf_list[#hint_state.buf_list + 1] = bctx.hbuf + for _, wctx in ipairs(bctx.contexts) do + window.clip_window_context(wctx, opts.direction) + end + end + + -- create the highlight groups; the highlight groups will allow us to clean everything at once when Hop quits + hint_state.hl_ns = vim.api.nvim_create_namespace('hop_hl') + hint_state.dim_ns = vim.api.nvim_create_namespace('hop_dim') + + -- backup namespaces of diagnostic + if vim.fn.has("nvim-0.6") == 1 then + hint_state.diag_ns = vim.diagnostic.get_namespaces() + end + + -- Store users cursorline state + hint_state.cursorline = vim.api.nvim_win_get_option(vim.api.nvim_get_current_win(), 'cursorline') + + return hint_state end --- Dim everything out to prepare the Hop session. +-- A hack to prevent #57 by deleting twice the namespace (it’s super weird). +local function clear_namespace(buf_list, hl_ns) + for _, buf in ipairs(buf_list) do + if vim.api.nvim_buf_is_valid(buf) then + vim.api.nvim_buf_clear_namespace(buf, hl_ns, 0, -1) + vim.api.nvim_buf_clear_namespace(buf, hl_ns, 0, -1) + end + end +end + +-- Set the highlight of unmatched lines of the buffer. -- -- - 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 function set_unmatched_lines(buf_handle, hl_ns, top_line, bottom_line, cursor_pos, direction, current_line_only) + local hint = require'hop.hint' + local prio = require'hop.priority' + local start_line = top_line local end_line = bottom_line local start_col = 0 @@ -53,9 +94,8 @@ local function apply_dimming(buf_handle, hl_ns, top_line, bottom_line, cursor_po 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_line = bottom_line - 1 + if cursor_pos[2] ~= 0 then end_col = cursor_pos[2] end end if current_line_only then @@ -68,13 +108,45 @@ local function apply_dimming(buf_handle, hl_ns, top_line, bottom_line, cursor_po end end - vim.api.nvim_buf_set_extmark(buf_handle, hl_ns, start_line, start_col, { + local extmark_options = { end_line = end_line, - end_col = end_col, hl_group = 'HopUnmatched', hl_eol = true, priority = prio.DIM_PRIO - }) + } + + if end_col then + local current_line = vim.api.nvim_buf_get_lines(buf_handle, cursor_pos[1] - 1, cursor_pos[1], true)[1] + local current_width = vim.fn.strdisplaywidth(current_line) + + if end_col > current_width then + end_col = current_width - 1 + end + + extmark_options.end_col = end_col + end + + vim.api.nvim_buf_set_extmark(buf_handle, hl_ns, start_line, start_col, + extmark_options) +end + +-- Dim everything out to prepare the Hop session for all windows. +local function apply_dimming(hint_state, opts) + local window = require'hop.window' + + for _, bctx in ipairs(hint_state.all_ctxs) do + for _, wctx in ipairs(bctx.contexts) do + window.clip_window_context(wctx, opts.direction) + -- dim everything out, add the virtual cursor and hide diagnostics + set_unmatched_lines(bctx.hbuf, hint_state.dim_ns, wctx.top_line, wctx.bot_line, wctx.cursor_pos, opts.direction, opts.current_line_only) + end + + if vim.fn.has("nvim-0.6") == 1 then + for ns in pairs(hint_state.diag_ns) do + vim.diagnostic.show(ns, bctx.hbuf, nil, { virtual_text = false }) + end + end + end end -- Add the virtual cursor, taking care to handle the cases where: @@ -83,6 +155,8 @@ end -- - the current line is empty -- - there are multibyte characters on the line local function add_virt_cur(ns) + local prio = require'hop.priority' + 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 @@ -90,6 +164,12 @@ local function add_virt_cur(ns) local virt_col = cur_info[5] - 1 local cur_line = vim.api.nvim_get_current_line() + -- toggle cursorline off if currently set + local cursorline_info = vim.api.nvim_win_get_option(vim.api.nvim_get_current_win(), 'cursorline') + if cursorline_info == true then + vim.api.nvim_win_set_option(vim.api.nvim_get_current_win(), 'cursorline', false) + end + -- 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, { @@ -114,27 +194,112 @@ local function add_virt_cur(ns) end end +-- Get pattern from input for hint and preview +function M.get_input_pattern(prompt, maxchar, opts) + local hint = require'hop.hint' + local jump_target = require'hop.jump_target' + + local hs = {} + if opts then + hs = create_hint_state(opts) + hs.preview_ns = vim.api.nvim_create_namespace('hop_preview') + apply_dimming(hs, opts) + add_virt_cur(hs.hl_ns) + end + + local K_Esc = vim.api.nvim_replace_termcodes('', true, false, true) + local K_BS = vim.api.nvim_replace_termcodes('', true, false, true) + local K_C_H = vim.api.nvim_replace_termcodes('', true, false, true) + local K_CR = vim.api.nvim_replace_termcodes('', true, false, true) + local K_NL = vim.api.nvim_replace_termcodes('', true, false, true) + local pat_keys = {} + local pat = '' + + while (true) do + pat = vim.fn.join(pat_keys, '') + if opts then + clear_namespace(hs.buf_list, hs.preview_ns) + if #pat > 0 then + local ok, re = pcall(jump_target.regex_by_case_searching, pat, false, opts) + if ok then + local jump_target_gtr = jump_target.jump_targets_by_scanning_lines(re) + local generated = jump_target_gtr(opts) + hint.set_hint_preview(hs.preview_ns, generated.jump_targets) + end + end + end + vim.api.nvim_echo({}, false, {}) + vim.cmd('redraw') + vim.api.nvim_echo({{prompt, 'Question'}, {pat}}, false, {}) + + local ok, key = pcall(vim.fn.getchar) + if not ok then -- Interrupted by + pat = nil + break + end + + if type(key) == 'number' then + key = vim.fn.nr2char(key) + elseif key:byte() == 128 then + -- It's a special key in string + end + + if key == K_Esc then + pat = nil + break + elseif key == K_CR or key == K_NL then + break + elseif key == K_BS or key == K_C_H then + pat_keys[#pat_keys] = nil + else + pat_keys[#pat_keys + 1] = key + end + + if maxchar and #pat_keys >= maxchar then + pat = vim.fn.join(pat_keys, '') + break + end + end + + if opts then + clear_namespace(hs.buf_list, hs.preview_ns) + -- quit only when got nothin for pattern to avoid blink of highlight + if not pat then M.quit(hs) end + end + vim.api.nvim_echo({}, false, {}) + vim.cmd('redraw') + return pat +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). +-- Add option to shift cursor by column offset -- -- 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 +function M.move_cursor_to(w, line, column, hint_offset, direction) + -- If we do not ask for an offset jump, we don’t have to retrieve any additional lines because we will jump to the + -- actual jump target. If we do want a jump with an offset, we need to retrieve 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 N visual columns. » + + -- If it is pending for operator shift column to the right by 1 + if vim.api.nvim_get_mode().mode == 'no' and direction ~= 1 then + column = column + 1 + end + + if hint_offset ~= nil and not (hint_offset == 0) then + -- Add `hint_offset` based on `charidx`. 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) + -- Since `charidx` returns -1 when `column` is the tail, subtract 1 and add 1 to the return value to get + -- the correct value. + local char_idx = vim.fn.charidx(buf_line, column - 1) + 1 + hint_offset + column = vim.fn.byteidx(buf_line, char_idx) 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}) + vim.api.nvim_win_set_cursor(w, { line, column }) end function M.hint_with(jump_target_gtr, opts) @@ -143,11 +308,13 @@ function M.hint_with(jump_target_gtr, 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) + M.move_cursor_to(jt.window, jt.line + 1, jt.column - 1, opts.hint_offset, opts.direction) end) end function M.hint_with_callback(jump_target_gtr, opts, callback) + local hint = require'hop.hint' + if opts == nil then opts = override_opts(opts) end @@ -157,64 +324,49 @@ function M.hint_with_callback(jump_target_gtr, opts, callback) 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 hint state + local hs = create_hint_state(opts) -- create jump targets local generated = jump_target_gtr(opts) local jump_target_count = #generated.jump_targets - local h = nil + local target_idx = 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 + target_idx = 0 + elseif vim.v.count > 0 then + target_idx = vim.v.count elseif jump_target_count == 1 and opts.jump_on_sole_occurrence then - local jt = generated.jump_targets[1] - callback(jt) + target_idx = 1 + end - clear_namespace(0, hl_ns) - clear_namespace(0, dim_ns) + if target_idx ~= nil then + local jt = generated.jump_targets[target_idx] + if jt then + callback(jt) + else + eprintln(' -> there’s no such thing we can see…', opts.teasing) + end + + clear_namespace(hs.buf_list, hs.hl_ns) + clear_namespace(hs.buf_list, hs.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) + hs.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) + -- dim everything out, add the virtual cursor and hide diagnostics + apply_dimming(hs, opts) + add_virt_cur(hs.hl_ns) + hint.set_hint_extmarks(hs.hl_ns, hs.hints, opts) vim.cmd('redraw') + local h = nil 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 + M.quit(hs) break end local not_special_key = true @@ -232,13 +384,11 @@ function M.hint_with_callback(jump_target_gtr, opts, callback) 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) + h = M.refine_hints(key, hs, 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 + M.quit(hs) -- 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 @@ -253,7 +403,9 @@ end -- -- 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) +function M.refine_hints(key, hint_state, callback, opts) + local hint = require'hop.hint' + local h, hints = hint.reduce_hints(hint_state.hints, key) if h == nil then @@ -264,15 +416,10 @@ function M.refine_hints(buf_list, key, hint_state, callback, opts) hint_state.hints = hints - for _, buf in ipairs(buf_list) do - clear_namespace(buf, hint_state.hl_ns) - end + clear_namespace(hint_state.buf_list, hint_state.hl_ns) 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 + M.quit(hint_state) -- prior to jump, register the current position into the jump list vim.cmd("normal! m'") @@ -283,16 +430,28 @@ function M.refine_hints(buf_list, key, hint_state, callback, opts) 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) +function M.quit(hint_state) + clear_namespace(hint_state.buf_list, hint_state.hl_ns) + clear_namespace(hint_state.buf_list, 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 + -- Restore users cursorline setting + if hint_state.cursorline == true then + vim.api.nvim_win_set_option(vim.api.nvim_get_current_win(), 'cursorline', true) + end + + for _, buf in ipairs(hint_state.buf_list) do + -- sometimes, buffers might be unloaded; that’s the case with floats for instance (we can invoke Hop from them but + -- then they disappear); we need to check whether the buffer is still valid before trying to do anything else with + -- it + if vim.api.nvim_buf_is_valid(buf) and vim.fn.has("nvim-0.6") == 1 then + for ns in pairs(hint_state.diag_ns) do vim.diagnostic.show(ns, buf) end + end end end function M.hint_words(opts) + local jump_target = require'hop.jump_target' + opts = override_opts(opts) local generator @@ -309,20 +468,26 @@ function M.hint_words(opts) end function M.hint_patterns(opts, pattern) + local jump_target = require'hop.jump_target' + opts = override_opts(opts) -- The pattern to search is either retrieved from the (optional) argument -- or directly from user input. - if pattern == nil then + local pat + if pattern then + pat = pattern + else + vim.cmd('redraw') vim.fn.inputsave() - - local ok - ok, pattern = pcall(vim.fn.input, 'Search: ') + pat = M.get_input_pattern('Hop pattern: ', nil, opts) vim.fn.inputrestore() + if not pat then return end + end - if not ok then - return - end + if #pat == 0 then + eprintln('-> empty pattern', opts.teasing) + return end local generator @@ -333,16 +498,18 @@ function M.hint_patterns(opts, pattern) end M.hint_with( - generator(jump_target.regex_by_case_searching(pattern, false, opts)), + generator(jump_target.regex_by_case_searching(pat, false, opts)), opts ) end function M.hint_char1(opts) + local jump_target = require'hop.jump_target' + opts = override_opts(opts) - local ok, c = pcall(vim.fn.getchar) - if not ok then + local c = M.get_input_pattern('Hop 1 char: ', 1) + if not c then return end @@ -354,32 +521,21 @@ function M.hint_char1(opts) end M.hint_with( - generator(jump_target.regex_by_case_searching(vim.fn.nr2char(c), true, opts)), + generator(jump_target.regex_by_case_searching(c, true, opts)), opts ) end function M.hint_char2(opts) + local jump_target = require'hop.jump_target' + opts = override_opts(opts) - local ok, a = pcall(vim.fn.getchar) - if not ok then + local c = M.get_input_pattern('Hop 2 char: ', 2) + if not c 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 @@ -388,12 +544,14 @@ function M.hint_char2(opts) end M.hint_with( - generator(jump_target.regex_by_case_searching(pattern, true, opts)), + generator(jump_target.regex_by_case_searching(c, true, opts)), opts ) end function M.hint_lines(opts) + local jump_target = require'hop.jump_target' + opts = override_opts(opts) local generator @@ -404,12 +562,37 @@ function M.hint_lines(opts) end M.hint_with( - generator(jump_target.regex_by_line_start()), + generator(jump_target.by_line_start()), opts ) end +function M.hint_vertical(opts) + local hint = require'hop.hint' + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + -- only makes sense as end position given movement goal. + opts.hint_position = hint.HintPosition.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 + + -- FIXME: need to exclude current and include empty lines. + M.hint_with( + generator(jump_target.regex_by_vertical()), + opts + ) +end + + function M.hint_lines_skip_whitespace(opts) + local jump_target = require'hop.jump_target' + opts = override_opts(opts) local generator @@ -426,6 +609,8 @@ function M.hint_lines_skip_whitespace(opts) end function M.hint_anywhere(opts) + local jump_target = require'hop.jump_target' + opts = override_opts(opts) local generator @@ -444,7 +629,7 @@ 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.opts = setmetatable(opts or {}, {__index = require'hop.defaults'}) M.initialized = true -- Insert the highlights and register the autocommand if asked to. diff --git a/bundle/hop.nvim/lua/hop/jump_target.lua b/bundle/hop.nvim/lua/hop/jump_target.lua index 91a8e1b02..513fc43f6 100644 --- a/bundle/hop.nvim/lua/hop/jump_target.lua +++ b/bundle/hop.nvim/lua/hop/jump_target.lua @@ -91,6 +91,13 @@ local function mark_jump_targets_line(buf_handle, win_handle, regex, line_contex if b == nil or (b == 0 and e == 0) then break end + -- Preview need a length to highlight the matched string. Zero means nothing to highlight. + local matched_length = e - b + -- As the make for jump target must be placed at a cell (but some pattern like '^' is + -- placed between cells), we should make sure e > b + if b == e then + e = e + 1 + end local colp = col + b if hint_position == hint.HintPosition.MIDDLE then @@ -99,10 +106,9 @@ local function mark_jump_targets_line(buf_handle, win_handle, regex, line_contex 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), + length = math.max(0, matched_length), buffer = buf_handle, window = win_handle, } @@ -342,10 +348,13 @@ function M.regex_by_searching(pat, plain_search) if plain_search then pat = vim.fn.escape(pat, '\\/.$^~[]') end + + local regex = vim.regex(pat) + return { oneshot = false, match = function(s) - return vim.regex(pat):match_str(s) + return regex:match_str(s) end } end @@ -364,10 +373,12 @@ function M.regex_by_case_searching(pat, plain_search, opts) pat = '\\c' .. pat end + local regex = vim.regex(pat) + return { oneshot = false, match = function(s) - return vim.regex(pat):match_str(s) + return regex:match_str(s) end } end @@ -378,22 +389,42 @@ function M.regex_by_word_start() end -- Line regex. -function M.regex_by_line_start() +function M.by_line_start() + local c = vim.fn.winsaveview().leftcol + return { oneshot = true, - match = function(_) - return 0, 1, false + match = function(s) + local l = vim.fn.strdisplaywidth(s) + if c > 0 and l == 0 then + return nil + end + + return 0, 1 + end + } +end + +-- Line regex at cursor position. +function M.regex_by_vertical() + local position = vim.api.nvim_win_get_cursor(0)[2] + local regex = vim.regex(string.format("^.\\{0,%d\\}\\(.\\|$\\)", position)) + return { + oneshot = true, + match = function(s) + return regex:match_str(s) 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") + local regex = vim.regex("\\S") + return { oneshot = true, match = function(s) - return pat:match_str(s) + return regex:match_str(s) end } end diff --git a/bundle/hop.nvim/lua/hop/perm.lua b/bundle/hop.nvim/lua/hop/perm.lua index 27b2c0072..e1d946b74 100644 --- a/bundle/hop.nvim/lua/hop/perm.lua +++ b/bundle/hop.nvim/lua/hop/perm.lua @@ -7,7 +7,7 @@ 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) + local _, e = keys:find(key, 1, true) if e == #keys then return nil diff --git a/bundle/hop.nvim/lua/hop/window.lua b/bundle/hop.nvim/lua/hop/window.lua index a93e2bf85..289b04ad8 100644 --- a/bundle/hop.nvim/lua/hop/window.lua +++ b/bundle/hop.nvim/lua/hop/window.lua @@ -4,6 +4,7 @@ local M = {} local function window_context(win_handle, cursor_pos) -- get a bunch of information about the window and the cursor + vim.api.nvim_set_current_win(win_handle) local win_info = vim.fn.getwininfo(win_handle)[1] local win_view = vim.fn.winsaveview() local top_line = win_info.topline - 1 @@ -52,9 +53,10 @@ function M.get_window_context(multi_windows) -- 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)) }, + contexts = { window_context(cur_hwin, {vim.fn.line('.'), vim.fn.charcol('.')} ) }, } if not multi_windows then @@ -131,7 +133,7 @@ end -- 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 + context.bot_line = context.cursor_pos[1] elseif direction == hint.HintDirection.AFTER_CURSOR then context.top_line = context.cursor_pos[1] - 1 end diff --git a/bundle/hop.nvim/plugin/hop.vim b/bundle/hop.nvim/plugin/hop.vim index f51358de2..6a2d319c1 100644 --- a/bundle/hop.nvim/plugin/hop.vim +++ b/bundle/hop.nvim/plugin/hop.vim @@ -47,12 +47,18 @@ command! HopLineBC lua require'hop'.hint_lines({ direction = require'hop.hint'.H 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. +" The jump-to-line command (non-whitespace). 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 vertical command (line jump preserving the column cursor position). +command! HopVertical lua require'hop'.hint_vertical() +command! HopVerticalBC lua require'hop'.hint_vertical({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopVerticalAC lua require'hop'.hint_vertical({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopVerticalMW lua require'hop'.hint_vertical({ 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 })