Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add text object selections for forms and elements #21

Merged
merged 1 commit into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading