DEV Community

Cover image for Taking Neovim to the Moon
Elves Sousa
Elves Sousa

Posted on

Taking Neovim to the Moon

I've been using Neovim as my main code editor for a while now, and so far, I haven't changed my mind. I wrote some articles about this editor, and one of them was about creating a simple configuration file that works for both Neovim and Vim. This one still works perfectly, but like everything else in the computer world, there is always another way to achieve the same goals. One of these ways is to configure Neovim using Lua language.

I received a few requests to create a Lua configuration, but I procrastinated until I actually needed to configure Neovim from scratch again. The opportunity came recently, as I needed to set up a Ryzentosh (Hackintosh with a Ryzen processor) for work, and there was a clean Neovim to configure. Before getting into the guide itself, I will comment about a few things.

The Lua language

Sample code in Lua

Lua is a programming language created by Brazilians Roberto Ierusalimschy, Waldemar Celes and Luiz Henrique de Figueiredo at PUC/RJ. It is a high-level, multi-paradigm, lightweight language with dynamic typing and automatic garbage collection.

It was made to automate and extend more complex applications, such as Petrobras projects, game engines, embedded systems, and other applications such as Neovim.

As being a satellite language has always been one of its goals, "Lua" (Moon in Portuguese) is quite an appropriate name. Its ease of learning and considerable speed have been positive points in its adoption.

This article will not make you a master of the language, nor is that the purpose. But you will know enough to configure Neovim.

About the config file


In the init.vim I made in another article, it was possible to create a single file that contains everything needed to have a comfortable experience with Neovim. With the init.lua that will be created here, this is also possible, and that's what I propose. On another occasion, we will separate it into several files, as it seems to be the standard way.

If you're new to Neovim, don't try to follow this guide. Start with the VimScript configuration file created in the other article, and once you're more comfortable with the environment, come back!

Config file

To get started, create an init.lua file in the .config/nvim directory, located in your user folder:

$ cd ~/.config/nvim/
$ touch init.lua
Enter fullscreen mode Exit fullscreen mode

Setting options

The goal is basically to translate the init.vim I already have into Lua language.

In VimScript, each option is set with the keyword set. In Lua, you use vim.opt.[option-name], which, let's face it, is not very readable if repeated several times. So, I decided to allocate vim.opt in a local variable called set (wow!). This makes everything more familiar and readable.

Here are the options I use:

-- Opções
local set = vim.opt

set.background = "dark"
set.clipboard = "unnamedplus"
set.completeopt = "noinsert,menuone,noselect"
set.cursorline = true
set.expandtab = true
set.foldexpr = "nvim_treesitter#foldexpr()"
set.foldmethod = "manual"
set.hidden = true
set.inccommand = "split"
set.mouse = "a"
set.number = true
set.relativenumber = true
set.shiftwidth = 2
set.smarttab = true
set.splitbelow = true
set.splitright = true
set.swapfile = false
set.tabstop = 2
set.termguicolors = true
set.title = true
set.ttimeoutlen = 0
set.updatetime = 250
set.wildmenu = true
set.wrap = true
Enter fullscreen mode Exit fullscreen mode

If you've used Neovim, you're probably already familiar with several of these options, but if you're not, here's an explanation of each one:

  • background=dark: apply the color set to dark screens. Not just the background of the screen, as it may seem.
  • clipboard=unnamedplus: enables the clipboard between Neovim and other system programs.
  • completeopt: modifies the behavior of the auto-complete menu to behave more like an IDE.
  • cursorline: highlights the current line in the editor.
  • expandtab: turns tabs into spaces.
  • foldexpr and foldmethod: these options were added to improve the code folding behavior in TreeSitter.
  • hidden: hide unused buffers.
  • inccommand=split: show replacements in a split window, before applying to the file.
  • mouse=a: allows the use of the mouse.
  • number: shows the line numbers.
  • relativenumber: shows lines numbers starting from the current one. Useful to assist in commands that use more lines.
  • shiftwidth=2: number of spaces when indenting the text.
  • splitbelow and splitright: change the behavior of splitting the screen with the command :split (split the screen horizontally) and :vsplit (vertically). In this case, the screens will always split below the current screen and to the right.
  • swapfile = false: inhibit the creation of Vim .swp files.
  • tabstop=2: number of spaces for tabs.
  • termguicolors: expands the number of usable colors, if the terminal emulator supports it.
  • title: shows the title of the file.
  • ttimeoutlen=0: time in milliseconds to accept commands.
  • updatetime: time in milliseconds that language servers use to check for errors.
  • wildmenu: shows a more advanced menu for autocomplete suggestions.

To see them in action, just quit Neovim and run it again (you know how to do that, don't you?), or go into shift + : command mode and type luafile %. This will read the init.lua file and apply the options to the current instance.


To add automatic syntax detection support for open files:

  filetype plugin indent on
  syntax on
Enter fullscreen mode Exit fullscreen mode

Note the vim.cmd: it makes possible to use VimScript in a .lua file. In this case, it's a very practical way to write less code.


Plugin manager

Packer: Extension manager for Neovim written in Lua

To install plugins, you need to install a manager first. Packer will be used for this, as it is made in Lua and obviously supports configuration in this language.

If you are using macOS or Linux, just run this command in the terminal:

$ git clone --depth 1\
Enter fullscreen mode Exit fullscreen mode

After restarting Neovim, you will have a series of commands that will make installing, updating and removing plugins much easier.

To add it to our configuration file, just add the following lines:

-- Plugins
local packer = require("packer")

-- Include packer.nvim
vim.cmd([[packadd packer.nvim]])

  -- Plugins are listed here
Enter fullscreen mode Exit fullscreen mode

Note the require("packer"): this is the way to import files in Lua. For anyone who has used NodeJS, this is very familiar. Following that, a command in VimScript is added so that Neovim recognizes the existence of the Packer package.

To add plugins to be installed, simply include use("user-in-github/repository"), in the anonymous function in packer.startup(). The first plugin to be added will be Packer itself:

  -- Plugin manager
Enter fullscreen mode Exit fullscreen mode

This way we can update Packer as well as other plugins.

Plugins used in this setup

Here is the plugins list:

  -- Completion
  -- Motor de snippets
  -- Formatting
  -- Language server
  -- Syntax parser
  -- Plugin manager
  -- Utilities
  -- Dependencies
  -- File browser
  -- Interface
  use({ "nvim-neo-tree/neo-tree.nvim", branch = "v2.x" })
  -- Color scheme
Enter fullscreen mode Exit fullscreen mode

A brief explanation of the function of each of them:


  • Neovim CMP: engine for autocomplete.
  • CMP Buffer: adds autocomplete based on the text typed in the open files (buffers).
  • CMP CmdLine: command line autocomplete.
  • CMP nvim-lsp: autocomplete using language server.
  • CMP Path: directory paths autocomplete.

Snippets engine

  • LuaSnip e Cmp LuaSnip: snippet engine needed for Neovim CMP


  • Null LS: provides several tools, such as diagnostics and code formatting.

Language server

  • Neovim LSP Config: language servers support for Neovim.
  • Neovim LSP Installer: installs language servers from a popup window.

Syntax parser

  • Treesitter: syntax highlighting.


  • Neovim Autopairs: automatically closes parentheses, square brackets and braces.
  • Neovim Colorizer: displays HEX, RGB or HSL colors in the editor.
  • Neovim GitSigns: shows git changes on the side


  • Plenary: Lua plugin framework for Neovim. Required for some extensions to work.
  • Neovim Web Devicons: enables the use of icon fonts in plugins. Detail: you need to have a "Nerd Font" installed on the system and applied in your terminal emulator.
  • NUI: UI library, used by some plugins.

File management

  • Telescope: fuzzy file search.


  • Bufferline: tab bar
  • Neo Tree: file/folder tree. Alternative to NetRW.
  • Lualine: status bar, similar to Airline.


  • Sobrio: theme made by me and that I use daily. Now with TreeSitter support.

Installing the extensions

To install, as we did the other time with VimPlug, it is necessary to use a command to install. However, unlike VimPlug, a single Packer command is responsible for removing, adding or updating the extensions: :PackerSync.

When using this command, a side split will appear and show the changes that Packer has made to your Neovim installation. Remember to always use this command when adding or removing plugins from your list.

Plugin startup

In addition to installing the files with Packer, you need to initialize the plugins so that they can be used in Neovim. Some of them work without options, others need more settings. To see some extensions working, we'll add the simplest ones first:

-- Misc plugins
-- Autopairs
    disable_filetype = { "TelescopePrompt" },

-- Colorizer

-- Git signs

-- Bufferline

-- Lualine
Enter fullscreen mode Exit fullscreen mode

This is a pattern. Plugins are usually initialized with require("plugin-name").setup().

Note that in the Autopairs plugin, I put an option to disable it from working in the Telescope window, to avoid unwanted behavior.

Neo Tree

Neo Tree
Neo Tree: Directory tree

Neo Tree is a NetRW replacement. By default, it already works fine, but I recommend adding the options below:

-- Neo tree
  close_if_last_window = false,
  enable_diagnostics = true,
  enable_git_status = true,
  popup_border_style = "rounded",
  sort_case_insensitive = false,
  filesystem = {
    filtered_items = {
      hide_dotfiles = false,
      hide_gitignored = false,
  window = { width = 30 },
Enter fullscreen mode Exit fullscreen mode

Explanation of the options:

  • close_if_last_window = false: keeps Neo Tree visible if there are no more files open.
  • enable_diagnostics = true: displays errors or warnings in the file, depending on the language server.
  • enable_git_status = true: displays Git change information.
  • popup_border_style = "rounded": rounds dialog borders.
  • sort_case_insensitive = false: ignores case when sorting files.
  • hide_dotfiles=false: show hidden files.
  • hide_gitignored = false: show files ignored by the version control.
  • window = { width = 30 }: defines the width of the Neo Tree split to 30% of the screen.

Color scheme

Sobrio: The theme I made and use

Depending on the theme you use, the configuration will vary. If the theme you chose has configuration available in Lua, the process is very similar to plugins. In the case of my theme, Sobrio, I just added it to the vim.cmd I mentioned at the beginning of this article:

-- Color scheme and syntax
  filetype plugin indent on
  syntax on
  colorscheme sobrio
Enter fullscreen mode Exit fullscreen mode


Sobrio Ghost
Sobrio Ghost: variant of the Sobrio theme, with transparent background

This theme I made has other variants, such as a light version (Sobrio Light), one with alternative colors (Sobrio Verde) and one with a transparent background (Sobrio Ghost).

Syntax parser configuration

TreeSitter is an extension responsible for the syntax highlighting presented on the screen. The result is usually much better than the default parser in Neovim.

Below is a sample configuration compatible with my workflow:

-- Syntax highlighting
  ensure_installed = {
  highlight = { enable = true },
  indent = { enable = true },
  autotag = {
  enable = true,
  filetypes = {
Enter fullscreen mode Exit fullscreen mode

Brief explanation of the options:

  • ensure_installed: list of languages to be installed in TreeSitter. If you wish, just use the all option and TreeSitter will use all the languages it supports, even the ones you never use or will ever use.
  • highlight: enables TreeSitter colorization.
  • indent: enables code indentation.
  • autotag: interprets XML or HTML tags in the languages listed in filetypes.

Code completion

Neovim CMP

Neovim CMP or nvim-cmp is an extension that enables code completion in Neovim. For it to work, several other related extensions need to be present.

-- Completion
local cmp = require("cmp")

  snippet = {
    expand = function(args)
  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" },

-- File types specifics
cmp.setup.filetype("gitcommit", {
  sources = cmp.config.sources({
    { name = "cmp_git" },
  }, {
    { name = "buffer" },

-- Command line completion
cmp.setup.cmdline("/", {
  mapping = cmp.mapping.preset.cmdline(),
  sources = { { name = "buffer" } },

cmp.setup.cmdline(":", {
  mapping = cmp.mapping.preset.cmdline(),
  sources = cmp.config.sources({
    { name = "path" },
  }, {
    { name = "cmdline" },
Enter fullscreen mode Exit fullscreen mode

Most of this code I inserted according to the suggestions available in the author's repository. The only thing I did was adapting it for use with the LuaSnip suggestion engine.

In the snippet option, you configure which suggestion engine will be used. In mapping, keyboard shortcuts to CMP commands are added. The sources option lets you sort the suggestion sources by priority. The code above sets the following the priorities:

  1. Language server suggestions
  2. LuaSnip pre-made codes
  3. Words written in the current file
  4. Directory paths on your computer

The cmp.setup.filetypes block allows you to configure the behavior of Neovim CMP for a given file type. In this case, rules for using cmp_git in Git-related operations.

cmp.setup.cmdline configures options for Neovim's command mode. The fact that there are two blocks is because commands can start with the characters / or : and in each case the priorities of suggestions change.

Code formatting

NullLS is an extension that provides various tools based on the language server. One such tool is code formatting.

By default, nothing is done. You need to add some settings for NullLS to kick in. Below is sample setup for those who use Python, Rust, PHP, JavaScript/TypeScript, HTML, CSS/SCSS and Lua.

-- Formatting
local diagnostics = require("null-ls").builtins.diagnostics
local formatting = require("null-ls").builtins.formatting
local augroup = vim.api.nvim_create_augroup("LspFormatting", {})

  sources = {,
  on_attach = function(client, bufnr)
    if == "tsserver" or == "rust_analyzer" or == "pyright" then
      client.resolved_capabilities.document_formatting = false

    if client.supports_method("textDocument/formatting") then
      vim.api.nvim_clear_autocmds({ group = augroup, buffer = bufnr })
      vim.api.nvim_create_autocmd("BufWritePre", {
        group = augroup,
        callback = function()

-- Auto commands
vim.cmd([[ autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync() ]])
Enter fullscreen mode Exit fullscreen mode

In the sources option, you list the code formatting sources that will be used. The list of supported formatters is in the project repository. Note that it is not enough to just add to the list, the executable of each one must be installed on the machine to work.

The function in on_attach has two uses:

  1. Disables formatting on some servers, to prevent NullLS from asking which font to use every time it applies formatting, or from having strange behaviors.
  2. Create an autocmd to apply formatting on save, if the current file is supported.

Formatters installation

Each formatter has its own way of installing, but in general, they are available in three ways:

  1. Package manager for the programming language: Cargo for Rust, PIP for Python, NPM or Yarn for JavaScript/TypeScript.
  2. System package manager: Homebrew on macOS; Pacman, APT, DNF, etc. for Linux distributions.
  3. Compiling directly from source code. If you are not a Gentoo Linux user or want to contribute to the language server development, I do not recommend this option.

Installation examples

The formatters used in this setup were Black (Python), Rustfmt (Rust), PHP CS Fixer (PHP), Prettier (JavaScript and derivatives, HTML, CSS and derivatives) and Stylua (Lua).

To install Stylua, Prettier, PHP CS Fixer and Black using their respective package managers:

$ cargo install stylua
$ pip install black
$ (sudo) npm i -g prettier
$ composer global require friendsofphp/php-cs-fixer
Enter fullscreen mode Exit fullscreen mode

To install Stylua with Pacman on Arch Linux:

$ sudo pacman -S stylua
Enter fullscreen mode Exit fullscreen mode

Using Homebrew, you can install PHP CS Fixer as follows:

$ brew install php-cs-fixer
Enter fullscreen mode Exit fullscreen mode

By the way, Homebrew is not just for macOs. It's available for Linux and Windows, too.

Language servers

Neovim has a built-in language detector. It can be configured from scratch, but to help in this process, the Neovim team made the Neovim LSP Config extension. With it, just start lspconfig mentioning the name of the language server you want to activate in Neovim. Here's an example below:

-- Language servers
local lspconfig = require("lspconfig")
local caps = vim.lsp.protocol.make_client_capabilities()
local no_format = function(client, bufnr)
  client.resolved_capabilities.document_formatting = false

-- Capabilities
caps.textDocument.completion.completionItem.snippetSupport = true

-- Python
  capabilities = caps,
  on_attach = no_format

-- PHP
lspconfig.phpactor.setup({ capabilities = caps })

-- JavaScript/Typescript
  capabilities = caps,
  on_attach = no_format

-- Rust
  capabilities = snip_caps,
  on_attach = no_format

-- Emmet
  capabilities = snip_caps,
  filetypes = {
Enter fullscreen mode Exit fullscreen mode

The capabilities option was used to add snippets and code suggestions based on the programming language. The servers setup above were Pyright for Python, PhpActor for PHP, TSServer for Javascript/TypeScript, Rust Analyzer for Rust and Emmet to make HTML and CSS/Sass authoring more supportable.

Language servers must also have their executables present on the machine to work. Fortunately, the Neovim LSP Installer extension helps with this. Just run the command :LSPInstallInfo and a list of several servers will appear.

LSP Installer
LSP Installer dialog

To install, just press i on the desired server name.
To update, press u. For uninstalling, press x. Everything is very visual, and if you get lost, just press ? to access help.

Why not CoC?

Despite being very useful and much simpler to configure, it is slower. Before writing this article I used it daily, and Neovim with LSP configured in Lua proved to be much snappier.

Probably, the fact that CoC is done in TypeScript and VimScript, languages slower than Lua, are the cause of this.

Keyboard shortcuts

Nothing new here. In the same way I did with the options at the beginning of the article, I placed the functions with less readable names in local variables with names similar to those used in VimScript.

-- Key bindings
local map = vim.api.nvim_set_keymap
local kmap = vim.keymap.set
local opts = { noremap = true, silent = true }

-- Leader
vim.g.mapleader = " "

-- Vim
map("n", "<F5>", ":Neotree toggle<CR>", opts)
map("n", "<C-q>", ":q!<CR>", opts)
map("n", "<F4>", ":bd<CR>", opts)
map("n", "<F6>", ":sp<CR>:terminal<CR>", opts)
map("n", "<S-Tab>", "gT", opts)
map("n", "<Tab>", "gt", opts)
map("n", "<silent> <Tab>", ":tabnew<CR>", opts)
map("n", "<C-p>", ':lua require("telescope.builtin").find_files()<CR>', opts)

-- Diagnostics
kmap("n", "<space>e", vim.diagnostic.open_float, opts)
kmap("n", "[d", vim.diagnostic.goto_prev, opts)
kmap("n", "]d", vim.diagnostic.goto_next, opts)
kmap("n", "<space>q", vim.diagnostic.setloclist, opts)

local on_attach = function(client, bufnr)
  -- Enable completion triggered by <c-x><c-o>
  vim.api.nvim_buf_set_option(bufnr, "omnifunc", "v:lua.vim.lsp.omnifunc")

  -- Mappings.
  local bufopts = { noremap = true, silent = true, buffer = bufnr }
  kmap("n", "gD", vim.lsp.buf.declaration, bufopts)
  kmap("n", "gd", vim.lsp.buf.definition, bufopts)
  kmap("n", "K", vim.lsp.buf.hover, bufopts)
  kmap("n", "gi", vim.lsp.buf.implementation, bufopts)
  kmap("n", "<C-k>", vim.lsp.buf.signature_help, bufopts)
  kmap("n", "<space>wa", vim.lsp.buf.add_workspace_folder, bufopts)
  kmap("n", "<space>wr", vim.lsp.buf.remove_workspace_folder, bufopts)
  kmap("n", "<space>wl", function()
  end, bufopts)
  kmap("n", "<space>D", vim.lsp.buf.type_definition, bufopts)
  kmap("n", "<space>rn", vim.lsp.buf.rename, bufopts)
  kmap("n", "<space>ca", vim.lsp.buf.code_action, bufopts)
  kmap("n", "gr", vim.lsp.buf.references, bufopts)
  kmap("n", "<space>f", vim.lsp.buf.formatting, bufopts)
Enter fullscreen mode Exit fullscreen mode

Final details

One thing I hated as soon as I started using this setup was errors in virtual text on the same line. The error message is never concise enough to fit on the screen, and in the case of multiple occurrences, the readability is greatly affected.

Texto virtual
Errors in virtual text

With the code below, errors appear in floating boxes when holding the cursor over the error.

-- Floating diagnostics message
  float = { source = "always", border = border },
  virtual_text = false,
  signs = true,

-- Auto commands
vim.cmd([[ autocmd! CursorHold,CursorHoldI * lua vim.diagnostic.open_float(nil, {focus=false})]])
Enter fullscreen mode Exit fullscreen mode

Floating box error

If this doesn't bother you, just ignore this part.

Wrap up

Done! These lines are enough to have a working Neovim with a configuration file in Lua.

If you followed exactly what was shown so far, your init.lua ended up with about 380 lines, much more than the VimScript version (154 lines, adding init.vim and coc-settings.json).

To be honest, I thought it would be more than 500 lines, but I managed to reduce it a lot. The fact that I did it in a single file contributed to this, encouraging me to keep only the essentials. I did it that way too, because until the moment where I write these lines, I haven't found a tutorial that uses only one file. Usually, folders and folders with several Lua files are created in the process, which cause a lot of confusion in the beginning.

For the more experienced, obviously this configuration does not suit everything, but in my case, I was very satisfied with the result. In the next article, this file will be splitted into "modules", to improve the extensibility of this configuration.

If you want to check out the complete code, there's a link to my .dotfiles repository below.

See you!


If this article helped you in some way, consider donating. This will help me to create more content like this!

Top comments (13)

georgeanderson profile image
George Guimarães

Excellent writing, Elves. I'm yet to make the move to init.lua. But when I read such good articles such as this, I'm one inch more inclined to take the plunge. I just wish some of the configs, like code formatting, had less moving parts. Keep up the good work.

elvessousa profile image
Elves Sousa

Thanks a lot!

Same here... I wish less moving parts were needed, too. But the effort is worth it once you experience the speed boost given by Lua and Lua-based plugins.

dodobird profile image


voyeg3r profile image
Sérgio Araújo

Great article Elves! I would like to see "mason" instead of lsp-installer, from the same developer I think. I will keep an eye on your articles!

elvessousa profile image
Elves Sousa

Valeu, Sérgio!

Never heard of it before. I'll try it later!

nexxeln profile image
Shoubhit Dash

Amazing blog post, great config!

elvessousa profile image
Elves Sousa

Thanks a lot!

raguay profile image
Richard Guay

Have you tried the LunarVim configuration? It’s really nice also and completely in Lua.

moopet profile image
Ben Sinclair

I thought that's what this post would be about, given the word "moon" in the title.

elvessousa profile image
Elves Sousa

Hahah... That's interesting... "Lua" is the word for "Moon" in Portuguese, the native language of the Lua creators.

raguay profile image
Richard Guay

I actually thought the same until I read it and found out what Lua means.

elvessousa profile image
Elves Sousa

Not yet. It is the one Chris@Machine made, right?

raguay profile image
Richard Guay

Yes. His twitter is @chrisatmachine, so I assume it’s the same person. It’s been great for me.