diff --git a/README.md b/README.md index c97b7e7..3e036dc 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The goal of `nvim-paredit` is to provide a comparable s-expression editing experience in Neovim to that provided by Emacs. This is what is provided: -- Treesitter based lisp structural editing and cursor motions +- Treesitter based lisp structural editing, cursor motions and text object selections - Dot-repeatable keybindings - Language extensibility - Programmable API @@ -80,6 +80,20 @@ require("nvim-paredit").setup({ "Jump to previous element head", repeatable = false }, + + -- These are text object selection keybindings which can used with standard `d, y, c` + ["af"] = { + api.select_around_form, + "Around form", + repeatable = false, + mode = { "o" } + }, + ["if"] = { + api.select_in_form, + "In form", + repeatable = false, + mode = { "o" } + }, } }) ``` diff --git a/lua/nvim-paredit/api/deletions.lua b/lua/nvim-paredit/api/deletions.lua index d91aa64..7d24d2f 100644 --- a/lua/nvim-paredit/api/deletions.lua +++ b/lua/nvim-paredit/api/deletions.lua @@ -1,22 +1,13 @@ -local traversal = require("nvim-paredit.utils.traversal") -local ts = require("nvim-treesitter.ts_utils") -local langs = require("nvim-paredit.lang") +local selections = require("nvim-paredit.api.selections") local M = {} function M.delete_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 + local range = selections.get_range_around_form() + if not range then return end - local root = lang.get_node_root(current_form) - local range = { root:range() } - local buf = vim.api.nvim_get_current_buf() -- stylua: ignore vim.api.nvim_buf_set_text( @@ -28,39 +19,29 @@ function M.delete_form() end function M.delete_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 + local range = selections.get_range_in_form() + if not range then return end - local edges = lang.get_form_edges(current_form) - local buf = vim.api.nvim_get_current_buf() -- stylua: ignore vim.api.nvim_buf_set_text( buf, - edges.left.range[3], edges.left.range[4], - edges.right.range[1], edges.right.range[2], + range[1], range[2], + range[3], range[4], {} ) - vim.api.nvim_win_set_cursor(0, { edges.left.range[3] + 1, edges.left.range[4] }) + vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) end function M.delete_element() - local lang = langs.get_language_api() - local node = ts.get_node_at_cursor() - if not node then + local range = selections.get_element_range() + if not range then return end - local root = lang.get_node_root(node) - local range = { root:range() } - local buf = vim.api.nvim_get_current_buf() -- stylua: ignore vim.api.nvim_buf_set_text( diff --git a/lua/nvim-paredit/api/init.lua b/lua/nvim-paredit/api/init.lua index 761de9e..381d8d5 100644 --- a/lua/nvim-paredit/api/init.lua +++ b/lua/nvim-paredit/api/init.lua @@ -3,6 +3,7 @@ local barfing = require("nvim-paredit.api.barfing") local dragging = require("nvim-paredit.api.dragging") local raising = require("nvim-paredit.api.raising") local motions = require("nvim-paredit.api.motions") +local selections = require("nvim-paredit.api.selections") local deletions = require("nvim-paredit.api.deletions") local M = { @@ -22,6 +23,10 @@ local M = { move_to_next_element = motions.move_to_next_element, move_to_prev_element = motions.move_to_prev_element, + select_around_form = selections.select_around_form, + select_in_form = selections.select_in_form, + select_element = selections.select_element, + delete_form = deletions.delete_form, delete_in_form = deletions.delete_in_form, delete_element = deletions.delete_element, diff --git a/lua/nvim-paredit/api/selections.lua b/lua/nvim-paredit/api/selections.lua new file mode 100644 index 0000000..2ad85d6 --- /dev/null +++ b/lua/nvim-paredit/api/selections.lua @@ -0,0 +1,105 @@ +local traversal = require("nvim-paredit.utils.traversal") +local ts = require("nvim-treesitter.ts_utils") +local langs = require("nvim-paredit.lang") + +local M = {} + +function M.ensure_visual_mode() + if vim.api.nvim_get_mode().mode ~= "v" then + vim.api.nvim_command("normal! v") + end +end + +function M.get_range_around_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 root = lang.get_node_root(current_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 + 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(), { + lang = lang, + use_source = false, + }) + if not current_form then + return + end + + local edges = lang.get_form_edges(current_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 + 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() + if not node then + return + end + + local root = lang.get_node_root(node) + local range = { root:range() } + + -- stylua: ignore + return { + range[1], range[2], + range[3], range[4] + } +end + +function M.select_element() + local range = M.get_element_range() + 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] }) +end + +return M diff --git a/lua/nvim-paredit/defaults.lua b/lua/nvim-paredit/defaults.lua index 93dfeed..cc216e4 100644 --- a/lua/nvim-paredit/defaults.lua +++ b/lua/nvim-paredit/defaults.lua @@ -30,6 +30,19 @@ M.default_keys = { repeatable = false, operator = true, }, + + ["af"] = { + api.select_around_form, + "Around form", + repeatable = false, + mode = { "o", "v" } + }, + ["if"] = { + api.select_in_form, + "In form", + repeatable = false, + mode = { "o", "v" } + }, } M.defaults = { diff --git a/lua/nvim-paredit/utils/keybindings.lua b/lua/nvim-paredit/utils/keybindings.lua index 004af1a..8adfacf 100644 --- a/lua/nvim-paredit/utils/keybindings.lua +++ b/lua/nvim-paredit/utils/keybindings.lua @@ -42,10 +42,12 @@ function M.setup_keybindings(opts) fn = M.with_repeat(fn) end - vim.keymap.set({ "n", "x" }, keymap, fn, { + vim.keymap.set(action.mode or { "n", "x" }, keymap, fn, { desc = action[2], buffer = opts.buf or 0, expr = repeatable, + remap = false, + silent = true, }) if operator then @@ -53,6 +55,8 @@ function M.setup_keybindings(opts) desc = action[2], buffer = opts.buf or 0, expr = repeatable, + remap = false, + silent = true, }) end end diff --git a/tests/nvim-paredit/text_object_selections_spec.lua b/tests/nvim-paredit/text_object_selections_spec.lua new file mode 100644 index 0000000..9abdf51 --- /dev/null +++ b/tests/nvim-paredit/text_object_selections_spec.lua @@ -0,0 +1,102 @@ +local paredit = require("nvim-paredit.api") + +local prepare_buffer = require("tests.nvim-paredit.utils").prepare_buffer +local feedkeys = require("tests.nvim-paredit.utils").feedkeys +local expect = require("tests.nvim-paredit.utils").expect +local utils = require("tests.nvim-paredit.utils") + +describe("form deletions", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + + before_each(function() + vim.keymap.set("o", "af", paredit.select_around_form, { buffer = true, remap = false }) + vim.keymap.set("o", "if", paredit.select_in_form, { buffer = true, remap = false }) + end) + + it("should delete the form", function() + prepare_buffer({ + content = "(a a)", + cursor = { 1, 1 }, + }) + feedkeys("daf") + expect({ + content = "", + cursor = { 1, 0 }, + }) + end) + + it("should delete a multi line form", function() + prepare_buffer({ + content = { "(a", "b", "c)" }, + cursor = { 1, 1 }, + }) + feedkeys("daf") + expect({ + content = "", + cursor = { 1, 0 }, + }) + end) + + it("should delete a nested form", function() + prepare_buffer({ + content = "(a (b c))", + cursor = { 1, 5 }, + }) + feedkeys("daf") + expect({ + content = "(a )", + cursor = { 1, 3 }, + }) + end) + + it("should delete everything in the form", function() + prepare_buffer({ + content = "(a b)", + cursor = { 1, 2 }, + }) + feedkeys("dif") + expect({ + content = "()", + cursor = { 1, 1 }, + }) + end) + + it("should delete everything within a multi line form", function() + prepare_buffer({ + content = { "(a", "b", "c)" }, + cursor = { 2, 0 }, + }) + feedkeys("dif") + expect({ + content = "()", + cursor = { 1, 1 }, + }) + end) +end) + +describe("form selections", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + + before_each(function() + vim.keymap.set("v", "af", paredit.select_around_form, { buffer = true, remap = false }) + vim.keymap.set("v", "if", paredit.select_in_form, { buffer = true, remap = false }) + end) + + it("should select the form", function() + prepare_buffer({ + content = "(a a)", + cursor = { 1, 1 }, + }) + feedkeys("vaf") + assert.are.same("(a a)", utils.get_selected_text()) + end) + + it("should select within the form", function() + prepare_buffer({ + content = "(a a)", + cursor = { 1, 1 }, + }) + feedkeys("vif") + assert.are.same("a a", utils.get_selected_text()) + end) +end) diff --git a/tests/nvim-paredit/utils.lua b/tests/nvim-paredit/utils.lua index 3db5c18..9b3c454 100644 --- a/tests/nvim-paredit/utils.lua +++ b/tests/nvim-paredit/utils.lua @@ -58,4 +58,9 @@ function M.expect_all(action, expectations) end end +function M.get_selected_text() + vim.cmd('noau normal! "vy"') + return vim.fn.getreg("v") +end + return M