4-mappings.lua

   1-- Keybindings (qwerty).
   2
   3-- DESCRIPTION:
   4-- All mappings are defined here.
   5
   6--    Sections:
   7--       ## Base bindings
   8--       -> icons displayed on which-key.nvim
   9--       -> standard operations
  10--       -> clipboard
  11--       -> search highlighting
  12--       -> improved tabulation
  13--       -> improved gg
  14--       -> packages
  15--       -> buffers/tabs                       [buffers]
  16--       -> ui toggles                         [ui]
  17--       -> shifted movement keys
  18--       -> cmdline autocompletion
  19--       -> special cases
  20
  21--       ## Plugin bindings
  22--       -> alpha-nvim
  23--       -> git                                [git]
  24--       -> file browsers
  25--       -> session manager
  26--       -> smart-splits.nvim
  27--       -> aerial.nvim
  28--       -> litee-calltree.nvim
  29--       -> telescope.nivm                     [find]
  30--       -> toggleterm.nvim
  31--       -> dap.nvim                           [debugger]
  32--       -> tests                              [tests]
  33--       -> nvim-ufo
  34--       -> code documentation                 [docs]
  35--       -> ask chatgpt                        [neural]
  36--       -> hop.nvim
  37--       -> mason-lspconfig.nvim               [lsp]
  38
  39--
  40--   KEYBINDINGS REFERENCE
  41--   -------------------------------------------------------------------
  42--   |        Mode  | Norm | Ins | Cmd | Vis | Sel | Opr | Term | Lang |
  43--   Command        +------+-----+-----+-----+-----+-----+------+------+
  44--   [nore]map      | yes  |  -  |  -  | yes | yes | yes |  -   |  -   |
  45--   n[nore]map     | yes  |  -  |  -  |  -  |  -  |  -  |  -   |  -   |
  46--   [nore]map!     |  -   | yes | yes |  -  |  -  |  -  |  -   |  -   |
  47--   i[nore]map     |  -   | yes |  -  |  -  |  -  |  -  |  -   |  -   |
  48--   c[nore]map     |  -   |  -  | yes |  -  |  -  |  -  |  -   |  -   |
  49--   v[nore]map     |  -   |  -  |  -  | yes | yes |  -  |  -   |  -   |
  50--   x[nore]map     |  -   |  -  |  -  | yes |  -  |  -  |  -   |  -   |
  51--   s[nore]map     |  -   |  -  |  -  |  -  | yes |  -  |  -   |  -   |
  52--   o[nore]map     |  -   |  -  |  -  |  -  |  -  | yes |  -   |  -   |
  53--   t[nore]map     |  -   |  -  |  -  |  -  |  -  |  -  | yes  |  -   |
  54--   l[nore]map     |  -   | yes | yes |  -  |  -  |  -  |  -   | yes  |
  55--   -------------------------------------------------------------------
  56
  57local M = {}
  58local utils = require "base.utils"
  59local get_icon = utils.get_icon
  60local is_available = utils.is_available
  61local ui = require "base.utils.ui"
  62local maps = require("base.utils").get_mappings_template()
  63local is_android = vim.fn.isdirectory('/data') == 1 -- true if on android
  64
  65-- -------------------------------------------------------------------------
  66--
  67-- ## Base bindings ########################################################
  68--
  69-- -------------------------------------------------------------------------
  70
  71-- icons displayed on which-key.nvim ---------------------------------------
  72local icons = {
  73  f = { desc = get_icon("Search", 1, true) .. "Find" },
  74  p = { desc = get_icon("Package", 1, true) .. "Packages" },
  75  l = { desc = get_icon("ActiveLSP", 1, true) .. "LSP" },
  76  u = { desc = get_icon("Window", 1, true) .. "UI" },
  77  b = { desc = get_icon("Tab", 1, true) .. "Buffers" },
  78  bs = { desc = get_icon("Sort", 1, true) .. "Sort Buffers" },
  79  c = { desc = get_icon("Run", 1, true) .. "Compiler" },
  80  d = { desc = get_icon("Debugger", 1, true) .. "Debugger" },
  81  tt = { desc = get_icon("Test", 1, true) .. "Test" },
  82  dc = { desc = get_icon("Docs", 1, true) .. "Docs" },
  83  g = { desc = get_icon("Git", 1, true) .. "Git" },
  84  S = { desc = get_icon("Session", 1, true) .. "Session" },
  85  t = { desc = get_icon("Terminal", 1, true) .. "Terminal" },
  86}
  87
  88-- standard Operations -----------------------------------------------------
  89maps.n["j"] =
  90{ "v:count == 0 ? 'gj' : 'j'", expr = true, desc = "Move cursor down" }
  91maps.n["k"] =
  92{ "v:count == 0 ? 'gk' : 'k'", expr = true, desc = "Move cursor up" }
  93maps.n["<leader>w"] = { "<cmd>w<cr>", desc = "Save" }
  94maps.n["<leader>W"] =
  95{ function() vim.cmd "SudaWrite" end, desc = "Save as sudo" }
  96maps.n["<leader>n"] = { "<cmd>enew<cr>", desc = "New file" }
  97maps.n["<Leader>/"] = { "gcc", remap = true, desc = "Toggle comment line" }
  98maps.x["<Leader>/"] = { "gc", remap = true, desc = "Toggle comment" }
  99maps.n["gx"] =
 100{ utils.open_with_program, desc = "Open the file under cursor with a program" }
 101maps.n["<C-s>"] = { "<cmd>w!<cr>", desc = "Force write" }
 102maps.n["|"] = { "<cmd>vsplit<cr>", desc = "Vertical Split" }
 103maps.n["\\"] = { "<cmd>split<cr>", desc = "Horizontal Split" }
 104maps.i["<C-BS>"] = { "<C-W>", desc = "Enable CTRL+backsace to delete." }
 105maps.n["0"] =
 106{ "^", desc = "Go to the fist character of the line (aliases 0 to ^)" }
 107maps.n["<leader>q"] = { "<cmd>confirm q<cr>", desc = "Quit" }
 108maps.n["<leader>q"] = {
 109  function()
 110    -- Ask user for confirmation
 111    local choice = vim.fn.confirm("Do you really want to exit nvim?", "&Yes\n&No", 2)
 112    if choice == 1 then
 113      -- If user confirms, but there are still files to be saved: Ask
 114      vim.cmd('confirm quit')
 115    end
 116  end,
 117  desc = "Quit",
 118}
 119maps.n["<Tab>"] = {
 120  "<Tab>",
 121  noremap = true,
 122  silent = true,
 123  expr = false,
 124  desc = "FIX: Prevent TAB from behaving like <C-i>, as they share the same internal code",
 125}
 126
 127-- clipboard ---------------------------------------------------------------
 128
 129-- BUG: We disable these mappings on termux by default because <C-y>
 130--      is the keycode for scrolling, and remapping it would break it.
 131if not is_android then
 132  -- only useful when the option clipboard is commented on ./1-options.lua
 133  maps.n["<C-y>"] = { '"+y<esc>', desc = "Copy to cliboard" }
 134  maps.x["<C-y>"] = { '"+y<esc>', desc = "Copy to cliboard" }
 135  maps.n["<C-d>"] = { '"+y<esc>dd', desc = "Copy to clipboard and delete line" }
 136  maps.x["<C-d>"] = { '"+y<esc>dd', desc = "Copy to clipboard and delete line" }
 137  maps.n["<C-p>"] = { '"+p<esc>', desc = "Paste from clipboard" }
 138end
 139
 140-- Make 'c' key not copy to clipboard when changing a character.
 141maps.n["c"] = { '"_c', desc = "Change without yanking" }
 142maps.n["C"] = { '"_C', desc = "Change without yanking" }
 143maps.x["c"] = { '"_c', desc = "Change without yanking" }
 144maps.x["C"] = { '"_C', desc = "Change without yanking" }
 145
 146-- Make 'x' key not copy to clipboard when deleting a character.
 147maps.n["x"] = {
 148  -- Also let's allow 'x' key to delete blank lines in normal mode.
 149  function()
 150    if vim.fn.col "." == 1 then
 151      local line = vim.fn.getline "."
 152      if line:match "^%s*$" then
 153        vim.api.nvim_feedkeys('"_dd', "n", false)
 154        vim.api.nvim_feedkeys("$", "n", false)
 155      else
 156        vim.api.nvim_feedkeys('"_x', "n", false)
 157      end
 158    else
 159      vim.api.nvim_feedkeys('"_x', "n", false)
 160    end
 161  end,
 162  desc = "Delete character without yanking it",
 163}
 164maps.x["x"] = { '"_x', desc = "Delete all characters in line" }
 165
 166-- Same for shifted X
 167maps.n["X"] = {
 168  -- Also let's allow 'x' key to delete blank lines in normal mode.
 169  function()
 170    if vim.fn.col "." == 1 then
 171      local line = vim.fn.getline "."
 172      if line:match "^%s*$" then
 173        vim.api.nvim_feedkeys('"_dd', "n", false)
 174        vim.api.nvim_feedkeys("$", "n", false)
 175      else
 176        vim.api.nvim_feedkeys('"_X', "n", false)
 177      end
 178    else
 179      vim.api.nvim_feedkeys('"_X', "n", false)
 180    end
 181  end,
 182  desc = "Delete before character without yanking it",
 183}
 184maps.x["X"] = { '"_X', desc = "Delete all characters in line" }
 185
 186-- Override nvim default behavior so it doesn't auto-yank when pasting on visual mode.
 187maps.x["p"] = { "P", desc = "Paste content you've previourly yanked" }
 188maps.x["P"] = { "p", desc = "Yank what you are going to override, then paste" }
 189
 190-- search highlighting ------------------------------------------------------
 191-- use ESC to clear hlsearch, while preserving its original functionality.
 192--
 193-- TIP: If you prefer,  use <leader>ENTER instead of <ESC>
 194--      to avoid triggering it by accident.
 195maps.n["<ESC>"] = {
 196  function()
 197    if vim.fn.hlexists "Search" then
 198      vim.cmd "nohlsearch"
 199    else
 200      vim.api.nvim_feedkeys(
 201        vim.api.nvim_replace_termcodes("<ESC>", true, true, true),
 202        "n",
 203        true
 204      )
 205    end
 206  end,
 207}
 208
 209-- Improved tabulation ------------------------------------------------------
 210maps.x["<S-Tab>"] = { "<gv", desc = "unindent line" }
 211maps.x["<Tab>"] = { ">gv", desc = "indent line" }
 212maps.x["<"] = { "<gv", desc = "unindent line" }
 213maps.x[">"] = { ">gv", desc = "indent line" }
 214
 215-- improved gg --------------------------------------------------------------
 216maps.n["gg"] = {
 217  function()
 218    vim.g.minianimate_disable = true
 219    if vim.v.count > 0 then
 220      vim.cmd("normal! " .. vim.v.count .. "gg")
 221    else
 222      vim.cmd "normal! gg0"
 223    end
 224    vim.g.minianimate_disable = false
 225  end,
 226  desc = "gg and go to the first position",
 227}
 228maps.n["G"] = {
 229  function()
 230    vim.g.minianimate_disable = true
 231    vim.cmd "normal! G$"
 232    vim.g.minianimate_disable = false
 233  end,
 234  desc = "G and go to the last position",
 235}
 236maps.x["gg"] = {
 237  function()
 238    vim.g.minianimate_disable = true
 239    if vim.v.count > 0 then
 240      vim.cmd("normal! " .. vim.v.count .. "gg")
 241    else
 242      vim.cmd "normal! gg0"
 243    end
 244    vim.g.minianimate_disable = false
 245  end,
 246  desc = "gg and go to the first position (visual)",
 247}
 248maps.x["G"] = {
 249  function()
 250    vim.g.minianimate_disable = true
 251    vim.cmd "normal! G$"
 252    vim.g.minianimate_disable = false
 253  end,
 254  desc = "G and go to the last position (visual)",
 255}
 256maps.n["<C-a>"] = { -- to move to the previous position press ctrl + oo
 257  function()
 258    vim.g.minianimate_disable = true
 259    vim.cmd "normal! gg0vG$"
 260    vim.g.minianimate_disable = false
 261  end,
 262  desc = "Visually select all",
 263}
 264
 265-- packages -----------------------------------------------------------------
 266-- lazy
 267maps.n["<leader>p"] = icons.p
 268maps.n["<leader>pu"] =
 269{ function() require("lazy").check() end, desc = "Lazy open" }
 270maps.n["<leader>pU"] =
 271{ function() require("lazy").update() end, desc = "Lazy update" }
 272
 273-- mason
 274if is_available "mason.nvim" then
 275  maps.n["<leader>pm"] = { "<cmd>Mason<cr>", desc = "Mason open" }
 276  maps.n["<leader>pM"] = { "<cmd>MasonUpdateAll<cr>", desc = "Mason update" }
 277end
 278
 279-- treesitter
 280if is_available "nvim-treesitter" then
 281  maps.n["<leader>pT"] = { "<cmd>TSUpdate<cr>", desc = "Treesitter update" }
 282  maps.n["<leader>pt"] = { "<cmd>TSInstallInfo<cr>", desc = "Treesitter open" }
 283end
 284
 285-- nvim updater
 286maps.n["<leader>pD"] = { "<cmd>DistroUpdate<cr>", desc = "Distro update" }
 287maps.n["<leader>pv"] = { "<cmd>DistroReadVersion<cr>", desc = "Distro version" }
 288maps.n["<leader>pc"] = { "<cmd>DistroReadChangelog<cr>", desc = "Distro changelog" }
 289
 290-- buffers/tabs [buffers ]--------------------------------------------------
 291maps.n["<leader>c"] = { -- Close window and buffer at the same time.
 292  function() require("heirline-components.buffer").wipe() end,
 293  desc = "Wipe buffer",
 294}
 295maps.n["<leader>C"] = { -- Close buffer keeping the window.
 296  function() require("heirline-components.buffer").close() end,
 297  desc = "Close buffer",
 298}
 299-- Close buffer keeping the window → Without confirmation.
 300-- maps.n["<leader>X"] = {
 301--   function() require("heirline-components.buffer").close(0, true) end,
 302--   desc = "Force close buffer",
 303--
 304maps.n["<leader>ba"] = {
 305  function() vim.cmd "wa" end,
 306  desc = "Write all changed buffers",
 307}
 308maps.n["]b"] = {
 309  function()
 310    require("heirline-components.buffer").nav(vim.v.count > 0 and vim.v.count or 1)
 311  end,
 312  desc = "Next buffer",
 313}
 314maps.n["[b"] = {
 315  function()
 316    require("heirline-components.buffer").nav(-(vim.v.count > 0 and vim.v.count or 1))
 317  end,
 318  desc = "Previous buffer",
 319}
 320maps.n[">b"] = {
 321  function()
 322    require("heirline-components.buffer").move(vim.v.count > 0 and vim.v.count or 1)
 323  end,
 324  desc = "Move buffer tab right",
 325}
 326maps.n["<b"] = {
 327  function()
 328    require("heirline-components.buffer").move(-(vim.v.count > 0 and vim.v.count or 1))
 329  end,
 330  desc = "Move buffer tab left",
 331}
 332
 333maps.n["<leader>b"] = icons.b
 334maps.n["<leader>bc"] = {
 335  function() require("heirline-components.buffer").close_all(true) end,
 336  desc = "Close all buffers except current",
 337}
 338maps.n["<leader>bC"] = {
 339  function() require("heirline-components.buffer").close_all() end,
 340  desc = "Close all buffers",
 341}
 342maps.n["<leader>bb"] = {
 343  function()
 344    require("heirline-components.all").heirline.buffer_picker(
 345      function(bufnr) vim.api.nvim_win_set_buf(0, bufnr) end
 346    )
 347  end,
 348  desc = "Select buffer from tabline",
 349}
 350maps.n["<leader>bd"] = {
 351  function()
 352    require("heirline-components.all").heirline.buffer_picker(
 353      function(bufnr) require("heirline-components.buffer").close(bufnr) end
 354    )
 355  end,
 356  desc = "Delete buffer from tabline",
 357}
 358maps.n["<leader>bl"] = {
 359  function() require("heirline-components.buffer").close_left() end,
 360  desc = "Close all buffers to the left",
 361}
 362maps.n["<leader>br"] = {
 363  function() require("heirline-components.buffer").close_right() end,
 364  desc = "Close all buffers to the right",
 365}
 366maps.n["<leader>bs"] = icons.bs
 367maps.n["<leader>bse"] = {
 368  function() require("heirline-components.buffer").sort "extension" end,
 369  desc = "Sort by extension (buffers)",
 370}
 371maps.n["<leader>bsr"] = {
 372  function() require("heirline-components.buffer").sort "unique_path" end,
 373  desc = "Sort by relative path (buffers)",
 374}
 375maps.n["<leader>bsp"] = {
 376  function() require("heirline-components.buffer").sort "full_path" end,
 377  desc = "Sort by full path (buffers)",
 378}
 379maps.n["<leader>bsi"] = {
 380  function() require("heirline-components.buffer").sort "bufnr" end,
 381  desc = "Sort by buffer number (buffers)",
 382}
 383maps.n["<leader>bsm"] = {
 384  function() require("heirline-components.buffer").sort "modified" end,
 385  desc = "Sort by modification (buffers)",
 386}
 387maps.n["<leader>b\\"] = {
 388  function()
 389    require("heirline-components.all").heirline.buffer_picker(function(bufnr)
 390      vim.cmd.split()
 391      vim.api.nvim_win_set_buf(0, bufnr)
 392    end)
 393  end,
 394  desc = "Horizontal split buffer from tabline",
 395}
 396maps.n["<leader>b|"] = {
 397  function()
 398    require("heirline-components.all").heirline.buffer_picker(function(bufnr)
 399      vim.cmd.vsplit()
 400      vim.api.nvim_win_set_buf(0, bufnr)
 401    end)
 402  end,
 403  desc = "Vertical split buffer from tabline",
 404}
 405
 406-- quick movement aliases
 407maps.n["<C-k>"] = {
 408  function()
 409    require("heirline-components.buffer").nav(vim.v.count > 0 and vim.v.count or 1)
 410  end,
 411  desc = "Next buffer",
 412}
 413maps.n["<C-j>"] = {
 414  function()
 415    require("heirline-components.buffer").nav(-(vim.v.count > 0 and vim.v.count or 1))
 416  end,
 417  desc = "Previous buffer",
 418}
 419maps.n["<S-Down>"] = {
 420  function() vim.api.nvim_feedkeys("5j", "n", true) end,
 421  desc = "Fast move down",
 422}
 423maps.n["<S-Up>"] = {
 424  function() vim.api.nvim_feedkeys("5k", "n", true) end,
 425  desc = "Fast move up",
 426}
 427
 428-- tabs
 429maps.n["]t"] = { function() vim.cmd.tabnext() end, desc = "Next tab" }
 430maps.n["[t"] = { function() vim.cmd.tabprevious() end, desc = "Previous tab" }
 431
 432-- zen mode
 433if is_available "zen-mode.nvim" then
 434  maps.n["<leader>uz"] =
 435  { function() ui.toggle_zen_mode() end, desc = "Zen mode" }
 436end
 437
 438-- ui toggles [ui] ---------------------------------------------------------
 439maps.n["<leader>u"] = icons.u
 440if is_available "nvim-autopairs" then
 441  maps.n["<leader>ua"] = { ui.toggle_autopairs, desc = "Autopairs" }
 442end
 443maps.n["<leader>ub"] = { ui.toggle_background, desc = "Background" }
 444if is_available "nvim-cmp" then
 445  maps.n["<leader>uc"] = { ui.toggle_cmp, desc = "Autocompletion" }
 446end
 447if is_available "nvim-colorizer.lua" then
 448  maps.n["<leader>uC"] =
 449  { "<cmd>ColorizerToggle<cr>", desc = "color highlight" }
 450end
 451maps.n["<leader>ud"] = { ui.toggle_diagnostics, desc = "Diagnostics" }
 452maps.n["<leader>uD"] = { ui.set_indent, desc = "Change indent setting" }
 453maps.n["<leader>ug"] = { ui.toggle_signcolumn, desc = "Signcolumn" }
 454maps.n["<leader>ul"] = { ui.toggle_statusline, desc = "Statusline" }
 455maps.n["<leader>un"] = { ui.change_number, desc = "Change line numbering" }
 456maps.n["<leader>uP"] = { ui.toggle_paste, desc = "Paste mode" }
 457maps.n["<leader>us"] = { ui.toggle_spell, desc = "Spellcheck" }
 458maps.n["<leader>uS"] = { ui.toggle_conceal, desc = "Conceal" }
 459maps.n["<leader>ut"] = { ui.toggle_tabline, desc = "Tabline" }
 460maps.n["<leader>uu"] = { ui.toggle_url_effect, desc = "URL highlight" }
 461maps.n["<leader>uw"] = { ui.toggle_wrap, desc = "Wrap" }
 462maps.n["<leader>uy"] = { ui.toggle_buffer_syntax, desc = "Syntax highlight (buffer)" }
 463maps.n["<leader>uh"] = { ui.toggle_foldcolumn, desc = "Foldcolumn" }
 464maps.n["<leader>uN"] =
 465{ ui.toggle_ui_notifications, desc = "UI notifications" }
 466if is_available "lsp_signature.nvim" then
 467  maps.n["<leader>up"] = { ui.toggle_lsp_signature, desc = "LSP signature" }
 468end
 469if is_available "mini.animate" then
 470  maps.n["<leader>uA"] = { ui.toggle_animations, desc = "Animations" }
 471end
 472
 473-- shifted movement keys ----------------------------------------------------
 474maps.n["<S-Down>"] = {
 475  function() vim.api.nvim_feedkeys("7j", "n", true) end,
 476  desc = "Fast move down",
 477}
 478maps.n["<S-Up>"] = {
 479  function() vim.api.nvim_feedkeys("7k", "n", true) end,
 480  desc = "Fast move up",
 481}
 482maps.n["<S-PageDown>"] = {
 483  function()
 484    local current_line = vim.fn.line "."
 485    local total_lines = vim.fn.line "$"
 486    local target_line = current_line + 1 + math.floor(total_lines * 0.20)
 487    if target_line > total_lines then target_line = total_lines end
 488    vim.api.nvim_win_set_cursor(0, { target_line, 0 })
 489    vim.cmd "normal! zz"
 490  end,
 491  desc = "Page down exactly a 20% of the total size of the buffer",
 492}
 493maps.n["<S-PageUp>"] = {
 494  function()
 495    local current_line = vim.fn.line "."
 496    local target_line = current_line - 1 - math.floor(vim.fn.line "$" * 0.20)
 497    if target_line < 1 then target_line = 1 end
 498    vim.api.nvim_win_set_cursor(0, { target_line, 0 })
 499    vim.cmd "normal! zz"
 500  end,
 501  desc = "Page up exactly 20% of the total size of the buffer",
 502}
 503
 504-- cmdline autocompletion ---------------------------------------------------
 505maps.c["<Up>"] = {
 506  function() return vim.fn.wildmenumode() == 1 and "<Left>" or "<Up>" end,
 507  noremap = true,
 508  expr = true,
 509  desc = "Wildmenu fix for neovim bug #9953",
 510}
 511maps.c["<Down>"] = {
 512  function() return vim.fn.wildmenumode() == 1 and "<Right>" or "<Down>" end,
 513  noremap = true,
 514  expr = true,
 515  desc = "Wildmenu fix for neovim bug #9953",
 516}
 517maps.c["<Left>"] = {
 518  function() return vim.fn.wildmenumode() == 1 and "<Up>" or "<Left>" end,
 519  noremap = true,
 520  expr = true,
 521  desc = "Wildmenu fix for neovim bug #9953",
 522}
 523maps.c["<Right>"] = {
 524  function() return vim.fn.wildmenumode() == 1 and "<Down>" or "<Right>" end,
 525  noremap = true,
 526  expr = true,
 527  desc = "Wildmenu fix for neovim bug #9953",
 528}
 529
 530-- special cases ------------------------------------------------------------
 531vim.api.nvim_create_autocmd("BufWinEnter", {
 532  desc = "Make q close help, man, quickfix, dap floats",
 533  callback = function(args)
 534    local buftype =
 535        vim.api.nvim_get_option_value("buftype", { buf = args.buf })
 536    if vim.tbl_contains({ "help", "nofile", "quickfix" }, buftype) then
 537      vim.keymap.set(
 538        "n", "q", "<cmd>close<cr>",
 539        { buffer = args.buf, silent = true, nowait = true }
 540      )
 541    end
 542  end,
 543})
 544vim.api.nvim_create_autocmd("CmdwinEnter", {
 545  desc = "Make q close command history (q: and q?)",
 546  callback = function(args)
 547    vim.keymap.set(
 548      "n", "q", "<cmd>close<cr>",
 549      { buffer = args.buf, silent = true, nowait = true }
 550    )
 551  end,
 552})
 553
 554-- -------------------------------------------------------------------------
 555--
 556-- ## Plugin bindings
 557--
 558-- -------------------------------------------------------------------------
 559
 560-- alpha-nvim --------------------------------------------------------------
 561if is_available "alpha-nvim" then
 562  maps.n["<leader>h"] = {
 563    function()
 564      local wins = vim.api.nvim_tabpage_list_wins(0)
 565      if #wins > 1
 566          and vim.api.nvim_get_option_value("filetype", { win = wins[1] })
 567          == "neo-tree"
 568      then
 569        vim.fn.win_gotoid(wins[2]) -- go to non-neo-tree window to toggle alpha
 570      end
 571      require("alpha").start(false, require("alpha").default_config)
 572      vim.b.miniindentscope_disable = true
 573    end,
 574    desc = "Home screen",
 575  }
 576end
 577
 578-- [git] -----------------------------------------------------------
 579-- gitsigns.nvim
 580maps.n["<leader>g"] = icons.g
 581if is_available "gitsigns.nvim" then
 582  maps.n["<leader>g"] = icons.g
 583  maps.n["]g"] =
 584  { function() require("gitsigns").next_hunk() end, desc = "Next Git hunk" }
 585  maps.n["[g"] = {
 586    function() require("gitsigns").prev_hunk() end,
 587    desc = "Previous Git hunk",
 588  }
 589  maps.n["<leader>gl"] = {
 590    function() require("gitsigns").blame_line() end,
 591    desc = "View Git blame",
 592  }
 593  maps.n["<leader>gL"] = {
 594    function() require("gitsigns").blame_line { full = true } end,
 595    desc = "View full Git blame",
 596  }
 597  maps.n["<leader>gp"] = {
 598    function() require("gitsigns").preview_hunk() end,
 599    desc = "Preview Git hunk",
 600  }
 601  maps.n["<leader>gh"] = {
 602    function() require("gitsigns").reset_hunk() end,
 603    desc = "Reset Git hunk",
 604  }
 605  maps.n["<leader>gr"] = {
 606    function() require("gitsigns").reset_buffer() end,
 607    desc = "Reset Git buffer",
 608  }
 609  maps.n["<leader>gs"] = {
 610    function() require("gitsigns").stage_hunk() end,
 611    desc = "Stage Git hunk",
 612  }
 613  maps.n["<leader>gS"] = {
 614    function() require("gitsigns").stage_buffer() end,
 615    desc = "Stage Git buffer",
 616  }
 617  maps.n["<leader>gu"] = {
 618    function() require("gitsigns").undo_stage_hunk() end,
 619    desc = "Unstage Git hunk",
 620  }
 621  maps.n["<leader>gd"] = {
 622    function() require("gitsigns").diffthis() end,
 623    desc = "View Git diff",
 624  }
 625end
 626-- git fugitive
 627if is_available "vim-fugitive" then
 628  maps.n["<leader>gP"] = {
 629    function() vim.cmd ":GBrowse" end,
 630    desc = "Open in github ",
 631  }
 632end
 633-- git client
 634if vim.fn.executable "lazygit" == 1 then -- if lazygit exists, show it
 635  maps.n["<leader>gg"] = {
 636    function()
 637      local git_dir = vim.fn.finddir(".git", vim.fn.getcwd() .. ";")
 638      if git_dir ~= "" then
 639        vim.cmd "TermExec cmd='lazygit && exit'"
 640      else
 641        utils.notify("Not a git repository", vim.log.levels.WARN)
 642      end
 643    end,
 644    desc = "ToggleTerm lazygit",
 645  }
 646end
 647if vim.fn.executable "gitui" == 1 then -- if gitui exists, show it
 648  maps.n["<leader>gg"] = {
 649    function()
 650      local git_dir = vim.fn.finddir(".git", vim.fn.getcwd() .. ";")
 651      if git_dir ~= "" then
 652        if vim.fn.executable "keychain" == 1 then
 653          vim.cmd 'TermExec cmd="eval `keychain --eval ~/.ssh/github.key` && gitui && exit"'
 654        else
 655          vim.cmd "TermExec cmd='gitui && exit'"
 656        end
 657      else
 658        utils.notify("Not a git repository", vim.log.levels.WARN)
 659      end
 660    end,
 661    desc = "ToggleTerm gitui",
 662  }
 663end
 664
 665-- file browsers ------------------------------------
 666-- ranger
 667if is_available "rnvimr" then
 668  maps.n["<leader>r"] = { "<cmd>RnvimrToggle<cr>", desc = "Ranger" }
 669end
 670
 671-- neotree
 672if is_available "neo-tree.nvim" then
 673  maps.n["<leader>e"] = { "<cmd>Neotree toggle<cr>", desc = "Neotree" }
 674end
 675
 676-- session manager ---------------------------------------------------------
 677if is_available "neovim-session-manager" then
 678  maps.n["<leader>S"] = icons.S
 679  maps.n["<leader>Sl"] = {
 680    "<cmd>SessionManager! load_last_session<cr>",
 681    desc = "Load last session",
 682  }
 683  maps.n["<leader>Ss"] = {
 684    "<cmd>SessionManager! save_current_session<cr>",
 685    desc = "Save this session",
 686  }
 687  maps.n["<leader>Sd"] =
 688  { "<cmd>SessionManager! delete_session<cr>", desc = "Delete session" }
 689  maps.n["<leader>Sf"] =
 690  { "<cmd>SessionManager! load_session<cr>", desc = "Search sessions" }
 691  maps.n["<leader>S."] = {
 692    "<cmd>SessionManager! load_current_dir_session<cr>",
 693    desc = "Load current directory session",
 694  }
 695end
 696if is_available "resession.nvim" then
 697  maps.n["<leader>S"] = icons.S
 698  maps.n["<leader>Sl"] = {
 699    function() require("resession").load "Last Session" end,
 700    desc = "Load last session",
 701  }
 702  maps.n["<leader>Ss"] =
 703  { function() require("resession").save() end, desc = "Save this session" }
 704  maps.n["<leader>St"] = {
 705    function() require("resession").save_tab() end,
 706    desc = "Save this tab's session",
 707  }
 708  maps.n["<leader>Sd"] =
 709  { function() require("resession").delete() end, desc = "Delete a session" }
 710  maps.n["<leader>Sf"] =
 711  { function() require("resession").load() end, desc = "Load a session" }
 712  maps.n["<leader>S."] = {
 713    function()
 714      require("resession").load(vim.fn.getcwd(), { dir = "dirsession" })
 715    end,
 716    desc = "Load current directory session",
 717  }
 718end
 719
 720-- smart-splits.nivm
 721if is_available "smart-splits.nvim" then
 722  maps.n["<C-h>"] = {
 723    function() require("smart-splits").move_cursor_left() end,
 724    desc = "Move to left split",
 725  }
 726  maps.n["<C-j>"] = {
 727    function() require("smart-splits").move_cursor_down() end,
 728    desc = "Move to below split",
 729  }
 730  maps.n["<C-k>"] = {
 731    function() require("smart-splits").move_cursor_up() end,
 732    desc = "Move to above split",
 733  }
 734  maps.n["<C-l>"] = {
 735    function() require("smart-splits").move_cursor_right() end,
 736    desc = "Move to right split",
 737  }
 738  maps.n["<C-Up>"] = {
 739    function() require("smart-splits").resize_up() end,
 740    desc = "Resize split up",
 741  }
 742  maps.n["<C-Down>"] = {
 743    function() require("smart-splits").resize_down() end,
 744    desc = "Resize split down",
 745  }
 746  maps.n["<C-Left>"] = {
 747    function() require("smart-splits").resize_left() end,
 748    desc = "Resize split left",
 749  }
 750  maps.n["<C-Right>"] = {
 751    function() require("smart-splits").resize_right() end,
 752    desc = "Resize split right",
 753  }
 754else
 755  maps.n["<C-h>"] = { "<C-w>h", desc = "Move to left split" }
 756  maps.n["<C-j>"] = { "<C-w>j", desc = "Move to below split" }
 757  maps.n["<C-k>"] = { "<C-w>k", desc = "Move to above split" }
 758  maps.n["<C-l>"] = { "<C-w>l", desc = "Move to right split" }
 759  maps.n["<C-Up>"] = { "<cmd>resize -2<CR>", desc = "Resize split up" }
 760  maps.n["<C-Down>"] = { "<cmd>resize +2<CR>", desc = "Resize split down" }
 761  maps.n["<C-Left>"] =
 762  { "<cmd>vertical resize -2<CR>", desc = "Resize split left" }
 763  maps.n["<C-Right>"] =
 764  { "<cmd>vertical resize +2<CR>", desc = "Resize split right" }
 765end
 766
 767-- aerial.nvimm ------------------------------------------------------------
 768if is_available "aerial.nvim" then
 769  maps.n["<leader>i"] =
 770  { function() require("aerial").toggle() end, desc = "Aerial" }
 771end
 772
 773-- letee-calltree.nvimm ------------------------------------------------------------
 774if is_available "litee-calltree.nvim" then
 775  -- For every buffer, look for the one with filetype "calltree" and focus it.
 776  local calltree_delay = 1500 -- first run? wait a bit longer.
 777  local function focus_calltree()
 778    -- Note: No go to the previous cursor position, press ctrl+i / ctrl+o
 779    vim.defer_fn(function()
 780      for _, win in ipairs(vim.api.nvim_list_wins()) do
 781        local buf = vim.api.nvim_win_get_buf(win)
 782        local ft = vim.api.nvim_get_option_value('filetype', { buf = buf })
 783
 784        if ft == "calltree" then
 785          vim.api.nvim_set_current_win(win)
 786          return true
 787        end
 788      end
 789    end, calltree_delay)
 790    calltree_delay = 100
 791  end
 792  maps.n["gj"] = {
 793    function()
 794      vim.lsp.buf.incoming_calls()
 795      focus_calltree()
 796    end,
 797    desc = "Call tree (incoming)"
 798  }
 799  maps.n["gJ"] =
 800  {
 801    function()
 802      vim.lsp.buf.outgoing_calls()
 803      focus_calltree()
 804    end,
 805    desc = "Call tree (outgoing)"
 806  }
 807end
 808
 809-- telescope.nvim [find] ----------------------------------------------------
 810if is_available "telescope.nvim" then
 811  maps.n["<leader>f"] = icons.f
 812  maps.n["<leader>gb"] = {
 813    function() require("telescope.builtin").git_branches() end,
 814    desc = "Git branches",
 815  }
 816  maps.n["<leader>gc"] = {
 817    function()
 818      require("telescope.builtin").git_commits()
 819    end,
 820    desc = "Git commits (repository)"
 821  }
 822  maps.n["<leader>gC"] = {
 823    function()
 824      require("telescope.builtin").git_bcommits()
 825    end,
 826    desc = "Git commits (current file)"
 827  }
 828  maps.n["<leader>gt"] = {
 829    function() require("telescope.builtin").git_status() end,
 830    desc = "Git status",
 831  }
 832  maps.n["<leader>f<CR>"] = {
 833    function() require("telescope.builtin").resume() end,
 834    desc = "Resume previous search",
 835  }
 836  maps.n["<leader>f'"] = {
 837    function() require("telescope.builtin").marks() end,
 838    desc = "Find marks",
 839  }
 840  maps.n["<leader>fa"] = {
 841    function()
 842      local cwd = vim.fn.stdpath "config" .. "/.."
 843      local search_dirs = { vim.fn.stdpath "config" }
 844      if #search_dirs == 1 then cwd = search_dirs[1] end -- if only one directory, focus cwd
 845      require("telescope.builtin").find_files {
 846        prompt_title = "Config Files",
 847        search_dirs = search_dirs,
 848        cwd = cwd,
 849        follow = true,
 850      } -- call telescope
 851    end,
 852    desc = "Find nvim config files",
 853  }
 854  maps.n["<leader>fB"] = {
 855    function() require("telescope.builtin").buffers() end,
 856    desc = "Find buffers",
 857  }
 858  maps.n["<leader>fw"] = {
 859    function() require("telescope.builtin").grep_string() end,
 860    desc = "Find word under cursor in project",
 861  }
 862  maps.n["<leader>fC"] = {
 863    function() require("telescope.builtin").commands() end,
 864    desc = "Find commands",
 865  }
 866  -- Let's disable this. It is way too imprecise. Use rnvimr instead.
 867  -- maps.n["<leader>ff"] = {
 868  --   function()
 869  --     require("telescope.builtin").find_files { hidden = true, no_ignore = true }
 870  --   end,
 871  --   desc = "Find all files",
 872  -- }
 873  -- maps.n["<leader>fF"] = {
 874  --   function() require("telescope.builtin").find_files() end,
 875  --   desc = "Find files (no hidden)",
 876  -- }
 877  maps.n["<leader>fh"] = {
 878    function() require("telescope.builtin").help_tags() end,
 879    desc = "Find help",
 880  }
 881  maps.n["<leader>fk"] = {
 882    function() require("telescope.builtin").keymaps() end,
 883    desc = "Find keymaps",
 884  }
 885  maps.n["<leader>fm"] = {
 886    function() require("telescope.builtin").man_pages() end,
 887    desc = "Find man",
 888  }
 889  if is_available "nvim-notify" then
 890    maps.n["<leader>fn"] = {
 891      function() require("telescope").extensions.notify.notify() end,
 892      desc = "Find notifications",
 893    }
 894  end
 895  maps.n["<leader>fo"] = {
 896    function() require("telescope.builtin").oldfiles() end,
 897    desc = "Find recent",
 898  }
 899  maps.n["<leader>fv"] = {
 900    function() require("telescope.builtin").registers() end,
 901    desc = "Find vim registers",
 902  }
 903  maps.n["<leader>ft"] = {
 904    function()
 905      -- load color schemes before listing them
 906      pcall(vim.api.nvim_command, "doautocmd User LoadColorSchemes")
 907
 908      -- Open telescope
 909      pcall(require("telescope.builtin").colorscheme, {
 910        enable_preview = true,
 911        ignore_builtins = true
 912      })
 913    end,
 914    desc = "Find themes",
 915  }
 916  maps.n["<leader>ff"] = {
 917    function()
 918      require("telescope.builtin").live_grep {
 919        additional_args = function(args)
 920          return vim.list_extend(args, { "--hidden", "--no-ignore" })
 921        end,
 922      }
 923    end,
 924    desc = "Find words in project",
 925  }
 926  maps.n["<leader>fF"] = {
 927    function() require("telescope.builtin").live_grep() end,
 928    desc = "Find words in project (no hidden)",
 929  }
 930  maps.n["<leader>f/"] = {
 931    function() require("telescope.builtin").current_buffer_fuzzy_find() end,
 932    desc = "Find words in current buffer"
 933  }
 934
 935  -- Some lsp keymappings are here because they depend on telescope
 936  maps.n["<leader>l"] = icons.l
 937  maps.n["<leader>ls"] = {
 938    function()
 939      local aerial_avail, _ = pcall(require, "aerial")
 940      if aerial_avail then
 941        require("telescope").extensions.aerial.aerial()
 942      else
 943        require("telescope.builtin").lsp_document_symbols()
 944      end
 945    end,
 946    desc = "Search symbol in buffer", -- Useful to find every time a variable is assigned.
 947  }
 948  maps.n["gs"] = {
 949    function()
 950      local aerial_avail, _ = pcall(require, "aerial")
 951      if aerial_avail then
 952        require("telescope").extensions.aerial.aerial()
 953      else
 954        require("telescope.builtin").lsp_document_symbols()
 955      end
 956    end,
 957    desc = "Search symbol in buffer", -- Useful to find every time a variable is assigned.
 958  }
 959
 960  -- extra - project.nvim
 961  if is_available "project.nvim" then
 962    maps.n["<leader>fp"] = {
 963      function() vim.cmd "Telescope projects" end,
 964      desc = "Find project",
 965    }
 966  end
 967
 968  -- extra - spectre.nvim (search and replace in project)
 969  if is_available "nvim-spectre" then
 970    maps.n["<leader>fr"] = {
 971      function() require("spectre").toggle() end,
 972      desc = "Find and replace word in project",
 973    }
 974    maps.n["<leader>fb"] = {
 975      function() require("spectre").toggle { path = vim.fn.expand "%:t:p" } end,
 976      desc = "Find and replace word in buffer",
 977    }
 978  end
 979
 980  -- extra - luasnip
 981  if is_available "LuaSnip" and is_available "telescope-luasnip.nvim" then
 982    maps.n["<leader>fs"] = {
 983      function() require("telescope").extensions.luasnip.luasnip {} end,
 984      desc = "Find snippets",
 985    }
 986  end
 987
 988  -- extra - nvim-neoclip (neovim internal clipboard)
 989  --         Specially useful if you disable the shared clipboard in options.
 990  if is_available "nvim-neoclip.lua" then
 991    maps.n["<leader>fy"] = {
 992      function() require("telescope").extensions.neoclip.default() end,
 993      desc = "Find yank history",
 994    }
 995    maps.n["<leader>fq"] = {
 996      function() require("telescope").extensions.macroscope.default() end,
 997      desc = "Find macro history",
 998    }
 999  end
1000
1001  -- extra - undotree
1002  if is_available "telescope-undo.nvim" then
1003    maps.n["<leader>fu"] = {
1004      function() require("telescope").extensions.undo.undo() end,
1005      desc = "Find in undo tree",
1006    }
1007  end
1008
1009  -- extra - compiler
1010  if is_available "compiler.nvim" and is_available "overseer.nvim" then
1011    maps.n["<leader>m"] = icons.c
1012    maps.n["<leader>mm"] = {
1013      function() vim.cmd "CompilerOpen" end,
1014      desc = "Open compiler",
1015    }
1016    maps.n["<leader>mr"] = {
1017      function() vim.cmd "CompilerRedo" end,
1018      desc = "Compiler redo",
1019    }
1020    maps.n["<leader>mt"] = {
1021      function() vim.cmd "CompilerToggleResults" end,
1022      desc = "compiler results",
1023    }
1024    maps.n["<F6>"] = {
1025      function() vim.cmd "CompilerOpen" end,
1026      desc = "Open compiler",
1027    }
1028    maps.n["<S-F6>"] = {
1029      function() vim.cmd "CompilerRedo" end,
1030      desc = "Compiler redo",
1031    }
1032    maps.n["<S-F7>"] = {
1033      function() vim.cmd "CompilerToggleResults" end,
1034      desc = "compiler resume",
1035    }
1036  end
1037end
1038
1039-- toggleterm.nvim ----------------------------------------------------------
1040if is_available "toggleterm.nvim" then
1041  maps.n["<leader>t"] = icons.t
1042  maps.n["<leader>tt"] =
1043  { "<cmd>ToggleTerm direction=float<cr>", desc = "ToggleTerm float" }
1044  maps.n["<leader>th"] = {
1045    "<cmd>ToggleTerm size=10 direction=horizontal<cr>",
1046    desc = "Toggleterm horizontal split",
1047  }
1048  maps.n["<leader>tv"] = {
1049    "<cmd>ToggleTerm size=80 direction=vertical<cr>",
1050    desc = "Toggleterm vertical split",
1051  }
1052  maps.n["<F7>"] = { "<cmd>ToggleTerm<cr>", desc = "terminal" }
1053  maps.t["<F7>"] = maps.n["<F7>"]
1054  maps.n["<C-'>"] = maps.n["<F7>"] -- requires terminal that supports binding <C-'>
1055  maps.t["<C-'>"] = maps.n["<F7>"] -- requires terminal that supports binding <C-'>
1056end
1057
1058-- extra - improved terminal navigation
1059maps.t["<C-h>"] =
1060{ "<cmd>wincmd h<cr>", desc = "Terminal left window navigation" }
1061maps.t["<C-j>"] =
1062{ "<cmd>wincmd j<cr>", desc = "Terminal down window navigation" }
1063maps.t["<C-k>"] =
1064{ "<cmd>wincmd k<cr>", desc = "Terminal up window navigation" }
1065maps.t["<C-l>"] =
1066{ "<cmd>wincmd l<cr>", desc = "Terminal right window navigation" }
1067
1068-- dap.nvim [debugger] -----------------------------------------------------
1069-- Depending your terminal some F keys may not work. To fix it:
1070-- modified function keys found with `showkey -a` in the terminal to get key code
1071-- run `nvim -V3log +quit` and search through the "Terminal info" in the `log` file for the correct keyname
1072if is_available "nvim-dap" then
1073  maps.n["<leader>d"] = icons.d
1074  maps.x["<leader>d"] = icons.d
1075
1076  -- F keys
1077  maps.n["<F5>"] = {
1078    function()
1079      require("dap").continue()
1080    end,
1081    desc = "Debugger: Start"
1082  }
1083  maps.n["<S-F5>"] =
1084  { function() require("dap").terminate() end, desc = "Debugger: Stop" }
1085  maps.n["<C-F5>"] = {
1086    function() require("dap").restart_frame() end, desc = "Debugger: Restart" }
1087  maps.n["<F9>"] = {
1088    function() require("dap").toggle_breakpoint() end, desc = "Debugger: Toggle Breakpoint" }
1089  maps.n["<S-F9>"] = {
1090    function()
1091      vim.ui.input({ prompt = "Condition: " }, function(condition)
1092        if condition then require("dap").set_breakpoint(condition) end
1093      end)
1094    end,
1095    desc = "Debugger: Conditional Breakpoint",
1096  }
1097  maps.n["<F10>"] =
1098  { function() require("dap").step_over() end, desc = "Debugger: Step Over" }
1099  maps.n["<S-F10>"] =
1100  { function() require("dap").step_back() end, desc = "Debugger: Step Back" }
1101  maps.n["<F11>"] =
1102  { function() require("dap").step_into() end, desc = "Debugger: Step Into" }
1103  maps.n["<S-11>"] =
1104  { function() require("dap").step_out() end, desc = "Debugger: Step Out" }
1105
1106  -- Space + d
1107  maps.n["<leader>db"] = {
1108    function() require("dap").toggle_breakpoint() end,
1109    desc = "Breakpoint (F9)",
1110  }
1111  maps.n["<leader>dB"] = {
1112    function() require("dap").clear_breakpoints() end,
1113    desc = "Clear Breakpoints",
1114  }
1115  maps.n["<leader>dc"] =
1116  { function() require("dap").continue() end, desc = "Start/Continue (F5)" }
1117  maps.n["<leader>dC"] = {
1118    function()
1119      vim.ui.input({ prompt = "Condition: " }, function(condition)
1120        if condition then require("dap").set_breakpoint(condition) end
1121      end)
1122    end,
1123    desc = "Conditional Breakpoint (S-F9)",
1124  }
1125  maps.n["<leader>do"] =
1126  { function() require("dap").step_over() end, desc = "Step Over (F10)" }
1127  maps.n["<leader>do"] =
1128  { function() require("dap").step_back() end, desc = "Step Back (S-F10)" }
1129  maps.n["<leader>db"] =
1130  { function() require("dap").step_into() end, desc = "Step Into (F11)" }
1131  maps.n["<leader>dO"] =
1132  { function() require("dap").step_out() end, desc = "Step Out (S-F11)" }
1133  maps.n["<leader>dq"] =
1134  { function() require("dap").close() end, desc = "Close Session" }
1135  maps.n["<leader>dQ"] = {
1136    function() require("dap").terminate() end,
1137    desc = "Terminate Session (S-F5)",
1138  }
1139  maps.n["<leader>dp"] =
1140  { function() require("dap").pause() end, desc = "Pause" }
1141  maps.n["<leader>dr"] =
1142  { function() require("dap").restart_frame() end, desc = "Restart (C-F5)" }
1143  maps.n["<leader>dR"] =
1144  { function() require("dap").repl.toggle() end, desc = "REPL" }
1145  maps.n["<leader>ds"] =
1146  { function() require("dap").run_to_cursor() end, desc = "Run To Cursor" }
1147
1148  if is_available "nvim-dap-ui" then
1149    maps.n["<leader>dE"] = {
1150      function()
1151        vim.ui.input({ prompt = "Expression: " }, function(expr)
1152          if expr then require("dapui").eval(expr, { enter = true }) end
1153        end)
1154      end,
1155      desc = "Evaluate Input",
1156    }
1157    maps.x["<leader>dE"] =
1158    { function() require("dapui").eval() end, desc = "Evaluate Input" }
1159    maps.n["<leader>du"] =
1160    { function() require("dapui").toggle() end, desc = "Debugger UI" }
1161    maps.n["<leader>dh"] = {
1162      function() require("dap.ui.widgets").hover() end,
1163      desc = "Debugger Hover",
1164    }
1165  end
1166end
1167
1168-- testing [tests] -------------------------------------------------
1169-- neotest
1170maps.n["<leader>T"] = icons.tt
1171maps.x["<leader>T"] = icons.tt
1172if is_available "neotest" then
1173  maps.n["<leader>Tu"] = {
1174    function() require("neotest").run.run() end,
1175    desc = "Unit",
1176  }
1177  maps.n["<leader>Ts"] = {
1178    function() require("neotest").run.stop() end,
1179    desc = "Stop unit",
1180  }
1181  maps.n["<leader>Tf"] = {
1182    function() require("neotest").run.run(vim.fn.expand "%") end,
1183    desc = "File",
1184  }
1185  maps.n["<leader>Td"] = {
1186    function() require("neotest").run.run { strategy = "dap" } end,
1187    desc = "Unit in debugger",
1188  }
1189  maps.n["<leader>Tt"] = {
1190    function() require("neotest").summary.toggle() end,
1191    desc = "Neotest summary",
1192  }
1193  maps.n["<leader>TT"] = {
1194    function() require("neotest").output_panel.toggle() end,
1195    desc = "Output panel",
1196  }
1197end
1198
1199-- Extra - nvim-coverage
1200--         Your project must generate coverage/lcov.info for this to work.
1201--
1202--         On jest, make sure your packages.json file has this:
1203--         "test": "jest --coverage"
1204--
1205--         If you use other framework or language, refer to nvim-coverage docs:
1206--         https://github.com/andythigpen/nvim-coverage/blob/main/doc/nvim-coverage.txt
1207if is_available "nvim-coverage" then
1208  maps.n["<leader>Tc"] = {
1209    function()
1210      require("coverage").load(false)
1211      require("coverage").summary()
1212    end,
1213    desc = "Coverage",
1214  }
1215  maps.n["<leader>TC"] = {
1216    function()
1217      ui.toggle_coverage_signs()
1218    end,
1219    desc = "Coverage signs (toggle)",
1220  }
1221end
1222
1223-- Extra - nodejs testing commands
1224maps.n["<leader>Ta"] = {
1225  function() vim.cmd "TestNodejs" end,
1226  desc = "All",
1227}
1228maps.n["<leader>Te"] = {
1229  function() vim.cmd "TestNodejsE2e" end,
1230  desc = "E2e",
1231}
1232
1233-- nvim-ufo [code folding] --------------------------------------------------
1234if is_available "nvim-ufo" then
1235  maps.n["zR"] =
1236  { function() require("ufo").openAllFolds() end, desc = "Open all folds" }
1237  maps.n["zM"] =
1238  { function() require("ufo").closeAllFolds() end, desc = "Close all folds" }
1239  maps.n["zr"] = {
1240    function() require("ufo").openFoldsExceptKinds() end,
1241    desc = "Fold less",
1242  }
1243  maps.n["zm"] =
1244  { function() require("ufo").closeFoldsWith() end, desc = "Fold more" }
1245  maps.n["zp"] = {
1246    function() require("ufo").peekFoldedLinesUnderCursor() end,
1247    desc = "Peek fold",
1248  }
1249  maps.n["zn"] =
1250  {
1251    function() require("ufo").openFoldsExceptKinds({ 'comment' }) end,
1252    desc = "Fold comments"
1253  }
1254  maps.n["zN"] =
1255  {
1256    function() require("ufo").openFoldsExceptKinds({ 'region' }) end,
1257    desc = "Fold region"
1258  }
1259end
1260
1261-- code docmentation [docs] -------------------------------------------------
1262
1263if is_available "markdown-preview.nivm" or is_available "markmap.nvim" or is_available "dooku.nvim" then
1264  maps.n["<leader>D"] = icons.dc
1265
1266  -- Markdown preview
1267  if is_available "markdown-preview.nvim" then
1268    maps.n["<leader>Dp"] = {
1269      function() vim.cmd "MarkdownPreview" end,
1270      desc = "Markdown preview",
1271    }
1272  end
1273
1274  -- Markdown Mindmap
1275  if is_available "markmap.nvim" then
1276    maps.n["<leader>Dm"] = {
1277      function()
1278        if is_android then
1279          vim.cmd "MarkmapWatch"
1280        else
1281          vim.cmd "MarkmapOpen"
1282        end
1283      end,
1284      desc = "Markmap",
1285    }
1286  end
1287
1288  if is_available "dooku.nvim" then
1289    maps.n["<leader>Dd"] = {
1290      function() vim.cmd ":DookuGenerate" end,
1291      desc = "Open documentation",
1292    }
1293  end
1294end
1295
1296-- [neural] -----------------------------------------------------------------
1297if is_available "neural" or is_available "copilot" then
1298  maps.n["<leader>a"] = {
1299    function() require("neural").prompt() end,
1300    desc = "Ask chatgpt",
1301  }
1302end
1303
1304-- hop.nivm ----------------------------------------------------------------
1305if is_available "hop.nvim" then
1306  -- Note that Even though we are using ENTER for hop, you can still select items
1307  -- from special menus like 'quickfix', 'q?' and 'q:' with <C+ENTER>.
1308
1309  maps.n["<C-m>"] = { -- The terminal undersand C-m and ENTER as the same key.
1310    function()
1311      require("hop")
1312      vim.cmd("silent! HopWord")
1313    end,
1314    desc = "Hop to word",
1315  }
1316  maps.x["<C-m>"] = { -- The terminal undersand C-m and ENTER as the same key.
1317    function()
1318      require("hop")
1319      vim.cmd("silent! HopWord")
1320    end,
1321    desc = "Hop to word",
1322  }
1323end
1324
1325-- mason-lspconfig.nvim [lsp] -------------------------------------------------
1326-- WARNING: Don't delete this section, or you won't have LSP keymappings.
1327
1328--A function we call from the script to start lsp.
1329--@return table lsp_mappings
1330function M.lsp_mappings(client, bufnr)
1331  -- Helper function to check if any active LSP clients
1332  -- given a filter provide a specific capability.
1333  -- @param capability string The server capability to check for (example: "documentFormattingProvider").
1334  -- @param filter vim.lsp.get_clients.filter|nil A valid get_clients filter (see function docs).
1335  -- @return boolean # `true` if any of the clients provide the capability.
1336  local function has_capability(capability, filter)
1337    for _, lsp_client in ipairs(vim.lsp.get_clients(filter)) do
1338      if lsp_client.supports_method(capability) then return true end
1339    end
1340    return false
1341  end
1342
1343  local lsp_mappings = require("base.utils").get_mappings_template()
1344
1345  -- Diagnostics
1346  lsp_mappings.n["<leader>ld"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }
1347  lsp_mappings.n["[d"] = { function()
1348      -- TODO: Delete after dropping nvim 0.10 support.
1349      if vim.fn.has('nvim-0.11') == 1 then vim.diagnostic.jump({ count = -1 })
1350      else vim.diagnostic.goto_prev() end end, desc = "Previous diagnostic"
1351  }
1352  lsp_mappings.n["]d"] = { function()
1353    -- TODO: Delete after dropping nvim 0.10 support.
1354    if vim.fn.has('nvim-0.11') == 1 then vim.diagnostic.jump({ count = 1 })
1355    else vim.diagnostic.goto_next() end end, desc = "Next diagnostic" }
1356
1357  -- Diagnostics
1358  lsp_mappings.n["gl"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }
1359  if is_available "telescope.nvim" then
1360    lsp_mappings.n["<leader>lD"] =
1361      { function() require("telescope.builtin").diagnostics() end, desc = "Diagnostics" }
1362  end
1363
1364  -- LSP info
1365  if is_available "mason-lspconfig.nvim" then
1366    lsp_mappings.n["<leader>li"] = { "<cmd>LspInfo<cr>", desc = "LSP information" }
1367  end
1368
1369  if is_available "none-ls.nvim" then
1370    lsp_mappings.n["<leader>lI"] = { "<cmd>NullLsInfo<cr>", desc = "Null-ls information" }
1371  end
1372
1373  -- Code actions
1374  lsp_mappings.n["<leader>la"] = {
1375    function() vim.lsp.buf.code_action() end,
1376    desc = "LSP code action",
1377  }
1378  lsp_mappings.v["<leader>la"] = lsp_mappings.n["<leader>la"]
1379
1380  -- Codelens
1381  utils.add_autocmds_to_buffer("lsp_codelens_refresh", bufnr, {
1382    events = { "InsertLeave" },
1383    desc = "Refresh codelens",
1384    callback = function(args)
1385      if client.supports_method "textDocument/codeLens" then
1386        if vim.g.codelens_enabled then vim.lsp.codelens.refresh({ bufnr = args.buf }) end
1387      end
1388    end,
1389  })
1390  if client.supports_method "textDocument/codeLens" then -- on LspAttach
1391    if vim.g.codelens_enabled then vim.lsp.codelens.refresh({ bufnr = 0 }) end
1392  end
1393
1394  lsp_mappings.n["<leader>ll"] = {
1395    function()
1396      vim.lsp.codelens.run()
1397      vim.lsp.codelens.refresh({ bufnr = 0 })
1398    end,
1399    desc = "LSP CodeLens run",
1400  }
1401  lsp_mappings.n["<leader>uL"] = {
1402    function() ui.toggle_codelens() end,
1403    desc = "CodeLens",
1404  }
1405
1406  -- Formatting
1407  local formatting = require("base.utils.lsp").formatting
1408  lsp_mappings.n["<leader>lf"] = {
1409    function()
1410      vim.lsp.buf.format(M.format_opts)
1411      vim.cmd('checktime') -- update buffer to reflect changes.
1412    end,
1413    desc = "Format buffer",
1414  }
1415  lsp_mappings.v["<leader>lf"] = lsp_mappings.n["<leader>lf"]
1416
1417  vim.api.nvim_buf_create_user_command(
1418    bufnr,
1419    "Format",
1420    function() vim.lsp.buf.format(M.format_opts) end,
1421    { desc = "Format file with LSP" }
1422  )
1423  local autoformat = formatting.format_on_save
1424  local filetype = vim.api.nvim_get_option_value("filetype", { buf = bufnr })
1425  if
1426    autoformat.enabled
1427    and (vim.tbl_isempty(autoformat.allow_filetypes or {}) or vim.tbl_contains(autoformat.allow_filetypes, filetype))
1428    and (vim.tbl_isempty(autoformat.ignore_filetypes or {}) or not vim.tbl_contains(autoformat.ignore_filetypes, filetype))
1429  then
1430    utils.add_autocmds_to_buffer("lsp_auto_format", bufnr, {
1431      events = "BufWritePre",
1432      desc = "Autoformat on save",
1433      callback = function()
1434        if not has_capability("textDocument/formatting", { bufnr = bufnr }) then
1435          utils.del_autocmds_from_buffer("lsp_auto_format", bufnr)
1436          return
1437        end
1438        local autoformat_enabled = vim.b.autoformat_enabled
1439        if autoformat_enabled == nil then autoformat_enabled = vim.g.autoformat_enabled end
1440        if autoformat_enabled and ((not autoformat.filter) or autoformat.filter(bufnr)) then
1441          vim.lsp.buf.format(vim.tbl_deep_extend("force", M.format_opts, { bufnr = bufnr }))
1442        end
1443      end,
1444    })
1445    lsp_mappings.n["<leader>uf"] = {
1446      function() require("base.utils.ui").toggle_buffer_autoformat() end,
1447      desc = "Autoformatting (buffer)",
1448    }
1449    lsp_mappings.n["<leader>uF"] = {
1450      function() require("base.utils.ui").toggle_autoformat() end,
1451      desc = "Autoformatting (global)",
1452    }
1453  end
1454
1455  -- Highlight references when cursor holds
1456  utils.add_autocmds_to_buffer("lsp_document_highlight", bufnr, {
1457    {
1458      events = { "CursorHold", "CursorHoldI" },
1459      desc = "highlight references when cursor holds",
1460      callback = function()
1461        if has_capability("textDocument/documentHighlight", { bufnr = bufnr }) then
1462          vim.lsp.buf.document_highlight()
1463        end
1464      end,
1465    },
1466    {
1467      events = { "CursorMoved", "CursorMovedI", "BufLeave" },
1468      desc = "clear references when cursor moves",
1469      callback = function() vim.lsp.buf.clear_references() end,
1470    },
1471  })
1472
1473  -- Other LSP mappings
1474  lsp_mappings.n["<leader>lL"] = {
1475    function() vim.api.nvim_command(':LspRestart') end,
1476    desc = "LSP refresh",
1477  }
1478
1479  -- Goto definition / declaration
1480  lsp_mappings.n["gd"] = {
1481    function() vim.lsp.buf.definition() end,
1482    desc = "Goto definition of current symbol",
1483  }
1484  lsp_mappings.n["gD"] = {
1485    function() vim.lsp.buf.declaration() end,
1486    desc = "Goto declaration of current symbol",
1487  }
1488
1489  -- Goto implementation
1490  lsp_mappings.n["gI"] = {
1491    function() vim.lsp.buf.implementation() end,
1492    desc = "Goto implementation of current symbol",
1493  }
1494
1495  -- Goto type definition
1496  lsp_mappings.n["gT"] = {
1497    function() vim.lsp.buf.type_definition() end,
1498    desc = "Goto definition of current type",
1499  }
1500
1501  -- Goto references
1502  lsp_mappings.n["<leader>lR"] = {
1503    function() vim.lsp.buf.references() end,
1504    desc = "Hover references",
1505  }
1506  lsp_mappings.n["gr"] = {
1507    function() vim.lsp.buf.references() end,
1508    desc = "References of current symbol",
1509  }
1510
1511  -- Goto help
1512  lsp_mappings.n["gh"] = {
1513    function() vim.lsp.buf.hover() end,
1514    desc = "Hover help",
1515  }
1516  lsp_mappings.n["gH"] = {
1517    function() vim.lsp.buf.signature_help() end,
1518    desc = "Signature help",
1519  }
1520
1521  lsp_mappings.n["<leader>lh"] = {
1522    function() vim.lsp.buf.hover() end,
1523    desc = "Hover help",
1524  }
1525  lsp_mappings.n["<leader>lH"] = {
1526    function() vim.lsp.buf.signature_help() end,
1527    desc = "Signature help",
1528  }
1529
1530  -- Goto man
1531  lsp_mappings.n["gm"] = {
1532    function() vim.api.nvim_feedkeys("K", "n", false) end,
1533    desc = "Hover man",
1534  }
1535  lsp_mappings.n["<leader>lm"] = {
1536    function() vim.api.nvim_feedkeys("K", "n", false) end,
1537    desc = "Hover man",
1538  }
1539
1540  -- Rename symbol
1541  lsp_mappings.n["<leader>lr"] = {
1542    function() vim.lsp.buf.rename() end,
1543    desc = "Rename current symbol",
1544  }
1545
1546  -- Toggle inlay hints
1547  if vim.b.inlay_hints_enabled == nil then vim.b.inlay_hints_enabled = vim.g.inlay_hints_enabled end
1548  if vim.b.inlay_hints_enabled then vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) end
1549  lsp_mappings.n["<leader>uH"] = {
1550    function() require("base.utils.ui").toggle_buffer_inlay_hints(bufnr) end,
1551    desc = "LSP inlay hints (buffer)",
1552  }
1553
1554  -- Toggle semantic tokens
1555  if vim.g.semantic_tokens_enabled then
1556    vim.b[bufnr].semantic_tokens_enabled = true
1557    lsp_mappings.n["<leader>uY"] = {
1558      function() require("base.utils.ui").toggle_buffer_semantic_tokens(bufnr) end,
1559      desc = "LSP semantic highlight (buffer)",
1560    }
1561  else
1562    client.server_capabilities.semanticTokensProvider = nil
1563  end
1564
1565  -- LSP based search
1566  lsp_mappings.n["<leader>lS"] = { function() vim.lsp.buf.workspace_symbol() end, desc = "Search symbol in workspace" }
1567  lsp_mappings.n["gS"] = { function() vim.lsp.buf.workspace_symbol() end, desc = "Search symbol in workspace" }
1568
1569  -- LSP telescope
1570  if is_available "telescope.nvim" then -- setup telescope mappings if available
1571    if lsp_mappings.n.gd then lsp_mappings.n.gd[1] = function() require("telescope.builtin").lsp_definitions() end end
1572    if lsp_mappings.n.gI then
1573      lsp_mappings.n.gI[1] = function() require("telescope.builtin").lsp_implementations() end
1574    end
1575    if lsp_mappings.n.gr then lsp_mappings.n.gr[1] = function() require("telescope.builtin").lsp_references() end end
1576    if lsp_mappings.n["<leader>lR"] then
1577      lsp_mappings.n["<leader>lR"][1] = function() require("telescope.builtin").lsp_references() end
1578    end
1579    if lsp_mappings.n.gy then
1580      lsp_mappings.n.gy[1] = function() require("telescope.builtin").lsp_type_definitions() end
1581    end
1582    if lsp_mappings.n["<leader>lS"] then
1583      lsp_mappings.n["<leader>lS"][1] = function()
1584        vim.ui.input({ prompt = "Symbol Query: (leave empty for word under cursor)" }, function(query)
1585          if query then
1586            -- word under cursor if given query is empty
1587            if query == "" then query = vim.fn.expand "<cword>" end
1588            require("telescope.builtin").lsp_workspace_symbols {
1589              query = query,
1590              prompt_title = ("Find word (%s)"):format(query),
1591            }
1592          end
1593        end)
1594      end
1595    end
1596    if lsp_mappings.n["gS"] then
1597      lsp_mappings.n["gS"][1] = function()
1598        vim.ui.input({ prompt = "Symbol Query: (leave empty for word under cursor)" }, function(query)
1599          if query then
1600            -- word under cursor if given query is empty
1601            if query == "" then query = vim.fn.expand "<cword>" end
1602            require("telescope.builtin").lsp_workspace_symbols {
1603              query = query,
1604              prompt_title = ("Find word (%s)"):format(query),
1605            }
1606          end
1607        end)
1608      end
1609    end
1610  end
1611
1612  return lsp_mappings
1613end
1614
1615utils.set_mappings(maps)
1616return M