2021-07-04 16:12:17 -05:00
|
|
|
local parsers = require "nvim-treesitter.parsers"
|
|
|
|
|
local queries = require "nvim-treesitter.query"
|
|
|
|
|
local tsutils = require "nvim-treesitter.ts_utils"
|
2023-02-12 18:18:48 +00:00
|
|
|
local ts = vim.treesitter
|
2020-10-13 01:02:30 +02:00
|
|
|
|
2022-01-23 15:52:03 +01:00
|
|
|
local M = {}
|
|
|
|
|
|
|
|
|
|
M.avoid_force_reparsing = {
|
|
|
|
|
yaml = true,
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-05 13:40:41 +01:00
|
|
|
M.comment_parsers = {
|
|
|
|
|
comment = true,
|
|
|
|
|
jsdoc = true,
|
|
|
|
|
phpdoc = true,
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-24 03:07:52 -05:00
|
|
|
---@param root TSNode
|
|
|
|
|
---@param lnum integer
|
2023-04-01 18:31:00 +09:00
|
|
|
---@param col? integer
|
2023-02-24 03:07:52 -05:00
|
|
|
---@return TSNode
|
2023-04-01 18:31:00 +09:00
|
|
|
local function get_first_node_at_line(root, lnum, col)
|
|
|
|
|
col = col or vim.fn.indent(lnum)
|
2022-01-19 02:58:07 +06:00
|
|
|
return root:descendant_for_range(lnum - 1, col, lnum - 1, col)
|
|
|
|
|
end
|
|
|
|
|
|
2023-02-24 03:07:52 -05:00
|
|
|
---@param root TSNode
|
|
|
|
|
---@param lnum integer
|
2023-04-01 18:31:00 +09:00
|
|
|
---@param col? integer
|
2023-02-24 03:07:52 -05:00
|
|
|
---@return TSNode
|
2023-04-01 18:31:00 +09:00
|
|
|
local function get_last_node_at_line(root, lnum, col)
|
|
|
|
|
col = col or (#vim.fn.getline(lnum) - 1)
|
2022-01-19 02:58:07 +06:00
|
|
|
return root:descendant_for_range(lnum - 1, col, lnum - 1, col)
|
|
|
|
|
end
|
|
|
|
|
|
2023-02-24 03:07:52 -05:00
|
|
|
---@param bufnr integer
|
|
|
|
|
---@param node TSNode
|
|
|
|
|
---@param delimiter string
|
|
|
|
|
---@return TSNode|nil child
|
|
|
|
|
---@return boolean|nil is_end
|
2022-01-22 17:07:25 +01:00
|
|
|
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]
|
2022-01-22 19:10:34 +01:00
|
|
|
local end_char = { child:end_() }
|
2023-03-19 18:09:18 -07:00
|
|
|
local trimmed_after_delim
|
|
|
|
|
local escaped_delimiter = delimiter:gsub("[%-%.%+%[%]%(%)%$%^%%%?%*]", "%%%1")
|
|
|
|
|
trimmed_after_delim, _ = line:sub(end_char[2] + 1):gsub("[%s" .. escaped_delimiter .. "]*", "")
|
|
|
|
|
return child, #trimmed_after_delim == 0
|
2022-01-22 17:07:25 +01:00
|
|
|
end
|
2022-01-19 03:06:06 +06:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2021-03-30 08:18:24 -05:00
|
|
|
local get_indents = tsutils.memoize_by_buf_tick(function(bufnr, root, lang)
|
2022-01-19 02:22:29 +06:00
|
|
|
local map = {
|
2023-03-20 14:44:39 -07:00
|
|
|
indent = {
|
|
|
|
|
auto = {},
|
|
|
|
|
begin = {},
|
|
|
|
|
["end"] = {},
|
|
|
|
|
dedent = {},
|
|
|
|
|
branch = {},
|
|
|
|
|
ignore = {},
|
|
|
|
|
align = {},
|
|
|
|
|
zero = {},
|
|
|
|
|
},
|
2022-01-19 02:22:29 +06:00
|
|
|
}
|
|
|
|
|
|
2023-03-20 14:44:39 -07:00
|
|
|
local function split(to_split)
|
|
|
|
|
local t = {}
|
|
|
|
|
for str in string.gmatch(to_split, "([^.]+)") do
|
|
|
|
|
table.insert(t, str)
|
|
|
|
|
end
|
|
|
|
|
return t
|
|
|
|
|
end
|
|
|
|
|
|
2022-01-22 17:48:44 +01:00
|
|
|
for name, node, metadata in queries.iter_captures(bufnr, "indents", root, lang) do
|
2023-03-20 14:44:39 -07:00
|
|
|
local path = split(name)
|
|
|
|
|
-- node may contain a period so append directly.
|
|
|
|
|
table.insert(path, node:id())
|
|
|
|
|
queries.insert_to_path(map, path, metadata or {})
|
2022-01-22 17:48:44 +01:00
|
|
|
end
|
2020-10-13 01:02:30 +02:00
|
|
|
|
2022-01-19 02:22:29 +06:00
|
|
|
return map
|
2021-03-30 08:18:24 -05:00
|
|
|
end, {
|
|
|
|
|
-- Memoize by bufnr and lang together.
|
2022-01-18 21:17:26 +06:00
|
|
|
key = function(bufnr, root, lang)
|
|
|
|
|
return tostring(bufnr) .. root:id() .. "_" .. lang
|
2021-07-04 16:12:17 -05:00
|
|
|
end,
|
2021-03-30 08:18:24 -05:00
|
|
|
})
|
2020-10-13 01:02:30 +02:00
|
|
|
|
2022-01-18 21:17:26 +06:00
|
|
|
---@param lnum number (1-indexed)
|
2020-10-13 23:55:37 +02:00
|
|
|
function M.get_indent(lnum)
|
2022-01-23 15:52:03 +01:00
|
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
|
|
|
local parser = parsers.get_parser(bufnr)
|
2021-07-04 16:12:17 -05:00
|
|
|
if not parser or not lnum then
|
|
|
|
|
return -1
|
|
|
|
|
end
|
2022-01-22 19:10:34 +01:00
|
|
|
|
2022-01-23 15:52:03 +01:00
|
|
|
local root_lang = parsers.get_buf_lang(bufnr)
|
2022-01-22 19:10:34 +01:00
|
|
|
|
2022-01-23 15:52:03 +01:00
|
|
|
-- 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
|
2020-10-13 23:55:37 +02:00
|
|
|
|
2022-02-05 13:40:41 +01:00
|
|
|
-- Get language tree with smallest range around node that's not a comment parser
|
2023-02-24 03:07:52 -05:00
|
|
|
local root, lang_tree ---@type TSNode, LanguageTree
|
2022-02-05 13:40:41 +01:00
|
|
|
parser:for_each_tree(function(tstree, tree)
|
|
|
|
|
if not tstree or M.comment_parsers[tree:lang()] then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local local_root = tstree:root()
|
2023-02-12 18:18:48 +00:00
|
|
|
if ts.is_in_node_range(local_root, lnum - 1, 0) then
|
2022-02-05 13:40:41 +01:00
|
|
|
if not root or tsutils.node_length(root) >= tsutils.node_length(local_root) then
|
|
|
|
|
root = local_root
|
|
|
|
|
lang_tree = tree
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end)
|
2021-03-30 08:18:24 -05:00
|
|
|
|
|
|
|
|
-- Not likely, but just in case...
|
2021-07-04 16:12:17 -05:00
|
|
|
if not root then
|
|
|
|
|
return 0
|
|
|
|
|
end
|
2021-03-30 08:18:24 -05:00
|
|
|
|
|
|
|
|
local q = get_indents(vim.api.nvim_get_current_buf(), root, lang_tree:lang())
|
2022-01-18 21:17:26 +06:00
|
|
|
local is_empty_line = string.match(vim.fn.getline(lnum), "^%s*$") ~= nil
|
2023-02-24 03:07:52 -05:00
|
|
|
local node ---@type TSNode
|
2022-01-18 21:17:26 +06:00
|
|
|
if is_empty_line then
|
|
|
|
|
local prevlnum = vim.fn.prevnonblank(lnum)
|
2023-04-01 18:31:00 +09:00
|
|
|
local indent = vim.fn.indent(prevlnum)
|
|
|
|
|
local prevline = vim.trim(vim.fn.getline(prevlnum))
|
|
|
|
|
-- The final position can be trailing spaces, which should not affect indentation
|
|
|
|
|
node = get_last_node_at_line(root, prevlnum, indent + #prevline - 1)
|
|
|
|
|
if node:type():match "comment" then
|
|
|
|
|
-- The final node we capture of the previous line can be a comment node, which should also be ignored
|
|
|
|
|
-- Unless the last line is an entire line of comment, ignore the comment range and find the last node again
|
|
|
|
|
local first_node = get_first_node_at_line(root, prevlnum, indent)
|
2023-04-03 23:47:41 +09:00
|
|
|
local _, scol, _, _ = node:range()
|
2023-04-01 18:31:00 +09:00
|
|
|
if first_node:id() ~= node:id() then
|
|
|
|
|
-- In case the last captured node is a trailing comment node, re-trim the string
|
2023-04-03 23:47:41 +09:00
|
|
|
prevline = vim.trim(prevline:sub(1, scol - indent))
|
2023-04-01 18:31:00 +09:00
|
|
|
-- Add back indent as indent of prevline was trimmed away
|
|
|
|
|
local col = indent + #prevline - 1
|
|
|
|
|
node = get_last_node_at_line(root, prevlnum, col)
|
|
|
|
|
end
|
|
|
|
|
end
|
2023-03-20 14:44:39 -07:00
|
|
|
if q.indent["end"][node:id()] then
|
2022-01-21 17:11:46 +06:00
|
|
|
node = get_first_node_at_line(root, lnum)
|
|
|
|
|
end
|
2022-01-18 21:17:26 +06:00
|
|
|
else
|
2022-01-19 02:58:07 +06:00
|
|
|
node = get_first_node_at_line(root, lnum)
|
2020-11-06 23:13:50 +01:00
|
|
|
end
|
2020-10-13 23:55:37 +02:00
|
|
|
|
2022-01-18 21:17:26 +06:00
|
|
|
local indent_size = vim.fn.shiftwidth()
|
|
|
|
|
local indent = 0
|
2022-01-22 19:10:34 +01:00
|
|
|
local _, _, root_start = root:start()
|
|
|
|
|
if root_start ~= 0 then
|
2022-01-18 21:17:26 +06:00
|
|
|
-- injected tree
|
|
|
|
|
indent = vim.fn.indent(root:start() + 1)
|
2020-10-13 23:55:37 +02:00
|
|
|
end
|
2020-10-13 01:02:30 +02:00
|
|
|
|
2022-01-18 21:17:26 +06:00
|
|
|
-- tracks to ensure multiple indent levels are not applied for same line
|
|
|
|
|
local is_processed_by_row = {}
|
2020-11-06 23:13:50 +01:00
|
|
|
|
2023-03-20 14:44:39 -07:00
|
|
|
if q.indent.zero[node:id()] then
|
2022-01-22 18:24:38 +01:00
|
|
|
return 0
|
|
|
|
|
end
|
|
|
|
|
|
2020-10-13 23:55:37 +02:00
|
|
|
while node do
|
2022-01-18 21:17:26 +06:00
|
|
|
-- do 'autoindent' if not marked as @indent
|
2023-03-14 12:25:38 -07:00
|
|
|
if
|
2023-03-20 14:44:39 -07:00
|
|
|
not q.indent.begin[node:id()]
|
|
|
|
|
and not q.indent.align[node:id()]
|
|
|
|
|
and q.indent.auto[node:id()]
|
2023-03-14 12:25:38 -07:00
|
|
|
and node:start() < lnum - 1
|
|
|
|
|
and lnum - 1 <= node:end_()
|
|
|
|
|
then
|
2022-01-18 21:17:26 +06:00
|
|
|
return -1
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-27 08:28:40 +08:00
|
|
|
-- 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.
|
2023-03-20 14:44:39 -07:00
|
|
|
if
|
|
|
|
|
not q.indent.begin[node:id()]
|
|
|
|
|
and q.indent.ignore[node:id()]
|
|
|
|
|
and node:start() < lnum - 1
|
|
|
|
|
and lnum - 1 <= node:end_()
|
|
|
|
|
then
|
2022-01-18 21:17:26 +06:00
|
|
|
return 0
|
2021-01-08 00:52:30 +01:00
|
|
|
end
|
|
|
|
|
|
2022-01-18 21:17:26 +06:00
|
|
|
local srow, _, erow = node:range()
|
|
|
|
|
|
|
|
|
|
local is_processed = false
|
|
|
|
|
|
|
|
|
|
if
|
|
|
|
|
not is_processed_by_row[srow]
|
2023-03-20 14:44:39 -07:00
|
|
|
and ((q.indent.branch[node:id()] and srow == lnum - 1) or (q.indent.dedent[node:id()] and srow ~= lnum - 1))
|
2022-01-18 21:17:26 +06:00
|
|
|
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)
|
2022-07-21 13:48:03 +02:00
|
|
|
local should_process = not is_processed_by_row[srow]
|
|
|
|
|
local is_in_err = false
|
|
|
|
|
if should_process then
|
|
|
|
|
local parent = node:parent()
|
|
|
|
|
is_in_err = parent and parent:has_error()
|
|
|
|
|
end
|
2022-01-22 19:10:34 +01:00
|
|
|
if
|
2022-07-21 13:48:03 +02:00
|
|
|
should_process
|
|
|
|
|
and (
|
2023-03-20 14:44:39 -07:00
|
|
|
q.indent.begin[node:id()]
|
|
|
|
|
and (srow ~= erow or is_in_err or q.indent.begin[node:id()]["indent.immediate"])
|
|
|
|
|
and (srow ~= lnum - 1 or q.indent.begin[node:id()]["indent.start_at_same_line"])
|
2022-07-21 13:48:03 +02:00
|
|
|
)
|
2022-01-22 19:10:34 +01:00
|
|
|
then
|
2020-11-06 23:13:50 +01:00
|
|
|
indent = indent + indent_size
|
2022-01-18 21:17:26 +06:00
|
|
|
is_processed = true
|
2020-10-13 01:02:30 +02:00
|
|
|
end
|
2021-01-08 00:52:30 +01:00
|
|
|
|
2023-03-20 14:44:39 -07:00
|
|
|
if is_in_err and not q.indent.align[node:id()] then
|
2023-03-19 18:09:18 -07:00
|
|
|
-- only when the node is in error, promote the
|
|
|
|
|
-- first child's aligned indent to the error node
|
|
|
|
|
-- to work around ((ERROR "X" . (_)) @aligned_indent (#set! "delimeter" "AB"))
|
|
|
|
|
-- matching for all X, instead set do
|
|
|
|
|
-- (ERROR "X" @aligned_indent (#set! "delimeter" "AB") . (_))
|
|
|
|
|
-- and we will fish it out here.
|
|
|
|
|
for c in node:iter_children() do
|
2023-03-20 14:44:39 -07:00
|
|
|
if q.indent.align[c:id()] then
|
|
|
|
|
q.indent.align[node:id()] = q.indent.align[c:id()]
|
2023-03-19 18:09:18 -07:00
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2022-01-22 14:50:51 +01:00
|
|
|
-- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum)
|
2023-03-20 14:44:39 -07:00
|
|
|
if should_process and q.indent.align[node:id()] and (srow ~= erow or is_in_err) and (srow ~= lnum - 1) then
|
|
|
|
|
local metadata = q.indent.align[node:id()]
|
2023-03-19 18:09:18 -07:00
|
|
|
local o_delim_node, o_is_last_in_line ---@type TSNode|nil, boolean|nil
|
|
|
|
|
local c_delim_node, c_is_last_in_line ---@type TSNode|nil, boolean|nil, boolean|nil
|
|
|
|
|
local indent_is_absolute = false
|
2023-03-20 14:44:39 -07:00
|
|
|
if metadata["indent.open_delimiter"] then
|
|
|
|
|
o_delim_node, o_is_last_in_line = find_delimiter(bufnr, node, metadata["indent.open_delimiter"])
|
2022-01-22 17:48:44 +01:00
|
|
|
else
|
|
|
|
|
o_delim_node = node
|
2023-03-19 18:55:48 -07:00
|
|
|
end
|
2023-03-20 14:44:39 -07:00
|
|
|
if metadata["indent.close_delimiter"] then
|
|
|
|
|
c_delim_node, c_is_last_in_line = find_delimiter(bufnr, node, metadata["indent.close_delimiter"])
|
2023-03-19 18:55:48 -07:00
|
|
|
else
|
2023-03-14 12:25:38 -07:00
|
|
|
c_delim_node = node
|
2022-01-22 17:48:44 +01:00
|
|
|
end
|
2022-01-19 03:06:06 +06:00
|
|
|
|
|
|
|
|
if o_delim_node then
|
2023-03-19 18:09:18 -07:00
|
|
|
local o_srow, o_scol = o_delim_node:start()
|
|
|
|
|
local c_srow = nil
|
|
|
|
|
if c_delim_node then
|
|
|
|
|
c_srow, _ = c_delim_node:start()
|
|
|
|
|
end
|
|
|
|
|
if o_is_last_in_line then
|
2022-01-19 03:06:06 +06:00
|
|
|
-- hanging indent (previous line ended with starting delimiter)
|
2023-03-19 18:09:18 -07:00
|
|
|
-- should be processed like indent
|
|
|
|
|
if should_process then
|
|
|
|
|
indent = indent + indent_size * 1
|
|
|
|
|
if c_is_last_in_line then
|
|
|
|
|
-- If current line is outside the range of a node marked with `@aligned_indent`
|
|
|
|
|
-- Then its indent level shouldn't be affected by `@aligned_indent` node
|
|
|
|
|
if c_srow and c_srow < lnum - 1 then
|
|
|
|
|
indent = math.max(indent - indent_size, 0)
|
|
|
|
|
end
|
2023-03-14 12:25:38 -07:00
|
|
|
end
|
|
|
|
|
end
|
2023-03-19 18:09:18 -07:00
|
|
|
else
|
|
|
|
|
-- aligned indent
|
|
|
|
|
if c_is_last_in_line and c_srow and o_srow ~= c_srow and c_srow < lnum - 1 then
|
|
|
|
|
-- If current line is outside the range of a node marked with `@aligned_indent`
|
|
|
|
|
-- Then its indent level shouldn't be affected by `@aligned_indent` node
|
|
|
|
|
indent = math.max(indent - indent_size, 0)
|
|
|
|
|
else
|
2023-03-20 14:44:39 -07:00
|
|
|
indent = o_scol + (metadata["indent.increment"] or 1)
|
2023-03-19 18:09:18 -07:00
|
|
|
indent_is_absolute = true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
-- deal with the final line
|
|
|
|
|
local avoid_last_matching_next = false
|
|
|
|
|
if c_srow and c_srow ~= o_srow and c_srow == lnum - 1 then
|
|
|
|
|
-- delims end on current line, and are not open and closed same line.
|
|
|
|
|
-- then this last line may need additional indent to avoid clashes
|
2023-03-20 14:44:39 -07:00
|
|
|
-- with the next. `indent.avoid_last_matching_next` controls this behavior,
|
2023-03-19 18:09:18 -07:00
|
|
|
-- for example this is needed for function parameters.
|
2023-03-20 14:44:39 -07:00
|
|
|
avoid_last_matching_next = metadata["indent.avoid_last_matching_next"] or false
|
2023-03-19 18:09:18 -07:00
|
|
|
end
|
|
|
|
|
if avoid_last_matching_next then
|
|
|
|
|
-- last line must be indented more in cases where
|
|
|
|
|
-- it would be same indent as next line (we determine this as one
|
|
|
|
|
-- width more than the open indent to avoid confusing with any
|
|
|
|
|
-- hanging indents)
|
|
|
|
|
if indent <= vim.fn.indent(o_srow + 1) + indent_size then
|
|
|
|
|
indent = indent + indent_size * 1
|
2023-03-14 12:25:38 -07:00
|
|
|
else
|
2023-03-19 18:09:18 -07:00
|
|
|
indent = indent
|
2023-03-14 12:25:38 -07:00
|
|
|
end
|
2022-01-19 03:06:06 +06:00
|
|
|
end
|
2023-03-19 18:09:18 -07:00
|
|
|
is_processed = true
|
|
|
|
|
if indent_is_absolute then
|
|
|
|
|
-- don't allow further indenting by parent nodes, this is an absolute position
|
|
|
|
|
return indent
|
|
|
|
|
end
|
2022-01-19 03:06:06 +06:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2022-01-18 21:17:26 +06:00
|
|
|
is_processed_by_row[srow] = is_processed_by_row[srow] or is_processed
|
|
|
|
|
|
2021-01-08 00:52:30 +01:00
|
|
|
node = node:parent()
|
2020-10-13 01:02:30 +02:00
|
|
|
end
|
|
|
|
|
|
2020-11-06 23:13:50 +01:00
|
|
|
return indent
|
2020-10-13 01:02:30 +02:00
|
|
|
end
|
|
|
|
|
|
2023-02-24 03:07:52 -05:00
|
|
|
---@type table<integer, string>
|
2020-10-13 01:02:30 +02:00
|
|
|
local indent_funcs = {}
|
|
|
|
|
|
2023-02-24 03:07:52 -05:00
|
|
|
---@param bufnr integer
|
2020-10-13 01:02:30 +02:00
|
|
|
function M.attach(bufnr)
|
|
|
|
|
indent_funcs[bufnr] = vim.bo.indentexpr
|
2021-07-04 16:12:17 -05:00
|
|
|
vim.bo.indentexpr = "nvim_treesitter#indent()"
|
2020-10-13 01:02:30 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function M.detach(bufnr)
|
|
|
|
|
vim.bo.indentexpr = indent_funcs[bufnr]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return M
|