Skip to content

For Developers

kylechui edited this page Aug 29, 2022 · 1 revision

Table of Contents


Hi there! Welcome to a (hopefully) brief overview on the nvim-surround codebase. My hope is that this will help new contributors navigate the codebase, as well as shed some light on certain design decisions I've made. For more details, see the comments, and feel free to open a discussion tagging @kylechui!

Glossary

Note: For clarity/consistency, all singular elements are 1-indexed, and all ranges are endpoint-inclusive.

  • delimiter pair: A text pair that represents what surrounds a selection on the left and right.
  • add: Any action that introduces a new delimiter pair to the buffer, either through insert/normal/visual mode commands.
  • modify: Any action that deletes/changes an existing delimiter pair in the buffer.

See init.lua for more technical definitions (e.g. selection).

File Organization

As of this writing, the codebase currently takes the following form:

nvim-surround
│   lua/nvim-surround
│   ├── init.lua
│   └── ...
├── tests
│   └── minimal_init.lua
│   └── basics_spec.lua
│   └── ...
└── doc
    └── nvim-surround.txt

lua/nvim-surround/

This folder contains the actual code that runs the plugin. Here's a brief description of what each file does:

  • init.lua: Defines type annotations for the project, as well as all high-level functions for actually using the plugin, e.g. normal_surround and normal_callback.
  • config.lua: Defines a set of functions related to user configuration.
  • buffer.lua: Defines a bunch of helper functions related to the current buffer. The functions are grouped into a few categories (cursor, marks, buffer contents, highlights).
  • utils.lua: Defines a set of helpful utility functions that don't really fit in elsewhere.
  • cache.lua: Defines some local variables as cache, for dot-repeatability.

The remaining four files can be thought of as various "engines" for finding selection(s):

  • patterns.lua: Deals with Lua patterns.
  • motions.lua: Deals with Vim motions, either built-in or defined via :h operator-pending.
  • treesitter.lua: Deals with singular Tree-sitter nodes.
  • queries.lua: Deals with queries; can be thought of as analogous to capture clauses for Lua patterns.

tests/

Contains a set of plenary.nvim test cases. Also holds a minimal_init.lua, for standardized testing.

doc/

Contains the documentation for :h nvim-surround in nvim-surround.txt.

Configuration Flow

To provide various syntax sugars for configuring the plugin, while also preserving internal simplicity, there is a translation layer between the user-provided configuration and the internal configuration of the plugin. This is primarily handled by config.translate_opts.

The internal model requires that all surrounds contain all possible keys (add, find, delete, change.target, change.replacement).

For any given surround, the internal model is a restriction on the configuration options provided to the end user:

  • add: Necessarily a function that returns a pair of tables of strings. The elements of the pair represent the left/right delimiter pair. Tables of strings represent multi-line strings.
  • find: Necessarily a function that returns a selection.
  • delete: Necessarily a function that returns a pair of selections.
  • change.target: Same as delete.
  • change.replacement: Same as add.

Execution Flow

Adding a New Delimiter Pair

Here we describe the general code execution flow for an arbitrary add command.

  • There are no arguments passed to normal_surround, so the dot-repeat cache is cleared and normal_callback is called with a user-provided Vim motion. See :h opfunc for details.
    • The selection to be surrounded is determined by the [ and ] marks.
    • normal_callback queries the user for the delimiter pair to surround the selection. This pair is found by calling the surround's add key.
    • The dot-repeat cache is set.
  • normal_surround is re-called, but this time with the selection and delimiter pair.
    • The selection is surrounded with the delimiter pair.

The other functions in init.lua all behave similarly, often using this "back-and-forth" strategy for differentiating what should and should not be dot-repeated (visual-mode additions are not dot-repeatable).

Deleting an Existing Delimiter Pair

Here we describe the general code execution flow for an arbitrary delete command.

  • The delete_surround function is called by the user.
  • Since there are no arguments passed, delete_callback is called.
    • The user is queried for a character to be deleted.
  • delete_surround is then called with this character.
    • The selections to be deleted are computed by taking the selection found via the surround's find key, then "filtering" it down to a pair of selections via the surround's delete key.
    • The pair of selections are then deleted from the buffer, right delimiter first (since deleting the left one will shift the positions around in the buffer).

In general, the process for finding pairs of selections is done by taking a "parent" selection and "filtering" it down. This is done to provide more extensibility/modularity, as the parent selection may be found via a variety of different methods (i.e. Vim motion, Lua pattern, etc.).

Changing a delimiter pair to a different pair is more or less a combination of delete, then add operations.