Clever Adaptive Surround Pairs
A pair wrapping plugin with:
- Single Keymap (
<c-l>
to cycling forward/backward,u
to undo previous cycle) - Incremental node traversal (TS-powered)
- Multi-cursor aware, works with multicursor.nvim
iShot_2025-02-23_16.38.45.mp4
With lazy.nvim:
return {
"xzbdmw/clasp.nvim",
config = function()
require("clasp").setup({
pairs = { ["{"] = "}", ['"'] = '"', ["'"] = "'", ["("] = ")", ["["] = "]", ["<"] = ">" },
-- If called from insert mode, do not return to normal mode.
keep_insert_mode = true,
-- consider the following go code:
--
-- `var s make()[]int`
--
-- if we want to turn it into:
--
-- `var s make([]int)`
--
-- Directly parse would produce wrong nodes, so clasp always removes the
-- entire pair (`()` in this case) before parsing, in this case what the
-- parser would see is `var s make[]int`, but this is still not valid
-- grammar. For a better parse tree, we can aggressively remove all
-- alphabetic chars before cursor, so it becomes:
--
-- `var s []int`
--
-- Now we can correctly wrap the entire `[]int`, because it is identified
-- as a node. By default we only remove current pair(s) before parsing, in
-- most cases this is fine, but you can set `remove_pattern = "[a-zA-Z_%-]+$"`
-- to use a more aggressive approach if you run into problems.
remove_pattern = nil,
})
-- jumping from smallest region to largest region.
-- Press 'u' in normal mode or bind '<cmd>undo<cr>'
-- in insert mode to undo. You can interleave undo and wrap.
-- Initial state:
-- func(|)vim.keymap.foo('bar')
-- Keep pressing <c-l>:
-- func(vim)|.keymap.foo('bar')
-- func(vim.keymap)|.foo('bar')
-- func(vim.keymap.foo('bar'))|
-- Press 'u'
-- func(vim.keymap)|.foo('bar')
vim.keymap.set({ "n", "i" }, "<c-l>", function()
require("clasp").wrap('next')
end)
-- jumping from largest region to smallest region
-- Initial state:
-- func(|)vim.keymap.foo('bar')
-- Keep pressing <c-;>
-- func(vim.keymap.foo('bar'))|
-- func(vim.keymap)|.foo('bar')
-- func(vim)|.keymap.foo('bar')
-- Press 'u'
-- func(vim.keymap)|.foo('bar')
vim.keymap.set({ "n", "i" }, "<c-;>", function()
require("clasp").wrap('prev')
end)
-- If you want to exclude nodes whose end row is not current row
vim.keymap.set({ "n", "i" }, "<c-l>", function()
require("clasp").wrap('next', function(nodes)
local n = {}
for _, node in ipairs(nodes) do
if node.end_row == vim.api.nvim_win_get_cursor(0)[1] - 1 then
table.insert(n, node)
end
end
return n
end)
end)
end,
}
If you have multiple cursors, make sure you call wrap
in normal mode.
It is suggested to define mappings like this,
so you can keep pressing <c-l>
the same in insert mode:
vim.keymap.set({ "n", "i" }, "<c-l>", function()
if
vim.fn.mode() == "i"
and package.loaded["multicursor-nvim"]
and require("multicursor-nvim").numCursors() > 1
then
vim.cmd("stopinsert")
else
require("clasp").wrap("next")
end
end)
- ultimate-autopair.nvim
- Uses a regex heuristic combined with already matched pairs to determine right pair position, while this plugin relies on treesitter.
- The main motivation for writting this plugin is to support multi-cursor. So it works on normal mode and remembers each cursor's parsing state.
- nvim-autopairs
- Uses hint based fast wrap, each possible position has one label, only works for current line.
MIT License.