Flake for my NixOS devices
1{inputs, ...}: {
2 pkgs,
3 config,
4 lib,
5 ...
6}: {
7 imports = [inputs.nixvim.homeModules.nixvim];
8
9 options.cow.neovim.enable = lib.mkEnableOption "Neovim + Nixvim + Customizations";
10
11 config = lib.mkIf config.cow.neovim.enable {
12 cow.imperm.keep = [".local/share/nvim"];
13
14 home.sessionVariables.EDITOR = "nvim";
15
16 programs.nixvim = {
17 # Meta
18
19 enable = true;
20 enableMan = false;
21 defaultEditor = true;
22 viAlias = true;
23 vimAlias = true;
24
25 nixpkgs.pkgs = pkgs;
26
27 performance = {
28 byteCompileLua = {
29 enable = true;
30 nvimRuntime = true;
31 plugins = true;
32 };
33 combinePlugins = {
34 enable = true;
35 };
36 };
37
38 # Theming
39
40 colorschemes.catppuccin = lib.mkIf config.cow.cat.enable {
41 enable = true;
42 settings = {
43 inherit (config.catppuccin) flavor;
44 no_underline = false;
45 no_bold = false;
46 no_italics = false;
47 term_colors = true;
48 integrations = {
49 fidget = true;
50 markdown = true;
51 ufo = true;
52 rainbow_delimiters = true;
53 lsp_trouble = true;
54 which_key = true;
55 telescope.enabled = true;
56 treesitter = true;
57 lsp_saga = true;
58 illuminate = {
59 enabled = true;
60 lsp = true;
61 };
62 neotree = true;
63 native_lsp = {
64 enabled = true;
65 inlay_hints = {
66 background = true;
67 };
68 virtual_text = {
69 errors = ["italic"];
70 hints = ["italic"];
71 information = ["italic"];
72 warnings = ["italic"];
73 ok = ["italic"];
74 };
75 underlines = {
76 errors = ["underline"];
77 hints = ["underline"];
78 information = ["underline"];
79 warnings = ["underline"];
80 };
81 };
82 };
83 };
84 };
85
86 # Misc. Global Options
87
88 extraConfigLua = ''
89 vim.diagnostic.config({
90 signs = {
91 text = {
92 [vim.diagnostic.severity.ERROR] = "",
93 [vim.diagnostic.severity.WARN] = "",
94 },
95 },
96 })
97 '';
98
99 opts = {
100 number = true;
101 relativenumber = true;
102 smartindent = true;
103 cursorline = true;
104 showtabline = 2;
105 tabstop = 2;
106 shiftwidth = 2;
107 breakindent = true;
108 fillchars.__raw = "[[eob: ,fold: ,foldopen:,foldsep: ,foldclose:]]";
109 foldcolumn = "1";
110 foldlevel = 100;
111 foldlevelstart = 100;
112 foldenable = true;
113 mouse = "";
114 };
115
116 # Allow system clipboard copying if graphical env is enabled
117 clipboard.providers.wl-copy.enable = lib.mkDefault config.cow.gdi.enable;
118
119 # Associate .mdx extension to mdx buffer type
120 filetype.extension = {
121 mdx = "mdx";
122 };
123
124 # Auto Run Commands
125
126 autoCmd = [
127 {
128 group = "restore_cursor";
129 event = ["BufReadPost"];
130 pattern = "*";
131 callback.__raw = ''
132 function()
133 if
134 vim.fn.line "'\"" > 1
135 and vim.fn.line "'\"" <= vim.fn.line "$"
136 and vim.bo.filetype ~= "commit"
137 and vim.fn.index({ "xxd", "gitrebase" }, vim.bo.filetype) == -1
138 then
139 vim.cmd "normal! g`\""
140 end
141 end
142 '';
143 }
144 ];
145
146 autoGroups = {
147 # Try to restore cursor to the last position it was at last time this file was opened
148 restore_cursor = {};
149 };
150
151 # Keybinds
152
153 # globals.mapleader = " ";
154
155 keymaps = let
156 prefixMap = group:
157 builtins.map (k: {
158 action = "<cmd>${
159 if group ? "cmdPrefix"
160 then group.cmdPrefix + " "
161 else ""
162 }${k.action}<cr>";
163 key = "${group.prefix}${k.key}";
164 options = k.options;
165 })
166 group.keys;
167 in
168 lib.lists.flatten (
169 builtins.map (g:
170 if g ? "group"
171 then prefixMap g
172 else g) [
173 {
174 action = ''"+p'';
175 key = "<C-S-V>";
176 options.desc = "Paste from system clipboard";
177 }
178 {
179 action = ''"+y'';
180 key = "<C-S-C>";
181 options.desc = "Copy to system clipboard";
182 }
183 {
184 action = ''"+x'';
185 key = "<C-S-X>";
186 options.desc = "Cut to system clipboard";
187 }
188 {
189 action = "<cmd>Format<cr>";
190 key = "<C-S-I>";
191 options.desc = "Format Buffer";
192 }
193 {
194 group = "Tab Navigation";
195 prefix = "<Tab>";
196 keys = [
197 {
198 action = "BufferLineCycleNext";
199 key = "e";
200 options.desc = "Next Tab";
201 }
202 {
203 action = "BufferLineCyclePrev";
204 key = "q";
205 options.desc = "Previous Tab";
206 }
207 {
208 action = "Neotree toggle reveal";
209 key = "t";
210 options.desc = "Toggle Neotree";
211 }
212 ];
213 }
214 {
215 group = "Tab Closing";
216 prefix = "<Tab><Tab>";
217 keys = [
218 {
219 action = "BufferLineCloseLeft";
220 key = "q";
221 options.desc = "Close Tab Left";
222 }
223 {
224 action = "BufferLineCloseRight";
225 key = "e";
226 options.desc = "Close Tab Right";
227 }
228 {
229 action = "BufferLinePickClose";
230 key = "<Tab>";
231 options.desc = "Pick Tab and Close";
232 }
233 {
234 action = "BufferLineCloseOthers";
235 key = "w";
236 options.desc = "Close Other Tabs";
237 }
238 ];
239 }
240 {
241 action = "<cmd>Bdelete<cr>";
242 key = "<C-q>";
243 options.desc = "Close Current Buffer";
244 }
245 {
246 group = "LSP Actions";
247 prefix = "<C-.>";
248 cmdPrefix = "Lspsaga";
249 keys = [
250 {
251 action = "code_action code_action";
252 key = "a";
253 options.desc = "Code Actions";
254 }
255 {
256 action = "rename";
257 key = "r";
258 options.desc = "LSP Rename";
259 }
260 {
261 action = "diagnostic_jump_next";
262 key = "e";
263 options.desc = "Next Diagnostic";
264 }
265 {
266 action = "diagnostic_jump_previous";
267 key = "E";
268 options.desc = "Previous Diagnostic";
269 }
270 {
271 action = "goto_definition";
272 key = "d";
273 options.desc = "Jump to Definition";
274 }
275 {
276 action = "peek_definition";
277 key = "D";
278 options.desc = "Peek Definition";
279 }
280 {
281 action = "finder ref";
282 key = "fr";
283 options.desc = "Find References";
284 }
285 {
286 action = "finder imp";
287 key = "fi";
288 options.desc = "Find Implementations";
289 }
290 {
291 action = "finder def";
292 key = "fd";
293 options.desc = "Find Definitions";
294 }
295 {
296 action = "finder";
297 key = "ff";
298 options.desc = "Finder";
299 }
300 {
301 action = "hover_doc";
302 key = "h";
303 options.desc = "Hover Doc";
304 }
305 ];
306 }
307 {
308 group = "Telescope";
309 prefix = " ";
310 cmdPrefix = "Telescope";
311 keys = [
312 {
313 key = "u";
314 action = "undo";
315 options.desc = "Undo Tree";
316 }
317 {
318 key = "c";
319 action = "commands";
320 options.desc = "Browse Commands";
321 }
322 {
323 key = "w";
324 action = "spell_suggest";
325 options.desc = "Spell Suggest";
326 }
327 {
328 key = "s";
329 action = "lsp_document_symbols";
330 options.desc = "LSP Symbols";
331 }
332 {
333 key = "t";
334 action = "treesitter";
335 options.desc = "Treesitter Symbols";
336 }
337 {
338 key = "f";
339 action = "find_files";
340 options.desc = "Files";
341 }
342 {
343 key = "gs";
344 action = "git_status";
345 options.desc = "Git Status";
346 }
347 {
348 key = "gb";
349 action = "git_branches";
350 options.desc = "Git Branches";
351 }
352 {
353 key = "gc";
354 action = "git_commits";
355 options.desc = "Git Commits";
356 }
357 {
358 key = "r";
359 action = "oldfiles";
360 options.desc = "Recent Files";
361 }
362 {
363 key = "l";
364 action = "live_grep";
365 options.desc = "Live Grep";
366 }
367 ];
368 }
369 {
370 action.__raw = "[[<C-\\><C-n><C-w>]]";
371 mode = ["t"];
372 key = "<C-w>";
373 }
374 {
375 action.__raw = "[[<C-\\><C-n>]]";
376 mode = ["t"];
377 key = "<esc>";
378 }
379 {
380 action = "<cmd>WhichKey<cr>";
381 key = "<C-/>";
382 }
383 ]
384 );
385
386 # Plugins
387
388 dependencies = {
389 fd.enable = true;
390 ripgrep.enable = true;
391 tree-sitter.enable = true;
392 };
393
394 extraPlugins = with pkgs.vimPlugins;
395 (lib.optional config.cow.dev.web {plugin = pkgs.nvim-mdx;})
396 ++ [
397 {plugin = flatten-nvim;} # Opens neovim invocations in terminal windows in the current Neovim session
398 {plugin = satellite-nvim;} # Scrollbar
399 {plugin = tiny-devicons-auto-colors-nvim;} # Color language icons
400 ];
401
402 plugins = {
403 # Navigation
404
405 # Interactive Fuzzy Search w/ various providers
406 telescope = {
407 enable = true;
408 settings.defaults = {
409 layout_config.prompt_position = "top";
410 };
411 extensions = {
412 file-browser.enable = true;
413 ui-select.enable = true;
414 undo.enable = true;
415 };
416 };
417
418 # Highlight current row
419 illuminate.enable = true;
420 # Underline matching token in buffer
421 cursorline.enable = true;
422
423 # Nicer bdelete
424 bufdelete.enable = true;
425
426 # Tab bar
427 bufferline = {
428 enable = true;
429 settings.highlights.__raw = lib.mkIf config.cow.cat.enable ''
430 require("catppuccin.special.bufferline").get_theme()
431 '';
432 settings.options = {
433 indicator.style = "none";
434 show_buffer_close_icons = false;
435 show_close_icon = false;
436 offsets = [
437 {
438 filetype = "neo-tree";
439 highlight = "String";
440 text = "Files";
441 text_align = "center";
442 # separator = true;
443 }
444 ];
445 separator_style = "slant";
446 hover = {
447 enabled = true;
448 delay = 150;
449 reveal = ["close"];
450 };
451 sort_by = "insert_at_end";
452 diagnostics = "nvim_lsp";
453 diagnostics_indicator.__raw = ''
454 function(count, level, diagnostics_dict, context)
455 local icon = level:match("error") and " " or " "
456 return " " .. icon .. count
457 end
458 '';
459 };
460 };
461
462 # Tree file manager
463 neo-tree = {
464 enable = true;
465 settings = {
466 hide_root_node = false;
467 follow_current_file.enabled = true;
468 add_blank_line_at_top = true;
469 default_component_configs = {
470 container.right_padding = 2;
471 name.trailing_slash = true;
472 indent = {
473 indent_size = 2;
474 with_expanders = true;
475 };
476 };
477 window.width = 40;
478 auto_clean_after_session_restore = true;
479 close_if_last_window = true;
480 filesystem.components.name.__raw = ''
481 function(config, node, state)
482 local components = require('neo-tree.sources.common.components')
483 local name = components.name(config, node, state)
484 if node:get_depth() == 1 then
485 name.text = vim.fs.basename(vim.loop.cwd() or "") .. "/"
486 end
487 return name
488 end
489 '';
490 };
491 };
492
493 # In-buffer UI/tweaks
494
495 # Toggle relativenumber off when inserting
496 numbertoggle.enable = true;
497
498 # Folding implementation
499 nvim-ufo.enable = true;
500
501 # Nicer indenting
502 indent-o-matic.enable = true;
503 intellitab.enable = true;
504
505 # Image Previews
506 image.enable = true;
507
508 # Color Picker
509 ccc = {
510 enable = true;
511 settings = {
512 inputs = [
513 "ccc.input.rgb"
514 "ccc.input.hsl"
515 "ccc.input.hwb"
516 "ccc.input.lab"
517 "ccc.input.lch"
518 "ccc.input.oklab"
519 "ccc.input.oklch"
520 "ccc.input.cmyk"
521 "ccc.input.hsluv"
522 "ccc.input.okhsl"
523 "ccc.input.hsv"
524 "ccc.input.okhsv"
525 "ccc.input.xyz"
526 ];
527 };
528 };
529
530 # Completions
531 cmp = {
532 enable = true;
533 settings = {
534 sources = map (name: {inherit name;}) [
535 "nvim_lsp"
536 "nvim_lsp_signature_help"
537 "spell"
538 "path"
539 "buffer"
540 ];
541 mapping = {
542 "<Esc>" = "cmp.mapping.abort()";
543 "<Tab>" = "cmp.mapping.confirm({ select = true })";
544 "<Up>" = "cmp.mapping(cmp.mapping.select_prev_item(), {'i', 's'})";
545 "<Down>" = "cmp.mapping(cmp.mapping.select_next_item(), {'i', 's'})";
546 };
547 };
548 };
549
550 # Color-coded matching symbols
551 rainbow-delimiters.enable = true;
552
553 # Line number column + LSP + folding + etc.
554 statuscol = {
555 enable = true;
556 settings.segments = let
557 dispCond = {
558 __raw = ''
559 function(ln)
560 return vim.bo.filetype ~= "neo-tree"
561 end
562 '';
563 };
564 in [
565 {
566 click = "v:lua.ScSa";
567 condition = [
568 dispCond
569 ];
570 text = [
571 "%s"
572 ];
573 }
574 {
575 click = "v:lua.ScLa";
576 condition = [dispCond];
577 text = [
578 {
579 __raw = "require('statuscol.builtin').lnumfunc";
580 }
581 ];
582 }
583 {
584 click = "v:lua.ScFa";
585 condition = [
586 dispCond
587 {
588 __raw = "require('statuscol.builtin').not_empty";
589 }
590 ];
591 text = [
592 {
593 __raw = "require('statuscol.builtin').foldfunc";
594 }
595 " "
596 ];
597 }
598 ];
599 };
600
601 # Informational bottom line
602 lualine = {
603 enable = true;
604 settings = {
605 extensions = [
606 "trouble"
607 "toggleterm"
608 ];
609
610 options = {
611 theme = lib.mkIf config.cow.cat.enable "catppuccin";
612 disabled_filetypes = ["neo-tree"];
613 ignore_focus = ["neo-tree"];
614 };
615 };
616 };
617
618 # New Windows
619
620 # Nice notifications and progress indicator
621 fidget = {
622 enable = true;
623 settings.notification = {
624 override_vim_notify = true;
625 window = {
626 y_padding = 2;
627 x_padding = 2;
628 zindex = 50;
629 align = "top";
630 winblend = 0;
631 };
632 };
633 };
634
635 # Interactive keybind helper
636 which-key = {
637 enable = true;
638 settings = {
639 show_help = true;
640 preset = "modern";
641 win.wo.winblend = 8;
642 };
643 };
644
645 # Toggle a Terminal Window
646 toggleterm = {
647 enable = true;
648 luaConfig.post = ''
649 local flatten = require('flatten')
650
651 ---@type Terminal?
652 local saved_terminal
653
654 flatten.setup({
655 hooks = {
656 should_block = function(argv)
657 return vim.tbl_contains(argv, "-b")
658 end,
659 pre_open = function()
660 local term = require("toggleterm.terminal")
661 local termid = term.get_focused_id()
662 saved_terminal = term.get(termid)
663 end,
664 post_open = function(opts)
665 if saved_terminal then
666 saved_terminal:close()
667 else
668 vim.api.nvim_set_current_win(opts.winnr)
669 end
670
671 if opts.filetype == "gitcommit" or opts.filetype == "gitrebase" then
672 vim.api.nvim_create_autocmd("BufWritePost", {
673 buffer = opts.bufnr,
674 once = true,
675 callback = vim.schedule_wrap(function()
676 require('bufdelete').bufdelete(opts.bufnr, true)
677 end),
678 })
679 end
680 end,
681 block_end = function()
682 vim.schedule(function()
683 if saved_terminal then
684 saved_terminal:open()
685 saved_terminal = nil
686 end
687 end)
688 end,
689 },
690 window = {
691 open = "alternate",
692 },
693 })
694 '';
695 settings = {
696 size = 20;
697 open_mapping = "[[<C-x>]]";
698 direction = "horizontal";
699 start_in_insert = true;
700 insert_mappings = true;
701 terminal_mappings = true;
702 };
703 };
704
705 # Language Integration and LSPs
706
707 # Provider for syntax highlighting, symbols, etc. when not using an LSP
708 treesitter = {
709 enable = true;
710 luaConfig.post = lib.mkIf config.cow.dev.web ''
711 require('mdx').setup()
712 '';
713 settings = {
714 highlight = {
715 enable = true;
716 additional_vim_regex_highlighting = false;
717 };
718 indent.enable = true;
719 };
720 };
721
722 # Formatting code using multiple providers
723 conform-nvim = {
724 enable = true;
725 settings = {
726 formatters.treefmt = {
727 require_cwd = false;
728 };
729 formatters_by_ft = {
730 "*" = ["treefmt"];
731 };
732 default_format_opts = {
733 lsp_format = "fallback";
734 };
735 };
736 # Taken from https://github.com/stevearc/conform.nvim/blob/master/doc/recipes.md#format-command
737 luaConfig.post = ''
738 vim.api.nvim_create_user_command("Format", function(args)
739 local range = nil
740 if args.count ~= -1 then
741 local end_line = vim.api.nvim_buf_get_lines(0, args.line2 - 1, args.line2, true)[1]
742 range = {
743 start = { args.line1, 0 },
744 ["end"] = { args.line2, end_line:len() },
745 }
746 end
747 require("conform").format({ range = range, timeout_ms = 5000 })
748 end, { range = true })
749 '';
750 };
751
752 # Common buffer type associations to activate LSPs
753 lspconfig.enable = true;
754
755 # UI for many LSP features
756 lspsaga = {
757 enable = true;
758 settings = {
759 symbol_in_winbar.enable = false;
760 implement.enable = false;
761 lightbulb.enable = false;
762 ui = {
763 code_action = "";
764 actionfix = "";
765 };
766 hover = {
767 openCmd = "!xdg-open";
768 openLink = "<leader>o";
769 maxWidth = 0.5;
770 maxHeight = 0.4;
771 };
772 rename.autoSave = true;
773 finder = {
774 keys.close = "<ESC>";
775 };
776 codeAction.keys.quit = "<ESC>";
777 };
778 };
779
780 # Get latest version of deps in a Cargo.toml as inline hints
781 crates.enable = lib.mkDefault config.cow.dev.rust;
782
783 # Better TS LSP, etc.
784 typescript-tools.enable = lib.mkDefault config.cow.dev.web;
785
786 # Misc. UI
787
788 # UI and provider for diagnostics
789 trouble = {
790 enable = true;
791 };
792
793 # Icons used in many places for languages
794 web-devicons.enable = true;
795 };
796
797 lsp = lib.mkDefault {
798 inlayHints.enable = true;
799
800 servers = let
801 inherit
802 (config.cow.dev)
803 dotnet
804 python
805 haskell
806 rust
807 web
808 c
809 typst
810 ;
811 in {
812 clangd.enable = c;
813 tinymist.enable = typst;
814 astro.enable = web;
815 hls = lib.mkIf haskell {
816 enable = true;
817 # ghcPackage = pkgs.haskell.compiler.ghc912;
818 package = pkgs.haskell.packages.ghc912.haskell-language-server;
819 };
820 mdx_analyzer = lib.mkIf web {
821 enable = true;
822 package = pkgs.mdx-language-server;
823 };
824 # ts_ls.enable = web;
825 html.enable = web;
826 emmer_language_server.enable = web;
827 marksman.enable = web;
828 cssls.enable = web;
829 jsonls.enable = web;
830 yamlls.enable = web;
831 ruff.enable = python;
832 csharp_ls.enable = dotnet;
833 nil_ls.enable = true;
834 bashls.enable = true;
835 nushell.enable = config.cow.nushell.enable;
836 taplo.enable = rust;
837 typos_lsp.enable = true;
838 rust_analyzer = lib.mkIf rust {
839 enable = true;
840 package = pkgs.rust-analyzer-nightly;
841 packageFallback = true;
842 };
843 lemminx.enable = web;
844 eslint.enable = web;
845 just.enable = config.cow.utils.enable;
846 };
847 };
848 };
849 };
850}