Welcome back to YOUR possibly dev-life changing event! This is a straight continuation from Pt. 1 “But Why?”, I strongly suggest reading that first but it is completely optional. Let’s get down to the nitty-gritty stuff this time. In this post we will install Neovim from scratch, and by the end of it you’ll have a fully configured development setup to code the night away!
Installation
Installing Neovim is pretty straight forward, and there are plenty of distros that come prepackaged with many plugins and quality of life tools straight out of the box…but what we want is yet another DIY project, we aren’t chumps, are we?! If you aren’t a weakling, here is where you’re able to download and install Neovim for most operating systems. Follow the instructions acording to your OS before we move on.
Once installed, boot it up by running nvim in your terminal. If successfully installed, a Neovim instance should open up in your terminal like so:
Pretty bland, right? And don’t be afraid, you’re not stuck on this screen forever!
Hit : to pull up the command-line and write :q<enter> to quit out of that Neovim window. But we aren’t quitting! Let’s go through a few things before we dive back into our text-editor.
Configuration
Out the box, we don’t get a configuration file, pretty crazy right? With Neovim, we must create one ourselves. I am using a Linux distro, so commands will be Linux-based.
Let’s head back over to our terminal and create a configuration directory.
mkdir ~/.config/nvim && cd ~/.config/nvimThe above snippet creates our Neovim configuration directory and moves into it
Now, let’s create an init.lua file.
nvim .Opens up Neovim in our current directory
SHIFT + 5(%)Prompts the file creation command, where we can enter a filename we would like to create. Type init.lua
init.lua
So what does
init.luado, exactly?
In Neovim, the init.lua file is the entry point for your configuration when you use Lua instead of (or alongside) the traditional init.vim or vimrc file(s).
If we enter insert mode in our init.lua file(click i in the Neovim window) and write print("Hello world!") and save our file with :w and then source this file by hitting :so you should see the message printed out below the command line! (if you still don’t see it, type :messages)
Basically, every time you open up Neovim, it will run whatever is in your init.lua(or init.vim/vimrc if using Vimscript) first.
Splitting the config
Ok, so why would we want to split the configuration? The answer is simple: single-purpose files make it easier to organize, tweak, and grow your setup as you tailor your Neovim! The order of your config files doesn’t REALLY matter… but it CAN make a difference. Think of it like having separate drawers for socks, shirts, and pants. That organization makes it much easier for you to get ready, right? That’s what we’re going for with out configuration files! It just needs to make sense for us, as intuitive and easy to read as possible — I’ve shoved everything into my init.lua file in the past and trust me, spending time organizing things now is much, much easier, you’ll thank me later!
Now let’s create the folder/file structure for our configuration files!
Let’s cd over to ~/.config/nvim and create a new directory called lua, and within the lua directory, create a subdirectory called config. As the directory and subdirectory names state, this is where we’ll be creating our lua configuration files for Neovim!
~/.config/nvim/lua/configYour directory tree within your ~/.config/nvim should look like this:

Within the config subdirectory, we will create three essential files:
options.lua-> editor behavior and settingskeybinds.lua-> keyboard shortcuts (keymaps.lua works too if it makes more sense to you!)plugins.lua-> plugin manager and/or plugin list
options.lua
Let’s create a options.lua file in our config folder — you can do so however you’d like, I’ll be creating the file using nvim.
nvim options.lua # opens up nvim and creates the options.lua fileOnce the file is open, press i to enter insert mode and add print('our options file') and then hit <esc> + :w to write and save our options.lua file.
Now we need our init.lua file to point to our options.lua file. We can quickly jump from our options.lua file to the init.lua file by typing :e init.lua<enter>
Now inside our init file, enter insert mode once again and add this line:
require('config.options')Write and quit out of Neovim with :wq<enter> and then open Neovim back up again with nvim .. Try to familiarize your self with vim movements and use the hjkl keys to navigate instead of using the arrow keys!
Navigate back over to the options.lua file and let’s set up some options!
Add the following code to your file
vim.opt.number = true -- numbers the lines in your editor
vim.opt.cursorline = true -- shows which line your cursor is currently on
vim.opt.relativenumber = true -- numbers the lines relative to the line you are currently on
vim.opt.shiftwidth = 4 -- changes tab spacing to 4, change this to whatever your preference is!Write those changes with :w and then source it with :so to apply the changes without having to restart Neovim!
Your editor should look something like this:
Relative numbers and shiftwidth can be adjusted according to your preferences. I personally use vim.opt.relativenumber = true quite a bit to just around faster(e.g, using the screenshot as reference: You can hop over to the first line by hitting 2k to go up twice).
That’s it, simple and clean!
keybinds.lua
Let’s create our second file. If you aren’t already in the config subdirectory, cd into it with cd ~/.config/nvim/lua/config/ and with the following command, create the file within the directory:
nvim keybinds.lua # opens up nvim and creates the keybinds.lua fileHere is where we’ll create all of out keyboard shortcurts to make us zip around Neovim fast as f-!
In Vim there is a leader which is basically a customizable prefix key used to create your own keyboard shortcuts. When you define a keybind/mapping with the leader key, it looks like this: <leader>e -> this means press leader, then e.
By default, the leader key is set to the \ key, but let’s change that.
Within the keybinds.lua file, write and save the following:
vim.g.mapleader = ' ' -- this maps the leader key to the spacebar
vim.keymap.set('n', '<leader>cd', vim.cmd.Ex) -- "n" here applies the mapping in normal mode, "<leader>cd" is the key combination and "vim.cmd.Ex" open the file exploreresc to get out of insert mode and save with :w and then write :e init.lua to hop back over to our init file to include the keybinds file we just created.
Your init.lua file should now look like this:
require('config.options')
require('config.keybinds')Again, save with :w and source this file so we don’t have to restart Neovim with :so and let’s test out our first keybinding!
Hit space and then cd on your keyboard to open up the netrw file explorer!
Pretty sweet, right? We’ll come back to this a little later, let’s jump to the fun stuff for now: plugins!
plugins.lua
Neovim is powerful on its own, but what makes it truely come to life is with the use of plugins! Some plugins were created by the maintainers of Neovim, but most of them come from people like you – curious developers!
In order to manage our plugins, we’ll be using a plugin manager, very intuitive, I know! There are plenty of community created plugin managers, but we’ll be using lazy.nvim created by folke.
Lazy
Let’s create the file:
nvim ~/.config/nvim/lua/config/plugins.luaNow add this to the file:
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({
spec = {
-- import your plugins
{ import = "plugins" },
},
change_detection = { notify = false },
})That is the boilerplate pulled from the official documentation, with some slight modifications.
Ok, so let’s import our plugins.lua file! Head back to the init.lua with <leader>cd or :e init.lua
How about we import/require our lazy.lua file using Vim commands!?
Execute the following commands:
shift ventersVisual Modeand selects the whole lineyto yank the linepto paste it below the current linefkfinds the next occurance of a letter on the current line, in this case we skip over to the letter kcwentersInsert Modeand cuts the word- and then write
lazy
Concate these together and yet get this:
It can be a little confusing at first, but eventually it becomes second nature and you’ll be executing even more complex commands in a flash!
esc and :wq to save and quit. Open up Neovim again and you should see an error specifying that no specs were found for module “plugins”. This is expected since we haven’t created it yet. Let’s fix that!
Hit enter to continue and type :Lazy in order to start lazy. You should see this prompt:
Hit q to exit out of the prompt.
colors.lua
Now let’s create a directory called plugins within the .config/nvim/lua directory, this is where we’ll place all of the plugins we will be adding.
Once created, create a colors.lua file here — as the file name suggests, here is where we will place all of our plugins that affect our Neovim’s color theme!
Lazy expects each spec to return a table with plugins we wish to install. In this case, let’s use tiagovla’s tokyodark theme, add this to the file:
return {
{
"tiagovla/tokyodark.nvim",
lazy = false, -- makes sure we load this during startup
priority = 1000, -- make sure to load this before all the other start plugins
config = function()
vim.cmd.colorscheme "tokyodark" -- sets the colorscheme to tokyodark
end,
}
}Now hit :wq and start up Neovim again and Lazy will install the newly added plugin and apply it straight away! You should also notice that in the .config/nvim directory we not have a lazy-lock.json file. After every plugin update or installation, lazy.nvim records the exact versions of all installed plugins in the lazy-lock.json file. This is pretty much like how Node.js and its package-lock.json and Ruby’s Gemfile.lock work!
Installing plugins isn’t as hard as you thought, huh? Since most plugins are created by fellow developers, their documentation should always be your go-to whenever you want to snoop around and experiment too!
Before we move on, let’s add another plugin to our colors.lua file, lualine:
return {
{
"tiagovla/tokyodark.nvim",
priority = 1000,
config = function()
vim.cmd.colorscheme "tokyodark"
end
},
{
"nvim-lualine/lualine.nvim",
dependencies = {
"nvim-tree/nvim-web-devicons", -- imports icon dependency
},
opts = {
theme = "tokyodark", -- sets the lualine theme to tokyodark
}
},
}This plugin adds a line to the bottom part of our Neovim with a more aesthetically pleasing look! It is customizable, so if you want to edit yours in a any way, please reference their docs!
Let’s add another plugin, one of the most popular amongst Vim and Neovim users: Telescope!

Telescope
Just what is it, and why do we want to add it to our Neovim?
Telescope is:
- a modular, highly extendable fuzzy finder
- community driven, built on
neovimcore - builtin pickers, sorters and previewers
Let’s create a telescope.lua file within our .config/nvim/lua/plugins/ directory — this will be our telescope spec.
Within it, add the following code:
return {
'nvim-telescope/telescope.nvim', tag = '0.1.8',
dependencies = { 'nvim-lua/plenary.nvim' },
config = function()
local builtin = require('telescope.builtin')
vim.keymap.set('n', '<leader>ff', builtin.find_files, { desc = 'Telescope find files' })
vim.keymap.set('n', '<leader>fg', builtin.live_grep, { desc = 'Telescope live grep' })
vim.keymap.set('n', '<leader>fb', builtin.buffers, { desc = 'Telescope buffers' })
vim.keymap.set('n', '<leader>fh', builtin.help_tags, { desc = 'Telescope help tags' })
end
}Once you’ve added that, save and quit out of Neovim with :wq and open it back up again. Lazy will install telescope and add its default keybindings for some navigation.
Let’s try them out:
<leader>ff-> Find files with fuzzy finding!

<leader>fg-> full grep, greps through your directory

<leader>fb-> opens up your buffers, allowing you to select any open buffer you wish

<leader>fh-> help — super useful! All of the help files for your installed plugins can be found here

It’s no wonder this is one of the most popular plugins, the previews are easy on the eyes as well! With that we have a good starting point for telescope, Now let’s install Treesitter!
Treesitter
Another super popular plugin that provides:
- coding language parsers
- syntax highlighting
- indentation
- text objects
- code navigation
It does this by building a syntax tree of your code, resulting in more accurate and context-aware editing when compared to other traditional regex-based highlighting.
Useful, right? Ok, so let’s set it up!
Create a treesitter.lua file within out plugins directory(a.k.a our treesitter spec)
Add this code to the file:
return {
'nvim-treesitter/nvim-treesitter',
build = ":TSUpdate",
config = function()
local configs = require("nvim-treesitter.configs")
configs.setup({
highlight = { enable = true },
indent = { enable = true },
autoage = { enable = true },
ensure_installed = {
"lua", "ruby", "tsx", "typescript", "vim", "html", "javascript", "query", "vimdoc" -- add whatever coding language that you use in your day-to-day
},
auto_install = false,
})
end
}Edit the ensure_installed items with whatever language you use in your day-to-day coding! For more supposted languages, take a look at their docs!
If you’d like to automatically update the parsers to their latest versions, automate this by adding the following to your init.lua file:
require("lazy").setup({
{"nvim-treesitter/nvim-treesitter", branch = 'master', lazy = false, build = ":TSUpdate"}
})Save and quit again with :wq and open up Neovim again, treesitter should be installed now! Check by running the command :checkhealth, you should get the healthcheck prompt:
Another sweet command is :InspectTree, which opens up an interactive window that lets you visually inspect the syntax tree that is generated by Treesitter for the current buffer. This shows how your code is parsed, displaying the overall structure and types of syntax nodes under your cursor. This is useful for debugging or understanding Treesitter queries and highlighting, all of this can be customized, but we won’t deep dive into that here.
(chunk ; [0, 0] - [16, 0]
(return_statement ; [0, 0] - [15, 1]
(expression_list ; [0, 7] - [15, 1]
(table_constructor ; [0, 7] - [15, 1]
(field ; [1, 2] - [1, 35]
value: (string ; [1, 2] - [1, 35]
content: (string_content))) ; [1, 3] - [1, 34]
(field ; [2, 2] - [2, 21]
name: (identifier) ; [2, 2] - [2, 7]
value: (string ; [2, 10] - [2, 21]
content: (string_content))) ; [2, 11] - [2, 20]
(field ; [3, 2] - [14, 5]
name: (identifier) ; [3, 2] - [3, 8]
value: (function_definition ; [3, 11] - [14, 5]
parameters: (parameters) ; [3, 19] - [3, 21]
body: (block ; [4, 4] - [13, 6]
local_declaration: (variable_declaration ; [4, 4] - [4, 54]
(assignment_statement ; [4, 10] - [4, 54]
(variable_list ; [4, 10] - [4, 17]
name: (identifier)) ; [4, 10] - [4, 17]
(expression_list ; [4, 20] - [4, 54]
value: (function_call ; [4, 20] - [4, 54]
name: (identifier) ; [4, 20] - [4, 27]
arguments: (arguments ; [4, 27] - [4, 54]
(string ; [4, 28] - [4, 53]
content: (string_content))))))) ; [4, 29] - [4, 52]
(function_call ; [5, 4] - [13, 6]
name: (dot_index_expression ; [5, 4] - [5, 17]
table: (identifier) ; [5, 4] - [5, 11]
field: (identifier)) ; [5, 12] - [5, 17]
arguments: (arguments ; [5, 17] - [13, 6]
(table_constructor ; [5, 18] - [13, 5]
(field ; [6, 6] - [6, 35]
name: (identifier) ; [6, 6] - [6, 15]
value: (table_constructor ; [6, 18] - [6, 35]
(field ; [6, 20] - [6, 33]
name: (identifier) ; [6, 20] - [6, 26]
value: (true)))) ; [6, 29] - [6, 33]
(field ; [7, 6] - [7, 32]
name: (identifier) ; [7, 6] - [7, 12]
value: (table_constructor ; [7, 15] - [7, 32]
(field ; [7, 17] - [7, 30]
name: (identifier) ; [7, 17] - [7, 23]
value: (true)))) ; [7, 26] - [7, 30]
(field ; [8, 6] - [8, 33]
name: (identifier) ; [8, 6] - [8, 13]
value: (table_constructor ; [8, 16] - [8, 33]
(field ; [8, 18] - [8, 31]
name: (identifier) ; [8, 18] - [8, 24]
value: (true)))) ; [8, 27] - [8, 31]
(field ; [9, 6] - [11, 7]
name: (identifier) ; [9, 6] - [9, 22]
value: (table_constructor ; [9, 25] - [11, 7]
(field ; [10, 8] - [10, 13]
value: (string ; [10, 8] - [10, 13]
content: (string_content))) ; [10, 9] - [10, 12]
(field ; [10, 15] - [10, 21]
value: (string ; [10, 15] - [10, 21]
content: (string_content))) ; [10, 16] - [10, 20]
(field ; [10, 23] - [10, 28]
value: (string ; [10, 23] - [10, 28]
content: (string_content))) ; [10, 24] - [10, 27]
(field ; [10, 30] - [10, 42]
value: (string ; [10, 30] - [10, 42]
content: (string_content))) ; [10, 31] - [10, 41]
(field ; [10, 44] - [10, 49]
value: (string ; [10, 44] - [10, 49]
content: (string_content))) ; [10, 45] - [10, 48]
(field ; [10, 51] - [10, 57]
value: (string ; [10, 51] - [10, 57]
content: (string_content))) ; [10, 52] - [10, 56]
(field ; [10, 59] - [10, 71]
value: (string ; [10, 59] - [10, 71]
content: (string_content))) ; [10, 60] - [10, 70]
(field ; [10, 73] - [10, 80]
value: (string ; [10, 73] - [10, 80]
content: (string_content))) ; [10, 74] - [10, 79]
(field ; [10, 82] - [10, 90]
value: (string ; [10, 82] - [10, 90]
content: (string_content))))) ; [10, 83] - [10, 89]
(field ; [12, 6] - [12, 26]
name: (identifier) ; [12, 6] - [12, 18]
value: (false)))))))))))) ; [12, 21] - [12, 26]On to our next plugin: Harpoon
Harpoon
We cycle through multiple files within our development projects pretty frequently, so in order to organize that a little better Primeagen created this sucker for us. It’s extremely user friendly, especially if you think buffers can be hard to manage. With Harpoon, we can add files to a custom list and jump between them with simple keybinds without having to sweat too much.
Create the harpoon.lua spec within your .config/nvim/lua/plugins/ directory and add the following code:
local conf = require('telescope.config').values -- import telescope's default configurations
local themes = require('telescope.themes') -- import telescope's themes
-- function to open a telescope picker with the list of harpooned files
local function toggle_telescope(harpoon_files)
local file_paths = {}
for _, item in ipairs(harpoon_files.items) do
table.insert(file_paths, item.value)
end
-- use the 'ivy' theme for the telescope picker
local opts = themes.get_ivy({
prompt_title = "Workspace List"
})
-- create and open a new telescope picker with the harpooned files
require("telescope.pickers").new(opts, {
finder = require("telescope.finders").new_table({
results = file_paths,
}),
previewer = conf.file_previewer(opts), -- file previewer
sorter = conf.generic_sorter(opts), -- sorts the list
}):find()
end
return {
"ThePrimeagen/harpoon", -- plugin directory
branch = "harpoon2", -- harpoon2 here is the 'newer' branch
dependencies = {
"nvim-lua/plenary.nvim"
},
config = function()
local harpoon = require('harpoon') -- import the harpoon module
vim.keymap.set("n", "<leader>a", function() harpoon:list():add() end) -- add a file to the list
vim.keymap.set("n", "<leader>fl", function() toggle_telescope(harpoon:list()) end, -- open up the list in telescope
{ desc = "Open harpoon window" })
vim.keymap.set("n", "<C-e>", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end) -- toggle the harpoon quick menu UI
vim.keymap.set("n", "<C-n>", function() harpoon:list():next() end) -- jump to the next harpooned file
vim.keymap.set("n", "<C-p>", function() harpoon:list():prev() end) -- jump to the previous harpooned file
end
}See the comments in the code for some quick explanations on what each command does and here’s a little demo of harpoon in action:

Notice how we’re combining functionalities from previously installed plugins? This showcases the power of Neovim, you can configure it to do whatever shenanagins you can think of!
Our Neovim setup is coming along nicely, so far we have some styling, navigation, syntax highlighting, and custom file management!

But we’re still missing something…auto completions and LSP’s!
Autocompletion and LSP
We’ve got syntax highlighting, a theme, and fuzzy file finding, now it’s time for some development utilities: autocompletion and LSP(Language Server Protocol).
What is LSP?
If you’re not familiar with it, the Language Server Protocol is basically the secret sauce that powers intelligent features like:
- Autocompletion
- Go to definition
- Hover documentation
- Find references
- Diagnostics (error/warning squiggles)
In our case, it allows Neovim to talk to language servers (like lua-language-server, typescript-language-server, etc.), so you can get IDE-level feedback no matter what language you’re using to code your projects in.
Think of it like this:
Neovim says, “Hey, what’s this function do?”
The LSP replies, “Oh, that’s a User model method defined in line 12 of models/user.rb.”
If you’ve used popular IDE’s like VSCode and such you’ve probably used an LSP via an extension or two.
Setting up Autocompletion
Before we install LSP servers, let’s get autocompletion running.
We’ll be using nvim-cmp, the most popular completion engine for Neovim.
Let’s create a new file:
nvim ~/.config/nvim/lua/plugins/cmp.luaOnce created, add this to the file:
return {
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp", -- LSP completions
"hrsh7th/cmp-buffer", -- buffer words
"hrsh7th/cmp-path", -- filesystem paths
"hrsh7th/cmp-cmdline", -- command-line completions
"L3MON4D3/LuaSnip", -- snippet engine
"saadparwaiz1/cmp_luasnip", -- snippet completions
},
config = function()
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({
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.abort(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
}, {
{ name = "buffer" },
{ name = "path" },
}),
})
end
}Notice how we add the LuaSnip and cm_luasnip dependencies? We’ve been using lua code to configure our Neovim so far, right? Chances are you’ll be adding more stuff on your own in the future, so it’s best to have lua autocompletions configured even if it’s not going to be your main programming language for work or your own projects.
Again, save and quit with :wq and open Neovim up again. Lazy will install everything for you.
With that, you now have smart autocompletion for files, buffers, and commands. But in order to unlock full code intelligence, we need to bring in LSP servers.
LSP Setup
To make LSP configuration painless, we’ll need two helper plugins:
mason.nvim
→ installs language servers
mason-lspconfig.nvim
→ bridges Mason with Neovim’s LSP API
nvim-lspconfig
→ configures each server
Let’s create our lsp file with:
nvim ~/.config/nvim/lua/plugins/lsp.luaOnce created, add the following code to it:
---@diagnostic disable: undefined-global
return {
{
"neovim/nvim-lspconfig",
dependencies = {
"williamboman/mason.nvim",
"williamboman/mason-lspconfig.nvim",
"hrsh7th/cmp-nvim-lsp",
},
config = function()
-- Mason setup
require("mason").setup()
require("mason-lspconfig").setup({
ensure_installed = {
"ruby_lsp", -- ruby LSP from Shopify
-- "solargraph", -- still pretty popular, an alternative to ruby_lsp
"lua_ls", -- lua LSP
"html", -- html LSP
"cssls", -- css LSP
"emmet_ls", -- emmet LSP
"jsonls", -- json LSP
},
automatic_installation = true,
})
local capabilities = require("cmp_nvim_lsp").default_capabilities()
-- on_attach function to map keys after LSP attaches to buffer
local on_attach = function(_, bufnr)
local opts = { noremap = true, silent = true, buffer = bufnr }
local keymap = vim.keymap.set
keymap("n", "gd", vim.lsp.buf.definition, opts) -- go to definition
keymap("n", "gr", vim.lsp.buf.references, opts) -- go to references
keymap("n", "gi", vim.lsp.buf.implementation, opts) -- go to implementation
keymap("n", "K", vim.lsp.buf.hover, opts) -- hover
keymap("n", "<leader>rn", vim.lsp.buf.rename, opts) -- rename
keymap("n", "<leader>ca", vim.lsp.buf.code_action, opts) -- code action
keymap("n", "[d", function() vim.diagnostic.jump({ count = -1 }) end, opts) -- previous diagnostic
keymap("n", "]d", function() vim.diagnostic.jump({ count = 1 }) end, opts) -- next diagnostic
keymap("n", "<leader>f", function() vim.lsp.buf.format({ async = true }) end, opts) -- format
end
-- Ruby LSP
vim.lsp.config.ruby_lsp = {
cmd = { "ruby-lsp" },
filetypes = { "ruby" },
root_markers = { "Gemfile", ".git" },
capabilities = capabilities,
on_attach = on_attach,
}
-- Lua LSP
vim.lsp.config.lua_ls = {
cmd = { "lua-language-server" },
filetypes = { "lua" },
root_markers = { ".luarc.json", ".luarc.jsonc", ".luacheckrc", "stylua.toml", ".git" },
capabilities = capabilities,
on_attach = on_attach,
settings = {
Lua = {
runtime = { version = "LuaJIT" },
diagnostics = { globals = { "vim" } }, -- recognize the `vim` global
workspace = {
library = vim.api.nvim_get_runtime_file("", true),
checkThirdParty = false, -- disable annoying warnings for third-party libraries
},
telemetry = { enable = false },
},
},
}
-- HTML LSP
vim.lsp.config.html = {
cmd = { "vscode-html-language-server", "--stdio" },
filetypes = { "html", "erb" },
root_markers = { "package.json", ".git" },
capabilities = capabilities,
on_attach = on_attach,
}
-- CSS LSP
vim.lsp.config.cssls = {
cmd = { "vscode-css-language-server", "--stdio" },
filetypes = { "css", "scss", "less" },
root_markers = { "package.json", ".git" },
capabilities = capabilities,
on_attach = on_attach,
}
-- Emmet LSP
vim.lsp.config.emmet_ls = {
cmd = { "emmet-ls", "--stdio" },
filetypes = { "html", "css", "scss", "erb" },
root_markers = { "package.json", ".git" },
capabilities = capabilities,
on_attach = on_attach,
}
-- JSON LSP
vim.lsp.config.jsonls = {
cmd = { "vscode-json-language-server", "--stdio" },
filetypes = { "json", "jsonc" },
root_markers = { "package.json", ".git" },
capabilities = capabilities,
on_attach = on_attach,
}
-- diagnostics config with modern sign configuration
vim.diagnostic.config({
virtual_text = true,
signs = {
text = {
[vim.diagnostic.severity.ERROR] = "✗",
[vim.diagnostic.severity.WARN] = "⚠",
[vim.diagnostic.severity.HINT] = "💡",
[vim.diagnostic.severity.INFO] = "ℹ",
},
},
underline = true,
update_in_insert = false,
severity_sort = true,
})
end,
},
}With this configuration, we install Mason and LSP servers automatically and we add some basic keymaps for LSP actions like go-to-defition with gd
Again, save and quit with :wq, open up Neovim again and lazy should install everything. Once that’s done, run mMason with :Mason and it’s UI promp should appear and install the language servers we specified.
Once they are installed, restart Neovim, open a code file, and boom! Autocompletion, inline diagnostics, and hover info should all be working.
And voilà! You’ve got completion, linting, hover documentation, and even refactoring tools all running in a Neovim terminal!
Conclusion
That should get you going, but we aren’t done yet! Stay tuned for Pt. 3, where we’ll be fine-tuning our Neovim with some git plugins, copilot, vim motions cheatsheets, exercises and more!

References
- https://github.com/folke/lazy.nvim
- https://github.com/nvim-treesitter/nvim-treesitter
- https://github.com/nvim-telescope/telescope.nvim
- https://github.com/hrsh7th/nvim-cmp
- https://neovim.io/doc/user/lsp.html
- https://github.com/nvim-lualine/lualine.nvim
- https://github.com/ThePrimeagen/harpoon/tree/harpoon2
We want to work with you. Check out our Services page!










