1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-23 20:20:05 +08:00
SpaceVim/bundle/hop.nvim/lua/hop/init.lua

662 lines
19 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local M = {}
-- Ensure options are sound.
--
-- Some options cannot be used together. For instance, multi_windows and current_line_only dont really make sense used
-- together. This function will notify the user of such ill-formed configurations.
local function check_opts(opts)
if not opts then
return
end
if opts.multi_windows and opts.current_line_only then
vim.notify('Cannot use current_line_only across multiple windows', 3)
end
if vim.api.nvim_get_mode().mode ~= 'n' then
opts.multi_windows = false
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
-- Create hint state
--
-- {
-- all_ctxs: All windows's context
-- buf_list: All buffers displayed in all windows
-- <xxx>_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
-- A hack to prevent #57 by deleting twice the namespace (its 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 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
local end_col = nil
if direction == hint.HintDirection.AFTER_CURSOR then
start_col = cursor_pos[2]
elseif direction == hint.HintDirection.BEFORE_CURSOR then
end_line = bottom_line - 1
if cursor_pos[2] ~= 0 then end_col = cursor_pos[2] 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
local extmark_options = {
end_line = end_line,
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:
-- - 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 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
local cur_offset = cur_info[4]
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, {
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
-- 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('<Esc>', true, false, true)
local K_BS = vim.api.nvim_replace_termcodes('<BS>', true, false, true)
local K_C_H = vim.api.nvim_replace_termcodes('<C-H>', true, false, true)
local K_CR = vim.api.nvim_replace_termcodes('<CR>', true, false, true)
local K_NL = vim.api.nvim_replace_termcodes('<NL>', 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 <C-c>
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.
--
-- Add option to shift cursor by column offset
--
-- This function will update the jump list.
function M.move_cursor_to(w, line, column, hint_offset, direction)
-- If we do not ask for an offset jump, we dont 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 doesns 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]
-- 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 })
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.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
if not M.initialized then
vim.notify('Hop is not initialized; please call the setup function', 4)
return
end
-- 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 target_idx = nil
if jump_target_count == 0 then
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
target_idx = 1
end
if target_idx ~= nil then
local jt = generated.jump_targets[target_idx]
if jt then
callback(jt)
else
eprintln(' -> theres 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
hs.hints = hint.create_hints(generated.jump_targets, generated.indirect_jump_targets, 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
M.quit(hs)
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(key, hs, callback, opts)
vim.cmd('redraw')
else
-- If it's not, quit Hop
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
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(key, hint_state, callback, opts)
local hint = require'hop.hint'
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
clear_namespace(hint_state.buf_list, hint_state.hl_ns)
hint.set_hint_extmarks(hint_state.hl_ns, hints, opts)
else
M.quit(hint_state)
-- 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(hint_state)
clear_namespace(hint_state.buf_list, hint_state.hl_ns)
clear_namespace(hint_state.buf_list, hint_state.dim_ns)
-- 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; thats 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
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)
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.
local pat
if pattern then
pat = pattern
else
vim.cmd('redraw')
vim.fn.inputsave()
pat = M.get_input_pattern('Hop pattern: ', nil, opts)
vim.fn.inputrestore()
if not pat then return end
end
if #pat == 0 then
eprintln('-> empty pattern', opts.teasing)
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(pat, false, opts)),
opts
)
end
function M.hint_char1(opts)
local jump_target = require'hop.jump_target'
opts = override_opts(opts)
local c = M.get_input_pattern('Hop 1 char: ', 1)
if not c 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(c, true, opts)),
opts
)
end
function M.hint_char2(opts)
local jump_target = require'hop.jump_target'
opts = override_opts(opts)
local c = M.get_input_pattern('Hop 2 char: ', 2)
if not c 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(c, true, opts)),
opts
)
end
function M.hint_lines(opts)
local jump_target = require'hop.jump_target'
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.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
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)
local jump_target = require'hop.jump_target'
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 = require'hop.defaults'})
M.initialized = true
-- Insert the highlights and register the autocommand if asked to.
local highlight = require'hop.highlight'
highlight.insert_highlights()
if M.opts.create_hl_autocmd then
highlight.create_autocmd()
end
-- register Hop extensions, if any
if M.opts.extensions ~= nil then
for _, ext_name in pairs(opts.extensions) do
local ok, extension = pcall(require, ext_name)
if not ok then
-- 4 is error; thanks Neovim… :(
vim.notify(string.format('extension %s wasnt correctly loaded', ext_name), 4)
else
if extension.register == nil then
vim.notify(string.format('extension %s lacks the register function', ext_name), 4)
else
extension.register(opts)
end
end
end
end
end
return M