Hi, it’s Kn.

Neovim comes with cool new improvements like Lua remote plugin host, built-in LSP client (yes!), and Treesitter syntax engine I found there are already a bunch of great plugins that leverage those features. I tried them, and already love them! Also Since my current laptop is quite old ( a Macbook with core i5 gen 7th processor and 8gb ram ), Sublime Text or Vscode is too “hard” for him to handle.

I’d like to introduce my latest setup with Neovim 0.9 and modern plugins. Here is a quick summary of my set up:

  • lazy.nvim - is a modern plugin manager for Neovim
  • LSP-zero - A starting point to setup some lsp related features in neovim.
  • mason - Easily install and manage LSP servers, DAP servers, linters, and formatters
  • mason-lspconfig - Extension to mason.nvim that makes it easier to use lspconfig with mason.nvim.
  • nvim-cmp - A completion plugin for neovim coded in Lua.
  • mason-null-Ls - bridges mason.nvim with the null-ls plugin, making it easier to use both plugins together.
  • nvim-treesitter - Treesitter configurations and abstraction layer for Neovim
  • telescope.nvim - A highly extendable fuzzy finder over lists
  • lualine.nvim - A blazing fast and easy to configure neovim statusline plugin written in pure lua
  • nvim-tree.lua - A file explorer tree for neovim written in lua
  • Indent-blank-line - Indent guides for Neovim
  • gitsigns - Git integration for buffers
  • nvim-dap-ui, nvim-dap - Debug Adapter Protocol client implementation for Neovim
  • autopairs - autopairs for neovim
  • undotree - The undo history visualizer for VIM

And here is my dotfiles repository:

https://github.com/huyhoang8398/dotfiles

Prerequisites — iTerm2 and Patched Nerd Font

iTerm2 is a fast terminal emulator for macOS. Install one of Nerd Fonts for displaying fancy glyphs on your terminal. My current choice is JetBrainsMono Nerdfont. And use it on your terminal app. For example, on iTerm2:

Font Iterm configuration

My color theme is Moonfly, which is a true color Moonfly theme. The Moonfly theme for iTerm2 is in my dotfile repository here with a bit color customization.

Moonfly

TypeCategoryValueColor
BackgroundBackground#080808background
ForegroundForeground#bdbdbdbackground
BoldBold#eeeeeebackground
CursorCursor#9e9e9ebackground
Cursor TextCursor Text#080808background
SelectionSelection#b2ceeebackground
Selection TextSelection Text#080808background
Color 1Black (normal)#323437background
Color 2Red (normal)#ff5454background
Color 3Green (normal)#8cc85fbackground
Color 4Yellow (normal)#e3c78abackground
Color 5Blue (normal)#80a0ffbackground
Color 6Purple (normal)#cf87e8background
Color 7Cyan (normal)#79dac8background
Color 8White (normal)#c6c6c6background
Color 9Black (bright)#949494background
Color 10Red (bright)#ff5189background
Color 11Green (bright)#36c692background
Color 12Yellow (bright)#c2c292background
Color 13Blue (bright)#74b2ffbackground
Color 14Purple (bright)#ae81ffbackground
Color 15Cyan (bright)#85dc85background
Color 16White (bright)#e4e4e4background

Install Neovim

brew install neovim # MacOS
apt install neovim # Debian/Ubuntu
pacman -S neovim # Archlinux

Directory structure

Neovim conforms XDG Base Directory structure. Here is my config file structure:

.
├── init.lua    | Neovim initialization file
├── after/      | Standard auto-loading 'after' base directory
│   └── plugin/ | Auto-loading 'plugin' configs
├── ftdetect/   | Detect filetype
└── lua/        | Lua base directory
    ├── config/ | Plugin configs that ARE lazy-loaded via explicit 'require'
    └── custom/ | plugin-manager, mappings and options configs

Install plugin manager: Lazy.nvim

Lazy.nvim comes with lots of features that I found useful sunch as:

  • 📦 Manage all your Neovim plugins with a powerful UI
  • 🚀 Fast startup times thanks to automatic caching and bytecode compilation of Lua modules
  • 💾 Partial clones instead of shallow clones
  • 🔌 Automatic lazy-loading of Lua modules and lazy-loading on events, commands, filetypes, and key mappings
  • ⏳ Automatically install missing plugins before starting up Neovim, allowing you to start using it right away
  • 💪 Async execution for improved performance
  • 🛠️ No need to manually compile plugins
  • 🧪 Correct sequencing of dependencies
  • 📁 Configurable in multiple files
  • 📚 Generates helptags of the headings in README.md files for plugins that don’t have vimdocs
  • 💻 Dev options and patterns for using local plugins
  • 📊 Profiling tools to optimize performance
  • 🔒 Lockfile lazy-lock.json to keep track of installed plugins
  • 🔎 Automatically check for updates
  • 📋 Commit, branch, tag, version, and full Semver support
  • 📈 Statusline component to see the number of pending updates
  • 🎨 Automatically lazy-loads colorschemes

Setup

Firstly, create a lazy.lua file in ~/.config/nvim/lua/custom/ with this content You then can add the following Lua code to your lazy.lua to bootstrap lazy.nvim:

-- Lazy.nvim --
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", -- latest stable release
		lazypath,
	})
end
vim.opt.rtp:prepend(lazypath)

Next step is to add lazy.nvim below the code added in the prior step in lazy.lua:

require("lazy").setup(plugins, opts)

plugins: this is all the plugin that you want to install and should be a table or a string

  • table: a list with your Plugin Spec
  • string: a Lua module name that contains your Plugin Spec.

opts: this should be lazy nvim options itself and should be a table.

Example Options

local opts = {
	install = {
		missing = true,
		colorscheme = { "moonfly", "habamax" },
	},

	ui = {
		border = "single",
		icons = {
			ft = "",
			lazy = "󰂠 ",
			loaded = "",
			not_loaded = "",
		},
	},

	performance = {
		rtp = {
			disabled_plugins = {
				"gzip",
				"netrwPlugin",
				"rplugin",
				"tarPlugin",
				"tohtml",
				"tutor",
				"zipPlugin",
			},
		},
	},
}

Example Plugin Install

local plugins = {
	{
		"bluz71/vim-moonfly-colors",
		name = "moonfly",
		lazy = false,
		priority = 1000,
		config = function()
			require("config.moonfly")
		end,
	},
	-- lualine (status line) --
	{
		"nvim-lualine/lualine.nvim",
		dependencies = { "nvim-tree/nvim-web-devicons" },
		config = function()
			require("config.lualine")
		end,
	},
}

In this Blog, I will cover only some essenstial plugins, the rest you could see in my dotfiles link above

Colorscheme

Im using Moonfly - a dark charcoal theme with some options:

local g = vim.g

g.moonflyWinSeparator = 2
g.moonflyVirtualTextColor = true
g.moonflyUnderlineMatchParen = true
g.moonflyCursorColor = true
g.moonflyTransparent = true
g.moonflyNormalFloat = true

-- Overwrite folder color in nvimtree
vim.api.nvim_create_autocmd("ColorScheme", {
	pattern = "moonfly",
	callback = function()
		vim.api.nvim_set_hl(0, "NvimTreeFolderIcon", { link = "MoonflyBlue" })
	end,
	group = vim.api.nvim_create_augroup("CustomHighlight", {}),
})

It supports treesitter and other plugins very well and Also the maintainer is really friendly (We talk alots in Discord)

Colorscheme Example

Status line: Lualine

Lualine

nvim-lualine/lualine provides a flexible way to configure statusline

local lualine_sections = {
	lualine_a = { "mode" },
	lualine_b = { "branch", "b:gitsigns_status" },
	lualine_c = {
		"filename",
		{
			"diagnostics",
			sources = { "nvim_diagnostic" },
			--symbols = { error = "E:", warn = "W:", info = "I:", hint = "H:" },
		},
		"g:metals_status",
	},
	lualine_x = { "filetype" },
	lualine_y = { { "fileformat", icons_enabled = false }, "encoding" },
	lualine_z = { "progress", "location" },
}

local opts = {
	options = {
		icons_enabled = true,
		globalstatus = true,
		theme = "moonfly",
		--component_separators = "|",
		--section_separators = "",
		disabled_filetypes = {},
	},
	sections = lualine_sections,
	inactive_sections = vim.deepcopy(lualine_sections),
	tabline = {},
	extensions = { "nvim-tree" },
}

LSP Zero - LSP Lifesaver

The purpose of this plugin is to bundle all the “boilerplate code” necessary to have nvim-cmp (a popular autocompletion plugin) and nvim-lspconfig working together. And if you opt in, it can use mason.nvim to let you install language servers from inside neovim.

It used to be very difficult to setup language server, autocompletion, etc with CoC or similar, but now Neovim has a built-in LSP support. You can easily configure it by using neovim/nvim-lspconfig and Mason.

For me I found that LSP zero is lifesaver but you could also consider you might not need lsp-zero.

LSP setup

{
   "VonHeikemen/lsp-zero.nvim",
   branch = "v2.x",
   dependencies = {
    -- LSP Support
    { "neovim/nvim-lspconfig" },
    { "williamboman/mason.nvim" },
    {
        "williamboman/mason-lspconfig.nvim",
        config = function()
            require("config.mason-lspconfig")
        end,
    },
    {
        "jay-babu/mason-null-ls.nvim",
        event = { "BufReadPre", "BufNewFile" },
        dependencies = {
            "williamboman/mason.nvim",
            "jose-elias-alvarez/null-ls.nvim",
        },
        config = function()
            require("config.mason-null-ls")
        end,
    },

    -- Autocompletion
    {
        "hrsh7th/nvim-cmp",
        dependencies = {
            "hrsh7th/cmp-nvim-lua",
            "hrsh7th/cmp-buffer",
            "hrsh7th/cmp-path",
            "hrsh7th/cmp-nvim-lsp",
            "saadparwaiz1/cmp_luasnip",
            "hrsh7th/cmp-nvim-lsp-signature-help",
        },
        event = "InsertEnter",
    },
    {
        "L3MON4D3/LuaSnip",
        build = "make install_jsregexp",
        dependencies = {
            "rafamadriz/friendly-snippets",
        },
    },
   },
   config = function()
        require("config.lsp")
   end,
},

My above template will cover all requirement for LSP, Autocompletion and Snippet

Language servers are configured and initialized using nvim-lspconfig.

Example Config

local lsp = require("lsp-zero").preset({ float_border = "rounded", configure_diagnostics = true })
lsp.on_attach(function(client, bufnr)
	lsp.default_keymaps({ buffer = bufnr })
	local opts = { buffer = bufnr }
	vim.keymap.set("n", "gd", "<cmd>Telescope lsp_definitions<cr>", opts)
	vim.keymap.set("n", "gr", "<cmd>Telescope lsp_references<cr>", opts)
	vim.keymap.set("n", "gl", "<cmd>lua vim.diagnostic.open_float()<cr>", opts)
	vim.keymap.set("n", "[d", "<cmd>lua vim.diagnostic.goto_prev()<cr>", opts)
	vim.keymap.set("n", "]d", "<cmd>lua vim.diagnostic.goto_next()<cr>", opts)
	vim.keymap.set("n", "<leader>wa", vim.lsp.buf.add_workspace_folder, opts)
	vim.keymap.set("n", "<leader>wr", vim.lsp.buf.remove_workspace_folder, opts)
	vim.keymap.set("n", "<leader>wl", function()
		print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
	end, opts)
	vim.keymap.set("n", "<leader>ca", function()
		vim.lsp.buf.code_action({ apply = true })
	end, opts)
	vim.keymap.set("n", "<leader>D", vim.lsp.buf.type_definition, opts)
	vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
end)

Install a language server

Let’s try to use the language server for lua.

Open your init.lua and execute the command :LspInstall. Now mason.nvim will suggest a language server. Neovim should show a message like this.

Please select which server you want to install for filetype "lua":
1: lua_ls
Type number and <Enter> or click with the mouse (q or empty cancels):

Choose 1 for lua_ls, then press enter. A floating window will show up. When the server is done installing, a message should appear.

At the moment the language server can’t start automatically, restart Neovim so the language server can be configured properly. Once the server starts you’ll notice warning signs in the global variable vim, that means everything is well and good.

For automatically installing any language server, you could create a file in `after/plugin/mason-lspconfig.lua

local mason_lspconfig = require("mason-lspconfig")

mason_lspconfig.setup({
	ensure_installed = {
		"pyright",
		"lua_ls",
		"bashls",
		"docker_compose_language_service",
		"dockerls",
		"marksman",
	},
})

I mainly use Bash, Python and Go so it will will automtically install these languague server If you want to manully install this, try to run :Mason, a popup screen will show like this and you could chose whatever language server you want to install

Mason

Auto-completion: cmp

CMP

To get LSP-aware auto-completion feature with fancy pictograms, I use the following plugins:

  • L3MON4D3/LuaSnip - Snippet engine
  • hrsh7th/cmp-nvim-lsp - nvim-cmp source for neovim’s built-in LSP
  • hrsh7th/cmp-buffer - nvim-cmp source for buffer words
  • hrsh7th/nvim-cmp - A completion engine plugin for neovim
  • hrsh7th/cmp-path - Path completion
  • hrsh7th/cmp-nvim-lsp-signature-help - Signature help

Configure it like so:

-- Make sure you setup `cmp` after lsp-zero
local cmp = require("cmp")
local cmp_action = require("lsp-zero").cmp_action()

vim.tbl_map(function(type)
	require("luasnip.loaders.from_" .. type).lazy_load()
end, { "vscode", "snipmate", "lua" })
require("luasnip").filetype_extend("python", { "pydoc" })
require("luasnip").filetype_extend("sh", { "shelldoc" })

cmp.setup({
	snippet = {
		expand = function(args)
			require("luasnip").lsp_expand(args.body)
		end,
	},
	window = {
		completion = cmp.config.window.bordered(),
		documentation = cmp.config.window.bordered(),
	},
	sources = {
		{ name = "nvim_lsp" },
		{ name = "luasnip" },
		{ name = "buffer" },
		{ name = "nvim_lua" },
		{ name = "path" },
		{ name = "nvim_lsp_signature_help" },
	},
	mapping = {
		["<CR>"] = cmp.mapping.confirm({ select = false }),
		["<Tab>"] = cmp_action.luasnip_supertab(),
		["<S-Tab>"] = cmp_action.luasnip_shift_supertab(),
	},
})

Syntax highlightings: Treesitter

Treesitter

Treesitter is a popular language parser for syntax highlightings. First, install it:

brew install tree-sitter

Install nvim-treesitter/nvim-treesitter with Lazy and configure it like so:

local opts = {
	highlight = { enable = true },
	indent = { enable = true },
	autotag = { enable = true },
	ensure_installed = {
		"dockerfile",
		"go",
		"bash",
		"json",
		"lua",
		"markdown",
		"markdown_inline",
		"python",
		"yaml",
	},
}

require("nvim-treesitter.configs").setup(opts)

Fuzz finder: Telescope

Telescope

telescope.nvim provides an interactive fuzzy finder over lists, built on top of the latest Neovim features. I also use telescope-file-browser.nvim as a filer.

It’s so useful because you can search files while viewing the content of the files without actually opening them. It supports various sources like Vim, files, Git, LSP, and Treesitter. Check out the showcase of Telescope.

Install kyazdani42/nvim-web-devicons to get file icons on Telescope, statusline, and other supported plugins.

The configuration would look like so:

local opts = {
	defaults = {
		hl_result_eol = false,
		layout_config = {
			height = 0.8,
			prompt_position = "top",
			preview_width = 0.5,
			width = 0.9,
		},
		mappings = {
			i = {
				["<esc>"] = actions.close,
				["<C-f>"] = actions.results_scrolling_down,
				["<C-b>"] = actions.results_scrolling_up,
			},
		},
		multi_icon = "✚",
		prompt_prefix = "❯ ",
		selection_caret = "▶ ",
		sorting_strategy = "ascending",
	},
	pickers = {
		buffers = {
			show_all_buffers = true,
		},
	},
}
telescope.setup(opts)
telescope.load_extension("fzf")

keymaps

-- Mappings.
local map = vim.keymap.set
map("n", "<leader>\\", "<cmd>Telescope find_files<CR>")
map("n", "<leader>fa", "<cmd>Telescope find_files follow=true no_ignore=true hidden=true<CR>")
map("n", "<leader>fw", "<cmd>Telescope live_grep<CR>")
map("n", "<leader>fb", "<cmd>Telescope buffers<CR>")
map("n", "<leader>fh", "<cmd>Telescope help_tags<CR>")
map("n", "<leader>fo", "<cmd>Telescope oldfiles<CR>")
map("n", "<leader>fz", "<cmd>Telescope current_buffer_fuzzy_find<CR>")
map("n", "<leader>cm", "<cmd>Telescope git_commits<CR>")
map("n", "<leader>gt", "<cmd>Telescope git_status<CR>")
map("n", "<leader>ma", "<cmd>Telescope marks<CR>")
map("n", "<leader>dg", "<cmd>Telescope diagnostics<CR>")

Telescope Find Word

Code formatter: null-ls

I use null-ls for code formatting and extra diagnostics The config example as following:

require("mason").setup()
local null_ls = require("null-ls")
null_ls.setup({
	sources = {
		-- Replace these with the tools you want to install
		-- make sure the source name is supported by null-ls
		-- https://github.com/jose-elias-alvarez/null-ls.nvim/blob/main/doc/BUILTINS.md
		null_ls.builtins.formatting.black,
		null_ls.builtins.formatting.stylua,
		null_ls.builtins.formatting.shfmt,
		null_ls.builtins.diagnostics.hadolint,
		null_ls.builtins.diagnostics.selene,
		null_ls.builtins.diagnostics.shellcheck,
		null_ls.builtins.diagnostics.ruff,
		null_ls.builtins.diagnostics.mypy,
	},
})
-- See mason-null-ls.nvim's documentation for more details:
-- https://github.com/jay-babu/mason-null-ls.nvim#setup
require("mason-null-ls").setup({
	ensure_installed = {
		"shellcheck",
		"selene",
		"hadolint",
		"black",
		"shfmt",
		"stylua",
		"ruff",
		"mypy",
		"debugpy",
		"bash-debug-adapter",
	},
	automatic_installation = true,
})

Git markers: gitsigns

gitsign

lewis6991/gitsigns.nvim provides git decorations for current buffers. It helps you know which lines are currently changed. It works out of the box.

I made some configuration and hotkey to suite my usage

local opts = {
	signs = {
		add = { text = "│" },
		change = { text = "│" },
		delete = { text = "󰍵" },
		topdelete = { text = "‾" },
		changedelete = { text = "~" },
		untracked = { text = "│" },
	},
	on_attach = function(bufnr)
		local gs = package.loaded.gitsigns

		local function map(mode, l, r, opts)
			opts = opts or {}
			opts.buffer = bufnr
			vim.keymap.set(mode, l, r, opts)
		end

		-- Navigation
		map("n", "]c", function()
			if vim.wo.diff then
				return "]c"
			end
			vim.schedule(function()
				gs.next_hunk()
			end)
			return "<Ignore>"
		end, { expr = true })

		map("n", "[c", function()
			if vim.wo.diff then
				return "[c"
			end
			vim.schedule(function()
				gs.prev_hunk()
			end)
			return "<Ignore>"
		end, { expr = true })

		-- Actions
		map("n", "<leader>hs", gs.stage_hunk)
		map("n", "<leader>hr", gs.reset_hunk)
		map("v", "<leader>hs", function()
			gs.stage_hunk({ vim.fn.line("."), vim.fn.line("v") })
		end)
		map("v", "<leader>hr", function()
			gs.reset_hunk({ vim.fn.line("."), vim.fn.line("v") })
		end)
		map("n", "<leader>hS", gs.stage_buffer)
		map("n", "<leader>hu", gs.undo_stage_hunk)
		map("n", "<leader>hR", gs.reset_buffer)
		map("n", "<leader>hp", gs.preview_hunk)
		map("n", "<leader>hb", function()
			gs.blame_line({ full = true })
		end)
		map("n", "<leader>tb", gs.toggle_current_line_blame)
		map("n", "<leader>hd", gs.diffthis)
		map("n", "<leader>hD", function()
			gs.diffthis("~")
		end)
		map("n", "<leader>td", gs.toggle_deleted)

		-- Text object
		map({ "o", "x" }, "ih", ":<C-U>Gitsigns select_hunk<CR>")
	end,
}

require("gitsigns").setup(opts)

Undotree - my favorite extension

Undotree

The undo history visualizer for VIM Install it with

{ "mbbill/undotree" },

should work out of box

Debugging

DAP

Im currently using mfussenegger/nvim-dap for debugging, thanks to Microsoft Debug Adapter Protocol

local dap = require("dap")
local dapui = require("dapui")
-- bash debug
dap.adapters.bashdb = {
	type = "executable",
	command = vim.fn.stdpath("data") .. "/mason/packages/bash-debug-adapter/bash-debug-adapter",
	name = "bashdb",
}

dap.configurations.sh = {
	{
		type = "bashdb",
		request = "launch",
		name = "Launch file",
		showDebugOutput = true,
		pathBashdb = vim.fn.stdpath("data") .. "/mason/packages/bash-debug-adapter/extension/bashdb_dir/bashdb",
		pathBashdbLib = vim.fn.stdpath("data") .. "/mason/packages/bash-debug-adapter/extension/bashdb_dir",
		trace = true,
		file = "${file}",
		program = "${file}",
		cwd = "${workspaceFolder}",
		pathCat = "cat",
		pathBash = "/bin/bash",
		pathMkfifo = "mkfifo",
		pathPkill = "pkill",
		args = {},
		env = {},
		terminalKind = "integrated",
	},
}

dapui.setup()

-- use nvim-dap events to open and close the windows automatically
dap.listeners.after.event_initialized["dapui_config"] = function()
	dapui.open()
end
dap.listeners.before.event_terminated["dapui_config"] = function()
	dapui.close()
end
dap.listeners.before.event_exited["dapui_config"] = function()
	dapui.close()
end

with some keybind:

-- nvim-dap --
function _G.debug_python()
	require("dap-python").test_method()
end

keymap.set("n", "<leader>b", ":lua require'dap'.toggle_breakpoint()<CR>")
keymap.set("n", "<leader>B", ":lua require'dap'.set_breakpoint(vim.fn.input('Breakpoint condition: '))<CR>")
keymap.set("n", "<leader>db", ":lua require'dapui'.toggle()<CR>")
api("n", "<leader>dpr", ":call v:lua.debug_python()<CR>", { noremap = true, silent = true })

Finally, to combime all of this, you could add in your root init.lua file:

local colorscheme = vim.cmd.colorscheme
local fn = vim.fn

-- Enable the Lua loader byte-compilation cache.
if fn.has("nvim-0.9") == 1 then
	vim.loader.enable()
end

require("custom.options")
require("custom.keymaps")
require("custom.lazy") -- Plugin Manager

colorscheme("moonfly")
                                        ...

That’s pretty much it! I hope it’s helpful for improving your Neovim environment.