Learn Neovim - Code Editing Productivity Boosters
Series/Learn Neovim/Episode 20
Episode 20 of 28

Learn Neovim - Code Editing Productivity Boosters

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.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

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.

Main Discussion

Auto Closing Quotes & Brackets: nvim-autopairs

The 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.

lua/plugins/autopairs.lua
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:

  1. 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.
  2. 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.
  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.

Auto Closing HTML/JSX Tags: nvim-ts-autotag

In 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:

lua/plugins/autotag.lua
return {
  "windwp/nvim-ts-autotag",
  ft = { "html", "xml", "jsx", "tsx", "vue", "svelte", "astro" },
  opts = {},
}

What this plugin does is very contextual:

  • Type <div → automatically closed into <div></div> with the cursor in the middle.
  • Change the opening tag name (e.g. <div to <section) → the closing tag follows live. This is a very helpful feature when refactoring JSX.
  • Delete the opening tag → the closing tag is deleted too.

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.

Surround Text Manipulation: nvim-surround

Now 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.

lua/plugins/surround.lua
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:

Adding Surround (ys)

CommandAction
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

Changing Surround (cs)

CommandAction
cs"'Change "text" into 'text'
cs'<em>Change 'text' into <em>text</em>
cst"Change an HTML tag into "..."

Deleting Surround (ds)

CommandAction
ds"Delete a pair of double quotes
ds(Delete a bracket pair (but keep the text inside)
dstDelete 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.

Quick Code Commenting: Comment.nvim

The 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:

lua/plugins/comment.lua
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:

CommandAction
gccToggle the comment on the cursor line
gc3jComment the 3 lines below (motion)
gcapComment the whole paragraph (text object)
gc (visual)Toggle comments on the selected area
gbcToggle a block comment /* */
gcgcToggle 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.

Visual Keybinding Helper: which-key.nvim

Finally, 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:

lua/plugins/which-key.lua
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.

Combining Everything: A Mini Case Study

Let's practice the combination of all plugins in one real scenario. Imagine you are editing index.html and find text like this:

before.html
<p>Halo dunia</p>

You want to wrap it in <strong> and change "Halo dunia" to 'Halo dunia'. With nvim-surround:

  1. Place the cursor inside <p>Halo dunia</p>.
  2. Press 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>.
  3. Result: <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.

Common Pitfalls

MistakeSymptomSolution
Autopairs conflicts with nvim-cmpThe completion popup adds a double ), the cursor jumps wronglyAdd the cmp.event:on("confirm_done", ...) integration as above
Wrong surround syntaxys" does nothing, or errorsRemember the grammar order: operator first (ys/cs/ds), then text object (iw, ap), then target (", ', <b>)
Forgot the target on surroundcs" deletes the surround but with no new targetcs needs two arguments: what to remove and what to replace it with, e.g. cs"'
Which-key delay feels slowPopup appears late and slows down typingLower delay = 0 or set triggers only for <leader>
gcc overrides another keymapOther keymaps with gc stop workingCheck :verbose map gc to see the mapping collision
Autotag renames plain HTML tagsThe <div> tag changes while editing attributesMake 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.

Closing

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!

Learn Neovim - Code Editing Productivity Boosters | Learn Neovim