Introduction
IDEs like CLion and VS Code are incredibly powerful, they often feel bloated and can drain system resources. If you are developing or working with bare-metal hardware or something that needs to watch out for resources, you might want a leaner solution. Neovim offers the exact same code intelligence, but runs instantly right in your terminal. This guide will walk you through building a blazing fast, modern development environment from scratch.
Setup
Prerequisites
Before diving into the configuration, ensure you have the following ready:
1 Basic Terminal Fluency: You should be comfortable navigating directories and editing basic text files from the command line.
2 Essential Build Tools: Ensure you have git, make, and a C/C++ compiler (like gcc or clang) installed on your system. You can grab the basics by running for Arch Linux sudo pacman -S base-devel git.
You need to install Neovim into your machine. For Arch Linux, you can run sudo pacman -S neovim
If you are building from source or need an alternative installation method, refer to the official Neovim installation guide.
Go to folder ~/.config/nvim (for linux), this directory is the root place of all things related to your Neovim configuration.
After the installation process of Neovim, you need to choose your plugin manager. A plugin manager is a tool that automates the process of installing, updating, configuring, and removing extensions for Neovim. Without this, adding a new extension to Neovim would mean manually downloading source code from GitHub, extracting it, and placing the files into specific folders within your operating system.
In this guide you will be using Lazy plugin manager. The benefit is that it only loads the required plugin at the time. This ensures your text editor starts up faster than traditional setups.
Create a file called init.lua, then add this snippet into it. The goal is to detect if Lazy is installed. If not, it will clone the repo directly from GitHub then place it on the Neovim runtime path.
vim.g.mapleader = " "
vim.g.maplocalleader = " "
local opt = vim.opt
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
opt.rtp:prepend(lazypath)
require("lazy").setup({
{ import = "packages" },
}, {
change_detection = { notify = false },
})
Note:
- The
import = 'packages'line tells Lazy to look inside thelua/packagesdirectory relative to the Neovim configuration folder. - The
change_detectionline automatically tracks your configuration file and reloads them on the fly for you. By defaultchange_detectionis always on and always gives you notification every time something has been changed. - The
vim.g.mapleader = " "line sets your global leader key to the Spacebar, making all of your custom keybindings much faster to type
Configuration and Plugins
After downloading your Neovim and the plugin manager, you are ready to implement the bare minimum that can make Neovim usable like VS Code or CLion with only a total of around 30 plugins dispersed across 6 different folders.
Start by creating a parent folder called lua, and inside it, create a subfolder called packages.
Go into the packages directory.
Create a file called imports.lua inside your packages directory. This single file acts as a directory index, instructing lazy.nvim to automatically search for your subfolder for configuration
-- ~/.config/nvim/lua/packages/imports.lua
return {
{ import = "packages.coding" },
{ import = "packages.interface" },
{ import = "packages.treesitter" },
{ import = "packages.editor" },
{ import = "packages.utils" },
{ import = "packages.extras" },
}
1. Coding
With everything setup, you can jump into configuring the coding environment. Create a subfolder called coding inside the packages directory.
Create a file called lsp.lua. This file configures the Language Server Protocol (LSP), which allows Neovim to understand your codebase, jump to definitions, and analyze syntax.
-- ~/.config/nvim/lua/packages/coding/lsp.lua
return {
"neovim/nvim-lspconfig",
dependencies = { "hrsh7th/cmp-nvim-lsp" },
config = function()
-- Setup Lua Language Server
vim.lsp.config("lua_ls", {
cmd = { "lua-language-server" },
root_markers = { ".git", ".luarc.json", "init.lua" },
capabilities = require("cmp_nvim_lsp").default_capabilities(),
settings = {
Lua = {
runtime = { version = "LuaJIT" },
diagnostics = { globals = { "vim" } },
},
workspace = {
library = vim.api.nvim_get_runtime_file("", true),
checkThirdParty = false,
},
telemetry = {
enable = false,
},
},
})
vim.lsp.enable "lua_ls"
-- Setup Clangd (C/C++)
vim.lsp.config("clangd", {
cmd = { "clangd", "--background-index", "--clang-tidy" },
root_markers = { ".git", "compile_commands.json" },
capabilities = require("cmp_nvim_lsp").default_capabilities(),
})
vim.lsp.enable "clangd"
-- Setup Arduino (Requires arduino-cli and arduino-language-server)
vim.lsp.config("arduino_language_server", {
cmd = {
"arduino-language-server",
"-cli-config",
vim.fn.expand "~/.arduino15/arduino-cli.yaml",
"-cli",
"/usr/bin/arduino-cli",
"-clangd",
"/usr/bin/clangd",
},
root_markers = { "sketch.yaml", "arduino.json", ".git" },
capabilities = require("cmp_nvim_lsp").default_capabilities(),
})
vim.lsp.enable "arduino_language_server"
-- Global Keybindings (LspAttach)
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local opts = { buffer = args.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", "<leader>rn", vim.lsp.buf.rename, opts)
vim.keymap.set({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action, opts)
end,
})
-- Force filetype detection
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
pattern = { "*.ino", "*.pde" },
command = "set filetype=arduino",
})
end,
}
Note:
- The
lua_lsandclangdblocks tell Neovim how to talk to the language servers, but they do not install the tools. You must install the compilers and servers on your system (for example, on Arch Linux, runsudo pacman -S lua-language-server clang). - The
root_markerslisted for both servers (like.gitorcompile_commands.json) are crucial. They tell Neovim exactly where the project starts, allowing cross-file features to trace symbols across your entire codebase automatically. - The shortcut defined at the bottom uses an
LspAttachevent hook, meaning these keybinds only become active when a language server is successfully running in your current buffer. - For the Arduino setup to work, you must generate a configuration file first by running
arduino-cli config initin your terminal. Otherwise, Neovim will fail to find thearduino-cli.yamlfile.
How to use Language Server Protocol:
-
g + d: Go to code definition -
Shift + k: Hover the current word -
Space + c + a: Run code action
Next, create a file called autocomplete.lua. You need an engine to display the code suggestion as you type. This configuration sets up nvim-cmp and ties it directly to your LSP.
-- ~/.config/nvim/lua/packages/coding/autocomplete.lua
return {
"hrsh7th/nvim-cmp",
event = "InsertEnter",
dependencies = {
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
"rafamadriz/friendly-snippets",
},
config = function()
local cmp = require "cmp"
local luasnip = require "luasnip"
require("luasnip.loaders.from_vscode").lazy_load()
cmp.setup {
snippet = { expand = function(args) luasnip.lsp_expand(args.body) end },
mapping = cmp.mapping.preset.insert {
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<CR>"] = cmp.mapping.confirm { select = true },
["<Tab>"] = 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" }),
["<S-Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { "i", "s" }),
},
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
}, {
{ name = "buffer" },
}),
}
end,
}
Finally, create a file called format.lua. Formatting your code manually is a waste of development time. This file configures conform.nvim to automatically format your files every time you save.
-- ~/.config/nvim/lua/packages/coding/format.lua
return {
"stevearc/conform.nvim",
event = { "BufWritePre" },
keys = {
{
"<leader>fm",
function() require("conform").format { lsp_fallback = true } end,
desc = "Format File",
},
},
opts = {
formatters_by_ft = {
lua = { "stylua" },
c = { "clang-format" },
cpp = { "clang-format" },
},
format_on_save = { timeout_ms = 500, lsp_fallback = true },
},
}
How to use Conform:
-
Space + f + m: Format your current file
2. Editor
Create a subfolder called editor inside the packages directory.
Create a file called telescope.lua. When your project grows, manually navigating directories become a massive bottleneck. This file adds an extensible fuzzy finder to Neovim. It allows you to instantly search through your project files, grep specific text, and navigate active buffers without leaving your keyboard.
-- ~/.config/nvim/lua/packages/editor/telescope.lua
return {
"nvim-telescope/telescope.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
{
"nvim-telescope/telescope-fzf-native.nvim",
build = "make",
config = function() require("telescope").load_extension "fzf" end,
},
},
keys = {
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find Files" },
{ "<leader>fg", "<cmd>Telescope live_grep<cr>", desc = "Live Grep (Search Text)" },
{ "<leader>fb", "<cmd>Telescope buffers<cr>", desc = "Find Buffers" },
{ "<leader>fh", "<cmd>Telescope help_tags<cr>", desc = "Find Help" },
},
config = function()
local telescope = require "telescope"
telescope.setup {
defaults = {
layout_strategy = "horizontal",
layout_config = {
horizontal = { prompt_position = "top", preview_width = 0.55 },
vertical = { mirror = false },
width = 0.87,
height = 0.80,
preview_cutoff = 120,
},
sorting_strategy = "ascending",
winblend = 0,
mappings = {
i = {
["<C-k>"] = "move_selection_previous",
["<C-j>"] = "move_selection_next",
["<C-q>"] = "send_to_qflist",
},
},
},
}
end,
}
How to use Telescope:
-
Space + f + f: Open the file finder -
Space + f + g: Search for specific text across entire project -
Space + f + b: Find active buffers -
Space + f + h: Open the help finder with a tag
Next, create a file called overseer.lua. Switching between your editor and a separate terminal to run make breaks your focus. This plugin acts as a task runner, allowing you to compile your code directly within Neovim and parsing the output for errors.
-- ~/.config/nvim/lua/packages/editor/overseer.lua
return {
"stevearc/overseer.nvim",
-- Global Keybindings for Task Management
keys = {
{ "<leader>or", "<cmd>OverseerRun<CR>", desc = "Run Task" },
{ "<leader>ot", "<cmd>OverseerToggle<CR>", desc = "Toggle Overseer UI" },
{ "<leader>oi", "<cmd>OverseerInfo<CR>", desc = "Overseer Info" },
},
config = function()
local overseer = require "overseer"
overseer.setup {
dap = false,
strategy = "terminal",
templates = { "builtin" },
form = { border = "rounded" },
task_list = {
direction = "bottom",
min_height = 10,
max_height = 15,
},
}
-- Bear + Makefile (For C/C++ projects)
overseer.register_template {
name = "Build with Bear (Make)",
builder = function()
local makefile_match = vim.fs.find("Makefile", { upward = true, path = vim.fn.expand "%:p:h" })
local target_dir = #makefile_match > 0 and vim.fs.dirname(makefile_match[1]) or vim.fn.getcwd()
return {
cmd = { "bear" },
args = { "--", "make" },
cwd = target_dir,
components = { { "on_output_quickfix", open = true }, "default" },
}
end,
condition = {
filetype = { "c", "cpp" },
callback = function() return vim.fn.filereadable(vim.fn.getcwd() .. "/Makefile") == 1 end,
},
}
end,
}
Note:
-
Bearis a command-line tool that generate json compilation database (compile_commands.json) when you runmake. Without this database,clangdwill not be able to analyze your project's cross-file dependencies.
How to use Overseer:
-
Space + o + r: Open the run task menu -
Space + o + t: Toggle the overseer ui menu -
Space + o + i: Get the info of run task
Next, create a file called mini.lua. Neovim's default file explorer can be clunky and unpredictable. This plugin replaces it with highly intuitive, buffer-based file navigator and manipulator, giving you the visual tree.
-- ~/.config/nvim/lua/packages/editor/mini.lua
return {
{
"echasnovski/mini.files",
dependencies = {
"echasnovski/mini.bufremove",
},
keys = {
{
"<leader>e",
function() require("mini.files").open(vim.api.nvim_buf_get_name(0), true) end,
desc = "Open Current File",
},
{
"<leader>d",
function()
local bd = require("mini.bufremove").delete
if vim.bo.modified then
local choice = vim.fn.confirm(("Save changes to %q?"):format(vim.fn.bufname()), "&Yes\n&No\n&Cancel")
if choice == 1 then -- Yes
vim.cmd.write()
bd(0)
elseif choice == 2 then -- No
bd(0, true)
end
else
bd(0)
end
end,
desc = "Close current buffer",
},
},
config = function()
require("mini.files").setup {
windows = {
preview = true,
width_focus = 30,
width_nofocus = 15,
width_preview = 40,
},
options = {
use_as_default_explorer = true,
},
mappings = {
close = "q",
go_in = "l",
go_in_plus = "L",
go_out = "h",
go_out_plus = "H",
reset = "<BS>",
reveal_cwd = "@",
show_help = "g?",
synchronize = "=",
trim_left = "<",
trim_right = ">",
},
}
end,
},
}
How to use Mini:
-
Space + e: Open the current project file navigation -
Space + d: Close the current open file
In these two next files, you can combine them if you don't like too much files.
Create a file called autopairs.lua. Manually closing braces, brackets, and quotes is tedious and frequently causes compilation errors. This plugin automatically pairs them for you.
-- ~/.config/nvim/lua/packages/editor/autopairs.lua
return {
"windwp/nvim-autopairs",
event = "InsertEnter",
config = true,
}
Create a file called comment.lua. When debugging logic, you often need to quickly disable chunks of code. This plugin helps you instantly toggle comments on single lines or visual blocks by pressing gcc, gbc, and friends.
-- ~/.config/nvim/lua/packages/editor/comment.lua
return {
"numToStr/Comment.nvim",
config = function() require("Comment").setup() end,
}
3. Interface
Create a subfolder called interface inside the packages directory.
Create a file called theme.lua. This file handles your color scheme, as the default Neovim theme lacks the modern polish.
-- ~/.config/nvim/lua/packages/interface/theme.lua
return {
"catppuccin/nvim",
name = "catppuccin",
priority = 1000,
config = function()
require("catppuccin").setup {
flavour = "mocha",
transparent_background = true,
}
vim.cmd.colorscheme "catppuccin"
vim.cmd "hi statusline guibg=NONE"
end,
}
Note:
- The
vim.cmd "hi statusline guibg=NONE"andtransparent_background = trueline make it so that your neovim background is transparent - The
vim.cmd.colorscheme "catppuccin"line tells Neovim to use the Catppuccin color scheme.
Create a file dress.lua. This plugin acts as a UI wrapper that intercepts native Neovim input prompts and selection menus, routing them through Telescope instead.
-- ~/.config/nvim/lua/packages/interface/dress.lua
return {
"stevearc/dressing.nvim",
event = "VeryLazy",
opts = {
input = {
enabled = true,
},
select = {
enabled = true,
backend = { "telescope" },
},
},
}
Create a file noice.lua. This plugin completely replaces the default UI for messages, cmdline, and the popupmenu, giving it a modern, floating interface.
-- ~/.config/nvim/lua/packages/interface/noice.lua
return {
"folke/noice.nvim",
event = "VeryLazy",
dependencies = {
"MunifTanjim/nui.nvim",
"rcarriga/nvim-notify",
},
opts = {
lsp = {
override = {
["vim.lsp.util.convert_input_to_markdown_lines"] = true,
["vim.lsp.util.stylize_markdown"] = true,
["cmp.entry.get_documentation"] = true,
},
},
presets = {
bottom_search = true,
command_palette = true,
long_message_to_split = true,
},
},
}
When testing your configuration, you might encounter a floating error message. This happens because your terminal is fully transparent, breaking the plugin's shadow calculations. You can permanently fix this just by creating a file called bug.lua to set a fallback background color.
-- ~/.config/nvim/lua/packages/interface/bug.lua
return {
"rcarriga/nvim-notify",
opts = {
background_colour = "#000000",
},
}
4. Extras
Create a subfolder called extras inside the packages directory.
Create a file called trouble.lua. While the LSP shows you errors inline, reading long compiler diagnostics at the bottom of the screen is difficult. This plugin gives you a dedicated, easily readable list of all your code problems, warnings, and references without hard navigation.
-- ~/.config/nvim/lua/packages/extras/trouble.lua
return {
"folke/trouble.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
opts = {
focus = true,
},
cmd = "Trouble",
keys = {
{
"<leader>xx",
"<cmd>Trouble diagnostics toggle<cr>",
desc = "Diagnostics (Trouble)",
},
{
"<leader>xX",
"<cmd>Trouble diagnostics toggle filter.buf=0<cr>",
desc = "Buffer Diagnostics (Trouble)",
},
{
"<leader>cs",
"<cmd>Trouble symbols toggle focus=false<cr>",
desc = "Symbols (Trouble)",
},
{
"<leader>cl",
"<cmd>Trouble lsp toggle focus=false win.position=right<cr>",
desc = "LSP Definitions / references / ... (Trouble)",
},
{
"<leader>xL",
"<cmd>Trouble loclist toggle<cr>",
desc = "Location List (Trouble)",
},
{
"<leader>xQ",
"<cmd>Trouble qflist toggle<cr>",
desc = "Quickfix List (Trouble)",
},
},
}
How to use trouble:
-
Space + x + x: Get all problem at project -
Space + x + X: Get problem at current open file -
Space + x + L: Get the location problem as list -
Space + x + Q: Do a quickfix if possible -
Space + c + s: Toggle problem symbols -
Space + c + l: Toggle lsp problem
Create a file called todo.lua. This plugin highlights comments like // TODO or // FIXME in your codebase. This keeps you highly organized and ensures you never forget to implement pending logic.
-- ~/.config/nvim/lua/packages/extras/todo.lua
return {
"folke/todo-comments.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
opts = { signs = false }, -- set to true if you want icons in the sidebar
}
Create a file called indent_backline.lua. When dealing with deeply nested statements in your code, tracing the scope visually can be painful. This plugin adds vertical lines to ensure you are never confused about your current indentation level.
-- ~/.config/nvim/lua/packages/extras/indent_backline.lua
return {
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
opts = {
indent = {
char = "│",
tab_char = "│",
},
scope = { enabled = false }, -- turn on if you want the current scope highlighted
exclude = {
filetypes = {
"help",
"alpha",
"dashboard",
"neo-tree",
"Trouble",
"trouble",
"lazy",
"mason",
"notify",
},
},
},
}
5. Treesitter
Create a subfolder called treesitter inside the packages directory.
Create a file called treesitter.lua. The default syntax highlighting in Neovim is basic and often fails on complex logic. Treesitter parses your code into an actual syntax tree, providing you with the most accurate and responsive syntax highlighting possible.
-- ~/.config/nvim/lua/packages/treesitter/treesitter.lua
return {
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
dependencies = {
"nvim-tree/nvim-web-devicons",
"nvim-treesitter/nvim-treesitter-textobjects",
},
config = function()
require("nvim-treesitter.config").setup {
ensure_installed = { "lua", "c", "cpp", "vim", "vimdoc", "markdown", "asm" },
sync_install = false,
auto_install = true,
highlight = { enable = true },
indent = { enable = true },
-- Smart Selection (Text Objects)
textobjects = {
select = {
enable = true,
lookahead = true,
keymaps = {
-- Function selection
["af"] = "@function.outer", -- Select around function
["if"] = "@function.inner", -- Select inside function
-- Class selection
["ac"] = "@class.outer", -- Select around class
["ic"] = "@class.inner", -- Select inside class
-- Loop selection
["al"] = "@loop.outer",
["il"] = "@loop.inner",
},
},
},
}
end,
},
}
6. Utilities
Create a subfolder called utils inside the packages directory.
Create a file called which_keys.lua. A heavily customized Neovim relies on dozens of custom keybinds. This plugin ensures you do not forget them by automatically showcasing a menu of available options at the bottom of the screen whenever you press your key in mode other than typing.
-- ~/.config/nvim/lua/packages/utils/which_keys.lua
return {
{
"folke/which-key.nvim",
event = "VeryLazy",
init = function()
vim.o.timeout = true
vim.o.timeoutlen = 300
end,
opts = {
win = {
border = "rounded", -- none, single, double, shadow
},
},
config = function(_, opts)
local wk = require "which-key"
wk.setup(opts)
wk.add {
{ "<leader>f", group = "Find" }, -- Group name
{ "<leader>c", group = "Code" },
}
end,
},
}
First Run
With all of your configuration files in place, it is time to build the environment. Open your terminal and simply type nvim. If you have never opened Neovim, just wait a few seconds for Lazy.nvim to install.
Tips and Tricks
Now that Neovim is fully configured and good to be used, here are a few advanced tips to keep it running smoothly:
- Always Check Your Health: If your language servers or formatters ever act up, type
:checkhealthin normal mode. Neovim will run a comprehensive diagnostic check across all your plugins and tell you exactly what system dependency is missing. - Manage Your Compilation Database: Because you are using
bearwith Overseer, you will constantly be generatingcompile_commands.jsonfiles in your project directories. Be sure to addcompile_commands.jsonto your global.gitignorefile when using GitHub so you don't accidentally commit it to your repositories. - Keep Your Plugins Updated: One of the best features of
lazy.nvimis its built-in UI. You can type:Lazyto open the control panel, then pressUto automatically update all of your plugins to their latest stable versions.
Conclusion
By structuring your environment with tools like LSP, Telescope, and Overseer, you have successfully replaced the heavy overhead of a traditional IDE with a modular, terminal-based architecture. You now have a blazing fast, highly intelligent workspace built specifically for your development. Good luck with your compilations, and enjoy the speed of your new workflow.
Top comments (0)