In this episode we upgrade Neovim's editing capabilities with automation plugins: autopairs for brackets and tags, surround text manipulation, quick comments, and the which-key shortcut helper popup so your hands never stop to think.

After building an integrated terminal and task runner inside Neovim in episode 19, in this episode we will improve editing speed itself. Fast editing is not just about pressing keys quickly, but about eliminating unnecessary movements. Imagine a professional typist: they do not type faster, they just never repeat work. The same principle applies to programming — and that is what we will build with four productivity plugins: nvim-autopairs, nvim-ts-autotag, nvim-surround, and which-key.nvim.
Why is this topic relevant in real work? Imagine you are doing a code review of a large change: replacing all "..." strings with '...', adding <b> around text in a template file, or commenting out lines of code to find a bug. Without tools, each of these operations requires several tedious, error-prone manual movements. With surround, the "wrap text with something" operation becomes a single movement. Without autopairs, every opening bracket must be balanced by a closing bracket — a task that is, ironically, very easy to forget.
The combination of these four plugins genuinely reduces the number of keys you press in a day, and more importantly, reduces thought interruptions. Let's build them one by one.
nvim-autopairsThe concept is simple: when you type (, Neovim automatically adds ) and places the cursor in the middle. This plugin works for parentheses (), brackets [], braces {}, quotes ", ', backticks, and even supports html entities. Instead of typing ( ) — two keys — you only need to type one.
return {
{
"windwp/nvim-autopairs",
event = "InsertEnter",
opts = {
check_ts = true,
fast_wrap = {
map = "<M-e>",
chars = { "{", "[", "(", '"', "'" },
table = {
{ "{", "}" },
{ "[", "]" },
{ "(", ")" },
{ '"', '"' },
{ "'", "'" },
},
end_key = "$",
},
},
config = function(_, opts)
require("nvim-autopairs").setup(opts)
local cmp_autopairs = require("nvim-autopairs.completion.cmp")
local cmp = require("cmp")
cmp.event:on("confirm_done", cmp_autopairs.on_confirm_done())
end,
},
}Several important things about this configuration:
check_ts = true — enables integration with Treesitter (which we learned in episode 14). Autopairs becomes smarter: for example inside comments or strings, it will not add unneeded pairs.fast_wrap — a feature for wrapping existing text. Visually select the text, press M-e, then press the wrapping character; the text is immediately wrapped. This feature saves the movements we knew as text objects manipulation in episode 3.cmp.event:on("confirm_done", ...) — integration with nvim-cmp from episode 16. After selecting a completion item from LSP, sometimes the cursor sits right before an existing closing bracket — this plugin ensures the cursor stays inside the bracket pair, not past it.Important
The nvim-cmp integration is required if you install autopairs. Without this integration, a classic bug often appears: you select a suggestion from the completion popup that ends with ( (e.g. a function call from LSP), but autopairs instead adds an unwanted ) making the code foo()). The integration above keeps the behavior consistent.
nvim-ts-autotagIn the frontend world, brackets are not the only thing that needs auto-closing — HTML tags do too. Typing <div> then manually adding </div> is a task very prone to forgetting and typos. nvim-ts-autotag uses the Treesitter parser to close tags automatically, even handling complex cases:
return {
"windwp/nvim-ts-autotag",
ft = { "html", "xml", "jsx", "tsx", "vue", "svelte", "astro" },
opts = {},
}What this plugin does is very contextual:
<div → automatically closed into <div></div> with the cursor in the middle.<div to <section) → the closing tag follows live. This is a very helpful feature when refactoring JSX.Tip
Since nvim-ts-autotag is only useful for certain file types, we declare it with ft = { "html", ... }. This makes lazy.nvim load the plugin only when you open an HTML/JSX file — Neovim's startup time stays below the threshold we discuss in episode 23 later.
nvim-surroundNow we get to the plugin many people consider the most life-changing: nvim-surround. This plugin extends Neovim's text objects (which we learned in episode 3) with the ability to add, change, and delete "surrounds" — pairs of quotes, brackets, or tags wrapping text.
return {
{
"kylechui/nvim-surround",
version = "*",
opts = {
keymaps = {
insert = "<C-g>s",
insert_line = "<C-g>S",
normal = "ys",
normal_cur = "yss",
normal_line = "yS",
normal_cur_line = "ySS",
visual = "S",
visual_line = "gS",
delete = "ds",
change = "cs",
},
},
},
}The basic concept uses Vim's signature grammar pattern: operator + motion + target. Here is a summary of the available operations:
ys)| Command | Action |
|---|---|
ysiw" | Wrap a word in "..." |
ysiw' | Wrap a word in '...' |
yss) | Wrap the whole line in (...) |
ysiw<b> | Wrap a word in <b>...</b> (tag) |
S (visual) | Wrap the selected text in visual mode |
cs)| Command | Action |
|---|---|
cs"' | Change "text" into 'text' |
cs'<em> | Change 'text' into <em>text</em> |
cst" | Change an HTML tag into "..." |
ds)| Command | Action |
|---|---|
ds" | Delete a pair of double quotes |
ds( | Delete a bracket pair (but keep the text inside) |
dst | Delete opening & closing HTML tags |
Tip
A fitting analogy: nvim-surround is like wrapping and unwrapping in a GUI editor, but in a grammar form combinable with all the motions you have learned. ysiw" can be read as: yank surround inner word with ". Because it uses grammar, you can wrap a paragraph (ysap"), a bracket block (ysi)"), even text from a visual selection.
Note
Do you have to use kylechui/nvim-surround? There is a popular alternative called mini.surround offering similar functionality with a lighter footprint (no external dependency needed). If you prefer a minimal approach, mini.surround works; however, the ys/cs/ds syntax of nvim-surround is closer to the Vim ecosystem and widely adopted in tutorial documentation.
Comment.nvimThe operation programmers do most often after typing code is commenting and uncommenting — to temporarily disable code while debugging, or to add documentation. Comment.nvim offers smart commenting based on text objects and Treesitter:
return {
{
"numToStr/Comment.nvim",
keys = {
{ "gcc", desc = "Toggle comment baris" },
{ "gc", desc = "Toggle comment (motion/visual)" },
{ "gbc", desc = "Toggle comment block" },
{ "gb", desc = "Toggle comment block (motion/visual)" },
},
config = function()
local comment = require("Comment")
comment.setup({
toggler = {
line = "gcc",
block = "gbc",
},
opleader = {
line = "gc",
block = "gb",
},
mappings = {
basic = true,
extra = true,
},
})
end,
},
}Using it is very intuitive:
| Command | Action |
|---|---|
gcc | Toggle the comment on the cursor line |
gc3j | Comment the 3 lines below (motion) |
gcap | Comment the whole paragraph (text object) |
gc (visual) | Toggle comments on the selected area |
gbc | Toggle a block comment /* */ |
gcgc | Toggle comments on empty lines too |
Tip
The strength of Comment.nvim lies in its language-aware commentstring. In Go files it uses //, in Python #, in CSS /* */, and in files that already have a custom block comment string — it follows the buffer configuration rather than guessing. This is far smarter than the manual macros commonly used before this plugin existed.
which-key.nvimFinally, all the shortcuts you have made from episode 7 until now can number in the dozens. Humans cannot remember them all — and that is where which-key.nvim comes in. This plugin shows a popup with a list of keymaps every time you press the leader key or a keymap prefix:
return {
{
"folke/which-key.nvim",
event = "VeryLazy",
opts = {
spec = {
{ "<leader>f", group = "Find", mode = { "n", "v" } },
{ "<leader>g", group = "Git" },
{ "<leader>t", group = "Test / Terminal" },
{ "<leader>l", group = "LSP" },
{ "<leader>c", group = "Code" },
{ "<leader>b", group = "Buffer" },
},
triggers = {
{ "<auto>", mode = "n" },
{ "<leader>", mode = "n" },
},
delay = 0,
icons = {
group = "",
rules = false,
},
},
},
}When you press <leader>, a popup appears showing all keymaps starting with <leader>, grouped according to group. This is not just a cheatsheet: because every keymap must have a desc (description), which-key reads that desc and shows it in the popup. That is why since episode 7 we have always added desc to every keymap — now the benefit is visible.
Important
Make sure every keymap you create has a desc. Without desc, which-key displays confusing unlabeled keymaps. Example: vim.keymap.set("n", "<leader>ff", function() ... end, { desc = "Find file" }). If you already made keymaps in previous episodes without desc, adjust them now.
Let's practice the combination of all plugins in one real scenario. Imagine you are editing index.html and find text like this:
<p>Halo dunia</p>You want to wrap it in <strong> and change "Halo dunia" to 'Halo dunia'. With nvim-surround:
<p>Halo dunia</p>.cs<p> → the tag changes to... hmm, actually cs<p> requires a tag target. To change the tag surrounding the text, use cst<strong> — change the surrounding tag into <strong>.<strong>Halo dunia</strong>.Now comment out that line temporarily for debugging: position the cursor then press gcc. The line becomes <!-- <strong>Halo dunia</strong> -->. Press gcc again to restore it. All of this happens without a mouse and without switching modes manually.
| Mistake | Symptom | Solution |
|---|---|---|
Autopairs conflicts with nvim-cmp | The completion popup adds a double ), the cursor jumps wrongly | Add the cmp.event:on("confirm_done", ...) integration as above |
| Wrong surround syntax | ys" does nothing, or errors | Remember the grammar order: operator first (ys/cs/ds), then text object (iw, ap), then target (", ', <b>) |
| Forgot the target on surround | cs" deletes the surround but with no new target | cs needs two arguments: what to remove and what to replace it with, e.g. cs"' |
| Which-key delay feels slow | Popup appears late and slows down typing | Lower delay = 0 or set triggers only for <leader> |
gcc overrides another keymap | Other keymaps with gc stop working | Check :verbose map gc to see the mapping collision |
| Autotag renames plain HTML tags | The <div> tag changes while editing attributes | Make sure ft only contains the right file types; autotag uses Treesitter so the context must be valid |
Warning
The combination of nvim-autopairs and nvim-ts-autotag can "collide" with each other in JSX files if not configured. A common case: you type > in the middle of a tag, autopairs closes it, and autotag adds the closing tag — the final result is duplicated code. The safest solution is to let autopairs handle brackets/quotes and autotag handle tags; make sure check_ts = true is active so both are context-aware.
In episode 20 we added four editing weapons that touch the core of productivity: nvim-autopairs and nvim-ts-autotag for context-aware automatic closing pairs, nvim-surround for wrapping and changing text with the ys/cs/ds grammar, Comment.nvim for one-key comment toggling, and which-key.nvim for a living shortcut cheat-sheet.
What to remember: these plugins are not a goal, but a way to eliminate repetitive movements so your mind focuses on logic, not typing mechanics. The cumulative effect is felt after using them for a few days — try committing to one week of only using surround and comment, and feel the difference.
Now that editing is fast, it is time to add a "second brain" to Neovim. In episode 21, we will cover AI Code Completion Integration — integrating GitHub Copilot, Codeium, or Avante as a programming assistant working directly inside the editor. Stay motivated!