my neovim config
1-- ============================================================================
2-- ___ _
3-- / _ | ___ ___ ____ ___ _ __(_)_ _
4-- / __ |/ _ \/ _ `(_-<_ / _ \ |/ / / ' \
5-- /_/ |_/_//_/\_,_/___(_)_//_/___/_/_/_/_/
6--
7-- ============================================================================
8
9-- ============================================================================
10-- BOOTSTRAP LAZY. NVIM (Plugin Manager)
11-- ============================================================================
12local lazy_path = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
13if not vim.loop.fs_stat(lazy_path) then
14 vim.fn.system({
15 "git",
16 "clone",
17 "--filter=blob:none",
18 "https://github.com/folke/lazy.nvim.git",
19 "--branch=stable",
20 lazy_path,
21 })
22end
23vim.opt.rtp: prepend(lazy_path)
24-- ============================================================================
25-- CORE SETTINGS
26-- ============================================================================
27vim.loader.enable()
28package.path = package.path .. ';' .. vim.fn.stdpath('config') .. '/?.lua'
29-- I'm lazy
30local opt = vim.opt
31
32-- Nice Number Line
33opt.relativenumber = true
34opt.number = true
35
36-- Tabs & Spaces
37opt.expandtab = true
38opt.tabstop = 4
39opt.softtabstop = 4
40opt.shiftwidth = 4
41opt.smartindent = true
42opt.autoindent = true
43
44-- Searching
45opt.ignorecase = true
46opt.smartcase = true
47opt.hlsearch = true
48opt.incsearch = true
49
50-- Column Settings
51opt.signcolumn = "yes"
52opt.colorcolumn = "120"
53opt.wrap = false
54
55-- Cursor Settings
56opt.cursorline = true
57opt.cursorcolumn = true
58-- opt.guicursor =
59-- "n-v-c-sm:block,ci-ve:ver25,r-cr-o:hor20,i:block-blinkoff1-blinkon1"
60
61-- Window Settings
62opt.splitbelow = true
63opt.splitright = true
64opt.lazyredraw = true
65opt.showtabline = 0
66
67-- Spelling
68opt.spell = true
69opt.spelloptions = "camel"
70
71-- Other
72opt.mouse = "a"
73opt.showmode = false
74opt.termguicolors = true
75opt.hidden = true
76opt.clipboard = "unnamedplus"
77opt.formatoptions = "cjql"
78opt.laststatus = 3
79opt.completeopt = { "menu", "menuone", "preview" }
80opt.conceallevel = 2
81opt.concealcursor = ""
82-- opt.updatetime = 100
83
84-- Backup -- I have power issues :/
85opt.backup = true
86opt.swapfile = true
87opt.undodir = os.getenv("HOME") .. "/.cache/undodir"
88opt.undofile = true
89
90-- arabic support
91opt.encoding = "utf-8"
92opt.termbidi = true
93
94-- ============================================================================
95-- UTILITY FUNCTIONS
96-- ============================================================================
97local M = {}
98
99M.bootstrap = function(lazy_path)
100 if not vim.loop.fs_stat(lazy_path) then
101 vim.fn.system({
102 "git",
103 "clone",
104 "--filter=blob:none",
105 "https://github.com/folke/lazy.nvim.git",
106 "--branch=stable",
107 lazy_path,
108 })
109 end
110 vim.opt.rtp:prepend(lazy_path)
111end
112
113local cmds = { "nu!", "rnu!", "nonu!" }
114local current_index = 1
115
116function M.toggle_numbering()
117 current_index = current_index % #cmds + 1
118 vim.cmd("set " .. cmds[current_index])
119 local signcolumn_setting = "auto"
120 if cmds[current_index] == "nonu!" then signcolumn_setting = "yes: 4" end
121 vim.opt.signcolumn = signcolumn_setting
122end
123
124function M.custom_lua_format()
125 local buf = vim.api.nvim_get_current_buf()
126 local filepath = vim.api.nvim_buf_get_name(buf)
127 vim.api.nvim_command("write")
128 local cmd = string.format(
129 "lua-format -i --indent-width=2 --no-use-tab --keep-simple-function-one-line --keep-simple-control-block-one-line --single-quote-to-double-quote --spaces-inside-table-braces --spaces-around-equals-in-field %s",
130 filepath
131 )
132 os.execute(cmd)
133 vim.api.nvim_command("edit")
134end
135
136-- ============================================================================
137-- KEYBINDINGS
138-- ============================================================================
139
140local map = vim.keymap.set
141local cmd = vim.cmd
142
143-- Easy wq
144cmd(":command! WQ wq")
145cmd(":command! WQ wq")
146cmd(":command! Wq wq")
147cmd(":command! Wqa xall")
148cmd(":command! Waq xall")
149cmd(":command! WA wa")
150cmd(":command! Wa wa")
151cmd(":command! W w")
152cmd(":command! Q q")
153
154-- Set up <Space> to be the leader key
155map("n", "<Space>", "<NOP>", { noremap = true, silent = true })
156vim.g.mapleader = " "
157
158-- Easy :
159map("n", ";", ":", { noremap = true })
160
161-- Easy Resize
162map("n", "<M-c>", ":vertical resize -2<CR>", { noremap = true })
163map("n", "<M-k>", ":resize +2<CR>", { noremap = true })
164-- map("n", "<M-k>", ":resize -2<CR>", {noremap = true})
165map("n", "<M-d>", ":vertical resize +2<CR>", { noremap = true })
166
167-- Easy Capitalization
168map("n", "<F7>", "<Esc>V:s/\\v<(.)(\\w*)/\\u\\1\\L\\2/g<CR>:noh<CR>", {})
169map("v", "<F7>", "<Esc>V:s/\\v<(.)(\\w*)/\\u\\1\\L\\2/g<CR>:noh<CR>", {})
170map("i", "<F7>", "<Esc>V:s/\\v<(.)(\\w*)/\\u\\1\\L\\2/g<CR>:noh<CR>i", {})
171
172-- The Best Thing In vscode But Better
173map("v", "U", ":m '<-2<CR>gv=gv")
174map("v", "Y", ":m '>+1<CR>gv=gv")
175
176-- Replace The Current Word
177-- map.set("n", "<leader>s", [[:%s/\<<C-r><C-w>\>/<C-r><C-w>/gI<Left><Left><Left>]])
178
179-- The BEST MAP
180map("x", "<Leader>p", '"_dP')
181
182-- STOP USING THE ARROW KEYS !!
183-- map('n', '<Up>', [[:echoerr "Do not do that!!"<cr>]], {noremap = true})
184-- map('n', '<Down>', [[:echoerr "Do not do that!!"<cr>]], {noremap = true})
185-- map('n', '<Left>', [[:echoerr "Do not do that!!"<cr>]], {noremap = true})
186-- map('n', '<Right>', [[:echoerr "Do not do that!!"<cr>]], {noremap = true})
187
188-- I use Dvorak layout, so.. no hjkl :|
189-- Instead, I will use -kcd
190local modes = { "n", "v", "s", "o" } -- Normal, visual, select, operator-pending
191local keys = { { "h", "-" }, { "j", "c" }, { "l", "d" } }
192for _, mode in ipairs(modes) do
193 for _, key in ipairs(keys) do
194 map(mode, key[1], key[2], { noremap = true })
195 map(mode, key[2], key[1], { noremap = true })
196 end
197end
198
199-- Keybinds Reloading Init Files
200map("n", "<Leader>vr", "<Cmd>luafile $MYVIMRC<CR>", { noremap = true })
201
202-- Deleting Words With <C-Bs>
203map("i", "", "<C-w>", { noremap = true, silent = true })
204map("c", "", "<C-w>", { noremap = true, silent = true })
205map("i", "<C-BS>", "<C-w>", { noremap = true, silent = true })
206map("c", "<C-BS>", "<C-w>", { noremap = true })
207
208-- Better Indent/Unintend Lines And Blocks Of Text
209map("n", ">", ">>", { noremap = true, silent = true })
210map("n", "<", "<<", { noremap = true, silent = true })
211map("v", ">", ">gv", { noremap = true, silent = true })
212map("v", "<", "<gv", { noremap = true, silent = true })
213
214-- Make Y Actually Make Sense
215map("n", "Y", "yg$", { noremap = true, silent = true })
216
217-- Buffer Navigation
218map("n", "<Leader><Tab>", ":bn<CR>", { noremap = true, silent = true })
219map("n", "<Leader><S-Tab>", ":bp<CR>", { noremap = true, silent = true })
220
221-- Buffer Deletion
222map("n", "<Leader>bd", ":bd<CR>", { noremap = true, silent = true })
223
224-- Window Navigation
225map("n", "<C-h>", "<C-w>h", { noremap = true, silent = true })
226map("n", "<C-j>", "<C-w>j", { noremap = true, silent = true })
227map("n", "<C-k>", "<C-w>k", { noremap = true, silent = true })
228map("n", "<C-l>", "<C-w>l", { noremap = true, silent = true })
229
230-- Ctrl + W Close The Window
231-- map('n', '<C-w>', ':bd!<CR>', { noremap = true, silent = true })
232-- map('i', '<C-w>', ':bd!<CR>', { noremap = true, silent = true })
233
234-- split
235map("n", "<leader>wv", ":vsplit<CR>", { noremap = true })
236map("n", "<leader>ws", ":split<CR>", { noremap = true })
237
238-- Other
239map("n", "<Leader>l", ":noh<CR>", { noremap = true }) -- Clear highlights
240map("n", "<leader>o", ":pu =''<CR>", { noremap = true }) -- Insert a newline and back to normal mode
241map("n", "<leader>O", ":pu! =''<CR>", { noremap = true }) -- Insert a newline and back to normal mode
242
243-- Auto close parenthesis
244local autopairs = {
245 ["("] = ")",
246 ["["] = "]",
247 ["{"] = "}",
248 ["<"] = ">",
249}
250for open, close in pairs(autopairs) do
251 vim.keymap.set("i", open, function()
252 return open .. close .. "<Left>"
253 end, { expr = true, noremap = true })
254end
255
256-- ============================================================================
257-- PLUGINS
258-- ============================================================================
259local plugins = {
260 -- ========== Color Scheme ==========
261 {
262 "ellisonleao/gruvbox.nvim",
263 priority = 1000,
264 lazy = false,
265 enabled = true,
266 config = function()
267 require("gruvbox").setup({
268 transparent_mode = false,
269 -- overrides = {
270 -- NvimTreeNormal = {bg = '#32302f'},
271 -- NvimTreeEndOfBuffer = {fg = '#32302f'},
272 -- EndOfBuffer = {fg = "#32302f"},
273 -- NonText = {fg = "#5a524c"}
274 -- }
275 })
276 vim.cmd([[colorscheme gruvbox]])
277 vim.o.background = "dark" -- or "light" for light mode
278 end,
279 },
280 -- ========== Completion ==========
281 {
282 "L3MON4D3/LuaSnip",
283 -- follow latest release.
284 version = "v2.*", -- Replace <CurrentMajor> by the latest released major (first number of latest release)
285 -- install jsregexp (optional!).
286 build = "make install_jsregexp",
287 dependencies = { "rafamadriz/friendly-snippets" },
288 config = function()
289 require("luasnip.loaders.from_vscode").lazy_load()
290 require("snippets")
291 end,
292 },
293 {
294 "hrsh7th/nvim-cmp",
295 dependencies = {
296 "hrsh7th/cmp-nvim-lsp",
297 "hrsh7th/cmp-buffer",
298 -- 'hrsh7th/cmp-path',
299 "https://codeberg.org/FelipeLema/cmp-async-path",
300 "hrsh7th/cmp-cmdline",
301 "L3MON4D3/LuaSnip",
302 "saadparwaiz1/cmp_luasnip",
303 },
304 event = { "LspAttach", "InsertCharPre" },
305 config = function()
306 local cmp = require("cmp")
307 cmp.setup({
308 snippet = {
309 expand = function(args) require("luasnip").lsp_expand(args.body) end,
310 },
311 mapping = cmp.mapping.preset.insert({
312 ["<Tab>"] = cmp.mapping(function(fallback)
313 if cmp.visible() then
314 cmp.select_next_item()
315 else
316 fallback()
317 end
318 end, { "i", "s" }),
319 ["<S-Tab>"] = cmp.mapping(function(fallback)
320 if cmp.visible() then
321 cmp.select_prev_item()
322 else
323 fallback()
324 end
325 end, { "i", "s" }),
326 ["<C-;>"] = cmp.mapping.abort(),
327 ["<C-k>"] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
328
329 ["<C-g>"] = cmp.mapping(function()
330 if not cmp.visible() then cmp.complete() end
331 cmp.abort()
332 end, { "i", "s" }),
333 ["<c-y>"] = cmp.mapping(function(fallback)
334 if not cmp.visible() then fallback() end
335 cmp.scroll_docs(-1)
336 end, { "i", "s" }),
337 ["<c-e>"] = cmp.mapping(function(fallback)
338 if not cmp.visible() then fallback() end
339 cmp.scroll_docs(1)
340 end, { "i", "s" }),
341 ["<c-p>"] = cmp.mapping(function(fallback)
342 if not cmp.visible() then fallback() end
343 cmp.select_prev_item()
344 end, { "i", "s" }),
345 ["<c-n>"] = cmp.mapping(function(fallback)
346 if not cmp.visible() then fallback() end
347 cmp.select_next_item()
348 end, { "i", "s" }),
349 ["<cr>"] = cmp.mapping(function(fallback)
350 if cmp.get_selected_entry() == nil then fallback() end
351 cmp.confirm()
352 end, { "i", "s" }),
353 }),
354
355 sources = cmp.config.sources({
356 { name = "async_path", max_item_count = 20 },
357 {
358 name = "nvim_lsp",
359 max_item_count = 80,
360 },
361 {
362 name = "buffer",
363 max_item_count = 20,
364 option = {
365 get_bufnrs = function()
366 return vim.tbl_map(function(win) return vim.api.nvim_win_get_buf(win) end, vim.api.nvim_list_wins())
367 end,
368 },
369 },
370 { name = "luasnip" }, -- For luasnip users.
371 -- { name = 'ultisnips' }, -- For ultisnips users.
372 -- { name = 'snippy' }, -- For snippy users.
373 -- { name = 'crates' },
374 }),
375 -- eof
376 })
377
378 -- Set configuration for specific filetype.
379 cmp.setup.filetype("gitcommit", {
380 sources = cmp.config.sources({
381 { name = "git" }, -- You can specify the `git` source if [you were installed it](https://github.com/petertriho/cmp-git).
382 }, {
383 { name = "buffer" },
384 }),
385 })
386
387 -- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore).
388 cmp.setup.cmdline({ "/", "?" }, {
389 mapping = cmp.mapping.preset.cmdline(),
390 sources = {
391 { name = "buffer" },
392 },
393 })
394
395 -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
396 cmp.setup.cmdline(":", {
397 mapping = cmp.mapping.preset.cmdline(),
398 sources = cmp.config.sources({
399 { name = "path" },
400 }, {
401 { name = "cmdline" },
402 }),
403 matching = { disallow_symbol_nonprefix_matching = false },
404 })
405 end,
406 },
407
408 -- ========== UI & Navigation ==========
409 {
410 "nvim-lualine/lualine.nvim",
411 opts = {
412 options = {
413 icons_enabled = true,
414 theme = "gruvbox-material",
415 component_separators = { left = "|", right = "|" },
416 section_separators = { left = "", right = "" },
417 disabled_filetypes = { "NvimTree", "packer" },
418 always_divide_middle = true,
419 globalstatuses = true,
420 },
421 sections = {
422 lualine_a = { "mode" },
423 lualine_b = {
424 "branch",
425 "diff",
426 {
427 "diagnostics",
428 sources = { "nvim_lsp", "coc" },
429 update_in_insert = true,
430 always_visible = true,
431 },
432 },
433 lualine_c = { { "filename", file_status = true, path = 1 } },
434 lualine_x = { "filetype" },
435 lualine_y = { "%p%%", "location" },
436 lualine_z = {},
437 },
438 inactive_sections = {
439 lualine_a = {},
440 lualine_b = {},
441 lualine_c = { { "filename", file_status = true, path = 1 } },
442 lualine_x = { "filetype" },
443 lualine_y = { "%p%%", "location" },
444 lualine_z = {},
445 },
446 tabline = {},
447 extensions = { "fugitive", "fzf", "nvim-tree" },
448 },
449 },
450 -- Neovim file explorer: edit your filesystem like a buffer
451 {
452 "stevearc/oil.nvim",
453 opts = {},
454 dependencies = { { "echasnovski/mini.icons", opts = {} } },
455 config = function()
456 require("oil").setup({
457 default_file_explorer = true,
458 view_options = {
459 show_hidden = true,
460 },
461 keymaps = {
462 ["g?"] = "actions.show_help",
463 ["<CR>"] = "actions.select",
464 ["<C-s>"] = {
465 "actions.select",
466 opts = { vertical = true },
467 desc = "Open the entry in a vertical split",
468 },
469 ["<C-h>"] = {
470 "actions.select",
471 opts = { horizontal = true },
472 desc = "Open the entry in a horizontal split",
473 },
474 ["<C-t>"] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
475 ["<C-p>"] = "actions.preview",
476 ["q"] = "actions.close",
477 ["<C-l>"] = "actions.refresh",
478 ["-"] = "actions.parent",
479 ["_"] = "actions.open_cwd",
480 ["`"] = "actions.cd",
481 ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" },
482 ["gs"] = "actions.change_sort",
483 ["gx"] = "actions.open_external",
484 ["g."] = "actions.toggle_hidden",
485 ["g\\"] = "actions.toggle_trash",
486 },
487 float = {
488 padding = 3,
489 border = "rounded",
490 },
491 })
492 end,
493 },
494 --
495 {
496 "nvim-telescope/telescope.nvim",
497 lazy = false,
498 keys = {
499 {
500 "<Leader>ff",
501 ":lua require('telescope.builtin').find_files()<CR>",
502 { noremap = true, silent = true },
503 desc = "Find Files",
504 },
505 {
506 "<Leader>bb",
507 ":lua require('telescope.builtin').buffers()<CR>",
508 { noremap = true, silent = true },
509 desc = "Buffers",
510 },
511 {
512 "<Leader>g",
513 ":lua require('telescope.builtin').live_grep()<CR>",
514 { noremap = true, silent = true },
515 desc = "Live Grep",
516 },
517 {
518 "<Leader>h",
519 ":lua require('telescope.builtin').help_tags()<CR>",
520 { noremap = true, silent = true },
521 desc = "Help Tags",
522 },
523 {
524 "<Leader>ss",
525 ":lua require('telescope.builtin').spell_suggest()<CR>",
526 { noremap = true, silent = true },
527 desc = "Spell Suggest",
528 },
529 {
530 "<Leader>k",
531 ":lua require('telescope.builtin').keymaps()<CR>",
532 { noremap = true, silent = true },
533 desc = "Show Key Maps",
534 },
535 {
536 "<Leader>vo",
537 ":lua require('telescope.builtin').vim_options()<CR>",
538 { noremap = true, silent = true },
539 desc = "Neovim Options",
540 },
541 {
542 "<Leader>D",
543 ":lua require('telescope.builtin').diagnostics()<CR>",
544 { noremap = true, silent = true },
545 desc = "Diagnostics",
546 },
547 {
548 "<Leader>cs",
549 ":lua require('telescope.builtin').git_status()<CR>",
550 { noremap = true, silent = true },
551 desc = "Git Status",
552 },
553 },
554 dependencies = {
555 { "nvim-lua/plenary.nvim" },
556 { "nvim-telescope/telescope-fzf-native.nvim" },
557 -- ripgrep needs to be installed on the system 'pacman -S ripgrep'
558 },
559 config = function()
560 local actions = require("telescope.actions")
561 require("telescope").setup({
562 defaults = { mappings = { i = { ["<esc>"] = actions.close } } },
563 extensions = {
564 fzf = {
565 fuzzy = true,
566 override_generic_sorter = true,
567 override_file_sorter = true,
568 case_mode = "smart_case",
569 },
570 },
571 })
572 end,
573 },
574 -- ========== Linting ==========
575 -- Lightweight yet powerful formatter plugin for Neovim
576 {
577 "stevearc/conform.nvim",
578 lazy = true,
579 -- event = { "BufWritePre" },
580 cmd = { "ConformInfo" },
581 keys = {
582 {
583 "<leader>cf",
584 function() require("conform").format({ async = true }) end,
585 mode = "",
586 desc = "Format buffer",
587 },
588 },
589 opts = {
590 -- Map of filetype to formatters
591 formatters_by_ft = {
592 lua = { "stylua" },
593 -- Conform will run multiple formatters sequentially
594 go = { "goimports", "gofmt" },
595 -- You can also customize some of the format options for the filetype
596 rust = { "rustfmt", lsp_format = "fallback" },
597 -- You can use a function here to determine the formatters dynamically
598 python = function(bufnr)
599 if require("conform").get_formatter_info("ruff_format", bufnr).available then
600 return { "ruff_format" }
601 else
602 return { "isort", "black" }
603 end
604 end,
605 nix = { "nixfmt" },
606 ["markdown"] = { "prettier", "markdownlint-cli2", "markdown-toc" },
607 ["markdown.mdx"] = { "prettier", "markdownlint-cli2", "markdown-toc" },
608 -- Use the "*" filetype to run formatters on all filetypes.
609 -- ["*"] = { "codespell" },
610 -- Use the "_" filetype to run formatters on filetypes that don't
611 -- have other format
612 ["_"] = { "trim_whitespace" },
613 },
614 -- Set this to change the default values when calling conform.format()
615 -- This will also affect the default values for format_on_save/format_after_save
616 default_format_opts = { lsp_format = "fallback" },
617 -- If this is set, Conform will run the formatter asynchronously after save.
618 -- It will pass the table to conform.format().
619 -- This can also be a function that returns the table.
620 -- format_after_save = { lsp_format = "fallback" },
621 -- Set the log level. Use `:ConformInfo` to see the location of the log file.
622 log_level = vim.log.levels.ERROR,
623 -- Conform will notify you when a formatter errors
624 notify_on_error = true,
625 -- Conform will notify you when no formatters are available for the buffer
626 notify_no_formatters = true,
627 -- Custom formatters and overrides for built-in formatters
628 formatters = {
629 stylua = {
630 prepend_args = {
631 "--call-parentheses",
632 "Always",
633 "--collapse-simple-statement",
634 "Always",
635 "--indent-type",
636 "Tabs",
637 "--indent-width",
638 "2",
639 "--sort-requires",
640 },
641 },
642 ["markdown-toc"] = {
643 condition = function(_, ctx)
644 for _, line in ipairs(vim.api.nvim_buf_get_lines(ctx.buf, 0, -1, false)) do
645 if line:find("<!%-%- toc %-%->") then return true end
646 end
647 end,
648 },
649 ["markdownlint-cli2"] = {
650 condition = function(_, ctx)
651 local diag = vim.tbl_filter(function(d) return d.source == "markdownlint" end, vim.diagnostic.get(ctx.buf))
652 return #diag > 0
653 end,
654 },
655 },
656 },
657 },
658
659 -- ========== Navigation & Motion ==========
660 {
661 "ggandor/leap.nvim",
662 keys = {
663 { "s", mode = { "n", "x", "o" }, desc = "Leap forward to" },
664 { "S", mode = { "n", "x", "o" }, desc = "Leap backward to" },
665 { "gs", mode = { "n", "x", "o" }, desc = "Leap from windows" },
666 },
667 config = function(_, opts)
668 local leap = require("leap")
669 for k, v in pairs(opts) do
670 leap.opts[k] = v
671 end
672 leap.add_default_mappings(true)
673 vim.keymap.del({ "x", "o" }, "x")
674 vim.keymap.del({ "x", "o" }, "X")
675 end,
676 dependencies = { "tpope/vim-repeat" },
677 },
678
679 {
680 "ggandor/flit.nvim",
681 keys = function()
682 local ret = {}
683 for _, key in ipairs({ "f", "F", "t", "T" }) do
684 ret[#ret + 1] = { key, mode = { "n", "x", "o" }, desc = key }
685 end
686 return ret
687 end,
688 opts = { labeled_modes = "nx" },
689 dependencies = { "ggandor/leap.nvim" },
690 },
691
692 -- ========== Git & Tools ==========
693 {
694 "lewis6991/gitsigns.nvim",
695 lazy = false,
696 init = function()
697 require("gitsigns").setup({
698 attach_to_untracked = false,
699 current_line_blame = true, -- Toggle with `:Gitsigns toggle_current_line_blame`
700 current_line_blame_opts = {
701 virt_text = true,
702 virt_text_pos = "eol", -- 'eol' | 'overlay' | 'right_align'
703 delay = 200,
704 ignore_whitespace = false,
705 virt_text_priority = 100,
706 },
707 current_line_blame_formatter = "<author>, <author_time:%Y-%m-%d> - <summary>",
708 -- current_line_blame_formatter_opts = {
709 -- relative_time = false,
710 -- },
711 })
712 end,
713},
714 -- Git source for nvim-cmp
715 {
716 "petertriho/cmp-git",
717 dependencies = {
718 "nvim-lua/plenary.nvim",
719 },
720 init = function() table.insert(require("cmp").get_config().sources, { name = "git" }) end,
721 config = function()
722 local format = require("cmp_git.format")
723 local sort = require("cmp_git.sort")
724
725 require("cmp_git").setup({
726 -- defaults
727 filetypes = { "gitcommit", "octo" },
728 remotes = { "upstream", "origin", "github", "gitlab", "codeberg" }, -- in order of most to least prioritized
729 enableRemoteUrlRewrites = false, -- enable git url rewrites, see https://git-scm.com/docs/git-config#Documentation/git-config.txt-urlltbasegtinsteadOf
730 git = {
731 commits = {
732 limit = 100,
733 sort_by = sort.git.commits,
734 format = format.git.commits,
735 },
736 },
737 github = {
738 hosts = {}, -- list of private instances of github
739 issues = {
740 fields = { "title", "number", "body", "updatedAt", "state" },
741 filter = "all", -- assigned, created, mentioned, subscribed, all, repos
742 limit = 100,
743 state = "open", -- open, closed, all
744 sort_by = sort.github.issues,
745 format = format.github.issues,
746 },
747 mentions = {
748 limit = 100,
749 sort_by = sort.github.mentions,
750 format = format.github.mentions,
751 },
752 pull_requests = {
753 fields = { "title", "number", "body", "updatedAt", "state" },
754 limit = 100,
755 state = "open", -- open, closed, merged, all
756 sort_by = sort.github.pull_requests,
757 format = format.github.pull_requests,
758 },
759 },
760 gitlab = {
761 hosts = {}, -- list of private instances of gitlab
762 issues = {
763 limit = 100,
764 state = "opened", -- opened, closed, all
765 sort_by = sort.gitlab.issues,
766 format = format.gitlab.issues,
767 },
768 mentions = {
769 limit = 100,
770 sort_by = sort.gitlab.mentions,
771 format = format.gitlab.mentions,
772 },
773 merge_requests = {
774 limit = 100,
775 state = "opened", -- opened, closed, locked, merged
776 sort_by = sort.gitlab.merge_requests,
777 format = format.gitlab.merge_requests,
778 },
779 },
780 trigger_actions = {
781 {
782 debug_name = "git_commits",
783 trigger_character = ":",
784 action = function(sources, trigger_char, callback, params, git_info)
785 return sources.git:get_commits(callback, params, trigger_char)
786 end,
787 },
788 {
789 debug_name = "gitlab_issues",
790 trigger_character = "#",
791 action = function(sources, trigger_char, callback, params, git_info)
792 return sources.gitlab:get_issues(callback, git_info, trigger_char)
793 end,
794 },
795 {
796 debug_name = "gitlab_mentions",
797 trigger_character = "@",
798 action = function(sources, trigger_char, callback, params, git_info)
799 return sources.gitlab:get_mentions(callback, git_info, trigger_char)
800 end,
801 },
802 {
803 debug_name = "gitlab_mrs",
804 trigger_character = "!",
805 action = function(sources, trigger_char, callback, params, git_info)
806 return sources.gitlab:get_merge_requests(callback, git_info, trigger_char)
807 end,
808 },
809 {
810 debug_name = "github_issues_and_pr",
811 trigger_character = "#",
812 action = function(sources, trigger_char, callback, params, git_info)
813 return sources.github:get_issues_and_prs(callback, git_info, trigger_char)
814 end,
815 },
816 {
817 debug_name = "github_mentions",
818 trigger_character = "@",
819 action = function(sources, trigger_char, callback, params, git_info)
820 return sources.github:get_mentions(callback, git_info, trigger_char)
821 end,
822 },
823 },
824 })
825 end,
826},
827 -- ========== Language Support ==========
828 {
829 "NoahTheDuke/vim-just",
830 ft = { "just" },
831 },
832
833 -- ========== TreeSitter ==========
834 {
835 "nvim-treesitter/nvim-treesitter",
836 version = false, -- last release is way too old and doesn't work on Windows
837 build = ":TSUpdate",
838 event = { "VeryLazy" },
839 init = function(plugin)
840 -- PERF: add nvim-treesitter queries to the rtp and it's custom query predicates early
841 -- This is needed because a bunch of plugins no longer `require("nvim-treesitter")`, which
842 -- no longer trigger the **nvim-treesitter** module to be loaded in time.
843 -- Luckily, the only things that those plugins need are the custom queries, which we make available
844 -- during startup.
845 require("lazy.core.loader").add_to_rtp(plugin)
846 require("nvim-treesitter.query_predicates")
847 end,
848 dependencies = {
849 {
850 "nvim-treesitter/nvim-treesitter-textobjects",
851 config = function()
852 -- When in diff mode, we want to use the default
853 -- vim text objects c & C instead of the treesitter ones.
854 local move = require("nvim-treesitter.textobjects.move") ---@type table<string,fun(...)>
855 local configs = require("nvim-treesitter.configs")
856 for name, fn in pairs(move) do
857 if name:find("goto") == 1 then
858 move[name] = function(q, ...)
859 if vim.wo.diff then
860 local config = configs.get_module("textobjects.move")[name] ---@type table<string,string>
861 for key, query in pairs(config or {}) do
862 if q == query and key:find("[%]%[][cC]") then
863 vim.cmd("normal! " .. key)
864 return
865 end
866 end
867 end
868 return fn(q, ...)
869 end
870 end
871 end
872 end,
873 },
874 },
875 cmd = { "TSUpdateSync", "TSUpdate", "TSInstall" },
876 keys = {
877 { "<c-space>", desc = "Increment Selection" },
878 { "<bs>", desc = "Decrement Selection", mode = "x" },
879 },
880 opts = {
881 highlight = { enable = true },
882 indent = { enable = true },
883 ensure_installed = {
884 "rust", "bash", "c", "diff", "html", "javascript", "jsdoc", "json", "jsonc", "lua", "luadoc", "luap",
885 "markdown", "markdown_inline", "nim", "python", "query", "regex", "toml", "tsx", "typescript", "vim",
886 "nasm", "vimdoc", "xml", "yaml", "hurl", "nix", "zig", "asm", "jq", "just", "latex", "go", "gleam", "gitcommit",
887 "gitignore", "dockerfile", "go", "kdl", "kotlin", "java", "make", "pony", "po", "printf", "sql", "typst", "http",
888 "graphql", "earthfile", "editorconfig", "ssh_config", "tmux", "glsl", "hlsl", "ini", "udev", "llvm", "ispc", "inko",
889 "disassembly", "diff", "haskell", "corn", "odin",
890 },
891 incremental_selection = {
892 enable = true,
893 keymaps = {
894 init_selection = "<C-space>",
895 node_incremental = "<C-space>",
896 scope_incremental = false,
897 node_decremental = "<bs>",
898 },
899 },
900 textobjects = {
901 move = {
902 enable = true,
903 goto_next_start = { ["]f"] = "@function.outer", ["]c"] = "@class.outer" }, goto_next_end = { ["]F"] = "@function.outer", ["]C"] = "@class.outer" },
904 goto_previous_start = {
905 ["[f"] = "@function.outer",
906 ["[c"] = "@class.outer",
907 },
908 goto_previous_end = {
909 ["[F"] = "@function.outer",
910 ["[C"] = "@class.outer",
911 },
912 },
913 },
914 },
915 config = function(_, opts)
916 if type(opts.ensure_installed) == "table" then
917 ---@type table<string, boolean>
918 local added = {}
919 opts.ensure_installed = vim.tbl_filter(function(lang)
920 if added[lang] then return false end
921 added[lang] = true
922 return true
923 end, opts.ensure_installed)
924 end
925 require("nvim-treesitter.configs").setup(opts)
926 end,
927 },
928
929 -- ========== Cosmetics & Utilities ==========
930 {
931 "numToStr/Comment.nvim",
932 init = function() require("ts_context_commentstring").setup({}) end,
933 config = function()
934 require("Comment").setup({
935 pre_hook = require("ts_context_commentstring.integrations.comment_nvim").create_pre_hook(),
936 })
937 end,
938 lazy = true,
939 event = "VeryLazy",
940 keys = {
941 {
942 "<C-_>",
943 ":lua require('Comment.api').toggle.linewise.current()<CR>",
944 { noremap = true, silent = true },
945 desc = "",
946 mode = "n",
947 },
948 {
949 "<C-_>",
950 '<ESC><CMD>lua require("Comment.api").toggle.linewise(vim.fn.visualmode())<CR>',
951 { noremap = true, silent = true },
952 desc = "",
953 mode = "x",
954 },
955 {
956 "<C-_>",
957 ":lua require('Comment.api').toggle.linewise.current() <CR>",
958 { noremap = true, silent = true },
959 desc = "",
960 mode = "i",
961 }, -- same as above but fixes some weird issues
962 {
963 "<C-/>",
964 ":lua require('Comment.api').toggle.linewise.current()<CR>",
965 { noremap = true, silent = true },
966 desc = "",
967 mode = "n",
968 },
969 {
970 "<C-/>",
971 '<ESC><CMD>lua require("Comment.api").toggle.linewise(vim.fn.visualmode())<CR>',
972 { noremap = true, silent = true },
973 desc = "",
974 mode = "x",
975 },
976 {
977 "<C-/>",
978 ":lua require('Comment.api').toggle.linewise.current() <CR>",
979 { noremap = true, silent = true },
980 desc = "",
981 mode = "i",
982 },
983 },
984 dependencies = { "JoosepAlviste/nvim-ts-context-commentstring" },
985 },
986 {
987 "folke/todo-comments.nvim",
988 cmd = { "TodoTrouble", "TodoTelescope" },
989 lazy = false,
990 opts = {
991 signs = true, -- show icons in the signs column
992 sign_priority = 8, -- sign priority
993 -- keywords recognized as todo comments
994 keywords = {
995 FIX = {
996 icon = " ", -- icon used for the sign, and in search results
997 color = "error", -- can be a hex color, or a named color (see below)
998 alt = { "FIXME", "BUG", "FIXIT", "ISSUE" }, -- a set of other keywords that all map to this FIX keywords
999 -- signs = false, -- configure signs for some keywords individually
1000 },
1001 TODO = { icon = " ", color = "info" },
1002 HACK = { icon = " ", color = "warning" },
1003 WARN = { icon = " ", color = "warning", alt = { "WARNING", "XXX" } },
1004 PERF = { icon = " ", alt = { "OPTIM", "PERFORMANCE", "OPTIMIZE" } },
1005 NOTE = { icon = " ", color = "hint", alt = { "INFO" } },
1006 TEST = { icon = "⏲ ", color = "test", alt = { "TESTING", "PASSED", "FAILED" } },
1007 IMPORTANT = { icon = " ", color = "default", alt = { "IMPORT" } },
1008 },
1009 gui_style = {
1010 fg = "NONE", -- The gui style to use for the fg highlight group.
1011 bg = "BOLD", -- The gui style to use for the bg highlight group.
1012 },
1013 merge_keywords = true, -- when true, custom keywords will be merged with the defaults
1014 -- highlighting of the line containing the todo comment
1015 -- * before: highlights before the keyword (typically comment characters)
1016 -- * keyword: highlights of the keyword
1017 -- * after: highlights after the keyword (todo text)
1018 highlight = {
1019 multiline = true, -- enable multine todo comments
1020 multiline_pattern = "^.", -- lua pattern to match the next multiline from the start of the matched keyword
1021 multiline_context = 5, -- extra lines that will be re-evaluated when changing a line
1022 before = "", -- "fg" or "bg" or empty
1023 keyword = "wide", -- "fg", "bg", "wide", "wide_bg", "wide_fg" or empty. (wide and wide_bg is the same as bg, but will also highlight surrounding characters, wide_fg acts accordingly but with fg)
1024 after = "fg", -- "fg" or "bg" or empty
1025 -- INFO: pattern or table of pattern to match
1026 -- INFO (anas): this
1027 pattern = [[.*<(KEYWORDS)\s*(.*)*\s*:]],
1028 -- pattern = [[.*<(KEYWORDS)\s*:]], -- pattern or table of patterns, used for highlighting (vim regex)
1029 comments_only = true, -- uses treesitter to match keywords in comments only
1030 max_line_len = 400, -- ignore lines longer than this
1031 exclude = {}, -- list of file types to exclude highlighting
1032 },
1033 -- list of named colors where we try to extract the guifg from the
1034 -- list of highlight groups or use the hex color if hl not found as a fallback
1035 colors = {
1036 error = { "DiagnosticError", "ErrorMsg", "#DC2626" },
1037 warning = { "DiagnosticWarn", "WarningMsg", "#FBBF24" },
1038 info = { "DiagnosticInfo", "#2563EB" },
1039 hint = { "DiagnosticHint", "#10B981" },
1040 default = { "Identifier", "#7C3AED" },
1041 test = { "Identifier", "#FF00FF" },
1042 },
1043 search = {
1044 command = "rg",
1045 args = {
1046 "--color=never",
1047 "--no-heading",
1048 "--with-filename",
1049 "--line-number",
1050 "--column",
1051 },
1052 -- regex that will be used to match keywords.
1053 -- don't replace the (KEYWORDS) placeholder
1054 pattern = [[\b(KEYWORDS):]], -- ripgrep regex
1055 -- pattern = [[\b(KEYWORDS)\b]], -- match without the extra colon. You'll likely get false positives
1056 },
1057 },
1058 keys = {
1059 { "]t", function() require("todo-comments").jump_next() end, desc = "Next Todo Comment" },
1060 { "[t", function() require("todo-comments").jump_prev() end, desc = "Previous Todo Comment" },
1061 { "<leader>xt", "<cmd>Trouble todo toggle<cr>", desc = "Todo (Trouble)" },
1062 {
1063 "<leader>xT",
1064 "<cmd>Trouble todo toggle filter = {tag = {TODO,FIX,FIXME}}<cr>",
1065 desc = "Todo/Fix/Fixme (Trouble)",
1066 },
1067 { "<leader>st", "<cmd>TodoTelescope<cr>", desc = "Todo" },
1068 { "<leader>sT", "<cmd>TodoTelescope keywords=TODO,FIX,FIXME<cr>", desc = "Todo/Fix/Fixme" },
1069 },
1070 },
1071 -- Multiple cursors plugin for vim/neovim
1072 {
1073 "mg979/vim-visual-multi",
1074 init = function()
1075 vim.g.VM_maps = {
1076 ["Find Under"] = "<C-m>",
1077 ["Find Subword Under"] = "<C-m>",
1078 }
1079 end,
1080 },
1081 {
1082 "ggandor/leap.nvim",
1083 keys = {
1084 { "s", mode = { "n", "x", "o" }, desc = "Leap forward to" },
1085 { "S", mode = { "n", "x", "o" }, desc = "Leap backward to" },
1086 { "gs", mode = { "n", "x", "o" }, desc = "Leap from windows" },
1087 },
1088 config = function(_, opts)
1089 local leap = require("leap")
1090 for k, v in pairs(opts) do
1091 leap.opts[k] = v
1092 end
1093 leap.add_default_mappings(true)
1094 vim.keymap.del({ "x", "o" }, "x")
1095 vim.keymap.del({ "x", "o" }, "X")
1096 end,
1097 dependencies = { "tpope/vim-repeat" },
1098 },
1099 {
1100 "mbbill/undotree",
1101 lazy = true,
1102 keys = {
1103 { "<Leader>u", ":UndotreeToggle<CR>", { noremap = true }, desc = "", mode = "n" },
1104 },
1105 },
1106 {
1107 "folke/which-key.nvim",
1108 event = "VeryLazy",
1109 opts_extend = { "spec" },
1110 opts = {
1111 defaults = {},
1112 spec = {
1113 {
1114 mode = { "n", "v" },
1115 -- Group names based on keys defined in other plugins
1116 { "<leader>b", group = "buffers" }, -- bb
1117 { "<leader>c", group = "code/git" }, -- cf (format), cs (git status)
1118 { "<leader>f", group = "find files" }, -- ff
1119 { "<leader>s", group = "search/spell/todo" }, -- ss, st, sT
1120 { "<leader>v", group = "neovim" }, -- vo (options)
1121 { "<leader>x", group = "trouble" }, -- xt, xT
1122
1123 -- Standalone keys defined in Telescope/Undotree
1124 { "<leader>D", desc = "Diagnostics" },
1125 { "<leader>g", desc = "Live Grep" },
1126 { "<leader>h", desc = "Help Tags" },
1127 { "<leader>k", desc = "Keymaps" },
1128 { "<leader>u", desc = "Undotree Toggle" },
1129
1130 -- Navigation & Text Objects
1131 { "[", group = "prev" }, -- [t, [c, [f
1132 { "]", group = "next" }, -- ]t, ]c, ]f
1133 { "g", group = "goto/action" }, -- gs, gx, g?, g., etc
1134 { "gs", group = "surround/leap" },
1135 { "z", group = "fold" },
1136
1137 -- Window management
1138 {
1139 "<leader>w",
1140 group = "windows",
1141 proxy = "<c-w>",
1142 expand = function() return require("which-key.extras").expand.win() end,
1143 },
1144 { "gx", desc = "Open with system app" },
1145 },
1146 },
1147 },
1148 keys = {
1149 {
1150 "<leader>?",
1151 function() require("which-key").show({ global = false }) end,
1152 desc = "Buffer Keymaps (which-key)",
1153 },
1154 {
1155 "<c-w><space>",
1156 function() require("which-key").show({ keys = "<c-w>", loop = true }) end,
1157 desc = "Window Hydra Mode (which-key)",
1158 },
1159 },
1160 config = function(_, opts)
1161 local wk = require("which-key")
1162 wk.setup(opts)
1163 if not vim.tbl_isempty(opts.defaults) then
1164 vim.notify("which-key: opts.defaults is deprecated. Please use opts.spec instead.", "error")
1165 wk.register(opts.defaults)
1166 end
1167 end,
1168 },
1169 "tpope/vim-fugitive",
1170 "ellisonleao/glow.nvim",
1171}
1172
1173require("lazy").setup(plugins, {
1174 defaults = { lazy = false },
1175})
1176
1177-- ============================================================================
1178-- NEOVIDE CONFIGURATION
1179-- ============================================================================
1180if vim.g.neovide then
1181 vim.o.guifont = "JetBrains_Mono,Noto_Color_Emoji,Noto_Kufi_Arabic: h10"
1182end
1183
1184-- ============================================================================
1185-- AUTOCOMMANDS
1186-- ============================================================================
1187local augroup = vim.api.nvim_create_augroup
1188local autocmd = vim.api. nvim_create_autocmd
1189
1190-- Groff
1191local groff = augroup("groff", {})
1192autocmd("BufWritePost", {
1193 pattern = { "*.ms" },
1194 command = "silent ! pdfroff -ms % -e -t -p > %: r.pdf",
1195 group = groff,
1196})
1197
1198-- Indentation
1199local indention = augroup("indentaion", {})
1200autocmd("FileType", {
1201 pattern = {
1202 "html",
1203 "htmldjango",
1204 "css",
1205 "scss",
1206 "javascript",
1207 "javascriptreact",
1208 "typescript",
1209 "nix",
1210 "typescriptreact",
1211 "lua",
1212 "markdown",
1213 "jinja",
1214 "html. mustache",
1215 "html.handlebars",
1216 "prisma",
1217 },
1218 command = "setlocal tabstop=2 softtabstop=2 shiftwidth=2",
1219 group = indention,
1220})
1221
1222-- Highlight yanked text
1223local highlightYank = augroup("highlightYank", {})
1224autocmd("TextYankPost", {
1225 callback = function() vim.highlight.on_yank({ higroup = "Visual", timeout = 300 }) end,
1226 group = highlightYank,
1227})
1228
1229-- Center Screen On Insert
1230local centerScreen = augroup("centerScreen", {})
1231autocmd("InsertEnter", { pattern = "*", command = "norm zz", group = centerScreen })
1232
1233-- Opens PDF/Media files in PDFVIWER/BROWSER instead of viewing binary
1234local openMediaFiles = augroup("openMediaFiles", {})
1235autocmd("BufReadPost", {
1236 pattern = { "*.pdf" },
1237 callback = function()
1238 local command
1239 if pcall(os.getenv("PDFVIWER")) then
1240 command = os.getenv("PDFVIWER")
1241 else
1242 command = os.getenv("BROWSER")
1243 end
1244 vim.fn.jobstart(string.format("%s '%s'", command, vim.fn.expand("%")), { detach = true })
1245 vim.api.nvim_buf_delete(0, {})
1246 end,
1247 group = openMediaFiles,
1248})
1249
1250autocmd("BufReadPost", {
1251 pattern = { "*.png", "*.webp", "*.jpg", "*. jpeg", "*.mp4" },
1252 callback = function()
1253 local command = os.getenv("BROWSER")
1254 vim.fn.jobstart(string. format("%s '%s'", command, vim.fn.expand("%")), { detach = true })
1255 vim.api.nvim_buf_delete(0, {})
1256 end,
1257 group = openMediaFiles,
1258})
1259
1260-- restore cursor position
1261vim.api.nvim_create_autocmd("BufReadPost", {
1262 callback = function()
1263 local row, col = unpack(vim.api.nvim_buf_get_mark(0, '"'))
1264 if row > 0 and row <= vim.api.nvim_buf_line_count(0) then
1265 vim.api.nvim_win_set_cursor(0, { row, col })
1266 end
1267 end,
1268})
1269
1270-- make sure viminfo/shada is enabled
1271vim.opt.shada = "!,'100,<50,s10,h"
1272
1273-- EOF