Read-only mirror of https://codeberg.org/andyg/leap.nvim
at main 175 lines 5.6 kB view raw
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}