Read-only mirror of https://codeberg.org/andyg/leap.nvim
1-- Imports & aliases ///1
2
3local hl = require('leap.highlight')
4local opts = require('leap.opts')
5
6local set_beacons = require('leap.beacons').set_beacons
7local resolve_conflicts = require('leap.beacons').resolve_conflicts
8local light_up_beacons = require('leap.beacons').light_up_beacons
9
10local get_char = require('leap.input').get_char
11local get_char_keymapped = require('leap.input').get_char_keymapped
12
13local api = vim.api
14local keycode = vim.keycode
15-- Use these to handle multibyte characters.
16local split = vim.fn.split
17local lower = vim.fn.tolower
18local upper = vim.fn.toupper
19
20local abs = math.abs
21local ceil = math.ceil
22local floor = math.floor
23local min = math.min
24
25
26-- Utils ///1
27
28local function clamp(x, minval, maxval)
29 return (x < minval) and minval
30 or (x > maxval) and maxval
31 or x
32end
33
34local function echo(msg)
35 return api.nvim_echo({ { msg } }, false, {})
36end
37
38-- Returns to Normal mode and restores the cursor position.
39local function handle_interrupted_change_op()
40 -- |:help CTRL-\_CTRL-N|
41 local seq = keycode('<C-\\><C-N>' .. (vim.fn.col('.') > 1 and '<RIGHT>' or ''))
42 api.nvim_feedkeys(seq, 'n', true)
43end
44
45-- repeat.vim support (see the docs in the script:
46-- https://github.com/tpope/vim-repeat/blob/master/autoload/repeat.vim)
47local function set_dot_repeat_()
48 -- Note: We're not checking here whether the operation should be
49 -- repeated (see `set_dot_repeat()` in `leap()`).
50 local op = vim.v.operator
51 local force = vim.fn.mode(true):sub(3) -- v/V/ctrl-v
52 local leap = keycode('<cmd>lua require"leap".leap { dot_repeat = true }<cr>')
53 local seq = op .. force .. leap
54 if op == 'c' then
55 -- The (not-yet-inserted) replacement text will be available in
56 -- the '.' register once we're done (|:help quote.|).
57 seq = seq .. keycode('<c-r>.<esc>')
58 end
59 -- Using pcall, since vim-repeat might not be installed.
60 pcall(vim.fn['repeat#setreg'], seq, vim.v.register)
61 -- Note: we could feed [count] here, but instead we're saving
62 -- the value in our `state` table (`target_idx`).
63 pcall(vim.fn['repeat#set'], seq, -1)
64end
65
66-- Equivalence classes
67
68-- Return a char->equivalence-class lookup table.
69local function to_membership_lookup(eqv_classes)
70 local res = {}
71 for _, class in ipairs(eqv_classes) do
72 local class_tbl = (type(class) == 'string') and split(class, '\\zs') or class
73 for _, char in ipairs(class_tbl) do
74 res[char] = class_tbl
75 end
76 end
77 return res
78end
79
80-- We use this helper fn both for the search pattern and for the sublist
81-- keys, but 'smartcase' should only be considered in the search phase
82-- (see |:help leap-wildcard-problem|).
83local function get_equivalence_class(ch, consider_smartcase)
84 if
85 opts.case_sensitive -- deprecated
86 or not vim.go.ignorecase
87 or (consider_smartcase and vim.go.smartcase and lower(ch) ~= ch)
88 then
89 return opts.eqv_class_of[ch]
90 else
91 -- For collections in the search pattern, either is fine, since
92 -- 'ignorecase' applies here. With sublists, the contract is that
93 -- lowercase will be the representative character.
94 return opts.eqv_class_of[lower(ch)] or opts.eqv_class_of[upper(ch)]
95 end
96end
97
98local function get_representative_char(ch)
99 -- Choosing the first one from an equivalence class (arbitrary).
100 local eqclass = get_equivalence_class(ch)
101 ch = eqclass and eqclass[1] or ch
102 -- deprecated
103 return (opts.case_sensitive or not vim.go.ignorecase) and ch or lower(ch)
104end
105
106
107-- Search pattern ///1
108
109local function char_list_to_collection_str(chars)
110 local prepared = {
111 -- lua escape seqs (|:help lua-literal|)
112 ['\a'] = '\\a',
113 ['\b'] = '\\b',
114 ['\f'] = '\\f',
115 ['\n'] = '\\n',
116 ['\r'] = '\\r',
117 ['\t'] = '\\t',
118 ['\v'] = '\\v',
119 ['\\'] = '\\\\',
120 -- vim collection magic chars (|:help /collection|)
121 [']'] = '\\]',
122 ['^'] = '\\^',
123 ['-'] = '\\-',
124 }
125 return table.concat(vim.tbl_map(function(ch)
126 return prepared[ch] or ch
127 end, chars))
128end
129
130local function expand(char) -- <= 'a'
131 local lst = get_equivalence_class(char, true) or { char } -- => {'a','á','ä'}
132 return char_list_to_collection_str(lst) -- => 'aáä'
133end
134
135--- Transforms user input to the appropriate search pattern.
136---
137--- NOTE: If preview (two-step processing) is enabled, for any kind of
138--- input mapping (case-insensitivity, character classes, etc.) we need
139--- to tweak things in two different places:
140--- 1. For the first input, we can modify the search pattern itself.
141--- 2. The second input is only acquired once the search is done; for
142--- that, we need to play with the sublist keys
143---
144---@see populate_sublists
145---@param in1 string
146---@param in2? string
147---@param inputlen integer
148---
149local function prepare_pattern(in1, in2, inputlen)
150 local prefix = '\\V'
151 -- deprecated
152 if opts.case_sensitive == true then prefix = prefix .. '\\C' end
153 if opts.case_sensitive == false then prefix = prefix .. '\\c' end
154 if vim.fn.mode(1):match('V') then
155 -- Skip the current line in linewise modes. (Hardcode the number,
156 -- we might set the cursor before starting the search.)
157 local lnum = vim.fn.line('.')
158 prefix = prefix .. '\\(\\%<' .. lnum .. 'l\\|\\%>' .. lnum .. 'l\\)'
159 end
160
161 -- Two other convenience features:
162 -- 1. Same-character pairs (==) match longer sequences (=====) only
163 -- at the beginning.
164 -- 2. EOL can be matched by typing a newline alias twice. (See also
165 -- `populate_sublists()`.)
166
167 -- (Mind the order, format() interpolates nil without complaint.)
168 local in1_ = expand(in1) -- 'a'=>'aáä'
169 local p1 = ('\\[%s]'):format(in1_)
170 local notp1 = ('\\[^%s]'):format(in1_)
171 local p2 = in2 and ('\\[%s]'):format(expand(in2))
172 local notp1_or_eol = ([[\(%s\|\$\)]]):format(notp1)
173 local first_p1 = ([[\(\^\|%s\)\zs%s]]):format(notp1, p1)
174
175 local pattern
176 if p2 and (p1 ~= p2) then
177 pattern = p1 .. p2
178 elseif p2 and (p1 == p2) then
179 -- To implement [2], add `$` as another branch if `p1` might
180 -- represent newline.
181 pattern = first_p1 .. p1 .. (p1:match('\\n') and '\\|\\$' or '')
182 elseif inputlen == 2 then
183 -- Preview; now [2] will be handled by `populate_sublists()`, but
184 -- first we need to match `p1$` here.
185 pattern = first_p1 .. p1 .. '\\|' .. p1 .. notp1_or_eol
186 elseif inputlen == 1 then
187 pattern = first_p1 .. '\\|' .. p1 .. '\\ze' .. notp1_or_eol
188 end
189
190 return prefix .. '\\(' .. pattern .. '\\)'
191end
192
193
194-- Processing targets ///1
195
196--- Populates a sub-table in `targets` containing lists that allow for
197--- easy iteration through each subset of targets with a given successor
198--- char.
199---
200--- from:
201--- { T1, T2, T3, T4 }
202--- xa xb xa xc
203--- to:
204--- {
205--- T1, T2, T3, T4,
206--- sublists = {
207--- a = { T1, T3 },
208--- b = { T2 },
209--- c = { T4 },
210--- }
211--- }
212---
213---@see prepare_pattern
214local function populate_sublists(targets)
215 -- Setting a metatable to handle case insensitivity and equivalence
216 -- classes (in both cases: multiple keys -> one value).
217 -- If `ch` is not found, try to get a sublist belonging to some
218 -- common key: the equivalence class that `ch` belongs to (if there
219 -- is one), or, if case insensivity is set, the lowercased verison of
220 -- `ch`. (And in the above cases, `ch` will not be found, since we
221 -- also redirect to the common keys when inserting a new sublist.)
222 targets.sublists = setmetatable({}, {
223 __index = function(self, ch)
224 return rawget(self, get_representative_char(ch))
225 end,
226 __newindex = function(self, ch, sublist)
227 rawset(self, get_representative_char(ch), sublist)
228 end,
229 })
230 -- Filling the sublists.
231 for _, target in ipairs(targets) do
232 local ch1, ch2 = unpack(target.chars)
233 -- Handle newline matches. (See `prepare_pattern()` too.)
234 local key = (ch1 == '' or ch2 == '') and '\n' or ch2
235 if not targets.sublists[key] then
236 targets.sublists[key] = {}
237 end
238 table.insert(targets.sublists[key], target)
239 end
240end
241
242--- Returns a modified copy of the provided label list, that allows
243--- safe traversal.
244local function as_traversable(labels, prepend_next_key)
245 if #labels == 0 then return labels end
246
247 local keys = opts.keys
248
249 -- Remove the traversal keys if they appear on the label list.
250 local bad_keys = ''
251 for _, key in ipairs { keys.next_target, keys.prev_target } do
252 bad_keys = bad_keys .. (
253 type(key) == 'table'
254 and table.concat(vim.tbl_map(keycode, key))
255 or keycode(key)
256 )
257 end
258 local traversable = labels:gsub('[' .. bad_keys .. ']', '')
259
260 if prepend_next_key then
261 -- Use the "unsafe" next-key (|:help leap-clever-repeat|) as the
262 -- first label, if adequate, since its effect is equivalent.
263 local key_next = (type(keys.next_target) == 'table') and keys.next_target[2]
264 -- Prepend if actually displayable.
265 if key_next and keycode(key_next) == key_next and key_next:match('%S') then
266 traversable = key_next .. traversable
267 end
268 end
269
270 return traversable
271end
272
273local function all_in_the_same_window(targets)
274 if not targets[1].wininfo then return true end
275 local win = targets[1].wininfo.winid
276 -- Iterate backwards to get at least a negative answer in O(1), in
277 -- case the targets are ordered by window (which is almost certainly
278 -- true, but let's not enforce it as a contract by checking only the
279 -- last item here).
280 for i = #targets, 1, -1 do
281 if targets[i].wininfo.winid ~= win then
282 return false
283 end
284 end
285 return true
286end
287
288-- Problem:
289-- xy target #1
290-- xyL target #2 (labeled)
291-- ^ auto-jump would move the cursor here (covering the label)
292--
293-- Note: The situation implies backward search, and may arise in phase
294-- two, when only the chosen sublist remained.
295--
296-- Caveat: this case in fact depends on the label position, for which
297-- the `beacons` module is responsible (e.g. the label is on top of the
298-- match when repeating), but we're not considering that, and just err
299-- on the safe side instead of complicating the code.
300local function cursor_would_cover_the_first_label_on_autojump(targets)
301 local t1, t2 = targets[1], targets[2]
302 if t2 and t2.chars and not t2.is_offscreen then
303 local line1, col1 = unpack(t1.pos)
304 local line2, col2 = unpack(t2.pos)
305 return line1 == line2 and col1 == (col2 + #table.concat(t2.chars))
306 end
307end
308
309local function count_onscreen(targets)
310 local count = 0
311 for _, target in ipairs(targets) do
312 if not target.is_offscreen then count = count + 1 end
313 end
314 return count
315end
316
317---@param targets table
318---@param kwargs table
319local function prepare_labeled_targets(targets, kwargs)
320 local can_traverse = kwargs.can_traverse
321 local force_noautojump = kwargs.force_noautojump
322 local multi_windows = kwargs.multi_windows
323
324 local labels, safe_labels = opts.labels, opts.safe_labels
325 if can_traverse then
326 -- Stricter than necessary, for the sake of simplicity
327 -- (we haven't decided about autojump yet).
328 local prepend_next_key = not (
329 targets[1].is_offscreen
330 or targets[2] and targets[2].is_offscreen
331 )
332 labels, safe_labels =
333 as_traversable(opts.labels, prepend_next_key),
334 as_traversable(opts.safe_labels, prepend_next_key)
335 end
336
337 -- Sets a flag indicating whether we can automatically jump to the
338 -- first target, without having to select a label.
339 local function set_autojump()
340 if not (
341 force_noautojump
342 or #safe_labels == 0
343 -- Prevent shifting the viewport (we might want to select a label).
344 or targets[1].is_offscreen and #targets > 1
345 -- Problem: We are autojumping to some position in window A, but
346 -- our chosen labeled target happens to be in window B - in that
347 -- case we do not actually want to reposition the cursor in window
348 -- A. Restoring it afterwards would be overcomplicated, not to
349 -- mention that the jump itself is disorienting, especially
350 -- A->B->C (autojumping to B, before moving to C).
351 or multi_windows and not all_in_the_same_window(targets)
352 or cursor_would_cover_the_first_label_on_autojump(targets)
353 ) then
354 -- Forced or "smart" autojump, respectively.
355 targets.autojump = (#labels == 0)
356 or count_onscreen(targets) <= #safe_labels + 1
357 end
358 end
359
360 local function attach_label_set()
361 -- Note that `labels` => no `autojump`, and `autojump` =>
362 -- `safe_labels`, but the converse statmenets do not hold.
363 targets.label_set =
364 #labels == 0 and safe_labels
365 or #safe_labels == 0 and labels
366 or targets.autojump and safe_labels
367 or labels
368 end
369
370 -- Assigns a label to each target, by repeating the label set
371 -- indefinitely, and registers the number of the label group the
372 -- target is part of.
373 -- Note that these are once-and-for-all fixed attributes, regardless
374 -- of the actual UI state ('beacons').
375 local function set_labels()
376 -- We need to handle multibyte chars anyway, it's better to create
377 -- a table than calling `strcharpart()` for each access.
378 local labelset = split(targets.label_set, '\\zs')
379 local len_labelset = #labelset
380 local skipped = targets.autojump and 1 or 0
381 for i = (skipped + 1), #targets do
382 local target = targets[i]
383 if target then
384 local ii = i - skipped
385 if target.is_offscreen then
386 skipped = skipped + 1
387 else
388 local mod = ii % len_labelset
389 if mod == 0 then
390 target.label = labelset[len_labelset]
391 target.group = floor(ii / len_labelset)
392 else
393 target.label = labelset[mod]
394 target.group = floor(ii / len_labelset) + 1
395 end
396 end
397 end
398 end
399 end
400
401 -- Note: The three depend on each other, in this order.
402 set_autojump()
403 attach_label_set()
404 set_labels()
405end
406
407local function normalize_directional_indexes(targets)
408 -- Like: -7 -4 -2 | 1 3 7
409 -- => -3 -2 -1 | 1 2 3
410 local backward, forward = {}, {}
411 for _, t in ipairs(targets) do
412 table.insert(t.idx < 0 and backward or forward, t.idx)
413 end
414 table.sort(backward, function(x, y) return x > y end) -- {-2,-4,-7}
415 table.sort(forward) -- {1,3,7}
416
417 local new_idx = {}
418 for i, idx in ipairs(backward) do new_idx[idx] = -i end
419 for i, idx in ipairs(forward) do new_idx[idx] = i end
420
421 for _, t in ipairs(targets) do
422 t.idx = new_idx[t.idx]
423 end
424end
425
426
427-- Leap ///1
428
429-- State persisted between invocations.
430local state = {
431 ['repeat'] = {
432 in1 = nil,
433 in2 = nil,
434 pattern = nil,
435 -- For when wanting to repeat in relative direction
436 -- (for outside use only).
437 backward = nil,
438 inclusive = nil,
439 offset = nil,
440 inputlen = nil,
441 opts = nil
442 },
443 dot_repeat = {
444 targets = nil,
445 pattern = nil,
446 in1 = nil,
447 in2 = nil,
448 target_idx = nil,
449 backward = nil,
450 inclusive = nil,
451 offset = nil,
452 inputlen = nil,
453 opts = nil
454 },
455 -- We also use this table to make the argumens passed to `leap()`
456 -- accessible for autocommands (using event data would be cleaner,
457 -- but it is far too problematic, as it cannot handle tables with
458 -- mixed keys, metatables, function values, etc.).
459 args = nil,
460}
461
462--- Entry point for Leap motions.
463local function leap(kwargs)
464 -- Handling deprecated field names.
465 -- Note: Keep the legacy fields too, do not break user autocommands.
466 if kwargs.target_windows then kwargs.windows = kwargs.target_windows end
467 if kwargs.inclusive_op then kwargs.inclusive = kwargs.inclusive_op end
468
469 state.args = kwargs
470
471 local invoked_repeat = kwargs['repeat']
472 local invoked_dot_repeat = kwargs.dot_repeat
473 local windows = kwargs.windows
474 local user_given_opts = kwargs.opts
475 local user_given_targets = kwargs.targets
476 local user_given_action = kwargs.action
477 local action_can_traverse = kwargs.traversal
478
479 local dot_repeatable =
480 invoked_dot_repeat and state.dot_repeat
481 or kwargs
482
483 local is_backward = dot_repeatable.backward
484
485 local repeatable =
486 invoked_dot_repeat and state.dot_repeat
487 or invoked_repeat and state['repeat']
488 or kwargs
489
490 local is_inclusive = repeatable.inclusive
491 local offset = repeatable.offset
492 local user_given_inputlen = repeatable.inputlen
493 local user_given_pattern = repeatable.pattern
494
495 -- Chores to do before accessing `opts`.
496 do
497 -- `opts` hierarchy: current >> saved-for-repeat >> default
498
499 -- We might want to give specific arguments exclusively for
500 -- repeats - see e.g. `user.set_repeat_keys()` - so we merge the
501 -- saved values.
502 -- From here on we let the metatable of `opts` dispatch between
503 -- `current_call` vs `default` though.
504 opts.current_call =
505 not user_given_opts and {}
506 or invoked_repeat and vim.tbl_deep_extend(
507 'keep', user_given_opts, state['repeat'].opts or {}
508 )
509 or invoked_dot_repeat and vim.tbl_deep_extend(
510 'keep', user_given_opts, state.dot_repeat.opts or {}
511 )
512 or user_given_opts
513
514 if opts.current_call.equivalence_classes then
515 opts.current_call.eqv_class_of = setmetatable(
516 to_membership_lookup(opts.current_call.equivalence_classes),
517 -- Prevent merging with the defaults, as this is derived
518 -- programmatically from a list-like option (see `opts.lua`).
519 { merge = false }
520 )
521 end
522
523 -- Force the label lists into strings (table support is deprecated).
524 for _, tbl in ipairs { 'default', 'current_call' } do
525 for _, key in ipairs { 'labels', 'safe_labels' } do
526 if type(opts[tbl][key]) == 'table' then
527 opts[tbl][key] = table.concat(opts[tbl][key])
528 end
529 end
530 end
531 end
532
533 local is_directional = not windows
534 local no_labels_to_use = #opts.labels == 0 and #opts.safe_labels == 0
535
536 if not is_directional and no_labels_to_use then
537 echo('no labels to use')
538 return
539 end
540 if windows and #windows == 0 then
541 echo('no targetable windows')
542 return
543 end
544
545 -- We need to save the mode here, because the `:normal` command in
546 -- `jump.jump_to()` can change the state. See vim/vim#9332.
547 local mode = api.nvim_get_mode().mode
548 local is_visual_mode = mode:match('^[vV\22]')
549 local is_op_mode = mode:match('o')
550 local is_change_op = is_op_mode and (vim.v.operator == 'c')
551
552 local count
553 if is_directional then
554 if vim.v.count ~= 0 then
555 count = vim.v.count
556 elseif is_op_mode and no_labels_to_use then
557 count = 1
558 end
559 end
560
561 local is_keyboard_input = not (
562 invoked_repeat
563 or invoked_dot_repeat
564 or user_given_inputlen == 0
565 or type(user_given_pattern) == 'string'
566 or user_given_targets
567 )
568 local inputlen = user_given_inputlen or (is_keyboard_input and 2) or 0
569
570 -- Force the values in `opts.keys` into a table, and translate keycodes.
571 -- Using a metatable instead of deepcopy, in case one would modify
572 -- the entries on `LeapEnter` (or even later).
573 local keys = setmetatable({}, {
574 __index = function(_, k)
575 local v = opts.keys[k]
576 return vim.tbl_map(keycode, type(v) == 'string' and { v } or v)
577 end
578 })
579
580 -- The first key on a `keys` list is considered "safe"
581 -- (not to be used as search input).
582 local contains = vim.list_contains
583 local contains_first = function(t, v) return t[1] == v end
584
585 -- Ephemeral state (of the current call) that is not interesting
586 -- for the outside world.
587 local st = {
588 -- Multi-phase processing (show preview)?
589 phase =
590 is_keyboard_input
591 and inputlen == 2
592 and not no_labels_to_use
593 and opts.preview ~= false
594 and 1
595 or nil,
596 -- When repeating a `{char}<enter>` search (started to traverse
597 -- after the first input).
598 repeating_shortcut = false,
599 -- For traversal mode.
600 curr_idx = 0,
601 -- Currently selected label group, 0-indexed
602 -- (`target.group` starts at 1).
603 group_offset = 0,
604 -- For getting keymapped input (see `input.lua`).
605 prompt = nil,
606 errmsg = nil,
607 }
608
609 local function exec_user_autocmds(pattern)
610 api.nvim_exec_autocmds('User', { pattern = pattern, modeline = false })
611 end
612
613 local exit
614 do
615 local exited
616 exit = function()
617 exec_user_autocmds('LeapLeave')
618 assert(not exited)
619 exited = true
620 end
621 end
622
623 local function exit_early()
624 if is_change_op then handle_interrupted_change_op() end
625 if st.errmsg then echo(st.errmsg) end
626 exit()
627 end
628
629 local function with_redraw(callback)
630 exec_user_autocmds('LeapRedraw')
631 -- Should be called after `LeapRedraw` - the idea is that callbacks
632 -- clean up after themselves on that event (next time, that is).
633 if callback then callback() end
634 vim.cmd.redraw()
635 -- Should be after `redraw()`, to avoid a flickering effect
636 -- when jumping directly onto a label.
637 pcall(api.nvim__redraw, { cursor = true }) -- EXPERIMENTAL API
638 end
639
640 local function can_traverse(targets)
641 return action_can_traverse or (
642 is_directional
643 and not (count or is_op_mode or user_given_action)
644 and #targets >= 2
645 )
646 end
647
648 -- When traversing without labels, keep highlighting the same one
649 -- group of targets, and do not shift until reaching the end of the
650 -- group - it is less disorienting if the "snake" does not move
651 -- continuously, on every jump.
652 local function get_number_of_highlighted_traversal_targets()
653 local group_size = opts.max_highlighted_traversal_targets or 10 -- deprecated
654 -- Assumption: being here means we are after an autojump, and
655 -- started highlighting from the 2nd target (no `count`). Thus, we
656 -- can use `st.curr-idx` as the reference, instead of some
657 -- separate counter (but only because of the above).
658 local consumed = (st.curr_idx - 1) % group_size
659 local remaining = group_size - consumed
660 -- Switch just before the whole group gets eaten up.
661 return remaining <= 1 and (remaining + group_size) or remaining
662 end
663
664 local function get_highlighted_idx_range(targets, use_no_labels)
665 if use_no_labels and (opts.max_highlighted_traversal_targets == 0) then -- deprecated
666 return 0, -1 -- empty range
667 else
668 local n = get_number_of_highlighted_traversal_targets()
669 local start = st.curr_idx + 1
670 local _end = use_no_labels and min(start - 1 + n, #targets)
671 return start, _end
672 end
673 end
674
675 local function get_target_with_active_label(targets, input)
676 for idx, target in ipairs(targets) do
677 if target.label then
678 local relative_group = target.group - st.group_offset
679 if relative_group > 1 then -- beyond the active group
680 return
681 end
682 if relative_group == 1 and target.label == input then
683 return target, idx
684 end
685 end
686 end
687 end
688
689 -- Get inputs
690
691 local function get_repeat_input()
692 if not state['repeat'].in1 then
693 st.errmsg = 'no previous search'
694 return
695 else
696 if inputlen == 1 then
697 return state['repeat'].in1
698 elseif inputlen == 2 then
699 if not state['repeat'].in2 then
700 st.repeating_shortcut = true
701 end
702 return state['repeat'].in1, state['repeat'].in2
703 end
704 end
705 end
706
707 local function get_first_pattern_input()
708 with_redraw()
709 local in1, prompt = get_char_keymapped(st.prompt)
710 if in1 then
711 if contains_first(keys.next_target, in1) then
712 st.phase = nil
713 return get_repeat_input()
714 else
715 st.prompt = prompt
716 return in1
717 end
718 end
719 end
720
721 local function get_second_pattern_input(targets)
722 -- Note: `count` does _not_ automatically disable two-phase
723 -- processing altogether, as we might want to do a char<enter>
724 -- shortcut, but it implies not needing to show beacons.
725 if not count then
726 with_redraw(function() light_up_beacons(targets) end)
727 end
728 return get_char_keymapped(st.prompt)
729 end
730
731 local function get_full_pattern_input()
732 local in1, in2 = get_first_pattern_input()
733 if in1 and in2 then
734 return in1, in2
735 elseif in1 then
736 if inputlen == 1 then
737 return in1
738 else
739 local in2_ = get_char_keymapped(st.prompt)
740 if in2_ then
741 return in1, in2_
742 end
743 end
744 end
745 end
746
747 -- Get targets
748
749 ---@param pattern string
750 ---@param in1? string
751 ---@param in2? string
752 local function get_targets(pattern, in1, in2)
753 local errmsg = in1 and ('not found: ' .. in1 .. (in2 or '')) or 'no targets'
754 local targets = require('leap.search').get_targets(pattern, {
755 is_backward = is_backward,
756 windows = windows,
757 offset = offset,
758 is_op_mode = is_op_mode,
759 inputlen = inputlen,
760 })
761 if not targets then
762 st.errmsg = errmsg
763 return
764 else
765 return targets
766 end
767 end
768
769 local function get_user_given_targets(targets_)
770 local default_errmsg = 'no targets'
771 local targets, errmsg = (type(targets_) == 'function') and targets_() or targets_
772 if not targets or #targets == 0 then
773 st.errmsg = errmsg or default_errmsg
774 return
775 else
776 -- Fill wininfo-s when not provided.
777 if not targets[1].wininfo then
778 local wininfo = vim.fn.getwininfo(api.nvim_get_current_win())[1]
779 for _, t in ipairs(targets) do
780 t.wininfo = wininfo
781 end
782 end
783 return targets
784 end
785 end
786
787 --- Sets `autojump` and `label_set` attributes for the target list,
788 --- plus `label` and `group` attributes for each individual target.
789 ---@param targets table
790 local function prepare_labeled_targets_(targets)
791 prepare_labeled_targets(targets, {
792 can_traverse = can_traverse(targets),
793 force_noautojump =
794 not action_can_traverse
795 and (
796 -- No jump, doing sg else.
797 user_given_action
798 -- Should be able to select our target.
799 or is_op_mode and #targets > 1
800 ),
801 multi_windows = windows and #windows > 1,
802 })
803 end
804
805 -- Repeat
806
807 local repeat_state = {
808 offset = kwargs.offset,
809 backward = kwargs.backward,
810 inclusive = kwargs.inclusive,
811 pattern = kwargs.pattern,
812 inputlen = inputlen,
813 opts = vim.deepcopy(opts.current_call),
814 }
815
816 local function update_repeat_state(in1, in2)
817 if
818 not (invoked_repeat or invoked_dot_repeat)
819 and (is_keyboard_input or user_given_pattern)
820 then
821 state['repeat'] = vim.tbl_extend('error', repeat_state, {
822 in1 = is_keyboard_input and in1,
823 in2 = is_keyboard_input and in2,
824 })
825 end
826 end
827
828 local function set_dot_repeat(in1, in2, target_idx)
829 local function update_dot_repeat_state()
830 state.dot_repeat = vim.tbl_extend('error', repeat_state, {
831 target_idx = target_idx,
832 targets = user_given_targets,
833 in1 = is_keyboard_input and in1,
834 in2 = is_keyboard_input and in2,
835 })
836 if not is_directional then
837 state.dot_repeat.backward = target_idx < 0
838 state.dot_repeat.target_idx = abs(target_idx)
839 end
840 end
841
842 local is_dot_repeatable_op =
843 is_op_mode and (vim.o.cpo:match('y') or vim.v.operator ~= 'y')
844
845 local is_dot_repeatable_call =
846 is_dot_repeatable_op and not invoked_dot_repeat
847 and type(user_given_targets) ~= 'table'
848
849 if is_dot_repeatable_call then
850 update_dot_repeat_state()
851 set_dot_repeat_()
852 end
853 end
854
855 -- Jump
856
857 local jump_to
858 do
859 local is_first_jump = true
860 jump_to = function(target)
861 require('leap.jump').jump_to(target.pos, {
862 add_to_jumplist = is_first_jump,
863 win = target.wininfo.winid,
864 mode = mode,
865 offset = offset,
866 is_backward = is_backward or (target.idx and target.idx < 0),
867 is_inclusive = is_inclusive,
868 })
869 is_first_jump = false
870 end
871 end
872
873 local do_action = user_given_action or jump_to
874
875 -- Post-pattern loops
876
877 local function select(targets)
878 local n_groups = targets.label_set and ceil(#targets / #targets.label_set) or 0
879
880 local function display()
881 local use_no_labels = no_labels_to_use or st.repeating_shortcut
882 -- Do _not_ skip this on initial invocation - we might have
883 -- skipped setting the initial label states if using
884 -- `keys.next_target`.
885 set_beacons(targets, {
886 group_offset = st.group_offset,
887 phase = st.phase,
888 use_no_labels = use_no_labels
889 })
890 local start, _end = get_highlighted_idx_range(targets, use_no_labels)
891 with_redraw(function()
892 light_up_beacons(targets, start, _end)
893 end)
894 end
895
896 local function loop(is_first_invoc)
897 display()
898 if is_first_invoc then
899 exec_user_autocmds('LeapSelectPre')
900 end
901 local input = get_char()
902 if input then
903 local shift =
904 contains(keys.next_group, input) and 1
905 or (contains(keys.prev_group, input) and not is_first_invoc) and -1
906
907 if shift and n_groups > 1 then
908 st.group_offset = clamp(st.group_offset + shift, 0, n_groups - 1)
909 return loop(false)
910 else
911 return input
912 end
913 end
914 end
915
916 return loop(true)
917 end
918
919 ---@return integer?
920 local function traversal_get_new_idx(idx, in_, targets)
921 if contains(keys.next_target, in_) then
922 return min(idx + 1, #targets)
923 elseif contains(keys.prev_target, in_) then
924 -- Wrap around backwards (useful for treesitter selection).
925 return (idx <= 1) and #targets or (idx - 1)
926 end
927 end
928
929 local function traverse(targets, start_idx, use_no_labels)
930 local function on_first_invoc()
931 if use_no_labels then
932 for _, t in ipairs(targets) do
933 t.label = nil
934 end
935 else
936 -- Remove subsequent label groups.
937 for _, target in ipairs(targets) do
938 if target.group and target.group > 1 then
939 target.label = nil
940 target.beacon = nil
941 end
942 end
943 end
944 end
945
946 local function display()
947 set_beacons(targets, {
948 group_offset = st.group_offset,
949 phase = st.phase,
950 use_no_labels = use_no_labels,
951 })
952 local start, _end = get_highlighted_idx_range(targets, use_no_labels)
953 with_redraw(function()
954 light_up_beacons(targets, start, _end)
955 end)
956 end
957
958 local function loop(idx, is_first_invoc)
959 if is_first_invoc then on_first_invoc() end
960 st.curr_idx = idx
961 display() -- needs st.curr_idx
962
963 local input = get_char()
964 if input then
965 local new_idx = traversal_get_new_idx(idx, input, targets)
966 if new_idx then
967 do_action(targets[new_idx])
968 loop(new_idx, false)
969 else
970 -- We still want the labels (if there are) to function.
971 local target, new_idx = get_target_with_active_label(targets, input)
972 if target then
973 do_action(target)
974 -- Especially for treesitter selection, make it easier to correct.
975 if is_visual_mode then
976 loop(new_idx, false)
977 end
978 else
979 vim.fn.feedkeys(input, 'i')
980 end
981 end
982 end
983 end
984
985 loop(start_idx, true)
986 end
987
988 -- After all the stage-setting, here comes the main action you've all
989 -- been waiting for:
990
991 exec_user_autocmds('LeapEnter')
992
993 local need_in1 =
994 is_keyboard_input
995 or (
996 invoked_repeat
997 and type(state['repeat'].pattern) ~= 'string'
998 and state['repeat'].inputlen ~= 0
999 )
1000 or (
1001 invoked_dot_repeat
1002 and type(state.dot_repeat.pattern) ~= 'string'
1003 and state.dot_repeat.inputlen ~= 0
1004 and not state.dot_repeat.targets
1005 )
1006
1007 local in1, in2
1008
1009 if need_in1 then
1010 if is_keyboard_input then
1011 if st.phase then
1012 -- This might call `get_repeat_input()`, and also return `in2`,
1013 -- if using `keys.next_target`.
1014 in1, in2 = get_first_pattern_input() -- REDRAW
1015 else
1016 in1, in2 = get_full_pattern_input() -- REDRAW
1017 end
1018 elseif invoked_repeat then
1019 in1, in2 = get_repeat_input()
1020 elseif invoked_dot_repeat then
1021 in1, in2 = state.dot_repeat.in1, state.dot_repeat.in2
1022 end
1023
1024 if not in1 then
1025 return exit_early()
1026 end
1027 end
1028
1029 local targets
1030 do
1031 local user_given_targets_ =
1032 user_given_targets or (invoked_dot_repeat and state.dot_repeat.targets)
1033
1034 if user_given_targets_ then
1035 targets = get_user_given_targets(user_given_targets_)
1036 else
1037 local user_given_pattern_ =
1038 user_given_pattern
1039 or invoked_repeat and state['repeat'].pattern
1040 or invoked_dot_repeat and state.dot_repeat.pattern
1041
1042 local pattern
1043 if type(user_given_pattern_) == 'string' then
1044 pattern = user_given_pattern_
1045 elseif type(user_given_pattern_) == 'function' then
1046 local prepared = in1 and prepare_pattern(in1, in2, inputlen) or ''
1047 pattern = user_given_pattern_(prepared, { in1, in2 })
1048 else
1049 pattern = prepare_pattern(in1, in2, inputlen)
1050 end
1051
1052 targets = get_targets(pattern, in1, in2)
1053 end
1054
1055 if not targets then
1056 return exit_early()
1057 end
1058 end
1059
1060 if invoked_dot_repeat then
1061 local target = targets[state.dot_repeat.target_idx]
1062 if target then
1063 do_action(target)
1064 return exit()
1065 else
1066 return exit_early()
1067 end
1068 end
1069
1070 if st.phase then
1071 -- Show preview.
1072 populate_sublists(targets)
1073 for _, sublist in pairs(targets.sublists) do
1074 prepare_labeled_targets_(sublist)
1075 set_beacons(sublist, { phase = st.phase })
1076 end
1077 resolve_conflicts(targets)
1078 else
1079 local use_no_labels = no_labels_to_use or st.repeating_shortcut
1080 if use_no_labels then
1081 targets.autojump = true
1082 else
1083 prepare_labeled_targets_(targets)
1084 end
1085 end
1086
1087 local need_in2 = (inputlen == 2) and not (in2 or st.repeating_shortcut)
1088
1089 if need_in2 then
1090 in2 = get_second_pattern_input(targets) -- REDRAW
1091
1092 if not in2 then
1093 return exit_early()
1094 end
1095 end
1096
1097 if st.phase then st.phase = 2 end
1098
1099 -- Jump eagerly to the first/count-th match on the whole unfiltered
1100 -- target list?
1101 local is_shortcut =
1102 st.repeating_shortcut or contains_first(keys.next_target, in2)
1103
1104 -- Do this now - repeat can succeed, even if we fail this time.
1105 update_repeat_state(in1, not is_shortcut and in2 or nil)
1106
1107 if is_shortcut then
1108 local n = count or 1
1109 local target = targets[n]
1110 if not target then
1111 return exit_early()
1112 end
1113 -- Do this before `do_action()`, because it might erase forced motion.
1114 -- (The `:normal` command in `jump.jump_to()` can change the state
1115 -- of `mode()`. See vim/vim#9332.)
1116 set_dot_repeat(in1, nil, target.idx or n)
1117 do_action(target)
1118 if can_traverse(targets) then
1119 traverse(targets, 1, true)
1120 end
1121 return exit()
1122 end
1123
1124 exec_user_autocmds('LeapPatternPost')
1125
1126 -- Get the sublist for `in2`, and work with that from here on (except
1127 -- if we've been given custom targets).
1128 local targets2
1129 do
1130 -- (Mind the logic, do not fall back to `targets`.)
1131 targets2 = (not targets.sublists) and targets or targets.sublists[in2]
1132 if not targets2 then
1133 -- Note: at this point, `in2` might only be nil if
1134 -- `st.repeating_shortcut` is true; that case implies there are
1135 -- no sublists, and there _are_ targets.
1136 st.errmsg = ('not found: ' .. in1 .. in2)
1137 return exit_early()
1138 end
1139 if (targets2 ~= targets) and targets2[1].idx then
1140 normalize_directional_indexes(targets2) -- for dot-repeat
1141 end
1142 end
1143
1144 local function exit_with_action_on(idx)
1145 local target = targets2[idx]
1146 set_dot_repeat(in1, in2, target.idx or idx)
1147 do_action(target)
1148 exit()
1149 end
1150
1151 if count then
1152 if count > #targets2 then
1153 return exit_early()
1154 else
1155 return exit_with_action_on(count)
1156 end
1157 elseif invoked_repeat and not can_traverse(targets2) then
1158 return exit_with_action_on(1)
1159 end
1160
1161 if targets2.autojump then
1162 if #targets2 == 1 then
1163 return exit_with_action_on(1)
1164 else
1165 do_action(targets2[1])
1166 st.curr_idx = 1
1167 end
1168 end
1169
1170 local in_final = select(targets2) -- REDRAW (LOOP)
1171 if not in_final then
1172 return exit_early()
1173 end
1174
1175 -- Traversal - `prev_target` can also start it, wrapping backwards.
1176 if
1177 (
1178 contains(keys.next_target, in_final)
1179 or contains(keys.prev_target, in_final)
1180 )
1181 and can_traverse(targets2)
1182 and st.group_offset == 0
1183 then
1184 local use_no_labels =
1185 no_labels_to_use
1186 or st.repeating_shortcut
1187 or not targets2.autojump
1188 -- Note: `traverse` will set `st.curr-idx` to `new-idx`.
1189 local new_idx = traversal_get_new_idx(st.curr_idx, in_final, targets2)
1190 do_action(targets2[new_idx])
1191 traverse(targets2, new_idx, use_no_labels) -- REDRAW (LOOP)
1192 return exit()
1193 end
1194
1195 -- `next_target` accepts the first match if the cursor hasn't moved yet
1196 -- (no autojump).
1197 if
1198 contains(keys.next_target, in_final)
1199 and st.curr_idx == 0
1200 and st.group_offset == 0
1201 then
1202 return exit_with_action_on(1)
1203 end
1204
1205 -- Otherwise try to get a labeled target, and if no success, feed the key.
1206 local target, idx = get_target_with_active_label(targets2, in_final)
1207 if target and idx then
1208 if is_visual_mode and can_traverse(targets2) then
1209 do_action(targets2[idx])
1210 traverse(targets2, idx) -- REDRAW (LOOP)
1211 return exit()
1212 else
1213 return exit_with_action_on(idx)
1214 end
1215 else
1216 vim.fn.feedkeys(in_final, 'i')
1217 return exit()
1218 end
1219end
1220
1221
1222-- Init ///1
1223
1224local function init_highlight()
1225 hl:init()
1226 -- Colorscheme plugins might clear out our highlight definitions,
1227 -- without defining their own, so we re-init the highlight on every
1228 -- change.
1229 return api.nvim_create_autocmd('ColorScheme', {
1230 group = 'LeapDefault',
1231 -- Wrap it - do _not_ pass on event data as argument.
1232 callback = function(_) hl:init() end,
1233 })
1234end
1235
1236local function manage_vim_opts()
1237 local get_opt = api.nvim_get_option_value
1238 local set_opt = api.nvim_set_option_value
1239 local saved_vim_opts = {}
1240
1241 local function set_vim_opts(user_vim_opts)
1242 saved_vim_opts = {}
1243
1244 local windows =
1245 state.args.windows
1246 or state.args.target_windows -- deprecated
1247 or { api.nvim_get_current_win() }
1248
1249 for opt, val in pairs(user_vim_opts) do -- e.g.: `wo.scrolloff = 0`
1250 local scope, name = unpack(vim.split(opt, '.', { plain = true }))
1251 if scope == 'wo' then
1252 for _, win in ipairs(windows) do
1253 local saved_val = get_opt(name, { scope = 'local', win = win })
1254 saved_vim_opts[{ 'wo', win, name }] = saved_val
1255 local new_val = val
1256 if type(val) == 'function' then
1257 new_val = val(win)
1258 end
1259 set_opt(name, new_val, { scope = 'local', win = win })
1260 end
1261 elseif scope == 'bo' then
1262 for _, win in ipairs(windows) do
1263 local buf = api.nvim_win_get_buf(win)
1264 local saved_val = get_opt(name, { buf = buf })
1265 saved_vim_opts[{ 'bo', buf, name }] = saved_val
1266 local new_val = val
1267 if type(val) == 'function' then
1268 new_val = val(buf)
1269 end
1270 set_opt(name, new_val, { buf = buf })
1271 end
1272 elseif scope == 'go' then
1273 local saved_val = get_opt(name, { scope = 'global' })
1274 saved_vim_opts[name] = saved_val
1275 local new_val = val
1276 if type(val) == 'function' then
1277 new_val = val()
1278 end
1279 set_opt(name, new_val, { scope = 'global' })
1280 end
1281 end
1282 end
1283
1284 local function restore_vim_opts()
1285 for key, val in pairs(saved_vim_opts) do
1286 if type(key) == 'table' then
1287 if key[1] == 'wo' then
1288 local _, win, name = unpack(key)
1289 if api.nvim_win_is_valid(win) then
1290 set_opt(name, val, { scope = 'local', win = win })
1291 end
1292 elseif key[1] == 'bo' then
1293 local _, buf, name = unpack(key)
1294 if api.nvim_buf_is_valid(buf) then
1295 set_opt(name, val, { buf = buf })
1296 end
1297 end
1298 else
1299 set_opt(key, val, { scope = 'global' })
1300 end
1301 end
1302 end
1303
1304 api.nvim_create_autocmd('User', {
1305 pattern = 'LeapEnter',
1306 group = 'LeapDefault',
1307 callback = function(_) set_vim_opts(opts.vim_opts) end,
1308 })
1309
1310 api.nvim_create_autocmd('User', {
1311 pattern = 'LeapLeave',
1312 group = 'LeapDefault',
1313 callback = function(_) restore_vim_opts() end,
1314 })
1315end
1316
1317local function init()
1318 -- The equivalence class table can be potentially huge - let's do this
1319 -- here, and not each time `leap` is called, at least for the defaults.
1320 opts.default.eqv_class_of = to_membership_lookup(opts.default.equivalence_classes)
1321 api.nvim_create_augroup('LeapDefault', {})
1322 init_highlight()
1323 manage_vim_opts()
1324end
1325
1326init()
1327
1328
1329-- Module ///1
1330
1331return {
1332 state = state,
1333 leap = leap
1334}
1335
1336
1337-- vim: foldmethod=marker foldmarker=///,//>