diff --git a/README.md b/README.md index a47d5fa..515b577 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 top level form", + repeatable = false, + mode = { "o", "v" } + }, + ["iF"] = { + paredit.api.select_in_top_level_form, + "In top level 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..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,8 +17,15 @@ function M.delete_form() ) end -function M.delete_in_form() - local range = selections.get_range_in_form() +function M.delete_form() + delete_form_impl(selections.get_range_around_form()) +end + +function M.delete_top_level_form() + delete_form_impl(selections.get_range_around_top_level_form()) +end + +local function delete_in_form_impl(range) if not range then return end @@ -36,6 +42,14 @@ function M.delete_in_form() vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) end +function M.delete_in_form() + delete_in_form_impl(selections.get_range_in_form()) +end + +function M.delete_in_top_level_form() + delete_in_form_impl(selections.get_range_in_top_level_form()) +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..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,7 +20,13 @@ function M.get_range_around_form() return end - local root = lang.get_node_root(current_form) + local selected = current_form + + if node_fn then + selected = node_fn(selected) + end + + local root = lang.get_node_root(selected) local range = { root:range() } -- stylua: ignore @@ -30,8 +36,15 @@ function M.get_range_around_form() } end -function M.select_around_form() - local range = M.get_range_around_form() +function M.get_range_around_form() + return get_range_around_form_impl() +end + +function M.get_range_around_top_level_form() + return get_range_around_form_impl(traversal.get_top_level_node_below_document) +end + +local function select_around_form_impl(range) if not range then return end @@ -42,7 +55,15 @@ function M.select_around_form() vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 }) end -function M.get_range_in_form() +function M.select_around_form() + return select_around_form_impl(M.get_range_around_form()) +end + +function M.select_around_top_level_form() + return select_around_form_impl(M.get_range_around_top_level_form()) +end + +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, @@ -52,7 +73,13 @@ function M.get_range_in_form() return end - local edges = lang.get_form_edges(current_form) + local selected = current_form + + if node_fn then + selected = node_fn(selected) + end + + local edges = lang.get_form_edges(selected) -- stylua: ignore return { @@ -61,8 +88,15 @@ function M.get_range_in_form() } end -function M.select_in_form() - local range = M.get_range_in_form() +function M.get_range_in_form() + return get_range_in_form_impl() +end + +function M.get_range_in_top_level_form() + return get_range_in_form_impl(traversal.get_top_level_node_below_document) +end + +local function select_in_form_impl(range) if not range then return end @@ -73,6 +107,14 @@ function M.select_in_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() diff --git a/lua/nvim-paredit/defaults.lua b/lua/nvim-paredit/defaults.lua index 1a5e4ae..55f3af8 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 top level form", + repeatable = false, + mode = { "o", "v" }, + }, + ["iF"] = { + api.select_in_top_level_form, + "In top level 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..1fb4e5f 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") @@ -104,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")