From a4b224a115a16c0c4aa321d4b6e8492509a34b2d Mon Sep 17 00:00:00 2001 From: Oliver Caldwell Date: Sat, 23 Sep 2023 14:23:49 +0100 Subject: [PATCH 1/5] 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") From 4445caa6c2903221c86e240d6323387c2dd44401 Mon Sep 17 00:00:00 2001 From: Oliver Caldwell Date: Sat, 23 Sep 2023 14:35:49 +0100 Subject: [PATCH 2/5] Fix defaults mapping documentation for top level tools --- README.md | 4 ++-- lua/nvim-paredit/defaults.lua | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eeb9b9c..515b577 100644 --- a/README.md +++ b/README.md @@ -119,13 +119,13 @@ paredit.setup({ }, ["aF"] = { paredit.api.select_around_top_level_form, - "Around form", + "Around top level form", repeatable = false, mode = { "o", "v" } }, ["iF"] = { paredit.api.select_in_top_level_form, - "In form", + "In top level form", repeatable = false, mode = { "o", "v" } }, diff --git a/lua/nvim-paredit/defaults.lua b/lua/nvim-paredit/defaults.lua index 4978fd8..55f3af8 100644 --- a/lua/nvim-paredit/defaults.lua +++ b/lua/nvim-paredit/defaults.lua @@ -45,13 +45,13 @@ M.default_keys = { }, ["aF"] = { api.select_around_top_level_form, - "Around form", + "Around top level form", repeatable = false, mode = { "o", "v" }, }, ["iF"] = { api.select_in_top_level_form, - "In form", + "In top level form", repeatable = false, mode = { "o", "v" }, }, From e94263e231d239a41d1228c9462fe3cc407806ce Mon Sep 17 00:00:00 2001 From: Oliver Caldwell Date: Sat, 23 Sep 2023 14:46:02 +0100 Subject: [PATCH 3/5] Deduplicate selections code, reusing common code between current and top level form selections --- lua/nvim-paredit/api/selections.lua | 99 +++++++++++------------------ 1 file changed, 38 insertions(+), 61 deletions(-) diff --git a/lua/nvim-paredit/api/selections.lua b/lua/nvim-paredit/api/selections.lua index f65c8c7..1531801 100644 --- a/lua/nvim-paredit/api/selections.lua +++ b/lua/nvim-paredit/api/selections.lua @@ -10,7 +10,7 @@ function M.ensure_visual_mode() end end -function M.get_range_around_form() +local function get_range_around_form_impl(node_fn) local lang = langs.get_language_api() local current_form = traversal.find_nearest_form(ts.get_node_at_cursor(), { lang = lang, @@ -20,28 +20,13 @@ function M.get_range_around_form() return end - local root = lang.get_node_root(current_form) - local range = { root:range() } - - -- stylua: ignore - return { - range[1], range[2], - range[3], range[4], - } -end + local selected = current_form -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 + if node_fn then + selected = node_fn(selected) end - local top_level_form = traversal.get_top_level_node_below_document(current_form) - local root = lang.get_node_root(top_level_form) + local root = lang.get_node_root(selected) local range = { root:range() } -- stylua: ignore @@ -51,20 +36,15 @@ function M.get_range_around_top_level_form() } end -function M.select_around_form() - local range = M.get_range_around_form() - if not range then - return - end +function M.get_range_around_form() + return get_range_around_form_impl() +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 }) +function M.get_range_around_top_level_form() + return get_range_around_form_impl(traversal.get_top_level_node_below_document) end -function M.select_around_top_level_form() - local range = M.get_range_around_top_level_form() +local function select_around_form_impl(range) if not range then return end @@ -75,26 +55,15 @@ function M.select_around_top_level_form() 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(), { - lang = lang, - use_source = false, - }) - if not current_form then - return - end - - local edges = lang.get_form_edges(current_form) +function M.select_around_form() + return select_around_form_impl(M.get_range_around_form()) +end - -- stylua: ignore - return { - edges.left.range[3], edges.left.range[4], - edges.right.range[1], edges.right.range[2], - } +function M.select_around_top_level_form() + return select_around_form_impl(M.get_range_around_top_level_form()) end -function M.get_range_in_top_level_form() +local function get_range_in_form_impl(node_fn) local lang = langs.get_language_api() local current_form = traversal.find_nearest_form(ts.get_node_at_cursor(), { lang = lang, @@ -104,8 +73,13 @@ function M.get_range_in_top_level_form() return end - local top_level_form = traversal.get_top_level_node_below_document(current_form) - local edges = lang.get_form_edges(top_level_form) + local selected = current_form + + if node_fn then + selected = node_fn(selected) + end + + local edges = lang.get_form_edges(selected) -- stylua: ignore return { @@ -114,20 +88,15 @@ function M.get_range_in_top_level_form() } end -function M.select_in_form() - local range = M.get_range_in_form() - if not range then - return - end +function M.get_range_in_form() + return get_range_in_form_impl() +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 }) +function M.get_range_in_top_level_form() + return get_range_in_form_impl(traversal.get_top_level_node_below_document) end -function M.select_in_top_level_form() - local range = M.get_range_in_top_level_form() +local function select_in_form_impl(range) if not range then return end @@ -138,6 +107,14 @@ function M.select_in_top_level_form() vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 }) end +function M.select_in_form() + return select_in_form_impl(M.get_range_in_form()) +end + +function M.select_in_top_level_form() + return select_in_form_impl(M.get_range_in_top_level_form()) +end + function M.get_element_range() local lang = langs.get_language_api() local node = ts.get_node_at_cursor() From e757bf61798d411605954e62cb2a95b2a1cba9ae Mon Sep 17 00:00:00 2001 From: Oliver Caldwell Date: Sat, 23 Sep 2023 14:51:50 +0100 Subject: [PATCH 4/5] Deduplicate deletions code, combine functions into shared, configurable local functions --- lua/nvim-paredit/api/deletions.lua | 44 ++++++++---------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/lua/nvim-paredit/api/deletions.lua b/lua/nvim-paredit/api/deletions.lua index 3bcb00b..b54a2aa 100644 --- a/lua/nvim-paredit/api/deletions.lua +++ b/lua/nvim-paredit/api/deletions.lua @@ -2,8 +2,7 @@ local selections = require("nvim-paredit.api.selections") local M = {} -function M.delete_form() - local range = selections.get_range_around_form() +local function delete_form_impl(range) if not range then return end @@ -18,24 +17,15 @@ 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 +function M.delete_form() + delete_form_impl(selections.get_range_around_form()) +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], - {} - ) +function M.delete_top_level_form() + delete_form_impl(selections.get_range_around_top_level_form()) end -function M.delete_in_form() - local range = selections.get_range_in_form() +local function delete_in_form_impl(range) if not range then return end @@ -52,22 +42,12 @@ 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], - {} - ) +function M.delete_in_form() + delete_in_form_impl(selections.get_range_in_form()) +end - vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) +function M.delete_in_top_level_form() + delete_in_form_impl(selections.get_range_in_top_level_form()) end function M.delete_element() From d75689a5d94f8617d21a9446d5b485b9d6245edf Mon Sep 17 00:00:00 2001 From: Oliver Caldwell Date: Sat, 23 Sep 2023 14:54:41 +0100 Subject: [PATCH 5/5] Add a test for visually selecting top level forms --- .../text_object_selections_spec.lua | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/nvim-paredit/text_object_selections_spec.lua b/tests/nvim-paredit/text_object_selections_spec.lua index 83478fe..1fb4e5f 100644 --- a/tests/nvim-paredit/text_object_selections_spec.lua +++ b/tests/nvim-paredit/text_object_selections_spec.lua @@ -138,6 +138,34 @@ describe("form selections", function() end) end) +describe("top form selections", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + + before_each(function() + keybindings.setup_keybindings({ + keys = defaults.default_keys, + }) + end) + + it("should select the root form and not the siblings", function() + prepare_buffer({ + content = {"(+ 1 2)", "(foo (a", "a)) (/ 6 2)"}, + cursor = { 2, 6 }, + }) + feedkeys("vaF") + assert.are.same("(foo (a\na))", utils.get_selected_text()) + end) + + it("should select within the form", function() + prepare_buffer({ + content = {"(+ 1 2)", "(foo (a", "a)) (/ 6 2)"}, + cursor = { 2, 6 }, + }) + feedkeys("viF") + assert.are.same("foo (a\na)", utils.get_selected_text()) + end) +end) + describe("element deletions", function() vim.api.nvim_buf_set_option(0, "filetype", "clojure")