Transform Your Neovim into an IDE: A Step-by-Step Guide
Updates:
- 2025-05-14. Neovim has been upgraded to v0.11, so I simplified the LSP configurations. :)
- 2025-03-22. Replace the
nvim-cmp
plugin withblink.cmp
. - 2024-04-04. Use lazy.nvim rather than packer.nvim 🤗
- If you have configured your Neovim following my post previously, you may check this commit to see how to migrate from
packer.nvim
tolazy.nvim
. Remember to run:checkhealth lazy
after the migration, as we also need to remove some outdated files ofpacker.nvim
.
- If you have configured your Neovim following my post previously, you may check this commit to see how to migrate from
Further reading
- How to set up Neovim for a new programming language and get more control over code formatting, please refer to the next post
Version info
I use a MacBook Air M3 edition with macOS 15.5. This is the detailed information of Nvim
on my laptop.
NVIM v0.11.1
Build type: Release
LuaJIT 2.1.1744318430
system vimrc file: "$VIM/sysinit.vim"
fall-back for $VIM: "/opt/homebrew/Cellar/neovim/0.11.1/share/nvim"
Run :checkhealth for more info
Why Neovim
After using Vim
for one year, I find myself having trouble in configuring ~/.vimrc
. The syntax of Vimscript is not to my liking, leading me to switch Neovim(Nvim)
. Rather than migrating my old ~/.vimrc
. I decided to start from scratch and take this opportunity to re-evaluate my previous Vim configuration. I aim to replace my plugins with the latest SOTA(State-of-the-art) alternatives. It’s been some time since I last edited my ~/.vimrc
In my opinion, it’s essential to understand the meaning behind each option and statement in the configuration file. That’s the approach I took in this post. My goal is to make the configuration files self-contained and easily understandable. To achieve this, I aim to provide clear explanations for each setting and include comments to enhance readability.
💡 Please note that I may have missed some options. However, as a reminder, you can always access the help docs in the
Nvim
by typing:h <name>
to get more information.
💡 This post assumes that you have a basic understanding of Vim
The basics
Lua
In my Nvim
configuration, I will use the Lua programming language as much as possible. Thus, it’s recommended that the reader familiarize themselves with Lua. Take a look at Learn Lua in Y minutes
Configuration files paths
The configuration directory for Nvim
is located at ~/.config/nvim
. On Linux/Mac, Nvim
will read ~/.config/nvim/init.lua
when it starts up. Theoretically, we can put everything inside this single file. It’s a bad practice, though. To keep things organized, I prefer to break it down into smaller, more manageable parts.
If you follow this post to configure your Nvim
, your ~/.config/nvim
should look like this⬇️
.
├── init.lua
└── lua
├── colorscheme.lua
├── keymaps.lua
├── lsp.lua
├── options.lua
└── plugins.lua
The explanations
init.lua
is the entry point. We will “import” other*.lua
files ininit.lua
colorscheme.lua
for the themekeymaps.lua
for key mappingslsp.lua
for the LSP supportoptions.lua
for some global optionsplugins.lua
for third-party plugins
lua
folder. When we userequire
to import a module in Lua, it searches this folder.- Replace the path separator
/
with.
, and remove the.lua
suffix - this is how you get the parameter for therequire
function.
- Replace the path separator
Options
We mainly use these: vim.g
, vim.opt
, and vim.cmd
. I made a cheatsheet below:
In Vim |
In Nvim |
Note |
---|---|---|
let g:foo = bar |
vim.g.foo = bar |
|
set foo = bar |
vim.opt.foo = bar |
set foo = vim.opt.foo = true |
some_vimscript |
vim.cmd(some_vimscript) |
key mappings
The syntax of key binding in Nvim
:
vim.keymap.set(<mode>, <key>, <action>, <opts>)
For a detailed explanation, please refer to :h vim.keymap.set
Configure Nvim from scratch
Now we can configure Nvim
step by step :)
Install Neovim
I am a Mac user, so I use Homebrew to install Nvim
1
$ brew install neovim
After completing the installation, if the ~/.config/nvim/
directory doesn’t exist, you should create the folder and init.lua
file
$ mkdir ~/.config/nvim
$ mkdir ~/.config/nvim/lua
$ touch ~/.config/nvim/init.lua
💡 Please note that after making any modifications to the
*.lua
files, you need to restart theNvim
to see the changes take effect. I will assume that you restart yourNvim
after each section.
Options configuration
The features:
- Use the system’s clipboard
- Use the mouse in
Nvim
- Tab and whitespace
- UI configuration
- Smart search
Create ~/.config/nvim/lua/options.lua
file and edit:
-- Hint: use `:h <option>` to figure out the meaning if needed
vim.opt.clipboard = 'unnamedplus' -- use system clipboard
vim.opt.completeopt = {'menu', 'menuone', 'noselect'}
vim.opt.mouse = 'a' -- allow the mouse to be used in Nvim
-- Tab
vim.opt.tabstop = 4 -- number of visual spaces per TAB
vim.opt.softtabstop = 4 -- number of spacesin tab when editing
vim.opt.shiftwidth = 4 -- insert 4 spaces on a tab
vim.opt.expandtab = true -- tabs are spaces, mainly because of python
-- UI config
vim.opt.number = true -- show absolute number
vim.opt.relativenumber = true -- add numbers to each line on the left side
vim.opt.cursorline = true -- highlight cursor line underneath the cursor horizontally
vim.opt.splitbelow = true -- open new vertical split bottom
vim.opt.splitright = true -- open new horizontal splits right
-- vim.opt.termguicolors = true -- enabl 24-bit RGB color in the TUI
vim.opt.showmode = false -- we are experienced, wo don't need the "-- INSERT --" mode hint
-- Searching
vim.opt.incsearch = true -- search as characters are entered
vim.opt.hlsearch = false -- do not highlight matches
vim.opt.ignorecase = true -- ignore case in searches by default
vim.opt.smartcase = true -- but make it case sensitive if an uppercase is entered
Then edit the init.lua
file, use require
to import the options.lua
file
require('options')
Key mappings configuration
The features:
- Use
<C-h/j/k/l>
to move the cursor among windows - Use
Ctrl
+ arrow keys to resize windows - In select mode, we can use
Tab
orShift-Tab
to change the indentation repeatedly
Create ~/.config/nvim/lua/keymaps.lua
and edit:
-- define common options
local opts = {
noremap = true, -- non-recursive
silent = true, -- do not show message
}
-----------------
-- Normal mode --
-----------------
-- Hint: see `:h vim.map.set()`
-- Better window navigation
vim.keymap.set('n', '<C-h>', '<C-w>h', opts)
vim.keymap.set('n', '<C-j>', '<C-w>j', opts)
vim.keymap.set('n', '<C-k>', '<C-w>k', opts)
vim.keymap.set('n', '<C-l>', '<C-w>l', opts)
-- Resize with arrows
-- delta: 2 lines
vim.keymap.set('n', '<C-Up>', ':resize -2<CR>', opts)
vim.keymap.set('n', '<C-Down>', ':resize +2<CR>', opts)
vim.keymap.set('n', '<C-Left>', ':vertical resize -2<CR>', opts)
vim.keymap.set('n', '<C-Right>', ':vertical resize +2<CR>', opts)
-----------------
-- Visual mode --
-----------------
-- Hint: start visual mode with the same area as the previous area and the same mode
vim.keymap.set('v', '<', '<gv', opts)
vim.keymap.set('v', '>', '>gv', opts)
Edit init.lua
and import keymaps.lua
... -- rest of the configuration
require('keymaps')
...
means that I omit other lines(in order to save the length of the post)Install package manager
A powerful Nvim
should be augmented with third-party plugins. I have selected lazy.nvim as my plugin manager, which has several amazing features, including:
- 🧪 Correct sequencing of dependencies
- 🔒 Lockfile
lazy-lock.json
to keep track of installed plugins - …
Create ~/.config/nvim/lua/plugins.lua
and paste the following code. At the moment, I haven’t added any third-party packages. The template code will bootstrap lazy.nvim
for us.
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({})
💡 The syntax of adding a third-party plugin in
lazy.nvim
is adding specification in the...
ofrequire("lazy").setup({})
.
Again, import plugins.lua
in init.lua
... -- rest of the configuration
require('plugins')
If you see a black window with no content when opening Nvim
, just wait for a moment as lazy.nvim
is in the process of installing itself☕️. After the Dashboard appears, you may type :Lazy
to check if it works correctly.
:q
to quit the floating window of lazy.nvim
.Colorscheme
My favorite theme - monokai.nvim. Add this plugin to plugins.lua
.
... -- rest of the configuration
require("lazy").setup({
"tanvirtin/monokai.nvim",
})
Save the changes and wait for lazy.nvim
to finish installing. Create ~/.config/nvim/lua/colorscheme.lua
and edit:
-- define your colorscheme here
local colorscheme = 'monokai_pro'
local is_ok, _ = pcall(vim.cmd, "colorscheme " .. colorscheme)
if not is_ok then
vim.notify('colorscheme ' .. colorscheme .. ' not found!')
return
end
The pcall
here refers to a protected call in Lua, which will return a boolean value to indicate its successful execution(a similar approach can be found in Go with the use of err
). By using pcall
instead of vim.cmd('colorscheme monokai_pro')
, we can avoid some annoying error messages in case the colorscheme is not installed2
Again, import colorscheme.lua
in init.lua
... -- rest of the configuration
require('colorscheme')
Auto-completion
blink.cmp is still in beta version, meaning that it may have breaking changes and many bugs.
In the previous edition of this post, I used nvim-cmp as the completion plugin. However, I found its configurations too complicated. This time, I am using blink.cmp plugin for a simple setup and better performance.
Let’s add this plugin to the plugins.lua
file.
... -- rest of the configuration
require("lazy").setup({
... -- rest of the configuration
{
"saghen/blink.cmp",
-- optional: provides snippets for the snippet source
dependencies = { "rafamadriz/friendly-snippets" },
-- Use a release tag to download pre-built binaries
version = "*",
-- AND/OR build from source, requires nightly: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust
-- build = 'cargo build --release',
-- If you use nix, you can build from source using the latest nightly rust with:
-- build = 'nix run .#build-plugin',
opts = {
-- 'default' (recommended) for mappings similar to built-in completions (C-y to accept)
-- 'super-tab' for mappings similar to VSCode (tab to accept)
-- 'enter' for enter to accept
-- 'none' for no mappings
--
-- All presets have the following mappings:
-- C-space: Open menu or open docs if already open
-- C-n/C-p or Up/Down: Select next/previous item
-- C-e: Hide menu
-- C-k: Toggle signature help (if signature.enabled = true)
--
-- See :h blink-cmp-config-keymap for defining your own keymap
keymap = {
-- Each keymap may be a list of commands and/or functions
preset = "enter",
-- Select completions
["<Up>"] = { "select_prev", "fallback" },
["<Down>"] = { "select_next", "fallback" },
["<Tab>"] = { "select_next", "fallback" },
["<S-Tab>"] = { "select_prev", "fallback" },
-- Scroll documentation
["<C-b>"] = { "scroll_documentation_up", "fallback" },
["<C-f>"] = { "scroll_documentation_down", "fallback" },
-- Show/hide signature
["<C-k>"] = { "show_signature", "hide_signature", "fallback" },
},
appearance = {
-- 'mono' (default) for 'Nerd Font Mono' or 'normal' for 'Nerd Font'
-- Adjusts spacing to ensure icons are aligned
nerd_font_variant = "mono",
},
sources = {
-- `lsp`, `buffer`, `snippets`, `path`, and `omni` are built-in
-- so you don't need to define them in `sources.providers`
default = { "lsp", "path", "snippets", "buffer" },
-- Sources are configured via the sources.providers table
},
-- (Default) Rust fuzzy matcher for typo resistance and significantly better performance
-- You may use a lua implementation instead by using `implementation = "lua"` or fallback to the lua implementation,
-- when the Rust fuzzy matcher is not available, by using `implementation = "prefer_rust"`
--
-- See the fuzzy documentation for more information
fuzzy = { implementation = "prefer_rust_with_warning" },
completion = {
-- The keyword should only match against the text before
keyword = { range = "prefix" },
menu = {
-- Use treesitter to highlight the label text for the given list of sources
draw = {
treesitter = { "lsp" },
},
},
-- Show completions after typing a trigger character, defined by the source
trigger = { show_on_trigger_character = true },
documentation = {
-- Show documentation automatically
auto_show = true,
},
},
-- Signature help when tying
signature = { enabled = true },
},
opts_extend = { "sources.default" },
}
})
By putting configurations in opts = { ... }
, we can change the default behavior of a plugin. The key configurations here are:
keymap
preset = "enter"
- Press<CR>
to confirm completion.select_prev, select_next
- cycle through the item. I set it to ⬆️/⬇️ or Tab/Shift-Tab.scroll_documentation_up, scroll_documentation_down
- Scroll the documentation up and down. I set it toCtrl-b, Ctrl-f
.
trigger = { show_on_trigger_character = true }
- Show completions after typing a trigger character.documentation = { auto_show = true }
- Show the documentation whenever an item is selected
🎙️ You can use basic completion now ~
LSP
After configuring blink.cmp
, Neovim can give basic completion suggestions. However, to turn Nvim
into an IDE, it is necessary to rely on LSP3 such that we can perform go to definition or other code actions. It is cumbersome to install and configure LSP one by one manually, as different LSPs have different installation steps, and it is inconvenient for future management. That’s where tools like mason.nvim and mason-lspconfig.nvim come in to make our lives easier. Let me try to explain what these two plugins do:
- mason.nvim: It’s an LSP manager that makes the management of LSP much easier.
- mason-lspconfig.nvim: It translate between mason.nvim server names and nvim-lspconfig server names. For example, the
lua_ls
innvim-lspconfig
is calledlua-language-server
inmason-lspconfig.nvim
. This plugin also automatically enables installed LSP.
Modify the plugins.lua
file:
... -- rest of the configuration
require("lazy").setup({
-- LSP manager
{ "mason-org/mason.nvim", opts = {} },
{
"mason-org/mason-lspconfig.nvim",
dependencies = {
"mason-org/mason.nvim",
"neovim/nvim-lspconfig",
},
opts = {
ensure_installed = { "pylsp" },
},
},
... -- rest of the configuration
})
The opts = {}
here means that the mason.nvim
will use default settings. I set pylsp
(Python’s LSP) in ensure_installed
when configuring mason-lspconfig.nvim
such that it can be automatically installed. Now we need to add the configurations for pylsp
. Let’s continue to modify the plugins.lua
file to add nvim-lspconfig
... -- rest of the configuration
require("lazy").setup({
{
"neovim/nvim-lspconfig",
config = function()
local lspconfig = require("lspconfig")
lspconfig.pylsp.setup({})
end,
},
... -- rest of the configuration
})
This time, I use config = function() ... end
to configure nvim-lspconfig
. The function here will be automatically executed. In the function body, the main job is calling lspconfig.pylsp.setup({})
to use the default settings of pylsp
.
If you want to change the default behaviors of a specific LSP, you may check the official documentation
After completing the above configuration, the LSP is ready to use. However, the default key mappings may not meet your needs. To this end, let’s create ~/.config/nvim/lua/lsp.lua
and add customized key mappings:
... -- rest of the configuration
-- Remove Global Default Key mapping
vim.keymap.del("n", "grn")
vim.keymap.del("n", "gra")
vim.keymap.del("n", "grr")
vim.keymap.del("n", "gri")
vim.keymap.del("n", "gO")
-- Create keymapping
-- LspAttach: After an LSP Client performs "initialize" and attaches to a buffer.
vim.api.nvim_create_autocmd("LspAttach", {
callback = function (args)
local keymap = vim.keymap
local lsp = vim.lsp
local bufopts = { noremap = true, silent = true }
keymap.set("n", "gr", lsp.buf.references, bufopts)
keymap.set("n", "gd", lsp.buf.definition, bufopts)
keymap.set("n", "<space>rn", lsp.buf.rename, bufopts)
keymap.set("n", "K", lsp.buf.hover, bufopts)
keymap.set("n", "<space>f", function()
require("conform").format({ async = true, lsp_fallback = true })
end, bufopts)
end
})
The configurations here first use vim.keymap.del
to remove default key mappings, then add custom key mappings with vim.keymap.set
. For example, pressing gd
in normal mode will jump to the definition of the symbol where the current cursor points. If you want to add more custom key mappings, add more lines here.
Finally, remember to add the following line in init.lua
... -- rest of the configuration
require('lsp')
Once you’ve restarted Nvim
, take a look at the status bar below. You should notice that Mason is busy installing the specified LSPs (Keep in mind that Nvim should not be closed during this process). To track the installation progress, enter :Mason
in Nvim
. While you wait, you can type g?
to get additional information about the Mason plugin.
Now we have a lightweight IDE🎉🎉🎉
Wrap-up
With this configuration, we successfully turned Nvim
into a lightweight IDE, which supports code highlighting, code completion, syntax checking, and other functionalities. It is completely open source and free 🤗.
I realized that even after trying different code editors and IDEs, I always found myself searching for Vim support. So I chose to turn Nvim
into an IDE and host the configuration files on my martinlwx/dotfiles. In this way, I can easily clone my configuration files to any new machine and have a consistent programming experience across machines.
Polishing tools require effort and time. To understand the purpose of each option, I had to search for various materials. However, despite the challenges, I firmly believe that it’s worth it. Understanding your tools allows you to further extend and customize them. This article aims to present a simple configuration, but there are still many beautification and customization things that can be done, including many excellent third-party plug-ins that have not been mentioned yet. The exploration and discovery are left to the readers.