Read-only mirror of https://codeberg.org/andyg/leap.nvim
at main 1337 lines 43 kB view raw
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=///,//>