$linuxjunkies
>

A Vim Config From Scratch

Build a Neovim config from scratch: sensible defaults, lazy.nvim plugin manager, Treesitter, LSP via Mason, and autocompletion — all explained step by step.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

Before you start

  • Neovim 0.9 or newer installed
  • git available in PATH for plugin cloning
  • A terminal emulator with 24-bit colour support (e.g. kitty, alacritty, GNOME Terminal 3.36+)
  • Basic familiarity with Vim modal editing (Normal, Insert, Command modes)

A well-tuned Vim (or Neovim) config makes the difference between a capable editor and one that actively fights you. This guide builds a configuration from an empty file: sensible defaults first, then a plugin manager, language server support, and a handful of plugins that pull their weight. Commands target Neovim (the actively developed fork) but the Vim-compatible sections are clearly marked.

Neovim vs Classic Vim

Neovim uses ~/.config/nvim/init.vim (Vimscript) or ~/.config/nvim/init.lua (Lua). Lua is now the preferred language for Neovim configs — it is faster, composable, and all modern plugins expect it. Classic Vim uses ~/.vimrc. This guide uses Lua + Neovim for the main path, with Vimscript equivalents noted where they differ.

Step 1: Install Neovim

Distro packages lag behind. Install at least Neovim 0.9 for full LSP and Treesitter support.

Debian / Ubuntu

sudo add-apt-repository ppa:neovim-ppa/stable
sudo apt update && sudo apt install neovim

Fedora / RHEL 9+

sudo dnf install neovim

Arch

sudo pacman -S neovim

Verify the version:

nvim --version

You should see NVIM v0.9.x or newer. Anything below 0.8 will break several plugins in this guide.

Step 2: Create the Config Directory

mkdir -p ~/.config/nvim
touch ~/.config/nvim/init.lua

All your configuration lives under ~/.config/nvim/. Lua files in ~/.config/nvim/lua/ are auto-discoverable via require(). A clean layout to start with:

~/.config/nvim/
├── init.lua          # entry point
└── lua/
    ├── options.lua   # editor settings
    ├── keymaps.lua   # key bindings
    └── plugins.lua   # plugin manager bootstrap

Step 3: Sensible Defaults

Open ~/.config/nvim/lua/options.lua and add the following. Each line is commented so you know what you are enabling and why.

nvim ~/.config/nvim/lua/options.lua
local opt = vim.opt

opt.number         = true      -- absolute line numbers
opt.relativenumber = true      -- relative numbers for fast jumps
opt.expandtab      = true      -- spaces, not tabs
opt.shiftwidth     = 4         -- indent width
opt.tabstop        = 4
opt.smartindent    = true
opt.wrap           = false     -- no line wrapping
opt.ignorecase     = true      -- case-insensitive search
opt.smartcase      = true      -- ...unless you type uppercase
opt.cursorline     = true      -- highlight current line
opt.termguicolors  = true      -- full 24-bit colour
opt.splitright     = true      -- vertical splits go right
opt.splitbelow     = true      -- horizontal splits go below
opt.scrolloff      = 8         -- keep 8 lines visible above/below cursor
opt.signcolumn     = "yes"     -- always show sign column (avoids jitter)
opt.updatetime     = 250       -- faster CursorHold (helps LSP)
opt.clipboard      = "unnamedplus"  -- share system clipboard

Wire this file into init.lua:

echo 'require("options")' >> ~/.config/nvim/init.lua

Step 4: Install lazy.nvim (Plugin Manager)

lazy.nvim is the current standard for Neovim. It loads plugins on demand, shows a clean UI, and supports lockfiles for reproducible installs. vim-plug remains a solid choice for classic Vim or for anyone preferring Vimscript — a note on it follows this section.

Add the bootstrap snippet to ~/.config/nvim/lua/plugins.lua. This downloads lazy.nvim automatically if it is not already present:

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)

require("lazy").setup({
  -- plugins go here
})

Register it in init.lua:

echo 'require("plugins")' >> ~/.config/nvim/init.lua

vim-plug Alternative (Vim / Vimscript users)

curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
  https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

Then in ~/.vimrc: wrap plugins between call plug#begin() and call plug#end(), then run :PlugInstall.

Step 5: Add Core Plugins

Replace the empty require("lazy").setup({ }) call with a practical plugin list:

require("lazy").setup({
  -- Colour scheme
  { "folke/tokyonight.nvim", priority = 1000,
    config = function() vim.cmd.colorscheme("tokyonight") end },

  -- Syntax highlighting via Treesitter
  { "nvim-treesitter/nvim-treesitter",
    build = ":TSUpdate",
    config = function()
      require("nvim-treesitter.configs").setup({
        ensure_installed = { "lua", "python", "bash", "c", "rust", "javascript" },
        highlight = { enable = true },
        indent    = { enable = true },
      })
    end },

  -- Fuzzy finder
  { "nvim-telescope/telescope.nvim",
    dependencies = { "nvim-lua/plenary.nvim" } },

  -- File tree
  { "nvim-tree/nvim-tree.lua",
    dependencies = { "nvim-tree/nvim-web-devicons" },
    config = function() require("nvim-tree").setup() end },

  -- Status line
  { "nvim-lualine/lualine.nvim",
    config = function() require("lualine").setup() end },

  -- LSP config + Mason (installs language servers)
  { "neovim/nvim-lspconfig" },
  { "williamboman/mason.nvim",
    build = ":MasonUpdate",
    config = function() require("mason").setup() end },
  { "williamboman/mason-lspconfig.nvim" },

  -- Autocompletion
  { "hrsh7th/nvim-cmp",
    dependencies = {
      "hrsh7th/cmp-nvim-lsp",
      "hrsh7th/cmp-buffer",
      "L3MON4D3/LuaSnip",
      "saadparwaiz1/cmp_luasnip",
    }},
})

Launch Neovim. lazy.nvim will clone and install everything automatically:

nvim

Step 6: Configure LSP

Mason installs language server binaries; mason-lspconfig bridges Mason with nvim-lspconfig. Add a new file ~/.config/nvim/lua/lsp.lua:

local lspconfig      = require("lspconfig")
local mason_lsp      = require("mason-lspconfig")
local capabilities   = require("cmp_nvim_lsp").default_capabilities()

mason_lsp.setup({
  ensure_installed = { "pyright", "lua_ls", "bashls", "rust_analyzer" },
  automatic_installation = true,
})

mason_lsp.setup_handlers({
  function(server_name)
    lspconfig[server_name].setup({ capabilities = capabilities })
  end,
})

-- Key bindings applied only when an LSP attaches
vim.api.nvim_create_autocmd("LspAttach", {
  callback = function(ev)
    local opts = { buffer = ev.buf }
    vim.keymap.set("n", "gd",  vim.lsp.buf.definition,      opts)
    vim.keymap.set("n", "K",   vim.lsp.buf.hover,           opts)
    vim.keymap.set("n", "rn", vim.lsp.buf.rename,   opts)
    vim.keymap.set("n", "ca", vim.lsp.buf.code_action, opts)
    vim.keymap.set("n", "[d",  vim.diagnostic.goto_prev,   opts)
    vim.keymap.set("n", "]d",  vim.diagnostic.goto_next,   opts)
  end,
})
echo 'require("lsp")' >> ~/.config/nvim/init.lua

Open any Python or Lua file and you should see diagnostics appearing in the sign column. Run :Mason to open the Mason UI and install additional servers interactively.

Step 7: Autocompletion Setup

Add ~/.config/nvim/lua/completion.lua:

local cmp     = require("cmp")
local luasnip = require("luasnip")

cmp.setup({
  snippet = {
    expand = function(args) luasnip.lsp_expand(args.body) end,
  },
  mapping = cmp.mapping.preset.insert({
    [""] = cmp.mapping.complete(),
    [""]      = cmp.mapping.confirm({ select = true }),
    [""]     = cmp.mapping(function(fallback)
      if cmp.visible()     then cmp.select_next_item()
      elseif luasnip.expand_or_jumpable() then luasnip.expand_or_jump()
      else fallback() end
    end, { "i", "s" }),
  }),
  sources = cmp.config.sources({
    { name = "nvim_lsp" },
    { name = "luasnip"  },
    { name = "buffer"   },
  }),
})
echo 'require("completion")' >> ~/.config/nvim/init.lua

Verification

  1. Run :checkhealth inside Neovim. Review any ERROR lines — warnings for optional features are normal.
  2. Open a Python file. Hover over a symbol and press K. A documentation popup should appear.
  3. Press :Telescope find_files to confirm fuzzy finding works.
  4. Run :Lazy to see the plugin manager dashboard confirming all plugins loaded.

Troubleshooting

  • LSP not attaching: Run :LspInfo in a relevant buffer. If it shows no client, check :Mason to confirm the server installed, and verify the file type with :set filetype?.
  • No colour / broken colours in terminal: Ensure your terminal supports 24-bit colour. For tmux users, add set -g default-terminal "tmux-256color" and set -as terminal-features ",xterm-256color:RGB" to ~/.tmux.conf.
  • lazy.nvim fails to bootstrap: Confirm git is installed and you have internet access. Corporate proxies require setting https_proxy before launching Neovim.
  • Slow startup: Run :Lazy profile to see per-plugin load times. Move heavy plugins to event = "VeryLazy" or specific file-type triggers.
tested on:Ubuntu 24.04Fedora 40Arch rollingDebian 12

Frequently asked questions

Can I use this config with classic Vim instead of Neovim?
The options and key-binding sections translate directly to ~/.vimrc, but lazy.nvim, native LSP, and Treesitter are Neovim-only. Use vim-plug plus coc.nvim for a comparable Vim experience.
How do I add support for a new programming language?
Open :Mason and install the relevant language server, then add its name to the ensure_installed list in lsp.lua. Add the language to Treesitter's ensure_installed list for syntax highlighting.
Is init.lua interchangeable with init.vim?
No. Neovim loads whichever file exists; if both exist, init.lua takes precedence in Neovim 0.9+. You can call vim.cmd() inside Lua to run any Vimscript snippet you need.
Why does my colour scheme look wrong inside tmux or SSH?
Both tmux and some SSH clients strip 24-bit colour escape codes. Set termguicolors to true in options.lua and configure your terminal emulator and tmux to advertise RGB colour support.
How do I keep plugins updated?
Run :Lazy update from inside Neovim. lazy.nvim also maintains a lazy-lock.json lockfile you can commit to version control for reproducible installs across machines.

Related guides