diff --git a/config/neovim/after/ftplugin/gitcommit.lua b/config/neovim/after/ftplugin/gitcommit.lua
new file mode 100644
index 00000000..f1aaed51
--- /dev/null
+++ b/config/neovim/after/ftplugin/gitcommit.lua
@@ -0,0 +1,8 @@
+vim.opt_local.colorcolumn = "50,72"
+vim.opt_local.spell = true
+vim.opt_local.textwidth = 72
+
+-- autocmd FileType gitcommit highlight ColorColumn ctermbg=8
+-- filetype indent on
+-- filetype on
+-- filetype plugin on
diff --git a/config/neovim/after/ftplugin/markdown.lua b/config/neovim/after/ftplugin/markdown.lua
new file mode 100644
index 00000000..5d96d74f
--- /dev/null
+++ b/config/neovim/after/ftplugin/markdown.lua
@@ -0,0 +1,4 @@
+-- TODO: Interim fix for https://github.com/nvim-treesitter/nvim-treesitter-context/issues/431.
+require("treesitter-context").disable()
+
+vim.opt_local.wrap = true;
diff --git a/config/neovim/after/ftplugin/rst.lua b/config/neovim/after/ftplugin/rst.lua
new file mode 100644
index 00000000..7aa8eee1
--- /dev/null
+++ b/config/neovim/after/ftplugin/rst.lua
@@ -0,0 +1,13 @@
+local opt = vim.opt_local
+
+opt.spell = true
+opt.wrap = true
+
+local cmp = require "cmp"
+local sources = cmp.get_config().sources
+
+-- TODO: confirm these aren't aleady in the list of sources to avoid duplicate suggestions.
+table.insert(sources, { name = "buffer" })
+table.insert(sources, { name = "path" })
+
+cmp.setup.buffer { sources = sources }
diff --git a/config/neovim/after/ftplugin/term.vim b/config/neovim/after/ftplugin/term.vim
new file mode 100644
index 00000000..16bbc95e
--- /dev/null
+++ b/config/neovim/after/ftplugin/term.vim
@@ -0,0 +1,4 @@
+setlocal norelativenumber
+setlocal nonumber
+
+setlocal scrolloff=0
diff --git a/config/neovim/autoload/opdavies.vim b/config/neovim/autoload/opdavies.vim
new file mode 100644
index 00000000..449666bf
--- /dev/null
+++ b/config/neovim/autoload/opdavies.vim
@@ -0,0 +1,12 @@
+if !exists('*opdavies#save_and_exec')
+  function! opdavies#save_and_exec() abort
+    if &filetype == 'vim'
+      :silent! write
+      :source %
+    elseif &filetype == 'lua'
+      :silent! write
+      :luafile %
+    endif
+    return
+  endfunction
+endif
diff --git a/config/neovim/lua/opdavies/globals.lua b/config/neovim/lua/opdavies/globals.lua
new file mode 100644
index 00000000..d6fd70c6
--- /dev/null
+++ b/config/neovim/lua/opdavies/globals.lua
@@ -0,0 +1,13 @@
+P = function(v)
+  print(vim.inspect(v))
+  return v
+end
+
+RELOAD = function(...)
+  return require("plenary.reload").reload_module(...)
+end
+
+R = function(name)
+  RELOAD(name)
+  return require(name)
+end
diff --git a/config/neovim/lua/opdavies/init.lua b/config/neovim/lua/opdavies/init.lua
new file mode 100644
index 00000000..7e5bc84d
--- /dev/null
+++ b/config/neovim/lua/opdavies/init.lua
@@ -0,0 +1,6 @@
+pcall("require", impatient)
+
+require "opdavies.globals"
+require "opdavies.keymaps"
+require "opdavies.options"
+require "opdavies.lsp"
diff --git a/config/neovim/lua/opdavies/keymaps.lua b/config/neovim/lua/opdavies/keymaps.lua
new file mode 100644
index 00000000..1693bc6d
--- /dev/null
+++ b/config/neovim/lua/opdavies/keymaps.lua
@@ -0,0 +1,109 @@
+local set = vim.keymap.set
+
+set("n", "<Leader>so", ":call opdavies#save_and_exec()<CR>")
+
+-- Format paragraphs to an 80 character line length.
+set("n", "<Leader>g", "gqap")
+set("x", "<Leader>g", "gqa")
+
+-- Make the current file executable
+set("n", "<Leader>x", ":!chmod +x %<Cr>")
+
+-- Yank from the current column to the end of the line
+set("n", "Y", "yg$")
+
+-- Keep things centred
+set("n", "n", "nzzzv")
+set("n", "N", "Nzzzv")
+
+-- Disable up and down arrow keys.
+set("v", "<down>", "<nop>")
+set("v", "<up>", "<nop>")
+
+-- Use the left and right arrow keys to change tabs.
+set("v", "<left>", "gT")
+set("v", "<right>", "gt")
+
+-- Easily switch back to visual mode.
+set("i", "jk", "<Esc>")
+
+-- Easy insertion of a trailing ; or , from insert mode
+set("i", ",,", "<Esc>A,<Esc>")
+set("i", ";;", "<Esc>A;<Esc>")
+
+set("n", "ga", "<Plug>(EasyAlign)")
+set("x", "ga", "<Plug>(EasyAlign)")
+
+-- Focus on the current buffer.
+set("n", "<leader>-", ":wincmd _<cr>:wincmd |<cr>", { noremap = true, silent = true })
+
+-- Automatically resize buffers.
+set("n", "<leader>=", ":wincmd =<cr>", { noremap = true, silent = true })
+
+-- Move line(s) up and down.
+local opts = { noremap = true, silent = true }
+set("i", "<M-j>", "<Esc>:m .+1<CR>==gi", opts)
+set("i", "<M-k>", "<Esc>:m .-2<CR>==gi", opts)
+set("n", "<M-j>", ":m .+1<CR>==", opts)
+set("n", "<M-k>", ":m .-2<CR>==", opts)
+set("v", "<M-j>", ":m '>+1<CR>gv=gv", opts)
+set("v", "<M-k>", ":m '<-2<CR>gv=gv", opts)
+
+-- Re-centre when navigating.
+set("n", "#", "#zz", opts)
+set("n", "%", "%zz", opts)
+set("n", "*", "*zz", opts)
+set("n", "<C-d>", "<C-d>zz", opts)
+set("n", "<C-i>", "<C-i>zz", opts)
+set("n", "<C-o>", "<C-o>zz", opts)
+set("n", "<C-u>", "<C-u>zz", opts)
+set("n", "G", "Gzz", opts)
+set("n", "N", "Nzz", opts)
+set("n", "gg", "ggzz", opts)
+set("n", "n", "Nzz", opts)
+set("n", "{", "{zz", opts)
+set("n", "}", "}zz", opts)
+
+-- Clears hlsearch after doing a search, otherwise just does normal <CR> stuff
+vim.cmd [[ nnoremap <expr> <CR> {-> v:hlsearch ? ":nohl\<CR>" : "\<CR>"}() ]]
+
+-- Quicker macro playback.
+set("n", "Q", "@qj")
+set("x", "Q", ":norm @q<CR>")
+
+-- Easier navigation between splits.
+set("n", "<C-h>", "<C-w><C-h>")
+set("n", "<C-j>", "<C-w><C-j>")
+set("n", "<C-k>", "<C-w><C-k>")
+set("n", "<C-l>", "<C-w><C-l>")
+
+set("v", "Q", "<nop>")
+
+set("v", "J", ":m '>+1<CR>gvrgv")
+set("v", "K", ":m '<-2<CR>gv=gv")
+
+set("n", "J", "mzJ`z")
+set("n", "<C-d>", "<C-d>zz")
+set("n", "<C-u>", "<C-u>zz")
+set("n", "n", "nzzzv")
+set("n", "N", "Nzzzv")
+
+-- Easily access project-specific notes.
+set("n", "<leader>en", function()
+  if vim.fn.filereadable ".ignored/notes" == 1 then
+    vim.cmd "tabnew .ignored/notes"
+  else
+    vim.cmd "tabnew notes"
+  end
+end)
+
+-- Easily access project-specific todos.
+set("n", "<leader>et", function()
+  if vim.fn.filereadable ".ignored/todo" == 1 then
+    vim.cmd "tabnew .ignored/todo"
+  else
+    vim.cmd "tabnew todo"
+  end
+end)
+
+set("n", "<leader>ec", ":edit composer.json")
diff --git a/config/neovim/lua/opdavies/lsp/handlers.lua b/config/neovim/lua/opdavies/lsp/handlers.lua
new file mode 100644
index 00000000..e44e86af
--- /dev/null
+++ b/config/neovim/lua/opdavies/lsp/handlers.lua
@@ -0,0 +1,71 @@
+local M = {}
+
+local function should_remove_diagnostic(messages_to_filter, message)
+  for _, filter_message in ipairs(messages_to_filter) do
+    if message:match(filter_message) then
+      return true
+    end
+  end
+
+  return false
+end
+
+M.definition = function()
+  local params = vim.lsp.util.make_position_params()
+
+  vim.lsp.buf_request(0, "textDocument/definition", params, function(err, result, ctx, config)
+    local bufnr = ctx.bufnr
+    local ft = vim.api.nvim_buf_get_option(bufnr, "filetype")
+
+    local new_result = vim.tbl_filter(function(v)
+      -- Remove any definitions within the nix store via the .direnv directory.
+      if string.find(v.targetUri, ".direnv") then
+        return false
+      end
+
+      -- Remove definitions within vendor-bin directory for PHP files.
+      if ft == "php" then
+        if string.find(v.targetUri, "vendor%-bin") then
+          return false
+        end
+      end
+
+      return true
+    end, result)
+
+    if #new_result > 0 then
+      result = new_result
+    end
+
+    vim.lsp.handlers["textDocument/definition"](err, result, ctx, config)
+    vim.cmd [[normal! zz]]
+  end)
+end
+
+M.on_publish_diagnostics = function(_, result, ctx, config)
+  local client = vim.lsp.get_client_by_id(ctx.client_id)
+
+  if client.name == "cssls" then
+    local filtered_diagnostics = {}
+
+    local messages_to_filter = {
+      "Unknown at rule @apply",
+      "Unknown at rule @tailwind",
+    }
+
+    -- For each diagnostic, ensure its mesages doesn't match one I want to
+    -- ignore before adding it to the result. If it matches, don't add it to the
+    -- result and it won't be shown.
+    for _, diagnostic in ipairs(result.diagnostics) do
+      if not should_remove_diagnostic(messages_to_filter, diagnostic.message) then
+        table.insert(filtered_diagnostics, diagnostic)
+      end
+    end
+
+    result.diagnostics = filtered_diagnostics
+  end
+
+  vim.lsp.diagnostic.on_publish_diagnostics(_, result, ctx, config)
+end
+
+return M
diff --git a/config/neovim/lua/opdavies/lsp/init.lua b/config/neovim/lua/opdavies/lsp/init.lua
new file mode 100644
index 00000000..6ae90f76
--- /dev/null
+++ b/config/neovim/lua/opdavies/lsp/init.lua
@@ -0,0 +1,126 @@
+local lspconfig = require "lspconfig"
+local nvim_status = require "lsp-status"
+
+local handlers = require "opdavies.lsp.handlers"
+
+require("neodev").setup {}
+
+local servers = {
+  bashls = true,
+
+  cssls = {
+    on_attach = function(client, bufnr)
+      vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(handlers.on_publish_diagnostics, {})
+    end,
+  },
+
+  gopls = true,
+  html = true,
+
+  intelephense = {
+    filetypes = { "php", "module", "test", "inc" },
+  },
+
+  lua_ls = {
+    settings = {
+      Lua = {
+        completion = {
+          callSnippet = "Replace",
+        },
+
+        diagnostics = {
+          globals = { "vim" },
+        },
+
+        runtime = {
+          version = "LuaJIT",
+        },
+
+        telemetry = {
+          enabled = false,
+        },
+
+        workspace = {
+          library = vim.api.nvim_get_runtime_file("", true),
+        },
+      },
+    },
+  },
+
+  marksman = true,
+  nil_ls = true,
+
+  tailwindcss = {
+    filetypes = { "html", "javascript", "twig", "typescript", "vue" },
+
+    settings = {
+      init_options = {
+        userLanguages = {
+          ["html.twig"] = "html",
+        },
+      },
+    },
+  },
+
+  terraformls = true,
+  tsserver = true,
+  vuels = true,
+
+  yamlls = {
+    settings = {
+      yaml = {
+        keyOrdering = false,
+      },
+    },
+  },
+}
+
+local capabilities = require("cmp_nvim_lsp").default_capabilities(vim.lsp.protocol.make_client_capabilities())
+
+for server_name, config in pairs(servers) do
+  if config == true then
+    config = {}
+  end
+
+  config = vim.tbl_deep_extend("force", {}, {
+    capabilities = capabilities,
+  }, config)
+
+  lspconfig[server_name].setup(config)
+end
+
+vim.diagnostic.config {
+  float = { source = true },
+  signs = true,
+  underline = false,
+  update_in_insert = false,
+  virtual_text = { spacing = 2 },
+}
+
+vim.api.nvim_create_autocmd("LspAttach", {
+  callback = function()
+    local builtin = require "telescope.builtin"
+
+    -- buf_inoremap { "<C-k>", vim.lsp.buf.signature_help }
+    -- buf_nnoremap { "<leader>ca", vim.lsp.buf.code_action }
+    -- buf_nnoremap { "<leader>d", vim.diagnostic.open_float }
+    -- buf_nnoremap { "<leader>rn", vim.lsp.buf.rename }
+    -- buf_nnoremap { "<leader>rr", "<cmd>LspRestart<cr>" }
+    -- buf_nnoremap { "[d", vim.diagnostic.goto_prev }
+    -- buf_nnoremap { "]d", vim.diagnostic.goto_next }
+
+    -- buf_nnoremap { "gD", vim.lsp.buf.declaration }
+    -- buf_nnoremap { "gd", handlers.definition }
+    -- buf_nnoremap { "gi", vim.lsp.buf.implementation }
+    -- buf_nnoremap { "gT", vim.lsp.buf.type_definition }
+
+    vim.keymap.set("n", "gd", builtin.lsp_definitions, { buffer = 0 })
+    vim.keymap.set("n", "gr", builtin.lsp_references, { buffer = 0 })
+    vim.keymap.set("n", "gD", vim.lsp.buf.declaration, { buffer = 0 })
+    vim.keymap.set("n", "gT", vim.lsp.buf.type_definition, { buffer = 0 })
+    vim.keymap.set("n", "K", vim.lsp.buf.hover, { buffer = 0 })
+
+    vim.keymap.set("n", "<space>cr", vim.lsp.buf.rename, { buffer = 0 })
+    vim.keymap.set("n", "<space>ca", vim.lsp.buf.code_action, { buffer = 0 })
+  end,
+})
diff --git a/config/neovim/lua/opdavies/options.lua b/config/neovim/lua/opdavies/options.lua
new file mode 100644
index 00000000..cb6a5895
--- /dev/null
+++ b/config/neovim/lua/opdavies/options.lua
@@ -0,0 +1,52 @@
+vim.g.mapleader = " "
+vim.g.snippets = "luasnip"
+
+local settings = {
+  autoindent = true,
+  backup = false,
+  breakindent = true,
+  colorcolumn = "80",
+  expandtab = true,
+  foldlevel = 1,
+  foldlevelstart = 99,
+  foldmethod = "indent",
+  formatoptions = "clqjp",
+  hidden = false,
+  hlsearch = false,
+  inccommand = "split",
+  laststatus = 3,
+  linebreak = true,
+  list = true,
+  mouse = "",
+  number = true,
+  pumblend = 10,
+  pumheight = 10,
+  relativenumber = true,
+  scrolloff = 5,
+  shiftwidth = 2,
+  showmode = false,
+  signcolumn = "yes:1",
+  smartindent = true,
+  softtabstop = 2,
+  spellfile = "/home/opdavies/Code/opdavies.nvim/spell/en.utf-8.add",
+  swapfile = false,
+  syntax = "on",
+  tabstop = 2,
+  termguicolors = true,
+  textwidth = 0,
+  undodir = os.getenv "HOME" .. "/.vim/undodir",
+  undofile = true,
+  updatetime = 1000,
+  wrap = false,
+}
+
+for key, value in pairs(settings) do
+  vim.o[key] = value
+end
+
+vim.opt.backupdir:remove "." -- keep backups out of the current directory
+vim.opt.clipboard:append "unnamedplus"
+vim.opt.completeopt = { "menu", "menuone", "noinsert", "noselect" }
+vim.opt.listchars:append {
+  trail = "ยท",
+}
diff --git a/config/neovim/lua/opdavies/snippets/ft/bash.lua b/config/neovim/lua/opdavies/snippets/ft/bash.lua
new file mode 100644
index 00000000..4859a803
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/bash.lua
@@ -0,0 +1,31 @@
+local ls = require "luasnip"
+
+local fmta = require("luasnip.extras.fmt").fmta
+
+return {
+  run = fmta(
+    [=[
+    #!/usr/bin/env bash
+
+    set -o errexit
+    set -o nounset
+    set -o pipefail
+
+    function help {
+      printf "%s <<task>> [args]\n\nTasks:\n" "${0}"
+
+      compgen -A function | grep -v "^_" | cat -n
+
+      printf "\nExtended help:\n  Each task has comments for general usage\n"
+    }
+
+    # Include any local tasks.
+    # https://stackoverflow.com/a/6659698
+    [[ -e "${BASH_SOURCE%/*}/run.local" ]] && source "${BASH_SOURCE%/*}/run.local"
+
+    TIMEFORMAT="Task completed in %3lR"
+    time "${@:-help}"
+    ]=],
+    {}
+  ),
+}
diff --git a/config/neovim/lua/opdavies/snippets/ft/javascript.lua b/config/neovim/lua/opdavies/snippets/ft/javascript.lua
new file mode 100644
index 00000000..d3e795f8
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/javascript.lua
@@ -0,0 +1,10 @@
+local fmta = require("luasnip.extras.fmt").fmta
+local ls = require "luasnip"
+
+local i = ls.insert_node
+
+local M = {
+  log = fmta("console.log(<>);", { i(1, "value") }),
+}
+
+return M
diff --git a/config/neovim/lua/opdavies/snippets/ft/lua.lua b/config/neovim/lua/opdavies/snippets/ft/lua.lua
new file mode 100644
index 00000000..c2abfba8
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/lua.lua
@@ -0,0 +1,27 @@
+local ls = require "luasnip"
+
+local fmt = require("luasnip.extras.fmt").fmt
+local rep = require("luasnip.extras").rep
+
+local f, i = ls.function_node, ls.insert_node
+
+return {
+  pcall = fmt(
+    [[
+      local status_ok, {} = pcall(require, "{}")
+      if not status_ok then
+        return
+      end
+    ]],
+    { i(1), rep(1) }
+  ),
+
+  req = fmt([[local {} = require "{}"]], {
+    f(function(import_name)
+      local parts = vim.split(import_name[1][1], ".", true)
+
+      return parts[#parts] or ""
+    end, { 1 }),
+    i(1),
+  }),
+}
diff --git a/config/neovim/lua/opdavies/snippets/ft/markdown.lua b/config/neovim/lua/opdavies/snippets/ft/markdown.lua
new file mode 100644
index 00000000..b2a76e46
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/markdown.lua
@@ -0,0 +1,20 @@
+local fmt = require("luasnip.extras.fmt").fmt
+local ls = require "luasnip"
+
+local i = ls.insert_node
+
+local M = {
+  frontmatter = fmt(
+    [[
+    ---
+    title: {}
+    ---
+    {}
+    ]],
+    { i(1), i(0) }
+  ),
+
+  link = fmt([[[{}]({}){} ]], { i(1), i(2), i(0) }),
+}
+
+return M
diff --git a/config/neovim/lua/opdavies/snippets/ft/nix.lua b/config/neovim/lua/opdavies/snippets/ft/nix.lua
new file mode 100644
index 00000000..1ef44d5c
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/nix.lua
@@ -0,0 +1,23 @@
+local fmta = require("luasnip.extras.fmt").fmta
+local ls = require "luasnip"
+
+local c = ls.choice_node
+local i = ls.insert_node
+local t = ls.text_node
+
+local M = {
+  vimplugin = fmta(
+    [[
+    {
+      plugin = <>.<>;
+      type = "lua";
+      config = ''
+        <>
+      '';
+    }<>
+    ]],
+    { c(1, { t "vimPlugins", t "customVim" }), i(2), i(3), i(0) }
+  ),
+}
+
+return M
diff --git a/config/neovim/lua/opdavies/snippets/ft/php.lua b/config/neovim/lua/opdavies/snippets/ft/php.lua
new file mode 100644
index 00000000..77ecd67a
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/php.lua
@@ -0,0 +1,115 @@
+local fmta = require("luasnip.extras.fmt").fmta
+local ls = require "luasnip"
+
+local c = ls.choice_node
+local f = ls.function_node
+local i = ls.insert_node
+local t = ls.text_node
+
+local M = {
+  __construct = fmta(
+    [[
+    public function __construct(<>) {
+      <>
+    }
+  ]],
+    { i(1), i(0) }
+  ),
+
+  __invoke = fmta(
+    [[
+    public function __invoke(<>) {
+      <>
+    }
+  ]],
+    { i(1), i(0) }
+  ),
+
+  drupalclass = fmta(
+    [[
+    <<?php
+
+    declare(strict_types=1);
+
+    namespace <>;
+
+    final class <> {
+
+      <>
+
+    }]],
+    {
+      f(function()
+        local filepath = vim.fn.expand "%:h"
+        local filepath_parts = vim.fn.split(filepath, "/")
+
+        if not vim.tbl_contains(filepath_parts, "src") then
+          return ""
+        end
+
+        local namespace_parts = { "Drupal" }
+
+        local is_test_file = vim.tbl_contains(filepath_parts, "tests")
+        if is_test_file then
+          table.insert(namespace_parts, "Tests")
+        end
+
+        -- Find and add the module name.
+        for k, v in ipairs(filepath_parts) do
+          if v == "src" then
+            if is_test_file then
+              table.insert(namespace_parts, filepath_parts[k - 2])
+            else
+              table.insert(namespace_parts, filepath_parts[k - 1])
+            end
+          end
+        end
+
+        -- Add the rest of the namespace.
+        local namespace = vim.split(filepath, "src/")
+        local final_part = (namespace[2] or ""):gsub("/", "\\")
+        table.insert(namespace_parts, final_part)
+
+        return table.concat(namespace_parts, "\\")
+      end),
+      f(function()
+        return vim.fn.expand "%:t:r"
+      end),
+      i(0),
+    }
+  ),
+
+  func = fmta("function <>(<>)<> {\n  <>\n}<>", { i(1), i(2), i(3), i(4), i(0) }),
+
+  met = fmta(
+    [[
+    <> function <>(<>)<> {
+      <>
+    }<>
+    ]],
+    { c(1, { t "public", t "protected", t "private" }), i(2), i(3), i(4), i(5), i(0) }
+  ),
+
+  pest = fmta("<>('<>', function() {\n  <>\n});", { c(1, { t "it", t "test" }), i(2), i(0) }),
+
+  test = fmta(
+    [[
+    public function test<>(): void {
+      <>
+    }<>
+    ]],
+    { i(1), i(2), i(0) }
+  ),
+
+  testa = fmta(
+    [[
+    /** @test */
+    public function <>(): void {
+      <>
+    }<>
+    ]],
+    { i(1), i(2), i(0) }
+  ),
+}
+
+return M
diff --git a/config/neovim/lua/opdavies/snippets/ft/rst.lua b/config/neovim/lua/opdavies/snippets/ft/rst.lua
new file mode 100644
index 00000000..206bbf1c
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/rst.lua
@@ -0,0 +1,49 @@
+local fmta = require("luasnip.extras.fmt").fmta
+local ls = require "luasnip"
+
+local i = ls.insert_node
+local f = ls.function_node
+
+local fill_line = function(char)
+  return function()
+    local row = vim.api.nvim_win_get_cursor(0)[1]
+    local lines = vim.api.nvim_buf_get_lines(0, row - 2, row, false)
+    return string.rep(char, #lines[1])
+  end
+end
+
+local M = {
+  class = { ".. class:: ", i(1) },
+  footer = { ".. footer:: ", i(1) },
+  link = { ".. _", i(1), ":" },
+  raw = { ".. raw:: ", i(1) },
+
+  -- TODO: add an optional new line and ":width" property.
+  image = { ".. image:: ", i(1) },
+
+  head = f(fill_line "=", {}),
+  sub = f(fill_line "-", {}),
+  subsub = f(fill_line "^", {}),
+
+  -- Add a page break with an optional page template.
+  pb = fmta(
+    [[
+    .. raw:: pdf
+
+       PageBreak<>
+    ]],
+    { i(0) }
+  ),
+
+  -- Add a new speaker note.
+  ta = fmta(
+    [[
+    .. raw:: pdf
+
+       TextAnnotation "<>"
+    ]],
+    { i(0) }
+  ),
+}
+
+return M
diff --git a/config/neovim/lua/opdavies/snippets/ft/scss.lua b/config/neovim/lua/opdavies/snippets/ft/scss.lua
new file mode 100644
index 00000000..846cbcb2
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/scss.lua
@@ -0,0 +1,10 @@
+local fmta = require("luasnip.extras.fmt").fmta
+local ls = require "luasnip"
+
+local i = ls.insert_node
+
+local M = {
+  bp = fmta("@include breakpoint(<>) {\n  <>\n}", { i(1), i(0) }),
+}
+
+return M
diff --git a/config/neovim/lua/opdavies/snippets/ft/yaml.lua b/config/neovim/lua/opdavies/snippets/ft/yaml.lua
new file mode 100644
index 00000000..4c5b3ce2
--- /dev/null
+++ b/config/neovim/lua/opdavies/snippets/ft/yaml.lua
@@ -0,0 +1,39 @@
+local fmta = require("luasnip.extras.fmt").fmta
+local ls = require "luasnip"
+local rep = require("luasnip.extras").rep
+
+local c = ls.choice_node
+local i = ls.insert_node
+local t = ls.text_node
+
+local M = {
+  drupal_info = fmta(
+    [[
+    name: <module_name>
+    description: <description>
+    core_version_requirement: ^10 || ^11
+    type: <type>
+    package: <package>
+    ]],
+    { module_name = i(1), description = i(2), type = c(3, { t "module", t "theme" }), package = i(0) }
+  ),
+
+  drupal_route = fmta(
+    [[
+    <module>.<route>:
+      path: /<path>
+      defaults:
+        _controller: Drupal\<module_same>\Controller\<class>
+        # _form:
+        # _title:
+        # _title_callback:
+      methods: [GET]
+      requirements:
+        _permission: access content
+        # _access: TRUE<finish>
+    ]],
+    { module = i(1), route = i(2), path = i(3), module_same = rep(1), class = i(4), finish = i(0) }
+  ),
+}
+
+return M
diff --git a/config/neovim/plugin/colorscheme.lua b/config/neovim/plugin/colorscheme.lua
new file mode 100644
index 00000000..d949f5fd
--- /dev/null
+++ b/config/neovim/plugin/colorscheme.lua
@@ -0,0 +1,25 @@
+local status_ok, catppuccin = pcall(require, "catppuccin")
+if not status_ok then
+  return
+end
+
+catppuccin.setup {
+  flavour = "macchiato",
+  integrations = {
+    cmp = true,
+    gitsigns = true,
+    mini = {
+      enabled = true,
+      indentscope_color = "",
+    },
+    native_lsp = {
+      enabled = true,
+    },
+    telescope = true,
+    treesitter = true,
+  },
+  term_colors = true,
+  transparent_background = true,
+}
+
+vim.cmd.colorscheme "catppuccin"
diff --git a/config/neovim/plugin/comment.lua b/config/neovim/plugin/comment.lua
new file mode 100644
index 00000000..eb350eb9
--- /dev/null
+++ b/config/neovim/plugin/comment.lua
@@ -0,0 +1,19 @@
+local status_ok, comment = pcall(require, "Comment")
+if not status_ok then
+  return
+end
+
+comment.setup {
+  padding = true,
+
+  opleader = {
+    line = "gc",
+    block = "gb",
+  },
+
+  mappings = {
+    basic = true,
+    extra = true,
+    extended = false,
+  },
+}
diff --git a/config/neovim/plugin/completion.lua b/config/neovim/plugin/completion.lua
new file mode 100644
index 00000000..ab21c534
--- /dev/null
+++ b/config/neovim/plugin/completion.lua
@@ -0,0 +1,155 @@
+local cmp = require "cmp"
+local ls = require "luasnip"
+
+vim.opt.shortmess:append "c"
+
+cmp.setup {
+  snippet = {
+    expand = function(args)
+      ls.lsp_expand(args.body)
+    end,
+  },
+
+  mapping = cmp.mapping.preset.insert {
+    ["<C-e>"] = cmp.mapping.close(),
+
+    ["<C-h>"] = cmp.mapping(function()
+      if ls.locally_jumpable(-1) then
+        ls.jump(-1)
+      end
+    end, { "i", "s" }),
+
+    ["<C-l>"] = cmp.mapping(function()
+      if ls.expand_or_locally_jumpable() then
+        ls.expand_or_jump()
+      end
+    end, { "i", "s" }),
+
+    ["<C-y>"] = cmp.mapping.confirm { select = true },
+    ["<tab>"] = cmp.config.disable,
+  },
+
+  sources = {
+    { name = "nvim_lsp" },
+    { name = "nvim_lua" },
+    { name = "luasnip" },
+    { name = "buffer" },
+    { name = "calc" },
+  },
+
+  sorting = {
+    comparators = {
+      cmp.config.compare.offset,
+      cmp.config.compare.exact,
+      cmp.config.compare.score,
+      cmp.config.compare.kind,
+      cmp.config.compare.sort_text,
+      cmp.config.compare.length,
+      cmp.config.compare.order,
+    },
+  },
+
+  formatting = {
+    format = require("lspkind").cmp_format {
+      with_text = true,
+      menu = {
+        buffer = "[buf]",
+        cmp_tabnine = "[tn]",
+        luasnip = "[snip]",
+        nvim_lsp = "[lsp]",
+        nvim_lua = "[lua]",
+        path = "[path]",
+      },
+    },
+  },
+
+  experimental = {
+    ghost_text = true,
+    native_menu = false,
+  },
+}
+
+cmp.setup.filetype({ "mysql", "sql" }, {
+  sources = {
+    { name = "vim-dadbod-completion" },
+    { name = "buffer" },
+  },
+})
+
+local snippet = ls.snippet
+local i = ls.insert_node
+local t = ls.text_node
+
+local shortcut = function(val)
+  if type(val) == "string" then
+    return { t { val }, i(0) }
+  end
+
+  if type(val) == "table" then
+    for k, v in ipairs(val) do
+      if type(v) == "string" then
+        val[k] = t { v }
+      end
+    end
+  end
+
+  return val
+end
+
+local make = function(tbl)
+  local result = {}
+  for k, v in pairs(tbl) do
+    table.insert(result, (snippet({ trig = k, desc = v.desc }, shortcut(v))))
+  end
+
+  return result
+end
+
+local snippets = {}
+
+for _, ft_path in ipairs(vim.api.nvim_get_runtime_file("lua/opdavies/snippets/ft/*.lua", true)) do
+  local ft = vim.fn.fnamemodify(ft_path, ":t:r")
+  snippets[ft] = make(loadfile(ft_path)())
+
+  ls.add_snippets(ft, snippets[ft])
+end
+
+ls.add_snippets("js", snippets.javascript)
+ls.add_snippets("typescript", snippets.javascript)
+ls.add_snippets("vue", snippets.javascript)
+
+-- Include any snippets to use in presentations.
+for _, ft_path in ipairs(vim.api.nvim_get_runtime_file("lua/opdavies/snippets/talks/*.lua", true)) do
+  loadfile(ft_path)()
+end
+
+require("luasnip.loaders.from_vscode").lazy_load()
+
+ls.config.set_config {
+  enable_autosnippets = true,
+  history = true,
+  updateevents = "TextChanged,TextChangedI",
+}
+
+-- Expand the current item or just to the next item within the snippet.
+vim.keymap.set({ "i", "s" }, "<c-k>", function()
+  if ls.expand_or_jumpable() then
+    ls.expand_or_jump()
+  end
+end, { silent = true })
+
+-- Jump backwards.
+vim.keymap.set({ "i", "s" }, "<c-j>", function()
+  if ls.jumpable(-1) then
+    ls.jump(-1)
+  end
+end, { silent = true })
+
+-- Select within a list of options.
+vim.keymap.set("i", "<c-l>", function()
+  if ls.choice_active() then
+    ls.change_choice(1)
+  end
+end)
+
+vim.keymap.set("n", "<leader><leader>s", "<cmd>source ~/Code/opdavies.nvim/after/plugin/luasnip.lua<CR>")
diff --git a/config/neovim/plugin/conform.lua b/config/neovim/plugin/conform.lua
new file mode 100644
index 00000000..fc5ef783
--- /dev/null
+++ b/config/neovim/plugin/conform.lua
@@ -0,0 +1,45 @@
+local conform = require "conform"
+
+conform.setup {
+  formatters_by_ft = {
+    bash = { "shellcheck" },
+    javascript = { { "prettierd", "prettier" } },
+    just = { "just" },
+    lua = { "stylua" },
+    nix = { { "nixfmt" } },
+    php = { { "php_cs_fixer", "phpcbf" } },
+    terraform = { "terraform_fmt" },
+    yaml = { "yamlfmt" },
+  },
+
+  format_on_save = function(bufnr)
+    -- Disable with a global or buffer-local variable.
+    if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then
+      return
+    end
+
+    return {
+      lsp_fallback = false,
+      quiet = true,
+    }
+  end,
+}
+
+vim.api.nvim_create_user_command("FormatDisable", function(args)
+  if args.bang then
+    -- FormatDisable! will disable formatting just for this buffer
+    vim.b.disable_autoformat = true
+  else
+    vim.g.disable_autoformat = true
+  end
+end, {
+  desc = "Disable autoformat-on-save",
+  bang = true,
+})
+
+vim.api.nvim_create_user_command("FormatEnable", function()
+  vim.b.disable_autoformat = false
+  vim.g.disable_autoformat = false
+end, {
+  desc = "Re-enable autoformat-on-save",
+})
diff --git a/config/neovim/plugin/dap.lua b/config/neovim/plugin/dap.lua
new file mode 100644
index 00000000..f9c1f3ce
--- /dev/null
+++ b/config/neovim/plugin/dap.lua
@@ -0,0 +1,69 @@
+local dap = require "dap"
+local ui = require "dapui"
+
+dap.adapters.php = {
+  type = "executable",
+  command = "node",
+  args = { os.getenv "HOME" .. "/build/vscode-php-debug/out/phpDebug.js" },
+}
+
+dap.configurations.php = {
+  {
+    type = "php",
+    request = "launch",
+    name = "Listen for Xdebug",
+    port = 9003,
+    pathMappings = {
+      ["/app"] = "${workspaceFolder}",
+      ["/var/www/html"] = "${workspaceFolder}",
+    },
+  },
+}
+
+dap.listeners.after.event_initialized["ui_config"] = function()
+  ui.open()
+end
+
+dap.listeners.before.event_terminated["ui_config"] = function()
+  ui.close()
+end
+
+dap.listeners.before.event_exited["ui_config"] = function()
+  ui.close()
+end
+
+ui.setup {
+  layouts = {
+    {
+      elements = {
+        { id = "scopes", size = 0.25 },
+        "breakpoints",
+        "stacks",
+        "watches",
+      },
+      size = 40, -- 40 columns
+      position = "right",
+    },
+    {
+      elements = {
+        "repl",
+        "console",
+      },
+      size = 0.25, -- 25% of total lines
+      position = "bottom",
+    },
+  },
+}
+
+require("nvim-dap-virtual-text").setup {
+  commented = true,
+}
+
+vim.keymap.set("n", "<leader>b", dap.toggle_breakpoint)
+vim.keymap.set("n", "<leader>gb", dap.run_to_cursor)
+
+vim.keymap.set("n", "<F1>", dap.continue)
+vim.keymap.set("n", "<F2>", dap.step_into)
+vim.keymap.set("n", "<F3>", dap.step_over)
+vim.keymap.set("n", "<F4>", dap.step_out)
+vim.keymap.set("n", "<F5>", dap.step_back)
diff --git a/config/neovim/plugin/dial.lua b/config/neovim/plugin/dial.lua
new file mode 100644
index 00000000..10f33cfa
--- /dev/null
+++ b/config/neovim/plugin/dial.lua
@@ -0,0 +1,45 @@
+local augend = require "dial.augend"
+local dial_config = require "dial.config"
+
+dial_config.augends:register_group {
+  visual = {
+    augend.integer.alias.decimal,
+    augend.integer.alias.hex,
+    augend.date.alias["%Y/%m/%d"],
+    augend.constant.alias.alpha,
+    augend.constant.alias.Alpha,
+  },
+
+  mygroup = {
+    augend.constant.new {
+      elements = { "TRUE", "FALSE" },
+      word = true,
+      cyclic = true,
+    },
+
+    augend.constant.new {
+      elements = { "public", "protected", "private" },
+      word = true,
+      cyclic = true,
+    },
+
+    augend.constant.new {
+      elements = { "&&", "||" },
+      word = false,
+      cyclic = true,
+    },
+
+    augend.date.alias["%d/%m/%Y"],
+    augend.constant.alias.bool, -- boolean value (true <-> false)
+    augend.integer.alias.decimal,
+    augend.integer.alias.hex,
+    augend.semver.alias.semver,
+  },
+}
+
+local dial_map = require "dial.map"
+
+vim.keymap.set("n", "<C-a>", dial_map.inc_normal "mygroup")
+vim.keymap.set("n", "<C-x>", dial_map.dec_normal "mygroup")
+vim.keymap.set("v", "<C-a>", dial_map.inc_normal "visual")
+vim.keymap.set("v", "<C-x>", dial_map.dec_normal "visual")
diff --git a/config/neovim/plugin/edit_alternate.lua b/config/neovim/plugin/edit_alternate.lua
new file mode 100644
index 00000000..293a28b6
--- /dev/null
+++ b/config/neovim/plugin/edit_alternate.lua
@@ -0,0 +1,40 @@
+vim.fn["edit_alternate#rule#add"]("php", function(filename)
+  if filename:find "Test.php$" then
+    filename = filename:gsub("Test.php$", ".php")
+
+    if filename:find "tests/src/" then
+      -- Drupal tests. Remove the `src/{type}` from the path.
+      return filename:gsub("tests/src/(.-)/", "src/")
+    else
+      return filename:gsub("tests/", "src/")
+    end
+  else
+    filename = filename:gsub(".php$", "Test.php")
+
+    if filename:find "modules/custom" then
+      -- Drupal test types.
+      local test_types = { "Functional", "FunctionalJavaScript", "Kernel", "Unit" }
+
+      for _, test_type in ipairs(test_types) do
+        local filename_with_test_type = filename:gsub("src/", string.format("tests/src/%s/", test_type))
+
+        -- Return the first matching test file that exists.
+        if vim.fn.filereadable(filename_with_test_type) == 1 then
+          return filename_with_test_type
+        end
+      end
+    end
+  end
+end)
+
+if vim.fn.filereadable "fractal.config.js" == 1 then
+  vim.fn["edit_alternate#rule#add"]("twig", function(filename)
+    return (filename:gsub("%.twig$", ".config.yml"))
+  end)
+
+  vim.fn["edit_alternate#rule#add"]("yml", function(filename)
+    return (filename:gsub("%.config.yml$", ".twig"))
+  end)
+end
+
+vim.keymap.set("n", "<leader>ea", "<CMD>EditAlternate<CR>", { silent = true })
diff --git a/config/neovim/plugin/fidget.lua b/config/neovim/plugin/fidget.lua
new file mode 100644
index 00000000..2a0375fa
--- /dev/null
+++ b/config/neovim/plugin/fidget.lua
@@ -0,0 +1,7 @@
+require("fidget").setup {
+  notification = {
+    window = {
+      winblend = 0,
+    },
+  },
+}
diff --git a/config/neovim/plugin/filetype.lua b/config/neovim/plugin/filetype.lua
new file mode 100644
index 00000000..81ec0a15
--- /dev/null
+++ b/config/neovim/plugin/filetype.lua
@@ -0,0 +1,9 @@
+vim.filetype.add {
+  extension = {
+    inc = "php",
+    install = "php",
+    module = "php",
+    pcss = "scss",
+    theme = "php",
+  },
+}
diff --git a/config/neovim/plugin/fugitive.lua b/config/neovim/plugin/fugitive.lua
new file mode 100644
index 00000000..4aded48c
--- /dev/null
+++ b/config/neovim/plugin/fugitive.lua
@@ -0,0 +1,25 @@
+vim.keymap.set("n", "<leader>gc", "<cmd>Git commit<cr><C-w>K")
+
+-- Open the ":Git" window in its own buffer, not a split.
+vim.keymap.set("n", "<leader>gs", "<cmd>0Git<cr>")
+
+vim.api.nvim_create_autocmd("BufWinEnter", {
+  pattern = "*",
+
+  callback = function()
+    if vim.bo.ft ~= "fugitive" then
+      return
+    end
+
+    local bufnr = vim.api.nvim_get_current_buf()
+    local opts = { buffer = bufnr, remap = false }
+
+    vim.keymap.set("n", "<leader>p", function()
+      vim.cmd.Git "push"
+    end, opts)
+
+    vim.keymap.set("n", "<leader>P", function()
+      vim.cmd.Git { "pull", "--rebase" }
+    end, opts)
+  end,
+})
diff --git a/config/neovim/plugin/gitsigns.lua b/config/neovim/plugin/gitsigns.lua
new file mode 100644
index 00000000..16b2f800
--- /dev/null
+++ b/config/neovim/plugin/gitsigns.lua
@@ -0,0 +1,30 @@
+local gitsigns = require "gitsigns"
+
+gitsigns.setup {
+  linehl = false,
+  numhl = true,
+}
+
+local set = vim.keymap.set
+
+set("n", "[h", "<cmd>Gitsigns prev_hunk<CR>")
+set("n", "]h", "<cmd>Gitsigns next_hunk<CR>")
+
+set("n", "<leader>hR", gitsigns.reset_buffer)
+set("n", "<leader>hS", gitsigns.stage_buffer)
+set("n", "<leader>hb", gitsigns.blame_line)
+set("n", "<leader>hp", gitsigns.preview_hunk)
+set("n", "<leader>hr", gitsigns.reset_hunk)
+set("n", "<leader>hs", gitsigns.stage_hunk)
+set("n", "<leader>hu", gitsigns.undo_stage_hunk)
+
+set("v", "<leader>hr", function()
+  gitsigns.reset_hunk { vim.fn.line ".", vim.fn.line "v" }
+end)
+
+set("v", "<leader>hs", function()
+  gitsigns.stage_hunk { vim.fn.line ".", vim.fn.line "v" }
+end)
+
+-- Text object.
+set({ "o", "x" }, "ih", ":<C-U>Gitsigns select_hunk<CR>")
diff --git a/config/neovim/plugin/harpoon.lua b/config/neovim/plugin/harpoon.lua
new file mode 100644
index 00000000..03769f0a
--- /dev/null
+++ b/config/neovim/plugin/harpoon.lua
@@ -0,0 +1,13 @@
+require("harpoon").setup()
+
+local mark = require "harpoon.mark"
+local ui = require "harpoon.ui"
+
+vim.keymap.set("n", "<M-h><M-l>", ui.toggle_quick_menu)
+vim.keymap.set("n", "<M-h><M-m>", mark.add_file)
+
+for i = 1, 5 do
+  vim.keymap.set("n", string.format("<space>%s", i), function()
+    ui.nav_file(i)
+  end)
+end
diff --git a/config/neovim/plugin/lint.lua b/config/neovim/plugin/lint.lua
new file mode 100644
index 00000000..f862107a
--- /dev/null
+++ b/config/neovim/plugin/lint.lua
@@ -0,0 +1,19 @@
+local lint = require "lint"
+
+lint.linters_by_ft = {
+  dockerfile = { "hadolint" },
+  javascript = { "eslint_d" },
+  json = { "jsonlint" },
+  lua = { "luacheck" },
+  markdown = { "markdownlint" },
+  nix = { "nix" },
+  php = { "php", "phpcs", "phpstan" },
+}
+local lint_augroup = vim.api.nvim_create_augroup("lint", { clear = true })
+
+vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost", "InsertLeave" }, {
+  group = lint_augroup,
+  callback = function()
+    lint.try_lint()
+  end,
+})
diff --git a/config/neovim/plugin/mini.lua b/config/neovim/plugin/mini.lua
new file mode 100644
index 00000000..6c217467
--- /dev/null
+++ b/config/neovim/plugin/mini.lua
@@ -0,0 +1,23 @@
+require("mini.ai").setup { n_lines = 500 }
+
+require("mini.align").setup {}
+
+require("mini.bracketed").setup {}
+
+require("mini.hipatterns").setup {
+  highlighters = {
+    note = { pattern = "%f[%w]()NOTE()%f[%W]", group = "MiniHipatternsNote" },
+    todo = { pattern = "%f[%w]()TODO()%f[%W]", group = "MiniHipatternsTodo" },
+  },
+}
+
+require("mini.move").setup {}
+
+require("mini.operators").setup {}
+
+require("mini.statusline").setup {
+  set_vim_settings = false,
+  use_icons = false,
+}
+
+require("mini.surround").setup {}
diff --git a/config/neovim/plugin/netrw.lua b/config/neovim/plugin/netrw.lua
new file mode 100644
index 00000000..1feb376d
--- /dev/null
+++ b/config/neovim/plugin/netrw.lua
@@ -0,0 +1,6 @@
+vim.keymap.set("n", "<leader>pv", vim.cmd.Ex)
+
+vim.g.netrw_banner = 0
+vim.g.netrw_browse_split = 0
+vim.g.netrw_liststyle = 3
+vim.g.netrw_winsize = 20
diff --git a/config/neovim/plugin/oil.lua b/config/neovim/plugin/oil.lua
new file mode 100644
index 00000000..5799eb20
--- /dev/null
+++ b/config/neovim/plugin/oil.lua
@@ -0,0 +1,16 @@
+require("oil").setup {
+  columns = { "icon" },
+
+  keymaps = {
+    ["<C-h>"] = false,
+    ["<M-h>"] = "actions.select_split",
+  },
+
+  skip_confirm_for_simple_edits = true,
+
+  view_options = {
+    show_hidden = true,
+  },
+}
+
+vim.keymap.set("n", "-", "<Cmd>Oil<cr>", { desc = "Open parent directory" })
diff --git a/config/neovim/plugin/refactoring.lua b/config/neovim/plugin/refactoring.lua
new file mode 100644
index 00000000..a23a0bb2
--- /dev/null
+++ b/config/neovim/plugin/refactoring.lua
@@ -0,0 +1,12 @@
+local refactoring = require "refactoring"
+
+-- TODO: add keymaps - https://github.com/ThePrimeagen/refactoring.nvim#configuration-for-refactoring-operations
+refactoring.setup {}
+
+local opts = { silent = true }
+
+vim.keymap.set("n", "<Leader>ri", "<Cmd>lua require 'refactoring'.refactor 'Inline Variable'<Cr>", opts)
+
+vim.keymap.set("v", "<Leader>re", "<Esc><Cmd>lua require 'refactoring'.refactor 'Extract Function'<Cr>", opts)
+vim.keymap.set("v", "<Leader>ri", "<Esc><Cmd>lua require 'refactoring'.refactor 'Inline Variable'<Cr>", opts)
+vim.keymap.set("v", "<Leader>rv", "<Esc><Cmd>lua require 'refactoring'.refactor 'Extract Variable'<Cr>", opts)
diff --git a/config/neovim/plugin/sort.lua b/config/neovim/plugin/sort.lua
new file mode 100644
index 00000000..bdf96d76
--- /dev/null
+++ b/config/neovim/plugin/sort.lua
@@ -0,0 +1,10 @@
+require("sort").setup()
+
+vim.cmd([[
+  nnoremap <silent> go" vi"<Esc><Cmd>Sort<CR>
+  nnoremap <silent> go' vi'<Esc><Cmd>Sort<CR>
+  nnoremap <silent> go( vi(<Esc><Cmd>Sort<CR>
+  nnoremap <silent> go[ vi[<Esc><Cmd>Sort<CR>
+  nnoremap <silent> gop vip<Esc><Cmd>Sort<CR>
+  nnoremap <silent> go{ vi{<Esc><Cmd>Sort<CR>
+]])
diff --git a/config/neovim/plugin/spectre.lua b/config/neovim/plugin/spectre.lua
new file mode 100644
index 00000000..b67854e6
--- /dev/null
+++ b/config/neovim/plugin/spectre.lua
@@ -0,0 +1 @@
+require("spectre").setup()
diff --git a/config/neovim/plugin/telescope.lua b/config/neovim/plugin/telescope.lua
new file mode 100644
index 00000000..79fafd63
--- /dev/null
+++ b/config/neovim/plugin/telescope.lua
@@ -0,0 +1,67 @@
+local telescope = require "telescope"
+
+telescope.setup {
+  defaults = {
+    layout_config = { prompt_position = "top" },
+    path_display = { truncate = 1 },
+    prompt_prefix = "$ ",
+    sorting_strategy = "ascending",
+  },
+
+  pickers = {
+    lsp_references = {
+      previewer = false,
+    },
+  },
+
+  extensions = {
+    ["ui-select"] = {
+      require("telescope.themes").get_dropdown {},
+    },
+  },
+}
+
+telescope.load_extension "fzf"
+telescope.load_extension "refactoring"
+telescope.load_extension "ui-select"
+
+local builtin = require "telescope.builtin"
+
+local M = {}
+
+M.diagnostics = function()
+  builtin.diagnostics { bufnr = 0 }
+end
+
+M.grep_bluecheese = function()
+  builtin.live_grep { cwd = "web/sites/default/themes/bluecheese" }
+end
+
+M.grep_drupalorg_theme = function()
+  builtin.live_grep { cwd = "web/themes/contrib/drupalorg_theme" }
+end
+
+M.search_all_files = function()
+  builtin.find_files {
+    find_command = { "rg", "--no-ignore", "--files" },
+  }
+end
+
+vim.keymap.set("n", "<space>/", builtin.current_buffer_fuzzy_find)
+vim.keymap.set("n", "<space>fb", builtin.buffers)
+vim.keymap.set("n", "<space>fd", builtin.find_files)
+vim.keymap.set("n", "<space>fg", builtin.live_grep)
+vim.keymap.set("n", "<space>fh", builtin.help_tags)
+vim.keymap.set("n", "<space>fi", M.search_all_files)
+vim.keymap.set("n", "<space>fk", builtin.keymaps)
+vim.keymap.set("n", "<space>ft", builtin.git_files)
+
+vim.keymap.set("n", "<space>dl", M.diagnostics)
+vim.keymap.set("n", "<space>ds", builtin.lsp_document_symbols)
+
+vim.keymap.set("n", "<space>gw", builtin.grep_string)
+
+vim.keymap.set("n", "<space>dgb", M.grep_bluecheese)
+vim.keymap.set("n", "<space>dgd", M.grep_drupalorg_theme)
+
+vim.keymap.set({ "n", "v" }, "<space>gw", builtin.grep_string)
diff --git a/config/neovim/plugin/treesitter.lua b/config/neovim/plugin/treesitter.lua
new file mode 100644
index 00000000..df5bd1ab
--- /dev/null
+++ b/config/neovim/plugin/treesitter.lua
@@ -0,0 +1,136 @@
+local configs = require "nvim-treesitter.configs"
+local context = require "treesitter-context"
+local ts_repeat_move = require "nvim-treesitter.textobjects.repeatable_move"
+
+configs.setup {
+  autotag = {
+    enable = true,
+  },
+
+  context_commenting = {
+    enable = true,
+  },
+
+  highlight = {
+    enable = true,
+  },
+
+  indent = {
+    disable = { "yaml" },
+    enable = true,
+  },
+
+  matchup = {
+    enable = true,
+  },
+
+  textobjects = {
+    select = {
+      enable = true,
+      lookahead = true,
+
+      keymaps = {
+        ["a="] = { query = "@assignment.outer", desc = "Select outer part of an assignment" },
+        ["i="] = { query = "@assignment.inner", desc = "Select inner part of an assignment" },
+        ["l="] = { query = "@assignment.lhs", desc = "Select left hand side of an assignment" },
+        ["r="] = { query = "@assignment.rhs", desc = "Select right hand side of an assignment" },
+
+        ["a:"] = { query = "@property.outer", desc = "Select outer part of an object property" },
+        ["i:"] = { query = "@property.inner", desc = "Select inner part of an object property" },
+        ["l:"] = { query = "@property.lhs", desc = "Select left part of an object property" },
+        ["r:"] = { query = "@property.rhs", desc = "Select right part of an object property" },
+
+        ["aa"] = { query = "@parameter.outer", desc = "Select outer part of a parameter/argument" },
+        ["ia"] = { query = "@parameter.inner", desc = "Select inner part of a parameter/argument" },
+
+        ["ac"] = { query = "@class.outer", desc = "Select outer part of a class" },
+        ["ic"] = { query = "@class.inner", desc = "Select inner part of a class" },
+
+        ["af"] = { query = "@call.outer", desc = "Select outer part of a function call" },
+        ["if"] = { query = "@call.inner", desc = "Select inner part of a function call" },
+
+        ["ai"] = { query = "@conditional.outer", desc = "Select outer part of a conditional" },
+        ["ii"] = { query = "@conditional.inner", desc = "Select inner part of a conditional" },
+
+        ["al"] = { query = "@loop.outer", desc = "Select outer part of a loop" },
+        ["il"] = { query = "@loop.inner", desc = "Select inner part of a loop" },
+
+        ["am"] = { query = "@function.outer", desc = "Select outer part of a method/function definition" },
+        ["im"] = { query = "@function.inner", desc = "Select inner part of a method/function definition" },
+      },
+    },
+  },
+
+  swap = {
+    enable = true,
+
+    swap_next = {
+      ["<leader>na"] = "@parameter.inner", -- swap parameters/argument with next
+      ["<leader>n:"] = "@property.outer", -- swap object property with next
+      ["<leader>nm"] = "@function.outer", -- swap function with next
+    },
+
+    swap_previous = {
+      ["<leader>pa"] = "@parameter.inner", -- swap parameters/argument with prev
+      ["<leader>p:"] = "@property.outer", -- swap object property with prev
+      ["<leader>pm"] = "@function.outer", -- swap function with previous
+    },
+  },
+
+  move = {
+    enable = true,
+    set_jumps = true, -- whether to set jumps in the jumplist
+
+    goto_next_start = {
+      ["]f"] = { query = "@call.outer", desc = "Next function call start" },
+      ["]m"] = { query = "@function.outer", desc = "Next method/function def start" },
+      ["]c"] = { query = "@class.outer", desc = "Next class start" },
+      ["]i"] = { query = "@conditional.outer", desc = "Next conditional start" },
+      ["]l"] = { query = "@loop.outer", desc = "Next loop start" },
+
+      ["]s"] = { query = "@scope", query_group = "locals", desc = "Next scope" },
+      ["]z"] = { query = "@fold", query_group = "folds", desc = "Next fold" },
+    },
+
+    goto_next_end = {
+      ["]F"] = { query = "@call.outer", desc = "Next function call end" },
+      ["]M"] = { query = "@function.outer", desc = "Next method/function def end" },
+      ["]C"] = { query = "@class.outer", desc = "Next class end" },
+      ["]I"] = { query = "@conditional.outer", desc = "Next conditional end" },
+      ["]L"] = { query = "@loop.outer", desc = "Next loop end" },
+    },
+
+    goto_previous_start = {
+      ["[f"] = { query = "@call.outer", desc = "Prev function call start" },
+      ["[m"] = { query = "@function.outer", desc = "Prev method/function def start" },
+      ["[c"] = { query = "@class.outer", desc = "Prev class start" },
+      ["[i"] = { query = "@conditional.outer", desc = "Prev conditional start" },
+      ["[l"] = { query = "@loop.outer", desc = "Prev loop start" },
+    },
+
+    goto_previous_end = {
+      ["[F"] = { query = "@call.outer", desc = "Prev function call end" },
+      ["[M"] = { query = "@function.outer", desc = "Prev method/function def end" },
+      ["[C"] = { query = "@class.outer", desc = "Prev class end" },
+      ["[I"] = { query = "@conditional.outer", desc = "Prev conditional end" },
+      ["[L"] = { query = "@loop.outer", desc = "Prev loop end" },
+    },
+  },
+}
+
+local set = vim.keymap.set
+
+set("n", "<leader>th", "<cmd>TSHighlightCapturesUnderCursor<CR>")
+set("n", "<leader>tp", "<cmd>TSPlaygroundToggle<CR>")
+
+-- vim way: ; goes to the direction you were moving.
+set({ "n", "o", "x" }, ";", ts_repeat_move.repeat_last_move)
+set({ "n", "o", "x" }, ",", ts_repeat_move.repeat_last_move_opposite)
+
+-- Optionally, make builtin f, F, t, T also repeatable with ; and ,
+set({ "n", "o", "x" }, "f", ts_repeat_move.builtin_f)
+set({ "n", "o", "x" }, "F", ts_repeat_move.builtin_F)
+set({ "n", "o", "x" }, "t", ts_repeat_move.builtin_t)
+set({ "n", "o", "x" }, "T", ts_repeat_move.builtin_T)
+
+context.setup { enable = true }
diff --git a/config/neovim/plugin/treesj.lua b/config/neovim/plugin/treesj.lua
new file mode 100644
index 00000000..a23b3ce3
--- /dev/null
+++ b/config/neovim/plugin/treesj.lua
@@ -0,0 +1,8 @@
+local tsj = require "treesj"
+
+tsj.setup {
+  use_default_keymaps = false,
+}
+
+vim.keymap.set("n", "gJ", tsj.join)
+vim.keymap.set("n", "gS", tsj.split)
diff --git a/config/neovim/plugin/undotree.lua b/config/neovim/plugin/undotree.lua
new file mode 100644
index 00000000..b6b9276a
--- /dev/null
+++ b/config/neovim/plugin/undotree.lua
@@ -0,0 +1 @@
+vim.keymap.set("n", "<leader>u", vim.cmd.UndotreeToggle)
diff --git a/config/neovim/plugin/vim-test.lua b/config/neovim/plugin/vim-test.lua
new file mode 100644
index 00000000..b82c4663
--- /dev/null
+++ b/config/neovim/plugin/vim-test.lua
@@ -0,0 +1,23 @@
+local map = vim.api.nvim_set_keymap
+
+local options = {
+  silent = true,
+}
+
+map("n", "<leader>tf", ":TestFile<CR>", options)
+map("n", "<leader>tg", ":TestVisit<CR>", options)
+map("n", "<leader>tl", ":TestLast<CR>", options)
+map("n", "<leader>tn", ":TestNearest<CR>", options)
+map("n", "<leader>ts", ":TestSuite<CR>", options)
+
+vim.cmd [[
+  let test#echo_command = 0
+  let test#strategy = "neovim_sticky"
+
+  let g:test#neovim_sticky#kill_previous = 1
+  let g:test#neovim_sticky#reopen_window = 1
+  let g:test#preserve_screen = 0
+
+  let test#php#phpunit#executable = './run test'
+  let test#php#phpunit#options = '--colors=always --testdox'
+]]
diff --git a/flake.lock b/flake.lock
index cade14b7..8974dd1c 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,23 +1,5 @@
 {
   "nodes": {
-    "flake-parts": {
-      "inputs": {
-        "nixpkgs-lib": "nixpkgs-lib"
-      },
-      "locked": {
-        "lastModified": 1727826117,
-        "narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=",
-        "owner": "hercules-ci",
-        "repo": "flake-parts",
-        "rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1",
-        "type": "github"
-      },
-      "original": {
-        "owner": "hercules-ci",
-        "repo": "flake-parts",
-        "type": "github"
-      }
-    },
     "home-manager": {
       "inputs": {
         "nixpkgs": [
@@ -71,34 +53,6 @@
         "type": "github"
       }
     },
-    "nixpkgs-2305": {
-      "locked": {
-        "lastModified": 1704290814,
-        "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421",
-        "type": "github"
-      },
-      "original": {
-        "owner": "NixOS",
-        "ref": "nixos-23.05",
-        "repo": "nixpkgs",
-        "type": "github"
-      }
-    },
-    "nixpkgs-lib": {
-      "locked": {
-        "lastModified": 1727825735,
-        "narHash": "sha256-0xHYkMkeLVQAMa7gvkddbPqpxph+hDzdu1XdGPJR+Os=",
-        "type": "tarball",
-        "url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
-      },
-      "original": {
-        "type": "tarball",
-        "url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
-      }
-    },
     "nixpkgs-unstable": {
       "locked": {
         "lastModified": 1728018373,
@@ -115,52 +69,12 @@
         "type": "github"
       }
     },
-    "nixpkgs-unstable_2": {
-      "locked": {
-        "lastModified": 1727802920,
-        "narHash": "sha256-HP89HZOT0ReIbI7IJZJQoJgxvB2Tn28V6XS3MNKnfLs=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "27e30d177e57d912d614c88c622dcfdb2e6e6515",
-        "type": "github"
-      },
-      "original": {
-        "owner": "NixOS",
-        "ref": "nixos-unstable",
-        "repo": "nixpkgs",
-        "type": "github"
-      }
-    },
-    "opdavies-nvim": {
-      "inputs": {
-        "flake-parts": "flake-parts",
-        "nixpkgs": [
-          "nixpkgs"
-        ],
-        "nixpkgs-2305": "nixpkgs-2305",
-        "nixpkgs-unstable": "nixpkgs-unstable_2"
-      },
-      "locked": {
-        "lastModified": 1728128218,
-        "narHash": "sha256-FD+TxbOgH0CBX+7hWJrKvW75KUSIQVsERQMBQGKwNeA=",
-        "owner": "opdavies",
-        "repo": "opdavies.nvim",
-        "rev": "ddb9c3199aac6733aa5eadcede729dc28c335911",
-        "type": "github"
-      },
-      "original": {
-        "owner": "opdavies",
-        "repo": "opdavies.nvim",
-        "type": "github"
-      }
-    },
     "root": {
       "inputs": {
         "home-manager": "home-manager",
         "nixos-hardware": "nixos-hardware",
         "nixpkgs": "nixpkgs",
-        "nixpkgs-unstable": "nixpkgs-unstable",
-        "opdavies-nvim": "opdavies-nvim"
+        "nixpkgs-unstable": "nixpkgs-unstable"
       }
     }
   },
diff --git a/flake.nix b/flake.nix
index 13a3fe8f..df8aec0c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -8,10 +8,6 @@
     nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
 
     nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
-
-    opdavies-nvim.inputs.nixpkgs.follows = "nixpkgs";
-    opdavies-nvim.url = "github:opdavies/opdavies.nvim";
-    # opdavies-nvim.url = "path:/home/opdavies/Code/opdavies.nvim";
   };
 
   outputs =
@@ -40,9 +36,17 @@
       mkWsl = import ./lib/wsl2 { inherit inputs self username; };
 
       inherit (pkgs) mkShell;
+      inherit (pkgs.vimUtils) buildVimPlugin;
     in
     {
-      packages.${system}.default = mkShell { buildInputs = with pkgs; [ bashInteractive ]; };
+      packages.${system} = {
+        default = mkShell { buildInputs = with pkgs; [ bashInteractive ]; };
+
+        opdavies-nvim = buildVimPlugin {
+          name = "opdavies-nvim";
+          src = ./config/neovim;
+        };
+      };
 
       formatter.${system} = pkgs.nixfmt-rfc-style;
 
diff --git a/lib/shared/home-manager.nix b/lib/shared/home-manager.nix
index 41723693..4ecaa9aa 100644
--- a/lib/shared/home-manager.nix
+++ b/lib/shared/home-manager.nix
@@ -20,7 +20,7 @@ in
 
   imports = [
     (import ./modules/git.nix { inherit inputs pkgs pkgsUnstable; })
-    (import ./modules/neovim.nix { inherit inputs; })
+    (import ./modules/neovim.nix { inherit inputs pkgs; })
     ./modules/bat.nix
     ./modules/bin.nix
     ./modules/direnv.nix
diff --git a/lib/shared/modules/neovim.nix b/lib/shared/modules/neovim.nix
index 2bd540e8..03f7e5b6 100644
--- a/lib/shared/modules/neovim.nix
+++ b/lib/shared/modules/neovim.nix
@@ -1,10 +1,313 @@
-{ inputs }:
-{ pkgs, ... }:
+{ inputs, pkgs, ... }:
+
 let
-  system = pkgs.system;
+  inherit (pkgs) fetchFromGitHub;
+  inherit (pkgs.vimUtils) buildVimPlugin;
+
+  # TODO: move these back into an overlay.
+  customVim = {
+    conf-vim = buildVimPlugin {
+      name = "conf-vim";
+      src = fetchFromGitHub {
+        owner = "tjdevries";
+        repo = "conf.vim";
+        rev = "master";
+        sha256 = "AjiTJsoim0BAnyfqk1IQzNsa6jhFM2+A66E7q9sJqz0=";
+      };
+    };
+
+    edit-alternate-vim = buildVimPlugin {
+      name = "edit-alternate-vim";
+      src = fetchFromGitHub {
+        owner = "tjdevries";
+        repo = "edit_alternate.vim";
+        rev = "master";
+        sha256 = "mEKnqYAhgrdxPRoKf4S4yYecdFIHGg8bDxpqPuC1+S4=";
+      };
+    };
+
+    standard-vim = buildVimPlugin {
+      name = "standard-vim";
+      src = fetchFromGitHub {
+        owner = "tjdevries";
+        repo = "standard.vim";
+        rev = "master";
+        sha256 = "9VwkvV1Dv6cE4uDkPp36DozjWJOclDR883yDMYw000E=";
+      };
+    };
+
+    vim-autoread = buildVimPlugin {
+      name = "vim-autoread";
+      src = fetchFromGitHub {
+        owner = "djoshea";
+        repo = "vim-autoread";
+        rev = "24061f84652d768bfb85d222c88580b3af138dab";
+        sha256 = "fSADjNt1V9jgAPjxggbh7Nogcxyisi18KaVve8j+c3w=";
+      };
+    };
+
+    vim-textobj-indent = buildVimPlugin {
+      name = "vim-textobj-indent";
+      src = fetchFromGitHub {
+        owner = "kana";
+        repo = "vim-textobj-indent";
+        rev = "deb76867c302f933c8f21753806cbf2d8461b548";
+        sha256 = "oFzUPG+IOkbKZ2gU/kduQ3G/LsLDlEjFhRP0BHBE+1Q=";
+      };
+    };
+
+    toggle-checkbox-nvim = buildVimPlugin {
+      name = "toggle-checkbox-nvim";
+      src = fetchFromGitHub {
+        owner = "opdavies";
+        repo = "toggle-checkbox.nvim";
+        rev = "main";
+        sha256 = "4YSEagQnLK5MBl2z53e6sOBlCDm220GYVlc6A+HNywg=";
+      };
+    };
+
+    vim-heritage = buildVimPlugin {
+      name = "vim-heritage";
+      src = fetchFromGitHub {
+        owner = "jessarcher";
+        repo = "vim-heritage";
+        rev = "cffa05c78c0991c998adc4504d761b3068547db6";
+        sha256 = "Lebe5V1XFxn4kSZ+ImZ69Vst9Nbc0N7eA9IzOCijFS0=";
+      };
+    };
+
+    vim-textobj-xmlattr = buildVimPlugin {
+      name = "vim-textobj-xmlattr";
+      src = fetchFromGitHub {
+        owner = "whatyouhide";
+        repo = "vim-textobj-xmlattr";
+        rev = "694a297f1d75fd527e87da9769f3c6519a87ebb1";
+        sha256 = "+91FVP95oh00flINdltqx6qJuijYo56tHIh3J098G2Q=";
+      };
+    };
+
+    tabline-vim = buildVimPlugin {
+      name = "tabline-vim";
+      src = fetchFromGitHub {
+        owner = "mkitt";
+        repo = "tabline.vim";
+        rev = "69c9698a3240860adaba93615f44778a9ab724b4";
+        sha256 = "51b8PxyKqBdeIvmmZyF2hpMBjkyrlZDdTB1opr5JZ7Y=";
+      };
+    };
+
+    vim-caser = buildVimPlugin {
+      name = "vim-caser";
+      src = fetchFromGitHub {
+        owner = "arthurxavierx";
+        repo = "vim-caser";
+        rev = "6bc9f41d170711c58e0157d882a5fe8c30f34bf6";
+        sha256 = "PXAY01O/cHvAdWx3V/pyWFeiV5qJGvLcAKhl5DQc0Ps=";
+      };
+    };
+
+    vim-zoom = buildVimPlugin {
+      name = "vim-zoom";
+      src = fetchFromGitHub {
+        owner = "dhruvasagar";
+        repo = "vim-zoom";
+        rev = "01c737005312c09e0449d6518decf8cedfee32c7";
+        sha256 = "/ADzScsG0u6RJbEtfO23Gup2NYdhPkExqqOPVcQa7aQ=";
+      };
+    };
+  };
 in
 {
-  programs.neovim = inputs.opdavies-nvim.lib.mkHomeManager { inherit system; };
+  programs.neovim = {
+    enable = true;
+
+    plugins = with pkgs.vimPlugins; [
+      comment-nvim
+      dial-nvim
+      fidget-nvim
+      gitsigns-nvim
+      harpoon
+      impatient-nvim
+      mini-nvim
+      neodev-nvim
+      nvim-spectre
+      nvim-web-devicons
+      oil-nvim
+      refactoring-nvim
+      sort-nvim
+      treesj
+      undotree
+      vim-abolish
+      vim-eunuch
+      vim-highlightedyank
+      vim-just
+      vim-nix
+      vim-obsession
+      vim-pasta
+      vim-repeat
+      vim-sleuth
+      vim-sort-motion
+      vim-terraform
+      vim-textobj-user
+      vim-unimpaired
+
+      customVim.conf-vim
+      customVim.edit-alternate-vim
+      customVim.standard-vim
+      customVim.tabline-vim
+      customVim.vim-autoread
+      customVim.vim-textobj-indent
+      customVim.vim-textobj-xmlattr
+      customVim.vim-zoom
+
+      # Testing
+      vim-test
+
+      # Git
+      committia-vim
+      diffview-nvim
+      vim-fugitive
+
+      # Debugging
+      nvim-dap
+      nvim-dap-ui
+      nvim-dap-virtual-text
+
+      # Treesitter
+      (pkgs.vimPlugins.nvim-treesitter.withPlugins (
+        plugins: with plugins; [
+          bash
+          comment
+          css
+          csv
+          dockerfile
+          gitattributes
+          gitignore
+          go
+          html
+          javascript
+          json
+          kdl
+          lua
+          luadoc
+          make
+          markdown
+          markdown_inline
+          nix
+          php
+          phpdoc
+          query
+          rst
+          scss
+          sql
+          terraform
+          twig
+          typescript
+          vim
+          vimdoc
+          vue
+          xml
+          yaml
+        ]
+      ))
+      nvim-treesitter-context
+      nvim-treesitter-textobjects
+
+      # LSP, linting and formatting
+      conform-nvim
+      lsp-status-nvim
+      nvim-lint
+      nvim-lspconfig
+
+      # Completion
+      cmp-buffer
+      cmp-calc
+      cmp-cmdline
+      cmp-nvim-lsp
+      cmp-path
+      cmp-treesitter
+      cmp_luasnip
+      lspkind-nvim
+      nvim-cmp
+
+      # Snippets
+      friendly-snippets
+      luasnip
+
+      # Telescope
+      plenary-nvim
+      popup-nvim
+      telescope-frecency-nvim
+      telescope-fzf-native-nvim
+      telescope-live-grep-args-nvim
+      telescope-nvim
+      telescope-ui-select-nvim
+
+      # Databases
+      vim-dadbod
+      vim-dadbod-ui
+      vim-dadbod-completion
+
+      # Themes
+      catppuccin-nvim
+
+      # Configuration.
+      inputs.self.packages.${pkgs.system}.opdavies-nvim
+    ];
+
+    extraLuaConfig = ''
+      if vim.loader then
+        vim.loader.enable()
+      end
+
+      require "opdavies"
+    '';
+
+    extraPackages = with pkgs; [
+      # Languages
+      nodePackages.typescript
+      nodejs-slim
+      php81
+
+      # Language servers
+      gopls
+      lua-language-server
+      lua54Packages.luacheck
+      marksman
+      nil
+      nodePackages."@tailwindcss/language-server"
+      nodePackages.bash-language-server
+      nodePackages.dockerfile-language-server-nodejs
+      nodePackages.intelephense
+      nodePackages.typescript-language-server
+      nodePackages.vls
+      nodePackages.volar
+      nodePackages.vscode-langservers-extracted
+      nodePackages.vue-language-server
+      nodePackages.yaml-language-server
+      phpactor
+      terraform-ls
+
+      # Formatters
+      black
+      eslint_d
+      nixfmt-rfc-style
+      nodePackages.prettier
+      stylua
+      yamlfmt
+
+      # Tools
+      hadolint
+      html-tidy
+      nodePackages.jsonlint
+      nodePackages.markdownlint-cli
+      php82Packages.php-codesniffer
+      php82Packages.phpstan
+      proselint
+      shellcheck
+      yamllint
+    ];
+  };
 
   home.file.".markdownlint.yaml".text = ''
     default: true