Read-only mirror of https://codeberg.org/andyg/leap.nvim
1local api = vim.api
2
3local function action(kwargs)
4 kwargs = kwargs or {}
5 local jumper = kwargs.jumper
6 local input = kwargs.input
7 local use_count = kwargs.count ~= false
8
9 local state = {
10 args = kwargs,
11 -- `jumper` can mess with these below.
12 mode = vim.fn.mode(1),
13 count = vim.v.count,
14 register = vim.v.register
15 }
16
17 local src_win = vim.fn.win_getid()
18 local saved_view = vim.fn.winsaveview()
19 -- Set an extmark as an anchor, so that we can execute remote delete
20 -- commands in the backward direction, and move together with the text.
21 local anch_ns = api.nvim_create_namespace('')
22 local anch_id = api.nvim_buf_set_extmark(
23 0, anch_ns, saved_view.lnum - 1, saved_view.col, {}
24 )
25
26 jumper = jumper or function()
27 -- We are back in Normal mode when this call is executed, so _we_
28 -- should tell Leap whether it is OK to autojump.
29 -- If `input` is given, all bets are off - before moving on to a
30 -- labeled target, we would have to undo whatever action was taken
31 -- (practically impossible).
32 -- Visual and Operator-pending mode are also problematic, because
33 -- we could only re-trigger them inside the `leap()` call, with a
34 -- custom action, and that would prevent users from customizing
35 -- the jumper.
36 local no_autojump = (input and #input > 0) or state.mode ~= 'n'
37 require('leap').leap {
38 windows = require('leap.user').get_focusable_windows(),
39 opts = no_autojump and { safe_labels = '' } or nil,
40 }
41 end
42
43 local function to_normal_mode()
44 if state.mode:match('^[vV\22]') then
45 api.nvim_feedkeys(state.mode, 'n', false)
46 elseif state.mode:match('o') then
47 -- I'm just cargo-culting this TBH, but the combination of
48 -- the two indeed seems to work reliably.
49 api.nvim_feedkeys(vim.keycode('<C-\\><C-N>'), 'nx', false)
50 api.nvim_feedkeys(vim.keycode('<esc>'), 'n', false)
51 end
52 end
53
54 local function back_to_pending_action()
55 if state.mode:match('^[vV\22]') then
56 api.nvim_feedkeys(state.mode, 'n', false)
57 elseif state.mode:match('o') then
58 local count = (use_count and state.count > 0) and state.count or ''
59 local register = '"' .. state.register
60 local op = vim.v.operator
61 local force = state.mode:sub(3)
62 api.nvim_feedkeys(count .. register .. op .. force, 'n', false)
63 end
64 end
65
66 local function cursor_moved()
67 return vim.fn.win_getid() ~= src_win
68 or vim.fn.line('.') ~= saved_view.lnum
69 or vim.fn.col('.') ~= saved_view.col + 1
70 end
71
72 local function restore_cursor()
73 if vim.fn.win_getid() ~= src_win then
74 api.nvim_set_current_win(src_win)
75 end
76 vim.fn.winrestview(saved_view)
77 local anch_pos = api.nvim_buf_get_extmark_by_id(0, anch_ns, anch_id, {})
78 api.nvim_win_set_cursor(0, { anch_pos[1] + 1, anch_pos[2] })
79 api.nvim_buf_clear_namespace(0, anch_ns, 0, -1)
80 end
81
82 local function register_followup_actions()
83 local action_canceled = false
84 -- Register "cancel" keys.
85 local listener_id = vim.on_key(function(key, _)
86 if key == vim.keycode('<esc>') or key == vim.keycode('<c-c>') then
87 action_canceled = true
88 end
89 end)
90 api.nvim_create_autocmd('ModeChanged', {
91 pattern = '*:*',
92 once = true,
93 callback = function ()
94 local is_change_op = vim.fn.mode(1):match('o') and (vim.v.operator == 'c')
95 api.nvim_create_autocmd('ModeChanged', {
96 pattern = is_change_op and 'i:n' or '*:n',
97 once = true,
98 callback = vim.schedule_wrap(function()
99 restore_cursor()
100 vim.on_key(nil, listener_id) -- remove listener
101 if not action_canceled then
102 api.nvim_exec_autocmds('User', {
103 pattern = 'RemoteOperationDone',
104 data = state
105 })
106 end
107 end)
108 })
109 end
110 })
111 end
112
113 local function after_jump()
114 if not cursor_moved() then return end
115 -- Add target postion to jumplist.
116 vim.cmd('norm! m`')
117 back_to_pending_action() -- (feedkeys...)
118 -- No 'n' flag, custom mappings should work here.
119 if input then api.nvim_feedkeys(input, '', false) end
120 -- Wait for `feedkeys`.
121 vim.schedule(register_followup_actions)
122 end
123
124 -- Execute "spooky" action: jump - operate - restore.
125
126 to_normal_mode() -- (feedkeys...)
127 -- Wait for `feedkeys`.
128 vim.schedule(function()
129 if type(jumper) == 'function' then
130 jumper()
131 -- Wait for `jumper` to finish its business.
132 vim.schedule(function() after_jump() end)
133 elseif type(jumper) == 'string' then
134 -- API note: `jumper` could of course call `feedkeys` itself,
135 -- but then we would need an independent parameter that tells
136 -- whether to wait for `CmdlineLeave`.
137 api.nvim_feedkeys(jumper, 'n', false)
138 vim.schedule(function()
139 -- Wait for finishing the search command (autocmd), and then
140 -- for actually leaving the command line (schedule wrap).
141 api.nvim_create_autocmd('CmdlineLeave', {
142 once = true,
143 callback = vim.schedule_wrap(after_jump)
144 })
145 end)
146 end
147 end)
148end
149
150return {
151 action = action
152}