diff --git a/README.md b/README.md index 3d4a2af..1a80e76 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,18 @@ Emacs. This is what is provided: ![Demo](./assets/demo.gif) -## Project Status - -This is currently **beta software**. It works well in the workflows of the current maintainers but has not been -thoroughly tested with many users. - -It currently only has first-class support for the `clojure` language and has a focus on supporting the fundamental -paredit operations and motions. +## Index + +- **[Installation](#installation)** +- **[Configuration](#configuration)** +- **[API Reference](./docs/api-reference.md)** +- **[Recipes](./docs/recipes.md)** +- **[Auto Indentation](#auto-indentation)** +- **[Pairwise Dragging](#pairwise-dragging)** +- **[Language Support](#language-support)** + - **[Language Extension Spec](./docs/language-extension-spec.md)** + - **[Third-Party Language Extensions](#third-party-language-extensions)** +- **[Prior Art](#prior-art)** ## Installation @@ -46,6 +51,20 @@ paredit operations and motions. ## Configuration +```lua +local paredit = require("nvim-paredit") +paredit.setup({ + -- Change some keys + keys = { + ["o"] = false, + ["r"] = { paredit.api.raise_form, "Raise form" }, + }, +}) +``` + +
+ Default Configuration Values + ```lua local paredit = require("nvim-paredit") paredit.setup({ @@ -185,6 +204,14 @@ paredit.setup({ }) ``` +
+ +--- + +## API Reference + +See **[api-reference.md](./docs/api-reference.md)** + ## Auto Indentation Nvim-paredit comes with built-in support for fixing form indentation when performing slurp and barf operations. By @@ -197,73 +224,10 @@ fast and does not result in any UI lag or jitter. The goal is _not_ to be 100% correct. The implementation follows a simple set of rules which account for most scenarios but not all. If a more correct implementation is needed then the native implementation can be replaced by setting the -configuration property `intent.indentor`. For example an implementation using `vim.lsp.buf.format` could be built if the -user doesn't mind sacrificing performance for correctness. - -### Recipes +configuration property `intent.indentor`. -
- vim.lsp.buf.format - -Below is a reference implementation for using `vim.lsp.buf.format` to replace the native implementation. This -implementation won't be nearly as performant but it will be more correct. - -```lua -local function lsp_indent(event, opts) - local traversal = require("nvim-paredit.utils.traversal") - local utils = require("nvim-paredit.indentation.utils") - local langs = require("nvim-paredit.lang") - - local lang = langs.get_language_api() - - local parent = event.parent - - local child - if event.type == "slurp-forwards" then - child = parent:named_child(parent:named_child_count() - 1) - elseif event.type == "slurp-backwards" then - child = parent:named_child(1) - elseif event.type == "barf-forwards" then - child = traversal.get_next_sibling_ignoring_comments(event.parent, { lang = lang }) - elseif event.type == "barf-backwards" then - child = event.parent - else - return - end - - local child_range = { child:range() } - local lines = utils.find_affected_lines(child, utils.get_node_line_range(child_range)) - - vim.lsp.buf.format({ - bufnr = opts.buf or 0, - range = { - ["start"] = { lines[1] + 1, 0 }, - ["end"] = { lines[#lines] + 1, 0 }, - }, - }) -end - -local child_range = { child:range() } -local lines = utils.find_affected_lines(child, utils.get_node_line_range(child_range)) - -vim.lsp.buf.format({ - bufnr = opts.buf or 0, - range = { - ["start"] = { lines[1] + 1, 0 }, - ["end"] = { lines[#lines] + 1, 0 }, - }, -}) -end - -require("nvim-paredit").setup({ - indent = { - enabled = true, - indentor = lsp_indent, - }, -}) -``` - -
+For example an implementation using `vim.lsp.buf.format` could be built if you don't mind sacrificing performance for +correctness. See the **[lsp indentation recipe](./docs/recipes.md#lsp-indentation)** for an example of this. ## Pairwise Dragging @@ -280,7 +244,8 @@ For example: :a 1} ``` -This is enabled by default and can be disabled by setting `dragging.auto_drag_pairs = false`. +This is enabled by default and can be disabled by setting `dragging.auto_drag_pairs = false` in the +[configuration](#configuration). Pairwise dragging works using treesitter queries to identify element pairs within some localized node. This means you can very easily extend the paredit pairwise implementation by simply adding new treesitter queries to your nvim @@ -288,8 +253,8 @@ configuration. You might want to extend if: -1) You are a language extension author and want to add pairwise dragging support to your extension. -2) You want to add support for some syntax not supported by nvim-paredit. +1. You are a language extension author and want to add pairwise dragging support to your extension. +2. You want to add support for some syntax not supported by nvim-paredit. This is especially useful if you have your own clojure macros that you want to enable pairwise dragging on. @@ -305,7 +270,7 @@ As an example if you want to add support for the following clojure macro: (my-custom-bindings [a 1 b 2] - (println a b)) + (println a b)) ``` You can add the following TS query @@ -323,168 +288,24 @@ You can add the following TS query ## Language Support As this is built using Treesitter it requires that you have the relevant Treesitter grammar installed for your language -of choice. Additionally `nvim-paredit` will need explicit support for the treesitter grammar as the node names and -metadata of nodes vary between languages. +of choice. Additionally `nvim-paredit` will need explicit support for the treesitter grammar used by your language as +the node names and metadata of nodes vary between languages. Right now `nvim-paredit` only has built in support for `clojure` but exposes an extension API for adding support for -other lisp dialects. This API is considered **very alpha** and may change without warning to properly account for other -languages when attempts are made to add support. +other lisp dialects. See **[third-party language extensions](#third-party-language-extensions)** for some existing +support for other languages. -Extensions can either be added as config when calling `setup`: +If you are an extension author and would like to add support for a lisp dialect take a look at the +[Language Extension Spec](./docs/language-extension-spec.md) for an overview on how to achieve this. -```lua -require("nvim-paredit").setup({ - extensions = { - commonlisp = { - -- Should return the 'root' of the given Treesitter node. For example: - -- The node at cursor in the below example is `()` or 'list_lit': - -- '(|) - -- But the node root is `'()` or 'quoting_lit' - get_node_root = function(node) end, - -- This is the inverse of `get_node_root` for forms and should find the inner node for which - -- the forms elements are direct children. - -- - -- For example given the node `'()` or 'quoting_lit', this function should return `()` or 'list_lit'. - unwrap_form = function(node) end, - -- Accepts a Treesitter node and should return true or false depending on whether the given node - -- can be considered a 'form' - node_is_form = function(node) end, - -- Accepts a Treesitter node and should return true or false depending on whether the given node - -- can be considered a 'comment' - node_is_comment = function(node) end, - -- Accepts a Treesitter node representing a form and should return the 'edges' of the node. This - -- includes the node text and the range covered by the node - get_form_edges = function(node) - return { - left = { text = "#{", range = { 0, 0, 0, 2 } }, - right = { text = "}", range = { 0, 5, 0, 6 } }, - } - end, - }, - }, -}) -``` - -Or by calling the `add_language_extension` API directly before the setup. This would be the recommended approach for -extension plugin authors. - -```lua -require("nvim-paredit").extension.add_language_extension("commonlisp", { ... }). -``` - -### Existing Language Extensions - -- [fennel](https://github.com/julienvincent/nvim-paredit-fennel) -- [scheme](https://github.com/ekaitz-zarraga/nvim-paredit-scheme) - ---- - -As no attempt has been made to add support for other grammars I have no idea if the language extension API's are -actually sufficient for adding additional languages. They will evolve as attempts are made. - -## API - -The core API is exposed as `paredit.api`: - -```lua -local paredit = require("nvim-paredit") -paredit.api.slurp_forwards() -``` - -- **`slurp_forwards`** -- **`slurp_backwards`** -- **`barf_forwards`** -- **`barf_backwards`** -- **`drag_element_forwards`** -- **`drag_element_backwards`** -- **`drag_pair_forwards`** -- **`drag_pair_backwards`** -- **`drag_form_forwards`** -- **`drag_form_backwards`** -- **`raise_element`** -- **`raise_form`** -- **`delete_form`** -- **`delete_in_form`** -- **`delete_top_level_form`** -- **`delete_in_top_level_form`** -- **`delete_element`** -- **`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` - -## Additional API usage recipes - -### `vim-sexp` wrap form (head/tail) replication - -Require api module: - -```lua -local paredit = require("nvim-paredit") -``` - -Add following keybindings to config: - -```lua -["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", -}, - -["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 -["i"] = { - function() - paredit.cursor.place_cursor( - paredit.wrap.wrap_enclosing_form_under_cursor("( ", ")"), - { placement = "inner_start", mode = "insert" } - ) - end, - "Wrap form insert head", -}, - -["I"] = { - function() - paredit.cursor.place_cursor( - paredit.wrap.wrap_enclosing_form_under_cursor("(", ")"), - { placement = "inner_end", mode = "insert" } - ) - end, - "Wrap form insert tail", -} -``` +### Third-Party Language Extensions -Same approach can be used for other `vim-sexp` keybindings (e.g. `e[`) with cursor placement or without. +- **[fennel](https://github.com/julienvincent/nvim-paredit-fennel)** +- **[scheme](https://github.com/ekaitz-zarraga/nvim-paredit-scheme)** ## Prior Art -### [vim-sexp](https://github.com/guns/vim-sexp) +#### [vim-sexp](https://github.com/guns/vim-sexp) Currently the de-facto s-expression editing plugin with the most extensive set of available editing operations. If you are looking for a more complete plugin with a wider range of supported languages then you might want to look into using @@ -498,7 +319,7 @@ The main reasons you might want to consider `nvim-paredit` instead are: - Automatic form/element indentations on slurp/barf - Subjectively better out-of-the-box keybindings -### [vim-sexp-mappings-for-regular-people](https://github.com/tpope/vim-sexp-mappings-for-regular-people) +#### [vim-sexp-mappings-for-regular-people](https://github.com/tpope/vim-sexp-mappings-for-regular-people) A companion to `vim-sexp` which configures `vim-sexp` with better mappings. The default mappings for `nvim-paredit` were derived from here. diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..b5e6933 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,309 @@ +## API Documentation + +The core API is exposed under the `paredit.api` module + +### Index + +- **[Slurp / Barf](#slurp--barf)** + - [slurp_forwards](#slurp_forwardsopts) + - [slurp_backwards](#slurp_backwardsopts) + - [barf_forwards](#barf_forwardsopts) + - [barf_backwards](#barf_backwardsopts) +- **[Dragging](#dragging)** + - [drag_element_forwards](#drag_element_forwardsopts) + - [drag_element_backwards](#drag_element_backwardsopts) + - [drag_pair_forwards](#drag_pair_forwards) + - [drag_pair_backwards](#drag_pair_backwards) + - [drag_form_forwards](#drag_form_forwards) + - [drag_form_backwards](#drag_form_backwards) +- **[Editing](#editing)** + - [raise_element](#raise_element) + - [raise_form](#raise_form) + - [delete_form](#delete_form) + - [delete_in_form](#delete_in_form) + - [delete_top_level_form](#delete_top_level_form) + - [delete_in_top_level_form](#delete_in_top_level_form) + - [delete_element](#delete_element) +- **[Motions](#motions)** + - [move_to_next_element_tail](#move_to_next_element_tail) + - [move_to_next_element_head](#move_to_next_element_head) + - [move_to_prev_element_tail](#move_to_prev_element_tail) + - [move_to_prev_element_head](#move_to_prev_element_head) + - [move_to_parent_form_start](#move_to_parent_form_start) + - [move_to_parent_form_end](#move_to_parent_form_end) +- **[Selections](#selections)** + - [select_around_form](#select_around_form) + - [select_in_form](#select_in_form) + - [select_around_top_level_form](#select_around_top_level_form) + - [select_in_top_level_form](#select_in_top_level_form) + - [select_element](#select_element) +- **[Wrapping](#wrapping)** + - [wrap_element_under_cursor](#wrap_element_under_cursorprefix-suffix) + - [wrap_enclosing_form_under_cursor](#wrap_enclosing_form_under_cursor) + - [unwrap_form_under_cursor](#unwrap_form_under_cursor) +- **[Cursor Manipulation](#cursor-manipulation)** + - [place_cursor](#place_cursorrange_or_node-opts) + +--- + +### **[Slurp / Barf](#slurp--barf)** + +##### `SlurpBarfOpts` + +```lua +{ + cursor_behaviour = "auto", -- remain, follow, auto + indent = { + enabled = false, + indentor = require("nvim-paredit.indentation.native").indentor, + }, +} +``` + +#### `slurp_forwards([opts])` + +Expands the current form by pulling in the next expression into the form. + +- **`opts`** - see **[SlurpBarfOpts](#SlurpBarfOpts)** + +--- + +#### `slurp_backwards([opts])` + +Expands the current form by pulling the previous expression into the form. + +- **`opts`** - see **[SlurpBarfOpts](#SlurpBarfOpts)** + +--- + +#### `barf_forwards([opts])` + +Removes the last expression from the current form, pushing it outwards. + +- **`opts`** - see **[SlurpBarfOpts](#SlurpBarfOpts)** + +--- + +#### `barf_backwards([opts])` + +Removes the first expression from the current form, pushing it outwards. + +- **`opts`** - see **[SlurpBarfOpts](#SlurpBarfOpts)** + +--- + +### **[Dragging](#dragging)** + +##### `ElementDragOpts` + +```lua +{ + dragging = { + enable_auto_drag = true + } +} +``` + +#### `drag_element_forwards([opts])` + +Moves the current element or pair forwards within its form. + +- **`opts`** - see **[ElementDragOpts](#ElementDragOpts)** + +--- + +#### `drag_element_backwards([opts])` + +Moves the current element or pair backwards within its form. + +- **`opts`** - see **[ElementDragOpts](#ElementDragOpts)** + +--- + +#### `drag_pair_forwards()` + +Moves the current pair of elements forwards within its form. + +**Inputs:** + +- `pair`: (Optional) The pair of elements to drag forwards. Defaults to the pair at the current cursor position. + +--- + +#### `drag_pair_backwards()` + +Moves the current pair of elements backwards within its form. + +**Inputs:** + +- `pair`: (Optional) The pair of elements to drag backwards. Defaults to the pair at the current cursor position. + +--- + +#### `drag_form_forwards()` + +Moves the current form forwards within its parent form. + +--- + +#### `drag_form_backwards()` + +Moves the current form backwards within its parent form. + +--- + +### **[Editing](#editing)** + +#### `raise_element()` + +Raises the current element, removing it from its enclosing form. + +--- + +#### `raise_form()` + +Raises the current form, removing it from its enclosing form. + +--- + +#### `delete_form()` + +Deletes the current form. + +--- + +#### `delete_in_form()` + +Deletes the content inside the current form without removing the form itself. + +--- + +#### `delete_top_level_form()` + +Deletes the current top-level form. + +--- + +#### `delete_in_top_level_form()` + +Deletes the content inside the current top-level form without removing the form itself. + +--- + +#### `delete_element()` + +Deletes the current element. + +--- + +### **Motions** + +#### `move_to_next_element_tail()` + +Moves the cursor to the tail of the next element in the form. + +--- + +#### `move_to_next_element_head()` + +Moves the cursor to the head of the next element in the form. + +--- + +#### `move_to_prev_element_head()` + +Moves the cursor to the head of the previous element in the form. + +--- + +#### `move_to_prev_element_tail()` + +Moves the cursor to the tail of the previous element in the form. + +--- + +#### `move_to_parent_form_start()` + +Moves the cursor to the start of the parent form. + +--- + +#### `move_to_parent_form_end()` + +Moves the cursor to the end of the parent form. + +--- + +### **Selections** + +#### `select_around_form()` + +Selects the form surrounding the cursor, including the enclosing delimiters. + +--- + +#### `select_in_form()` + +Selects the content inside the form surrounding the cursor, excluding the enclosing delimiters. + +--- + +#### `select_around_top_level_form()` + +Selects the top-level form surrounding the cursor, including the enclosing delimiters. + +--- + +#### `select_in_top_level_form()` + +Selects the content inside the top-level form surrounding the cursor, excluding the enclosing delimiters. + +--- + +#### `select_element()` + +Selects the current element under the cursor. + +--- + +### **Wrapping** + +#### `wrap_element_under_cursor(prefix, suffix)` + +Wraps the element under the cursor with a prefix and suffix. + +- `prefix`: string +- `suffix`: string + +Returns The wrapped `TSNode`. + +--- + +#### `wrap_enclosing_form_under_cursor()` + +Wraps the enclosing form under the cursor with a prefix and suffix. + +- `prefix`: string +- `suffix`: string + +Returns The wrapped `TSNode`. + +#### `unwrap_form_under_cursor()` + +Unwraps the nearest form under the cursor. This is called splice in other paredit implementations. + +--- + +### **Cursor Manipulation** + +These APIs are exposed from `paredit.api.cursor`. + +#### `place_cursor(range_or_node, opts)` + +Places the cursor at a specific position within a `TSNode`. + +- `node`: The `TSNode` to operate within +- `opts` table + - `placement`: (Optional) The position relative to the node. Can be `left_edge`, `inner_start`, `inner_end`, or + `right_edge`. Defaults to `left_edge`. + - `mode`: (Optional) The mode for cursor placement. Currently only `insert` is supported, defaults to `normal`. diff --git a/docs/language-extension-spec.md b/docs/language-extension-spec.md new file mode 100644 index 0000000..ab7c628 --- /dev/null +++ b/docs/language-extension-spec.md @@ -0,0 +1,55 @@ +# Language Extension Spec + +> [!WARNING] +> +> This API is considered **very alpha** and may change without warning + +### API + +```lua +local language_extension = { + -- Should return the 'root' of the given Treesitter node. For example: + -- The node at cursor in the below example is `()` or 'list_lit': + -- '(|) + -- But the node root is `'()` or 'quoting_lit' + get_node_root = function(node) end, + -- This is the inverse of `get_node_root` for forms and should find the inner node for which + -- the forms elements are direct children. + -- + -- For example given the node `'()` or 'quoting_lit', this function should return `()` or 'list_lit'. + unwrap_form = function(node) end, + -- Accepts a Treesitter node and should return true or false depending on whether the given node + -- can be considered a 'form' + node_is_form = function(node) end, + -- Accepts a Treesitter node and should return true or false depending on whether the given node + -- can be considered a 'comment' + node_is_comment = function(node) end, + -- Accepts a Treesitter node representing a form and should return the 'edges' of the node. This + -- includes the node text and the range covered by the node + get_form_edges = function(node) + return { + left = { text = "#{", range = { 0, 0, 0, 2 } }, + right = { text = "}", range = { 0, 5, 0, 6 } }, + } + end, +} +``` + +See also [the clojure implementation](../lua/nvim-paredit/lang/clojure.lua) for a good reference. + +## Registration + +Extensions can either be registered as config when calling `setup` or by calling the `add_language_extension` API before +the setup. The latter would be the recommended approach for extension plugin authors. + +```lua +require("nvim-paredit").setup({ + extensions = { + commonlisp = { ... }, + }, +}) +``` + +```lua +require("nvim-paredit").extension.add_language_extension("commonlisp", { ... }). +``` diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 0000000..392a162 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,121 @@ +# Recipes + +### Lsp Indentation + +Below is a reference implementation for using `vim.lsp.buf.format` to replace the native indent implementation. This +implementation won't be nearly as performant, but it will be more correct. + +```lua +local function lsp_indent(event, opts) + local traversal = require("nvim-paredit.utils.traversal") + local utils = require("nvim-paredit.indentation.utils") + local langs = require("nvim-paredit.lang") + + local lang = langs.get_language_api() + + local parent = event.parent + + local child + if event.type == "slurp-forwards" then + child = parent:named_child(parent:named_child_count() - 1) + elseif event.type == "slurp-backwards" then + child = parent:named_child(1) + elseif event.type == "barf-forwards" then + child = traversal.get_next_sibling_ignoring_comments(event.parent, { lang = lang }) + elseif event.type == "barf-backwards" then + child = event.parent + else + return + end + + local child_range = { child:range() } + local lines = utils.find_affected_lines(child, utils.get_node_line_range(child_range)) + + vim.lsp.buf.format({ + bufnr = opts.buf or 0, + range = { + ["start"] = { lines[1] + 1, 0 }, + ["end"] = { lines[#lines] + 1, 0 }, + }, + }) +end + +local child_range = { child:range() } +local lines = utils.find_affected_lines(child, utils.get_node_line_range(child_range)) + +vim.lsp.buf.format({ + bufnr = opts.buf or 0, + range = { + ["start"] = { lines[1] + 1, 0 }, + ["end"] = { lines[#lines] + 1, 0 }, + }, +}) +end + +require("nvim-paredit").setup({ + indent = { + enabled = true, + indentor = lsp_indent, + }, +}) +``` + +### Wrap form (head/tail) + +This is to mimic the behaviour from `vim-sexp` + +Require api module: + +```lua +local paredit = require("nvim-paredit") +``` + +Add following keybindings to config: + +```lua +["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", +}, + +["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 +["i"] = { + function() + paredit.cursor.place_cursor( + paredit.wrap.wrap_enclosing_form_under_cursor("( ", ")"), + { placement = "inner_start", mode = "insert" } + ) + end, + "Wrap form insert head", +}, + +["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. `e[`) with cursor placement or without.