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

Pairwise dragging #67

Merged
merged 3 commits into from
Oct 12, 2024
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
240 changes: 170 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
</p>
</div>

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:
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, cursor motions and text object selections
- Dot-repeatable keybindings
Expand All @@ -24,9 +25,11 @@ The goal of `nvim-paredit` is to provide a comparable s-expression editing exper

## 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.
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.
It currently only has first-class support for the `clojure` language and has a focus on supporting the fundamental
paredit operations and motions.

## Installation

Expand Down Expand Up @@ -62,6 +65,13 @@ paredit.setup({
-- this case it will place the cursor on the moved edge
cursor_behaviour = "auto", -- remain, follow, auto

dragging = {
-- If set to `true` paredit will attempt to infer if an element being
-- dragged is part of a 'paired' form like as a map. If so then the element
-- will be dragged along with it's pair.
auto_drag_pairs = true,
},

indent = {
-- This controls how nvim-paredit handles indentation when performing operations which
-- should change the indentation of the form (such as when slurping or barfing).
Expand All @@ -85,6 +95,9 @@ paredit.setup({
[">e"] = { paredit.api.drag_element_forwards, "Drag element right" },
["<e"] = { paredit.api.drag_element_backwards, "Drag element left" },

[">p"] = { api.drag_pair_forwards, "Drag element pairs right" },
["<p"] = { api.drag_pair_backwards, "Drag element pairs left" },

[">f"] = { paredit.api.drag_form_forwards, "Drag form right" },
["<f"] = { paredit.api.drag_form_backwards, "Drag form left" },

Expand Down Expand Up @@ -136,25 +149,25 @@ paredit.setup({
paredit.api.select_around_form,
"Around form",
repeatable = false,
mode = { "o", "v" }
mode = { "o", "v" },
},
["if"] = {
paredit.api.select_in_form,
"In form",
repeatable = false,
mode = { "o", "v" }
mode = { "o", "v" },
},
["aF"] = {
paredit.api.select_around_top_level_form,
"Around top level form",
repeatable = false,
mode = { "o", "v" }
mode = { "o", "v" },
},
["iF"] = {
paredit.api.select_in_top_level_form,
"In top level form",
repeatable = false,
mode = { "o", "v" }
mode = { "o", "v" },
},
["ae"] = {
paredit.api.select_element,
Expand All @@ -168,74 +181,154 @@ paredit.setup({
repeatable = false,
mode = { "o", "v" },
},
}
},
})
```

## Auto Indentation

Nvim-paredit comes with built-in support for fixing form indentation when performing slurp and barf operations. By default this behaviour is disabled and can be enabled by setting `indent.enabled = true` in the [configuration](#configuration)
Nvim-paredit comes with built-in support for fixing form indentation when performing slurp and barf operations. By
default this behaviour is disabled and can be enabled by setting `indent.enabled = true` in the
[configuration](#configuration)

The main goal of this implementation is to provide a visual aid to the user, allowing them to confirm they are operating on the correct node and to know when to stop when performing recursive slurp/barf operations. This implementation is fast and does not result in any UI lag or jitter.
The main goal of this implementation is to provide a visual aid to the user, allowing them to confirm they are operating
on the correct node and to know when to stop when performing recursive slurp/barf operations. This implementation is
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.
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

<details>
<summary><code>vim.lsp.buf.format</code></summary>

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

require("nvim-paredit").setup({
indent = {
enabled = true,
indentor = lsp_indent
}
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,
},
})
```

</details>

## Pairwise Dragging

Nvim-paredit has support for dragging elements pairwise. If an element being dragged is within a form that contains
pairs of elements (such as a clojure `map`) then the element will be dragged along with it's pair.

For example:

```clojure
{:a 1
|:b 2}
;; Drag backwards
{|:b 2
:a 1}
```

This is enabled by default and can be disabled by setting `dragging.auto_drag_pairs = false`.

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

This is especially useful if you have your own clojure macros that you want to enable pairwise dragging on.

All you need to do to extend is to add a new file called `queries/<language>/paredit/pairwise.scm` in your nvim config
directory. Make sure to include the `;; extends` directive to the file or you will overwrite any pre-existing queries
defined by nvim-paredit or other language extensions.

As an example if you want to add support for the following clojure macro:

```clojure
(defmacro my-custom-bindings [bindings & body]
...)

(my-custom-bindings [a 1
b 2]
(println a b))
```

You can add the following TS query

```scm
;; extends

(list_lit
(sym_lit) @fn-name
(vec_lit
(_) @pair)
(#eq? @fn-name "my-custom-bindings"))
```

## 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.
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.

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

Extensions can either be added as config when calling `setup`:

Expand All @@ -247,22 +340,18 @@ require("nvim-paredit").setup({
-- 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,
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,
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,
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,
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)
Expand All @@ -271,25 +360,27 @@ require("nvim-paredit").setup({
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.
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)
- [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.
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

Expand All @@ -306,6 +397,8 @@ paredit.api.slurp_forwards()
- **`barf_backwards`**
- **`drag_element_forwards`**
- **`drag_element_backwards`**
- **`drag_pair_forwards`**
- **`drag_pair_backwards`**
- **`drag_form_forwards`**
- **`drag_form_backwards`**
- **`raise_element`**
Expand Down Expand Up @@ -334,10 +427,13 @@ Cursor api `paredit.cursor`
### `vim-sexp` wrap form (head/tail) replication

Require api module:

```lua
local paredit = require("nvim-paredit")
```

Add following keybindings to config:

```lua
["<localleader>w"] = {
function()
Expand Down Expand Up @@ -383,13 +479,16 @@ Add following keybindings to config:
"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)

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 this instead.
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
this instead.

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

Expand All @@ -401,4 +500,5 @@ The main reasons you might want to consider `nvim-paredit` instead are:

### [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.
A companion to `vim-sexp` which configures `vim-sexp` with better mappings. The default mappings for `nvim-paredit` were
derived from here.
Loading
Loading