Neovim Complete Setup - Setting up Neovim From Scratch
A lot of people have asked about my Vim setup: how I use it and what configurations I have made. That's why, in this post, we will start from scratch and configure the Vim editor exactly as I have, line by line.
Here, we will focus solely on configuring the editor from start to finish. Additionally, I must mention that Vim is not the best editor if you are just starting programming or if you need to complete a task quickly. If you're a beginner, please download, for example, VS Code, use it, and focus on learning the programming language.
It makes sense to spend time configuring Vim only when you want to improve your coding workflow and already have a lot of experience, as well as some time to invest in learning and configuring Vim.
Terminal
So, what am I using overall? Since I am on a Mac, I use the Alacritty terminal.
This is how it looks, and as you can see, it is available on all operating systems. The main point is that it is blazingly fast. It doesn't have anything built-in; no tabs, no splits, nothing.
Tmux
Inside my terminal, I use Tmux. What is Tmux? It's a terminal multiplexer, which means it allows you to create tabs and splits.
The main question from students is typically, why can't I just download iTerm and use it like everyone else? It also has tabs and splits.
The main point is that Tmux saves your session. What does this mean? Just imagine that I opened my project and typed something in my code. Then, at some point, I close my Tmux session. Now, I am not in a session at all, and everything is closed.
By simply typing t
, which brings back Tmux, I am directly returned to the same state as before. This means all tabs and splits are there, all projects are still running, and even my cursor is in exactly the same place as it was previously.
The main point of Tmux is that it saves sessions in memory, and you can simply jump back to them. This functionality isn't available within a standard terminal, but Tmux integrates smoothly with iTerm, bringing tabs and splits to a console that lacks them.
Vim
Let's discuss Vim. I'm not using plain Vim; I'm using Neovim, a fork of Vim that's better developed and has more features.
Previously, they didn't have many differences, but nowadays Neovim supports Lua as a programming language for writing plugins. Lua plugins are easier to create compared to Vimscript plugins, which was the language we used previously. Lua is an extremely simple language, similar to JavaScript for me. That's why I use Neovim inside my terminal, as you've already seen in all my videos and posts.
Starting From Scratch
Now, I want to remove my entire Neovim configuration. I am currently in the folder ~/.config/nvim
, and I've deleted all files inside it.
When I open it, this is how it looks, completely empty, with nothing configured.
The first thing we need to do is create a file called init.lua
, which will serve as our entry point.
touch init.lua
We can reopen Neovim by typing nvim
and begin writing our configuration. What we want to do is to require the folder where we will store all our configurations.
require('ejiqpep.core')
In our case here, I write require
followed by my username. Typically, people write their username in this part. This single line will require a Lua file from the lua
folder. We want to create a new folder here named lua
, as it will automatically load any Lua file from this folder.
mkdir lua
mkdir lua/ejiqpep
mkdir lua/ejiqpep/core
mkdir lua/ejiqpep/plugins
Now I want to navigate into this username directory and create two subdirectories. The first will be core
, and the second will be plugins
.
If we open the init.lua
file, we'll encounter an error that lua/core
cannot be opened. This is expected because we haven't created any files inside this directory yet.
touch lua/ejiqpep/core/init.lua
The concept is that within the initial init.lua
file, we require the init.lua
file from our core
directory. Inside core
, we intend to store Neovim settings and our key mappings.
touch lua/ejiqpep/core/settings.lua
touch lua/ejiqpep/core/keymaps.lua
Now, inside our init.lua
file within the core
directory, we need to require both of these files.
<!-- lua/ejiqpep/core/init.lua -->
require ('ejiqpep.core.keymaps')
require ('ejiqpep.core.settings')
Now, when I type nvim
, we don't encounter any errors because all our required files were successfully required. Yes, they're completely empty and don't do anything, but that's fine for now.
Lazy.nvim
It is time to configure a package manager.
<!-- init.lua -->
require('ejiqpep.core')
require('ejiqpep.lazy')
Now, in our main file, I want to require a lazy file. This file is a plugin manager called lazy.nvim
that we will use.
But we haven't created this lazy.lua
file yet.
touch lua/ejiqpep/lazy.nvim
Let's configure the lazy package manager now.
<!-- lua/ejiqpep/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)
require("lazy").setup({ { import = "ejiqpep.plugins" } }, {
checker = {
enabled = false,
notify = false,
},
change_detection = {
notify = false,
},
})
This is the default configuration that you can find on their GitHub page. The first part of the file downloads lazy.nvim
if it's missing. The second part is about requiring all our plugins. As you can see, it will import all files from the ejiqpep.plugins
directory.
We don't need to individually import each plugin. They will all be loaded automatically.
Neovim will throw an error when we start it now because we haven't created any plugins and it can't load anything. But that's fine for now.
Color scheme
Let's configure colors for our editor.
touch lua/ejiqpep/plugins/colors.lua
Here, we will write all the logic related to coloring our editor.
<!-- lua/ejiqpep/plugins/colors.lua -->
return {
"ellisonleao/gruvbox.nvim",
priority = 1000,
config = function()
require("gruvbox").setup({})
vim.cmd([[
colorscheme gruvbox
]])
end,
}
This Lua code will serve as the structure for all our plugins. We return an object with configuration. Here, you can see the name of the repository that will be installed.
It's a Gruvbox color scheme that I'm using. Additionally, we provide priority
and config
. Priority defines when our plugins must be loaded, and config
is a function where we typically configure the package. In our case here, we call a setup
function and set gruvbox
as our color scheme.
If we restart Neovim, our Gruvbox theme is applied.
After the restart, you'll see a window from lazy.nvim
. It will automatically download any missing plugins, such as the gruvbox plugin in our case.
File Tree
Before we continue with installing other packages, I'd like to show you the file structure of our configuration.
This is the finalized structure with all installed plugins. Here, you can see a clear separation between core
and plugins
. Core
contains keymaps
and settings
, while in plugins
we store all our plugin configurations.
Nvim Settings
The next thing I want to do is go through all my settings in Neovim.
<!-- lua/ejiqpep/core/settings.lua -->
-- Set <space> as the leader key
vim.g.mapleader = " "
vim.g.maplocalleader = " "
-- Set highlight on search
vim.o.hlsearch = true
-- Make line numbers default
vim.wo.number = true
-- Enable mouse mode
vim.o.mouse = "a"
-- Sync clipboard between OS and Neovim.
vim.o.clipboard = "unnamedplus"
-- Enable break indent
vim.o.breakindent = true
-- No swap files
vim.opt.swapfile = false
vim.opt.backup = false
vim.opt.writebackup = false
-- Save undo history
vim.o.undofile = true
vim.opt.undodir = os.getenv("HOME") .. "/.vim/undodir"
-- Case-insensitive searching UNLESS \C or capital in search
vim.o.ignorecase = true
vim.o.smartcase = true
-- Keep signcolumn on by default
vim.wo.signcolumn = "yes"
-- Decrease update time
vim.o.updatetime = 250
vim.o.timeoutlen = 300
-- Set completeopt to have a better completion experience
vim.o.completeopt = "menuone,noselect"
-- NOTE: You should make sure your terminal supports this
vim.o.termguicolors = true
-- Don't show modes (insert/visual)
vim.opt.showmode = false
-- " Open splits on the right and below
vim.opt.splitbelow = true
vim.opt.splitright = true
-- " update vim after file update from outside
vim.opt.autoread = true
-- " Indentation
vim.opt.autoindent = true
vim.opt.smartindent = true
vim.opt.smarttab = true
vim.opt.tabstop = 2
vim.opt.softtabstop = 2
vim.opt.shiftwidth = 2
-- " Always use spaces insted of tabs
vim.opt.expandtab = true
-- " Don't wrap lines
vim.opt.wrap = true
-- " Wrap lines at convenient points
vim.opt.linebreak = true
-- " Show line breaks
vim.opt.showbreak = "↳"
-- " https://github.com/vim/vim/blob/master/runtime/doc/russian.txt
-- " Enable hotkeys for Russian layout
vim.opt.langmap =
"ФИСВУАПРШОЛДЬТЩЗЙКЫЕГМЦЧНЯ;ABCDEFGHIJKLMNOPQRSTUVWXYZ,фисвуапршолдьтщзйкыегмцчня;abcdefghijklmnopqrstuvwxyz"
-- " Start scrolling when we'are 8 lines aways from borders
vim.opt.scrolloff = 8
vim.opt.sidescrolloff = 15
vim.opt.sidescroll = 5
-- " This makes vim act like all other editors, buffers can
-- " exist in the background without being in a window.
vim.opt.hidden = true
-- " Add the g flag to search/replace by default
vim.opt.gdefault = true
-- Lazy redraw
vim.o.lazyredraw = true
- Map our Leader key to Space. This is our main key to make keybinds.
- Highlight search in the file.
- Show line numbers.
- Disable the mouse completely.
- Enable copying between Neovim and the system clipboard.
- Wrap our lines.
- Disable swap files but keep undo history.
- Decrease update time to make Neovim faster.
- Configure how and where we open splits.
- Change the indentation.
- Adjust how we scroll on the screen.
Feel free to copy these settings and update them later according to your personal preferences.
Keymaps
The next thing we want to configure is the hotkeys that we want to use inside Neovim.
<!-- lua/ejiqpep/core/keymaps.lua -->
vim.g.mapleader = " "
vim.g.maplocalleader = " "
-- Keymaps for better default experience
vim.keymap.set({ "n", "v" }, "<Space>", "<Nop>", { silent = true })
-- Space + s saves the file
vim.keymap.set("n", "<Leader>s", ":write<CR>", { silent = true })
-- Move normally between wrapped lines
vim.keymap.set("n", "k", "v:count == 0 ? 'gk' : 'k'", { expr = true, silent = true })
vim.keymap.set("n", "j", "v:count == 0 ? 'gj' : 'j'", { expr = true, silent = true })
-- Move to first symbol on the line
vim.keymap.set("n", "H", "^")
-- Move to last symbol of the line
vim.keymap.set("n", "L", "$")
-- Shift + q - Quit
vim.keymap.set("n", "Q", "<C-W>q")
-- vv - Makes vertical split
vim.keymap.set("n", "vv", "<C-W>v")
-- ss - Makes horizontal split
vim.keymap.set("n", "ss", "<C-W>s")
-- Quick jumping between splits
vim.keymap.set("n", "<C-j>", "<C-w>j")
vim.keymap.set("n", "<C-k>", "<C-w>k")
vim.keymap.set("n", "<C-h>", "<C-w>h")
vim.keymap.set("n", "<C-l>", "<C-w>l")
-- Indenting in visual mode (tab/shift+tab)
vim.keymap.set("v", "<Tab>", ">gv")
vim.keymap.set("v", "<S-Tab>", "<gv")
-- Move to the end of yanked text after yank and paste
vim.cmd("vnoremap <silent> y y`]")
vim.cmd("vnoremap <silent> p p`]")
vim.cmd("nnoremap <silent> p p`]")
-- Space + Space to clean search highlight
vim.keymap.set("n", "<Leader>h", ":noh<CR>", { silent = true })
-- Fixes pasting after visual selection.
vim.keymap.set("v", "p", '"_dP')
In total, I have fewer than 50 lines of custom keymaps. By default, Neovim has an extensive amount of keymaps, so you don't really need lots of custom ones. As you can see, we use vim.keymap.set
to create a keymap. Inside, we provide a mode in which we want to apply it, then a hotkey, and a command that must be executed. Let's check what custom keymaps I have.
Here are the custom keymaps:
- Pressing Space followed by s saves the current file.
- Pressing k or j will move the cursor without jumping between lines.
- Pressing H and L jumps to the beginning and end of the line.
- Pressing Q closes the current buffer.
- Pressing vv and ss creates vertical and horizontal splits.
- Using Ctrl + k, Ctrl + j, Ctrl + h, Ctrl + l jumps between splits in different directions.
- Pressing Tab and Shift + Tab indents code in virtual mode.
- Pressing y and p moves the cursor to the end of pasted or copied text, similar to other editors.
- Pressing Space followed by h will clear search highlights.
Now, the only thing left is to go through all the plugins I have installed and show you what they do.
Commenting Code
The first plugin is for commenting code.
touch lua/ejiqpep/plugins/comment.lua
You can name the file whatever you want. What's most important is the GitHub URL you provide inside.
<!-- lua/ejiqpep/plugins/comment.lua -->
return {
"numToStr/Comment.nvim",
event = { "BufReadPre", "BufNewFile" },
dependencies = {
"JoosepAlviste/nvim-ts-context-commentstring",
},
config = function()
local comment = require("Comment")
local ts_context_commentstring = require("ts_context_commentstring.integrations.comment_nvim")
comment.setup({
-- for commenting tsx and jsx files
pre_hook = ts_context_commentstring.create_pre_hook(),
})
end,
}
As you can see here, we used an event
. It means that this plugin will be loaded when we open a file or create a new file. Additionally, we provided the dependencies for this package and a config
function. Now, we can use gcc
, for example, to comment a line or gcG
to comment the whole file.
Fuzzy finder
The next package that I want to show you is a fuzzy search tool. It will help us find any files or text that we need quickly and efficiently.
touch lua/ejiqpep/plugins/fzf.lua
Now I will copy and paste everything inside so we can check it.
<!-- lua/ejiqpep/plugins/fzf.lua -->
return {
"ibhagwan/fzf-lua",
event = "VeryLazy",
config = function()
require("fzf-lua").setup({
colorscheme = "gruvbox",
winopts = {
fullscreen = true,
preview = {
layout = "vertical",
vertical = "up:45%", -- up|down:size
},
},
fzf_opts = {
["--keep-right"] = "", -- https://github.com/ibhagwan/fzf-lua/issues/269
["--layout"] = "default",
-- ["--ansi"] = false,
},
keymap = {
fzf = {
["ctrl-r"] = "select-all+accept", -- https://github.com/ibhagwan/fzf-lua/issues/324
},
},
files = {
git_icons = false,
file_icons = false,
},
})
vim.keymap.set("n", "<c-P>", "<cmd>lua require('fzf-lua').files()<CR>", { silent = true })
vim.keymap.set("n", "<leader>b", "<cmd>lua require('fzf-lua').buffers()<CR>", { desc = "Fuzzy find recent files" })
vim.keymap.set(
"n",
"<leader>/",
"<cmd>lua require('fzf-lua').live_grep_resume()<CR>",
{ desc = "Find string in cwd" }
)
vim.keymap.set("n", "gr", ":FzfLua lsp_references<CR>")
end,
}
First of all, we set up how the fzf windows will look and which color scheme they will use. Then, I create several keybinds. Ctrl + p
finds files in the project, Space + b
shows a fuzzy finder with the list of opened buffers, and Space + /
gives you live search in the project. Additionally, I use gr
to find references through LSP, but we will talk about LSP config later.
This is how file search looks like with a preview.
If you've been exploring Vim packages for some time, you might wonder, "Why don't we use Telescope instead of fzf?" Telescope is the most popular fuzzy finder, but it is extremely slow on large projects, so it doesn't fit my workflow.
Indentation
The next plugin that I use is for displaying indentation in the file.
touch lua/ejiqpep/plugins/indent.lua
Let's paste the config inside.
<!-- lua/ejiqpep/plugins/indent.lua -->
return {
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
opts = {},
config = function()
require("ibl").setup({
scope = { enabled = false },
indent = { char = "|" },
})
end
}
It looks the same as other configs, but here we specify a branch to use with the main
property.
You can see these vertical lines which show the indentation in the code. That's what this package does.
Status line
Another package is Lua-Line, which will provide a status line for us.
touch lua/ejiqpep/plugins/lualine-nvim.lua
Let's add our config
<!-- lua/ejiqpep/plugins/lualine-nvim.lua -->
return {
"nvim-lualine/lualine.nvim",
config = function()
local colors = {
default_background = "#504945",
default_text = "#EBDBB2",
modified_background = "#AA4542",
saved_background = "#84A598",
}
local theme = {
normal = {
a = { bg = colors.saved_background, fg = colors.default_text },
b = { bg = colors.default_background, fg = colors.default_text },
c = { fg = colors.default_text, bg = colors.default_background },
z = { fg = colors.default_text, bg = colors.default_background },
},
}
local function modified_text()
if vim.bo.modified then
return "✘"
end
return " "
end
require("lualine").setup({
options = {
theme = theme,
},
sections = {
lualine_a = {
{
modified_text,
separator = { right = "" },
padding = {
left = 3,
right = 3,
},
color = function()
if vim.bo.modified then
return { bg = colors.modified_background, fg = colors.default_text }
end
end,
},
},
lualine_b = {
{ "filename", file_status = false, path = 4 },
},
lualine_c = {},
lualine_x = {},
lualine_y = {},
lualine_z = {},
},
})
end,
}
In this configuration, I provide a gruvbox theme and remove all status symbols except for showing if the file is modified or not and its filename.
This is how it looks now.
Eslint and Prettier
Another package that I'm using is a none-ls
package, which allows me to install different formatting tools. I mostly use just Prettier and ESLint, but you can configure tools for any language there.
touch lua/ejiqpep/plugins/none-ls.lua
Let's add a config here.
<!-- lua/ejiqpep/plugins/none-ls.lua -->
return {
"nvimtools/none-ls.nvim",
lazy = true,
event = { "BufReadPre", "BufNewFile" },
dependencies = { 'nvim-lua/plenary.nvim' },
config = function()
local null_ls = require("null-ls")
local null_ls_utils = require("null-ls.utils")
local formatting = null_ls.builtins.formatting
local diagnostics = null_ls.builtins.diagnostics
-- to setup format on save
local augroup = vim.api.nvim_create_augroup("LspFormatting", {})
null_ls.setup({
root_dir = null_ls_utils.root_pattern(".null-ls-root", "Makefile", ".git", "package.json"),
sources = {
formatting.prettierd.with({
disabled_filetypes = {
"markdown",
"md",
},
}),
formatting.stylua,
diagnostics.eslint_d.with({
condition = function(utils)
return utils.root_has_file({ ".eslintrc.js", ".eslintrc.cjs" })
end,
}),
},
-- configure format on save
on_attach = function(current_client, bufnr)
if current_client.supports_method("textDocument/formatting") then
vim.api.nvim_clear_autocmds({ group = augroup, buffer = bufnr })
vim.api.nvim_create_autocmd("BufWritePre", {
group = augroup,
buffer = bufnr,
callback = function()
vim.lsp.buf.format({
filter = function(client)
-- only use null-ls for formatting instead of lsp server
return client.name == "null-ls"
end,
bufnr = bufnr,
})
end,
})
end
end,
})
end,
}
Here, we enable Prettier and ESLint and set them up to be called on file save.
Mason
Now we're coming to the Language Server. It's the most powerful tool as it allows us to implement language validation, import dependencies, go to definition, renaming, and much more. In order for it to work, we need several things.
First of all is Mason, which allows us to install LSP servers.
touch lua/ejiqpep/plugins/mason.lua
Now we need to create our config.
<!-- lua/ejiqpep/plugins/mason.lua -->
return {
"williamboman/mason.nvim",
dependencies = {
"williamboman/mason-lspconfig.nvim",
"WhoIsSethDaniel/mason-tool-installer.nvim",
},
config = function()
local mason = require("mason")
local mason_lspconfig = require("mason-lspconfig")
local mason_tool_installer = require("mason-tool-installer")
mason.setup({})
mason_lspconfig.setup({
ensure_installed = {
"tsserver",
"angularls",
"lua_ls",
"html",
"cssls",
},
automatic_installation = true,
})
mason_tool_installer.setup({
ensure_installed = {
"prettier",
"stylua",
"eslint_d",
"prettierd",
},
})
end,
}
Here, I installed the TypeScript server, Angular server, Lua server, HTML and CSS server. Additionally, I installed formatting tools like Prettier, ESLint, and Stylua.
When you restart Neovim, Mason will open a window and install all servers.
File Tree
Another package that we need is a file tree. Here, we will use the nvim-tree
package for that.
touch lua/ejiqpep/plugins/nvim-tree.lua
Let's create a config.
<!-- lua/ejiqpep/plugins/nvim-tree.lua -->
return {
"nvim-tree/nvim-tree.lua",
dependencies = { "antosha417/nvim-lsp-file-operations" },
enabled = true,
config = function()
require("lsp-file-operations").setup()
-- recommended settings from nvim-tree documentation
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
-- change color for arrows in tree to light blue
vim.cmd([[ highlight NvimTreeFolderArrowClosed guifg=#3FC5FF ]])
vim.cmd([[ highlight NvimTreeFolderArrowOpen guifg=#3FC5FF ]])
local function natural_cmp(left, right)
-- Prioritize directories over files.
if left.type ~= "directory" and right.type == "directory" then
return false
elseif left.type == "directory" and right.type ~= "directory" then
return true
end
left = left.name:lower()
right = right.name:lower()
if left == right then
return false
end
for i = 1, math.max(string.len(left), string.len(right)), 1 do
local l = string.sub(left, i, -1)
local r = string.sub(right, i, -1)
if type(tonumber(string.sub(l, 1, 1))) == "number" and type(tonumber(string.sub(r, 1, 1))) == "number" then
local l_number = tonumber(string.match(l, "^[0-9]+"))
local r_number = tonumber(string.match(r, "^[0-9]+"))
if l_number ~= r_number then
return l_number < r_number
end
elseif string.sub(l, 1, 1) ~= string.sub(r, 1, 1) then
return l < r
end
end
end
require("nvim-tree").setup({
sort = {
sorter = function(nodes)
table.sort(nodes, natural_cmp)
end,
},
view = {
width = 35,
},
renderer = {
indent_markers = {
enable = true,
},
icons = {
glyphs = {
folder = {
arrow_closed = "", -- arrow when folder is closed
arrow_open = "", -- arrow when folder is open
},
},
-- show = {
-- file = false,
-- folder = false,
-- folder_arrow = true,
-- git = false,
-- modified = false,
-- diagnostics = false,
-- bookmarks = false,
-- },
},
},
-- disable window_picker for
-- explorer to work well with
-- window splits
actions = {
open_file = {
window_picker = {
enable = false,
},
},
},
filters = {
custom = { ".DS_Store" },
},
git = {
enable = false,
},
diagnostics = {
enable = true,
show_on_dirs = true,
icons = {
hint = "",
info = "",
warning = "",
error = "",
},
},
})
vim.keymap.set("n", "<leader>ee", "<Cmd>NvimTreeToggle<CR>", { desc = "Toggle file explorer" })
vim.keymap.set(
"n",
"<leader>ef",
"<cmd>NvimTreeFindFileToggle<CR>",
{ desc = "Toggle file explorer on current file" }
)
vim.keymap.set("n", "<leader>ec", "<cmd>NvimTreeCollapse<CR>", { desc = "Collapse file explorer" }) -- collapse file explorer
vim.keymap.set("n", "<leader>er", "<cmd>NvimTreeRefresh<CR>", { desc = "Refresh file explorer" }) -- refresh file explorer
end,
}
Here, I set up how it will look and added several keybinds. Space + ee
toggles the file tree. Space + ef
opens the tree and brings the current file into focus, while Space + ec
collapses the whole tree.
You can now see our file structure on the left.
Treesitter
Treesitter is a helper plugin that many other plugins use. It allows access to all symbols and text parts in the file.
touch lua/ejiqpep/plugins/treesitter.lua
Now let's create a config.
<!-- lua/ejiqpep/plugins/treesitter.lua -->
return {
"nvim-treesitter/nvim-treesitter",
event = { "BufReadPost", "BufNewFile" },
build = ":TSUpdate",
enabled = true,
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = {
"vimdoc",
"javascript",
"typescript",
"lua",
"ruby",
"html",
"tsx",
"bash",
"markdown",
"markdown_inline",
},
indent = { enable = true },
highlight = {
enable = true,
use_languagetree = true,
-- disable = { "markdown" },
},
})
end,
-- enable nvim-ts-context-commentstring plugin for commenting tsx and jsx
require("ts_context_commentstring").setup({}),
}
Inside the config, we install all syntax highlighting for the file types that we need.
As you can see now, it looks much better.
LSP
The last three plugins that we need to install work together and implement LSP support. With Mason, we already installed servers for LSP, but now we need a client part.
touch lua/ejiqpep/plugins/nvim-cmp.lua
touch lua/ejiqpep/plugins/nvim-lspconfig.lua
touch lua/ejiqpep/plugins/nvim-autopairs.lua
Nvim-cmp
is responsible for autocomplete for LSP.
<!-- lua/ejiqpep/plugins/nvim-cmp.lua -->
return {
"hrsh7th/nvim-cmp",
event = "InsertEnter",
dependencies = {
"hrsh7th/cmp-buffer", -- source for text in buffer
"hrsh7th/cmp-path", -- source for file system paths
"L3MON4D3/LuaSnip", -- snippet engine
"saadparwaiz1/cmp_luasnip", -- for autocompletion
"rafamadriz/friendly-snippets", -- useful snippets
"onsails/lspkind.nvim", -- vs-code like pictograms
},
enabled = true,
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
local lspkind = require("lspkind")
local has_words_before = function()
unpack = unpack or table.unpack
local line, col = unpack(vim.api.nvim_win_get_cursor(0))
return col ~= 0 and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match("%s") == nil
end
-- loads vscode style snippets from installed plugins (e.g. friendly-snippets)
require("luasnip.loaders.from_vscode").lazy_load()
cmp.setup({
completion = {
completeopt = "menu,menuone,preview,noselect",
},
snippet = { -- configure how nvim-cmp interacts with snippet engine
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
mapping = cmp.mapping.preset.insert({
["<C-k>"] = cmp.mapping.select_prev_item(), -- previous suggestion
["<C-j>"] = cmp.mapping.select_next_item(), -- next suggestion
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(), -- show completion suggestions
["<C-e>"] = cmp.mapping.abort(), -- close completion window
["<CR>"] = cmp.mapping.confirm({ select = false }),
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
-- You could replace the expand_or_jumpable() calls with expand_or_locally_jumpable()
-- that way you will only jump inside the snippet region
elseif luasnip.expand_or_jumpable() then
luasnip.expand_or_jump()
elseif has_words_before() then
cmp.complete()
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 for autocompletion
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" }, -- snippets
{ name = "buffer" }, -- text within current buffer
{ name = "path" }, -- file system paths
}),
-- configure lspkind for vs-code like pictograms in completion menu
formatting = {
format = lspkind.cmp_format({
maxwidth = 50,
ellipsis_char = "...",
}),
},
})
end,
}
Here, we configure the settings for autocomplete, snippets, and which keys we are using for selecting autocomplete items.
Nvim-lspconfig
is responsible for displaying errors on the client inside Vim.
<!-- lua/ejiqpep/plugins/nvim-lspconfig.lua -->
return {
"neovim/nvim-lspconfig",
event = { "BufReadPre", "BufNewFile" },
dependencies = {
"hrsh7th/cmp-nvim-lsp",
},
enabled = true,
config = function()
local lspconfig = require("lspconfig")
local util = require("lspconfig.util")
local cmp_nvim_lsp = require("cmp_nvim_lsp")
-- Disable inline error messages
vim.diagnostic.config({
virtual_text = false,
float = {
border = "single",
},
})
-- Add border to floating window
vim.lsp.handlers["textDocument/signatureHelp"] =
vim.lsp.with(vim.lsp.handlers.hover, { border = "single", silent = true })
vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(vim.lsp.handlers.hover, { border = "single", silend = true })
-- Make float window transparent start
local set_hl_for_floating_window = function()
vim.api.nvim_set_hl(0, "NormalFloat", {
link = "Normal",
})
vim.api.nvim_set_hl(0, "FloatBorder", {
bg = "none",
})
end
set_hl_for_floating_window()
vim.api.nvim_create_autocmd("ColorScheme", {
pattern = "*",
desc = "Avoid overwritten by loading color schemes later",
callback = set_hl_for_floating_window,
})
-- Make float window transparent end
local on_attach = function(client, bufnr)
vim.keymap.set(
"n",
"K",
vim.lsp.buf.hover,
{ buffer = bufnr, desc = "Show documentation for what is under cursor" }
)
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, { buffer = bufnr, desc = "Smart rename" })
vim.keymap.set(
{ "n", "v" },
"gf",
vim.lsp.buf.code_action,
{ buffer = bufnr, desc = "See available code actions" }
)
vim.keymap.set(
"n",
"<leader>d",
vim.diagnostic.open_float,
{ buffer = bufnr, desc = "Show diagnostics for line" }
)
-- vim.keymap.set("n", "gR", "<cmd>Telescope lsp_references<CR>", {buffer = bufnr, desc = 'Show definition, references'})
vim.keymap.set("n", "gd", vim.lsp.buf.definition, { buffer = bufnr, desc = "Go to definition" })
end
local capabilities = cmp_nvim_lsp.default_capabilities()
local signs = { Error = "✖", Warn = "", Hint = "", Info = "" }
for type, icon in pairs(signs) do
local hl = "DiagnosticSign" .. type
vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = "" })
end
-- configure typescript server with plugin
lspconfig["tsserver"].setup({
capabilities = capabilities,
on_attach = on_attach,
})
-- configure html server
lspconfig["html"].setup({
capabilities = capabilities,
on_attach = on_attach,
})
-- configure angular server
lspconfig["angularls"].setup({
capabilities = capabilities,
on_attach = on_attach,
root_dir = util.root_pattern("angular.json", "project.json", "nx.json"),
})
-- configure lua server (with special settings)
lspconfig["lua_ls"].setup({
capabilities = capabilities,
on_attach = on_attach,
settings = { -- custom settings for lua
Lua = {
-- make the language server recognize "vim" global
diagnostics = {
globals = { "vim" },
},
workspace = {
-- make language server aware of runtime files
library = {
[vim.fn.expand("$VIMRUNTIME/lua")] = true,
[vim.fn.stdpath("config") .. "/lua"] = true,
},
},
},
},
})
-- configure css server
lspconfig["cssls"].setup({
capabilities = capabilities,
on_attach = on_attach,
})
end,
}
As you can see, we set up every language server that we need as well as hotkeys. For example, Shift + K
shows the error message in the tooltip on your text.
And last but not least is the Autopairs plugin.
<!-- lua/ejiqpep/plugins/nvim-autopairs.lua -->
return {
"windwp/nvim-autopairs",
event = "InsertEnter",
dependencies = {
"hrsh7th/nvim-cmp",
},
config = function()
local autopairs = require("nvim-autopairs")
autopairs.setup({
check_ts = true, -- enable treesitter
ts_config = {
lua = { "string" }, -- don't add pairs in lua string treesitter nodes
javascript = { "template_string" }, -- don't add pairs in javscript template_string treesitter nodes
java = false, -- don't check treesitter on java
},
})
-- import nvim-autopairs completion functionality
local cmp_autopairs = require("nvim-autopairs.completion.cmp")
-- import nvim-cmp plugin (completions plugin)
local cmp = require("cmp")
-- make autopairs and completion work together
cmp.event:on("confirm_done", cmp_autopairs.on_confirm_done())
end,
}
It automatically closes brackets and quotes correctly.
This is how LSP is working at the end. It can show errors and documentation for certain things.
So, this is how you set up Neovim from scratch.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done