I’ve primarily used VS Code and Vim over the years. Both have served me well, but recently I decided to transition from Vim to Neovim to take advantage of modern features like built-in LSP, asynchronous plugins, and advanced autocompletion.
In this article, I’ll share my personal Neovim workflow in 2025, optimized for speed, navigation, and productivity. This setup is specifically tailored on my Mac Mini M4
Why I choose Neovim
Vim has been my go-to editor for years, but it started to feel limiting compared to the developer experience I had in VS Code. Neovim bridges that gap by keeping the speed and modal editing of Vim while adding modern capabilities.
Many of these features like LSP integration and async tooling are technically possible in Vim, but they’re harder to achieve and maintain. Vim lacks a native LSP client, so you need to rely on third-party plugins that often feel fragmented and require heavy manual configuration. Async support is also less mature, which can make tasks like running formatters or linters feel sluggish compared to Neovim.
Another pain point: the Vim version bundled with macOS lacks Python support. That rules out many advanced plugins and integrations. Neovim, on the other hand, ships with Python support and a standardized API, making it much easier to extend.
With Neovim, these features are built in and standardized, which makes setup simpler and the overall workflow smoother.
Installing Neovim from scratch
Getting started with Neovim is straightforward. On macOS, the easiest way is through Homebrew:
brew install neovim
Once installed, I keep my configuration under ~/.config/nvim/. My setup uses lazy.nvim as the plugin manager because it’s fast, modern, and handles lazy-loading out of the box. Bootstrapping it only takes a few lines of Lua:
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable",
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
From there, everything else, plugins, LSP servers, and keymaps can be layered on cleanly. The barrier to entry is much lower compared to Vim, where plugin management and language tooling often require piecing together multiple systems.
My Neovim core plugin setup
Once Neovim is installed, the real power comes from plugins. I use lazy.nvim as the plugin manager because it’s fast, lightweight, and supports lazy-loading. From there, I add only the essentials that improve navigation, code intelligence, and workflow efficiency.
Here’s the core set I rely on:
- File Navigation
telescope.nvimfor fuzzy file search and live grep.
- Syntax and Highlighting
- UI and Aesthetics
gruvbox.nvimas my preferred theme.
- Editing Utilities
vim-commentaryfor quick commenting.
- Git Integration
This setup keeps things lean while covering the core areas: navigation, readability, and productivity. I avoid adding too many plugins at once, which keeps the editor responsive and maintainable.
LSP and language support
To manage LSP servers, I use:
mason.nvimfor installing language servers.mason-lspconfig.nvimfor bridging Mason with Neovim’s LSP client.nvim-lspconfigfor configuring individual servers.
Here are the main languages I work with:
Python → I use
pylspwithrufffor linting and formatting. It’s lightweight and avoids the Node.js dependency required by Pyright.Go →
goplsis reliable for code navigation and formatting.Elixir →
elixirlsprovides rich insights like diagnostics and code actions.Lua →
lua_lsfor configuring Neovim itself.
What makes Neovim stand out here is the consistency; all servers use the same LSP client, so I don’t need to configure separate systems for each language. Compared to Vim, where this requires patchwork plugins, Neovim’s approach is much cleaner.
My keymaps and navigation philosophy
My approach to keymaps is to keep them simple, memorable, and consistent across plugins. I’ve set , as my leader key, which makes sequences short and fast to type without interfering with core Vim motions.
Here’s how I break things down:
File navigation
For file navigation, I rely on a small set of intuitive keymaps. Pressing ,f opens file search through Telescope, while ,s lets me run a live grep across the project. I use ,b to quickly switch between open buffers and ,h to search help tags when I need documentation. For directory browsing, ,e toggles the NvimTree file explorer, and ,E focuses it if it’s already open.
Window management
I manage splits and window layouts with a few consistent keymaps. ,v creates a vertical split, ,H a horizontal split, and ,= balances all open windows. To close a split, I simply press ,x. I can also resize windows on the fly using <C-Up> and <C-Down> to adjust height, and <C-Left> and <C-Right> to adjust width.
LSP navigation
For LSP-powered navigation, I combine familiar Vim defaults with leader-based shortcuts. gd and gD jump to a symbol’s definition or declaration, while gi navigates to implementations and gr lists all references. Hovering over a symbol with K shows documentation instantly. I use ,r for renaming, ,a for code actions, and [d / ]d to jump between diagnostics. Finally, ,F formats code using the LSP.
Autocompletion and snippets
My Neovim setup leverages nvim-cmp for intelligent autocompletion, powered by both LSP sources and the current buffer. Pressing <C-Space> triggers completion manually, while <CR> confirms the selected item. I navigate suggestions quickly with <Tab> and <S-Tab>. For snippet support, I use LuaSnip, which integrates seamlessly with LSP-based completions. Snippets can be expanded, and placeholders jumped through efficiently, keeping code writing fluid and reducing repetitive typing. This combination of autocompletion and snippets ensures fast, context-aware coding without leaving the keyboard’s home row.
Formatting and Linting
In my workflow, consistent code style and automated linting are essential to maintain readability and catch errors early. Neovim’s built-in LSP integration, combined with null-ls, allows me to centralize formatting and linting across multiple languages without relying on editor-specific tools.
For Python, I use pylsp with Ruff as both a linter and a formatter. This setup ensures that code style and static analysis run automatically whenever I save a file, without needing external commands. For other languages like Lua, Go, Elixir, or JavaScript, the respective LSP servers handle formatting, while null-ls can supplement any additional tooling such as custom linters or formatters.
I’ve mapped formatting to a single shortcut (,F), making it effortless to format code on demand.
UI Themes and Statusline
A visually coherent environment helps maintain focus during long coding sessions. I use gruvbox.nvim as my primary colorscheme because it provides a balanced contrast and works well in both light and dark environments. Neovim’s termguicolors option ensures that the full range of 24-bit colors is used, giving a crisp, modern look to syntax highlighting, file trees, and Telescope results.
For the statusline, I rely on lualine.nvim configured with the same gruvbox theme. It displays essential information like the current mode, file name, file type, git branch, and diagnostics from LSP—all in a clean, unobtrusive manner. This setup allows me to quickly glance at context and status without distraction, keeping the interface minimal yet informative.
Wasi's Log