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

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"
  use "nvim-lua/popup.nvim"
  use "nvim-lua/plenary.nvim"
  use "kyazdani42/nvim-web-devicons"
  -- ==> Load nvim-treesitter.
  use { "nvim-telescope/telescope.nvim",
    requires = { {"nvim-lua/plenary.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
  }
  -- ==> Load which-key.
  -- ==> Load colorscheme plugins.
  -- ==> Load lualine.
  -- ==> Load language support.

  use "neovim/nvim-lspconfig"
  -- pyright doesn't provide formatting
  use { "psf/black", cmd = {"Black"}}

  use "Rykka/riv.vim"

  -- 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)

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
}

which-key

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
use {
  "folke/which-key.nvim",
  config = function()
    require("which-key").setup {}
  end
}

colorscheme plugins

Off in its own code block because I tend to cycle through them a lot.

-- # Load colorscheme plugins
use { "catppuccin/nvim", as = "catpuccin" }
use 'pineapplegiant/spaceduck'
use 'folke/tokyonight.nvim'
use 'EdenEast/nightfox.nvim'

Lualine

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

Load language support

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

nvim-nu.nvim
adds support for #nushell to Neovim
Vim-Jinja2-Syntax
highlights the Jinja / Nunjucks / Tera family of text template languages
-- # Load language support
use {
  "LhKipp/nvim-nu",
  run = ":TSInstall nu",
  config = function()
    require("nu").setup{}
  end
}

use "glench/vim-jinja2-syntax"

Filetype behaviors

cmd[[filetype plugin on]]
cmd[[autocmd FileType * setlocal formatoptions-=cro]]
cmd[[autocmd FocusGained * checktime]]
cmd[[colorscheme spaceduck]]
cmd[[autocmd BufWritePre *.py execute 'Black']]

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.

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.

g.mapleader = ' '
g.maplocalleader = ','
g.python3_host_prog = '~/.pyenv/versions/neovim/bin/python'

All the 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

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

Key mappings

General key mappings

map("n", "<bs>", ":nohlsearch<cr>", { silent = true })

Telescope mappings

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>")

LSP

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

vim.diagnostic.config({
  virtual_text = false,
})

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

Pyright language server

require("lspconfig")["pyright"].setup {
  on_attach = on_attach,
  flags = {
    -- This will be the default in neovim 0.7+
    debounce_text_changes = 150,
  }
}