mirror of
https://github.com/nvim-treesitter/nvim-treesitter.git
synced 2026-07-02 03:26:52 -04:00
294 lines
9 KiB
Lua
294 lines
9 KiB
Lua
local api = vim.api
|
|
|
|
local parsers = require'nvim-treesitter.parsers'
|
|
local utils = require'nvim-treesitter.utils'
|
|
|
|
local M = {}
|
|
|
|
--- Gets the actual text content of a node
|
|
-- @param node the node to get the text from
|
|
-- @param bufnr the buffer containing the node
|
|
-- @return list of lines of text of the node
|
|
function M.get_node_text(node, bufnr)
|
|
local bufnr = bufnr or api.nvim_get_current_buf()
|
|
if not node then return {} end
|
|
|
|
-- We have to remember that end_col is end-exclusive
|
|
local start_row, start_col, end_row, end_col = M.get_node_range(node)
|
|
|
|
if start_row ~= end_row then
|
|
local lines = api.nvim_buf_get_lines(bufnr, start_row, end_row+1, false)
|
|
lines[1] = string.sub(lines[1], start_col+1)
|
|
lines[#lines] = string.sub(lines[#lines], 1, end_col)
|
|
return lines
|
|
else
|
|
local line = api.nvim_buf_get_lines(bufnr, start_row, start_row+1, false)[1]
|
|
-- If line is nil then the line is empty
|
|
return line and { string.sub(line, start_col+1, end_col) } or {}
|
|
end
|
|
end
|
|
|
|
--- Determines whether a node is the parent of another
|
|
-- @param dest the possible parent
|
|
-- @param source the possible child node
|
|
function M.is_parent(dest, source)
|
|
if not (dest and source) then return false end
|
|
|
|
local current = source
|
|
while current ~= nil do
|
|
if current == dest then
|
|
return true
|
|
end
|
|
|
|
current = current:parent()
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--- Get next node with same parent
|
|
-- @param node node
|
|
-- @param allow_switch_parents allow switching parents if last node
|
|
-- @param allow_next_parent allow next parent if last node and next parent without children
|
|
function M.get_next_node(node, allow_switch_parents, allow_next_parent)
|
|
local destination_node
|
|
local parent = node:parent()
|
|
|
|
if not parent then return end
|
|
local found_pos = 0
|
|
for i = 0,parent:named_child_count()-1,1 do
|
|
if parent:named_child(i) == node then
|
|
found_pos = i
|
|
break
|
|
end
|
|
end
|
|
if parent:named_child_count() > found_pos + 1 then
|
|
destination_node = parent:named_child(found_pos + 1)
|
|
elseif allow_switch_parents then
|
|
local next_node = M.get_next_node(node:parent())
|
|
if next_node and next_node:named_child_count() > 0 then
|
|
destination_node = next_node:named_child(0)
|
|
elseif next_node and allow_next_parent then
|
|
destination_node = next_node
|
|
end
|
|
end
|
|
|
|
return destination_node
|
|
end
|
|
|
|
--- Get previous node with same parent
|
|
-- @param node node
|
|
-- @param allow_switch_parents allow switching parents if first node
|
|
-- @param allow_previous_parent allow previous parent if first node and previous parent without children
|
|
function M.get_previous_node(node, allow_switch_parents, allow_previous_parent)
|
|
local destination_node
|
|
local parent = node:parent()
|
|
if not parent then return end
|
|
|
|
local found_pos = 0
|
|
for i = 0,parent:named_child_count()-1,1 do
|
|
if parent:named_child(i) == node then
|
|
found_pos = i
|
|
break
|
|
end
|
|
end
|
|
if 0 < found_pos then
|
|
destination_node = parent:named_child(found_pos - 1)
|
|
elseif allow_switch_parents then
|
|
local previous_node = M.get_previous_node(node:parent())
|
|
if previous_node and previous_node:named_child_count() > 0 then
|
|
destination_node = previous_node:named_child(previous_node:named_child_count() - 1)
|
|
elseif previous_node and allow_previous_parent then
|
|
destination_node = previous_node
|
|
end
|
|
end
|
|
return destination_node
|
|
end
|
|
|
|
function M.get_named_children(node)
|
|
local nodes = {}
|
|
for i=0,node:named_child_count() - 1,1 do
|
|
nodes[i+1] = node:named_child(i)
|
|
end
|
|
return nodes
|
|
end
|
|
|
|
function M.get_node_at_cursor(winnr)
|
|
if not parsers.has_parser() then return end
|
|
local cursor = api.nvim_win_get_cursor(winnr or 0)
|
|
local root = parsers.get_parser():parse():root()
|
|
return root:named_descendant_for_range(cursor[1]-1,cursor[2],cursor[1]-1,cursor[2])
|
|
end
|
|
|
|
function M.highlight_node(node, buf, hl_namespace, hl_group)
|
|
if not node then return end
|
|
M.highlight_range({node:range()}, buf, hl_namespace, hl_group)
|
|
end
|
|
|
|
function M.highlight_range(range, buf, hl_namespace, hl_group)
|
|
local start_row, start_col, end_row, end_col = unpack(range)
|
|
vim.highlight.range(buf, hl_namespace, hl_group, {start_row, start_col}, {end_row, end_col})
|
|
end
|
|
|
|
-- Set visual selection to node
|
|
function M.update_selection(buf, node)
|
|
local start_row, start_col, end_row, end_col = M.get_node_range(node)
|
|
|
|
if end_row == vim.fn.line('$') then
|
|
end_col = #vim.fn.getline('$')
|
|
end
|
|
|
|
-- Convert to 1-based indices
|
|
start_row = start_row + 1
|
|
start_col = start_col + 1
|
|
end_row = end_row + 1
|
|
end_col = end_col + 1
|
|
|
|
vim.fn.setpos(".", { buf, start_row, start_col, 0 })
|
|
vim.fn.nvim_exec("normal! v", false)
|
|
|
|
-- Convert exclusive end position to inclusive
|
|
if end_col == 1 then
|
|
vim.fn.setpos(".", { buf, end_row - 1, -1, 0 })
|
|
else
|
|
vim.fn.setpos(".", { buf, end_row, end_col - 1, 0 })
|
|
end
|
|
end
|
|
|
|
-- Byte length of node range
|
|
function M.node_length(node)
|
|
local _, _, start_byte = node:start()
|
|
local _, _, end_byte = node:end_()
|
|
return end_byte - start_byte
|
|
end
|
|
|
|
--- Determines whether (line, col) position is in node range
|
|
-- @param node Node defining the range
|
|
-- @param line A line (0-based)
|
|
-- @param col A column (0-based)
|
|
function M.is_in_node_range(node, line, col)
|
|
local start_line, start_col, end_line, end_col = node:range()
|
|
if line >= start_line and line <= end_line then
|
|
if line == start_line and line == end_line then
|
|
return col >= start_col and col < end_col
|
|
elseif line == start_line then
|
|
return col >= start_col
|
|
elseif line == end_line then
|
|
return col < end_col
|
|
else
|
|
return true
|
|
end
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
function M.get_node_range(node_or_range)
|
|
if type(node_or_range) == 'table' then
|
|
return unpack(node_or_range)
|
|
else
|
|
return node_or_range:range()
|
|
end
|
|
end
|
|
|
|
function M.node_to_lsp_range(node)
|
|
local start_line, start_col, end_line, end_col = M.get_node_range(node)
|
|
local rtn = {}
|
|
rtn.start = { line = start_line, character = start_col }
|
|
rtn['end'] = { line = end_line, character = end_col }
|
|
return rtn
|
|
end
|
|
|
|
--- Memoizes a function based on the buffer tick of the provided bufnr.
|
|
-- The cache entry is cleared when the buffer is detached to avoid memory leaks.
|
|
-- @param fn: the fn to memoize, taking the bufnr as first argument
|
|
-- @returns a memoized function
|
|
function M.memoize_by_buf_tick(fn)
|
|
local cache = {}
|
|
|
|
return function(bufnr)
|
|
if cache[bufnr] then
|
|
return cache[bufnr]
|
|
else
|
|
cache[bufnr] = {}
|
|
api.nvim_buf_attach(bufnr, false,
|
|
{
|
|
on_changedtick = function() cache[bufnr] = fn(bufnr) end,
|
|
on_detach = function() cache[bufnr] = nil end
|
|
}
|
|
)
|
|
end
|
|
|
|
cache[bufnr] = fn(bufnr)
|
|
return cache[bufnr]
|
|
end
|
|
end
|
|
|
|
function M.swap_nodes(node_or_range1, node_or_range2, bufnr, cursor_to_second)
|
|
if not node_or_range1 or not node_or_range2 then return end
|
|
local range1 = M.node_to_lsp_range(node_or_range1)
|
|
local range2 = M.node_to_lsp_range(node_or_range2)
|
|
|
|
local text1 = M.get_node_text(node_or_range1)
|
|
local text2 = M.get_node_text(node_or_range2)
|
|
|
|
local edit1 = { range = range1, newText = table.concat(text2, '\n') }
|
|
local edit2 = { range = range2, newText = table.concat(text1, '\n') }
|
|
vim.lsp.util.apply_text_edits({edit1, edit2}, bufnr)
|
|
|
|
if cursor_to_second then
|
|
utils.set_jump()
|
|
|
|
local char_delta = 0
|
|
local line_delta = 0
|
|
if range1["end"].line < range2.start.line
|
|
or (range1["end"].line == range2.start.line and range1["end"].character < range2.start.character) then
|
|
line_delta = #text2 - #text1
|
|
end
|
|
|
|
if range1["end"].line == range2.start.line and range1["end"].character < range2.start.character then
|
|
if line_delta ~= 0 then
|
|
--- why?
|
|
--correction_after_line_change = -range2.start.character
|
|
--text_now_before_range2 = #(text2[#text2])
|
|
--space_between_ranges = range2.start.character - range1["end"].character
|
|
--char_delta = correction_after_line_change + text_now_before_range2 + space_between_ranges
|
|
--- Equivalent to:
|
|
char_delta = #(text2[#text2]) - range1["end"].character
|
|
|
|
-- add range1.start.character if last line of range1 (now text2) does not start at 0
|
|
if range1.start.line == range2.start.line + line_delta then
|
|
char_delta = char_delta + range1.start.character
|
|
end
|
|
else
|
|
char_delta = #(text2[#text2]) - #(text1[#text1])
|
|
end
|
|
end
|
|
|
|
api.nvim_win_set_cursor(api.nvim_get_current_win(),
|
|
{range2.start.line + 1 + line_delta,
|
|
range2.start.character + char_delta})
|
|
end
|
|
end
|
|
|
|
function M.goto_node(node, goto_end, avoid_set_jump)
|
|
if not node then return end
|
|
if not avoid_set_jump then
|
|
utils.set_jump()
|
|
end
|
|
local range = {node:range()}
|
|
local position
|
|
if not goto_end then
|
|
position = { range[1], range[2] }
|
|
else
|
|
-- ranges are exclusive: -1 character!
|
|
if range[4] == 0 then
|
|
position = { range[3] - 1, -1 }
|
|
else
|
|
position = { range[3], range[4] - 1 }
|
|
end
|
|
end
|
|
api.nvim_win_set_cursor(0, { position[1] + 1, position[2] })
|
|
end
|
|
|
|
return M
|