Skip to content

Commit

Permalink
Add text object selections for forms and elements
Browse files Browse the repository at this point in the history
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` and friends.
  • Loading branch information
julienvincent committed Aug 10, 2023
1 parent 041039b commit 2d5412e
Show file tree
Hide file tree
Showing 7 changed files with 207 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
99 changes: 99 additions & 0 deletions lua/nvim-paredit/api/selections.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
local traversal = require("nvim-paredit.utils.traversal")
local ts = require("nvim-treesitter.ts_utils")
local langs = require("nvim-paredit.lang")

local M = {}

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

vim.api.nvim_command("normal! v")
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

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

vim.api.nvim_command("normal! v")
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

vim.api.nvim_command("normal! v")
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" }
},
["if"] = {
api.select_in_form,
"In form",
repeatable = false,
mode = { "o" }
},
}

M.defaults = {
Expand Down
4 changes: 3 additions & 1 deletion lua/nvim-paredit/utils/keybindings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions tests/nvim-paredit/text_object_selections_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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

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 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)

0 comments on commit 2d5412e

Please sign in to comment.