If you're thinking about bookmarking or linking this page, maybe go for the main config instead. I try, but down here in the details it's a bit of a mess. Individual files get renamed. Lots.

My init.lua for Neovim

Updated

Gradually working in explainer text as I go. Working off of multiple resources.

I’d probably tuck all of these into different Lua files if I were assembling this like a normal project. Since I’m using Yarner to generate the config from this Markdown file, I’ll lean on the narrative flow for as long as possible and tangle everything to init.lua.

It’s all the same to Neovim once it gets loaded into memory.

init.lua global prep

Most Neovim Lua functionality is contained in the vim module. Pull some of the frequently used ones into the current namespace, to save a little typing for our fingers.

vim.cmd
vim commands (eg cmd('pwd'))
vim.fn
vim functions (eg fn.buffer())
vim.g
a table for global variables
vim.opt
vim options
local cmd = vim.cmd
local fn = vim.fn
local g = vim.g
local opt = vim.opt

helper functions

Conveniences I copied from other init.lua examples.

map

Creates mappings with noremap option enabled by default.

local function map(mode, lhs, rhs, opts)
  local options = {noremap = true}

  if opts then options = vim.tbl_extend('force', options, opts) end

  vim.api.nvim_set_keymap(mode, lhs, rhs, options)
end

Packer bootstrap

Install the plugin manager Packer if it’s not already installed.

local install_path = fn.stdpath('data')..'/site/pack/packer/start/packer.nvim'

if fn.empty(fn.glob(install_path)) > 0 then
  packer_bootstrap = fn.system({
    'git',
    'clone',
    '--depth', '1',
    'https://github.com/wbthomason/packer.nvim',
    install_path
  })
end

Load plugins via Packer

packer.nvim brings the flexibility — and complexity — of Emacs use-package to Neovim. How cool is that?

require('packer').startup(function(use)
  use "wbthomason/packer.nvim"

  -- ==> Load plugins.

  -- automatically set up your configuration after cloning packer.nvim
  -- put this at the end after all plugins
  if packer_bootstrap then
    require('packer').sync()
  end
end)
-- # Load plugins
-- ==> Load popup.nvim.
-- ==> Load plenary.nvim.
-- ==> Load nvim-web-devicons.
-- ==> Load nvim-treesitter.
-- ==> Load telescope.nvim.
-- ==> Load null-ls.nvim.
-- ==> Load nvim-lspconfig.
-- ==> Load which-key.nvim.
-- ==> Load nightfox.nvim.
-- ==> Load lualine.
-- ==> Load language support.
-- ==> Load Riv.

popup.nvim

-- # Load popup.nvim
use "nvim-lua/popup.nvim"

plenary.nvim

-- # Load plenary.nvim
use "nvim-lua/plenary.nvim"

nvim-web-devicons

-- # Load nvim-web-devicons
use "kyazdani42/nvim-web-devicons"

nvim-treesitter

nvim-treesitter is an experimental feature of Neovim. Something to do with syntax highlighting? Both it and the plugins that use it change frequently. So I better follow the instructions about keeping everything up to date when I sync.

-- # Load nvim-treesitter
use {
  "nvim-treesitter/nvim-treesitter",
  run = ":TSUpdate",
  config = function()
    require("nvim-treesitter.configs").setup {
      ensure_installed = "all",
      highlight = {
        enable = true,
      },
      additional_vim_regex_highlighting = false,
    }

    local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
    parser_config.just = {
      install_info = {
        url = "https://github.com/IndianBoy42/tree-sitter-just", -- local path or git repo
        files = { "src/parser.c", "src/scanner.cc" },
        branch = "main",
      },
      maintainers = { "@IndianBoy42" },
    }
  end
}

telescope.nvim

telescope.nvim is a ridiculously fancy fuzzy-finder.

-- # Load telescope.nvim
use { "nvim-telescope/telescope.nvim",
  requires = { {"nvim-lua/plenary.nvim"} },
}

Global key bindings for telescope.nvim

Showing the global telescope.nvim key bindings here, though Yarner will be inserting them outside all this plugin definition stuff. I haven’t figured out how to do global keybindings in a plugin setup quite yet.

-- # Add global bindings for telescope.nvim
map("n", "<leader>ff", "<cmd>lua require('telescope.builtin').find_files()<cr>")
map("n", "<leader>fg", "<cmd>lua require('telescope.builtin').live_grep()<cr>")
map("n", "<leader>fb", "<cmd>lua require('telescope.builtin').buffers()<cr>")
map("n", "<leader>fh", "<cmd>lua require('telescope.builtin').help_tags()<cr>")

null-ls.nvim

-- # Load null-ls.nvim
use {
  "jose-elias-alvarez/null-ls.nvim",
  config = function()
    require("null-ls").setup({
      sources = {
        require("null-ls").builtins.formatting.stylua,
        require("null-ls").builtins.code_actions.proselint,
      }
    })
  end
}

which-key.nvim

I first bumped into the which-key help menu in Doom Emacs. Start a chained key binding like SPC, a menu pops up showing what chains are available. Indispensable there. Indispensable here. Thank goodness folks are porting so many Emacs packages to Neovim.

-- # Load which-key.nvim
use {
  "folke/which-key.nvim",
  config = function()
    require("which-key").setup {}
  end
}

nvim-lspconfig

-- # Load nvim-lspconfig
use "neovim/nvim-lspconfig"

Stock LSP setup

-- # Use suggested config for LSP
local lsp_opts = { noremap=true, silent=true }
map('n', '<space>e', '<cmd>lua vim.diagnostic.open_float()<CR>', lsp_opts)
map('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<CR>', lsp_opts)
map('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<CR>', lsp_opts)
map('n', '<space>q', '<cmd>lua vim.diagnostic.setloclist()<CR>', lsp_opts)

-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local lspconfig_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.
  -- See `:help vim.lsp.*` for documentation on any of the below functions
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>ca', '<cmd>lua vim.lsp.buf.code_action()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', lsp_opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', lsp_opts)
end

-- ==> Set up individual LSP servers.

Set up LSP language servers

-- # Set up individual LSP servers
require("lspconfig").pyright.setup {
  on_attach = lspconfig_on_attach,
}
require("lspconfig").tsserver.setup {
  on_attach = lspconfig_on_attach,
}

nightfox.nvim

-- # Load nightfox.nvim
use "EdenEast/nightfox.nvim"

lualine.nvim

-- # Load lualine
use { 'nvim-lualine/lualine.nvim',
  requires = { 'kyazdani42/nvim-web-devicons', opt = true },
  config = function()
    require('lualine').setup({
      options = {
        theme = "duskfox",
      }
    })
  end,
}

Language support

A couple of the tools I use regularly require some special handling.

-- # Load language support
-- ==> Load nvim-nu.
-- ==> Load vim-jinja2-syntax
-- ==> Load black.

nvim-nu

nvim-nu adds #nushell support to Neovim.

-- # Load nvim-nu
use {
  "LhKipp/nvim-nu",
  run = ":TSInstall nu",
  config = function()
    require("nu").setup{}
  end
}

vim-jinja2-syntax

Vim-Jinja2-Syntax highlights the Jinja / Nunjucks / Tera family of text template languages

-- # Load vim-jinja2-syntax
use "glench/vim-jinja2-syntax"

black

Because pyright doesn’t provide formatting

-- # Load black
use { "psf/black", cmd = {"Black"}}
File hooks for black
-- # Format Python files with Black when saving
cmd[[autocmd BufWritePre *.py execute 'Black']]

Riv for knowledge management

I bounce way too much between systems. Right now I use Riv when in Neovim. What can I say? I like reStructuredText.

g:riv_file_link_style
use Riv’s :doc: role instead of [[...]] for wiki links
-- # Load Riv
use {
  "Rykka/riv.vim",
  config = function()
    riv_main = {
      path = "~/Dropbox/riv/main"
    }
    riv_work = {
      path = "~/work/riv"
    }
    vim.g.riv_projects = {riv_main, riv_work}
    vim.g.riv_file_link_style = 2
  end
}

Global Settings

-- ==> Set colorscheme.
-- ==> Set filetype behaviors and hooks.
-- ==> Set global variables.
-- ==> Set global options.
-- ==> Configure diagnostics.
-- ==> Add global key bindings.
-- ==> Use suggested config for LSP.

Set a colorscheme

-- # Set colorscheme
cmd[[colorscheme nightfox]]

Filetype behaviors and hooks

-- # Set filetype behaviors and hooks
cmd[[filetype plugin on]]
cmd[[autocmd FileType * setlocal formatoptions-=cro]]
cmd[[autocmd FocusGained * checktime]]
-- ==> Format Python files with Black when saving.
-- ==> Recognize Astro files.

Recognize Astro files

Eventually Astro will be able to fully build my site. And I’ll be ready, thanks to treesitter. I just need to tell the editor which ones are the Astro files.

-- # Recognize Astro files
cmd[[autocmd BufEnter *.astro set ft=astro]]

A few global variables

I use SPC as my global leader key, and the comma for my local leader. Also, I tend to fiddle a lot with Pyenv so I set up a dedicated virtualenv for Neovim.

-- # Set global variables
g.mapleader = ' '
g.maplocalleader = ','
g.python3_host_prog = '~/.pyenv/versions/neovim/bin/python'

g.markdown_fenced_languages = {
  "bash=sh",
  "python",
  "lua",
}
g.rst_syntax_code_list = { "python" }

Global options

-- # Set global options
opt.autoread = true
opt.background = 'dark'
opt.completeopt = {'menuone', 'noinsert', 'noselect'}  -- completion options (for deoplete)
opt.cursorline = true               -- highlight current line
opt.encoding = "utf-8"
opt.expandtab = true                -- spaces instead of tabs
opt.hidden = true                   -- enable background buffers
opt.ignorecase = true               -- ignore case in search
opt.joinspaces = false              -- no double spaces with join
opt.list = true                     -- show some invisible characters
opt.maxmempattern = 1000            -- for Riv
opt.mouse = "nv"                    -- Enable mouse in normal and visual modes
opt.number = true                   -- show line numbers
opt.relativenumber = true           -- number relative to current line
opt.scrolloff = 4                   -- lines of context
opt.shiftround = true               -- round indent
opt.shiftwidth = 2                  -- size of indent
opt.sidescrolloff = 8               -- columns of context
opt.smartcase = true                -- do not ignore case with capitals
opt.smartindent = true              -- insert indents automatically
opt.splitbelow = true              -- put new windows below current
opt.splitright = true               -- put new vertical splits to right
opt.termguicolors = true            -- truecolor support
opt.wildmode = {'list', 'longest'}  -- command-line completion mode
opt.wrap = false  -- disable line wrap

Diagnostic settings

See :help vim.diagnostic.* for documentation on any of the below functions

-- # Configure diagnostics
vim.diagnostic.config({
  virtual_text = false,
})

Global key bindings

-- # Add global key bindings
-- ==> Add global bindings for telescope.nvim.
-- ==> <bs> clears search highlights.

I described the telescope.nvim and LSP global bindings earlier. This is where they get used.

Then a little thing I’ve gotten used to. Let search highlight matches, but clear those highlights out when I hit Backspace.

-- # <bs> clears search highlights
map("n", "<bs>", ":nohlsearch<cr>", { silent = true })