ArkarDev

Neovim from scratch with Lazy.nvim

Jul 14, 2024Arkar Kaung Myat
VimDev Tools

Yet another guide to build your Neovim environment from the ground up using Lazy.nvim.

Recently I switched my neovim plugin manager to Lazy.nvim. This article is going to be second version of Neovim setup from scratch using Lazy.nvim.

I will be using a docker instance and step by step work through to how to transform that into feature complete text editor.

Let's go ahead and spin up a container, if you want to setup in your local computer its fine too.

Make sure you have installed the latest version of Neovim and have git installed.

docker run -it --rm ubuntu:latest bash

# Configuration location

The next step is to create a Neovim configuration file.

Go and checkout nvim folder under your home directory ~/.config/nvim. You can create one if you don't have any.

# Basic configurations

Before we dive into crazy plugin system, I like to add basic sets and remaps for my editor.

Here is how I like to my configuration folder structure.

└── ~/.config/nvim/
    ├── lua/
    │   ├── core
    │   └── plugins
    └── init.lua

I will be putting all the default neovim configurations in core directory for example remaps and sets.

Create a file called sets.lua under core directory.

-- lua/core/sets.lua

vim.g.netrw_banner = 0
vim.opt.guicursor = ""
vim.opt.spelllang = "en_us"
vim.opt.nu = true
vim.opt.relativenumber = true
vim.opt.clipboard = "unnamedplus"
vim.opt.tabstop = 4
vim.opt.softtabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.smartindent = true
vim.opt.wrap = false
vim.opt.hlsearch = false
vim.opt.incsearch = true
vim.opt.termguicolors = true
vim.opt.scrolloff = 8
vim.opt.signcolumn = "yes"
vim.opt.isfname:append("@-@")
vim.opt.updatetime = 50
vim.opt.colorcolumn = "80"
vim.opt.swapfile = false
vim.opt.backup = false
vim.opt.undodir = os.getenv("HOME") .. "/.vim/undodir"
vim.opt.undofile = true
vim.o.tabstop = 2
vim.o.shiftwidth = 2
vim.o.expandtab = true

And a file called remaps.lua as well.

-- lua/core/remaps.lua

-- leader key mapping
vim.g.mapleader = " ";

-- Open netrw 
vim.keymap.set("n","<leader>pv",vim.cmd.Ex);

-- move up and down
vim.keymap.set("v", "K", ":m '<-2<CR>gv=gv")
vim.keymap.set("v", "J", ":m '>+1<CR>gv=gv")

-- half page movement
vim.keymap.set("n", "<C-d>", "<C-d>zz")
vim.keymap.set("n", "<C-u>", "<C-u>zz")

-- jk escape 
vim.keymap.set("i", "jk", "<ESC>")

To check back your directory should look like this

.
└── ~/.config/nvim/
    ├── lua/
    │   ├── core/
    │   │   ├── sets.lua
    │   │   └── remaps.lua
    │   └── plugins
    └── init.lua

# Including modules

We now have two configuration files but if you check your Neovim , you will see nothing is happening. The thing is we just created two seperate lua files and we will have to include those modules for Neovim to know.

In Neovim, you can set all your user specific configurations to init.lua.

All we have to do is import those configurations to init.lua.

Create a file named init.lua inside core directory and include those two modules.

-- /core/init.lua
require("core.remaps")
require("core.sets")

Finally, you will have to include the core module itself into root's init.lua file.

-- /.config/nvim/init.lua
require("core")

After that you can now quit Neovim and launch it agian . You should see all the configuration working properly.

Your final directory should look like this.

.
└── ~/.config/nvim/
    ├── lua/
    │   ├── core/
    │   │   ├── init.lua
    │   │   ├── sets.lua
    │   │   └── remaps.lua
    │   └── plugins
    └── init.lua

# Installing Plugins with lazy.nvim

As for next step, let's install our lazy.nvim .

Create a file named lazi.lua under lua folder. You can checkout lazy.nvim github for more options and configurations. Here is what I will be using in this setup.

-- /.config/nvim/lua/lazi.lua

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 = 'plugins' },
})

Include the lazy configuration to your root init file.

-- /.config/nvim/init.lua
require("core")
require("lazi")

After this is all you have to do is restart neovim.

Once you restart neovim, you should see message like this, no module plugins found.

Let's take a step back and look at our configuration file.

require('lazy').setup({
  { import = 'plugins' },
})

In our lazi.lua , you can see import = 'plugins' . What it means is lazy is looking for plugins in plugins directory we defined which is currently empty.

Its time for us to throw our favorite plugins in the folder.

# Installing plugins

Let's see how we can install plugins with lazy.

# Treesitter

To install treesitter , you need to create a file named treesitter.lua inside plugin folder.

-- ~/.config/nvim/lua/plugins/treesitter.lua

return {
    "nvim-treesitter/nvim-treesitter",
    config = function()
                local configs = require("nvim-treesitter.configs")

                configs.setup({
                    ensure_installed = { "c", "lua", "vim", "vimdoc", "rust", "javascript", "html" },
                    sync_install = false,
                    highlight = { enable = true },
                    indent = { enable = true },  
                })
    end;
}

You can check out more configuration in treesitter official documentation. Restart Neovim and then sit back and relax you should see lazy doing all the work for you.

# Mason

To install mason , create another file in plugins directory.

return {
  "williamboman/mason.nvim",
  dependencies = {
    "williamboman/mason-lspconfig.nvim",
    "WhoIsSethDaniel/mason-tool-installer.nvim",
  },
  config = function()
    -- import mason
    local mason = require("mason")

    -- import mason-lspconfig
    local mason_lspconfig = require("mason-lspconfig")

    local mason_tool_installer = require("mason-tool-installer")

    -- enable mason and configure icons
    mason.setup({
      ui = {
        icons = {
          package_installed = "✓",
          package_pending = "➜",
          package_uninstalled = "✗",
        },
      },
    })

    mason_lspconfig.setup({
      -- list of servers for mason to install
      ensure_installed = {
        "tsserver",
        "html",
        "cssls",
        "tailwindcss",
        "svelte",
        "lua_ls",
        "graphql",
        "emmet_ls",
      },
      -- auto-install configured servers (with lspconfig)
      automatic_installation = true, -- not the same as ensure_installed
    })

    mason_tool_installer.setup({
      ensure_installed = {
        "prettier", -- prettier formatter
        "stylua", -- lua formatter
        "eslint_d", -- js linter
      },
    })
  end,
}

To check back your directory structure should look like this.

.
└── ~/.config/nvim/
    ├── lua/
    │   ├── core/
    │   │   ├── init.lua
    │   │   ├── sets.lua
    │   │   └── remaps.lua
    │   └── plugins
    │   │   ├── treesitter.lua
    │   │   ├── .....lua
    │   │   └── mason.lua
    │   └── lazi.lua
    └── init.lua

# Installing lsp

return {
	"neovim/nvim-lspconfig",
	config = function()
		-- Setup language servers.
		local lspconfig = require("lspconfig")
		lspconfig.pyright.setup({})
		lspconfig.tsserver.setup({})
		lspconfig.emmet_language_server.setup({
			filetypes = {
				"css",
        "svelte",
				"eruby",
				"html",
				"javascript",
				"javascriptreact",
				"less",
				"sass",
				"scss",
				"pug",
				"typescriptreact",
			},
			-- Read more about this options in the [vscode docs](https://code.visualstudio.com/docs/editor/emmet#_emmet-configuration).
			-- **Note:** only the options listed in the table are supported.
			init_options = {
				---@type table<string, string>
				includeLanguages = {},
				--- @type string[]
				excludeLanguages = {},
				--- @type string[]
				extensionsPath = {},
				--- @type table<string, any> [Emmet Docs](https://docs.emmet.io/customization/preferences/)
				preferences = {},
				--- @type boolean Defaults to `true`
				showAbbreviationSuggestions = true,
				--- @type "always" | "never" Defaults to `"always"`
				showExpandedAbbreviation = "always",
				--- @type boolean Defaults to `false`
				showSuggestionsAsSnippets = false,
				--- @type table<string, any> [Emmet Docs](https://docs.emmet.io/customization/syntax-profiles/)
				syntaxProfiles = {},
				--- @type table<string, string> [Emmet Docs](https://docs.emmet.io/customization/snippets/#variables)
				variables = {},
			},
		})
		lspconfig.svelte.setup({})
		lspconfig.clangd.setup({})
		lspconfig.tailwindcss.setup({})
		lspconfig.rust_analyzer.setup({
			-- Server-specific settings. See `:help lspconfig-setup`
			settings = {
				["rust-analyzer"] = {
          checkOnSave = {
            command = "clippy",
          },
        },
			},
		})
		-- Global mappings.
		-- See `:help vim.diagnostic.*` for documentation on any of the below functions
		--
		vim.keymap.set("n", "<leader>dp", vim.diagnostic.goto_prev)
		vim.keymap.set("n", "<leader>dn", vim.diagnostic.goto_next)
		vim.keymap.set("n", "<space>df", vim.diagnostic.open_float)
		vim.keymap.set("n", "<space>dl", vim.diagnostic.setloclist)

		-- Use LspAttach autocommand to only map the following keys
		-- after the language server attaches to the current buffer
		vim.api.nvim_create_autocmd("LspAttach", {
			group = vim.api.nvim_create_augroup("UserLspConfig", {}),
			callback = function(ev)
				-- Enable completion triggered by <c-x><c-o>
				vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc"

				-- Buffer local mappings.
				-- See `:help vim.lsp.*` for documentation on any of the below functions
				local opts = { buffer = ev.buf }
				vim.keymap.set("n", "gD", vim.lsp.buf.declaration, opts)
				vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
				vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
				vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts)
				vim.keymap.set("n", "gt", vim.lsp.buf.type_definition, opts)
				vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
				vim.keymap.set("n", "<leader>r", vim.lsp.buf.rename, opts)
				vim.keymap.set({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action, opts)

				vim.keymap.set("n", "<C-k>", vim.lsp.buf.signature_help, opts)
				vim.keymap.set("n", "<space>wa", vim.lsp.buf.add_workspace_folder, opts)
				vim.keymap.set("n", "<space>wr", vim.lsp.buf.remove_workspace_folder, opts)
				vim.keymap.set("n", "<space>wl", function()
					print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
				end, opts)
				vim.keymap.set("n", "<space>f", function()
					vim.lsp.buf.format({ async = true })
				end, opts)
			end,
		})
	end,
}

You should see the pattern by now. If you want to add a plugin, create a file in plugins folder and then let lazy do the job. Most of the plugins now have guide to install lazy readily.

Lazy also watch for changes and you should see it automatically installing new plugins.

You can also manually open the menu using :Lazy command as well.

That's it. I will not be focusing a lot on Neovim and plugins related stuffs. Hopefully this article is helpful for you if you are planning to try lazy.nvim.

I recommend you definitely should.