Build a Mouseless Neovim IDE
Configure Neovim from scratch with LSP, fuzzy finding, and autocompletion.
Introduction
30 Second Summary
Every time you reach for your mouse to click a file tab, scroll to a function, or switch between open files, you break your flow. Those tiny interruptions add up to hours of lost focus every week.
In this project, you will build a complete Neovim development environment from scratch by writing every line of configuration yourself in Lua. You will add fuzzy file finding, language-server-powered code intelligence, and autocompletion, all driven entirely by the keyboard.
What You'll Build
You open your terminal, type nvim, and jump to any file by name, hop to function definitions, and accept autocompletion suggestions, all without your hands ever leaving the keyboard.
By the end of this project, you'll have:
- The ability to instantly open any file by typing a few characters into Telescope, a keyboard-driven fuzzy finder that replaces clicking through file trees.
- Working Go to Definition with gd, Find References with gr, and autocompletion as you type, powered by the same Language Server Protocol that VS Code uses.
- Keyboard-only code navigation using Vim motions and rich, structural syntax highlighting from Treesitter that understands functions, types, and keywords instead of relying on basic pattern matching.
- Secret Mission: Add a keybinding discovery popup and a polished status bar to make your setup truly pro-level.
Do I need any prerequisites?
This project runs on Linux and assumes you are comfortable with basic terminal commands like cd and ls. You will also need Go and Python installed on your machine, since the project sets up language servers for both languages.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to write every line of config yourself instead of installing a pre-built distribution.
Install Neovim and Create Your Base Config
You committed to building your Neovim IDE from scratch. Before you can configure anything, you need Neovim itself installed on your machine and a config file where all your customizations will live.
In this step, you will install Neovim 0.12.4 and ripgrep, then create a base init.lua config file with sane defaults that turn vanilla Neovim from hostile to comfortable.
In this step, get ready to:
- Install Neovim and ripgrep on your Linux machine.
- Create the Neovim config directory and base init.lua file with leader key, line numbers, and editor settings.
- Open Neovim and verify your settings are active.
Install Neovim and ripgrep
Neovim provides an AppImage for Linux, which is a self-contained binary you can download and run without a package manager. You will download it, make it available system-wide, and then install ripgrep (a fast search tool that Telescope needs later for searching across files).
- Download the Neovim 0.12.4 AppImage by running this command:
curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage
- Make the AppImage executable by running:
chmod u+x nvim-linux-x86_64.appimage
- Move the AppImage to a permanent location so you can run nvim from anywhere:
sudo mkdir -p /opt/nvim
sudo mv nvim-linux-x86_64.appimage /opt/nvim/nvim
- Add Neovim to your PATH by appending this line to your shell config file:
echo 'export PATH="$PATH:/opt/nvim/"' >> ~/.bashrc
source ~/.bashrc
- Verify Neovim is installed by running:
nvim --version
You should see NVIM v0.12.4 at the top of the output.
Seeing command not found?
Make sure you ran source ~/.bashrc to reload your shell config. If you use zsh instead of bash, replace ~/.bashrc with ~/.zshrc in both commands above.
If you get a FUSE-related error when running the AppImage, extract it instead:
cd /opt/nvim
sudo ./nvim --appimage-extract
sudo ln -s /opt/nvim/squashfs-root/AppRun /usr/local/bin/nvim
Help me troubleshoot my Neovim installation on Linux.
- Install ripgrep by running:
sudo apt install ripgrep
- Verify ripgrep is installed by running:
rg --version
You should see a version number in the output, confirming ripgrep is ready.
Why do you need ripgrep?
Telescope (the fuzzy finder plugin you will add in Step 3) uses ripgrep under the hood for its live_grep feature. Without it, you would not be able to search for text across all files in your project.
Create your Neovim config directory and base settings
Neovim looks for its configuration at ~/.config/nvim/init.lua. This single file is where your entire editor setup lives. Right now it does not exist, so you need to create it.
- Create the Neovim config directory by running:
mkdir -p ~/.config/nvim
- Create and open the config file by running:
nvim ~/.config/nvim/init.lua
Neovim opens with an empty file. You are now inside the editor in Normal mode.
What is Normal mode?
Neovim uses modal editing. In Normal mode, every key is a command (not a letter you type). To start typing text, you press i to enter Insert mode. To go back to Normal mode, press Esc.
- Press i to enter Insert mode. You should see -- INSERT -- at the bottom of the screen.
- Type (or paste) the leader key configuration:
-- Set leader key to Space (must be before lazy.nvim setup)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
What is the leader key?
The leader key is a custom prefix for your own keyboard shortcuts. By setting it to Space, you can create shortcuts like Space f f to find files. It must be set before loading any plugins so they can register keybindings against it.
- Below the leader key lines, add the base editor settings:
-- Base settings
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.mouse = ""
vim.opt.showmode = false
vim.opt.clipboard = "unnamedplus"
vim.opt.breakindent = true
vim.opt.undofile = true
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.signcolumn = "yes"
vim.opt.updatetime = 250
vim.opt.timeoutlen = 300
vim.opt.splitright = true
vim.opt.splitbelow = true
vim.opt.cursorline = true
vim.opt.scrolloff = 10
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.termguicolors = true
What do these settings do?
- number and relativenumber show line numbers on the left. Relative numbers show how many lines away each line is from your cursor, which makes jumping with 5j or 12k instant.
- mouse = "" disables the mouse entirely. This forces you to use keyboard-only navigation, which is the whole point of this project.
- clipboard = "unnamedplus" syncs Neovim's clipboard with your system clipboard so you can copy and paste between Neovim and other apps.
- ignorecase and smartcase together make search case-insensitive unless you type a capital letter, which then makes it case-sensitive.
- cursorline highlights the line your cursor is on, making it easy to find your position in the file.
- Press Esc to return to Normal mode.
- Save the file and quit by typing :wq and pressing Enter.
Saving and quitting in Neovim
The : key enters Command mode. w means write (save) and q means quit. You will use :wq constantly throughout this project.
✔️ Awesome, I've got everything!
Great. Make sure you have saved init.lua with :wq.
ⓧ I'd like to double check the full code
Here is the complete ~/.config/nvim/init.lua file at this point. Compare it with yours:
-- Set leader key to Space (must be before lazy.nvim setup)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
-- Base settings
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.mouse = ""
vim.opt.showmode = false
vim.opt.clipboard = "unnamedplus"
vim.opt.breakindent = true
vim.opt.undofile = true
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.signcolumn = "yes"
vim.opt.updatetime = 250
vim.opt.timeoutlen = 300
vim.opt.splitright = true
vim.opt.splitbelow = true
vim.opt.cursorline = true
vim.opt.scrolloff = 10
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.termguicolors = true
Verify your base settings are active
Before you move on, you need to confirm that Neovim is actually reading your config and applying the settings you just wrote.
- What do you expect to see when you open Neovim now? Think about which settings should produce a visible change.
- Open Neovim by running:
nvim
You should see line numbers on the left side of the screen. The current line shows an absolute number, and the lines above and below show relative distances (1, 2, 3...). The line your cursor is on is highlighted.
- Quit Neovim by typing :q and pressing Enter.
Not seeing line numbers or highlighting?
Make sure your file is saved at the exact path ~/.config/nvim/init.lua. You can verify by running cat ~/.config/nvim/init.lua in your terminal and checking that the content matches. If the file is empty or missing, re-create it.
Also check for typos in the Lua code. A single misplaced character can prevent the entire config from loading.
Help me troubleshoot my init.lua settings.
Your editor has line numbers, smart search, system clipboard, and a highlighted cursor line. Next up, you will open real code and learn the Vim motions that let you navigate without ever touching the mouse.
Navigate Real Code with Vim Motions
Your Neovim environment is installed and configured with sane defaults. Line numbers, relative numbers, and clipboard integration are all ready.
Before adding any plugins, you need to build muscle memory for keyboard-only code navigation. In this step, you will open a real Go file and practice the core Vim motions that make mouseless editing possible.
In this step, get ready to:
- Practice core navigation motions on a real Go file.
- Edit code using Vim commands without touching a mouse.
- Discover the limits of vanilla Neovim.
Create a sample Go project and open it in Neovim
You need a real code file with multiple functions to practice navigating between. A small Go program works well because it has clear function definitions and calls.
- Create a sample Go project with a practice file by running these commands:
mkdir -p ~/go-sample
cat << 'EOF' > ~/go-sample/main.go
package main
import "fmt"
func greet(name string) string {
return "Hello, " + name + "!"
}
func add(a int, b int) int {
return a + b
}
func main() {
message := greet("NextWork")
fmt.Println(message)
result := add(3, 5)
fmt.Println("Sum:", result)
}
EOF
What's in this Go file?
This file has three functions: greet returns a greeting string, add returns the sum of two numbers, and main calls both of them.
Having multiple functions gives you targets to navigate between, search for, and eventually jump to with code intelligence.
- Verify the file was created by running:
ls ~/go-sample/
You should see main.go in the output.
- Open the file in Neovim by running:
nvim ~/go-sample/main.go
The file opens with line numbers on the left side and the cursor line highlighted. Your cursor starts at the top of the file in normal mode.
What is modal editing?
Neovim uses modal editing. You start in normal mode, where every key is a command instead of a character you type. You press i to switch to insert mode for typing text.
Press Esc to return to normal mode at any time. This is the foundation of mouseless editing.
Practice these navigation motions in normal mode:
- Press h, j, k, l to move the cursor left, down, up, and right.
- Press w to jump forward by one word. Press b to jump backward. Press e to jump to the end of the current word.
- Press gg to jump to the first line of the file.
- Press G to jump to the last line of the file.
- Press 0 to jump to the start of the current line.
- Press $ to jump to the end of the current line.
Your cursor moves through the file without any mouse interaction. The relative line numbers on the left show how many lines away each line is from your cursor.
Cursor not moving?
Make sure you are in normal mode. If you see -- INSERT -- at the bottom of the screen, press Esc first. Navigation keys only work as commands in normal mode.
help me debug cursor movement in Neovim
Edit code and search for functions
Navigation is only half the story. Vim motions also let you delete, copy, replace, and search through code without leaving the keyboard.
Try these editing commands while still in the file:
- Press i to enter insert mode. Your cursor changes shape.
- Type a few characters at the cursor position.
- Press Esc to return to normal mode.
- Move to any line with j or k and press dd to delete it.
- Press u to undo. The deleted line reappears.
- Press yy to copy the current line.
- Move down with j and press p to paste the copied line below.
What does ciw mean?
ciw stands for "change inner word." It deletes the word under your cursor and drops you into insert mode so you can type a replacement. Vim has many text objects like this.
diw deletes a word without entering insert mode. ci" changes everything inside double quotes. These compound motions are what make Vim editing fast.
- Move your cursor onto any word in the file.
- Press ciw to delete that word and enter insert mode.
- Type a replacement word and press Esc to return to normal mode.
Now try searching the file:
- Type /func and press Enter to search for the word "func" in the file.
- Press n to jump to the next match. Each func keyword in the file highlights as you cycle through.
You just navigated, edited, copied, and searched through a Go file using only your keyboard.
- Press u repeatedly until all your practice edits are undone.
- Type :wq and press Enter to save the file and quit Neovim.
Stuck in a weird mode?
If your keypresses are typing characters instead of running commands, press Esc to return to normal mode. If you see a : at the bottom of the screen, press Esc to cancel the command and go back to normal mode.
help me get unstuck in Neovim
Try jumping to a function definition
- Reopen the file in Neovim by running:
nvim ~/go-sample/main.go
You can navigate, edit, and search through code entirely by keyboard. But there is one thing you probably do all the time in a graphical editor like VS Code: clicking on a function call to jump straight to where that function is defined.
Scroll down to the main function using G. Put your cursor on the word greet in the line message := greet("NextWork"). What do you think will happen when you press gd?
- Press gd to try jumping to the definition of greet.
Without a language server, gd falls back to a basic pattern search. It might jump somewhere, but it does not actually understand your code. It cannot reliably find the real definition, especially across files in a real project.
- Now try finding all the places greet is used. Press grr to try listing references.
Nothing useful happens. Without a language server attached, the references command has nothing to query.
- Press o to open a new line below and enter insert mode.
- Type fmt. and wait a moment for autocomplete suggestions to appear.
No suggestions appear. No dropdown. No code intelligence at all.
This is the wall
Vanilla Neovim has no understanding of your code's structure. It cannot reliably find where greet is defined. It cannot list all the places greet is used. It cannot offer completions for fmt..
Everything you just tried requires plugins that connect Neovim to a language server. That is exactly what you will add in the next steps.
- Press Esc to return to normal mode.
- Type :q! and press Enter to quit without saving the test edits.
You have the muscle memory for Vim motions, and you have felt the gap where code intelligence should be. Next up, you will add a plugin manager and fuzzy file finding to start closing that gap.
Add a Plugin Manager and Fuzzy Finding
In the last step, you navigated and edited Go code purely by keyboard. You also hit a wall: no way to jump to definitions, no autocompletion, and no way to find files across a project.
Solving those problems starts with a plugin manager. In this step, you will bootstrap lazy.nvim to manage Neovim plugins and install Telescope, a fuzzy finder that gives you instant file searching and project-wide grep from the keyboard.
In this step, get ready to:
- Bootstrap lazy.nvim as your Neovim plugin manager.
- Install Telescope for keyboard-driven file finding and project-wide search.
- Test fuzzy finding with Space ff and live grep with Space fg.
Bootstrap lazy.nvim in your config
lazy.nvim is a plugin manager for Neovim. It clones plugins from GitHub, loads them on demand, and keeps them updated.
The bootstrap code you are about to add checks whether lazy.nvim is already cloned on your machine. If it is missing, it downloads it automatically the next time you open Neovim.
- Open your Neovim config file by running:
nvim ~/.config/nvim/init.lua
- Press G to jump to the last line of the file.
- Press o to open a new line below and enter insert mode.
- Paste the following bootstrap code:
-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
What does this code do?
The bootstrap checks whether lazy.nvim already exists in Neovim's data directory. If it does not, it runs git clone to download lazy.nvim from GitHub.
The last line, vim.opt.rtp:prepend(lazypath), adds lazy.nvim to Neovim's runtime path so it can be loaded with require("lazy").
Why lazy.nvim instead of Neovim's built-in packages?
Neovim ships with its own package system, but lazy.nvim is the dominant plugin manager in the ecosystem. Most plugin READMEs show lazy.nvim installation examples first.
Learning lazy.nvim gives you the most transferable setup knowledge. Any plugin you find online will have a lazy.nvim spec ready to copy.
- Press Esc to return to normal mode.
- Save init.lua by typing :w and pressing Enter. The bootstrap activates the next time Neovim starts.
Add Telescope as your first plugin
Telescope is a fuzzy finder for Neovim. It lets you search for files by name, grep across your entire project, and switch between open buffers. Think of it as VS Code's Ctrl+P, but entirely keyboard-driven.
- Your cursor should still be in init.lua. Press G to jump to the last line of the file.
- Press o to open a new line below and enter insert mode.
- Paste the following plugin setup code:
-- Plugin setup
require("lazy").setup({
-- Telescope: fuzzy finding
{
"nvim-telescope/telescope.nvim",
version = "*",
dependencies = { "nvim-lua/plenary.nvim" },
config = function()
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 = "Find buffers" })
vim.keymap.set("n", "<leader>fh", builtin.help_tags, { desc = "Help tags" })
end,
},
}, {
install = { colorscheme = { "habamax" } },
checker = { enabled = true },
})
What does this plugin spec do?
- "nvim-telescope/telescope.nvim" tells lazy.nvim which GitHub repo to clone.
- dependencies installs plenary.nvim, a utility library that Telescope requires.
- The config function runs after Telescope loads. It creates four keybindings that all start with your leader key (Space).
- <leader>ff opens the file finder. <leader>fg searches text across all files using ripgrep. <leader>fb lists open buffers. <leader>fh searches Neovim help pages.
- Press Esc to return to normal mode.
- Save and quit by typing :wq and pressing Enter.
Test fuzzy finding across your project
- Navigate to your sample Go project by running:
cd ~/go-sample
- Open the directory in Neovim by running:
nvim .
When Neovim opens, lazy.nvim detects that Telescope and plenary.nvim are missing. It clones them from GitHub automatically and opens a floating window showing the install progress. Press q to close the panel once installation finishes.
Before you test your new keybinding, predict what will happen when you press Space then f then f. Will a popup appear? Will something show in the command line?
- In normal mode, press Space followed by f then f.
A Telescope floating window appears showing all files in your project. Type part of a filename and watch the results filter in real time. Press Enter to open the highlighted file. Press Esc to close the finder.
- Press Space followed by f then g to open live grep.
- Type a function name from your Go file and watch results appear across all files. Navigate results with j and k, then press Enter to jump to that line.
- Press Space followed by f then b to see your open buffers. Select one with Enter to switch to it.
Telescope not opening?
- If pressing Space ff does nothing, make sure you saved init.lua with the updated plugin code and reopened Neovim. Lazy.nvim only installs plugins on launch.
- If live grep (Space fg) shows no results, make sure ripgrep is installed. Run rg --version in your terminal to check.
- If you see a Lua error on startup, check that your bootstrap code and plugin setup are pasted correctly with no missing commas or brackets.
- Telescope is not working when I press Space ff in Neovim. How do I debug this?
✔️ Awesome, I've got everything!
Nice work. Double check you have saved init.lua and that Telescope opens when you press Space ff.
ⓧ I'd like to double check the full code
Your init.lua should now have three sections in this order. Scroll through your file and confirm each one is present.
- Leader key and base settings at the top: vim.g.mapleader, vim.g.maplocalleader, then all the vim.opt lines (number, relativenumber, mouse, through termguicolors).
- Bootstrap lazy.nvim in the middle: starts with local lazypath = vim.fn.stdpath("data") and ends with vim.opt.rtp:prepend(lazypath).
- Plugin setup at the bottom: require("lazy").setup({ containing the Telescope spec with four vim.keymap.set calls (ff, fg, fb, fh), followed by install and checker options.
Your plugin manager is running and you have instant file finding across any project. Next up, you will add Treesitter to give your code real, structure-aware syntax highlighting.
Add Treesitter for Real Syntax Highlighting
You now have Telescope giving you instant file finding and project-wide search from the keyboard.
But take a look at your Go file right now. Neovim uses basic regex patterns to color your code, which means it cannot tell the difference between a function name and a variable name. Treesitter solves this by parsing your code into an Abstract Syntax Tree, giving each element a precise structural role that Neovim highlights accurately.
In this step, get ready to:
- Add nvim-treesitter to your plugins and install parsers for Go, Python, and Lua.
- Verify that syntax highlighting is richer and inspect the live syntax tree.
Add the Treesitter plugin to your config
Treesitter parses your code into a tree of named nodes (function declarations, type names, string literals) instead of matching lines with regular expressions. This means every element gets highlighted based on what it actually is in your language's grammar.
- Open init.lua in Neovim by running this command:
nvim ~/.config/nvim/init.lua
- Scroll down to the require("lazy").setup({ block.
- Find the closing }, of the Telescope plugin entry (the line right after end,).
- Press o to open a new line below and enter insert mode.
- Add the following code below that closing }, and before the }, { options line:
-- Treesitter: syntax highlighting
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
require("nvim-treesitter").setup({
install_dir = vim.fn.stdpath("data") .. "/site",
})
require("nvim-treesitter").install({ "go", "python", "lua" }):wait(60000)
end,
},
What does this code do?
- build = ":TSUpdate" runs the :TSUpdate command whenever the plugin updates, keeping your parsers in sync with the latest grammar definitions.
- require("nvim-treesitter").setup() initializes Treesitter with install_dir pointing to Neovim's data directory so parsers are stored in a known location.
- require("nvim-treesitter").install({ "go", "python", "lua" }) downloads and compiles parsers for these three languages.
- :wait(60000) makes the install synchronous so the parsers are fully ready before you start editing.
- Press Esc to return to normal mode.
- Save the file by typing :w and pressing Enter.
✔️ Awesome, I've got everything!
Great. Double check you have saved init.lua before moving on.
ⓧ I'd like to double check the full code
Scroll through your init.lua and confirm these sections appear in this order:
- Leader key and base settings at the top (vim.g.mapleader through vim.opt.termguicolors).
- The lazy.nvim bootstrap block in the middle (local lazypath through vim.opt.rtp:prepend(lazypath)).
- The require("lazy").setup({ block at the bottom, containing the Telescope plugin entry first and the new Treesitter plugin entry second.
Make sure the Treesitter entry sits inside the plugins table (between the opening { after setup( and the }, { options line), not after it.
See the difference in your Go file
For lazy.nvim to install the new plugin and for Treesitter to compile the parsers, you need to close and reopen Neovim.
- Quit Neovim by typing :q and pressing Enter.
- Reopen Neovim with your Go file by running this command:
nvim ~/go-sample/main.go
What happens when Neovim opens?
On this first launch with the new config, lazy.nvim detects the Treesitter plugin and clones it from GitHub. Then Treesitter compiles the Go, Python, and Lua parsers. You may see progress messages at the bottom of the screen for a few seconds while this happens.
Before you look at the file, think about what you expect. Will function names look different from variable names now? Will keywords like func and return have their own color?
Look at your main.go file. Function names, type declarations, string literals, and keywords now each have distinct, consistent colors. Every structural element of your code is highlighted based on what it actually is in Go's grammar.
- See the syntax tree that powers this highlighting by typing :InspectTree and pressing Enter.
A split window opens showing a text representation of every node in the syntax tree. As you move your cursor through the tree, the corresponding code in your source file highlights.
What is InspectTree showing me?
This is the Abstract Syntax Tree that Treesitter built from your Go code. Each indented line is a node: function_declaration, identifier, string_literal, and so on.
This is why Treesitter highlighting is so much more accurate than regex. It knows that main after func is a function name, not just any word. Regex cannot make that distinction.
- Close the InspectTree window by typing :q and pressing Enter. This closes the tree pane and returns you to your Go file.
Code still looks the same as before?
- Make sure the Treesitter plugin entry is inside the plugins table in require("lazy").setup({, not after the closing }).
- Check that you saved init.lua with :w before quitting. Unsaved changes are lost when you close Neovim.
- Parser compilation requires a C compiler. If you see compilation errors, install build tools by running sudo apt install build-essential in your terminal, then reopen Neovim.
- Still stuck? Why is my Treesitter syntax highlighting not working after adding the plugin to init.lua?
Your code is now highlighted based on its actual structure, not guesswork from regex. Next up, you will wire up language servers so that gd finally jumps to definitions and autocompletion appears as you type.
Add LSP and Autocompletion
Your code looks beautiful with Treesitter highlighting, but Neovim still cannot understand what your code means.
This is the big payoff. You are going to wire up the same language servers that VS Code uses so that gd jumps to any function definition, gr finds all references, and autocompletion pops up as you type.
In this step, get ready to:
- Install Mason and nvim-lspconfig to manage language servers for Go and Python.
- Configure nvim-cmp for intelligent autocompletion with LSP, buffer, and path sources.
- Wire up LSP keybindings so you can jump to definitions, find references, and rename symbols.
Add Mason and nvim-lspconfig for language server management
Language servers are the programs that power Go to Definition, Find References, and hover documentation. Mason installs these servers for you inside Neovim's data directory, so you never need to run go install or npm manually.
- Open ~/.config/nvim/init.lua in Neovim by running:
nvim ~/.config/nvim/init.lua
- Scroll down to the plugins table inside require("lazy").setup({.
- Find the closing of the Treesitter plugin block. Place your cursor on the line after the Treesitter block's closing }, and add the Mason and nvim-lspconfig plugin specs by copying the following code:
-- Mason: language server installer
{
"mason-org/mason.nvim",
opts = {},
},
-- nvim-lspconfig: LSP server configurations
{
"neovim/nvim-lspconfig",
dependencies = { "mason-org/mason.nvim" },
},
What do these plugins do?
Mason downloads and manages language server binaries (like gopls for Go and pyright for Python) so you do not need to install them yourself.
nvim-lspconfig provides the configuration files that tell Neovim how to communicate with each language server. It is a data-only plugin. The actual LSP client is built into Neovim itself.
The plugin specs tell lazy.nvim what to install, but you also need to tell Neovim which language servers to activate.
- Scroll down past the end of the require("lazy").setup({ ... }) block. Find the very last line of your file (after the closing ) of the setup call).
- Add the LSP enable call below it by copying the following code:
-- LSP setup
vim.lsp.enable({ "gopls", "pyright" })
What does vim.lsp.enable do?
This tells Neovim to automatically start gopls when you open a Go file and pyright when you open a Python file. Neovim reads the server configs that nvim-lspconfig provides and connects to the right server for each file type.
- Save the file and quit Neovim by pressing Esc, then typing :wq and pressing Enter.
- Reopen Neovim by running:
nvim
You should see lazy.nvim download Mason and nvim-lspconfig. Once the install finishes, you need to tell Mason to download the actual language server binaries.
- Install the Go and Python language servers by running this Neovim command:
:MasonInstall gopls pyright
You should see Mason download and install both servers. Each one shows a progress indicator and a success message when it finishes.
Mason install failing?
- Make sure you have Go installed. Mason needs go on your PATH to build gopls.
- Make sure you have an internet connection. Mason downloads servers from GitHub.
- Check that the Mason and nvim-lspconfig plugin specs are inside the require("lazy").setup({ block, not outside it.
Why can't Mason find gopls or pyright?
Add nvim-cmp for autocompletion
Your language servers are running, but Neovim does not show a completion menu as you type. nvim-cmp provides a popup completion menu that pulls suggestions from multiple sources: the language server, words in your current file, and file paths.
- Quit Neovim with :q, then reopen your config file by running:
nvim ~/.config/nvim/init.lua
- Inside the plugins table, find the nvim-lspconfig block you just added. Place your cursor after its closing }, and add the nvim-cmp plugin spec with its keybinding mappings by copying the following code:
-- nvim-cmp: autocompletion
{
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
},
config = function()
local cmp = require("cmp")
cmp.setup({
snippet = {
expand = function(args)
vim.snippet.expand(args.body)
end,
},
mapping = cmp.mapping.preset.insert({
["<C-n>"] = cmp.mapping.select_next_item(),
["<C-p>"] = cmp.mapping.select_prev_item(),
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.abort(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
}),
What do these mappings do?
- Ctrl+n and Ctrl+p move down and up through the completion list.
- Ctrl+Space forces the completion menu open if it has not appeared automatically.
- Enter confirms the selected suggestion. Ctrl+e dismisses the menu without choosing anything.
- Directly below the mapping section you just pasted (right after the closing }),), add the completion sources and close the plugin spec by copying this code:
sources = cmp.config.sources({
{ name = "nvim_lsp" },
}, {
{ name = "buffer" },
{ name = "path" },
}),
})
end,
},
What are completion sources?
Sources tell nvim-cmp where to pull suggestions from. The nvim_lsp source gets intelligent suggestions from the language server (function signatures, type info). The buffer source suggests words from your current file. The path source completes file paths.
LSP suggestions have higher priority because they appear in the first group. Buffer and path suggestions only show when the LSP has nothing to offer.
- Save and quit with Esc, then :wq.
- Open your sample Go file to test autocompletion by running:
nvim ~/go-sample/main.go
- Press o to open a new line below the cursor and enter insert mode.
- Start typing fmt. and watch for a completion popup to appear with function suggestions like Println and Printf.
- Press Esc to return to normal mode. Press u to undo the line you just typed.
No completion popup appearing?
- Make sure gopls is installed. Run :Mason inside Neovim to check that gopls shows as installed.
- The Go file must be inside a Go module. If your ~/go-sample directory does not have a go.mod file, gopls will not start. Run go mod init go-sample in that directory first.
- Try pressing Ctrl+Space to manually trigger the completion menu.
Help me debug my nvim-cmp autocompletion setup
Add LSP keybindings for code navigation
Autocompletion is working, but you still cannot jump to a function's definition or find all places where it is used. You need keybindings that connect to the language server's navigation features.
- Open your config file by running:
nvim ~/.config/nvim/init.lua
- Scroll to the bottom of the file. Find the vim.lsp.enable line you added earlier.
- Below it, add the LspAttach autocommand that wires up keybindings whenever a language server connects to a buffer. Copy the following code:
-- LSP keybindings (attach to every LSP-enabled buffer)
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(event)
local opts = { buffer = event.buf }
vim.keymap.set("n", "gd", vim.lsp.buf.definition, vim.tbl_extend("force", opts, { desc = "Go to definition" }))
vim.keymap.set("n", "gr", vim.lsp.buf.references, vim.tbl_extend("force", opts, { desc = "Find references" }))
vim.keymap.set("n", "K", vim.lsp.buf.hover, vim.tbl_extend("force", opts, { desc = "Hover documentation" }))
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, vim.tbl_extend("force", opts, { desc = "Rename symbol" }))
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, vim.tbl_extend("force", opts, { desc = "Code action" }))
end,
})
What does this autocommand do?
The LspAttach event fires every time a language server connects to a buffer. The callback sets keybindings that only work in that buffer, so LSP features are available exactly where they apply.
- gd jumps to where a function or variable is defined.
- gr shows every place a symbol is referenced across the project.
- K opens a floating window with documentation for the symbol under the cursor.
- Space rn renames a symbol everywhere it appears.
- Space ca opens a list of code actions the server suggests (like organizing imports).
- Save and quit with Esc, then :wq.
Now for the moment of truth. Before you test, think back to Step 2 when you tried pressing gd on a function call and nothing happened. What do you think will happen this time?
- Open your Go file to test the full setup by running:
nvim ~/go-sample/main.go
- Place your cursor on a function call (like a call to one of the functions in your file).
- Press gd. Your cursor jumps straight to that function's definition.
- Press gr on the same function name. A list of every reference to that function appears.
- Press K on a function name. A floating window with the function's documentation appears.
- Press o to open a new line and start typing a function name. Autocompletion suggestions appear as you type.
You now have everything VS Code gives you, without the mouse. Go to Definition, Find References, hover docs, and autocompletion are all wired up and running on the same language servers.
gd not jumping to the definition?
- Confirm the language server is attached by running :lua print(vim.inspect(vim.lsp.get_clients())) inside Neovim. You should see gopls listed.
- Make sure your cursor is on the function call, not on a keyword like func or package.
- Make sure your go-sample directory has a go.mod file. Without it, gopls cannot analyze your project.
Help me debug my LSP keybindings in Neovim
✔️ Awesome, I've got everything!
Double check you have saved init.lua. Your Neovim is now a real IDE with fuzzy finding, syntax highlighting, language server intelligence, and autocompletion.
ⓧ I'd like to double check the full code
Your init.lua is now quite long. Scroll through it and confirm you have each of these sections in order:
- Leader key settings (vim.g.mapleader and vim.g.maplocalleader) at the very top.
- Base settings block (vim.opt.number through vim.opt.termguicolors).
- lazy.nvim bootstrap block (cloning from GitHub).
- require("lazy").setup({ containing: Telescope, Treesitter, Mason, nvim-lspconfig, nvim-cmp (in that order).
- vim.lsp.enable({ "gopls", "pyright" }) after the require("lazy").setup() call.
- The LspAttach autocommand with keybindings for gd, gr, K, <leader>rn, and <leader>ca at the very end.
Your Neovim now has every core feature that VS Code provides: fuzzy finding, syntax highlighting, Go to Definition, Find References, hover docs, rename, code actions, and autocompletion. The Secret Mission takes it further with keybinding discovery and a polished status bar.
Secret mission
Add Which-Key and Lualine
Your Neovim IDE is fully functional, but how will you remember all those keybindings as a beginner? And the status bar at the bottom is barely showing anything useful. Add which-key for keybinding discovery and lualine for a polished status bar to complete your pro-level setup.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Neovim config directory (~/.config/nvim/).
- Neovim plugin and LSP data (~/.local/share/nvim/).
- Neovim state files (~/.local/state/nvim/).
- Sample Go project (~/go-sample/).
Keep everything running
No action needed. Choose this if you plan to keep using your Neovim setup as your daily editor.
- Your Neovim config lives at ~/.config/nvim/init.lua and persists across reboots.
- Plugins are cached in ~/.local/share/nvim/lazy/ and load automatically when you open Neovim.
- Language servers (gopls and pyright) installed by Mason stay in their Neovim data directory.
- Neovim and ripgrep remain installed on your system.
Pause - I'll come back to this later
Back up your config to a Git repository so you can version-control and share your setup across machines.
- Copy your config directory to a backup location by running:
cp -r ~/.config/nvim ~/nvim-config-backup
- You can initialize a Git repository inside ~/nvim-config-backup and push it to GitHub for safe keeping.
- Everything else (plugins, language servers, state files) stays in place and works when you return.
Delete - I don't want to use this again
Remove all Neovim configuration, plugins, and project files to reset to a clean slate.
- Remove the Neovim config directory, plugin data, and state files by running:
rm -rf ~/.config/nvim
rm -rf ~/.local/share/nvim
rm -rf ~/.local/state/nvim
- Remove the sample Go project by running:
rm -rf ~/go-sample
- After running these commands, opening nvim starts with no config, no plugins, and no cached state.
Nice Work!
Nice Work!
You just built a complete, mouseless Neovim IDE from a single init.lua file you wrote yourself. Every line of config is yours, and you understand exactly what it does.
You've learned how to:
- Navigate and edit code purely by keyboard using Vim motions (w, b, gg, G, dd, ciw), and bootstrap a full plugin system with lazy.nvim and Telescope for instant file finding and project-wide grep.
- Add Treesitter for AST-based syntax highlighting that understands your code's structure, not just regex patterns.
- Wire up LSP with Mason and nvim-cmp for Go to Definition, Find References, hover docs, rename, code actions, and intelligent autocompletion powered by gopls and pyright.
- Secret Mission: Added which-key for keybinding discovery and lualine for a polished status bar showing mode, branch, filename, diagnostics, and position.
Ready to quiz yourself?