mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-03-12 17:55:41 +08:00
206 lines
5.9 KiB
Lua
Vendored
206 lines
5.9 KiB
Lua
Vendored
local parsers = require "nvim-treesitter.parsers"
|
|
local queries = require "nvim-treesitter.query"
|
|
local tsutils = require "nvim-treesitter.ts_utils"
|
|
|
|
local M = {}
|
|
|
|
M.avoid_force_reparsing = {
|
|
yaml = true,
|
|
}
|
|
|
|
M.comment_parsers = {
|
|
comment = true,
|
|
jsdoc = true,
|
|
phpdoc = true,
|
|
}
|
|
|
|
local function get_first_node_at_line(root, lnum)
|
|
local col = vim.fn.indent(lnum)
|
|
return root:descendant_for_range(lnum - 1, col, lnum - 1, col)
|
|
end
|
|
|
|
local function get_last_node_at_line(root, lnum)
|
|
local col = #vim.fn.getline(lnum) - 1
|
|
return root:descendant_for_range(lnum - 1, col, lnum - 1, col)
|
|
end
|
|
|
|
local function find_delimiter(bufnr, node, delimiter)
|
|
for child, _ in node:iter_children() do
|
|
if child:type() == delimiter then
|
|
local linenr = child:start()
|
|
local line = vim.api.nvim_buf_get_lines(bufnr, linenr, linenr + 1, false)[1]
|
|
local end_char = { child:end_() }
|
|
return child, #line == end_char[2]
|
|
end
|
|
end
|
|
end
|
|
|
|
local get_indents = tsutils.memoize_by_buf_tick(function(bufnr, root, lang)
|
|
local map = {
|
|
auto = {},
|
|
indent = {},
|
|
indent_end = {},
|
|
dedent = {},
|
|
branch = {},
|
|
ignore = {},
|
|
aligned_indent = {},
|
|
zero_indent = {},
|
|
}
|
|
|
|
for name, node, metadata in queries.iter_captures(bufnr, "indents", root, lang) do
|
|
map[name][node:id()] = metadata or {}
|
|
end
|
|
|
|
return map
|
|
end, {
|
|
-- Memoize by bufnr and lang together.
|
|
key = function(bufnr, root, lang)
|
|
return tostring(bufnr) .. root:id() .. "_" .. lang
|
|
end,
|
|
})
|
|
|
|
---@param lnum number (1-indexed)
|
|
function M.get_indent(lnum)
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
local parser = parsers.get_parser(bufnr)
|
|
if not parser or not lnum then
|
|
return -1
|
|
end
|
|
|
|
local root_lang = parsers.get_buf_lang(bufnr)
|
|
|
|
-- some languages like Python will actually have worse results when re-parsing at opened new line
|
|
if not M.avoid_force_reparsing[root_lang] then
|
|
-- Reparse in case we got triggered by ":h indentkeys"
|
|
parser:parse()
|
|
end
|
|
|
|
-- Get language tree with smallest range around node that's not a comment parser
|
|
local root, lang_tree
|
|
parser:for_each_tree(function(tstree, tree)
|
|
if not tstree or M.comment_parsers[tree:lang()] then
|
|
return
|
|
end
|
|
local local_root = tstree:root()
|
|
if tsutils.is_in_node_range(local_root, lnum - 1, 0) then
|
|
if not root or tsutils.node_length(root) >= tsutils.node_length(local_root) then
|
|
root = local_root
|
|
lang_tree = tree
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Not likely, but just in case...
|
|
if not root then
|
|
return 0
|
|
end
|
|
|
|
local q = get_indents(vim.api.nvim_get_current_buf(), root, lang_tree:lang())
|
|
local is_empty_line = string.match(vim.fn.getline(lnum), "^%s*$") ~= nil
|
|
local node
|
|
if is_empty_line then
|
|
local prevlnum = vim.fn.prevnonblank(lnum)
|
|
node = get_last_node_at_line(root, prevlnum)
|
|
if q.indent_end[node:id()] then
|
|
node = get_first_node_at_line(root, lnum)
|
|
end
|
|
else
|
|
node = get_first_node_at_line(root, lnum)
|
|
end
|
|
|
|
local indent_size = vim.fn.shiftwidth()
|
|
local indent = 0
|
|
local _, _, root_start = root:start()
|
|
if root_start ~= 0 then
|
|
-- injected tree
|
|
indent = vim.fn.indent(root:start() + 1)
|
|
end
|
|
|
|
-- tracks to ensure multiple indent levels are not applied for same line
|
|
local is_processed_by_row = {}
|
|
|
|
if q.zero_indent[node:id()] then
|
|
return 0
|
|
end
|
|
|
|
while node do
|
|
-- do 'autoindent' if not marked as @indent
|
|
if not q.indent[node:id()] and q.auto[node:id()] and node:start() < lnum - 1 and lnum - 1 <= node:end_() then
|
|
return -1
|
|
end
|
|
|
|
-- Do not indent if we are inside an @ignore block.
|
|
-- If a node spans from L1,C1 to L2,C2, we know that lines where L1 < line <= L2 would
|
|
-- have their indentations contained by the node.
|
|
if not q.indent[node:id()] and q.ignore[node:id()] and node:start() < lnum - 1 and lnum - 1 <= node:end_() then
|
|
return 0
|
|
end
|
|
|
|
local srow, _, erow = node:range()
|
|
|
|
local is_processed = false
|
|
|
|
if
|
|
not is_processed_by_row[srow]
|
|
and ((q.branch[node:id()] and srow == lnum - 1) or (q.dedent[node:id()] and srow ~= lnum - 1))
|
|
then
|
|
indent = indent - indent_size
|
|
is_processed = true
|
|
end
|
|
|
|
-- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum)
|
|
if
|
|
not is_processed_by_row[srow]
|
|
-- Dear stylua, please don't change the semantics of this statement!
|
|
-- stylua: ignore start
|
|
and (q.indent[node:id()] and srow ~= erow and ((srow ~= lnum - 1) or q.indent[node:id()].start_at_same_line))
|
|
-- stylua: ignore end
|
|
then
|
|
indent = indent + indent_size
|
|
is_processed = true
|
|
end
|
|
|
|
-- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum)
|
|
if q.aligned_indent[node:id()] and srow ~= erow and (srow ~= lnum - 1) then
|
|
local metadata = q.aligned_indent[node:id()]
|
|
local o_delim_node, is_last_in_line
|
|
if metadata.delimiter then
|
|
local opening_delimiter = metadata.delimiter and metadata.delimiter:sub(1, 1)
|
|
o_delim_node, is_last_in_line = find_delimiter(bufnr, node, opening_delimiter)
|
|
else
|
|
o_delim_node = node
|
|
end
|
|
|
|
if o_delim_node then
|
|
if is_last_in_line then
|
|
-- hanging indent (previous line ended with starting delimiter)
|
|
indent = indent + indent_size * 1
|
|
else
|
|
local _, o_scol = o_delim_node:start()
|
|
return math.max(indent, 0) + o_scol + (metadata.increment or 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
is_processed_by_row[srow] = is_processed_by_row[srow] or is_processed
|
|
|
|
node = node:parent()
|
|
end
|
|
|
|
return indent
|
|
end
|
|
|
|
local indent_funcs = {}
|
|
|
|
function M.attach(bufnr)
|
|
indent_funcs[bufnr] = vim.bo.indentexpr
|
|
vim.bo.indentexpr = "nvim_treesitter#indent()"
|
|
vim.api.nvim_command("au Filetype " .. vim.bo.filetype .. " setlocal indentexpr=nvim_treesitter#indent()")
|
|
end
|
|
|
|
function M.detach(bufnr)
|
|
vim.bo.indentexpr = indent_funcs[bufnr]
|
|
end
|
|
|
|
return M
|