Telescope, Treesitter and essential coding plugins for Neovim
- Published on
- ·7 min read
Beyond the LSP
So, LSP is good - it's the base layer. But that alone? Not enough. To build a real development environment, you need plugins that handle search, code manipulation, Git. The essentials, basically. I'm going to walk you through the ones I actually use every single day, and honestly, I'd be lost without them.
Telescope: the hunt tool
Telescope is my Swiss Army knife. The plugin I probably use 10 times a day. It's a fuzzy finder - you point it at files, text, buffers, help docs, literally everything you can think of searching for, and it finds it from a single, unified interface.
Performance with fzf-native
Look, out of the box Telescope... it's not snappy on a huge codebase. Thousands of files? You feel it. I installed telescope-fzf-native - it's a C binary that completely replaces the search algorithm. The difference? Night and day. Literally.
The keybindings (the ones you actually press)
local builtin = require("telescope.builtin")
vim.keymap.set("n", "<leader>ff", builtin.find_files, { desc = "Find files" })
vim.keymap.set("n", "<leader>fg", builtin.live_grep, { desc = "Live grep" })
vim.keymap.set("n", "<leader>fb", builtin.buffers, { desc = "Buffers" })
vim.keymap.set("n", "<leader>fh", builtin.help_tags, { desc = "Help tags" })
vim.keymap.set("n", "<leader>fr", builtin.oldfiles, { desc = "Recent files" })
vim.keymap.set("n", "<leader>fw", builtin.grep_string, { desc = "Grep word under cursor" })
vim.keymap.set("n", "<leader>/", builtin.current_buffer_fuzzy_find, { desc = "Fuzzy find in buffer" })
You're mostly using <leader>ff to jump to a file, <leader>fg to scan the entire project for text. And <leader>/? That's my killer move - search within the current buffer. It's 100x better than vanilla /.
Configuration and filters (pro tip: ignore patterns are everything)
require("telescope").setup({
defaults = {
path_display = { "truncate" },
file_ignore_patterns = {
"node_modules/",
".git/",
".next/",
"dist/",
"%.lock",
"%.min%.js",
"%.min%.css",
},
mappings = {
i = {
["<C-j>"] = require("telescope.actions").move_selection_next,
["<C-k>"] = require("telescope.actions").move_selection_previous,
["<C-q>"] = require("telescope.actions").send_selected_to_qflist
+ require("telescope.actions").open_qflist,
},
},
},
})
Here's what actually matters:
- Ignore patterns : I've blacklisted
node_modules,.git,.next,dist, lock files, and minified assets. On a Next.js project, without this, you'd get 50,000 garbage results. No thanks. - Truncated path display : Long paths are noise. Truncating them makes the UI cleaner.
- C-j/C-k in insert mode : Navigate without leaving insert mode. Small thing, massive quality-of-life improvement.
- C-q : Sends results to quickfix. Invaluable when you need to do a bulk search-and-replace across multiple files.
The preview pane? It uses Treesitter for syntax highlighting. Not regex voodoo - actual parsing.
Treesitter: the real deal
Treesitter - it's not just a syntax highlighter (if it were, I'd be disappointed). It's an actual incremental parser that understands code structure. It knows where functions start and end, where comments are, how blocks nest. Not primitive regex - real AST parsing.
18 parsers that cover everything I do
require("nvim-treesitter.configs").setup({
ensure_installed = {
"bash", "c", "css", "dockerfile", "go", "html",
"javascript", "json", "lua", "markdown", "markdown_inline",
"python", "rust", "tsx", "typescript", "vim", "vimdoc", "yaml",
},
auto_install = true,
highlight = { enable = true },
indent = { enable = true },
})
18 total. Bash, C, CSS, Docker, Go, HTML, JavaScript, JSON, Lua, Markdown, Python, Rust, TypeScript, Vim, YAML. That's like 95% of what I code in. New language? auto_install = true means it just installs itself. Magic.
Autotag for HTML/JSX (genuinely a game-changer in React)
nvim-ts-autotag - you close an HTML tag, Treesitter closes it for you automatically. Better yet: you rename the opening tag? The closing tag updates too. Before this, I'd leave tags hanging open. Now? Impossible.
Tailwind CSS (three separate plugins because why not)
Frontend work? I've got three Tailwind plugins running:
- tailwind-tools.nvim : Color preview right in the buffer. Long class lists are "concealed" for readability. Class order? Auto-sorted. Pure luxury.
Editing essentials
nvim-autopairs
{
"windwp/nvim-autopairs",
event = "InsertEnter",
config = function()
local npairs = require("nvim-autopairs")
npairs.setup({
check_ts = true,
})
-- Integration with nvim-cmp
local cmp_autopairs = require("nvim-autopairs.completion.cmp")
require("cmp").event:on("confirm_done", cmp_autopairs.on_confirm_done())
end,
}
Auto-closing parens, brackets, braces, quotes - everything that needs closing. But here's the clever bit: check_ts = true asks Treesitter first. No stray parens in comments, no quote disasters in strings. Smart. And cmp integration? When you accept a completion, the pairs close too.
Comment.nvim (comment code without thinking)
{
"numToStr/Comment.nvim",
opts = {},
}
gcc toggles a line between commented and uncommented. gc plus a motion? gcap comments an entire paragraph. gc3j comments 3 lines. The plugin knows the comment syntax for the language. No thinking required.
nvim-surround (manipulate the "wrappers")
{
"kylechui/nvim-surround",
version = "*",
event = "VeryLazy",
opts = {},
}
Have a word you want to wrap in quotes? Parentheses? Backticks? This is the tool.
ys{motion}{char}- add a wrapper.ysiw"wraps the word under the cursor in quotes.cs{old}{new}- change.cs"'replaces double quotes with singles. I do this constantly.ds{char}- delete.ds(removes the surrounding parens.
todo-comments.nvim (because we leave TODOs everywhere)
{
"folke/todo-comments.nvim",
event = "VimEnter",
dependencies = { "nvim-lua/plenary.nvim" },
opts = { signs = false },
}
You scatter TODO and FIXME throughout your code? This plugin highlights them in distinct colors. TODO in blue, FIXME in red, BUG in orange. You see them and remember to fix them.
Formatting with Conform.nvim (never do this manually)
Automatic formatting is handled by Conform.nvim:
{
"stevearc/conform.nvim",
event = "BufWritePre",
config = function()
require("conform").setup({
formatters_by_ft = {
javascript = { "prettier" },
typescript = { "prettier" },
typescriptreact = { "prettier" },
javascriptreact = { "prettier" },
css = { "prettier" },
html = { "prettier" },
json = { "prettier" },
yaml = { "prettier" },
markdown = { "prettier" },
lua = { "stylua" },
python = { "ruff_format" },
},
format_on_save = {
timeout_ms = 3000,
lsp_format = "fallback",
},
})
vim.keymap.set({ "n", "v" }, "<leader>cf", function()
require("conform").format({ async = true, lsp_format = "fallback" })
end, { desc = "Format file or selection" })
end,
}
Here's the strategy:
- Prettier for all web stuff - JS, TS, CSS, HTML, JSON, YAML, Markdown. I spent 2 hours debugging this the first time, but it's bulletproof now.
- Stylua for Lua (including my Neovim config). Because even your editor should be formatted correctly.
- ruff_format for Python (faster than black and looks like actual code).
- Format on save with a 3-second timeout. Takes too long? Falls back to LSP.
- LSP fallback - if nothing else works, ask the LSP to format.
<leader>cffor manual formatting (rare, but useful for debugging).
Mason installs the binaries automatically via mason-conform. I've never had to install them by hand.
Gitsigns: seeing Git in the margin (it's beautiful, trust me)
Gitsigns displays Git changes directly in the sign column, line by line. Little colored blocks showing what changed.
{
"lewis6991/gitsigns.nvim",
opts = {
on_attach = function(bufnr)
local gitsigns = require("gitsigns")
local map = function(mode, l, r, opts)
opts = opts or {}
opts.buffer = bufnr
vim.keymap.set(mode, l, r, opts)
end
-- Jump between changes
map("n", "]h", gitsigns.next_hunk, { desc = "Next hunk" })
map("n", "[h", gitsigns.prev_hunk, { desc = "Previous hunk" })
-- Actions on hunks
map("n", "<leader>hs", gitsigns.stage_hunk, { desc = "Stage hunk" })
map("n", "<leader>hr", gitsigns.reset_hunk, { desc = "Reset hunk" })
map("n", "<leader>hp", gitsigns.preview_hunk, { desc = "Preview hunk" })
map("n", "<leader>hb", gitsigns.blame_line, { desc = "Blame line" })
end,
},
}
The symbols in the gutter? They say "added", "changed", "deleted". The keybindings let you jump between hunks (]h / [h), stage/reset individual hunks, preview the diff before committing, or see who modified that line (blame). All without leaving Neovim.
The bottom line
That's the arsenal. Telescope for instant searching. Treesitter for deep code understanding. Conform that formats everything cleanly without being asked. Gitsigns showing you the history in the margin. Not flashy plugins - each does one thing and does it right. That's the Unix philosophy I love about Neovim.
Neovim from scratch series — This article is part of a complete series on configuring Neovim from scratch.
Previous: Native LSP in Neovim 0.11: zero plugins, zero compromises | Next: Terminal, Git and global search integrated in Neovim