Skip to content

Commit

Permalink
Merge pull request #21 from julienvincent/jv/selections
Browse files Browse the repository at this point in the history
Add text object selections for forms and elements
  • Loading branch information
julienvincent authored Aug 12, 2023
2 parents 16747c3 + 53deaeb commit bdd11cb
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 31 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
},
}
})
```
Expand Down
39 changes: 10 additions & 29 deletions lua/nvim-paredit/api/deletions.lua
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions lua/nvim-paredit/api/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
Expand Down
105 changes: 105 additions & 0 deletions lua/nvim-paredit/api/selections.lua
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions lua/nvim-paredit/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 5 additions & 1 deletion lua/nvim-paredit/utils/keybindings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,21 @@ 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
vim.keymap.set("o", keymap, M.visualize(fn), {
desc = action[2],
buffer = opts.buf or 0,
expr = repeatable,
remap = false,
silent = true,
})
end
end
Expand Down
102 changes: 102 additions & 0 deletions tests/nvim-paredit/text_object_selections_spec.lua
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions tests/nvim-paredit/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit bdd11cb

Please sign in to comment.