From a4b224a115a16c0c4aa321d4b6e8492509a34b2d Mon Sep 17 00:00:00 2001 From: Oliver Caldwell Date: Sat, 23 Sep 2023 14:23:49 +0100 Subject: [PATCH] Initial pass with top level form selection and deletion Could maybe do with more methods and tests, but this is already working and the tests are passing. --- README.md | 14 ++++ lua/nvim-paredit/api/deletions.lua | 34 ++++++++++ lua/nvim-paredit/api/init.lua | 4 ++ lua/nvim-paredit/api/selections.lua | 65 +++++++++++++++++++ lua/nvim-paredit/defaults.lua | 12 ++++ lua/nvim-paredit/utils/traversal.lua | 36 +++++++++- .../text_object_selections_spec.lua | 34 ++++++++++ 7 files changed, 196 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a47d5fa..eeb9b9c 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,18 @@ paredit.setup({ repeatable = false, mode = { "o", "v" } }, + ["aF"] = { + paredit.api.select_around_top_level_form, + "Around form", + repeatable = false, + mode = { "o", "v" } + }, + ["iF"] = { + paredit.api.select_in_top_level_form, + "In form", + repeatable = false, + mode = { "o", "v" } + }, ["ae"] = { paredit.api.select_element, "Around element", @@ -272,6 +284,8 @@ paredit.api.slurp_forwards() - **`raise_form`** - **`delete_form`** - **`delete_in_form`** +- **`delete_top_level_form`** +- **`delete_in_top_level_form`** - **`delete_element`** - **`move_to_next_element`** - **`move_to_prev_element`** diff --git a/lua/nvim-paredit/api/deletions.lua b/lua/nvim-paredit/api/deletions.lua index 7d24d2f..3bcb00b 100644 --- a/lua/nvim-paredit/api/deletions.lua +++ b/lua/nvim-paredit/api/deletions.lua @@ -18,6 +18,22 @@ function M.delete_form() ) end +function M.delete_top_level_form() + local range = selections.get_range_around_top_level_form() + if not range then + return + end + + local buf = vim.api.nvim_get_current_buf() + -- stylua: ignore + vim.api.nvim_buf_set_text( + buf, + range[1], range[2], + range[3], range[4], + {} + ) +end + function M.delete_in_form() local range = selections.get_range_in_form() if not range then @@ -36,6 +52,24 @@ function M.delete_in_form() vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) end +function M.delete_in_top_level_form() + local range = selections.get_range_in_top_level_form() + if not range then + return + end + + local buf = vim.api.nvim_get_current_buf() + -- stylua: ignore + vim.api.nvim_buf_set_text( + buf, + range[1], range[2], + range[3], range[4], + {} + ) + + vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) +end + function M.delete_element() local range = selections.get_element_range() if not range then diff --git a/lua/nvim-paredit/api/init.lua b/lua/nvim-paredit/api/init.lua index 381d8d5..ea655c5 100644 --- a/lua/nvim-paredit/api/init.lua +++ b/lua/nvim-paredit/api/init.lua @@ -25,10 +25,14 @@ local M = { select_around_form = selections.select_around_form, select_in_form = selections.select_in_form, + select_around_top_level_form = selections.select_around_top_level_form, + select_in_top_level_form = selections.select_in_top_level_form, select_element = selections.select_element, delete_form = deletions.delete_form, delete_in_form = deletions.delete_in_form, + delete_top_level_form = deletions.delete_top_level_form, + delete_in_top_level_form = deletions.delete_in_top_level_form, delete_element = deletions.delete_element, } diff --git a/lua/nvim-paredit/api/selections.lua b/lua/nvim-paredit/api/selections.lua index 69a4550..f65c8c7 100644 --- a/lua/nvim-paredit/api/selections.lua +++ b/lua/nvim-paredit/api/selections.lua @@ -30,6 +30,27 @@ function M.get_range_around_form() } end +function M.get_range_around_top_level_form() + local lang = langs.get_language_api() + local current_form = traversal.find_nearest_form(ts.get_node_at_cursor(), { + lang = lang, + use_source = false, + }) + if not current_form then + return + end + + local top_level_form = traversal.get_top_level_node_below_document(current_form) + local root = lang.get_node_root(top_level_form) + local range = { root:range() } + + -- stylua: ignore + return { + range[1], range[2], + range[3], range[4], + } +end + function M.select_around_form() local range = M.get_range_around_form() if not range then @@ -42,6 +63,18 @@ function M.select_around_form() vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 }) end +function M.select_around_top_level_form() + local range = M.get_range_around_top_level_form() + if not range then + return + end + + M.ensure_visual_mode() + vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) + vim.api.nvim_command("normal! o") + vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 }) +end + function M.get_range_in_form() local lang = langs.get_language_api() local current_form = traversal.find_nearest_form(ts.get_node_at_cursor(), { @@ -61,6 +94,26 @@ function M.get_range_in_form() } end +function M.get_range_in_top_level_form() + local lang = langs.get_language_api() + local current_form = traversal.find_nearest_form(ts.get_node_at_cursor(), { + lang = lang, + use_source = false, + }) + if not current_form then + return + end + + local top_level_form = traversal.get_top_level_node_below_document(current_form) + local edges = lang.get_form_edges(top_level_form) + + -- stylua: ignore + return { + edges.left.range[3], edges.left.range[4], + edges.right.range[1], edges.right.range[2], + } +end + function M.select_in_form() local range = M.get_range_in_form() if not range then @@ -73,6 +126,18 @@ function M.select_in_form() vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 }) end +function M.select_in_top_level_form() + local range = M.get_range_in_top_level_form() + if not range then + return + end + + M.ensure_visual_mode() + vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) + vim.api.nvim_command("normal! o") + vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 }) +end + function M.get_element_range() local lang = langs.get_language_api() local node = ts.get_node_at_cursor() diff --git a/lua/nvim-paredit/defaults.lua b/lua/nvim-paredit/defaults.lua index 1a5e4ae..4978fd8 100644 --- a/lua/nvim-paredit/defaults.lua +++ b/lua/nvim-paredit/defaults.lua @@ -43,6 +43,18 @@ M.default_keys = { repeatable = false, mode = { "o", "v" }, }, + ["aF"] = { + api.select_around_top_level_form, + "Around form", + repeatable = false, + mode = { "o", "v" }, + }, + ["iF"] = { + api.select_in_top_level_form, + "In form", + repeatable = false, + mode = { "o", "v" }, + }, ["ae"] = { api.select_element, diff --git a/lua/nvim-paredit/utils/traversal.lua b/lua/nvim-paredit/utils/traversal.lua index beb4b08..5e30356 100644 --- a/lua/nvim-paredit/utils/traversal.lua +++ b/lua/nvim-paredit/utils/traversal.lua @@ -33,14 +33,14 @@ end function M.get_last_child_ignoring_comments(node, opts) return get_child_ignoring_comments(node, node:named_child_count() - 1, { direction = -1, - lang = opts.lang + lang = opts.lang, }) end function M.get_first_child_ignoring_comments(node, opts) return get_child_ignoring_comments(node, 0, { direction = 1, - lang = opts.lang + lang = opts.lang, }) end @@ -89,7 +89,7 @@ local function get_sibling_ignoring_comments(node, opts) elseif opts.count > 1 then local new_opts = vim.tbl_deep_extend("force", opts, { count = opts.count - 1, - sibling = sibling + sibling = sibling, }) return get_sibling_ignoring_comments(sibling, new_opts) end @@ -139,4 +139,34 @@ function M.find_root_element_relative_to(root, child) return M.find_root_element_relative_to(root, parent) end +function M.get_top_level_node_below_document(node) + -- Document + -- - Branch A + -- -- Node X + -- --- Sub-node 1 + -- - Branch B + -- -- Node Y + -- --- Sub-node 2 + -- --- Sub-node 3 + + -- If we call this function on "Sub-node 2" we expect "Branch B" to be + -- returned, the top level one below the document itself. We know which + -- node is the document because it lacks a parent, just like Batman. + + local parent = node:parent() + + -- Does the node have a parent? If so, we might be at the right level. + -- If not, we should just return the node right away, we're already too high. + if parent then + -- If the parent _also_ has a parent then we still need to go higher, recur. + if parent:parent() then + return M.get_top_level_node_below_document(parent) + end + end + + -- As soon as we don't have a grandparent or parent, return the node + -- we're on because it means we're one step below the top level document node. + return node +end + return M diff --git a/tests/nvim-paredit/text_object_selections_spec.lua b/tests/nvim-paredit/text_object_selections_spec.lua index d36550c..83478fe 100644 --- a/tests/nvim-paredit/text_object_selections_spec.lua +++ b/tests/nvim-paredit/text_object_selections_spec.lua @@ -76,6 +76,40 @@ describe("form deletions", function() end) end) +describe("top level form deletions", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + + before_each(function() + keybindings.setup_keybindings({ + keys = defaults.default_keys, + }) + end) + + it("should delete the top level form, leaving other forms intact", function() + prepare_buffer({ + content = { "(+ 1 2)", "(foo (a", "b", "c)) (comment thing)", "(x y)" }, + cursor = { 2, 7 }, + }) + feedkeys("daF") + expect({ + content = { "(+ 1 2)", " (comment thing)", "(x y)" }, + cursor = { 2, 0 }, + }) + end) + + it("should delete inside the top level form, leaving other forms and the outer parenthesis pair intact", function() + prepare_buffer({ + content = { "(+ 1 2)", "(foo (a", "b", "c)) (comment thing)", "(x y)" }, + cursor = { 2, 7 }, + }) + feedkeys("diF") + expect({ + content = { "(+ 1 2)", "() (comment thing)", "(x y)" }, + cursor = { 2, 1 }, + }) + end) +end) + describe("form selections", function() vim.api.nvim_buf_set_option(0, "filetype", "clojure")