Editor Enhancement Plugins
These plugins enhance the core editing experience without adding LSP complexity. They make common operations faster and add new motion vocabulary.
1. nvim-autopairs — Smart Bracket Completion
Automatically closes brackets, quotes, and other pairs as you type:
lua/plugins/editor.lua
{
"windwp/nvim-autopairs",
event = "InsertEnter",
dependencies = { "hrsh7th/nvim-cmp" },
config = function()
local autopairs = require("nvim-autopairs")
local cmp_autopairs = require("nvim-autopairs.completion.cmp")
local cmp = require("cmp")
autopairs.setup({
check_ts = true, -- use treesitter to check pairs
ts_config = {
lua = { "string" }, -- don't add pairs inside lua strings
javascript = { "template_string" },
},
disable_filetype = { "TelescopePrompt", "spectre_panel" },
fast_wrap = {
map = "<M-e>", -- Alt+e to wrap selection in pair
chars = { "{", "[", "(", '"', "'" },
pattern = string.gsub([[ [%'%"%)%>%]%)%}%,] ]], "%s+", ""),
end_key = "$",
keys = "qwertyuiopzxcvbnmasdfghjkl",
check_comma = true,
highlight = "Search",
highlight_grey = "Comment",
},
})
-- Integrate with nvim-cmp: add ( after function completion
cmp.event:on("confirm_done", cmp_autopairs.on_confirm_done())
end,
},
Key Behaviors
| Typed | Result | Cursor |
|---|---|---|
( | () | inside |
" | "" | inside |
{<CR> | {<CR>}<CR> | indented inside |
<BS> inside () | removes both | — |
<M-e> on word | wraps in pair | — |
2. nvim-surround — Add/Change/Delete Surroundings
The best surround plugin. Works on any delimiter, quotes, function calls, HTML tags, and custom pairs:
{
"kylechui/nvim-surround",
version = "*",
event = "VeryLazy",
opts = {
keymaps = {
insert = "<C-g>s", -- in insert mode
insert_line = "<C-g>S",
normal = "ys", -- ys{motion}{char}
normal_cur = "yss", -- yss{char} for current line
normal_line = "yS",
normal_cur_line = "ySS",
visual = "S", -- S{char} in visual mode
visual_line = "gS",
delete = "ds", -- ds{char}
change = "cs", -- cs{old}{new}
change_line = "cS",
},
},
},
Usage Examples
# Add surroundings:
ysw" → surround word with double quotes: word → "word"
ys3w( → surround 3 words with parens
ysiw[ → surround inner word with brackets
yss{ → surround current line with braces
ysiw<em> → surround inner word in HTML tag: <em>word</em>
# Change surroundings:
cs"' → change " to ': "hello" → 'hello'
cs({ → change ( to {: (args) → {args}
cs"<strong> → change quote to HTML tag
# Delete surroundings:
ds" → delete surrounding quotes: "hello" → hello
ds( → delete surrounding parens: (args) → args
dst → delete surrounding HTML tag: <p>text</p> → text
# Visual mode:
viw S" → select inner word, surround with "
V S<div> → select line, surround with div tag
3. Comment.nvim — Toggle Comments
{
"numToStr/Comment.nvim",
keys = {
{ "gcc", mode = "n", desc = "Comment line" },
{ "gbc", mode = "n", desc = "Block comment line" },
{ "gc", mode = "v", desc = "Comment selection" },
{ "gb", mode = "v", desc = "Block comment selection" },
},
opts = {
padding = true,
sticky = true,
ignore = "^$", -- ignore blank lines
mappings = {
basic = true,
extra = true,
},
pre_hook = function(ctx)
-- Use treesitter-context-commentstring if available
local ok, tsc = pcall(require, "ts_context_commentstring.integrations.comment_nvim")
if ok then return tsc.create_pre_hook()(ctx) end
end,
},
},
-- Context-aware comments for embedded languages (JSX, PHP+HTML, etc.)
{
"JoosepAlviste/nvim-ts-context-commentstring",
lazy = true,
opts = { enable_autocmd = false },
},
Usage
n: gcc → toggle comment on current line
n: gc3j → comment current + 3 lines down
n: gco → add comment on line below
n: gcO → add comment on line above
n: gcA → add comment at end of line
v: gc → toggle comment on selection
v: gb → block comment on selection
Context-Aware Comments
With ts_context_commentstring, comments use the correct syntax for mixed files:
<!-- In <template> section of Vue: uses HTML comment -->
<div>content</div>
// In <script> section: uses JS comment
const x = 1
4. Flash.nvim — Fast Cursor Jumping
Flash replaces traditional f/t/s motions with a visual label-based jump system:
{
"folke/flash.nvim",
event = "VeryLazy",
opts = {
labels = "asdfghjklqwertyuiopzxcvbnm",
search = {
mode = "fuzzy",
incremental = true,
},
jump = {
jumplist = true,
pos = "start",
history = false,
register = false,
nohlsearch = false,
autojump = false,
},
label = {
uppercase = false,
exclude = "",
current = true,
after = true,
before = false,
style = "overlay",
reuse = "lowercase",
distance = true,
min_pattern_length = 0,
rainbow = { enabled = false, shade = 5 },
},
highlight = {
backdrop = true,
matches = true,
priority = 5000,
groups = {
match = "FlashMatch",
current = "FlashCurrent",
backdrop = "FlashBackdrop",
label = "FlashLabel",
},
},
modes = {
-- enhanced f/t/F/T
char = {
enabled = true,
autohide = true,
jump_labels = true,
multi_line = true,
label = { exclude = "hjkliardc" },
char_actions = function(motion)
return {
[";"] = "next",
[","] = "prev",
[motion:lower()] = "next",
[motion:upper()] = "prev",
}
end,
search = { wrap = false },
highlight = { backdrop = false },
jump = { register = false },
},
treesitter = {
labels = "abcdefghijklmnopqrstuvwxyz",
jump = { pos = "range" },
search = { incremental = false },
label = { before = true, after = true, style = "inline" },
highlight = { backdrop = false, matches = false },
},
},
},
keys = {
{ "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash jump" },
{ "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash treesitter" },
{ "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" },
{ "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter search" },
{ "<c-s>", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle flash search" },
},
},
How Flash Works
Type: s → activates flash jump mode
Type: fun → highlights all "fun" matches with labels (a, b, c...)
Type: c → jump to the match labelled 'c'
Type: S → treesitter jump
→ labels entire syntax nodes (functions, classes, blocks)
Operator mode:
dS → delete + treesitter select → pick a node to delete
ySiw) → surround inner word after jumping
5. mini.nvim — 40+ Micro Plugins in One
mini.nvim is a collection of small, focused plugins. Use only the modules you need:
{
"echasnovski/mini.nvim",
version = false,
config = function()
-- mini.ai: better text objects (extends i/a with more targets)
require("mini.ai").setup({
n_lines = 500,
custom_textobjects = {
-- whole buffer
B = function()
local from = { line = 1, col = 1 }
local to = {
line = vim.fn.line("$"),
col = math.max(vim.fn.getline("$"):len(), 1),
}
return { from = from, to = to }
end,
},
})
-- mini.move: move lines/selections with Alt+hjkl
require("mini.move").setup({
mappings = {
left = "<M-h>",
right = "<M-l>",
down = "<M-j>",
up = "<M-k>",
line_left = "<M-h>",
line_right = "<M-l>",
line_down = "<M-j>",
line_up = "<M-k>",
},
})
-- mini.pairs: alternative to nvim-autopairs (lighter)
-- require("mini.pairs").setup()
-- mini.splitjoin: split/join arguments (gS to split, gJ to join)
require("mini.splitjoin").setup({
mappings = { toggle = "gs" },
})
-- mini.bufremove: safer buffer deletion that keeps window open
require("mini.bufremove").setup()
vim.keymap.set("n", "<leader>bd", function()
require("mini.bufremove").delete(0, false)
end, { desc = "Delete buffer" })
-- mini.hipatterns: highlight patterns (TODO, FIXME, hex colors)
local hipatterns = require("mini.hipatterns")
hipatterns.setup({
highlighters = {
fixme = { pattern = "%f[%w]()FIXME()%f[%W]", group = "MiniHipatternsFixme" },
hack = { pattern = "%f[%w]()HACK()%f[%W]", group = "MiniHipatternsHack" },
todo = { pattern = "%f[%w]()TODO()%f[%W]", group = "MiniHipatternsTodo" },
note = { pattern = "%f[%w]()NOTE()%f[%W]", group = "MiniHipatternsNote" },
-- highlight hex colors inline
hex_color = hipatterns.gen_highlighter.hex_color(),
},
})
-- mini.indentscope: animated indent scope indicator
require("mini.indentscope").setup({
symbol = "│",
options = { try_as_border = true },
draw = { animation = require("mini.indentscope").gen_animation.none() },
})
end,
},
mini.nvim Module Reference
| Module | Purpose | Alternative |
|---|---|---|
mini.ai | Extended text objects | treesitter-textobjects |
mini.move | Move lines/selections | — |
mini.pairs | Auto-pairs | nvim-autopairs |
mini.surround | Surround | nvim-surround |
mini.comment | Toggle comments | Comment.nvim |
mini.bufremove | Safe buffer delete | — |
mini.splitjoin | Split/join args | splitjoin.vim |
mini.hipatterns | Pattern highlights | todo-comments |
mini.indentscope | Animated indent | indent-blankline |
mini.sessions | Session management | auto-session |
mini.starter | Dashboard | alpha-nvim |
mini.files | File manager | oil.nvim |
mini.diff | Inline diff | gitsigns |
mini.statusline | Status line | lualine |
mini.tabline | Buffer tabs | bufferline |
mini.map | Code minimap | — |
mini.clue | Key hints | which-key |
6. Todo-comments.nvim — Highlight TODOs
{
"folke/todo-comments.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
event = { "BufReadPost", "BufNewFile" },
keys = {
{ "]t", function() require("todo-comments").jump_next() end, desc = "Next TODO" },
{ "[t", function() require("todo-comments").jump_prev() end, desc = "Prev TODO" },
{ "<leader>ft", "<cmd>TodoTelescope<cr>", desc = "Find TODOs" },
{ "<leader>fT", "<cmd>TodoTrouble<cr>", desc = "TODO list (Trouble)" },
},
opts = {
signs = true,
sign_priority = 8,
keywords = {
FIX = { icon = " ", color = "error", alt = { "FIXME", "BUG", "FIXIT", "ISSUE" } },
TODO = { icon = " ", color = "info" },
HACK = { icon = " ", color = "warning" },
WARN = { icon = " ", color = "warning", alt = { "WARNING", "XXX" } },
PERF = { icon = " ", color = "hint", alt = { "OPTIM", "PERFORMANCE", "OPTIMIZE" } },
NOTE = { icon = " ", color = "hint", alt = { "INFO" } },
TEST = { icon = "⏲ ", color = "test", alt = { "TESTING", "PASSED", "FAILED" } },
},
highlight = {
multiline = true,
multiline_pattern = "^.",
multiline_context = 10,
before = "",
keyword = "wide",
after = "fg",
pattern = [[.*<(KEYWORDS)\s*:]],
comments_only = true,
},
},
},
Usage in code:
-- TODO: implement this feature
-- FIXME: this breaks when x is nil
-- HACK: workaround for upstream issue #123
-- NOTE: this is intentional behavior
-- PERF: consider caching this result