Read-only mirror of https://codeberg.org/andyg/leap.nvim
1local api = vim.api
2
3local function get_nodes()
4 if not pcall(vim.treesitter.get_parser) then
5 return nil, 'No treesitter parser for this filetype.'
6 else
7 local node = vim.treesitter.get_node()
8 if node then
9 local nodes = { node }
10 local parent = node:parent()
11 while parent do
12 table.insert(nodes, parent)
13 parent = parent:parent()
14 end
15 return nodes
16 end
17 end
18end
19
20local function nodes_to_targets(nodes)
21 local targets = {}
22 local prev_range = {} -- to skip duplicate ranges
23 local is_linewise = vim.fn.mode(1):match('V')
24
25 for _, node in ipairs(nodes) do
26 local startline, startcol, endline, endcol = node:range() -- (0, 0)
27 if not (is_linewise and startline == endline) then
28 -- Adjust the end position if necessary. (It is exclusive,
29 -- so if we are on the very first column, move to the end of
30 -- the previous line, to the newline character.)
31 if endcol == 0 then
32 endline = endline - 1
33 endcol = #(vim.fn.getline(endline + 1)) + 1 -- include EOL (+1)
34 end
35
36 -- Check duplicates based on the adjusted ranges
37 -- (relevant for linewise mode)!
38 local range = is_linewise and { startline, endline }
39 or { startline, startcol, endline, endcol }
40 -- Instead of skipping, keep this (the outer one), and remove
41 -- the previous (better for linewise mode).
42 if vim.deep_equal(range, prev_range) then
43 table.remove(targets)
44 end
45 prev_range = range
46
47 -- Create target ((0,0) -> (1,1)).
48 -- `endcol` is exclusive, but we want to put the inline labels
49 -- after it, so still +1.
50 table.insert(targets, {
51 pos = { startline + 1, startcol + 1 },
52 endpos = { endline + 1, endcol + 1 }
53 })
54 end
55 end
56
57 if #targets > 0 then
58 return targets
59 end
60end
61
62local function get_targets()
63 local nodes, err = get_nodes()
64 if not nodes then
65 return nil, err
66 else
67 return nodes_to_targets(nodes)
68 end
69end
70
71local function select_range(target)
72 -- Enter Visual mode.
73 local mode = vim.fn.mode(1)
74 if mode:match('no?') then
75 vim.cmd('normal! ' .. (mode:match('[V\22]') or 'v'))
76 end
77 -- Do the rest without leaving Visual mode midway, so that
78 -- leap.remote.action() can keep working.
79
80 -- Move the cursor to the start of the Visual area if needed.
81 if vim.fn.line('v') ~= vim.fn.line('.') or vim.fn.col('v') ~= vim.fn.col('.') then
82 vim.cmd('normal! o')
83 end
84
85 vim.fn.cursor(unpack(target.pos))
86 vim.cmd('normal! o')
87 local endline, endcol = unpack(target.endpos)
88 vim.fn.cursor(endline, endcol - 1)
89
90 -- Move to the start. This might be more intuitive for incremental
91 -- selection, when the whole range is not visible - nodes are usually
92 -- harder to identify at their end.
93 vim.cmd('normal! o')
94
95 -- Force redrawing the selection if the text has been scrolled.
96 pcall(api.nvim__redraw, { flush = true }) -- EXPERIMENTAL
97end
98
99-- Fill the gap left by the cursor (which is down on the command line).
100-- Note: redrawing the cursor with nvim__redraw() is not a satisfying
101-- solution, since the cursor might still appear in a wrong place
102-- (thanks to inline labels).
103local function fill_cursor_pos(targets, start_idx)
104 local ns = api.nvim_create_namespace('')
105 -- Set auto-cleanup.
106 api.nvim_create_autocmd('User', {
107 pattern = { 'LeapRedraw', 'LeapLeave' },
108 once = true,
109 callback = function ()
110 api.nvim_buf_clear_namespace(0, ns, 0, -1)
111 end,
112 })
113
114 local line = vim.fn.line('.')
115 local col = vim.fn.col('.')
116 local line_str = vim.fn.getline(line)
117 local ch_at_curpos = vim.fn.strpart(line_str, col - 1, 1, true)
118 -- On an empty line, use space.
119 local text = (ch_at_curpos == '') and ' ' or ch_at_curpos
120
121 -- Problem: If there is an inline label for the same position, this
122 -- extmark will not be shifted.
123 local first = targets[start_idx]
124 local conflict = first and first.pos[1] == line and first.pos[2] == col
125 -- Solution (hack): Shift by the number of labels on the given line.
126 -- Note: Getting the cursor's screenpos would not work, as it has not
127 -- moved yet.
128 -- TODO: What if there are other inline extmarks, besides our ones?
129 local shift = 1
130 if conflict then
131 for idx = start_idx + 1, #targets do
132 if targets[idx] and targets[idx].pos[1] == line then
133 shift = shift + 1
134 else
135 break
136 end
137 end
138 end
139
140 api.nvim_buf_set_extmark(0, ns, line - 1, col - 1, {
141 virt_text = { { text, 'Visual' } },
142 virt_text_pos = 'overlay',
143 virt_text_win_col = conflict and (col - 1 + shift) or nil,
144 hl_mode = 'combine',
145 })
146end
147
148local function select(kwargs)
149 kwargs = kwargs or {}
150 local leap = require('leap')
151 local traversal = not vim.fn.mode(1):match('o')
152
153 local ok, context = pcall(require, 'treesitter-context')
154 local context_enabled = ok and context.enabled()
155
156 if context_enabled then context.disable() end
157
158 leap.leap {
159 windows = { api.nvim_get_current_win() },
160 targets = get_targets,
161 action = select_range,
162 traversal = traversal,
163 opts = vim.tbl_extend('keep', kwargs.opts or {}, {
164 labels = traversal and '' or nil,
165 on_beacons = traversal and fill_cursor_pos or nil,
166 virt_text_pos = 'inline',
167 })
168 }
169
170 if context_enabled then context.enable() end
171end
172
173return {
174 select = select
175}