My personal configuration files and scripts.
1-- Preamble ⦃
2
3-- Add a more convenient way of creating key bindings.
4local function map(mode, lhs, rhs, options)
5 local base_options = { noremap = true, silent = true }
6 vim.keymap.set(
7 mode,
8 lhs,
9 rhs,
10 not options and base_options
11 or vim.tbl_extend("force", base_options, options)
12 )
13end
14
15-- Bootstrap the plugin manager.
16local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
17if not vim.uv.fs_stat(lazypath) then
18 vim.fn.system({
19 "git",
20 "clone",
21 "--filter=blob:none",
22 "https://github.com/folke/lazy.nvim.git",
23 "--branch=stable",
24 lazypath,
25 })
26end
27vim.opt.rtp:prepend(lazypath)
28
29-- Set the global leader to SPC and the local leader to SPC m.
30vim.g.mapleader = " " -- NOTE: Must be set before specifying plugins.
31vim.g.maplocalleader = " m"
32
33-- ⦄
34
35-- Plugins ⦃
36
37local lazy_config = {
38 performance = {
39 rtp = {
40 -- Prevent loading bundled plugins that are not required or superseded by
41 -- other plugins.
42 disabled_plugins = {
43 "gzip",
44 "matchit",
45 "matchparen",
46 "netrwPlugin",
47 "tarPlugin",
48 "tohtml",
49 "tutor",
50 "zipPlugin",
51 },
52 },
53 },
54}
55
56require("lazy").setup({
57 -- Libraries ⦃
58 { "nvim-lua/plenary.nvim", name = "plenary" },
59 -- ⦄
60
61 -- Visuals ⦃
62 {
63 "catppuccin/nvim",
64 name = "catppuccin",
65 priority = 1000,
66 opts = {
67 show_end_of_buffer = true,
68 dim_inactive = { enabled = true },
69 custom_highlights = function(colors)
70 return {
71 -- Make the background for folds slightly more muted than the default
72 -- of `colors.surface1`.
73 Folded = { bg = colors.surface0 },
74 }
75 end,
76 },
77 },
78 {
79 "nvim-lualine/lualine.nvim",
80 event = "VeryLazy",
81 dependencies = { "kyazdani42/nvim-web-devicons" },
82 opts = {
83 options = {
84 theme = "catppuccin",
85 section_separators = { left = "", right = "" },
86 -- Only display one 'global' status line rather than one per window.
87 globalstatus = true,
88 },
89 sections = {
90 lualine_a = {
91 { "mode", separator = { left = "" }, right_padding = 2 },
92 },
93 lualine_b = { "filename", "branch" },
94 lualine_c = {},
95 lualine_x = {},
96 lualine_y = {
97 "filetype",
98 {
99 "fileformat",
100 separator = { right = "" },
101 padding = { left = 1, right = 0 },
102 },
103 "encoding",
104 "progress",
105 },
106 lualine_z = {
107 { "location", separator = { right = "" }, left_padding = 2 },
108 },
109 },
110 inactive_sections = {
111 lualine_a = { "filename" },
112 lualine_b = {},
113 lualine_c = {},
114 lualine_x = {},
115 lualine_y = {},
116 lualine_z = { "location" },
117 },
118 extensions = { "lazy", "mundo", "quickfix" },
119 },
120 },
121 { "stevearc/dressing.nvim", name = "dressing", event = "VeryLazy" },
122 -- ⦄
123
124 -- Interface ⦃
125 {
126 "folke/persistence.nvim",
127 event = "BufReadPre", -- this will only start session saving when an actual file was opened
128 opts = {},
129 },
130 {
131 "folke/which-key.nvim",
132 name = "which-key",
133 config = true,
134 },
135 {
136 "folke/snacks.nvim",
137 dependencies = { "echasnovski/mini.nvim", version = false },
138 priority = 1000,
139 lazy = false,
140 opts = {
141 dashboard = { enabled = true },
142 explorer = { enabled = true },
143 input = { enabled = true },
144 picker = { enabled = true },
145 session = { enabled = true },
146 statuscolumn = {
147 enabled = true,
148 folds = {
149 -- Display an icon for an open fold.
150 open = true,
151 -- Indicate added, changed, and deleted lines.
152 git_hl = true,
153 },
154 },
155 zen = { enabled = true },
156 },
157 keys = {
158 {
159 "<leader><leader>",
160 function()
161 Snacks.picker.smart()
162 end,
163 desc = "Smart Find Files",
164 },
165 {
166 "<leader>,",
167 function()
168 Snacks.picker.buffers()
169 end,
170 desc = "Buffers",
171 },
172 {
173 "<leader>/",
174 function()
175 Snacks.picker.grep()
176 end,
177 desc = "Grep",
178 },
179 {
180 "<leader>:",
181 function()
182 Snacks.picker.command_history()
183 end,
184 desc = "Command History",
185 },
186 {
187 "<leader>n",
188 function()
189 Snacks.picker.notifications()
190 end,
191 desc = "Notification History",
192 },
193 {
194 "<leader>e",
195 function()
196 Snacks.explorer()
197 end,
198 desc = "File Explorer",
199 },
200
201 {
202 "<leader>fb",
203 function()
204 Snacks.picker.buffers()
205 end,
206 desc = "Buffers",
207 },
208 {
209 "<leader>fc",
210 function()
211 Snacks.picker.files({ cwd = vim.fn.stdpath("config") })
212 end,
213 desc = "Find Config File",
214 },
215 {
216 "<leader>ff",
217 function()
218 Snacks.picker.files()
219 end,
220 desc = "Find Files",
221 },
222 {
223 "<leader>fg",
224 function()
225 Snacks.picker.git_files()
226 end,
227 desc = "Find Git Files",
228 },
229 {
230 "<leader>fp",
231 function()
232 Snacks.picker.projects()
233 end,
234 desc = "Projects",
235 },
236 {
237 "<leader>fr",
238 function()
239 Snacks.picker.recent()
240 end,
241 desc = "Recent",
242 },
243
244 {
245 "<leader>z",
246 function()
247 Snacks.zen()
248 end,
249 desc = "Toggle Zen Mode",
250 },
251 {
252 "<leader>N",
253 desc = "Neovim News",
254 function()
255 Snacks.win({
256 file = vim.api.nvim_get_runtime_file("doc/news.txt", false)[1],
257 width = 0.9,
258 height = 0.8,
259 wo = {
260 spell = false,
261 wrap = false,
262 signcolumn = "yes",
263 statuscolumn = " ",
264 conceallevel = 3,
265 },
266 })
267 end,
268 },
269 },
270 },
271 {
272 "mikavilpas/yazi.nvim",
273 event = "VeryLazy",
274 dependencies = { "folke/snacks.nvim", lazy = false },
275 opts = {
276 open_for_directories = true,
277 },
278 init = function()
279 -- Fake loading the netrw plugin so that the `open_for_directories` option
280 -- functions.
281 vim.g.loaded_netrwPlugin = 1
282 end,
283 keys = {
284 {
285 "<leader>fb",
286 mode = { "n", "v" },
287 "<cmd>Yazi<cr>",
288 desc = "Browse files",
289 },
290 },
291 },
292 { "simnalamburt/vim-mundo", cmd = { "MundoShow", "MundoToggle" } },
293 {
294 "lewis6991/gitsigns.nvim",
295 name = "gitsigns",
296 event = "VeryLazy",
297 config = true,
298 },
299 {
300 "folke/todo-comments.nvim",
301 dependencies = { "folke/snacks.nvim" },
302 event = { "BufNewFile", "BufReadPost" },
303 opts = {
304 search = { pattern = [[\b(KEYWORDS)(\([^\)]*\))?:]] },
305 highlight = { pattern = [[.*<((KEYWORDS)%(\(.{-1,}\))?):]] },
306 keywords = {
307 -- This keyword is often used in Rust code to mark comments justifying
308 -- why the code inside an unsafe block is actually safe.
309 SAFETY = { icon = "☢", color = "warning" },
310 }
311 },
312 keys = {
313 {
314 "<leader>st",
315 function()
316 Snacks.picker.todo_comments()
317 end,
318 desc = "Todo",
319 },
320 {
321 "<leader>sT",
322 function()
323 Snacks.picker.todo_comments({ keywords = { "TODO", "FIX", "FIXME" } })
324 end,
325 desc = "Todo/Fix/Fixme",
326 },
327 },
328 },
329 {
330 "NvChad/nvim-colorizer.lua",
331 name = "colorizer",
332 event = "VeryLazy",
333 opts = { filetypes = { "html", "css", "javascript", "typescript" } },
334 },
335 { "rcarriga/nvim-notify", name = "notify", event = "VeryLazy" },
336 { "folke/noice.nvim", event = "VeryLazy" },
337 -- ⦄
338
339 -- Editing ⦃
340 {
341 "tmsvg/pear-tree",
342 event = "InsertEnter",
343 init = function()
344 vim.g.pear_tree_smart_openers = 1
345 vim.g.pear_tree_smart_closers = 1
346 vim.g.pear_tree_smart_backspace = 1
347 end,
348 },
349 { "echasnovski/mini.ai", config = true },
350 { "echasnovski/mini.comment", config = true },
351 { "kylechui/nvim-surround", config = true },
352 {
353 "ggandor/leap.nvim",
354 name = "leap",
355 config = function()
356 require("leap").add_default_mappings()
357 end,
358 },
359 { "ggandor/leap-spooky.nvim", name = "leap-spooky", config = true },
360 { "ggandor/flit.nvim", name = "flit", config = true },
361 -- ⦄
362
363 -- Completion and Diagnostics ⦃
364 -- TODO: Set up completion.
365 --- ⦄
366
367 -- Language Support ⦃
368 {
369 "nvim-treesitter/nvim-treesitter",
370 -- TODO: Remove this once main is the default branch.
371 branch = "main",
372 lazy = false,
373 build = function()
374 local ts = require("nvim-treesitter")
375 ts.install({
376 "bash",
377 "bibtex",
378 "c",
379 "c_sharp",
380 "cmake",
381 "cpp",
382 "css",
383 "dockerfile",
384 "eex",
385 "elixir",
386 "erlang",
387 "fish",
388 "glsl",
389 "go",
390 "haskell",
391 "heex",
392 "html",
393 "java",
394 "javascript",
395 "jsdoc",
396 "json",
397 "just",
398 "kotlin",
399 "lua",
400 "markdown",
401 "markdown_inline",
402 "meson",
403 "ninja",
404 "nix",
405 "ocaml",
406 "ocaml_interface",
407 "perl",
408 "php",
409 "prolog",
410 "python",
411 "qmljs",
412 "query",
413 "regex",
414 "ruby",
415 "rust",
416 "sql",
417 "svelte",
418 "toml",
419 "typescript",
420 "typst",
421 "vim",
422 "vimdoc",
423 "wgsl",
424 "wit",
425 "yaml",
426 "zig",
427 })
428 ts.update()
429 end,
430 },
431 { "Shirk/vim-gas" },
432 { "lervag/vimtex", ft = "tex" },
433 {
434 "nvim-neorg/neorg",
435 ft = "norg",
436 config = function()
437 require("neorg").setup({
438 load = {
439 ["core.defaults"] = {},
440 ["core.norg.concealer"] = {},
441 ["core.norg.completion"] = {
442 config = { engine = "nvim-cmp" },
443 },
444 ["core.integrations.nvim-cmp"] = {},
445 },
446 })
447 end,
448 },
449 -- ⦄
450}, lazy_config)
451
452-- ⦄
453
454-- Visuals ⦃
455
456-- Use 24-bit colour like it's 1995!
457vim.opt.termguicolors = true
458
459-- Load my default colorscheme of choice.
460vim.cmd.colorscheme("catppuccin-mocha")
461
462-- The current mode is already displayed in the status line (by Lualine).
463vim.opt.showmode = false
464
465-- Modify cursor styling so that...
466vim.opt.guicursor = {
467 -- the normal, visual, command line normal, and show-match modes use a block
468 -- cursor with the default colours defined by the terminal;
469 "n-v-c-sm:block",
470 -- the insert, command line insert, and visual with selection modes use a
471 -- blinking, vertical cursor with colours from the `Cursor` highlight group;
472 "i-ci-ve:ver25-blinkwait700-blinkoff400-blinkon250-Cursor/lCursor",
473 -- the replace, command line replace, and operator-pending modes use a
474 -- blinking, horizontal cursor with colours from the `Cursor` highlight group;
475 "r-cr-o:hor20-blinkwait700-blinkoff400-blinkon250-Cursor/lCursor",
476 -- the built-in terminal uses a blinking, block cursor with colours from the
477 -- `TermCursor` highlight group.
478 "t:block-blinkon500-blinkoff500-TermCursor",
479}
480
481-- ⦄
482
483-- Interface ⦃
484
485map("n", "<leader>q", ":qa<CR>", { desc = "Quit" })
486map("n", "<leader>Q", ":qa<CR>", { desc = "Force quit" })
487
488-- Disable arrow keys in normal/visual mode so that I can break the habit.
489for _, direction in ipairs({ "up", "down", "left", "right" }) do
490 map("", "<" .. direction .. ">", "<nop>")
491end
492
493-- Set the title of the window if possible.
494vim.opt.title = true
495
496-- Always display the sign column to avoid unwarranted jumps.
497vim.opt.signcolumn = "yes"
498
499-- Do not wrap lines longer than the window width.
500vim.opt.wrap = false
501
502-- Align wrapped lines with the indentation for that line.
503vim.opt.breakindent = true
504
505-- Enable flash on yank.
506vim.api.nvim_create_autocmd("TextYankPost", {
507 group = vim.api.nvim_create_augroup("YankFlash", {}),
508 callback = function()
509 vim.highlight.on_yank({ higroup = "Visual", timeout = 300 })
510 end,
511})
512
513-- Improve the behaviour of search.
514vim.opt.ignorecase = true
515vim.opt.smartcase = true
516vim.opt.gdefault = true
517
518-- Start scrolling 4 lines before the edge of the window.
519vim.opt.scrolloff = 4
520vim.opt.sidescrolloff = 4
521
522-- Enable Tree-Sitter-powered folding and automatically close folds that are 12
523-- or more levels deep. That much nesting should be fairly rare.
524vim.opt.foldlevel = 12
525vim.opt.foldmethod = "expr"
526vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()"
527vim.opt.fillchars:append({ fold = " " })
528
529-- Enable relative line numbers.
530vim.opt.number = true
531vim.opt.relativenumber = true
532
533-- More specifically, use relative line numbers in normal mode and absolute line
534-- numbers in insert mode.
535local line_number_group = vim.api.nvim_create_augroup("LineNumber", {})
536vim.api.nvim_create_autocmd(
537 { "BufEnter", "FocusGained", "InsertLeave", "WinEnter" },
538 {
539 group = line_number_group,
540 callback = function()
541 if vim.opt.number:get() then
542 vim.opt.relativenumber = true
543 end
544 end,
545 }
546)
547vim.api.nvim_create_autocmd(
548 { "BufLeave", "FocusLost", "InsertEnter", "WinLeave" },
549 {
550 group = line_number_group,
551 callback = function()
552 if vim.opt.number:get() then
553 vim.opt.relativenumber = false
554 end
555 end,
556 }
557)
558
559-- Disable line numbers in the integrated terminal and default to insert mode.
560vim.api.nvim_create_autocmd("TermOpen", {
561 group = vim.api.nvim_create_augroup("CustomizeTerminal", {}),
562 callback = function()
563 vim.opt_local.number = false
564 vim.opt_local.relativenumber = false
565
566 vim.cmd.startinsert()
567 end,
568})
569
570-- Highlight the line the cursor is on in the currently active buffer.
571local cursor_line_group = vim.api.nvim_create_augroup("ShowCursorLine", {})
572vim.api.nvim_create_autocmd({ "VimEnter", "WinEnter", "BufWinEnter" }, {
573 group = cursor_line_group,
574 callback = function()
575 vim.opt_local.cursorline = true
576 end,
577})
578vim.api.nvim_create_autocmd({ "WinLeave" }, {
579 group = cursor_line_group,
580 callback = function()
581 vim.opt_local.cursorline = false
582 end,
583})
584
585-- Jump to the last known cursor position when opening a file, so long as the
586-- position is still valid and we are not editing a commit message (it's likely
587-- a different one than last time).
588vim.api.nvim_create_autocmd("BufReadPost", {
589 group = vim.api.nvim_create_augroup("RestorePosition", {}),
590 callback = function(args)
591 local last_posn = vim.fn.line([['"]])
592
593 local is_valid_line = 0 <= last_posn and last_posn <= vim.fn.line("$")
594 -- TODO: Investigate replacing this by more generally excluding such files
595 -- from shada.
596 local is_commit =
597 vim.list_contains({ "commit", "jjdescription" }, vim.b[args.buf].filetype)
598
599 if not is_valid_line or is_commit then
600 return
601 end
602
603 vim.cmd.normal({ args = { 'g`"' }, bang = true })
604 -- Deferring this command is somewhat of a hack to ensure that the fold
605 -- containing the cursor is expanded since folds can be created (are
606 -- created?) _after_ the `BufReadPost` event.
607 vim.defer_fn(function()
608 vim.cmd.normal({ args = { "zv" }, bang = true })
609 end, 5)
610 end,
611})
612
613-- Automatically create parent directories when saving a file.
614vim.api.nvim_create_autocmd("BufWritePre", {
615 group = vim.api.nvim_create_augroup("CreateParentDirs", {}),
616 callback = function(event)
617 local file = vim.uv.fs_realpath(event.match) or event.match
618
619 vim.fn.mkdir(vim.fn.fnamemodify(file, ":p:h"), "p")
620 local backup = vim.fn.fnamemodify(file, ":p:~:h")
621 backup = backup:gsub("[/\\]", "%%")
622 vim.opt_global.backupext = backup
623 end,
624})
625
626-- ⦄
627
628-- Editing ⦃
629
630-- Use 2 spaces for indentation.
631local indent_width = 2
632vim.opt.tabstop = indent_width -- indenting with tab
633vim.opt.softtabstop = indent_width
634vim.opt.shiftwidth = indent_width -- indenting with >
635vim.opt.expandtab = true -- use spaces, not tab characters
636
637-- Use 80 characters as the maximum line length.
638local text_width = 80
639vim.opt.textwidth = text_width
640vim.opt.colorcolumn = { text_width + 1 }
641
642-- Stop the Rust ftplugin from fucking with the above.
643vim.g.rust_recommended_style = false
644
645-- Delete a level of indentation with Shift+Tab.
646map("i", "<S-Tab>", "<C-d>")
647
648-- Map Ctrl+Backspace to delete backwards by words.
649map("i", "<C-BS>", "<C-w>")
650
651-- Display invisible characters.
652vim.opt.list = true
653vim.opt.listchars = {
654 tab = "•·",
655 trail = "·",
656 extends = "❯",
657 precedes = "❮",
658 nbsp = "×",
659}
660
661-- Persist undo history.
662vim.opt.undofile = true
663
664-- Add dedicated key bindings for yanking to and pasting from the system
665-- clipboard.
666map({ "n", "v" }, "<C-y>", '"+y', { desc = "Yank to clipboard" })
667map({ "n", "v" }, "<C-p>", '"+p', { desc = "Paste from clipboard" })
668
669-- Associate the GLSL filetype with the typical file extensions used for
670-- shaders.
671vim.api.nvim_create_autocmd({ "BufNewFile", "BufRead" }, {
672 group = vim.api.nvim_create_augroup("AssociateGlsl", {}),
673 pattern = { "*.vert", "*.frag", "*.tesc", "*.geom", "*.comp" },
674 callback = function()
675 vim.opt_local.filetype = "glsl"
676 end,
677})
678
679-- ⦄
680
681-- Completion and Diagnostics ⦃
682
683-- Use British English and New Zealand English dictionaries for spell checking.
684vim.opt.spelllang = { "en_gb", "en_nz" }
685
686-- Enable spell checking.
687vim.opt.spell = true
688
689-- ⦄
690
691-- vim: set foldlevel=0 foldmethod=marker foldmarker=⦃,⦄: