aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/nvim-treesitter.txt105
-rw-r--r--lua/nvim-treesitter/indent.lua101
-rw-r--r--queries/python/indents.scm49
-rw-r--r--tests/indent/python/aligned_indent_2.py12
-rw-r--r--tests/indent/python/control_flow.py2
-rw-r--r--tests/indent/python/error_state_def.py6
-rw-r--r--tests/indent/python/error_state_dict.py6
-rw-r--r--tests/indent/python/error_state_funcall.py5
-rw-r--r--tests/indent/python/error_state_list.py5
-rw-r--r--tests/indent/python/error_state_set.py5
-rw-r--r--tests/indent/python/error_state_tuple.py7
-rw-r--r--tests/indent/python/error_state_tuple_align.py7
-rw-r--r--tests/indent/python/return_dedent.py23
-rw-r--r--tests/indent/python_spec.lua16
14 files changed, 297 insertions, 52 deletions
diff --git a/doc/nvim-treesitter.txt b/doc/nvim-treesitter.txt
index f05693676..5509dc8af 100644
--- a/doc/nvim-treesitter.txt
+++ b/doc/nvim-treesitter.txt
@@ -222,7 +222,112 @@ Supported options:
enable = true
},
}
+
+`@indent` *nvim-treesitter-indentation-queries*
+Queries can use the following captures: `@indent` and `@dedent`,
+`@branch`, `@indent_end` or `@aligned_indent`. An `@ignore` capture tells
+treesitter to ignore indentation and a `@zero_indent` capture sets
+the indentation to 0.
+
+`@indent` *nvim-treesitter-indentation-indent*
+The `@indent` specifies that the next line should be indented. Multiple
+indents on the same line get collapsed. Eg.
+
+>
+ (
+ (if_statement)
+ (ERROR "else") @indent
+ )
<
+Indent can also have `immediate_indent` set using a `#set!` directive, which
+permits the next line to indent even when the block intended to be indented
+has no content yet, improving interactive typing.
+
+eg for python:
+>
+ ((if_statement) @indent
+ (#set! "immediate_indent" 1))
+<
+
+Will allow:
+>
+ if True:<CR>
+ # Auto indent to here
+
+`@indent_end` *nvim-treesitter-indentation-indent_end*
+An `@indent_end` capture is used to specify that the indented region ends and
+any text subsequent to the capture should be dedented.
+
+`@branch` *nvim-treesitter-indentation-branch*
+An `@branch` capture is used to specify that a dedented region starts
+at the line including the captured nodes.
+
+`@dedent` *nvim-treesitter-indentation-dedent*
+A `@dedent` capture specifies dedenting starting on the next line.
+>
+`@aligned_indent` *nvim-treesitter-indentation-aligned_indent*
+Aligned indent blocks may be specified with the `@aligned_indent` capture.
+This permits
+
+>
+ foo(a,
+ b,
+ c)
+<
+As well as
+>
+ foo(
+ a,
+ b,
+ c)
+<
+and finally
+>
+ foo(
+ a,
+ b,
+ c
+ )
+<
+To specify the delimiters to use `open_delimiter` and `close_delimiter`
+should be used. Eg.
+>
+ ((argument_list) @aligned_indent
+ (#set! "open_delimiter" "(")
+ (#set! "close_delimiter" ")"))
+<
+
+For some languages the last line of an `aligned_indent` block must not be
+the same indent as the natural next line.
+
+For example in python:
+
+>
+ if (a > b and
+ c < d):
+ pass
+
+Is not correct, whereas
+>
+ if (a > b and
+ c < d):
+ pass
+
+Would be correctly indented. This behavior may be chosen using
+`avoid_last_matching_next`. Eg.
+
+>
+ (if_statement
+ condition: (parenthesized_expression) @aligned_indent
+ (#set! "open_delimiter" "(")
+ (#set! "close_delimiter" ")")
+ (#set! "avoid_last_matching_next" 1)
+ )
+<
+Could be used to specify that the last line of an `@aligned_indent` capture
+should be additionally indented to avoid clashing with the indent of the first
+line of the block inside an if.
+
==============================================================================
COMMANDS *nvim-treesitter-commands*
diff --git a/lua/nvim-treesitter/indent.lua b/lua/nvim-treesitter/indent.lua
index cf819d05b..8035a73d8 100644
--- a/lua/nvim-treesitter/indent.lua
+++ b/lua/nvim-treesitter/indent.lua
@@ -42,7 +42,10 @@ local function find_delimiter(bufnr, node, delimiter)
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]
+ 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
end
end
end
@@ -185,51 +188,93 @@ function M.get_indent(lnum)
is_processed = true
end
+ if is_in_err and not q.aligned_indent[node:id()] then
+ -- 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
+ if q.aligned_indent[c:id()] then
+ q.aligned_indent[node:id()] = q.aligned_indent[c:id()]
+ break
+ end
+ end
+ 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
+ if should_process and q.aligned_indent[node:id()] and (srow ~= erow or is_in_err) and (srow ~= lnum - 1) then
local metadata = q.aligned_indent[node:id()]
- local o_delim_node, is_last_in_line ---@type TSNode|nil, boolean|nil
- local c_delim_node ---@type TSNode|nil
+ 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
if metadata.delimiter then
---@type string
local opening_delimiter = metadata.delimiter and metadata.delimiter:sub(1, 1)
- o_delim_node, is_last_in_line = find_delimiter(bufnr, node, opening_delimiter)
+ o_delim_node, o_is_last_in_line = find_delimiter(bufnr, node, opening_delimiter)
+ ---@type string
local closing_delimiter = metadata.delimiter and metadata.delimiter:sub(2, 2)
- c_delim_node, _ = find_delimiter(bufnr, node, closing_delimiter)
+ c_delim_node, c_is_last_in_line = find_delimiter(bufnr, node, closing_delimiter)
else
o_delim_node = node
c_delim_node = node
end
if o_delim_node then
- if is_last_in_line then
+ 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
-- hanging indent (previous line ended with starting delimiter)
- indent = indent + indent_size * 1
- else
- local o_srow, o_scol = o_delim_node:start()
- local final_line_indent = false
- if c_delim_node then
- local c_srow, _ = c_delim_node:start()
- if c_srow ~= o_srow and c_srow == lnum - 1 then
- -- delims end on current line, and are not open and closed same line.
- -- final_line_indent controls this behavior, for example this is not desirable
- -- for a tuple.
- final_line_indent = metadata.final_line_indent or false
+ -- 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
end
end
- if final_line_indent then
- -- last line must be indented more in cases where
- -- it would be same indent as next line
- local aligned_indent = o_scol + (metadata.increment or 1)
- if aligned_indent <= indent then
- return indent + indent_size * 1
- else
- return aligned_indent
- end
+ 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
+ indent = o_scol + (metadata.increment or 1)
+ 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
+ -- with the next. `avoid_last_matching_next` controls this behavior,
+ -- for example this is needed for function parameters.
+ avoid_last_matching_next = metadata.avoid_last_matching_next or false
+ 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
else
- return o_scol + (metadata.increment or 1)
+ indent = indent
end
end
+ is_processed = true
+ if indent_is_absolute then
+ -- don't allow further indenting by parent nodes, this is an absolute position
+ return indent
+ end
end
end
diff --git a/queries/python/indents.scm b/queries/python/indents.scm
index cc13587ec..82933c159 100644
--- a/queries/python/indents.scm
+++ b/queries/python/indents.scm
@@ -1,8 +1,4 @@
[
- (list)
- (dictionary)
- (set)
-
(import_from_statement)
(parenthesized_expression)
@@ -20,6 +16,16 @@
(concatenated_string)
] @indent
+((list) @aligned_indent
+ (#set! "delimiter" "[]")
+)
+((dictionary) @aligned_indent
+ (#set! "delimiter" "{}")
+)
+((set) @aligned_indent
+ (#set! "delimiter" "{}")
+)
+
((for_statement) @indent
(#set! "immediate_indent" 1))
((if_statement) @indent
@@ -28,8 +34,7 @@
(#set! "immediate_indent" 1))
((try_statement) @indent
(#set! "immediate_indent" 1))
-((ERROR "try" ":") @indent
- (#set! "immediate_indent" 1))
+(ERROR "try" ":" @indent (#set! "immediate_indent" 1))
((function_definition) @indent
(#set! "immediate_indent" 1))
((class_definition) @indent
@@ -40,27 +45,24 @@
(if_statement
condition: (parenthesized_expression) @aligned_indent
(#set! "delimiter" "()")
- (#set! "final_line_indent" 1) ; parenthesized_expression already indented
-)
+ (#set! "avoid_last_matching_next" 1))
(while_statement
condition: (parenthesized_expression) @aligned_indent
(#set! "delimiter" "()")
- (#set! "final_line_indent" 1) ; parenthesized_expression already indented
-)
+ (#set! "avoid_last_matching_next" 1))
-((ERROR "(" . (_)) @aligned_indent
- (#set! "delimiter" "()"))
-((argument_list ")" @indent_end) @aligned_indent
+(ERROR "(" @aligned_indent (#set! "delimiter" "()") . (_))
+((argument_list) @aligned_indent
(#set! "delimiter" "()"))
((parameters) @aligned_indent
(#set! "delimiter" "()")
- (#set! "final_line_indent" 1))
-((tuple ")" @indent_end) @aligned_indent
+ (#set! "avoid_last_matching_next" 1))
+((tuple) @aligned_indent
(#set! "delimiter" "()"))
-(list "]" @indent_end)
-(dictionary "}" @indent_end)
-(set "}" @indent_end)
+(ERROR "[" @aligned_indent (#set! "delimiter" "[]") . (_))
+
+(ERROR "{" @aligned_indent (#set! "delimiter" "{}") . (_))
(parenthesized_expression ")" @indent_end)
(generator_expression ")" @indent_end)
@@ -75,9 +77,17 @@
(return_statement
[
(_) @indent_end
- (_ (_) @indent_end .)
+ (_
+ [
+ (_)
+ ")"
+ "}"
+ "]"
+ ] @indent_end .)
(attribute
attribute: (_) @indent_end)
+ (call
+ arguments: (_ ")" @indent_end))
"return" @indent_end
] .)
@@ -92,3 +102,4 @@
] @branch
(string) @auto
+
diff --git a/tests/indent/python/aligned_indent_2.py b/tests/indent/python/aligned_indent_2.py
new file mode 100644
index 000000000..124f7142f
--- /dev/null
+++ b/tests/indent/python/aligned_indent_2.py
@@ -0,0 +1,12 @@
+if True:
+ print(1, 2, 3)
+
+if True:
+ print(
+ 1,
+ 2,
+ 3
+ )
+ print(1,
+ 2,
+ 3)
diff --git a/tests/indent/python/control_flow.py b/tests/indent/python/control_flow.py
index 7ec02e3ff..fca528a2d 100644
--- a/tests/indent/python/control_flow.py
+++ b/tests/indent/python/control_flow.py
@@ -26,5 +26,3 @@ while (a > 4 and
pass
try:
- pass
-
diff --git a/tests/indent/python/error_state_def.py b/tests/indent/python/error_state_def.py
new file mode 100644
index 000000000..943fbfac3
--- /dev/null
+++ b/tests/indent/python/error_state_def.py
@@ -0,0 +1,6 @@
+def foo(a,
+ b,
+ c):
+ pass
+
+def foobar(a,
diff --git a/tests/indent/python/error_state_dict.py b/tests/indent/python/error_state_dict.py
new file mode 100644
index 000000000..dec92b1f9
--- /dev/null
+++ b/tests/indent/python/error_state_dict.py
@@ -0,0 +1,6 @@
+
+d = {1:4,
+ 2:3,
+ 4:5}
+
+d2 = {1:3,
diff --git a/tests/indent/python/error_state_funcall.py b/tests/indent/python/error_state_funcall.py
new file mode 100644
index 000000000..e86720ed1
--- /dev/null
+++ b/tests/indent/python/error_state_funcall.py
@@ -0,0 +1,5 @@
+
+f(1,2,3,
+ 4,5,6)
+
+g(1,2,3,
diff --git a/tests/indent/python/error_state_list.py b/tests/indent/python/error_state_list.py
new file mode 100644
index 000000000..09823bb05
--- /dev/null
+++ b/tests/indent/python/error_state_list.py
@@ -0,0 +1,5 @@
+l = [1,
+ 2,
+ 3]
+
+l2 = [1,
diff --git a/tests/indent/python/error_state_set.py b/tests/indent/python/error_state_set.py
new file mode 100644
index 000000000..31338de68
--- /dev/null
+++ b/tests/indent/python/error_state_set.py
@@ -0,0 +1,5 @@
+s = {1,
+ 2,
+ 3}
+
+s2 = {1,
diff --git a/tests/indent/python/error_state_tuple.py b/tests/indent/python/error_state_tuple.py
new file mode 100644
index 000000000..fda59c8c3
--- /dev/null
+++ b/tests/indent/python/error_state_tuple.py
@@ -0,0 +1,7 @@
+(
+ a,
+ b,
+ c,
+)
+
+(a,
diff --git a/tests/indent/python/error_state_tuple_align.py b/tests/indent/python/error_state_tuple_align.py
new file mode 100644
index 000000000..fda59c8c3
--- /dev/null
+++ b/tests/indent/python/error_state_tuple_align.py
@@ -0,0 +1,7 @@
+(
+ a,
+ b,
+ c,
+)
+
+(a,
diff --git a/tests/indent/python/return_dedent.py b/tests/indent/python/return_dedent.py
index 44d6219de..8171bc9d1 100644
--- a/tests/indent/python/return_dedent.py
+++ b/tests/indent/python/return_dedent.py
@@ -14,3 +14,26 @@ def a():
return (
1, 2, 3
)
+
+def a():
+ return b(
+ 1, 2, 3
+ )
+
+def a():
+ return [1, 2, 3]
+
+def a():
+ return {1, 2, 3}
+
+def a():
+ return {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+
+def a():
+ return [
+ a for a in range (1, 3)
+ ]
diff --git a/tests/indent/python_spec.lua b/tests/indent/python_spec.lua
index c400de542..6f4e0fec7 100644
--- a/tests/indent/python_spec.lua
+++ b/tests/indent/python_spec.lua
@@ -17,6 +17,9 @@ describe("indent Python:", function()
describe("new line:", function()
run:new_line("aligned_indent.py", { on_line = 1, text = "arg3,", indent = 19 })
+ run:new_line("aligned_indent_2.py", { on_line = 2, text = "x", indent = 4 })
+ run:new_line("aligned_indent_2.py", { on_line = 9, text = "x", indent = 4 })
+ run:new_line("aligned_indent_2.py", { on_line = 12, text = "x", indent = 4 })
run:new_line("basic_blocks.py", { on_line = 1, text = "wait,", indent = 4 })
run:new_line("basic_blocks.py", { on_line = 6, text = "x += 1", indent = 4 })
run:new_line("basic_blocks.py", { on_line = 7, text = "x += 1", indent = 4 })
@@ -35,11 +38,18 @@ describe("indent Python:", function()
run:new_line("control_flow.py", { on_line = 22, text = "x = 4", indent = 4 })
run:new_line("control_flow.py", { on_line = 24, text = "c < 6 and", indent = 7 })
run:new_line("control_flow.py", { on_line = 26, text = "x = 4", indent = 4 })
- run:new_line("control_flow.py", { on_line = 29, text = "x = 4", indent = 4 })
+ run:new_line("control_flow.py", { on_line = 28, text = "x = 4", indent = 4 })
run:new_line("branches.py", { on_line = 25, text = "x > 9 and", indent = 4 })
run:new_line("branches.py", { on_line = 29, text = "and x > 9", indent = 4 })
- run:new_line("hanging_indent.py", { on_line = 1, text = "arg0,", indent = 8 })
+ run:new_line("hanging_indent.py", { on_line = 1, text = "arg0,", indent = 4 })
run:new_line("hanging_indent.py", { on_line = 5, text = "0,", indent = 4 })
+ run:new_line("error_state_def.py", { on_line = 6, text = "b,", indent = 11 })
+ run:new_line("error_state_tuple.py", { on_line = 7, text = "b,", indent = 1 })
+ run:new_line("error_state_tuple_align.py", { on_line = 7, text = "b,", indent = 1 })
+ run:new_line("error_state_list.py", { on_line = 5, text = "3,", indent = 6 })
+ run:new_line("error_state_dict.py", { on_line = 6, text = "9:10,", indent = 6 })
+ run:new_line("error_state_set.py", { on_line = 5, text = "9,", indent = 6 })
+ run:new_line("error_state_funcall.py", { on_line = 5, text = "6,", indent = 2 })
run:new_line(
"join_lines.py",
{ on_line = 1, text = "+ 1 \\", indent = 4 },
@@ -75,7 +85,7 @@ describe("indent Python:", function()
run:new_line("line_after_indent.py", { on_line = 55, text = "x", indent = 4 })
run:new_line("line_after_indent.py", { on_line = 63, text = "x", indent = 4 })
- for _, line in ipairs { 2, 5, 8, 11, 16 } do
+ for _, line in ipairs { 2, 5, 8, 11, 16, 21, 24, 27, 34, 39 } do
run:new_line("return_dedent.py", { on_line = line, text = "x", indent = 0 })
end
end)