Skip to content

Commit

Permalink
Merge pull request #14 from julienvincent/feat/form-update-api
Browse files Browse the repository at this point in the history
feat: form/cursor api
  • Loading branch information
armed authored Aug 20, 2023
2 parents b90f2fe + d375fc9 commit c24a806
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 26 deletions.
79 changes: 73 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ require("nvim-paredit").setup({
["<localleader>o"] = { paredit.api.raise_form, "Raise form" },
["<localleader>O"] = { paredit.api.raise_element, "Raise element" },

["E"] = {
["E"] = {
paredit.api.move_to_next_element,
"Jump to next element tail",
-- by default all keybindings are dot repeatable
repeatable = false,
mode = { "n", "x", "o", "v" },
},
["B"] = {
paredit.api.move_to_prev_element,
paredit.api.move_to_prev_element,
"Jump to previous element head",
repeatable = false,
mode = { "n", "x", "o", "v" },
Expand Down Expand Up @@ -180,6 +180,73 @@ paredit.api.slurp_forwards()
- **`move_to_next_element`**
- **`move_to_prev_element`**

Form/element wrap api is in `paredit.wrap` module:

- **`wrap_element_under_cursor`** - accepts prefix and suffix, returns wrapped `TSNode`
- **`wrap_enclosing_form_under_cursor`** - accepts prefix and suffix, returns wrapped `TSNode`

Cursor api `paredit.cursor`

- **`place_cursor`** - accepts `TSNode`, and following options:
- `placement` - enumeration `left_edge`,`inner_start`,`inner_end`,`right_edge`
- `mode` - currently only `insert` is supported, defaults to `normal`

## API usage recipes

### `vim-sexp` wrap form (head/tail) replication

Require api module:
```lua
local paredit = require("nvim-paredit.api")
```
Add following keybindings to config:
```lua
["<localleader>w"] = {
function()
-- place cursor and set mode to `insert`
paredit.cursor.place_cursor(
-- wrap element under cursor with `( ` and `)`
paredit.wrap.wrap_element_under_cursor("( ", ")"),
-- cursor placement opts
{ placement = "inner_start", mode = "insert" }
)
end,
"Wrap element insert head",
},

["<localleader>W"] = {
function()
paredit.cursor.place_cursor(
paredit.wrap.wrap_element_under_cursor("(", ")"),
{ placement = "inner_end", mode = "insert" }
)
end,
"Wrap element insert tail",
},

-- same as above but for enclosing form
["<localleader>i"] = {
function()
paredit.cursor.place_cursor(
paredit.wrap.wrap_enclosing_form_under_cursor("( ", ")"),
{ placement = "inner_start", mode = "insert" }
)
end,
"Wrap form insert head",
},

["<localleader>I"] = {
function()
paredit.cursor.place_cursor(
paredit.wrap.wrap_enclosing_form_under_cursor("(", ")"),
{ placement = "inner_end", mode = "insert" }
)
end,
"Wrap form insert tail",
}
```
Same approach can be used for other `vim-sexp` keybindings (e.g. `<localleader>e[`) with cursor placement or without.

## Prior Art

### [vim-sexp](https://github.com/guns/vim-sexp)
Expand All @@ -188,10 +255,10 @@ Currently the de-facto s-expression editing plugin with the most extensive set o

The main reasons you might want to consider `nvim-paredit` instead are:

+ Easier configuration and an exposed lua API
+ Control over how the cursor is moved during slurp/barf. (For example if you don't want the cursor to always be moved)
+ Recursive slurp/barf operations. If your cursor is in a nested form you can still slurp from the forms parent(s)
+ Subjectively better out-of-the-box keybindings
- Easier configuration and an exposed lua API
- Control over how the cursor is moved during slurp/barf. (For example if you don't want the cursor to always be moved)
- Recursive slurp/barf operations. If your cursor is in a nested form you can still slurp from the forms parent(s)
- Subjectively better out-of-the-box keybindings

### [vim-sexp-mappings-for-regular-people](https://github.com/tpope/vim-sexp-mappings-for-regular-people)

Expand Down
30 changes: 30 additions & 0 deletions lua/nvim-paredit/api/cursor.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
local M = {}

function M.insert_mode()
vim.api.nvim_feedkeys("i", "n", true)
end

function M.place_cursor(form, opts)
if not form then
return
end

local range = { form:range() }
local cursor_pos
if opts.placement == "left_edge" then
cursor_pos = { range[1] + 1, range[2] }
elseif opts.placement == "inner_start" then
cursor_pos = { range[1] + 1, range[2] + 1 }
elseif opts.placement == "inned_end" then
cursor_pos = { range[3] + 1, range[4] - 2 }
else
cursor_pos = { range[3] + 1, range[4] - 1 }
end
vim.api.nvim_win_set_cursor(0, cursor_pos)

if opts.mode == "insert" then
M.insert_mode()
end
end

return M
10 changes: 1 addition & 9 deletions lua/nvim-paredit/api/motions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ local langs = require("nvim-paredit.lang")

local M = {}

local default_whitespace_chars = { " ", "," }

-- When the cursor is placed on whitespace within a form then the node returned by
-- the treesitter `get_node_at_cursor` fn is the outer form and not a child within
-- the form.
Expand All @@ -24,13 +22,7 @@ local function get_next_node_from_cursor(lang, reversed)
local cursor = vim.api.nvim_win_get_cursor(0)
cursor = { cursor[1] - 1, cursor[2] }

local char_under_cursor = vim.api.nvim_buf_get_text(0, cursor[1], cursor[2], cursor[1], cursor[2] + 1, {})
local char_is_whitespace = common.included_in_table(
lang.whitespace_chars or default_whitespace_chars,
char_under_cursor[1]
) or char_under_cursor[1] == ""

if not (lang.node_is_form(current_node) and char_is_whitespace) then
if not (lang.node_is_form(current_node) and common.is_whitespace_under_cursor(lang)) then
return lang.get_node_root(current_node)
end

Expand Down
102 changes: 102 additions & 0 deletions lua/nvim-paredit/api/wrap.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
local traversal = require("nvim-paredit.utils.traversal")
local common = require("nvim-paredit.utils.common")
local ts = require("nvim-treesitter.ts_utils")
local langs = require("nvim-paredit.lang")

local M = {}

local function reparse(buf)
local parser = vim.treesitter.get_parser(buf, vim.bo.filetype)
parser:parse()
end

function M.find_element_under_cursor(lang)
local node = ts.get_node_at_cursor()
return lang.get_node_root(node)
end

function M.find_form(element, lang)
return traversal.find_nearest_form(element, { lang = lang, use_source = false })
end

function M.find_parend_form(element, lang)
local nearest_form = M.find_form(element, lang)

if not nearest_form then
return element
end

local parent = nearest_form

if nearest_form:equal(element) then
parent = nearest_form:parent()
end

if parent and parent:type() ~= "source" then
return M.find_form(parent, lang)
end
return nearest_form
end

function M.wrap_element(buf, element, prefix, suffix)
prefix = prefix or ""
suffix = suffix or ""

local range = { element:range() }
vim.api.nvim_buf_set_text(buf, range[3], range[4], range[3], range[4], { suffix })
vim.api.nvim_buf_set_text(buf, range[1], range[2], range[1], range[2], { prefix })
end

function M.wrap_element_under_cursor(prefix, suffix)
local buf = vim.api.nvim_get_current_buf()
local lang = langs.get_language_api()
local current_element = M.find_element_under_cursor(lang)

if not current_element then
return
end
if lang.node_is_comment(current_element) then
return
end
if common.is_whitespace_under_cursor(lang) then
return
end

M.wrap_element(buf, current_element, prefix, suffix)

reparse(buf)

current_element = lang.get_node_root(ts.get_node_at_cursor())
return M.find_form(current_element, lang)
end

function M.wrap_enclosing_form_under_cursor(prefix, suffix)
local buf = vim.api.nvim_get_current_buf()
local lang = langs.get_language_api()
local current_element = M.find_element_under_cursor(lang)

if not current_element then
return
end

local use_direct_parent = common.is_whitespace_under_cursor(lang) or lang.node_is_comment(ts.get_node_at_cursor())

local form = M.find_form(current_element, lang)
if not use_direct_parent and form:type() ~= "source" then
form = M.find_parend_form(current_element, lang)
end

M.wrap_element(buf, form, prefix, suffix)

reparse(buf)

current_element = M.find_element_under_cursor(lang)
if use_direct_parent then
form = current_element
else
form = M.find_parend_form(current_element, lang)
end
return M.find_parend_form(form, lang)
end

return M
2 changes: 2 additions & 0 deletions lua/nvim-paredit/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ local lang = require("nvim-paredit.lang")

local M = {
api = require("nvim-paredit.api"),
wrap = require("nvim-paredit.api.wrap"),
cursor = require("nvim-paredit.api.cursor"),
}

local function setup_keybingings(filetype, buf)
Expand Down
26 changes: 15 additions & 11 deletions lua/nvim-paredit/lang/init.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
local common = require("nvim-paredit.utils.common")

local langs = {
clojure = require("nvim-paredit.lang.clojure"),
}

local M = {}

local function keys(tbl)
local result = {}
for k, _ in pairs(tbl) do
Expand All @@ -10,16 +14,16 @@ local function keys(tbl)
return result
end

return {
get_language_api = function()
return langs[vim.bo.filetype]
end,
function M.get_language_api()
return langs[vim.bo.filetype]
end

add_language_extension = function(filetype, api)
langs[filetype] = api
end,
function M.add_language_extension(filetype, api)
langs[filetype] = api
end

filetypes = function()
return keys(langs)
end,
}
function M.filetypes()
return keys(langs)
end

return M
13 changes: 13 additions & 0 deletions lua/nvim-paredit/utils/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,18 @@ function M.ensure_visual_mode()
end
end

M.default_whitespace_chars = { " " }

function M.is_whitespace_under_cursor(lang)
local cursor = vim.api.nvim_win_get_cursor(0)
cursor = { cursor[1] - 1, cursor[2] }

local char_under_cursor = vim.api.nvim_buf_get_text(0, cursor[1], cursor[2], cursor[1], cursor[2] + 1, {})
return M.included_in_table(
lang.whitespace_chars or M.default_whitespace_chars,
char_under_cursor[1]
) or char_under_cursor[1] == ""
end

return M

Loading

0 comments on commit c24a806

Please sign in to comment.