Skip Navigation

Basic Neovim Configuration

This lesson explains how to configure the Neovim editor with some settings that are helpful for system administration purposes.

Page Contents

Introduction to Neovim

Out of the box, Alpine Linux includes a basic implementation of the Vi editor provided by BusyBox. While vi gets the job done for basic setup tasks, it does have several limitations. First, this version of Vi only supports the most basic Vi commands. There is an improved version of Vi called Vim (for Vi Improved) that offers more commands and functionality.1 However, Vim has grown quite bloated in terms of its code base, and it must be configured using the somewhat arcane Vimscript language.2

The Neovim project has reimplemented Vim with a cleaner design that makes it easier to extend and reuse.3 In addition, Neovim can be configured with Lua4 instead of Vimscript. Lua is a simple language that is often used to automate various applications, including video games (Roblox, World of Warcraft, and Dota 2 are just a few examples).

Installing Neovim

To install Neovim on Alpine Linux, run:

doas apk add neovim tree-sitter-lua hunspell hunspell-en-us

Basic Configuration

Before we can configure and run Neovim for the first time, we need to create the directory that will hold the configuration file. We’re going to configure Neovim only for our user account. Since enterprise systems often have more than one system administrator, each of whom may have different configuration preferences, it is better to configure an editor on a per-user basis instead of system-wide. Another issue that can occur with an advanced editor like Neovim is that too much customization can have security implications if this editor is run as the root user. In fact, I recommend using the BusyBox Vi editor whenever it is necessary to run the editor as the root user.

To create our user configuration directory and start editing the configuration file, run:

mkdir -p $HOME/.config/nvim
nvim $HOME/.config/nvim/init.lua

Go into insert mode (Neovim uses the i key, just like regular Vi) and add the following Lua code:

vim.opt.number = true
vim.opt.cursorline = true
vim.opt.showmode = true

Press the Escape key. Save the file with :w, but do NOT exit (NO q).

You can now reload the lua configuration by typing:

:luafile %

and pressing Enter.

Magically, you should see line numbers appear on the left side, and the line number containing the cursor should be highlighted. Move to the bottom line if the cursor isn’t already there. Press o to open a new line below it, press Enter to leave a blank line, and then add these two lines to make searching case-insensitive by default:

vim.opt.ignorecase = true
vim.opt.smartcase = true

Save and reload the lua configuration to test it. You won’t see much change this time. Add another blank line and add some lines to enable mouse support once we get to the graphical environment:

vim.opt.clipboard = 'unnamedplus'
vim.opt.completeopt = {'menu', 'menuone', 'noselect'}
vim.opt.mouse = 'a'

Again, save and reload the configuration to be sure you didn’t make a mistake. Add another blank line and configure automatic indentation to use 4 spaces, following the standard used in Python:

vim.opt.tabstop = 4
vim.opt.softtabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.smartindent = true

Save and reload the configuration, then enable spellcheck by adding a blank line followed by:

vim.opt.spelllang = 'en_us'
vim.opt.spell = true

Save and reload the configuration. You won’t see any indication of misspelled words just yet, but this is expected. Add a blank line and enable syntax highlighting like so:

vim.cmd('syntax enable')
vim.cmd('filetype plugin indent on')

Save and reload the configuration. Again, there will be no changes. Finally, leave a blank line and then add some color settings and a Lua comment with some misspelled words in it:

vim.cmd('colorscheme elflord')
vim.cmd('hi SpellBad ctermbg=red')

-- this iz teh end!

Save and reload the configuration. Your colors should now be a lot more vivid, and you should have working syntax highlighting of your Lua code. The misspelled words in your Lua comment at the bottom should be highlighted in red.

Saving as Root

A common issue encountered in system administration is that one often forgets to open a system file as the root user before trying to edit it. After making various changes, this fact is only discovered when an attempt to save the file fails with a permission denied error.

To avoid this problem, we can use Lua to write a small program that implements a command named “:W” with an uppercase W. This command will automatically call doas for us in order to save a copy of the file as the root user. Since this Lua program is a bit complex, it would be better to store it in a separate file instead of just typing it into our init.lua.

Exit Neovim if you haven’t already done so, then run the following commands in the shell:

mkdir $HOME/.config/nvim/lua
nvim $HOME/.config/nvim/lua/save_as_root.lua

We’re creating a file named “save_as_root.lua” in a “lua” subdirectory of our Neovim configuration directory.

Into this file, carefully insert the following Lua code. The lines starting with two dashes are comments that explain how the save_as_root function works. You do not need to type in these lines, as you can just refer back to my website in the future for an explanation.

local function save_as_root(opts)
    local tool = nil

    -- On Alpine Linux, we use doas. However, make this code work on any Linux
    -- distribution by also supporing sudo. Figure out which one we have,
    -- favoring doas if both are installed.
    if vim.system({'which', 'doas'}):wait().code == 0 then
        tool = 'doas'
    elseif vim.system({'which', 'sudo'}):wait().code == 0 then
        tool = 'sudo'
    end

    -- If neither doas nor sudo are available, return since we can't do anything
    if not tool then
        print('Error: neither doas nor sudo is available')
        return
    end

    local output = ''

    -- We now define a function inside a function (an example of a closure) that lets
    -- us detect if doas/sudo asks for a password. If it asks, we ask the user to
    -- enter their password using vim.fn.inputsecret in order to obscure it. We then
    -- send that password over to doas/sudo to authenticate.
    local function handle_stdout(channel_id, data, name)
        if #data > 0 then
            if data[1]:find('password') then
                local password = vim.fn.inputsecret('(' .. tool .. ') password: ')
                vim.fn.chansend(channel_id, { password, '' })
            else
                output = output .. data[1]:gsub('\r', '\n')
            end
        end
    end

    -- Create a temporary file by getting a temporary filename and writing the contents
    -- of our buffer to it.
    local temp_file = vim.fn.tempname()
    vim.cmd.write(temp_file)

    -- Although we might not use it often, :w supports "save as" functionality by letting
    -- us specify an alternate filename as an argument. We can support the same thing
    -- for :W by picking up the argument.
    local ebang = true
    local filename = vim.api.nvim_buf_get_name(0)
    if opts.args ~= '' then
        ebang = false
        filename = opts.args
    end

    -- Both doas and sudo expect to be running directly in a terminal emulator
    -- (either an actual tty or a pseudoterminal [pty]). Neither command is
    -- happy with basic input redirection, so we have to use the following
    -- construct. What we're doing is using doas/sudo to copy our temporary file
    -- to our original file. We could use the cp command here, but to avoid the
    -- risk of shell differences, we use dd instead.
    local channel_id = vim.fn.jobstart({tool, 'dd', 'if=' .. temp_file, 'of=' .. filename}, {
        on_stdout = handle_stdout,
        pty = true,
    })

    if vim.fn.jobwait({channel_id})[1] == 0 then
        -- Success: reload the buffer and clear the modified flag, unless we saved using an
        -- alternate filename
        if ebang then
            vim.cmd('e!')
        end

        vim.cmd('redraw')
        vim.print('Written (as root): ' .. filename)
    else
        -- Something went wrong, so display the error message in the status line
        vim.print(output)
    end

    -- Clean up the temporary file
    vim.fn.delete(temp_file)
end

vim.api.nvim_create_user_command('W', save_as_root, {desc = 'Save file as root', nargs = '?'})

Double-check that you didn’t make any typos, then save the file and exit Neovim.

Go back to editing your init.lua file:

nvim $HOME/.config/nvim/init.lua

At the bottom of the file, you can replace the misspelled comment with:

require('save_as_root')

Save, exit, then restart Neovim. Check for any error messages.

Now, you can test this functionality by editing /etc/motd. As your regular user, run:

nvim /etc/motd

Add a line to the bottom of the file. It can say anything you want (Hello, World maybe?). If you try to save by pressing Escape and then giving the :w command, you will see a permission error. Test your function by using an upper-case W (:W). You should be prompted for your user account password. After you enter it, the file should be saved (if you typed your password correctly, at least).

You can verify that your message of the day file has been saved successfully by exiting Neovim and then running:

exit

Log in as your regular user again, and your modified message should be displayed.

Completed Configuration

Upon finishing the configuration, your Neovim init.lua should look like the following (I put blank lines between sections for clarity):

vim.opt.ignorecase = true
vim.opt.smartcase = true

vim.opt.number = true
vim.opt.cursorline = true
vim.opt.showmode = true

vim.opt.ignorecase = true
vim.opt.smartcase = true

vim.opt.clipboard = 'unnamedplus'
vim.opt.completeopt = {'menu', 'menuone', 'noselect'}
vim.opt.mouse = 'a'

vim.opt.tabstop = 4
vim.opt.softtabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.smartindent = true

vim.opt.spelllang = 'en_us'
vim.opt.spell = true

vim.cmd('syntax enable')
vim.cmd('filetype plugin indent on')

vim.cmd('colorscheme elflord')
vim.cmd('hi SpellBad ctermbg=red')

require('save_as_root')

Optional Customization

This section explains optional additional ways to configure Neovim. There are plenty of example configurations “out there” on the Internet that you can follow.

Perhaps the easiest first tweak is to find a different color scheme if you don’t like elflord. A quick way to test color schemes is to run:

nvim

Run the following command in Normal Mode to see a list of available color schemes:

:r!apk info -L neovim | grep 'runtime/colors' | awk -F '/' '{print $NF}'

Everything that ends in .vim is a possible color scheme. To test another color scheme, use the :colorscheme command. For example, there is (ironically) a color scheme named “murphy.vim” (not mine). I can switch to it using:

:colorscheme murphy

Try out the other color schemes by simply changing the “murphy” to the name of another color scheme from the list (minus the .vim ending). Once you have found a color scheme you like, edit your init.lua file and change the line:

vim.cmd('colorscheme elflord')

to the name of your desired color scheme.

There are many, many more things you can do with Neovim. Quite a few websites explain how to install package managers that let you download preconfigured Neovim configurations and plugins. Some of these resources include:

Notes and References


  1. Vim

  2. Vimscript 

  3. Neovim

  4. Lua