A Noob's Neovim Journey Pt. 2 - Featured Image

A Noob’s Neovim Journey: Pt. 2 “But How?”

Beginner's Guide to Neovim: How to get started

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:

Neovim Interface

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/nvim

The 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

Neovim Create Filename

init.lua

So what does init.lua do, 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)

init.lua print

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/config

Your directory tree within your ~/.config/nvim should look like this:
directory tree

Within the config subdirectory, we will create three essential files:

  • options.lua -> editor behavior and settings
  • keybinds.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 file

Once 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:

Neovim options

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 file

Here 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 explorer

esc 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!

neovim 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.lua

Now 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:

  1. shift v enters Visual Mode and selects the whole line
  2. y to yank the line
  3. p to paste it below the current line
  4. fk finds the next occurance of a letter on the current line, in this case we skip over to the letter k
  5. cw enters Insert Mode and cuts the word
  6. and then write lazy

Concate these together and yet get this:

vim copy paste edit

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:

lazy 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!

lazy-lock.json

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!

Let's do this

Telescope

Just what is it, and why do we want to add it to our Neovim?

Telescope is:

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!
    telescope find files
  • <leader>fg -> full grep, greps through your directory
    telescope live grep
  • <leader>fb -> opens up your buffers, allowing you to select any open buffer you wish
    telescope buffers
  • <leader>fh -> help — super useful! All of the help files for your installed plugins can be found here
    telescope help

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:

treesitter healthcheck

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:
Harpoon demo

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!

Dear god

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

Once 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.lua

Once 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!

Let's party

References

We want to work with you. Check out our Services page!