Skip to content

Configuration

kylechui edited this page Jul 28, 2022 · 6 revisions

Warning: Everything on this page is specific to the pattern-matching branch, and will not work with the main branch!

Table of Contents


The Basics

To configure nvim-surround, simply call require("nvim-surround").setup with your desired configuration. This sets up your "global configuration" for the plugin, which is enabled on all buffers.

To configure buffer-local behavior, call require("nvim-surround").buffer_setup. One common use case is to run this command in ftplugin/[language].lua, to have a custom setup per filetype.

Configuration for nvim-surround is additive, meaning that any configuration you provide will be merged with the default, and the result is used. This means that it is unnecessary to re-define all defaults in your configuration if you just want to add one or two things. In particular, when you call require("nvim-surround").setup the provided configuration overrides the default configuration where there are conflicts, and uses them otherwise. Furthermore, when you call require("nvim-surround").buffer_setup the provided configuration overrides your global configuration where there are conflicts, and uses them otherwise.

Definitions

A selection is a table with two keys first_pos and last_pos, each of which is a pair of integers representing a line number/column pair, inclusive. For example:

local selection = {
    first_pos = { 1, 1 },
    last_pos = { 10, 5 },
}

A pair of selections is a table with two keys left and right, each of which is a selection, inclusive. For example:

local selections = {
    left = {
        first_pos = { 1, 1 },
        last_pos = { 1, 3 },
    },
    right = {
        first_pos = { 5, 6 },
        last_pos = { 6, 3 },
    },
}

Modifying Defaults

The default configuration gives a list of the provided defaults. To override any of the given defaults, replace the corresponding value. To disable any of the provided defaults, set the corresponding value to false. For example, the following configuration disables b and B as aliases, and uses them to add LaTeX-style braces instead of their defaults.

-- Put this in ftplugin/tex.lua
require("nvim-surround").buffer_setup({
    delimiters = {
        ["b"] = {
            add = { "\\left(", "\\right)" },
        },
        ["B"] = {
            add = { "\\left{", "\\right}" },
        },
    },
    aliases = {
        ["b"] = false,
        ["B"] = false,
    },
})

keymaps

The values of this table are strings or false. If a string, then that string is the keymap used to trigger a certain action. If false, then the keymap is disabled.

delimiters

This table contains the definitions for all the various surround actions. Each key must be a string of exactly one character (with one exception), and maps to a table with the following three (optional) keys.

The one exception to this rule is the key invalid_key_behavior, which defines what behavior is exhibited when a key doesn't exist, i.e. throw an error, do nothing, or something else entirely. TODO: Find a better place to explain this.

add

The value of this key is a function that returns the delimiter pair that is used when adding a new surround to the buffer (via insert/normal/visual or any of their variants).

Note: nvim-surround uses the idea that tables of strings denote multi-line strings. Delimiters are pairs of tables of strings, with the first element denoting the left delimiter, and the second element denoting the right.

If the delimiter pairs to be added are static, then the function can be omitted and the add key can directly take on the value of the table. Furthermore, delimiters that are just a single line can be represented by a string, instead of a table with one element. For example, the following configurations are equivalent:

-- "Standard" form
require("nvim-surround").buffer_setup({
    delimiters = {
        ["*"] = {
            add = function()
                return { { "multi", "line", "string" }, { "single-line string" } },
            end,
        },
    },
})
-- Using syntactic sugar
require("nvim-surround").buffer_setup({
    delimiters = {
        ["*"] = {
            add = { { "multi", "line", "string" }, "single-line string" },
        },
    },
})

While these delimiter pairs can be static, anything can be run in these functions. User input, the current buffer's contents, and even Tree-sitter queries are all fair game to be used when determining the delimiter pair.

find

The value of the find key is either a function that returns a selection or a string that is interpreted as a Lua pattern, which is used to find the selection that includes the delimiter pair to be modified. If it is omitted, then nvim-surround defaults to using the a[char] text-object to find the selection. For example, when modifying parentheses, the a) text-object already exists, so the find key is omitted.

delete

The value of this optional key is a function that returns a pair of selections, or a string that is interpreted as a Lua pattern, which is used to narrow down the found selection to the selections to be deleted. The way this is done is via match groups, where match groups determine what the left and right delimiters are. However, due to quirks of the Lua language, four match groups are needed:

  • The match group containing the left delimiter
  • An empty match group () immediately following the first group
  • The match group containing the right delimiter
  • An empty match group () immediately following the second group

If this key is omitted, then the delimiter pair is assumed to exist at the ends of the selection, and the length of the pair given in the add key is used to discern the selections. For example, the ] surround omits the delete key, the length of both [ and ] is 1, so the first and last characters get deleted.

change

The change key holds a table that contains two keys, target and replacement. If this key is omitted, then the delimiter pair is assumed to exist at the ends of the selection, and is deleted. The user is then queried for a second character, which is used to add a new delimiter pair at the old locations.

target

Like delete, the value of this key is a function, or string that is interpreted as a Lua pattern, and is used to narrow down the found selection to the selections to be changed. Again, you must provide all four match groups, two for the selections and two empty groups. This is explicitly a different string from the delete key, since it is often the case that deleting and changing surrounding pairs affect two different selections. For example, deleting a function call includes the parentheses, but changing (renaming) it does not.

replacement

Like add, the value of this optional key is a function that returns the delimiter pair that is used when adding a new surround to the buffer. If this key is provided, then the user is not queried for another character, and this function is used to replace the deleted delimiters. If it is omitted, then the add key for a user-queried character is used to provide the replacement.

aliases

TODO

move_cursor

TODO

Case Study: HTML Tags

Consider the default configuration for HTML tags via the t key (the only difference with T is how changing tags is handled):

["t"] = {
    add = function()
        local input = get_input("Enter the HTML tag: ")
        if input then
            local element = input:match("^<?([%w-]+)")
            local attributes = input:match("%s+([^>]+)>?$")
            if not element then
                return nil
            end

            local open = attributes and element .. " " .. attributes or element
            local close = element

            return { { "<" .. open .. ">" }, { "</" .. close .. ">" } }
        end
    end,
    delete = "^(%b<>)().-(%b<>)()$",
    change = {
        target = "^<([%w-]*)().-([^/]*)()>$",
        replacement = function()
            local element = get_input("Enter the HTML element: ")
            if element then
                return { { element }, { element } }
            end
        end,
    },
},

Adding a HTML Tag

After the user triggers the add function, the following occurs:

  • The user is queried for some input
  • The input is split into its element and attributes
  • The opening tag is created using both the element and attributes, whereas the closing tag only uses the element type
  • A table is returned that contains the opening/closing tag, with the corresponding angle brackets

For example:

  • The user triggers the add function, via insert/normal/visual keymaps (or any of their variants)
  • The user inputs div id="some div" and hits <CR>
  • The input string is split such that the element contains div, and the attributes are id="some div"
  • The open/closing tags are created and returned, <div id="some div"> and </div>

Deleting a HTML Tag

TODO: delete key should accept functions as well

When the user types dst, the following occurs:

  • nvim-surround tries to use the find key to find the surrounding HTML selection, but none exists
    • It instead defaults to the at text-object, and properly finds the nearest tag selection
    • Note: This method is preferred to pattern matching, as it properly deals with nested HTML tags
  • nvim-surround uses the delete key, and since it is a string, interprets it as a Lua pattern
    • In this case, the Lua pattern matches the outermost set of HTML tags
  • The selections get deleted

For example, consider the following buffer, where the cursor lays on the placeholder text:

<div id="some id"
     class="some class">
    Some placeholder text.
    <h1>
        Hello world!
    </h1>
</div>
  • nvim-surround uses the at text-object to get the surrounding HTML selection, which in this case happens to be the entire buffer
  • The Lua pattern in the delete key has the value ^(%b<>)().-(%b<>)()$
    • The left delimiter is the opening HTML tag, and the right delimiter the closing tag
    • Empty capture groups are provided immediately after both capture groups
  • The outer div tag gets deleted

Changing a HTML Tag

TODO: change.target key should accept functions as well

When the user types cst, the following occurs:

  • nvim-surround tries to use the find key to find the surrounding HTML selection, but none exists
    • It instead defaults to the at text-object, and properly finds the nearest tag selection
    • Note: This method is preferred to pattern matching, as it properly deals with nested HTML tags
  • nvim-surround uses the change.target key, and since it is a string, interprets it as a Lua pattern
    • In this case, the Lua pattern matches the tag type of the outer div tag
  • The selections get deleted, and nvim-surround checks if a change.replacement key exists
    • Since one does, the user is not queried for another input character, and the change.replacement function is used to replace the deleted selections

Consider the same sample buffer from before, with the cursor again residing on the placeholder text:

<div id="some id"
     class="some class">
    Some placeholder text.
    <h1>
        Hello world!
    </h1>
</div>
  • nvim-surround uses the at text-object to get the surrounding HTML selection, which in this case happens to be the entire buffer
  • The Lua pattern in the change.target key has the value ^<([%w-]*)().-([^/]*)()>$, which selects only the tag type
    • In this case, just the word div on the first and last line are selected
    • Empty capture groups are provided immediately after both capture groups
  • The divs get deleted and change.replacement is called, which queries the user for replacement text
    • If the user types h2 and then hits <CR>, then the div gets replaced by h2, yielding the following buffer:
<h2 id="some id"
     class="some class">
    Some placeholder text.
    <h1>
        Hello world!
    </h1>
</h2>

Case Study: Function Calls

Consider the default configuration for function calls via the f key:

["f"] = {
    add = function()
        local result = get_input("Enter the function name: ")
        if result then
            return { { result .. "(" }, { ")" } }
        end
    end,
    find = "[%w_]+%b()",
    delete = "^([%w_]+%()().-(%))()$",
    change = {
        target = "^([%w_]+)().-()()$",
        replacement = function()
            local result = get_input("Enter the function name: ")
            if result then
                return { { result }, { "" } }
            end
        end,
    },
},

Adding a Function Call

After the user triggers the add function, the following occurs:

  • The user is queried for the function name
  • The left delimiter ends up being [function name](, and the right delimiter )

Consider the following buffer, where the cursor is over the word args. If the user types ysiwf, and then func_name for the function name, the buffer

local eval = args

would get transformed into

local eval = func_name(args)

Deleting a Function Call

When the user types dsf, the following occurs:

  • nvim-surround uses the find key to find the nearest function call
  • The delete key is then used to match the function name and opening parenthesis, as well as the closing parenthesis
  • The function call is deleted

Consider the following buffer, where the cursor resides on some_args:

local eval = func_1(func_2(some_args))

When dsf is typed, the following occurs:

  • nvim-surround uses the find key [%w_]+%b() to find the selection, which in this case is the inner function call func_2(some_args)
  • The delete key ^([%w_]+%()().-(%))()$ matches the delimiter pair to func_2( and ), which then gets deleted

The resulting buffer is

local eval = func_1(some_args)

Changing a Function Call

When the user types csf, the following occurs:

  • nvim-surround uses the find key to find the nearest function call
  • The change.target key is then used to match the function name, which is then deleted
  • Since a change.replacement key is provided, the user is not queried for another character, and change.replacement is evaluated to see what pair should replace the deleted pair
  • In this case, the operation acts to just rename the function

Consider the same buffer, but the cursor now resides on eval:

local eval = func_1(func_2(some_args))

When csf is typed, the following occurs:

  • nvim-surround uses the find key [%w_]+%b() to find the selection, which in this case is the outer function call func_1(func_2(some_args))
  • The change.target key ^([%w_]+)().-()()$ matches the delimiter pair to func_1
    • Note that since we only wish to replace the function name, we only provide a match group for the left selection to change
    • It is still necessary to provide all four capture groups; we simply let the third one be empty in place of capturing the right selection
  • Since the change.replacement key exists, the user is not queried for another input character, and the change.replacement function is evaluated for the replacement text
    • If the user types a_new_name and hits <CR>, then the function gets renamed to a_new_name, yielding the following buffer:
local eval = a_new_name(func_2(some_args))