Trevor Hauter

How to build a full IDE in Neovim from scratch (2026)

Welcome! In this tutorial we will go over how to create a modern IDE inside of Neovim from scratch. You will learn how to structure your configuration, install and configure plugins, and set up core parts of an IDE such as fuzzy file finding and code completion. I’ve created a tutorial repository you can use as reference while you follow along.

This tutorial assumes you are familiar with the basics of vim motions. If you are unfamiliar or get stuck, keep this handy vim cheatsheet close by for reference. Now let’s jump right in!

First things first, let’s install Neovim. Once you’ve done that, you can launch neovim inside a terminal with the nvim command.

Project structure

Next, let’s create a directory to store our configuration.

mkdir ~/.config/nvim

This is where neovim will look every time it’s launched. I recommend storing this in version control, either with your other dotfiles or in a repository of its own. To prevent overloading of the term “configuration”, I’m going to refer to the files inside the ~/.config/nvim directory as our project going forward.

Let’s establish our core project structure. This will keep things nice and organized as we expand/modify our project!

We’re going to create a directory for our core configuration options (native nvim settings & generic settings), another for plugins, and an init file to tell Neovim where to look when everything is loaded up. We can create all of these by running the following commands inside our root project directory ~/.config/nvim

touch init.lua && mkdir lua \
&& mkdir lua/config && touch lua/config/options.lua \
&& mkdir lua/plugins && touch lua/plugins/init.lua

This yields the following tree structure

~/.config/nvim
├── init.lua
└── lua
    ├── config
    │   └── options.lua
    └── plugins
        └── init.lua

Now, when neovim launches, it will look in init.lua at our project root. Let’s point init.lua to lua/config/options.lua, which is going to be the central file that loads everything else we care about.

-- ~/.config/nvim/init.lua
require("config.options")

Now, let’s add some of our first settings into our lua/config/options.lua file!

-- ~/.config/nvim//lua/config/options.lua
vim.g.mapleader = " " -- Prefix used for keybinds

require("plugins") -- Will load plugins directory automatically

-- Set default indentation length (in spaces)
vim.bo.shiftwidth = 2
vim.bo.softtabstop = 2
vim.bo.tabstop = 2

-- Use spaces for tab in insert mode ;)
vim.opt.expandtab = true

-- Don't highlight every search result when performing a `/` search
vim.opt.hlsearch = false

-- Show line numbers in editor
vim.opt.nu = true

-- Make line numbers relative to cursor (great for efficient motions!)
vim.opt.relativenumber = true

-- Always keep 8 lines of buffer at the top or bottom
vim.opt.scrolloff = 8

-- Leaves space for signs and other hints in line number column
vim.opt.signcolumn = "yes"

-- Better indentation
vim.opt.smartindent = true

-- I have swapfiles disabled for convenience, 
-- may want to enable if memory is a concern
vim.opt.swapfile = false

-- Wrap text at edge of screen
vim.opt.wrap = true

-- Adjust styling of all floating windows
vim.api.nvim_set_hl(0, "NormalFloat", { link = "Normal" })
vim.api.nvim_set_hl(0, "FloatBorder", { link = "FloatBorder" })
vim.api.nvim_set_hl(0, "FloatTitle", { link = "Title" })

There are many more options we can set, but this will get us started with some good defaults. If you aren’t sure what a setting does, simply type :h $setting inside Neovim to open up the documentation, or use a search engine to learn more.

Next, in lua/plugins/init.lua, let’s add some simple code so that when we create new files in the plugins directory, Neovim loads them in automatically. This will make managing our project a lot easier! :-)

-- ~/.config/nvim/lua/plugins/init.lua
-- Require all files in directory
local config_path = vim.fn.stdpath("config")
local directory_name = "plugins"

for _, file in
    ipairs(
        vim.fn.readdir(
            config_path .. "/lua/" .. directory_name,
            [[v:val =~ '\.lua$']]
        )
    )
do
    local file = file:gsub("%.lua$", "")

    if file ~= "init" then
        require(directory_name .. "." .. file)
    end
end

Nice! At this point, we have our structure in place, and are ready to add our first plugin!

Installing plugins

The plugin ecosystem in Neovim is rich and diverse, and is one of the highlights of the editor. You can find tools and solutions for just about any workflow, allowing you to have an editor that is truly yours. Some even refer to Neovim as a PDE (personal development environment) for this very reason.

For our first plugin, let’s install a theme. My favorite is gruvbox-material, which is also the theme used for syntax highlighting on this site, but you can choose whatever you like!

Let’s create a file in our plugins directory and add our configuration for our theme

-- ~/.config/nvim/lua/plugins/theme.lua
vim.pack.add({ "https://github.com/sainnhe/gruvbox-material" })

if vim.fn.has("termguicolors") == 1 then
    vim.opt.termguicolors = true
end

-- Theme specific plugin options
vim.g.gruvbox_material_enable_italic = true
vim.g.gruvbox_material_enable_bold = true

-- Can be used in tandem with a transparent terminal 
-- setup to make Neovim transparent!
vim.g.gruvbox_material_transparent_background = 1
vim.o.background = "dark"

vim.g.gruvbox_material_background = "soft"
vim.g.gruvbox_material_better_performance = 1

-- Important! This tells neovim what colorscheme to use
vim.cmd.colorscheme("gruvbox-material")

The next time you open Neovim, you should be prompted to install your theme. Confirm the download, relaunch and bam! You’ve just installed your very first plugin, and your snazzy new theme looks amazing! This will be the general flow for installing and using new plugins.

You can configure your theme further by looking at the documentation in the themes repository. I encourage you to read through the documentation for plugins you are interested in, as they will have everything you need to get up and running quickly!

Now, let’s focus on project navigation. Every great IDE needs a file explorer, and we need look no further than nvim-tree.lua

Let’s create a file for our plugin and add our configuration.

-- ~/.config/nvim/lua/plugins/nvim-tree.lua
vim.pack.add({
    -- web-devicons are a dependency for nvim-tree, but also many other plugins
    { src = "https://github.com/nvim-tree/nvim-web-devicons" },
    { src = "https://github.com/nvim-tree/nvim-tree.lua" },
})

-- Turn off netrw (builtin Neovim file explorer)
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1

require("nvim-tree").setup({
    view = {
        -- Adapt size of explorer to file/directory names
        adaptive_size = true,
        -- Show relative line numbers in file explorer
        number = true,
        relativenumber = true,
    },
    -- Hide git/gitignore by default
    filters = { custom = { "^.git$", "^.gitignore$" } },
})


local nvim_tree_api = require("nvim-tree.api")

-- Toggle nvim-tree in normal mode with ctrl + n
vim.keymap.set(
    "n",
    "<C-n>",
    nvim_tree_api.tree.toggle,
    { noremap = true, silent = true }
)

-- Open your current file in nvim-tree with <leader> + n
vim.keymap.set(
    "n",
    "<leader>n",
    function()
        nvim_tree_api.tree.find_file({
            open = true,
            update_root = "<bang>",
            focus = true,
        })
    end,
    { noremap = true, silent = true }
)

After installing our plugins, we now have a beautiful file explorer right inside of neovim! (Remember, <leader> is set to space)

Now let’s kick it up a notch. Let’s add the powerful search plugin fzf.lua. This will allow us to easily search for files, use grep (or ripgrep) to search for patterns and phrases, or filter/search results from commands like git status to find the files you were just working in!

Before we set up our plugin configuration, we’ll need to install a few dependencies. (Learn more about fzf.lua dependencies)

Now that we have those installed, let’s add the configuration for our plugin.

-- ~/.config/nvim/lua/plugins/fzf.lua

vim.pack.add({ "https://github.com/ibhagwan/fzf-lua" })

local fzf = require("fzf-lua")

-- Live grep project
vim.keymap.set({ "n", "v" }, "<leader>/", fzf.live_grep)

-- Search files returned from git status
vim.keymap.set({ "n", "v" }, "<leader>d", fzf.git_status)

-- Search files throughout the project
vim.keymap.set({ "n", "v" }, "<leader>f", fzf.files)

-- Search for the highlighted phrase, or the word under your cursor
vim.keymap.set({ "n", "v" }, "sw", fzf.grep_cword)

It’s just that easy! Now we can find what we need in our project with a few simple keybinds. Be sure to check out the fzf.lua commands to see what else you can add to make your editor even more powerful.

Next, let’s install an important plugin, nvim-treesitter. This will give us better syntax highlighting and enable quality of life features like snippets, context queues, and more in our editor. Read more about Treesitter on Learn nvim

First off, we must install our dependencies. Once we’ve done that, we’re ready to add our configuration!

-- ~/.config/nvim/lua/plugins/nvim-treesitter.lua
vim.pack.add({ "https://github.com/nvim-treesitter/nvim-treesitter" })

local treesitter = require("nvim-treesitter")

treesitter.install({"python", "zig"})

-- auto-highlight files when opened
vim.api.nvim_create_autocmd("FileType", {
    callback = function(args)
        if
            vim.list_contains(
                treesitter.get_installed(),
                vim.treesitter.language.get_lang(args.match)
            )
        then
            vim.treesitter.start(args.buf)
        end
    end,
})

This will automatically install the Treesitter parsers for the languages you choose. Check the nvim-treesitter documentation to see a list of available parsers.

There are a number of other plugins I use everyday. To keep this post from being too long, I will leave a list of recommended plugins and their configurations in our tutorial repository below. Feel free to pick and choose whatever seems interesting to you!

Note: Some of these configurations, such as the one for eyeliner.nvim, or gitsigns.nvim include (optional) color overrides, because the default colors do not provide enough contrast with the recommended theme for this project. This is achieved with a separate lua/config/colors.lua file, which is loaded in after everything at the bottom of lua/options.lua. This ensures that the color overrides are applied after all the plugins have been loaded in.

There are countless great plugins available and I encourage you to explore and see what else is out there! Be warned - you may end up spending all of your time customizing your editor…

Installing and configuring language servers

Now we get to the really fun part. Adding code completion, diagnostics (errors & warnings), code navigation (go-to definition), and more to our editor! You can learn more about how lsps work on Learn nvim.

Neovim can support any language you throw at it. To make installing and using language servers simple, we’re going to use 3 plugins. mason.nvim, to manage the installation of language servers, and mason-lspconfig.nvim & nvim-lspconfig to hook everything up behind the scenes. Don’t worry, using this is much simpler than it may seem! Let’s take a look at what installation looks like.

First, make sure you have the necessary requirements for mason.nvim. Then we can add our configuration.

-- ~/.config/nvim/lua/plugins/mason.lua
vim.pack.add({
    { src = "https://github.com/williamboman/mason.nvim" },
    { src = "https://github.com/williamboman/mason-lspconfig.nvim" },
    { src = "https://github.com/neovim/nvim-lspconfig" },
})

Now, let’s tell mason.nvim what language servers we’d like to have installed. I’m adding the language servers for Python and Zig here, but you can find a language server for just about any language you could want in the mason registry.

-- ~/.config/nvim/lua/config/lsps.lua
require("mason").setup({})
require("mason-lspconfig").setup({
    ensure_installed = {
        "pyright",
        "zls",
    },
    automatic_enable = true,
})

Then let’s call lua/config/lsps.lua on startup by adding a line in lua/config/options.lua (options file for reference)

# ~/.config/nvim/lua/config/options.lua
require("plugins")
+require("config.lsps")

The next time we launch Neovim we will install our dependencies, then mason will automatically handle the installation of these language servers for us. Beautiful!

We’re almost there, but we have a few more plugins to really give us a great experience. This includes better autocomplete, and lots of helpful snippets to speed up our development! We’re going to install blink.cmp to power our completion, & friendly-snippets to provide us with our snippets.

First, we’re going to need to install Rust, as it is a dependency for blink.cmp.

Once you’ve done that, let’s go ahead and add our configuration

-- ~/.config/nvim/lua/plugins/blink.lua
vim.pack.add({
    "https://github.com/saghen/blink.lib", -- dependency for blink.cmp
    "https://github.com/saghen/blink.cmp",
    "https://github.com/rafamadriz/friendly-snippets",
})

local cmp = require("blink.cmp")
cmp.build():pwait()
cmp.setup()

Now, once you relaunch Neovim, everything will install. It will take a bit the first time as everything is compiling and getting ready on your computer.

Lastly, let’s configure some keybinds for our project so we can use all of our beautiful language server shortcuts that will now be available to us! I’m including a generic keybind file since we are using vims native lsp actions. They are powered under the hood by the plugins we just installed and configured.

-- ~/.config/nvim/lua/config/keymaps.lua
-- LSP keybinds
vim.keymap.set("n", "gd", vim.lsp.buf.definition) -- Go to definition
vim.keymap.set("n", "gD", vim.lsp.buf.declaration) -- Go to declaration
vim.keymap.set("n", "gr", vim.lsp.buf.references) -- Find references
vim.keymap.set("n", "K", vim.lsp.buf.hover) -- Show documentation
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename) -- Rename symbol
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action) -- Code actions
vim.keymap.set("n", "gi", vim.lsp.buf.implementation) -- Go to implementation
vim.keymap.set("n", "gy", vim.lsp.buf.type_definition) -- Go to type definition

vim.keymap.set("n", "[d", vim.diagnostic.goto_prev) -- Previous diagnostic
vim.keymap.set("n", "]d", vim.diagnostic.goto_next) -- Next diagnostic
vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float) -- Show error

And again, let’s ensure this is loaded by our config/options.lua file

# ~/.config/nvim/lua/config/options.lua
require("plugins")
require("config.lsps")
+require("config.keymaps")

Now let’s open a file and test it!

Wow, that wasn’t so bad, and we nearly have a complete modern IDE!

Setting up automatic code formatting

You know what pairs well with language servers? Automatic code formatting! For this, we are going to use a fantastic plugin conform.nvim. It’s important to note that this plugin doesn’t handle the installation of linters or formatters.

Let’s create the configuration for our plugin. I’ve included a few examples to make understanding the configuration easier!

-- ~/.config/nvim/lua/config/conform
vim.pack.add({ "https://github.com/stevearc/conform.nvim" })

require("conform").setup({
    formatters_by_ft = {
        lua = { "stylua" },
        python = { "isort", "black" },
        zig = { "zigfmt" },
    },
})

-- Autocommand that runs conform on on save
vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = "*",
    callback = function(args)
        require("conform").format({ bufnr = args.buf })
    end,
})

And there you have it, you now have a modern IDE set up inside Neovim! Thank you so much for reading, I hope this tutorial has been helpful. If you have any feedback or questions, don’t hesitate to reach out to me!

← The importance of user centered software