Neovim Complete Setup - Setting up Neovim From Scratch

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.

Alacritty

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.

tmux

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.

Neovim website

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.

Empty neovim

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.

lazy nvim

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.

gruvbox

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.

lazy

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.

file structure

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
  1. Map our Leader key to Space. This is our main key to make keybinds.
  2. Highlight search in the file.
  3. Show line numbers.
  4. Disable the mouse completely.
  5. Enable copying between Neovim and the system clipboard.
  6. Wrap our lines.
  7. Disable swap files but keep undo history.
  8. Decrease update time to make Neovim faster.
  9. Configure how and where we open splits.
  10. Change the indentation.
  11. 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:

  1. Pressing Space followed by s saves the current file.
  2. Pressing k or j will move the cursor without jumping between lines.
  3. Pressing H and L jumps to the beginning and end of the line.
  4. Pressing Q closes the current buffer.
  5. Pressing vv and ss creates vertical and horizontal splits.
  6. Using Ctrl + k, Ctrl + j, Ctrl + h, Ctrl + l jumps between splits in different directions.
  7. Pressing Tab and Shift + Tab indents code in virtual mode.
  8. Pressing y and p moves the cursor to the end of pasted or copied text, similar to other editors.
  9. 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.

fzf

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.

indent

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.

lualine

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.

mason

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.

file 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.

treesitter

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.

lsp

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.

Unlock unlimited learning with our All-Access Subscription! Gain instant access to all current courses, future releases, and updates by subscribing here. Never miss out on new content and stay at the forefront of your field!

📚 Source code of what we've done