diff options
| -rw-r--r-- | doc/nvim-treesitter.txt | 105 | ||||
| -rw-r--r-- | lua/nvim-treesitter/indent.lua | 101 | ||||
| -rw-r--r-- | queries/python/indents.scm | 49 | ||||
| -rw-r--r-- | tests/indent/python/aligned_indent_2.py | 12 | ||||
| -rw-r--r-- | tests/indent/python/control_flow.py | 2 | ||||
| -rw-r--r-- | tests/indent/python/error_state_def.py | 6 | ||||
| -rw-r--r-- | tests/indent/python/error_state_dict.py | 6 | ||||
| -rw-r--r-- | tests/indent/python/error_state_funcall.py | 5 | ||||
| -rw-r--r-- | tests/indent/python/error_state_list.py | 5 | ||||
| -rw-r--r-- | tests/indent/python/error_state_set.py | 5 | ||||
| -rw-r--r-- | tests/indent/python/error_state_tuple.py | 7 | ||||
| -rw-r--r-- | tests/indent/python/error_state_tuple_align.py | 7 | ||||
| -rw-r--r-- | tests/indent/python/return_dedent.py | 23 | ||||
| -rw-r--r-- | tests/indent/python_spec.lua | 16 |
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) |
