Skip to content

colemak dh

Zeioth edited this page Aug 11, 2025 · 82 revisions

Copy paste this into the mappings file. These mappings are updated at the same time as the qwerty mappings, because I actually use colemak-dh myself. So rest assured.

-- Keybindings (colemak-dh).

-- DESCRIPTION:
-- All mappings are defined here.

--    Sections:
--       ## Base bindings
--       -> icons displayed on which-key.nvim
--       -> standard operations
--       -> clipboard
--       -> search highlighting
--       -> improved tabulation
--       -> improved gg
--       -> packages
--       -> buffers/tabs                       [buffers]
--       -> ui toggles                         [ui]
--       -> shifted movement keys
--       -> cmdline autocompletion
--       -> special cases

--       ## Plugin bindings
--       -> alpha-nvim
--       -> nvmenu
--       -> git                                [git]
--       -> file browsers
--       -> session manager
--       -> smart-splits.nvim
--       -> aerial.nvim
--       -> litee-calltree.nvim
--       -> telescope.nvim                     [find]
--       -> toggleterm.nvim
--       -> dap.nvim                           [debugger]
--       -> tests                              [tests]
--       -> nvim-ufo
--       -> code documentation                 [docs]
--       -> ask chatgpt                        [neural]
--       -> hop.nvim
--       -> mason-lspconfig.nvim               [lsp]

--       ## Remaps
--       -> colemak-dh remaps
--
--
--   MAPPINGS REFERENCE
--   -------------------------------------------------------------------
--   |        Mode  | Norm | Ins | Cmd | Vis | Sel | Opr | Term | Lang |
--   Command        +------+-----+-----+-----+-----+-----+------+------+
--   [nore]map      | yes  |  -  |  -  | yes | yes | yes |  -   |  -   |
--   n[nore]map     | yes  |  -  |  -  |  -  |  -  |  -  |  -   |  -   |
--   [nore]map!     |  -   | yes | yes |  -  |  -  |  -  |  -   |  -   |
--   i[nore]map     |  -   | yes |  -  |  -  |  -  |  -  |  -   |  -   |
--   c[nore]map     |  -   |  -  | yes |  -  |  -  |  -  |  -   |  -   |
--   v[nore]map     |  -   |  -  |  -  | yes | yes |  -  |  -   |  -   |
--   x[nore]map     |  -   |  -  |  -  | yes |  -  |  -  |  -   |  -   |
--   s[nore]map     |  -   |  -  |  -  |  -  | yes |  -  |  -   |  -   |
--   o[nore]map     |  -   |  -  |  -  |  -  |  -  | yes |  -   |  -   |
--   t[nore]map     |  -   |  -  |  -  |  -  |  -  |  -  | yes  |  -   |
--   l[nore]map     |  -   | yes | yes |  -  |  -  |  -  |  -   | yes  |
--   -------------------------------------------------------------------

local M = {}
local utils = require("base.utils")
local get_icon = utils.get_icon
local is_available = utils.is_available
local ui = require("base.utils.ui")
local maps = require("base.utils").get_mappings_template()
local is_android = vim.fn.isdirectory("/data") == 1 -- true if on android

-- -------------------------------------------------------------------------
--
-- ## Base bindings ########################################################
--
-- -------------------------------------------------------------------------

-- icons displayed on which-key.nvim ---------------------------------------
local icons = {
  f = { desc = get_icon("Find", true) .. " Find" },
  p = { desc = get_icon("Packages", true) .. " Packages" },
  l = { desc = get_icon("ActiveLSP", true) .. " LSP" },
  u = { desc = get_icon("UI", true) .. " UI" },
  b = { desc = get_icon("Buffer", true) .. " Buffers" },
  bs = { desc = get_icon("Sort", true) .. " Sort Buffers" },
  c = { desc = get_icon("Run", true) .. " Compiler" },
  d = { desc = get_icon("Debugger", true) .. " Debugger" },
  tt = { desc = get_icon("Test", true) .. " Test" },
  dc = { desc = get_icon("Docs", true) .. " Docs" },
  g = { desc = get_icon("Git", true) .. " Git" },
  S = { desc = get_icon("Session", true) .. " Session" },
  t = { desc = get_icon("Terminal", true) .. " Terminal" },
}

-- standard Operations -----------------------------------------------------
maps.n["j"] =
{ "v:count == 0 ? 'gj' : 'j'", expr = true, desc = "Move cursor down" }
maps.n["k"] =
{ "v:count == 0 ? 'gk' : 'k'", expr = true, desc = "Move cursor up" }
maps.n["<leader>w"] = { "<cmd>w<cr>", desc = "Save" }
maps.n["<leader>W"] =
{ function() vim.cmd("SudaWrite") end, desc = "Save as sudo" }
maps.n["<leader>n"] = { "<cmd>enew<cr>", desc = "New file" }
maps.n["<Leader>/"] = { "gcc", remap = true, desc = "Toggle comment line" }
maps.x["<Leader>/"] = { "gc", remap = true, desc = "Toggle comment" }
maps.n["gx"] = {
  utils.open_with_program,
  desc = "Open the file under cursor with a program",
}
maps.n["<C-s>"] = { "<cmd>w!<cr>", desc = "Force write" }
maps.n["|"] = { "<cmd>vsplit<cr>", desc = "Vertical Split" }
maps.n["\\"] = { "<cmd>split<cr>", desc = "Horizontal Split" }
maps.i["<C-BS>"] = { "<C-W>", desc = "Enable CTRL+backsace to delete." }
maps.n["0"] =
{ "^", desc = "Go to the fist character of the line (aliases 0 to ^)" }
maps.n["<C-q>"] = { "<cmd>q!<cr>", desc = "Force quit" }
maps.n["<leader>q"] = {
  function()
    -- Ask user for confirmation
    local choice =
        vim.fn.confirm("Do you really want to exit nvim?", "&Yes\n&No", 2)
    if choice == 1 then
      -- If user confirms, but there are still files to be saved: Ask
      vim.cmd("confirm quit")
    end
  end,
  desc = "Quit",
}
maps.n["<Tab>"] = {
  "<Tab>",
  noremap = true,
  silent = true,
  expr = false,
  desc = "FIX: Prevent TAB from behaving like <C-i>, as they share the same internal code",
}

-- clipboard ---------------------------------------------------------------

-- BUG: We disable these mappings on termux by default because <C-y>
--      is the keycode for scrolling, and remapping it would break it.
if not is_android then
  -- only useful when the option clipboard is commented on ./1-options.lua
  maps.n["<C-y>"] = { '"+y<esc>', desc = "Copy to cliboard" }
  maps.x["<C-y>"] = { '"+y<esc>', desc = "Copy to cliboard" }
  maps.n["<C-d>"] =
  { '"+y<esc>dd', desc = "Copy to clipboard and delete line" }
  maps.x["<C-d>"] =
  { '"+y<esc>dd', desc = "Copy to clipboard and delete line" }
  maps.n["<C-p>"] = { '"+p<esc>', desc = "Paste from cliboard" }
end

-- Make 'c' key not copy to clipboard when changing a character.
maps.n["c"] = { '"_c', desc = "Change without yanking" }
maps.n["C"] = { '"_C', desc = "Change without yanking" }
maps.x["c"] = { '"_c', desc = "Change without yanking" }
maps.x["C"] = { '"_C', desc = "Change without yanking" }

-- Make 'x' key not copy to clipboard when deleting a character.
maps.n["x"] = {
  -- Also let's allow 'x' key to delete blank lines in normal mode.
  function()
    if vim.fn.col(".") == 1 then
      local line = vim.fn.getline(".")
      if line:match("^%s*$") then
        vim.api.nvim_feedkeys('"_dd', "n", false)
        vim.api.nvim_feedkeys("$", "n", false)
      else
        vim.api.nvim_feedkeys('"_x', "n", false)
      end
    else
      vim.api.nvim_feedkeys('"_x', "n", false)
    end
  end,
  desc = "Delete character without yanking it",
}
maps.x["x"] = { '"_x', desc = "Delete all characters in line" }

-- Same for shifted X
maps.n["X"] = {
  -- Also let's allow 'x' key to delete blank lines in normal mode.
  function()
    if vim.fn.col(".") == 1 then
      local line = vim.fn.getline(".")
      if line:match("^%s*$") then
        vim.api.nvim_feedkeys('"_dd', "n", false)
        vim.api.nvim_feedkeys("$", "n", false)
      else
        vim.api.nvim_feedkeys('"_X', "n", false)
      end
    else
      vim.api.nvim_feedkeys('"_X', "n", false)
    end
  end,
  desc = "Delete before character without yanking it",
}
maps.x["X"] = { '"_X', desc = "Delete all characters in line" }

-- Override nvim default behavior so it doesn't auto-yank when pasting on visual mode.
maps.x["p"] = { "P", desc = "Paste content you've previourly yanked" }
maps.x["P"] = { "p", desc = "Yank what you are going to override, then paste" }

-- search highlighing ------------------------------------------------------
-- use ESC to clear hlsearch, while preserving its original functionality.
--
-- TIP: If you prefer,  use <leader>ENTER instead of <ESC>
--      to avoid triggering it by accident.
maps.n["<ESC>"] = {
  function()
    if vim.fn.hlexists("Search") then
      vim.cmd("nohlsearch")
    else
      vim.api.nvim_feedkeys(
        vim.api.nvim_replace_termcodes("<ESC>", true, true, true),
        "n",
        true
      )
    end
  end,
}

-- Improved tabulation ------------------------------------------------------
maps.x["<S-Tab>"] = { "<gv", desc = "unindent line" }
maps.x["<Tab>"] = { ">gv", desc = "indent line" }
maps.x["<"] = { "<gv", desc = "unindent line" }
maps.x[">"] = { ">gv", desc = "indent line" } -- BUGGED: When more than 2 lines selected, loses selection

-- improved gg --------------------------------------------------------------
maps.n["gg"] = {
  function()
    vim.g.minianimate_disable = true
    if vim.v.count > 0 then
      vim.cmd("normal! " .. vim.v.count .. "gg")
    else
      vim.cmd("normal! gg0")
    end
    vim.g.minianimate_disable = false
  end,
  desc = "gg and go to the first position",
}
maps.n["G"] = {
  function()
    vim.g.minianimate_disable = true
    vim.cmd("normal! G$")
    vim.g.minianimate_disable = false
  end,
  desc = "G and go to the last position",
}
maps.x["gg"] = {
  function()
    vim.g.minianimate_disable = true
    if vim.v.count > 0 then
      vim.cmd("normal! " .. vim.v.count .. "gg")
    else
      vim.cmd("normal! gg0")
    end
    vim.g.minianimate_disable = false
  end,
  desc = "gg and go to the first position (visual)",
}
maps.x["G"] = {
  function()
    vim.g.minianimate_disable = true
    vim.cmd("normal! G$")
    vim.g.minianimate_disable = false
  end,
  desc = "G and go to the last position (visual)",
}
maps.n["<C-a>"] = { -- to move to the previous position press ctrl + ii
  function()
    vim.g.minianimate_disable = true
    vim.cmd("normal! gg0vG$")
    vim.g.minianimate_disable = false
  end,
  desc = "Visually select all",
}

-- packages -----------------------------------------------------------------
-- lazy
maps.n["<leader>p"] = icons.p
maps.n["<leader>pu"] =
{ function() require("lazy").check() end, desc = "Lazy open" }
maps.n["<leader>pU"] =
{ function() require("lazy").update() end, desc = "Lazy update" }

-- mason
if is_available("mason.nvim") then
  maps.n["<leader>pm"] = { "<cmd>Mason<cr>", desc = "Mason open" }
  maps.n["<leader>pM"] = { "<cmd>MasonUpdateAll<cr>", desc = "Mason update" }
end

-- treesitter
if is_available("nvim-treesitter") then
  maps.n["<leader>pT"] = { "<cmd>TSUpdate<cr>", desc = "Treesitter update" }
  maps.n["<leader>pt"] = { "<cmd>TSInstallInfo<cr>", desc = "Treesitter open" }
end

-- nvim updater
maps.n["<leader>pD"] = { "<cmd>DistroUpdate<cr>", desc = "Distro update" }
maps.n["<leader>pv"] =
{ "<cmd>DistroReadVersion<cr>", desc = "Distro version" }
maps.n["<leader>pc"] =
{ "<cmd>DistroReadChangelog<cr>", desc = "Distro changelog" }

-- buffers/tabs [buffers ]--------------------------------------------------
maps.n["<leader>c"] = { -- Closes window and buffer at the same time.
  function() require("heirline-components.buffer").wipe() end,
  desc = "Wipe buffer",
}
maps.n["<leader>C"] = { -- Closes buffer while keeping the window
  function() require("heirline-components.buffer").close() end,
  desc = "Close buffer",
}
maps.n["<leader>bw"] = {     -- Closes the window
  function()
    vim.cmd("silent! close") -- Be aware you can't close the last window
  end,
  desc = "Close window",
}
-- Closes buffer while keeping the window. Without confirmation.
-- maps.n["<leader>X"] = {
--   function() require("heirline-components.buffer").close(0, true) end,
--   desc = "Force close buffer",
-- }
maps.n["<leader>ba"] = {
  function() vim.cmd("wa") end,
  desc = "Write all changed buffers",
}
maps.n["]b"] = {
  function()
    require("heirline-components.buffer").nav(
      vim.v.count > 0 and vim.v.count or 1
    )
  end,
  desc = "Next buffer",
}
maps.n["[b"] = {
  function()
    require("heirline-components.buffer").nav(
      -(vim.v.count > 0 and vim.v.count or 1)
    )
  end,
  desc = "Previous buffer",
}
maps.n[">b"] = {
  function()
    require("heirline-components.buffer").move(
      vim.v.count > 0 and vim.v.count or 1
    )
  end,
  desc = "Move buffer tab right",
}
maps.n["<b"] = {
  function()
    require("heirline-components.buffer").move(
      -(vim.v.count > 0 and vim.v.count or 1)
    )
  end,
  desc = "Move buffer tab left",
}

maps.n["<leader>b"] = icons.b
maps.n["<leader>bc"] = {
  function() require("heirline-components.buffer").close_all(true) end,
  desc = "Close all buffers except current",
}
maps.n["<leader>bC"] = {
  function() require("heirline-components.buffer").close_all() end,
  desc = "Close all buffers",
}
maps.n["<leader>bb"] = {
  function()
    require("heirline-components.all").heirline.buffer_picker(
      function(bufnr) vim.api.nvim_win_set_buf(0, bufnr) end
    )
  end,
  desc = "Select buffer from tabline",
}
maps.n["<leader>bd"] = {
  function()
    require("heirline-components.all").heirline.buffer_picker(
      function(bufnr) require("heirline-components.buffer").close(bufnr) end
    )
  end,
  desc = "Delete buffer from tabline",
}
maps.n["<leader>bl"] = {
  function() require("heirline-components.buffer").close_left() end,
  desc = "Close all buffers to the left",
}
maps.n["<leader>br"] = {
  function() require("heirline-components.buffer").close_right() end,
  desc = "Close all buffers to the right",
}
maps.n["<leader>bs"] = icons.bs
maps.n["<leader>bse"] = {
  function() require("heirline-components.buffer").sort("extension") end,
  desc = "Sort by extension (buffers)",
}
maps.n["<leader>bsr"] = {
  function() require("heirline-components.buffer").sort("unique_path") end,
  desc = "Sort by relative path (buffers)",
}
maps.n["<leader>bsp"] = {
  function() require("heirline-components.buffer").sort("full_path") end,
  desc = "Sort by full path (buffers)",
}
maps.n["<leader>bsi"] = {
  function() require("heirline-components.buffer").sort("bufnr") end,
  desc = "Sort by buffer number (buffers)",
}
maps.n["<leader>bsm"] = {
  function() require("heirline-components.buffer").sort("modified") end,
  desc = "Sort by modification (buffers)",
}
maps.n["<leader>b\\"] = {
  function()
    require("heirline-components.all").heirline.buffer_picker(function(bufnr)
      vim.cmd.split()
      vim.api.nvim_win_set_buf(0, bufnr)
    end)
  end,
  desc = "Horizontal split buffer from tabline",
}
maps.n["<leader>b|"] = {
  function()
    require("heirline-components.all").heirline.buffer_picker(function(bufnr)
      vim.cmd.vsplit()
      vim.api.nvim_win_set_buf(0, bufnr)
    end)
  end,
  desc = "Vertical split buffer from tabline",
}

-- quick buffer switching
maps.n["<C-e>"] = {
  function()
    require("heirline-components.buffer").nav(
      vim.v.count > 0 and vim.v.count or 1
    )
  end,
  desc = "Next buffer",
}
maps.n["<C-n>"] = {
  function()
    require("heirline-components.buffer").nav(
      -(vim.v.count > 0 and vim.v.count or 1)
    )
  end,
  desc = "Previous buffer",
}

-- tabs
maps.n["]t"] = { function() vim.cmd.tabnext() end, desc = "Next tab" }
maps.n["[t"] = { function() vim.cmd.tabprevious() end, desc = "Previous tab" }

-- zen mode
if is_available("zen-mode.nvim") then
  maps.n["<leader>uz"] =
  { function() ui.toggle_zen_mode() end, desc = "Zen mode" }
end

-- ui toggles [ui] ---------------------------------------------------------
maps.n["<leader>u"] = icons.u
if is_available("nvim-autopairs") then
  maps.n["<leader>ua"] = { ui.toggle_autopairs, desc = "Autopairs" }
end
maps.n["<leader>ub"] = { ui.toggle_background, desc = "Background" }

if is_available("nvim-cmp") then
  maps.n["<leader>uc"] = { ui.toggle_cmp, desc = "Autocompletion" }
end
if is_available("nvim-colorizer.lua") then
  maps.n["<leader>uC"] =
  { "<cmd>ColorizerToggle<cr>", desc = "Color highlight" }
end
maps.n["<leader>ud"] = { ui.toggle_diagnostics, desc = "Diagnostics" }
maps.n["<leader>uD"] = { ui.set_indent, desc = "Change indent setting" }
maps.n["<leader>ug"] = { ui.toggle_signcolumn, desc = "Signcolumn" }
maps.n["<leader>ul"] = { ui.toggle_statusline, desc = "Statusline" }
maps.n["<leader>un"] = { ui.change_number, desc = "Change line numbering" }
maps.n["<leader>uN"] =
{ ui.toggle_ui_notifications, desc = "UI notifications" }
maps.n["<leader>uP"] = { ui.toggle_paste, desc = "Paste mode" }
maps.n["<leader>us"] = { ui.toggle_spell, desc = "Spellcheck" }
maps.n["<leader>uS"] = { ui.toggle_conceal, desc = "Conceal" }
maps.n["<leader>ut"] = { ui.toggle_tabline, desc = "Tabline" }
maps.n["<leader>uu"] = { ui.toggle_url_effect, desc = "URL highlight" }
maps.n["<leader>uw"] = { ui.toggle_wrap, desc = "Wrap" }
maps.n["<leader>uy"] =
{ ui.toggle_buffer_syntax, desc = "Syntax highlight (buffer)" }
maps.n["<leader>uh"] = { ui.toggle_foldcolumn, desc = "Foldcolumn" }
if is_available("lsp_signature.nvim") then
  maps.n["<leader>up"] = { ui.toggle_lsp_signature, desc = "LSP signature" }
end
if is_available("mini.animate") then
  maps.n["<leader>uA"] = { ui.toggle_animations, desc = "Animations" }
end

-- shifted movement keys ----------------------------------------------------
maps.n["<S-Down>"] = {
  function() vim.api.nvim_feedkeys("7j", "n", true) end,
  desc = "Fast move down",
}
maps.n["<S-Up>"] = {
  function() vim.api.nvim_feedkeys("7k", "n", true) end,
  desc = "Fast move up",
}
maps.n["<S-PageDown>"] = {
  function()
    local current_line = vim.fn.line(".")
    local total_lines = vim.fn.line("$")
    local target_line = current_line + 1 + math.floor(total_lines * 0.20)
    if target_line > total_lines then target_line = total_lines end
    vim.api.nvim_win_set_cursor(0, { target_line, 0 })
    vim.cmd("normal! zz")
  end,
  desc = "Page down exactly a 20% of the total size of the buffer",
}
maps.n["<S-PageUp>"] = {
  function()
    local current_line = vim.fn.line(".")
    local target_line = current_line - 1 - math.floor(vim.fn.line("$") * 0.20)
    if target_line < 1 then target_line = 1 end
    vim.api.nvim_win_set_cursor(0, { target_line, 0 })
    vim.cmd("normal! zz")
  end,
  desc = "Page up exactly 20% of the total size of the buffer",
}

-- cmdline autocompletion ---------------------------------------------------
maps.c["<Up>"] = {
  function() return vim.fn.wildmenumode() == 1 and "<Left>" or "<Up>" end,
  noremap = true,
  expr = true,
  desc = "Wildmenu fix for neovim bug #9953",
}
maps.c["<Down>"] = {
  function() return vim.fn.wildmenumode() == 1 and "<Right>" or "<Down>" end,
  noremap = true,
  expr = true,
  desc = "Wildmenu fix for neovim bug #9953",
}
maps.c["<Left>"] = {
  function() return vim.fn.wildmenumode() == 1 and "<Up>" or "<Left>" end,
  noremap = true,
  expr = true,
  desc = "Wildmenu fix for neovim bug #9953",
}
maps.c["<Right>"] = {
  function() return vim.fn.wildmenumode() == 1 and "<Down>" or "<Right>" end,
  noremap = true,
  expr = true,
  desc = "Wildmenu fix for neovim bug #9953",
}

-- special cases -------------------------------------------------
vim.api.nvim_create_autocmd("BufWinEnter", {
  desc = "Make q close help, man, quickfix, dap floats",
  callback = function(args)
    local buftype =
        vim.api.nvim_get_option_value("buftype", { buf = args.buf })
    if vim.tbl_contains({ "help", "nofile", "quickfix" }, buftype) then
      vim.keymap.set(
        "n",
        "q",
        "<cmd>close<cr>",
        { buffer = args.buf, silent = true, nowait = true }
      )
    end
  end,
})
vim.api.nvim_create_autocmd("CmdwinEnter", {
  desc = "Make q close command history (q: and q?)",
  callback = function(args)
    vim.keymap.set(
      "n",
      "q",
      "<cmd>close<cr>",
      { buffer = args.buf, silent = true, nowait = true }
    )
  end,
})

-- -------------------------------------------------------------------------
--
-- ## Plugin bindings
--
-- -------------------------------------------------------------------------

-- alpha-nvim --------------------------------------------------------------
if is_available("alpha-nvim") then
  maps.n["<leader>h"] = {
    function()
      local wins = vim.api.nvim_tabpage_list_wins(0)
      if
          #wins > 1
          and vim.api.nvim_get_option_value("filetype", {})
          == "neo-tree"
      then
        vim.fn.win_gotoid(wins[2]) -- go to non-neo-tree window to toggle alpha
      end
      require("alpha").start(false, require("alpha").default_config)
      vim.b.miniindentscope_disable = true
    end,
    desc = "Home screen",
  }
end

-- [git] -----------------------------------------------------------
-- gitsigns.nvim
maps.n["<leader>g"] = icons.g
if is_available("gitsigns.nvim") then
  maps.n["<leader>g"] = icons.g
  maps.n["]g"] =
  { function() require("gitsigns").nav_hunk('next') end, desc = "Next Git hunk" }
  maps.n["[g"] = {
    function() require("gitsigns").nav_hunk('prev') end,
    desc = "Previous Git hunk",
  }
  maps.n["<leader>gl"] = {
    function() require("gitsigns").blame_line() end,
    desc = "View Git blame",
  }
  maps.n["<leader>gL"] = {
    function() require("gitsigns").blame_line({ full = true }) end,
    desc = "View full Git blame",
  }
  maps.n["<leader>gp"] = {
    function() require("gitsigns").preview_hunk() end,
    desc = "Preview Git hunk",
  }
  maps.n["<leader>gh"] = {
    function() require("gitsigns").reset_hunk() end,
    desc = "Reset Git hunk",
  }
  maps.n["<leader>gr"] = {
    function() require("gitsigns").reset_buffer() end,
    desc = "Reset Git buffer",
  }
  maps.n["<leader>gs"] = {
    function() require("gitsigns").stage_hunk() end,
    desc = "Stage Git hunk",
  }
  maps.n["<leader>gS"] = {
    function() require("gitsigns").stage_buffer() end,
    desc = "Stage Git buffer",
  }
  maps.n["<leader>gu"] = {
    function() require("gitsigns").undo_stage_hunk() end,
    desc = "Unstage Git hunk",
  }
  maps.n["<leader>gd"] = {
    function() require("gitsigns").diffthis() end,
    desc = "View Git diff",
  }
end
-- git fugitive
if is_available("vim-fugitive") then
  maps.n["<leader>gP"] = {
    function() vim.cmd(":GBrowse") end,
    desc = "Open in github ",
  }
end
-- git client
if vim.fn.executable("lazygit") == 1 then -- if lazygit exists, show it
  maps.n["<leader>gg"] = {
    function()
      local git_dir = vim.fn.finddir(".git", vim.fn.getcwd() .. ";")
      if git_dir ~= "" then
        vim.cmd("TermExec cmd='lazygit && exit'")
      else
        utils.notify("Not a git repository", vim.log.levels.WARN)
      end
    end,
    desc = "ToggleTerm lazygit",
  }
end
if vim.fn.executable("gitui") == 1 then -- if gitui exists, show it
  maps.n["<leader>gg"] = {
    function()
      local git_dir = vim.fn.finddir(".git", vim.fn.getcwd() .. ";")
      if git_dir ~= "" then
        if vim.fn.executable("keychain") == 1 then
          vim.cmd(
            'TermExec cmd="eval `keychain --eval ~/.ssh/github.key` && gitui && exit"'
          )
        else
          vim.cmd("TermExec cmd='gitui && exit'")
        end
      else
        utils.notify("Not a git repository", vim.log.levels.WARN)
      end
    end,
    desc = "ToggleTerm gitui",
  }
end

-- file browsers ------------------------------------
-- yazi
if is_available("yazi.nvim") and vim.fn.executable("yazi") == 1 then
  maps.n["<leader>r"] = {
    -- TODO: use 'Yazi toggle' instead once yazi v0.4.0 is released.
    "<cmd>Yazi<CR>",
    desc = "File browser",
  }
end

-- neotree
if is_available("neo-tree.nvim") then
  maps.n["<leader>e"] = { "<cmd>Neotree toggle<cr>", desc = "Neotree" }
end

-- session manager ---------------------------------------------------------
if is_available("neovim-session-manager") then
  maps.n["<leader>S"] = icons.S
  maps.n["<leader>Sl"] = {
    "<cmd>SessionManager! load_last_session<cr>",
    desc = "Load last session",
  }
  maps.n["<leader>Ss"] = {
    "<cmd>SessionManager! save_current_session<cr>",
    desc = "Save this session",
  }
  maps.n["<leader>Sd"] =
  { "<cmd>SessionManager! delete_session<cr>", desc = "Delete session" }
  maps.n["<leader>Sf"] =
  { "<cmd>SessionManager! load_session<cr>", desc = "Search sessions" }
  maps.n["<leader>S."] = {
    "<cmd>SessionManager! load_current_dir_session<cr>",
    desc = "Load current directory session",
  }
end
if is_available("resession.nvim") then
  maps.n["<leader>S"] = icons.S
  maps.n["<leader>Sl"] = {
    function() require("resession").load("Last Session") end,
    desc = "Load last session",
  }
  maps.n["<leader>Ss"] =
  { function() require("resession").save() end, desc = "Save this session" }
  maps.n["<leader>St"] = {
    function() require("resession").save_tab() end,
    desc = "Save this tab's session",
  }
  maps.n["<leader>Sd"] =
  { function() require("resession").delete() end, desc = "Delete a session" }
  maps.n["<leader>Sf"] =
  { function() require("resession").load() end, desc = "Load a session" }
  maps.n["<leader>S."] = {
    function()
      require("resession").load(vim.fn.getcwd(), { dir = "dirsession" })
    end,
    desc = "Load current directory session",
  }
end

-- smart-splits.nvim --------------------------------------------------------
if is_available("smart-splits.nvim") then
  -- cycle window
  maps.n["<C-l>"] = {
    function() require("smart-splits").move_cursor_left() end,
    desc = "Cycle window (laterally)",
  }
  maps.n["<C-u>"] = {
    function() require("smart-splits").move_cursor_down() end,
    desc = "Cycle window (fron upside down)",
  }
  -- resize window
  maps.n["<C-Up>"] = {
    function() require("smart-splits").resize_up() end,
    desc = "Resize split up",
  }
  maps.n["<C-Down>"] = {
    function() require("smart-splits").resize_down() end,
    desc = "Resize split down",
  }
  maps.n["<C-Left>"] = {
    function() require("smart-splits").resize_left() end,
    desc = "Resize split left",
  }
  maps.n["<C-Right>"] = {
    function() require("smart-splits").resize_right() end,
    desc = "Resize split right",
  }
else
  maps.n["<C-l>"] = { "<C-w>l", desc = "Cycle window (laterally)" }
  maps.n["<C-u>"] = { "<C-w>j", desc = "Cycle window (fron upside down)" }
  maps.n["<C-Up>"] = { "<cmd>resize -2<CR>", desc = "Resize split up" }
  maps.n["<C-Down>"] = { "<cmd>resize +2<CR>", desc = "Resize split down" }
  maps.n["<C-Left>"] =
  { "<cmd>vertical resize -2<CR>", desc = "Resize split left" }
  maps.n["<C-Right>"] =
  { "<cmd>vertical resize +2<CR>", desc = "Resize split right" }
end

-- aerial.nvimm ------------------------------------------------------------
if is_available("aerial.nvim") then
  maps.n["<leader>i"] =
  { function() require("aerial").toggle() end, desc = "Aerial" }
end

-- letee-calltree.nvimm ------------------------------------------------------------
if is_available("litee-calltree.nvim") then
  -- For every buffer, look for the one with filetype "calltree" and focus it.
  local calltree_delay = 1500 -- first run? wait a big longer
  local function focus_calltree()
    -- Note: No go to the previous cursor position, press ctrl+i / ctrl+o
    vim.defer_fn(function()
      for _, win in ipairs(vim.api.nvim_list_wins()) do
        local buf = vim.api.nvim_win_get_buf(win)
        local ft = vim.api.nvim_get_option_value("filetype", { buf = buf })

        if ft == "calltree" then
          vim.api.nvim_set_current_win(win)
          return true
        end
      end
    end, calltree_delay)
    calltree_delay = 100
  end
  maps.n["gj"] = {
    function()
      vim.lsp.buf.incoming_calls()
      focus_calltree()
    end,
    desc = "Call tree (incoming)",
  }
  maps.n["gJ"] = {
    function()
      vim.lsp.buf.outgoing_calls()
      focus_calltree()
    end,
    desc = "Call tree (outgoing)",
  }
end

-- telescope.nvim [find] ----------------------------------------------------
if is_available("telescope.nvim") then
  maps.n["<leader>f"] = icons.f
  maps.n["<leader>gb"] = {
    function() require("telescope.builtin").git_branches() end,
    desc = "Git branches",
  }
  maps.n["<leader>gc"] = {
    function() require("telescope.builtin").git_commits() end,
    desc = "Git commits (repository)",
  }
  maps.n["<leader>gC"] = {
    function() require("telescope.builtin").git_bcommits() end,
    desc = "Git commits (current file)",
  }
  maps.n["<leader>gt"] = {
    function() require("telescope.builtin").git_status() end,
    desc = "Git status",
  }
  maps.n["<leader>f<CR>"] = {
    function() require("telescope.builtin").resume() end,
    desc = "Resume previous search",
  }
  maps.n["<leader>f'"] = {
    function() require("telescope.builtin").marks() end,
    desc = "Find marks",
  }
  maps.n["<leader>fa"] = {
    function()
      local cwd = vim.fn.stdpath("config") .. "/.."
      local search_dirs = { vim.fn.stdpath("config") }
      if #search_dirs == 1 then cwd = search_dirs[1] end -- if only one directory, focus cwd
      require("telescope.builtin").find_files({
        prompt_title = "Config Files",
        search_dirs = search_dirs,
        cwd = cwd,
        follow = true,
      }) -- call telescope
    end,
    desc = "Find nvim config files",
  }
  maps.n["<leader>fb"] = {
    function() require("telescope.builtin").buffers() end,
    desc = "Find buffers",
  }
  maps.n["<leader>fw"] = {
    function() require("telescope.builtin").grep_string() end,
    desc = "Find word under cursor in project",
  }
  maps.n["<leader>fC"] = {
    function() require("telescope.builtin").commands() end,
    desc = "Find commands",
  }
  -- Let's disable this. It is way too imprecise. Use rnvimr instead.
  -- maps.n["<leader>ff"] = {
  --   function()
  --     require("telescope.builtin").find_files { hidden = true, no_ignore = true }
  --   end,
  --   desc = "Find all files",
  -- }
  -- maps.n["<leader>fF"] = {
  --   function() require("telescope.builtin").find_files() end,
  --   desc = "Find files (no hidden)",
  -- }
  maps.n["<leader>fh"] = {
    function() require("telescope.builtin").help_tags() end,
    desc = "Find help",
  }
  maps.n["<leader>fk"] = {
    function() require("telescope.builtin").keymaps() end,
    desc = "Find keymaps",
  }
  maps.n["<leader>fm"] = {
    function() require("telescope.builtin").man_pages() end,
    desc = "Find man",
  }
  if is_available("nvim-notify") then
    maps.n["<leader>fn"] = {
      function() require("telescope").extensions.notify.notify() end,
      desc = "Find notifications",
    }
  end
  maps.n["<leader>fo"] = {
    function() require("telescope.builtin").oldfiles() end,
    desc = "Find recent",
  }
  maps.n["<leader>fv"] = {
    function() require("telescope.builtin").registers() end,
    desc = "Find vim registers",
  }
  maps.n["<leader>ft"] = {
    function()
      -- load color schemes before listing them
      pcall(vim.api.nvim_command, "doautocmd User LoadColorSchemes")

      -- Open telescope
      pcall(require("telescope.builtin").colorscheme, {
        enable_preview = true,
        ignore_builtins = true,
      })
    end,
    desc = "Find themes",
  }
  maps.n["<leader>ff"] = {
    function()
      require("telescope.builtin").live_grep({
        additional_args = function(args)
          args.additional_args = { "--hidden", "--no-ignore" }
          return args.additional_args
        end,
      })
    end,
    desc = "Find words in project",
  }
  maps.n["<leader>fF"] = {
    function() require("telescope.builtin").live_grep() end,
    desc = "Find words in project (no hidden)",
  }
  maps.n["<leader>f/"] = {
    function() require("telescope.builtin").current_buffer_fuzzy_find() end,
    desc = "Find words in current buffer",
  }

  -- Some lsp keymappings are here because they depend on telescope
  maps.n["<leader>l"] = icons.l
  maps.n["<leader>ls"] = {
    function()
      local aerial_avail, _ = pcall(require, "aerial")
      if aerial_avail then
        require("telescope").extensions.aerial.aerial()
      else
        require("telescope.builtin").lsp_document_symbols()
      end
    end,
    desc = "Search symbol in buffer", -- Useful to find every time a variable is assigned.
  }
  maps.n["gs"] = {
    function()
      local aerial_avail, _ = pcall(require, "aerial")
      if aerial_avail then
        require("telescope").extensions.aerial.aerial()
      else
        require("telescope.builtin").lsp_document_symbols()
      end
    end,
    desc = "Search symbol in buffer", -- Useful to find every time a variable is assigned.
  }

  -- extra - project.nvim
  if is_available("project.nvim") then
    maps.n["<leader>fp"] = {
      function() vim.cmd("Telescope projects") end,
      desc = "Find project",
    }
  end

  -- extra - spectre.nvim (search and replace in project)
  if is_available("nvim-spectre") then
    maps.n["<leader>fr"] = {
      function() require("spectre").toggle() end,
      desc = "Find and replace word in project",
    }
    maps.n["<leader>fb"] = {
      function() require("spectre").toggle({ path = vim.fn.expand("%:t:p") }) end,
      desc = "Find and replace word in buffer",
    }
  end

  -- extra - luasnip
  if is_available("LuaSnip") and is_available("telescope-luasnip.nvim") then
    maps.n["<leader>fs"] = {
      function() require("telescope").extensions.luasnip.luasnip({}) end,
      desc = "Find snippets",
    }
  end
  -- workaround for: https://github.com/L3MON4D3/LuaSnip/issues/908
  if is_available("LuaSnip") then
    maps.n["<C-x"] = {
      "<cmd>lua require('luasnip').jump(1)",
      desc = "LuaSnip jump to next field",
    }
  end

  -- extra - nvim-neoclip (neovim internal clipboard)
  --         Specially useful if you disable the shared clipboard in options.
  if is_available("nvim-neoclip.lua") then
    maps.n["<leader>fy"] = {
      function() require("telescope").extensions.neoclip.default() end,
      desc = "Find yank history",
    }
    maps.n["<leader>fq"] = {
      function() require("telescope").extensions.macroscope.default() end,
      desc = "Find recorded macros",
    }
  end

  -- extra - undotree
  if is_available("telescope-undo.nvim") then
    maps.n["<leader>fu"] = {
      function() require("telescope").extensions.undo.undo() end,
      desc = "Find in undo tree",
    }
  end

  -- extra - compiler
  if is_available("compiler.nvim") and is_available("overseer.nvim") then
    maps.n["<leader>m"] = icons.c
    maps.n["<leader>mm"] = {
      function() vim.cmd("CompilerOpen") end,
      desc = "Open compiler",
    }
    maps.n["<leader>mr"] = {
      function() vim.cmd("CompilerRedo") end,
      desc = "Compiler redo",
    }
    maps.n["<leader>mt"] = {
      function() vim.cmd("CompilerToggleResults") end,
      desc = "Compiler results",
    }
    maps.n["<F6>"] = {
      function() vim.cmd("CompilerOpen") end,
      desc = "Open compiler",
    }
    maps.n["<S-F6>"] = {
      function() vim.cmd("CompilerRedo") end,
      desc = "Compiler redo",
    }
    maps.n["<S-F7>"] = {
      function() vim.cmd("CompilerToggleResults") end,
      desc = "Compiler resume",
    }
  end
end

-- toggleterm.nvim ----------------------------------------------------------
if is_available("toggleterm.nvim") then
  maps.n["<leader>t"] = icons.t
  maps.n["<leader>tt"] =
  { "<cmd>ToggleTerm direction=float<cr>", desc = "ToggleTerm float" }
  maps.n["<leader>th"] = {
    "<cmd>ToggleTerm size=10 direction=horizontal<cr>",
    desc = "Toggleterm horizontal split",
  }
  maps.n["<leader>tv"] = {
    "<cmd>ToggleTerm size=80 direction=vertical<cr>",
    desc = "Toggleterm vertical split",
  }
  maps.n["<F7>"] = { "<cmd>ToggleTerm<cr>", desc = "Perminal" }
  maps.t["<F7>"] = maps.n["<F7>"]
  maps.n["<C-'>"] = maps.n["<F7>"] -- requires terminal that supports binding <C-'>
  maps.t["<C-'>"] = maps.n["<F7>"] -- requires terminal that supports binding <C-'>
end

-- extra - improved terminal navigation
maps.t["<C-h>"] =
{ "<cmd>wincmd h<cr>", desc = "Terminal left window navigation" }
maps.t["<C-j>"] =
{ "<cmd>wincmd j<cr>", desc = "Terminal down window navigation" }
maps.t["<C-k>"] =
{ "<cmd>wincmd k<cr>", desc = "Terminal up window navigation" }
maps.t["<C-l>"] =
{ "<cmd>wincmd l<cr>", desc = "Terminal right window navigation" }

-- dap.nvim [debugger] -----------------------------------------------------
-- Depending your terminal some F keys may not work. To fix it:
-- modified function keys found with `showkey -a` in the terminal to get key code
-- run `nvim -V3log +quit` and search through the "Terminal info" in the `log` file for the correct keyname
if is_available("nvim-dap") then
  maps.n["<leader>d"] = icons.d
  maps.x["<leader>d"] = icons.d

  -- F keys
  maps.n["<F5>"] =
  { function() require("dap").continue() end, desc = "Debugger: Start" }
  maps.n["<S-F5>"] =
  { function() require("dap").terminate() end, desc = "Debugger: Stop" }
  maps.n["<C-F5>"] = {
    function() require("dap").restart_frame() end,
    desc = "Debugger: Restart",
  }
  maps.n["<F9>"] = {
    function() require("dap").toggle_breakpoint() end,
    desc = "Debugger: Toggle Breakpoint",
  }
  maps.n["<S-F9>"] = {
    function()
      vim.ui.input({ prompt = "Condition: " }, function(condition)
        if condition then require("dap").set_breakpoint(condition) end
      end)
    end,
    desc = "Debugger: Conditional Breakpoint",
  }
  maps.n["<F10>"] =
  { function() require("dap").step_over() end, desc = "Debugger: Step Over" }
  maps.n["<S-F10>"] =
  { function() require("dap").step_back() end, desc = "Debugger: Step Back" }
  maps.n["<F11>"] =
  { function() require("dap").step_into() end, desc = "Debugger: Step Into" }
  maps.n["<S-F11>"] =
  { function() require("dap").step_out() end, desc = "Debugger: Step Out" }

  -- Space + d
  maps.n["<leader>db"] = {
    function() require("dap").toggle_breakpoint() end,
    desc = "Breakpoint (F9)",
  }
  maps.n["<leader>dB"] = {
    function() require("dap").clear_breakpoints() end,
    desc = "Clear Breakpoints",
  }
  maps.n["<leader>dc"] =
  { function() require("dap").continue() end, desc = "Start/Continue (F5)" }
  maps.n["<leader>dC"] = {
    function()
      vim.ui.input({ prompt = "Condition: " }, function(condition)
        if condition then require("dap").set_breakpoint(condition) end
      end)
    end,
    desc = "Conditional Breakpoint (S-F9)",
  }
  maps.n["<leader>do"] =
  { function() require("dap").step_over() end, desc = "Step Over (F10)" }
  maps.n["<leader>do"] =
  { function() require("dap").step_back() end, desc = "Step Back (S-F10)" }
  maps.n["<leader>db"] =
  { function() require("dap").step_into() end, desc = "Step Into (F11)" }
  maps.n["<leader>dO"] =
  { function() require("dap").step_out() end, desc = "Step Out (S-F11)" }
  maps.n["<leader>dq"] =
  { function() require("dap").close() end, desc = "Close Session" }
  maps.n["<leader>dQ"] = {
    function() require("dap").terminate() end,
    desc = "Terminate Session (S-F5)",
  }
  maps.n["<leader>dp"] =
  { function() require("dap").pause() end, desc = "Pause" }
  maps.n["<leader>dr"] =
  { function() require("dap").restart_frame() end, desc = "Restart (C-F5)" }
  maps.n["<leader>dR"] =
  { function() require("dap").repl.toggle() end, desc = "REPL" }
  maps.n["<leader>ds"] =
  { function() require("dap").run_to_cursor() end, desc = "Run To Cursor" }

  if is_available("nvim-dap-ui") then
    maps.n["<leader>dE"] = {
      function()
        vim.ui.input({ prompt = "Expression: " }, function(expr)
          if expr then require("dapui").eval(expr, { enter = true }) end
        end)
      end,
      desc = "Evaluate Input",
    }
    maps.x["<leader>dE"] =
    { function() require("dapui").eval() end, desc = "Evaluate Input" }
    maps.n["<leader>du"] =
    { function() require("dapui").toggle() end, desc = "Debugger UI" }
    maps.n["<leader>dh"] = {
      function() require("dap.ui.widgets").hover() end,
      desc = "Debugger Hover",
    }
  end
end

-- testing [tests] -------------------------------------------------
-- neotest
maps.n["<leader>T"] = icons.tt
maps.x["<leader>T"] = icons.tt
if is_available("neotest") then
  maps.n["<leader>Tu"] = {
    function() require("neotest").run.run() end,
    desc = "Unit",
  }
  maps.n["<leader>Ts"] = {
    function() require("neotest").run.stop() end,
    desc = "Stop unit",
  }
  maps.n["<leader>Tf"] = {
    function() require("neotest").run.run(vim.fn.expand("%")) end,
    desc = "File",
  }
  maps.n["<leader>Td"] = {
    function() require("neotest").run.run({ strategy = "dap" }) end,
    desc = "Unit in debugger",
  }
  maps.n["<leader>Tt"] = {
    function() require("neotest").summary.toggle() end,
    desc = "Neotest summary",
  }
  maps.n["<leader>TT"] = {
    function() require("neotest").output_panel.toggle() end,
    desc = "Output panel",
  }
end

-- Extra - nvim-coverage
--         Your project must generate coverage/lcov.info for this to work.
--
--         On jest, make sure your packages.json file has this:
--         "test": "jest --coverage"
--
--         If you use other framework or language, refer to nvim-coverage docs:
--         https://github.com/andythigpen/nvim-coverage/blob/main/doc/nvim-coverage.txt
if is_available("nvim-coverage") then
  maps.n["<leader>Tc"] = {
    function()
      require("coverage").load(false)
      require("coverage").summary()
    end,
    desc = "Coverage",
  }
  maps.n["<leader>TC"] = {
    function() ui.toggle_coverage_signs() end,
    desc = "Coverage signs (toggle)",
  }
end

-- Extra - nodejs testing commands
maps.n["<leader>Ta"] = {
  function() vim.cmd("TestNodejs") end,
  desc = "All",
}
maps.n["<leader>Te"] = {
  function() vim.cmd("TestNodejsE2e") end,
  desc = "E2e",
}

-- nvim-ufo [code folding] --------------------------------------------------
if is_available("nvim-ufo") then
  maps.n["zR"] =
  { function() require("ufo").openAllFolds() end, desc = "Open all folds" }
  maps.n["zR"] =
  { function() require("ufo").openAllFolds() end, desc = "Open all folds" }
  maps.n["zM"] =
  { function() require("ufo").closeAllFolds() end, desc = "Close all folds" }
  maps.n["zr"] = {
    function() require("ufo").openFoldsExceptKinds({}) end,
    desc = "Fold less",
  }
  maps.n["zm"] =
  { function() require("ufo").closeFoldsWith() end, desc = "Fold more" }
  maps.n["zp"] = {
    function() require("ufo").peekFoldedLinesUnderCursor() end,
    desc = "Peek fold",
  }
  maps.n["zf"] =
  { function() require("ufo").openAllFolds() end, desc = "Open all folds" }
  maps.n["zn"] = {
    function() require("ufo").openFoldsExceptKinds({ "comment" }) end,
    desc = "Fold comments",
  }
  maps.n["zN"] = {
    function() require("ufo").openFoldsExceptKinds({ "region" }) end,
    desc = "Fold region",
  }
end

-- code docmentation [docs] -------------------------------------------------

if
    is_available("markdown-preview.nvim")
    or is_available("markmap.nvim")
    or is_available("dooku.nvim")
then
  maps.n["<leader>D"] = icons.dc

  -- Markdown preview
  if is_available("markdown-preview.nvim") then
    maps.n["<leader>Dp"] = {
      function() vim.cmd("silent! MarkdownPreview") end,
      desc = "Markdown preview",
    }
  end

  -- Markdown Mindmap
  if is_available("markmap.nvim") then
    maps.n["<leader>Dm"] = {
      function()
        if is_android then
          vim.cmd("MarkmapWatch")
        else
          vim.cmd("MarkmapOpen")
        end
      end,
      desc = "Markmap",
    }
  end

  if is_available("dooku.nvim") then
    maps.n["<leader>Dd"] = {
      function() vim.cmd(":DookuGenerate") end,
      desc = "Open documentation",
    }
  end
end

-- [neural] -----------------------------------------------------------------
if is_available("neural") or is_available("copilot") then
  maps.n["<leader>a"] = {
    function() require("neural").prompt() end,
    desc = "Ask chatgpt",
  }
end

-- hop.nvim ----------------------------------------------------------------
if is_available("hop.nvim") then
  -- Note that Even though we are using ENTER for hop, you can still select items
  -- from special menus like 'quickfix', 'q?' and 'q:' with <C+ENTER>.

  maps.n["<C-m>"] = { -- The terminal undersand C-m and ENTER as the same key.
    function()
      require("hop")
      vim.cmd("silent! HopWord")
    end,
    desc = "Hop to word",
  }
  maps.x["<C-m>"] = { -- The terminal undersand C-m and ENTER as the same key.
    function()
      require("hop")
      vim.cmd("silent! HopWord")
    end,
    desc = "Hop to word",
  }
end

-- mason-lspconfig.nvim [lsp] -------------------------------------------------
-- WARNING: Don't delete this section, or you won't have LSP keymappings.

--A function we call from the script to start lsp.
--@return table lsp_mappings
function M.lsp_mappings(client, bufnr)
  -- Helper function to check if any active LSP clients
  -- given a filter provide a specific capability.
  -- @param capability string The server capability to check for (example: "documentFormattingProvider").
  -- @param filter vim.lsp.get_clients.filter|nil A valid get_clients filter (see function docs).
  -- @return boolean # `true` if any of the clients provide the capability.
  local function has_capability(capability, filter)
    for _, lsp_client in ipairs(vim.lsp.get_clients(filter)) do
      if lsp_client.supports_method(capability) then return true end
    end
    return false
  end

  local lsp_mappings = require("base.utils").get_mappings_template()

  -- Diagnostics
  lsp_mappings.n["<leader>ld"] =
  { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }
  lsp_mappings.n["[d"] = {
    function() vim.diagnostic.jump({ count = -1 }) end,
    desc = "Previous diagnostic",
  }
  lsp_mappings.n["]d"] = {
    function() vim.diagnostic.jump({ count = 1 }) end,
    desc = "Next diagnostic",
  }

  -- Diagnostics
  lsp_mappings.n["gl"] =
  { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" }
  if is_available("telescope.nvim") then
    lsp_mappings.n["<leader>lD"] = {
      function() require("telescope.builtin").diagnostics() end,
      desc = "Diagnostics",
    }
  end

  -- LSP info
  if is_available("mason-lspconfig.nvim") then
    lsp_mappings.n["<leader>li"] =
    { "<cmd>LspInfo<cr>", desc = "LSP information" }
  end

  if is_available("none-ls.nvim") then
    lsp_mappings.n["<leader>lI"] =
    { "<cmd>NullLsInfo<cr>", desc = "Null-ls information" }
  end

  -- Code actions
  lsp_mappings.n["<leader>la"] = {
    function() vim.lsp.buf.code_action() end,
    desc = "LSP code action",
  }
  lsp_mappings.v["<leader>la"] = lsp_mappings.n["<leader>la"]

  -- Codelens
  utils.add_autocmds_to_buffer("lsp_codelens_refresh", bufnr, {
    events = { "InsertLeave" },
    desc = "Refresh codelens",
    callback = function(args)
      if client.supports_method("textDocument/codeLens") then
        if vim.g.codelens_enabled then
          vim.lsp.codelens.refresh({ bufnr = args.buf })
        end
      end
    end,
  })
  if client.supports_method("textDocument/codeLens") then -- on LspAttach
    if vim.g.codelens_enabled then vim.lsp.codelens.refresh({ bufnr = 0 }) end
  end

  lsp_mappings.n["<leader>ll"] = {
    function()
      vim.lsp.codelens.run()
      vim.lsp.codelens.refresh({ bufnr = 0 })
    end,
    desc = "LSP CodeLens run",
  }
  lsp_mappings.n["<leader>uL"] = {
    function() ui.toggle_codelens() end,
    desc = "CodeLens",
  }

  -- Formatting (keymapping)
  local formatting = require("base.utils.lsp").formatting
  local format_opts = require("base.utils.lsp").format_opts
  lsp_mappings.n["<leader>lf"] = {
    function()
      vim.lsp.buf.format(format_opts)
      vim.cmd("checktime") -- Sync buffer with changes
    end,
    desc = "Format buffer",
  }
  lsp_mappings.v["<leader>lf"] = lsp_mappings.n["<leader>lf"]

  -- Formatting (command)
  vim.api.nvim_buf_create_user_command(
    bufnr,
    "Format",
    function() vim.lsp.buf.format(format_opts) end,
    { desc = "Format file with LSP" }
  )

  -- Autoformatting (autocmd)
  local autoformat = formatting.format_on_save
  local filetype = vim.api.nvim_get_option_value("filetype", { buf = bufnr })

  -- guard clauses
  local is_autoformat_enabled = autoformat.enabled
  local is_filetype_allowed = vim.tbl_isempty(autoformat.allow_filetypes or {})
      or vim.tbl_contains(autoformat.allow_filetypes, filetype)
  local is_filetype_ignored = vim.tbl_isempty(
    autoformat.ignore_filetypes or {}
  ) or not vim.tbl_contains(autoformat.ignore_filetypes, filetype)

  if is_autoformat_enabled and is_filetype_allowed and is_filetype_ignored then
    utils.add_autocmds_to_buffer("lsp_auto_format", bufnr, {
      events = "BufWritePre", -- Trigger before save
      desc = "Autoformat on save",
      callback = function()
        -- guard clause: has_capability
        if
            not has_capability("textDocument/formatting", { bufnr = bufnr })
        then
          utils.del_autocmds_from_buffer("lsp_auto_format", bufnr)
          return
        end

        -- Get autoformat setting (buffer or global)
        local autoformat_enabled = vim.b.autoformat_enabled
            or vim.g.autoformat_enabled
        local has_no_filter = not autoformat.filter
        local passes_filter = autoformat.filter and autoformat.filter(bufnr)

        -- Use these variables in the if condition
        if autoformat_enabled and (has_no_filter or passes_filter) then
          vim.lsp.buf.format(
            vim.tbl_deep_extend("force", format_opts, { bufnr = bufnr })
          )
        end
      end,
    })

    -- Key mappings for toggling autoformat (buffer/global)
    lsp_mappings.n["<leader>uf"] = {
      function() require("base.utils.ui").toggle_buffer_autoformat() end,
      desc = "Toggle buffer autoformat",
    }
    lsp_mappings.n["<leader>uF"] = {
      function() require("base.utils.ui").toggle_autoformat() end,
      desc = "Toggle global autoformat",
    }
  end

  -- Highlight references when cursor holds
  utils.add_autocmds_to_buffer("lsp_document_highlight", bufnr, {
    {
      events = { "CursorHold", "CursorHoldI" },
      desc = "highlight references when cursor holds",
      callback = function()
        if
            has_capability("textDocument/documentHighlight", { bufnr = bufnr })
        then
          vim.lsp.buf.document_highlight()
        end
      end,
    },
    {
      events = { "CursorMoved", "CursorMovedI", "BufLeave" },
      desc = "clear references when cursor moves",
      callback = function() vim.lsp.buf.clear_references() end,
    },
  })

  -- Other LSP mappings
  lsp_mappings.n["<leader>lL"] = {
    function() vim.api.nvim_command(":LspRestart") end,
    desc = "LSP refresh",
  }

  -- Goto definition / declaration
  lsp_mappings.n["gd"] = {
    function() vim.lsp.buf.definition() end,
    desc = "Goto definition of current symbol",
  }
  lsp_mappings.n["gD"] = {
    function() vim.lsp.buf.declaration() end,
    desc = "Goto declaration of current symbol",
  }

  -- Goto implementation
  lsp_mappings.n["gI"] = {
    function() vim.lsp.buf.implementation() end,
    desc = "Goto implementation of current symbol",
  }

  -- Goto type definition
  lsp_mappings.n["gT"] = {
    function() vim.lsp.buf.type_definition() end,
    desc = "Goto definition of current type",
  }

  -- Goto references
  lsp_mappings.n["<leader>lR"] = {
    function() vim.lsp.buf.references() end,
    desc = "Hover references",
  }
  lsp_mappings.n["gr"] = {
    function() vim.lsp.buf.references() end,
    desc = "References of current symbol",
  }

  -- Goto help
  local lsp_hover_config = require("base.utils.lsp").lsp_hover_config
  lsp_mappings.n["gh"] = {
    function()
      vim.lsp.buf.hover(lsp_hover_config)
    end,
    desc = "Hover help",
  }
  lsp_mappings.n["gH"] = {
    function() vim.lsp.buf.signature_help(lsp_hover_config) end,
    desc = "Signature help",
  }

  lsp_mappings.n["<leader>lh"] = {
    function() vim.lsp.buf.hover(lsp_hover_config) end,
    desc = "Hover help",
  }
  lsp_mappings.n["<leader>lH"] = {
    function() vim.lsp.buf.signature_help(lsp_hover_config) end,
    desc = "Signature help",
  }

  -- Goto man
  lsp_mappings.n["gm"] = {
    function() vim.api.nvim_feedkeys("K", "n", false) end,
    desc = "Hover man",
  }
  lsp_mappings.n["<leader>lm"] = {
    function() vim.api.nvim_feedkeys("K", "n", false) end,
    desc = "Hover man",
  }

  -- Rename symbol
  lsp_mappings.n["<leader>lr"] = {
    function() vim.lsp.buf.rename() end,
    desc = "Rename current symbol",
  }

  -- Toggle inlay hints
  if vim.b.inlay_hints_enabled == nil then
    vim.b.inlay_hints_enabled = vim.g.inlay_hints_enabled
  end
  if vim.b.inlay_hints_enabled then
    vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
  end
  lsp_mappings.n["<leader>uH"] = {
    function() require("base.utils.ui").toggle_buffer_inlay_hints(bufnr) end,
    desc = "LSP inlay hints (buffer)",
  }

  -- Toggle semantic tokens
  if vim.g.semantic_tokens_enabled then
    vim.b[bufnr].semantic_tokens_enabled = true
    lsp_mappings.n["<leader>uY"] = {
      function() require("base.utils.ui").toggle_buffer_semantic_tokens(bufnr) end,
      desc = "LSP semantic highlight (buffer)",
    }
  else
    client.server_capabilities.semanticTokensProvider = nil
  end

  -- LSP based search
  lsp_mappings.n["<leader>lS"] = {
    function() vim.lsp.buf.workspace_symbol() end,
    desc = "Search symbol in workspace",
  }
  lsp_mappings.n["gS"] = {
    function() vim.lsp.buf.workspace_symbol() end,
    desc = "Search symbol in workspace",
  }

  -- LSP telescope
  if is_available("telescope.nvim") then -- setup telescope mappings if available
    if lsp_mappings.n.gd then
      lsp_mappings.n.gd[1] = function()
        require("telescope.builtin").lsp_definitions()
      end
    end
    if lsp_mappings.n.gI then
      lsp_mappings.n.gI[1] = function()
        require("telescope.builtin").lsp_implementations()
      end
    end
    if lsp_mappings.n.gr then
      lsp_mappings.n.gr[1] = function()
        require("telescope.builtin").lsp_references()
      end
    end
    if lsp_mappings.n["<leader>lR"] then
      lsp_mappings.n["<leader>lR"][1] = function()
        require("telescope.builtin").lsp_references()
      end
    end
    if lsp_mappings.n.gy then
      lsp_mappings.n.gy[1] = function()
        require("telescope.builtin").lsp_type_definitions()
      end
    end
    if lsp_mappings.n["<leader>lS"] then
      lsp_mappings.n["<leader>lS"][1] = function()
        vim.ui.input(
          { prompt = "Symbol Query: (leave empty for word under cursor)" },
          function(query)
            if query then
              -- word under cursor if given query is empty
              if query == "" then query = vim.fn.expand("<cword>") end
              require("telescope.builtin").lsp_workspace_symbols({
                query = query,
                prompt_title = ("Find word (%s)"):format(query),
              })
            end
          end
        )
      end
    end
    if lsp_mappings.n["gS"] then
      lsp_mappings.n["gS"][1] = function()
        vim.ui.input(
          { prompt = "Symbol Query: (leave empty for word under cursor)" },
          function(query)
            if query then
              -- word under cursor if given query is empty
              if query == "" then query = vim.fn.expand("<cword>") end
              require("telescope.builtin").lsp_workspace_symbols({
                query = query,
                prompt_title = ("Find word (%s)"):format(query),
              })
            end
          end
        )
      end
    end
  end

  return lsp_mappings
end

-- colemak-dh remaps (normal) ----------------------------------------------
-- These remaps exchange the position of some keys

-- movement keys
maps.n["m"] = { "h", desc = "Movement left (colemak-dh remap)" }
maps.n["n"] = { "j", desc = "Movement down (colemak-dh remap)" }
maps.n["e"] = { "k", desc = "Movement up (colemak-dh remap)" }
maps.n["i"] = { "l", desc = "Movement right (colemak-dh remap)" }
maps.n["M"] = { "H", desc = "Movement left (colemak-dh remap)" }
maps.n["N"] = { "J", desc = "Movement down (colemak-dh remap)" }
maps.n["E"] = { "K", desc = "Movement up (colemak-dh remap)" }
maps.n["I"] = { "L", desc = "Movement right (colemak-dh remap)" }

-- insert
maps.n["o"] = { "i", desc = "Insert (colemak-dh remap)" }
maps.n["O"] = { "I", desc = "Insert (colemak-dh remap)" }

-- substitute
maps.n["j"] = { "s", desc = "Substitute (colemak-dh remap)" }
maps.n["J"] = { "S", desc = "Substitute (colemak-dh remap)" }

-- marker
maps.n["w"] = { "m", desc = "Marker (colemak-dh remap)" }
maps.n["W"] = { "M", desc = "Marker (colemak-dh remap)" }

-- next word first char
maps.n["b"] = { "w", desc = "Next word first char (colemak-dh remap)" }
maps.n["B"] = { "W", desc = "Next word first char (colemak-dh remap)" }

-- search next
maps.n["k"] = { "n", desc = "Search next (colemak-dh remap)" }
maps.n["K"] = { "N", desc = "Search prev (colemak-dh remap)" }

-- next word
maps.n["t"] = { "e", desc = "Next word (colemak-dh remap)" }
maps.n["T"] = { "E", desc = "Next word (colemak-dh remap)" }

-- prev word
maps.n["s"] = { "b", desc = "Prev word (colemak-dh remap)" }
maps.n["S"] = { "B", desc = "Prev word (colemak-dh remap)" }

-- find till
maps.n["h"] = { "t", desc = "Find untill (colemak-dh remap)" }
maps.n["H"] = { "T", desc = "Find untill (colemak-dh remap)" }

-- ctrl-i / ctrl-o
maps.n["<C-i>"] = { "<C-o>", desc = "Go to next change (colemak-dh remap)" }
maps.n["<C-o>"] = { "<C-i>", desc = "Go to prev change (colemak-dh remap)" }

-- add empty line
maps.n["l"] = {
  "o<ESC>0",
  desc = "Add empty line without entering insert mode (colemak-dh remap)",
}
maps.n["L"] = {
  "O<ESC>0",
  desc = "Add empty line without entering insert mode (colemak-dh remap)",
}

-- colemak-dh remaps (visual) -----------------------------------------------
-- These remaps exchange the position of some keys

-- movement keys
maps.x["m"] = { "h", desc = "Movement left (colemak-dh remap)" }
maps.x["n"] = { "j", desc = "Movement down (colemak-dh remap)" }
maps.x["e"] = { "k", desc = "Movement up (colemak-dh remap)" }
maps.x["i"] = { "l", desc = "Movement right (colemak-dh remap)" }
maps.x["M"] = { "H", desc = "Movement left (colemak-dh remap)" }
maps.x["N"] = { "J", desc = "Movement down (colemak-dh remap)" }
maps.x["E"] = { "K", desc = "Movement up (colemak-dh remap)" }
maps.x["I"] = { "L", desc = "Movement right (colemak-dh remap)" }

-- insert
maps.x["o"] = { "i", desc = "Insert (colemak-dh remap)" }
maps.x["O"] = { "I", desc = "Insert (colemak-dh remap)" }

-- substitute
maps.x["j"] = { "s", desc = "Substitute (colemak-dh remap)" }
maps.x["J"] = { "S", desc = "Substitute (colemak-dh remap)" }

-- marker (Don't set this one on visual mode or it will mess the w motion)
-- maps.x["w"]     = { "m", desc = "Marker (colemak-dh remap)" }
-- maps.x["W"]     = { "M", desc = "Marker (colemak-dh remap)" }

-- next word first char
maps.x["b"] = { "w", desc = "Next word first char (colemak-dh remap)" }
maps.x["B"] = { "W", desc = "Next word first char (colemak-dh remap)" }

-- search next
maps.x["k"] = { "n", desc = "Search next (colemak-dh remap)" }
maps.x["K"] = { "N", desc = "Search prev (colemak-dh remap)" }

-- next word
maps.x["t"] = { "e", desc = "Next word (colemak-dh remap)" }
maps.x["T"] = { "E", desc = "Next word (colemak-dh remap)" }

-- prev word
maps.x["s"] = { "b", desc = "Prev word (colemak-dh remap)" }
maps.x["S"] = { "B", desc = "Prev word (colemak-dh remap)" }

-- find till
maps.x["h"] = { "t", desc = "Find untill (colemak-dh remap)" }
maps.x["H"] = { "T", desc = "Find untill (colemak-dh remap)" }

utils.set_mappings(maps)
return M
Clone this wiki locally