From edfcf4a921fedf974695d95bf4e120f1302cf751 Mon Sep 17 00:00:00 2001 From: Artem Medeu Date: Mon, 7 Aug 2023 11:23:32 +0600 Subject: [PATCH 1/7] fixes(#15): motions wasn't working correctly when cursor is in comment --- lua/nvim-paredit/api/motions.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/nvim-paredit/api/motions.lua b/lua/nvim-paredit/api/motions.lua index 0030ec0..0b72eab 100644 --- a/lua/nvim-paredit/api/motions.lua +++ b/lua/nvim-paredit/api/motions.lua @@ -88,6 +88,9 @@ function M._move_to_element(count, reversed) end end + if lang.node_is_comment(current_node) then + count = count + 1 + end local next_pos if is_in_middle and count == 1 then next_pos = node_edge From f467e4661e8ad28c6c7347bb75ed7a0a47c8a6de Mon Sep 17 00:00:00 2001 From: Artem Medeu Date: Mon, 7 Aug 2023 11:26:39 +0600 Subject: [PATCH 2/7] add test --- tests/nvim-paredit/motion_spec.lua | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/nvim-paredit/motion_spec.lua b/tests/nvim-paredit/motion_spec.lua index 4425429..dbc8826 100644 --- a/tests/nvim-paredit/motion_spec.lua +++ b/tests/nvim-paredit/motion_spec.lua @@ -74,6 +74,33 @@ describe("motions", function() }) end) + it("make an extra motion if cursor is in comment", function() + prepare_buffer({ + content = { "(aa", ";; comment", "bb)" }, + cursor = { 2, 3 }, + }) + paredit.move_to_next_element() + expect({ + cursor = { 3, 1 }, + }) + paredit.move_to_prev_element() + expect({ + cursor = { 3, 0 }, + }) + paredit.move_to_prev_element() + expect({ + cursor = { 1, 1 }, + }) + prepare_buffer({ + content = { "(aa", ";; comment", "bb)" }, + cursor = { 2, 3 }, + }) + paredit.move_to_prev_element() + expect({ + cursor = { 1, 1 }, + }) + end) + it("should move to the end of the current form before jumping to next", function() expect_all(paredit.move_to_next_element, { { From 2372e21dd723641486047cf8cf00c6080f04f6eb Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Mon, 7 Aug 2023 12:22:04 +0100 Subject: [PATCH 3/7] Allow passing no opts to setup() fixes #18 --- lua/nvim-paredit/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/nvim-paredit/init.lua b/lua/nvim-paredit/init.lua index d5f3a83..3da9860 100644 --- a/lua/nvim-paredit/init.lua +++ b/lua/nvim-paredit/init.lua @@ -21,6 +21,8 @@ local function setup_keybingings(filetype, buf) end function M.setup(opts) + opts = opts or {} + for filetype, api in pairs(opts.extensions or {}) do lang.add_language_extension(filetype, api) end From 041039b2ad81ee5bb4c19648aba9fb0c68262312 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Wed, 9 Aug 2023 22:44:51 +0100 Subject: [PATCH 4/7] Add API's for various deletion operations This implements a set of API's for deleting nodes and forms. + `delete_form` - Delete the entire form under the cursor + `delete_in_form` - Deletes all elements within the form under the cursor + `delete_element` - Deletes the element under the cursor Addresses #12 --- README.md | 3 + lua/nvim-paredit/api/deletions.lua | 74 ++++++++ lua/nvim-paredit/api/init.lua | 5 + tests/nvim-paredit/deletions_spec.lua | 242 ++++++++++++++++++++++++++ 4 files changed, 324 insertions(+) create mode 100644 lua/nvim-paredit/api/deletions.lua create mode 100644 tests/nvim-paredit/deletions_spec.lua diff --git a/README.md b/README.md index 1c8fe5f..c97b7e7 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,9 @@ paredit.api.slurp_forwards() - **`drag_form_backwards`** - **`raise_element`** - **`raise_form`** +- **`delete_form`** +- **`delete_in_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 new file mode 100644 index 0000000..d91aa64 --- /dev/null +++ b/lua/nvim-paredit/api/deletions.lua @@ -0,0 +1,74 @@ +local traversal = require("nvim-paredit.utils.traversal") +local ts = require("nvim-treesitter.ts_utils") +local langs = require("nvim-paredit.lang") + +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 + 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( + buf, + range[1], range[2], + range[3], range[4], + {} + ) +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 + 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], + {} + ) + + vim.api.nvim_win_set_cursor(0, { edges.left.range[3] + 1, edges.left.range[4] }) +end + +function M.delete_element() + 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() } + + 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 + +return M diff --git a/lua/nvim-paredit/api/init.lua b/lua/nvim-paredit/api/init.lua index 4287812..761de9e 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 deletions = require("nvim-paredit.api.deletions") local M = { slurp_forwards = slurping.slurp_forwards, @@ -20,6 +21,10 @@ local M = { move_to_next_element = motions.move_to_next_element, move_to_prev_element = motions.move_to_prev_element, + + delete_form = deletions.delete_form, + delete_in_form = deletions.delete_in_form, + delete_element = deletions.delete_element, } return M diff --git a/tests/nvim-paredit/deletions_spec.lua b/tests/nvim-paredit/deletions_spec.lua new file mode 100644 index 0000000..9b7436e --- /dev/null +++ b/tests/nvim-paredit/deletions_spec.lua @@ -0,0 +1,242 @@ +local paredit = require("nvim-paredit.api") + +local prepare_buffer = require("tests.nvim-paredit.utils").prepare_buffer +local expect_all = require("tests.nvim-paredit.utils").expect_all +local expect = require("tests.nvim-paredit.utils").expect + +describe("form deletions", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + + it("should delete the form", function() + prepare_buffer({ + content = "(a)", + cursor = { 1, 1 }, + }) + paredit.delete_form() + expect({ + content = "", + cursor = { 1, 0 }, + }) + end) + + it("should delete a multi line form", function() + prepare_buffer({ + content = { "(a", "b", "c)" }, + cursor = { 1, 1 }, + }) + paredit.delete_form() + expect({ + content = "", + cursor = { 1, 0 }, + }) + end) + + it("should delete a nested form", function() + prepare_buffer({ + content = "(a (a b c))", + cursor = { 1, 5 }, + }) + paredit.delete_form() + expect({ + content = "(a )", + cursor = { 1, 3 }, + }) + end) + + it("should delete different form types", function() + expect_all(paredit.delete_form, { + { + "list", + before_content = "(a)", + before_cursor = { 1, 1 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "vector", + before_content = "[a]", + before_cursor = { 1, 1 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "quoted list", + before_content = "`(a)", + before_cursor = { 1, 2 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "quoted list", + before_content = "'(a)", + before_cursor = { 1, 2 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "anon fn", + before_content = "#(a)", + before_cursor = { 1, 2 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "set", + before_content = "#{a}", + before_cursor = { 1, 2 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + }) + end) +end) + +describe("form inner deletions", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + + it("should delete everything in the form", function() + prepare_buffer({ + content = "(a b)", + cursor = { 1, 2 }, + }) + paredit.delete_in_form() + 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 }, + }) + paredit.delete_in_form() + expect({ + content = "()", + cursor = { 1, 1 }, + }) + end) + + it("should delete everyting within a nested form", function() + prepare_buffer({ + content = "(a (a b c))", + cursor = { 1, 5 }, + }) + paredit.delete_in_form() + expect({ + content = "(a ())", + cursor = { 1, 4 }, + }) + end) + + it("should delete within different form types", function() + expect_all(paredit.delete_in_form, { + { + "list", + before_content = "(a)", + before_cursor = { 1, 1 }, + after_content = "()", + after_cursor = { 1, 1 }, + }, + { + "vector", + before_content = "[a]", + before_cursor = { 1, 1 }, + after_content = "[]", + after_cursor = { 1, 1 }, + }, + { + "quoted list", + before_content = "`(a)", + before_cursor = { 1, 2 }, + after_content = "`()", + after_cursor = { 1, 2 }, + }, + { + "quoted list", + before_content = "'(a)", + before_cursor = { 1, 2 }, + after_content = "'()", + after_cursor = { 1, 2 }, + }, + { + "anon fn", + before_content = "#(a)", + before_cursor = { 1, 2 }, + after_content = "#()", + after_cursor = { 1, 2 }, + }, + { + "set", + before_content = "#{a}", + before_cursor = { 1, 2 }, + after_content = "#{}", + after_cursor = { 1, 2 }, + }, + }) + end) +end) + +describe("element deletions", function() + vim.api.nvim_buf_set_option(0, "filetype", "clojure") + + it("should delete the element under cursor", function() + prepare_buffer({ + content = "(ab cd)", + cursor = { 1, 4 }, + }) + paredit.delete_element() + expect({ + content = "(ab )", + cursor = { 1, 4 }, + }) + end) + + it("should delete different element types", function() + expect_all(paredit.delete_element, { + { + "list", + before_content = "(a)", + before_cursor = { 1, 0 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "vector", + before_content = "[a]", + before_cursor = { 1, 0 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "quoted list", + before_content = "`(a)", + before_cursor = { 1, 0 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "quoted list", + before_content = "'(a)", + before_cursor = { 1, 0 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "anon fn", + before_content = "#(a)", + before_cursor = { 1, 0 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + { + "set", + before_content = "#{a}", + before_cursor = { 1, 0 }, + after_content = "", + after_cursor = { 1, 0 }, + }, + }) + end) +end) From 53deaeb7fa2ca9763c31c493c6437d48d333959e Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Thu, 10 Aug 2023 11:19:38 +0100 Subject: [PATCH 5/7] Add text object selections for forms and elements This builds on top of the previous form/element deletions work by expanding it to work for text object selections. This allows constructing selections within/around forms and elements which can be operated on natively by `d`, `c`, `y`, `v` and friends. Closes #12 --- README.md | 16 ++- lua/nvim-paredit/api/deletions.lua | 39 ++----- lua/nvim-paredit/api/init.lua | 5 + lua/nvim-paredit/api/selections.lua | 105 ++++++++++++++++++ lua/nvim-paredit/defaults.lua | 13 +++ lua/nvim-paredit/utils/keybindings.lua | 6 +- .../text_object_selections_spec.lua | 102 +++++++++++++++++ tests/nvim-paredit/utils.lua | 5 + 8 files changed, 260 insertions(+), 31 deletions(-) create mode 100644 lua/nvim-paredit/api/selections.lua create mode 100644 tests/nvim-paredit/text_object_selections_spec.lua 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 From f37bcec94e44677290ec167967a6bb1848f917f0 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Sat, 12 Aug 2023 12:40:47 +0100 Subject: [PATCH 6/7] Replace `operator` keymap field with smarter funcs Currently the E/B motions are configured with `operator = true` which additionally adds the `o` mode keymap with the fn wrapped in `normal! v` to allow it to function in operator-pending mode. This moves responsibility into the keymap setup phase instead of the function handling the action, and in the current implementation won't work in visual mode operator-pending states. This commit shifts the responsibility to the motions API by allowing them to detect the current mode and conditionally set `normal! v`. --- README.md | 12 ++++++---- lua/nvim-paredit/api/motions.lua | 11 +++++++++ lua/nvim-paredit/defaults.lua | 8 +++---- lua/nvim-paredit/utils/common.lua | 6 +++++ lua/nvim-paredit/utils/keybindings.lua | 23 ------------------- tests/nvim-paredit/operator_motion_spec.lua | 9 ++++---- .../text_object_selections_spec.lua | 13 +++++++---- 7 files changed, 40 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 3e036dc..5547deb 100644 --- a/README.md +++ b/README.md @@ -73,26 +73,28 @@ require("nvim-paredit").setup({ paredit.api.move_to_next_element, "Jump to next element tail", -- by default all keybindings are dot repeatable - repeatable = false + repeatable = false, + mode = { "n", "x", "o", "v" }, }, ["B"] = { paredit.api.move_to_prev_element, "Jump to previous element head", - repeatable = false + repeatable = false, + mode = { "n", "x", "o", "v" }, }, - -- These are text object selection keybindings which can used with standard `d, y, c` + -- These are text object selection keybindings which can used with standard `d, y, c`, `v` ["af"] = { api.select_around_form, "Around form", repeatable = false, - mode = { "o" } + mode = { "o", "v" } }, ["if"] = { api.select_in_form, "In form", repeatable = false, - mode = { "o" } + mode = { "o", "v" } }, } }) diff --git a/lua/nvim-paredit/api/motions.lua b/lua/nvim-paredit/api/motions.lua index 0b72eab..c29cc21 100644 --- a/lua/nvim-paredit/api/motions.lua +++ b/lua/nvim-paredit/api/motions.lua @@ -124,11 +124,22 @@ function M._move_to_element(count, reversed) vim.api.nvim_win_set_cursor(0, cursor_pos) end +-- When in operator-pending mode (`o` or `no`) then we need to switch to +-- visual mode in order for the operator to apply over a range of text. +local function ensure_visual_if_operator_pending() + local mode = vim.api.nvim_get_mode().mode + if mode == "o" or mode == "no" then + common.ensure_visual_mode() + end +end + function M.move_to_prev_element() + ensure_visual_if_operator_pending() M._move_to_element(vim.v.count1, true) end function M.move_to_next_element() + ensure_visual_if_operator_pending() M._move_to_element(vim.v.count1, false) end diff --git a/lua/nvim-paredit/defaults.lua b/lua/nvim-paredit/defaults.lua index cc216e4..ee3944b 100644 --- a/lua/nvim-paredit/defaults.lua +++ b/lua/nvim-paredit/defaults.lua @@ -22,26 +22,26 @@ M.default_keys = { api.move_to_next_element, "Next element tail", repeatable = false, - operator = true, + mode = { "n", "x", "o", "v" }, }, ["B"] = { api.move_to_prev_element, "Previous element head", repeatable = false, - operator = true, + mode = { "n", "x", "o", "v" }, }, ["af"] = { api.select_around_form, "Around form", repeatable = false, - mode = { "o", "v" } + mode = { "o", "v" }, }, ["if"] = { api.select_in_form, "In form", repeatable = false, - mode = { "o", "v" } + mode = { "o", "v" }, }, } diff --git a/lua/nvim-paredit/utils/common.lua b/lua/nvim-paredit/utils/common.lua index 2196796..8e49b13 100644 --- a/lua/nvim-paredit/utils/common.lua +++ b/lua/nvim-paredit/utils/common.lua @@ -59,5 +59,11 @@ function M.intersection(tbl, original) return result end +function M.ensure_visual_mode() + if vim.api.nvim_get_mode().mode ~= "v" then + vim.api.nvim_command("normal! v") + end +end + return M diff --git a/lua/nvim-paredit/utils/keybindings.lua b/lua/nvim-paredit/utils/keybindings.lua index 8adfacf..11e85b3 100644 --- a/lua/nvim-paredit/utils/keybindings.lua +++ b/lua/nvim-paredit/utils/keybindings.lua @@ -17,25 +17,12 @@ function M.with_repeat(fn) end end --- we wrap motion keys with visual mode for operator mode --- such that dE/cE becomes dvE/cvE -function M.visualize(fn) - return function() - vim.api.nvim_command("normal! v") - fn() - end -end - function M.setup_keybindings(opts) for keymap, action in pairs(opts.keys) do local repeatable = true - local operator = false if type(action.repeatable) == "boolean" then repeatable = action.repeatable end - if type(action.operator) == "boolean" then - operator = action.operator - end local fn = action[1] if repeatable then @@ -49,16 +36,6 @@ function M.setup_keybindings(opts) remap = false, silent = true, }) - - if operator then - vim.keymap.set("o", keymap, M.visualize(fn), { - desc = action[2], - buffer = opts.buf or 0, - expr = repeatable, - remap = false, - silent = true, - }) - end end end diff --git a/tests/nvim-paredit/operator_motion_spec.lua b/tests/nvim-paredit/operator_motion_spec.lua index 3e6821a..1a2f051 100644 --- a/tests/nvim-paredit/operator_motion_spec.lua +++ b/tests/nvim-paredit/operator_motion_spec.lua @@ -2,15 +2,14 @@ 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 keybindings = require("nvim-paredit.utils.keybindings") -local motions = require("nvim-paredit.api.motions") -local next_element = keybindings.visualize(motions.move_to_next_element) -local prev_element = keybindings.visualize(motions.move_to_prev_element) +local defaults = require("nvim-paredit.defaults") describe("motions with operator pending", function() before_each(function() - vim.keymap.set("o", "E", next_element, { buffer = true }) - vim.keymap.set("o", "B", prev_element, { buffer = true }) + keybindings.setup_keybindings({ + keys = defaults.default_keys + }) end) it("should delete next form", function() diff --git a/tests/nvim-paredit/text_object_selections_spec.lua b/tests/nvim-paredit/text_object_selections_spec.lua index 9abdf51..6e1b8b5 100644 --- a/tests/nvim-paredit/text_object_selections_spec.lua +++ b/tests/nvim-paredit/text_object_selections_spec.lua @@ -1,4 +1,5 @@ -local paredit = require("nvim-paredit.api") +local keybindings = require("nvim-paredit.utils.keybindings") +local defaults = require("nvim-paredit.defaults") local prepare_buffer = require("tests.nvim-paredit.utils").prepare_buffer local feedkeys = require("tests.nvim-paredit.utils").feedkeys @@ -9,8 +10,9 @@ 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 }) + keybindings.setup_keybindings({ + keys = defaults.default_keys, + }) end) it("should delete the form", function() @@ -78,8 +80,9 @@ 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 }) + keybindings.setup_keybindings({ + keys = defaults.default_keys, + }) end) it("should select the form", function() From 51a3c4076992370ec77abda5010a1bc255951876 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Sat, 12 Aug 2023 13:05:24 +0100 Subject: [PATCH 7/7] Track vcount before switching to visual mode When switching to visual mode from operator-pending the value of the internal `vim.v.count1` var is reset to 1. This commit stores the value before switching to `normal! v`. --- lua/nvim-paredit/api/motions.lua | 6 ++-- tests/nvim-paredit/operator_motion_spec.lua | 34 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lua/nvim-paredit/api/motions.lua b/lua/nvim-paredit/api/motions.lua index c29cc21..fe1c121 100644 --- a/lua/nvim-paredit/api/motions.lua +++ b/lua/nvim-paredit/api/motions.lua @@ -134,13 +134,15 @@ local function ensure_visual_if_operator_pending() end function M.move_to_prev_element() + local count = vim.v.count1 ensure_visual_if_operator_pending() - M._move_to_element(vim.v.count1, true) + M._move_to_element(count, true) end function M.move_to_next_element() + local count = vim.v.count1 ensure_visual_if_operator_pending() - M._move_to_element(vim.v.count1, false) + M._move_to_element(count, false) end return M diff --git a/tests/nvim-paredit/operator_motion_spec.lua b/tests/nvim-paredit/operator_motion_spec.lua index 1a2f051..202e801 100644 --- a/tests/nvim-paredit/operator_motion_spec.lua +++ b/tests/nvim-paredit/operator_motion_spec.lua @@ -86,8 +86,36 @@ describe("motions with operator pending", function() cursor = { 1, 4 }, }) end) - after_each(function() - vim.keymap.del("o", "E") - vim.keymap.del("o", "B") +end) + +describe("motions with operator pending and v:count", function() + before_each(function() + keybindings.setup_keybindings({ + keys = defaults.default_keys + }) + end) + + it("should delete the next 2 elements", function() + prepare_buffer({ + content = "(aa bb cc)", + cursor = { 1, 4 }, + }) + feedkeys("d2") + expect({ + content = "(aa )", + cursor = { 1, 4 }, + }) + end) + + it("should delete the previous 2 elements", function() + prepare_buffer({ + content = "(aa bb cc)", + cursor = { 1, 8 }, + }) + feedkeys("d2") + expect({ + content = "(aa )", + cursor = { 1, 4 }, + }) end) end)