From e2eb1dd1973677925edecdd91772d2e4ba4d5dc3 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Sun, 10 Sep 2023 16:15:35 +0100 Subject: [PATCH 1/5] Add auto indentation correction on slurp/barf --- lua/nvim-paredit/api/barfing.lua | 35 ++++- lua/nvim-paredit/api/slurping.lua | 41 +++++- lua/nvim-paredit/config.lua | 4 +- lua/nvim-paredit/defaults.lua | 4 + lua/nvim-paredit/indentation/init.lua | 27 ++++ lua/nvim-paredit/indentation/native.lua | 136 +++++++++++++++++++ lua/nvim-paredit/indentation/utils.lua | 64 +++++++++ lua/nvim-paredit/init.lua | 4 +- lua/nvim-paredit/utils/common.lua | 32 ++--- tests/nvim-paredit/indentation_spec.lua | 169 ++++++++++++++++++++++++ 10 files changed, 483 insertions(+), 33 deletions(-) create mode 100644 lua/nvim-paredit/indentation/init.lua create mode 100644 lua/nvim-paredit/indentation/native.lua create mode 100644 lua/nvim-paredit/indentation/utils.lua create mode 100644 tests/nvim-paredit/indentation_spec.lua diff --git a/lua/nvim-paredit/api/barfing.lua b/lua/nvim-paredit/api/barfing.lua index 329987f..4d0c177 100644 --- a/lua/nvim-paredit/api/barfing.lua +++ b/lua/nvim-paredit/api/barfing.lua @@ -1,4 +1,5 @@ local traversal = require("nvim-paredit.utils.traversal") +local indentation = require("nvim-paredit.indentation") local common = require("nvim-paredit.utils.common") local ts = require("nvim-treesitter.ts_utils") local config = require("nvim-paredit.config") @@ -28,11 +29,11 @@ function M.barf_forwards(opts) local child if opts.reversed then child = traversal.get_first_child_ignoring_comments(form, { - lang = lang + lang = lang, }) else child = traversal.get_last_child_ignoring_comments(form, { - lang = lang + lang = lang, }) end if not child then @@ -42,7 +43,7 @@ function M.barf_forwards(opts) local edges = lang.get_form_edges(form) local sibling = traversal.get_prev_sibling_ignoring_comments(child, { - lang = lang + lang = lang, }) local end_pos @@ -55,6 +56,7 @@ function M.barf_forwards(opts) local buf = vim.api.nvim_get_current_buf() local range = edges.right.range + -- stylua: ignore vim.api.nvim_buf_set_text( buf, range[1], range[2], @@ -63,6 +65,7 @@ function M.barf_forwards(opts) ) local text = edges.right.text + -- stylua: ignore vim.api.nvim_buf_set_text(buf, end_pos[1], end_pos[2], end_pos[1], end_pos[2], @@ -77,6 +80,16 @@ function M.barf_forwards(opts) vim.api.nvim_win_set_cursor(0, { end_pos[1] + 1, end_pos[2] }) end end + + local event = { + type = "barf-forwards", + -- stylua: ignore + parent_range = { + edges.left.range[1], edges.left.range[2], + end_pos[1], end_pos[2], + }, + } + indentation.handle_indentation(event, opts) end function M.barf_backwards(opts) @@ -99,7 +112,7 @@ function M.barf_backwards(opts) end local child = traversal.get_first_child_ignoring_comments(form, { - lang = lang + lang = lang, }) if not child then return @@ -108,7 +121,7 @@ function M.barf_backwards(opts) local edges = lang.get_form_edges(lang.get_node_root(form)) local sibling = traversal.get_next_sibling_ignoring_comments(child, { - lang = lang + lang = lang, }) local end_pos @@ -121,6 +134,7 @@ function M.barf_backwards(opts) local buf = vim.api.nvim_get_current_buf() local text = edges.left.text + -- stylua: ignore vim.api.nvim_buf_set_text(buf, end_pos[1], end_pos[2], end_pos[1], end_pos[2], @@ -128,6 +142,7 @@ function M.barf_backwards(opts) ) local range = edges.left.range + -- stylua: ignore vim.api.nvim_buf_set_text( buf, range[1], range[2], @@ -143,6 +158,16 @@ function M.barf_backwards(opts) vim.api.nvim_win_set_cursor(0, { end_pos[1] + 1, end_pos[2] }) end end + + local event = { + type = "barf-backwards", + -- stylua: ignore + parent_range = { + end_pos[1], end_pos[2], + edges.right.range[1], edges.right.range[2], + }, + } + indentation.handle_indentation(event, opts) end return M diff --git a/lua/nvim-paredit/api/slurping.lua b/lua/nvim-paredit/api/slurping.lua index 47fb369..794072c 100644 --- a/lua/nvim-paredit/api/slurping.lua +++ b/lua/nvim-paredit/api/slurping.lua @@ -1,5 +1,5 @@ local traversal = require("nvim-paredit.utils.traversal") -local common = require("nvim-paredit.utils.common") +local indentation = require("nvim-paredit.indentation") local ts = require("nvim-treesitter.ts_utils") local config = require("nvim-paredit.config") local langs = require("nvim-paredit.lang") @@ -40,11 +40,12 @@ local function slurp(opts) end local buf = vim.api.nvim_get_current_buf() + local form_edges = lang.get_form_edges(form) local left_or_right_edge if opts.reversed then - left_or_right_edge = lang.get_form_edges(form).left + left_or_right_edge = form_edges.left else - left_or_right_edge = lang.get_form_edges(form).right + left_or_right_edge = form_edges.right end local start_or_end @@ -57,6 +58,7 @@ local function slurp(opts) local row = start_or_end[1] local col = start_or_end[2] + -- stylua: ignore vim.api.nvim_buf_set_text(buf, row, col, row, col, @@ -65,9 +67,10 @@ local function slurp(opts) local offset = 0 if opts.reversed and row == left_or_right_edge.range[1] then - offset = string.len(left_or_right_edge.text) + offset = #left_or_right_edge.text end + -- stylua: ignore vim.api.nvim_buf_set_text( buf, left_or_right_edge.range[1], left_or_right_edge.range[2] + offset, @@ -77,7 +80,7 @@ local function slurp(opts) local cursor_behaviour = opts.cursor_behaviour or config.config.cursor_behaviour if cursor_behaviour == "follow" then - local offset = 0 + offset = 0 if not opts.reversed then offset = string.len(left_or_right_edge.text) end @@ -88,6 +91,30 @@ local function slurp(opts) vim.api.nvim_win_set_cursor(0, cursor_pos) end end + + local operation_type + local new_range + if not opts.reversed then + operation_type = "slurp-forwards" + -- stylua: ignore + new_range = { + form_edges.left.range[1], form_edges.left.range[2], + row, col, + } + else + operation_type = "slurp-backwards" + -- stylua: ignore + new_range = { + row, col, + form_edges.right.range[1], form_edges.right.range[2], + } + end + + local event = { + type = operation_type, + parent_range = new_range, + } + indentation.handle_indentation(event, opts) end function M.slurp_forwards(opts) @@ -95,8 +122,8 @@ function M.slurp_forwards(opts) end function M.slurp_backwards(opts) - slurp(common.merge(opts or {}, { - reversed = true + slurp(vim.tbl_deep_extend("force", opts or {}, { + reversed = true, })) end diff --git a/lua/nvim-paredit/config.lua b/lua/nvim-paredit/config.lua index 1f43cb2..c09f05a 100644 --- a/lua/nvim-paredit/config.lua +++ b/lua/nvim-paredit/config.lua @@ -1,11 +1,9 @@ -local common = require("nvim-paredit.utils.common") - local M = {} M.config = {} function M.update_config(config) - M.config = common.merge(M.config, config) + M.config = vim.tbl_deep_extend("force", M.config, config) end return M diff --git a/lua/nvim-paredit/defaults.lua b/lua/nvim-paredit/defaults.lua index 2e40bf6..9796505 100644 --- a/lua/nvim-paredit/defaults.lua +++ b/lua/nvim-paredit/defaults.lua @@ -61,6 +61,10 @@ M.default_keys = { M.defaults = { use_default_keys = true, cursor_behaviour = "auto", -- remain, follow, auto + indent = { + enabled = true, + indentor = require("nvim-paredit.indentation.native").indentor, + }, keys = {}, } diff --git a/lua/nvim-paredit/indentation/init.lua b/lua/nvim-paredit/indentation/init.lua new file mode 100644 index 0000000..881bb79 --- /dev/null +++ b/lua/nvim-paredit/indentation/init.lua @@ -0,0 +1,27 @@ +local config = require("nvim-paredit.config") + +local M = {} + +function M.handle_indentation(event, opts) + local indent = opts.indent or config.config.indent or {} + if not indent.enabled or not indent.indentor then + return + end + + local tree = vim.treesitter.get_parser(0) + + tree:parse() + local parent = tree:named_node_for_range(event.parent_range) + + indent.indentor( + vim.tbl_deep_extend("force", event, { + tree = tree, + parent = parent, + }), + vim.tbl_deep_extend("force", opts, { + indent = indent, + }) + ) +end + +return M diff --git a/lua/nvim-paredit/indentation/native.lua b/lua/nvim-paredit/indentation/native.lua new file mode 100644 index 0000000..f47da35 --- /dev/null +++ b/lua/nvim-paredit/indentation/native.lua @@ -0,0 +1,136 @@ +local traversal = require("nvim-paredit.utils.traversal") +local utils = require("nvim-paredit.indentation.utils") +local langs = require("nvim-paredit.lang") + +local M = {} + +local function dedent_lines(lines, delta, opts) + -- stylua: ignore + local line_text = vim.api.nvim_buf_get_lines( + opts.buf or 0, + lines[1], lines[#lines] + 1, + false + ) + + local smallest_distance = delta + for _, line in ipairs(line_text) do + local first_char_index = string.find(line, "[^%s]") + if first_char_index and (first_char_index - 1) < smallest_distance then + smallest_distance = first_char_index - 1 + end + end + + for index, line in ipairs(lines) do + local deletion_range = smallest_distance + local contains_chars = string.find(line_text[index], "[^%s]") + if not contains_chars then + deletion_range = #line_text[index] + end + -- stylua: ignore + vim.api.nvim_buf_set_text( + opts.buf or 0, + line, 0, + line, deletion_range, + {} + ) + end +end + +local function indent_lines(lines, delta, opts) + if delta == 0 then + return + end + + if delta < 0 then + return dedent_lines(lines, delta * -1, opts) + end + + local chars = string.rep(" ", delta) + for _, line in ipairs(lines) do + -- stylua: ignore + vim.api.nvim_buf_set_text( + opts.buf or 0, + line, 0, + line, 0, + {chars} + ) + end +end + +local function indent_barf(event) + local lang = langs.get_language_api() + + local lhs + local node + if event.type == "barf-forwards" then + node = traversal.get_next_sibling_ignoring_comments(event.parent, { lang = lang }) + lhs = event.parent + else + node = event.parent + lhs = traversal.get_prev_sibling_ignoring_comments(event.parent, { lang = lang }) + end + + if not node or not lhs then + return + end + + local parent = node:parent() + + local lhs_range = { lhs:range() } + local node_range = { node:range() } + + if not utils.node_is_first_on_line(node, { lang = lang }) or lhs_range[1] == node_range[1] then + return + end + + local lines = utils.find_affected_lines(node, utils.get_node_line_range(node_range)) + + local delta + if parent:type() == "source" then + delta = node_range[2] + else + local form_edges = lang.get_form_edges(parent) + delta = node_range[2] - form_edges.left.range[2] - 1 + end + + indent_lines(lines, delta * -1, { + buf = event.buf, + }) +end + +local function indent_slurp(event) + local parent = event.parent + local lang = langs.get_language_api() + + local child + if event.type == "slurp-forwards" then + child = parent:named_child(parent:named_child_count() - 1) + else + child = parent:named_child(1) + end + + local parent_range = { parent:range() } + local child_range = { child:range() } + + if not utils.node_is_first_on_line(child, { lang = lang }) or parent_range[1] == child_range[1] then + return + end + + local lines = utils.find_affected_lines(child, utils.get_node_line_range(child_range)) + local form_edges = lang.get_form_edges(parent) + + local delta = form_edges.left.range[4] - child_range[2] + indent_lines(lines, delta, { + buf = event.buf, + }) +end + +function M.indentor(event, _) + if event.type == "slurp-forwards" or event.type == "slurp-backwards" then + indent_slurp(event) + else + indent_barf(event) + end +end + +return M diff --git a/lua/nvim-paredit/indentation/utils.lua b/lua/nvim-paredit/indentation/utils.lua new file mode 100644 index 0000000..0afb09c --- /dev/null +++ b/lua/nvim-paredit/indentation/utils.lua @@ -0,0 +1,64 @@ +local traversal = require("nvim-paredit.utils.traversal") +local common = require("nvim-paredit.utils.common") + +local M = {} + +function M.get_node_line_range(range) + local lines = {} + for i = range[1], range[3], 1 do + table.insert(lines, i) + end + return lines +end + +function M.get_node_rhs_siblings(node) + local nodes = {} + local current = node + while current do + table.insert(nodes, current) + current = current:next_named_sibling() + end + return nodes +end + +function M.find_affected_lines(node, lines) + local siblings = M.get_node_rhs_siblings(node) + for _, sibling in ipairs(siblings) do + local range = { sibling:range() } + + local sibling_is_affected = false + for _, line in ipairs(lines) do + if line == range[1] then + sibling_is_affected = true + end + end + + if sibling_is_affected then + local new_lines = M.get_node_line_range(range) + for _, row in ipairs(new_lines) do + table.insert(lines, row) + end + end + end + + local parent = node:parent() + if parent then + return M.find_affected_lines(parent, lines) + end + + return common.ordered_set(lines) +end + +function M.node_is_first_on_line(node, opts) + local node_range = { node:range() } + + local sibling = traversal.get_prev_sibling_ignoring_comments(node, opts) + if not sibling then + return true + end + + local sibling_range = { sibling:range() } + return sibling_range[3] ~= node_range[1] +end + +return M diff --git a/lua/nvim-paredit/init.lua b/lua/nvim-paredit/init.lua index 6b691b8..64b0895 100644 --- a/lua/nvim-paredit/init.lua +++ b/lua/nvim-paredit/init.lua @@ -49,11 +49,11 @@ function M.setup(opts) local keys = opts.keys or {} if type(opts.use_default_keys) ~= "boolean" or opts.use_default_keys then - keys = common.merge(defaults.default_keys, opts.keys or {}) + keys = vim.tbl_deep_extend("force", defaults.default_keys, opts.keys or {}) end config.update_config(defaults.defaults) - config.update_config(common.merge(opts, { + config.update_config(vim.tbl_deep_extend("force", opts, { keys = keys, })) diff --git a/lua/nvim-paredit/utils/common.lua b/lua/nvim-paredit/utils/common.lua index ea297e3..c4c7fbd 100644 --- a/lua/nvim-paredit/utils/common.lua +++ b/lua/nvim-paredit/utils/common.lua @@ -9,17 +9,6 @@ function M.included_in_table(table, item) return false end -function M.merge(a, b) - local result = {} - for k, v in pairs(a) do - result[k] = v - end - for k, v in pairs(b) do - result[k] = v - end - return result -end - -- Compares the two given { col, row } position tuples and returns -1/0/1 depending -- on whether `a` is less than, equal to or greater than `b` -- @@ -59,6 +48,20 @@ function M.intersection(tbl, original) return result end +function M.ordered_set(lines) + local seen = {} + local result = {} + for _, value in ipairs(lines) do + if not seen[value] then + table.insert(result, value) + seen[value] = true + end + end + + table.sort(result) + return result +end + function M.ensure_visual_mode() if vim.api.nvim_get_mode().mode ~= "v" then vim.api.nvim_command("normal! v") @@ -72,11 +75,8 @@ function M.is_whitespace_under_cursor(lang) cursor = { cursor[1] - 1, cursor[2] } local char_under_cursor = vim.api.nvim_buf_get_text(0, cursor[1], cursor[2], cursor[1], cursor[2] + 1, {}) - return M.included_in_table( - lang.whitespace_chars or M.default_whitespace_chars, - char_under_cursor[1] - ) or char_under_cursor[1] == "" + return M.included_in_table(lang.whitespace_chars or M.default_whitespace_chars, char_under_cursor[1]) + or char_under_cursor[1] == "" end return M - diff --git a/tests/nvim-paredit/indentation_spec.lua b/tests/nvim-paredit/indentation_spec.lua new file mode 100644 index 0000000..674ab6f --- /dev/null +++ b/tests/nvim-paredit/indentation_spec.lua @@ -0,0 +1,169 @@ +local defaults = require("nvim-paredit.defaults") +local paredit = require("nvim-paredit.api") + +local expect_all = require("tests.nvim-paredit.utils").expect_all + +local opts = vim.tbl_deep_extend("force", defaults.defaults, { + indent = { + enabled = true, + }, +}) + +describe("forward slurping indentation", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + local function slurp_forwards() + paredit.slurp_forwards(opts) + end + + expect_all(slurp_forwards, { + { + "should indent a nested child", + before_content = { "()", "a" }, + before_cursor = { 1, 1 }, + after_content = { "(", " a)" }, + after_cursor = { 1, 0 }, + }, + { + "should indent a multi-line child", + before_content = { "()", "(a", " b c)" }, + before_cursor = { 1, 1 }, + after_content = { "(", " (a", " b c))" }, + after_cursor = { 1, 0 }, + }, + { + "should indent a multi-line child that pushes other nodes", + before_content = { "()", "(a", " b) (c", "d) (e", "f)" }, + before_cursor = { 1, 1 }, + after_content = { "(", " (a", " b)) (c", " d) (e", " f)" }, + after_cursor = { 1, 0 }, + }, + { + "should not indent if node is not first on line", + before_content = { "(", "a) (a", "b)" }, + before_cursor = { 1, 1 }, + after_content = { "(", "a (a", "b))" }, + after_cursor = { 1, 0 }, + }, + { + "should not indent when on same line", + before_content = "() 1", + before_cursor = { 1, 1 }, + after_content = "( 1)", + after_cursor = { 1, 1 }, + }, + { + "should dedent when node is too far indented", + before_content = { "()", " a" }, + before_cursor = { 1, 1 }, + after_content = { "(", " a)" }, + after_cursor = { 1, 0 }, + }, + { + "should dedent without deleting characters", + before_content = { "()", " (a", " b)" }, + before_cursor = { 1, 1 }, + after_content = { "(", " (a", "b))" }, + after_cursor = { 1, 0 }, + }, + { + "should indent the correct node ignoring comments", + before_content = { "()", ";; comment", "a" }, + before_cursor = { 1, 1 }, + after_content = { "(", ";; comment", " a)" }, + after_cursor = { 1, 0 }, + }, + }) +end) + +describe("backward slurping indentation", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + local function slurp_backwards() + paredit.slurp_backwards(opts) + end + + expect_all(slurp_backwards, { + { + "should indent a nested child", + before_content = { "a", "(b)" }, + before_cursor = { 2, 1 }, + after_content = { "(a", " b)" }, + after_cursor = { 2, 2 }, + }, + { + "should not indent when on same line", + before_content = { "a (b)" }, + before_cursor = { 1, 3 }, + after_content = { "(a b)" }, + after_cursor = { 1, 3 }, + }, + }) +end) + +describe("forward barfing indentation", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + local function barf_forwards() + paredit.barf_forwards(opts) + end + + expect_all(barf_forwards, { + { + "should dedent the barfed child", + before_content = { "(", " a)" }, + before_cursor = { 1, 0 }, + after_content = { "()", "a" }, + after_cursor = { 1, 0 }, + }, + { + "should dedent a multi-line child and affected siblings", + before_content = { "(", " (a", " b c)) (a", " d)" }, + before_cursor = { 1, 0 }, + after_content = { "()", "(a", " b c) (a", "d)" }, + after_cursor = { 1, 0 }, + }, + { + "should not dedent if node is on the same line", + before_content = { "(a", "b c)" }, + before_cursor = { 1, 1 }, + after_content = { "(a", "b) c" }, + after_cursor = { 1, 1 }, + }, + { + "should not dedent when there is no indentation", + before_content = { "(", "a)" }, + before_cursor = { 1, 0 }, + after_content = { "()", "a" }, + after_cursor = { 1, 0 }, + }, + { + "should dedent the minimum amount without deleting chars", + before_content = { "(", " a) (b", " c)" }, + before_cursor = { 1, 0 }, + after_content = { "()", " a (b", "c)" }, + after_cursor = { 1, 0 }, + }, + { + "should dedent the correct node ignoring comments", + before_content = { "(", ";; comment", " a)" }, + before_cursor = { 1, 1 }, + after_content = { "()", ";; comment", "a" }, + after_cursor = { 1, 0 }, + }, + }) +end) + +describe("backward barfing indentation", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + local function barf_backwards() + paredit.barf_backwards(opts) + end + + expect_all(barf_backwards, { + { + "should dedent a nested child", + before_content = { "(a", " b)" }, + before_cursor = { 1, 0 }, + after_content = { "a", "(b)" }, + after_cursor = { 2, 1 }, + }, + }) +end) From 603dc8a2dda6ab0cfde875b247a969bde6d17732 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Sun, 10 Sep 2023 12:25:38 +0100 Subject: [PATCH 2/5] Save and restore cursor pos after indentation fix --- lua/nvim-paredit/indentation/native.lua | 30 ++++++++++++++++--------- tests/nvim-paredit/indentation_spec.lua | 9 +++++++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lua/nvim-paredit/indentation/native.lua b/lua/nvim-paredit/indentation/native.lua index f47da35..83e2dc8 100644 --- a/lua/nvim-paredit/indentation/native.lua +++ b/lua/nvim-paredit/indentation/native.lua @@ -1,5 +1,6 @@ local traversal = require("nvim-paredit.utils.traversal") local utils = require("nvim-paredit.indentation.utils") +local common = require("nvim-paredit.utils.common") local langs = require("nvim-paredit.lang") local M = {} @@ -34,6 +35,8 @@ local function dedent_lines(lines, delta, opts) {} ) end + + return smallest_distance end local function indent_lines(lines, delta, opts) @@ -41,19 +44,26 @@ local function indent_lines(lines, delta, opts) return end + local cursor_pos = vim.api.nvim_win_get_cursor(opts.buf or 0) + local cursor_delta = delta + if delta < 0 then - return dedent_lines(lines, delta * -1, opts) + cursor_delta = dedent_lines(lines, delta * -1, opts) * -1 + else + local chars = string.rep(" ", delta) + for _, line in ipairs(lines) do + -- stylua: ignore + vim.api.nvim_buf_set_text( + opts.buf or 0, + line, 0, + line, 0, + {chars} + ) + end end - local chars = string.rep(" ", delta) - for _, line in ipairs(lines) do - -- stylua: ignore - vim.api.nvim_buf_set_text( - opts.buf or 0, - line, 0, - line, 0, - {chars} - ) + if common.included_in_table(lines, cursor_pos[1] - 1) then + vim.api.nvim_win_set_cursor(opts.buf or 0, { cursor_pos[1], cursor_pos[2] + cursor_delta }) end end diff --git a/tests/nvim-paredit/indentation_spec.lua b/tests/nvim-paredit/indentation_spec.lua index 674ab6f..aad9b9e 100644 --- a/tests/nvim-paredit/indentation_spec.lua +++ b/tests/nvim-paredit/indentation_spec.lua @@ -163,7 +163,14 @@ describe("backward barfing indentation", function() before_content = { "(a", " b)" }, before_cursor = { 1, 0 }, after_content = { "a", "(b)" }, - after_cursor = { 2, 1 }, + after_cursor = { 2, 0 }, + }, + { + "should keep the cursor in the same place", + before_content = { "((a", " bc", " de))" }, + before_cursor = { 2, 3 }, + after_content = { "(a", " (bc", " de))" }, + after_cursor = { 2, 3 }, }, }) end) From 1f7b0924acd5e482a4c9c6222cceb0ad66876c79 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Sun, 10 Sep 2023 13:02:39 +0100 Subject: [PATCH 3/5] Use lhs sibling as indentation ref point --- lua/nvim-paredit/indentation/native.lua | 26 +++++++++++++++++---- lua/nvim-paredit/indentation/utils.lua | 31 +++++++++++++++++++++++++ tests/nvim-paredit/indentation_spec.lua | 24 +++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lua/nvim-paredit/indentation/native.lua b/lua/nvim-paredit/indentation/native.lua index 83e2dc8..53fd815 100644 --- a/lua/nvim-paredit/indentation/native.lua +++ b/lua/nvim-paredit/indentation/native.lua @@ -99,8 +99,17 @@ local function indent_barf(event) if parent:type() == "source" then delta = node_range[2] else - local form_edges = lang.get_form_edges(parent) - delta = node_range[2] - form_edges.left.range[2] - 1 + local row + local ref_node = utils.get_first_sibling_on_upper_line(node, { lang = lang }) + if ref_node then + local range = { ref_node:range() } + row = range[2] + else + local form_edges = lang.get_form_edges(parent) + row = form_edges.left.range[2] - 1 + end + + delta = node_range[2] - row end indent_lines(lines, delta * -1, { @@ -127,9 +136,18 @@ local function indent_slurp(event) end local lines = utils.find_affected_lines(child, utils.get_node_line_range(child_range)) - local form_edges = lang.get_form_edges(parent) - local delta = form_edges.left.range[4] - child_range[2] + local row + local ref_node = utils.get_first_sibling_on_upper_line(child, { lang = lang }) + if ref_node then + local range = { ref_node:range() } + row = range[2] + else + local form_edges = lang.get_form_edges(parent) + row = form_edges.left.range[4] + end + + local delta = row - child_range[2] indent_lines(lines, delta, { buf = event.buf, }) diff --git a/lua/nvim-paredit/indentation/utils.lua b/lua/nvim-paredit/indentation/utils.lua index 0afb09c..1c83cc9 100644 --- a/lua/nvim-paredit/indentation/utils.lua +++ b/lua/nvim-paredit/indentation/utils.lua @@ -61,4 +61,35 @@ function M.node_is_first_on_line(node, opts) return sibling_range[3] ~= node_range[1] end +-- This functions finds the closest sibling to a given `node` which is: +-- 1) Not on the same line (one line higher) +-- 2) Is the first node on the line +-- +-- This node can be used as an indentation reference point. +function M.get_first_sibling_on_upper_line(node, opts) + local node_range = { node:range() } + + local reference + local prev = node + + while prev do + prev = traversal.get_prev_sibling_ignoring_comments(prev, opts) + if not prev then + return reference + end + + local sibling_range = { prev:range() } + + if reference and reference:range() ~= sibling_range[1] then + return reference + end + + if sibling_range[1] ~= node_range[1] then + reference = prev + end + end + + return reference +end + return M diff --git a/tests/nvim-paredit/indentation_spec.lua b/tests/nvim-paredit/indentation_spec.lua index aad9b9e..33fb180 100644 --- a/tests/nvim-paredit/indentation_spec.lua +++ b/tests/nvim-paredit/indentation_spec.lua @@ -72,6 +72,14 @@ describe("forward slurping indentation", function() after_content = { "(", ";; comment", " a)" }, after_cursor = { 1, 0 }, }, + + { + "should indent to the first relevant siblings indentation", + before_content = { "(def a []", " target sibling)", "child" }, + before_cursor = { 1, 1 }, + after_content = { "(def a []", " target sibling", " child)" }, + after_cursor = { 1, 1 }, + }, }) end) @@ -148,6 +156,14 @@ describe("forward barfing indentation", function() after_content = { "()", ";; comment", "a" }, after_cursor = { 1, 0 }, }, + + { + "should indent to the first relevant siblings indentation", + before_content = { "(def a []", " target (sibling", " child))" }, + before_cursor = { 2, 10 }, + after_content = { "(def a []", " target (sibling)", " child)" }, + after_cursor = { 2, 10 }, + }, }) end) @@ -172,5 +188,13 @@ describe("backward barfing indentation", function() after_content = { "(a", " (bc", " de))" }, after_cursor = { 2, 3 }, }, + + { + "should indent to the first relevant siblings indentation", + before_content = { "(def a []", " target (sibling", " child))" }, + before_cursor = { 3, 1 }, + after_content = { "(def a []", " target sibling", " (child))" }, + after_cursor = { 3, 2 }, + }, }) end) From f1169317335b967dffc889c04701325e941e87a3 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Mon, 11 Sep 2023 14:25:40 +0100 Subject: [PATCH 4/5] Ensure indentation works on wrapped parents --- lua/nvim-paredit/indentation/native.lua | 12 +++++++----- tests/nvim-paredit/indentation_spec.lua | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lua/nvim-paredit/indentation/native.lua b/lua/nvim-paredit/indentation/native.lua index 53fd815..2aaec7b 100644 --- a/lua/nvim-paredit/indentation/native.lua +++ b/lua/nvim-paredit/indentation/native.lua @@ -70,14 +70,16 @@ end local function indent_barf(event) local lang = langs.get_language_api() + local form = lang.get_node_root(event.parent) + local lhs local node if event.type == "barf-forwards" then - node = traversal.get_next_sibling_ignoring_comments(event.parent, { lang = lang }) - lhs = event.parent + node = traversal.get_next_sibling_ignoring_comments(form, { lang = lang }) + lhs = form else - node = event.parent - lhs = traversal.get_prev_sibling_ignoring_comments(event.parent, { lang = lang }) + node = form + lhs = traversal.get_prev_sibling_ignoring_comments(form, { lang = lang }) end if not node or not lhs then @@ -118,8 +120,8 @@ local function indent_barf(event) end local function indent_slurp(event) - local parent = event.parent local lang = langs.get_language_api() + local parent = lang.unwrap_form(event.parent) local child if event.type == "slurp-forwards" then diff --git a/tests/nvim-paredit/indentation_spec.lua b/tests/nvim-paredit/indentation_spec.lua index 33fb180..53ab897 100644 --- a/tests/nvim-paredit/indentation_spec.lua +++ b/tests/nvim-paredit/indentation_spec.lua @@ -23,6 +23,13 @@ describe("forward slurping indentation", function() after_content = { "(", " a)" }, after_cursor = { 1, 0 }, }, + { + "should indent a nested child from a wrapped parent", + before_content = { "@()", "a" }, + before_cursor = { 1, 2 }, + after_content = { "@(", " a)" }, + after_cursor = { 1, 1 }, + }, { "should indent a multi-line child", before_content = { "()", "(a", " b c)" }, @@ -121,6 +128,13 @@ describe("forward barfing indentation", function() after_content = { "()", "a" }, after_cursor = { 1, 0 }, }, + { + "should dedent the barfed child from a wrapped parent", + before_content = { "@(", " a)" }, + before_cursor = { 1, 1 }, + after_content = { "@()", "a" }, + after_cursor = { 1, 1 }, + }, { "should dedent a multi-line child and affected siblings", before_content = { "(", " (a", " b c)) (a", " d)" }, From f40b42c21edd5790ce7642bd887c6af5f3530769 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Sun, 10 Sep 2023 16:15:44 +0100 Subject: [PATCH 5/5] Add README section on auto-indentation --- README.md | 80 +++++++++++++++++++++++++++++++++++ lua/nvim-paredit/defaults.lua | 2 +- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 926c5a4..c7cd957 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,27 @@ require("nvim-paredit").setup({ -- defaults to all supported file types including custom lang -- extensions (see next section) filetypes = { "clojure" }, + + -- This controls where the cursor is placed when performing slurp/barf operations + -- + -- - "remain" - It will never change the cursor position, keeping it in the same place + -- - "follow" - It will always place the cursor on the form edge that was moved + -- - "auto" - A combination of remain and follow, it will try keep the cursor in the original position + -- unless doing so would result in the cursor no longer being within the original form. In + -- this case it will place the cursor on the moved edge cursor_behaviour = "auto", -- remain, follow, auto + + indent = { + -- This controls how nvim-paredit handles indentation when performing operations which + -- should change the indentation of the form (such as when slurping or barfing). + -- + -- When set to true then it will attempt to fix the indentation of nodes operated on. + enabled = false, + -- A function that will be called after a slurp/barf if you want to provide a custom indentation + -- implementation. + indentor = require("nvim-paredit.indentation.native").indentor, + }, + -- list of default keybindings keys = { [">)"] = { paredit.api.slurp_forwards, "Slurp forwards" }, @@ -112,6 +132,65 @@ require("nvim-paredit").setup({ }) ``` +## Auto Indentation + +Nvim-paredit comes with built-in support for fixing form indentation when performing slurp and barf operations. By default this behaviour is disabled and can be enabled by setting `indent.enabled = true` in the [configuration](#configuration) + +The main goal of this implementation is to provide a visual aid to the user, allowing them to confirm they are operating on the correct node and to know when to stop when performing recursive slurp/barf operations. This implementation is fast and does not result in any UI lag or jitter. + +The goal is _not_ to be 100% correct. The implementation follows a simple set of rules which account for most scenarios but not all. If a more correct implementation is needed then the native implementation can be replaced by setting the configuration property `intent.indentor`. For example an implementation using `vim.lsp.buf.format` could be built if the user doesn't mind sacrificing performance for correctness. + +### Recipes + +
+ vim.lsp.buf.format + + Below is a reference implementation for using `vim.lsp.buf.format` to replace the native implementation. This implementation won't be nearly as performant but it will be more correct. + + ```lua + local function lsp_indent(event, opts) + local traversal = require("nvim-paredit.utils.traversal") + local utils = require("nvim-paredit.indentation.utils") + local langs = require("nvim-paredit.lang") + + local lang = langs.get_language_api() + + local parent = event.parent + + local child + if event.type == "slurp-forwards" then + child = parent:named_child(parent:named_child_count() - 1) + elseif event.type == "slurp-backwards" then + child = parent:named_child(1) + elseif event.type == "barf-forwards" then + child = traversal.get_next_sibling_ignoring_comments(event.parent, { lang = lang }) + elseif event.type == "barf-backwards" then + child = event.parent + else + return + end + + local child_range = { child:range() } + local lines = utils.find_affected_lines(child, utils.get_node_line_range(child_range)) + + vim.lsp.buf.format({ + bufnr = opts.buf or 0, + range = { + ["start"] = { lines[1] + 1, 0 }, + ["end"] = { lines[#lines] + 1, 0 }, + }, + }) + end + + require("nvim-paredit").setup({ + indent = { + enabled = true, + indentor = lsp_indent + } + }) + ``` +
+ ## Language Support As this is built using Treesitter it requires that you have the relevant Treesitter grammar installed for your language of choice. Additionally `nvim-paredit` will need explicit support for the treesitter grammar as the node names and metadata of nodes vary between languages. @@ -270,6 +349,7 @@ The main reasons you might want to consider `nvim-paredit` instead are: - Easier configuration and an exposed lua API - Control over how the cursor is moved during slurp/barf. (For example if you don't want the cursor to always be moved) - Recursive slurp/barf operations. If your cursor is in a nested form you can still slurp from the forms parent(s) +- Automatic form/element indentations on slurp/barf - Subjectively better out-of-the-box keybindings ### [vim-sexp-mappings-for-regular-people](https://github.com/tpope/vim-sexp-mappings-for-regular-people) diff --git a/lua/nvim-paredit/defaults.lua b/lua/nvim-paredit/defaults.lua index 9796505..1a5e4ae 100644 --- a/lua/nvim-paredit/defaults.lua +++ b/lua/nvim-paredit/defaults.lua @@ -62,7 +62,7 @@ M.defaults = { use_default_keys = true, cursor_behaviour = "auto", -- remain, follow, auto indent = { - enabled = true, + enabled = false, indentor = require("nvim-paredit.indentation.native").indentor, }, keys = {},