Skip to content
Draft
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
62 changes: 56 additions & 6 deletions doc/nvim-surround.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ CONTENTS *nvim-surround.contents*
3.3. Setup ......................................... |nvim-surround.setup|
3.3.1 Surrounds ...................... |nvim-surround.setup.surrounds|
3.3.2 Aliases .......................... |nvim-surround.setup.aliases|
3.3.3 Highlights ..................... |nvim-surround.setup.highlight|
3.3.4 Cursor ....................... |nvim-surround.setup.move_cursor|
3.3.5 Indentation ................. |nvim-surround.setup.indent_lines|
3.3.3 Cycles ........................... |nvim-surround.setup.cycles|
3.3.4 Highlights ..................... |nvim-surround.setup.highlight|
3.3.5 Cursor ....................... |nvim-surround.setup.move_cursor|
3.3.6 Indentation ................. |nvim-surround.setup.indent_lines|
3.4. Helpers ..................................... |nvim-surround.helpers|
4. Migration Guides ................................ |nvim-surround.migrating|
4.1. Migrating v3 to v4 ............... |nvim-surround.migrating.v3_to_v4|
Expand Down Expand Up @@ -62,6 +63,7 @@ For examples of custom surrounds, see:
* Adding a delimiter pair to the buffer
* Deleting a delimiter pair from the buffer
* Changing a delimiter pair to a different pair
* Toggling a delimiter pair through a configured cycle

--------------------------------------------------------------------------------
2.1. The Basics *nvim-surround.basics*
Expand Down Expand Up @@ -220,6 +222,18 @@ delimiter pair on new lines.
args
)

Users can bind the |<Plug>(nvim-surround-toggle)| mapping to create a toggle
operator, e.g. `ts`. When used, it toggles the nearest surrounding pair
through a configured cycle.

For example, with a `q` cycle configured to use double quotes, single quotes,
and backticks:

Old text Command New text ~
"some *text" tsq 'some text'
'some *text' tsq `some text`
`some *text` tsq "some text"

For the visual-mode `S` map, the type of visual mode determines how the
delimiter pair is added
* |charwise-visual|: Adds the pair around the selection
Expand Down Expand Up @@ -410,6 +424,7 @@ Both functions take a table as an argument, full of configuration options.
require("nvim-surround").setup({
surrounds = -- Defines surround keys and behavior
aliases = -- Defines aliases
cycles = -- Defines toggle cycles for user-defined toggle mappings
highlight = -- Defines highlight behavior
move_cursor = -- Defines cursor behavior after a surround action
indent_lines = -- Defines line indentation behavior
Expand Down Expand Up @@ -599,7 +614,42 @@ Default value:
},
<
--------------------------------------------------------------------------------
3.3.3. Highlights *nvim-surround.setup.highlight*
3.3.3. Cycles *nvim-surround.setup.cycles*

The cycles table maps characters to ordered lists of surround keys. These
cycles can be used by a user-defined mapping to
|<Plug>(nvim-surround-toggle)|, toggling the nearest matching surround to the
next entry in the list.

For example, the following configuration defines a `q` cycle that rotates
between double quotes, single quotes, and backticks, and binds it to `ts`:
>lua
require("nvim-surround").setup({
cycles = {
["q"] = { '"', "'", "`" },
},
})
vim.keymap.set("n", "ts", "<Plug>(nvim-surround-toggle)")
<

This means `tsq` will do the following:

Old text Command New text ~
"some *text" tsq 'some text'
'some *text' tsq `some text`
`some *text` tsq "some text"

To toggle only between single and double quotes, override the cycle:
>lua
require("nvim-surround").setup({
cycles = {
["q"] = { '"', "'" },
},
})
vim.keymap.set("n", "ts", "<Plug>(nvim-surround-toggle)")
<
--------------------------------------------------------------------------------
3.3.4. Highlights *nvim-surround.setup.highlight*

When adding a new delimiter pair to the buffer in normal mode, one can
optionally highlight the selection to be surrounded. Similarly, when changing
Expand All @@ -618,7 +668,7 @@ configured separately. The default highlight group used is `Visual`:
highlight default link NvimSurroundHighlight Visual
<
--------------------------------------------------------------------------------
3.3.4. Cursor *nvim-surround.setup.move_cursor*
3.3.5. Cursor *nvim-surround.setup.move_cursor*

By default (or when `move_cursor = "begin"`), when a surround action is
performed, the cursor moves to the beginning of the action.
Expand All @@ -645,7 +695,7 @@ of how the buffer changes.
(hello* world) csbffoo<CR> foo(he*llo world)

--------------------------------------------------------------------------------
3.3.5. Indentation *nvim-surround.setup.indent_lines*
3.3.6. Indentation *nvim-surround.setup.indent_lines*

By default, when a surround action is performed, |nvim-surround| tries to
indent things "appropriately", i.e. when it is enabled by a Vim option. The
Expand Down
2 changes: 2 additions & 0 deletions lua/nvim-surround/annotations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
---@class options
---@field surrounds table<string, surround>
---@field aliases table<string, string|string[]>
---@field cycles table<string, string[]>
---@field highlight { duration: integer }
---@field move_cursor false|"begin"|"sticky"
---@field indent_lines function
Expand All @@ -55,6 +56,7 @@
---@class user_options
---@field surrounds? table<string, false|user_surround>
---@field aliases? table<string, false|string|string[]>
---@field cycles? table<string, false|string[]>
---@field highlight? { duration: false|integer }
---@field move_cursor? false|"begin"|"sticky"
---@field indent_lines? false|function
2 changes: 2 additions & 0 deletions lua/nvim-surround/cache.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ M.normal = {}
M.delete = {}
---@type { del_char: string, add_delimiters: add_func, line_mode: boolean, count: integer }
M.change = {}
---@type { char: string, line_mode: boolean, count: integer }
M.toggle = {}

-- Sets the callback function for dot-repeating.
---@param func_name string A string representing the callback function's name.
Expand Down
26 changes: 25 additions & 1 deletion lua/nvim-surround/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ M.default_opts = {
["q"] = { '"', "'", "`" },
["s"] = { "}", "]", ")", ">", '"', "'", "`" },
},
cycles = {},
highlight = {
duration = 0,
},
Expand Down Expand Up @@ -533,14 +534,30 @@ M.translate_alias = function(user_alias)
return user_alias
end

-- Translates `cycle` into the internal form.
---@param user_cycle false|string[] The user-provided `cycle`.
---@return false|string[] @The translated `cycle`.
M.translate_cycle = function(user_cycle)
if not user_cycle then
return user_cycle
end

local input = require("nvim-surround.input")
local cycle = {}
for _, char in ipairs(user_cycle) do
cycle[#cycle + 1] = input.replace_termcodes(char)
end
return cycle
end

-- Translates the user-provided configuration into the internal form.
---@param user_opts user_options The user-provided options.
---@return options @The translated options.
M.translate_opts = function(user_opts)
local input = require("nvim-surround.input")
local opts = {}
for key, value in pairs(user_opts) do
if key == "surrounds" or key == "aliases" then
if key == "surrounds" or key == "aliases" or key == "cycles" then
elseif key == "indent_lines" then
opts[key] = value or function() end
else
Expand Down Expand Up @@ -568,6 +585,13 @@ M.translate_opts = function(user_opts)
opts.aliases[char] = M.translate_alias(user_alias)
end
end
if user_opts.cycles then
opts.cycles = {}
for char, user_cycle in pairs(user_opts.cycles) do
char = input.replace_termcodes(char)
opts.cycles[char] = M.translate_cycle(user_cycle)
end
end
return opts
end

Expand Down
83 changes: 83 additions & 0 deletions lua/nvim-surround/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,26 @@ M.change_surround = function(args)
cache.set_callback("v:lua.require'nvim-surround'.change_callback")
end

-- Toggle a surrounding delimiter pair to the next entry in a configured cycle.
---@param args { curpos: position, char: string, add_delimiters: add_func, line_mode: boolean }
---@return "g@l"|nil
M.toggle_surround = function(args)
local cache = require("nvim-surround.cache")
if not args.del_char or not args.add_delimiters then
cache.toggle = { line_mode = args.line_mode, count = vim.v.count1 }

vim.go.operatorfunc = "v:lua.require'nvim-surround'.toggle_callback"
return "g@l"
end

return M.change_surround({
curpos = args.curpos,
del_char = args.del_char,
add_delimiters = args.add_delimiters,
line_mode = args.line_mode,
})
end

--[====================================================================================================================[
Callback Functions
--]====================================================================================================================]
Expand Down Expand Up @@ -425,4 +445,67 @@ M.change_callback = function()
end
end

M.toggle_callback = function()
local config = require("nvim-surround.config")
local buffer = require("nvim-surround.buffer")
local cache = require("nvim-surround.cache")
local input = require("nvim-surround.input")
local utils = require("nvim-surround.utils")

cache.toggle.char = cache.toggle.char or input.get_char()
local char = cache.toggle.char
if not char then
return
end

local cycle = config.get_opts().cycles[char]
if not cycle or #cycle == 0 then
return
end

for _ = 1, cache.toggle.count do
local selections = utils.get_nearest_selections(char, "change")
if not selections then
return
end

local left_text = table.concat(buffer.get_text(selections.left))
local current_index
for index, candidate in ipairs(cycle) do
if candidate == left_text then
current_index = index
break
end
end
if not current_index then
return
end

local next_char = cycle[current_index % #cycle + 1]
local delimiters = config.get_delimiters(next_char, cache.toggle.line_mode)
if not delimiters then
return
end

local add_delimiters = function()
return delimiters
end
cache.change = {
del_char = char,
add_delimiters = add_delimiters,
line_mode = cache.toggle.line_mode,
count = 1,
}
M.change_surround({
del_char = char,
add_delimiters = add_delimiters,
line_mode = cache.toggle.line_mode,
count = 1,
curpos = buffer.get_curpos(),
})
end

cache.set_callback("v:lua.require'nvim-surround'.toggle_callback")
end

return M
7 changes: 7 additions & 0 deletions plugin/nvim-surround.lua
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,10 @@ end, {
expr = true,
silent = true,
})
vim.keymap.set("n", "<Plug>(nvim-surround-toggle)", function()
return require("nvim-surround").toggle_surround({ line_mode = false })
end, {
desc = "Toggle a surrounding pair through a configured cycle",
expr = true,
silent = true,
})
23 changes: 23 additions & 0 deletions tests/configuration_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,29 @@ describe("configuration", function()
check_lines({ "hey! hello world" })
end)

it("can configure custom toggle cycles", function()
require("nvim-surround").buffer_setup({
cycles = {
["q"] = { '"', "'" },
},
})
vim.keymap.set("n", "ts", "<Plug>(nvim-surround-toggle)")

set_lines({
[["hello"]],
[['world']],
})

set_curpos({ 1, 3 })
vim.cmd("normal tsq")
set_curpos({ 2, 3 })
vim.cmd("normal tsq")
check_lines({
[['hello']],
[["world"]],
})
end)

it("can use 'syntactic sugar' for add functions", function()
require("nvim-surround").buffer_setup({
surrounds = {
Expand Down
23 changes: 23 additions & 0 deletions tests/dot_repeat_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ describe("dot-repeat", function()
check_lines({ "<<<test>>>" })
end)

it("can dot-repeat quote toggles", function()
require("nvim-surround").buffer_setup({
cycles = {
["q"] = { '"', "'" },
},
})
vim.keymap.set("n", "ts", "<Plug>(nvim-surround-toggle)")

set_lines({
[["one"]],
[["two"]],
[["three"]],
})
set_curpos({ 1, 3 })
vim.cmd("normal tsq")
vim.cmd("normal j.j.")
check_lines({
[['one']],
[['two']],
[['three']],
})
end)

it("can add non-static delimiter pairs based on user input", function()
set_lines({ "here", "are", "some", "lines" })
vim.cmd("normal ysiwffunc_name" .. cr)
Expand Down
31 changes: 31 additions & 0 deletions tests/jumps_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,37 @@ describe("jumps", function()
})
end)

it("can toggle quotes using a cycle", function()
require("nvim-surround").buffer_setup({
cycles = {
["q"] = { '"', "'", "`" },
},
})
vim.keymap.set("n", "ts", "<Plug>(nvim-surround-toggle)")

set_lines({
[["hello "world""]],
})

set_curpos({ 1, 10 })
vim.cmd("normal tsq")
check_lines({
[["hello 'world'"]],
})

set_curpos({ 1, 10 })
vim.cmd("normal tsq")
check_lines({
[["hello `world`"]],
})

set_curpos({ 1, 10 })
vim.cmd("normal tsq")
check_lines({
[["hello "world""]],
})
end)

it("for quotes only target the current line", function()
set_lines({
[[This 'line' has quotes]],
Expand Down