local M = {} local icons = require("config.icons") function M.set_title() local title = " %t" local f = io.popen([[zsh -c ' source $XDG_CONFIG_HOME/zsh/configs/autogenerated/hashes print -Pn "$USER@$HOST [%3~] " ']]) if f ~= nil then title = f:read("*a") or "" title = title .. " %t" f:close() end vim.opt.titlestring = title end function M.buf_close(bufnr, force) local kill_command = "bd" local bo = vim.bo local api = vim.api local fnamemodify = vim.fn.fnamemodify if bufnr == 0 or bufnr == nil then bufnr = api.nvim_get_current_buf() end local bufname = api.nvim_buf_get_name(bufnr) if not force then local warning if bo[bufnr].modified then warning = string.format([[No write since last change for (%s)]], fnamemodify(bufname, ":t")) elseif api.nvim_buf_get_option(bufnr, "buftype") == "terminal" then warning = string.format([[Terminal %s will be killed]], bufname) end if warning then vim.ui.input({ prompt = string.format([[%s. Close it anyway? [y]es or [n]o (default: no): ]], warning), }, function(choice) if choice:match "ye?s?" then force = true end end) if not force then return end end end -- Get list of window IDs with the buffer to close local windows = vim.tbl_filter(function(win) return api.nvim_win_get_buf(win) == bufnr end, api.nvim_list_wins()) if #windows == 0 then return end if force then kill_command = kill_command .. "!" end -- Get list of active buffers local buffers = vim.tbl_filter(function(buf) return api.nvim_buf_is_valid(buf) and bo[buf].buflisted end, api.nvim_list_bufs()) -- If there is only one buffer (which has to be the current one), vim will -- create a new buffer on :bd. -- For more than one buffer, pick the previous buffer (wrapping around if necessary) if #buffers > 1 then for i, v in ipairs(buffers) do if v == bufnr then local prev_buf_idx = i == 1 and (#buffers - 1) or (i - 1) local prev_buffer = buffers[prev_buf_idx] for _, win in ipairs(windows) do api.nvim_win_set_buf(win, prev_buffer) end end end else vim.cmd('q!') end -- Check if buffer still exists, to ensure the target buffer wasn't killed -- due to options like bufhidden=wipe. if api.nvim_buf_is_valid(bufnr) and bo[bufnr].buflisted then vim.cmd(string.format("%s %d", kill_command, bufnr)) end end function M.is_available(plugin) local lazy_config_avail, lazy_config = pcall(require, "lazy.core.config") return lazy_config_avail and lazy_config.spec.plugins[plugin] ~= nil end function M.empty_map_table() local maps = {} for _, mode in ipairs { "", "n", "v", "x", "s", "o", "!", "i", "l", "c", "t" } do maps[mode] = {} end -- TODO: Check this on 0.10.0 release if vim.fn.has "nvim-0.10.0" == 1 then for _, abbr_mode in ipairs { "ia", "ca", "!a" } do maps[abbr_mode] = {} end end return maps end function M.toggle_term_cmd(opts) if not vim.g.user_terminals then vim.g.user_terminals = {} end local terms = vim.g.user_terminals -- if a command string is provided, create a basic table for Terminal:new() options if type(opts) == "string" then opts = { cmd = opts, hidden = true } end local num = vim.v.count > 0 and vim.v.count or 1 -- if terminal doesn't exist yet, create it if not terms[opts.cmd] then terms[opts.cmd] = {} end if not terms[opts.cmd][num] then if not opts.count then opts.count = vim.tbl_count(terms) * 100 + num end if not opts.on_exit then opts.on_exit = function() terms[opts.cmd][num] = nil end end terms[opts.cmd][num] = require("toggleterm.terminal").Terminal:new(opts) end -- toggle the terminal terms[opts.cmd][num]:toggle() end -- TODO: test this function M.file_worktree(file, worktrees) worktrees = worktrees or vim.g.git_worktrees if not worktrees then return end file = file or vim.fn.expand("%") for _, worktree in ipairs(worktrees) do local r = vim.fn.system({ "git", "--work-tree", worktree.toplevel, "--git-dir", worktree.gitdir, "ls-files", "--error-unmatch", file }):wait() if r.code == 0 then return worktree end end end function M.which_key_register() if M.which_key_queue then local wk_avail, wk = pcall(require, "which-key") if wk_avail then for mode, registration in pairs(M.which_key_queue) do wk.register(registration, { mode = mode }) end M.which_key_queue = nil end end end function M.set_mappings(map_table, base) base = base or {} for mode, maps in pairs(map_table) do for keymap, options in pairs(maps) do if options then local cmd = options local keymap_opts = base if type(options) == "table" then cmd = options[1] keymap_opts = vim.tbl_deep_extend("force", keymap_opts, options) keymap_opts[1] = nil end if not cmd or keymap_opts.name then -- which-key mapping if not keymap_opts.name then keymap_opts.name = keymap_opts.desc end if not M.which_key_queue then M.which_key_queue = {} end if not M.which_key_queue[mode] then M.which_key_queue[mode] = {} end M.which_key_queue[mode][keymap] = keymap_opts else -- not which-key mapping vim.keymap.set(mode, keymap, cmd, keymap_opts) end end end end if package.loaded["which-key"] then M.which_key_register() end end local function del_buffer_autocmd(augroup, bufnr) local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr }) if cmds_found then vim.tbl_map(function(cmd) vim.api.nvim_del_autocmd(cmd.id) end, cmds) end end local function add_buffer_autocmd(augroup, bufnr, autocmds) if not vim.tbl_islist(autocmds) then autocmds = { autocmds } end local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr }) if not cmds_found or vim.tbl_isempty(cmds) then vim.api.nvim_create_augroup(augroup, { clear = false }) for _, autocmd in ipairs(autocmds) do local events = autocmd.events autocmd.events = nil autocmd.group = augroup autocmd.buffer = bufnr vim.api.nvim_create_autocmd(events, autocmd) end end end local function has_capability(capability, filter) for _, client in ipairs(vim.lsp.get_active_clients(filter)) do if client.supports_method(capability) then return true end end return false end -- NOTE: LSP Keymaps here -- TODO: Optimize these lookups with a fucking table jesus -- https://stackoverflow.com/a/37493047 function M.lsp_on_attach(client, bufnr) local lsp_mappings = M.empty_map_table() lsp_mappings.n["ld"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" } lsp_mappings.n["[d"] = { function() vim.diagnostic.goto_prev() end, desc = "Previous diagnostic" } lsp_mappings.n["]d"] = { function() vim.diagnostic.goto_next() end, desc = "Next diagnostic" } lsp_mappings.n["gl"] = { function() vim.diagnostic.open_float() end, desc = "Hover diagnostics" } if M.is_available("telescope.nvim") then lsp_mappings.n["lD"] = { function() require("telescope.builtin").diagnostics() end, desc = "Search diagnostics", } end if M.is_available("mason-lspconfig.nvim") then lsp_mappings.n["li"] = { "LspInfo", desc = "LSP information" } end if M.is_available("null-ls.nvim") then lsp_mappings.n["lI"] = { "NullLsInfo", desc = "Null-ls information" } end if client.supports_method("textDocument/codeAction") then lsp_mappings.n["la"] = { function() vim.lsp.buf.code_action() end, desc = "Code action", } lsp_mappings.v["la"] = lsp_mappings.n["la"] end if client.supports_method("textDocument/codeLens") then vim.lsp.codelens.refresh() lsp_mappings.n["ll"] = { function() vim.lsp.codelens.refresh() end, desc = "Refresh CodeLens", } lsp_mappings.n["lL"] = { function() vim.lsp.codelens.run() end, desc = "Run CodeLens", } lsp_mappings.n["u"] = { desc = " " .. icons.ui.Gear .. " Utility" } lsp_mappings.n["uL"] = { function() vim.lsp.codelens.clear() end, desc = "Toggle CodeLens" } end if client.supports_method("textDocument/definition") then lsp_mappings.n["gd"] = { function() vim.lsp.buf.definition() end, desc = "Go to definition", } end if client.supports_method "textDocument/typeDefinition" then lsp_mappings.n["gy"] = { function() vim.lsp.buf.type_definition() end, desc = "Go to type definition", } end if client.supports_method("textDocument/declaration") then lsp_mappings.n["gD"] = { function() vim.lsp.buf.declaration() end, desc = "Go to declaration", } end if client.supports_method("textDocument/implementation") then lsp_mappings.n["gI"] = { function() vim.lsp.buf.implementation() end, desc = "List implementations", } end if client.supports_method("textDocument/references") then lsp_mappings.n["gr"] = { function() vim.lsp.buf.references() end, desc = "List references", } end if client.supports_method "workspace/symbol" then lsp_mappings.n["lG"] = { function() vim.lsp.buf.workspace_symbol() end, desc = "List symbols" } end if client.supports_method "textDocument/rename" then lsp_mappings.n["lr"] = { function() vim.lsp.buf.rename() end, desc = "Rename symbol", } end -- TODO: Check this on 0.10.0 release if client.supports_method "textDocument/semanticTokens/full" and vim.lsp.semantic_tokens then vim.b[bufnr].semantic_tokens_enabled = true lsp_mappings.n["u"] = { desc = " " .. icons.ui.Gear .. " Utility" } lsp_mappings.n["uY"] = { function() vim.b[bufnr].semantic_tokens_enabled = not vim.b[bufnr].semantic_tokens_enabled for _, active_client in ipairs(vim.lsp.get_active_clients { bufnr = bufnr }) do if active_client.server_capabilities.semanticTokensProvider then vim.lsp.semantic_tokens[vim.b[bufnr].semantic_tokens_enabled and "start" or "stop"](bufnr, active_client.id) end end end, desc = "Toggle LSP semantic highlight (buffer)", } end if client.supports_method("textDocument/formatting") then lsp_mappings.n["lf"] = { function() vim.lsp.buf.format(require("config.lsp.format")) end, desc = "Format buffer", } lsp_mappings.v["lf"] = lsp_mappings.n["lf"] end if client.supports_method("textDocument/documentHighlight") then add_buffer_autocmd("lsp_document_highlight", bufnr, { { events = { "CursorHold", "CursorHoldI" }, desc = "highlight references when cursor holds", callback = function() if not has_capability("textDocument/documentHighlight", { bufnr = bufnr }) then del_buffer_autocmd("lsp_document_highlight", bufnr) return end vim.lsp.buf.document_highlight() end, }, { events = { "CursorMoved", "CursorMovedI", "BufLeave" }, desc = "clear references when cursor moves", callback = function() vim.lsp.buf.clear_references() end, }, }) end if client.supports_method("textDocument/hover") then -- TODO: Check this on 0.10.0 release if vim.fn.has "nvim-0.10" == 0 then lsp_mappings.n["K"] = { function() vim.lsp.buf.hover() end, desc = "Hover symbol", } end end if client.supports_method("textDocument/inlayHint") then if vim.b.inlay_hints_enabled == nil then vim.b.inlay_hints_enabled = true end -- TODO: Check this on 0.10.0 release if vim.lsp.inlay_hint then if vim.b.inlay_hints_enabled then vim.lsp.inlay_hint.enable(bufnr, true) end lsp_mappings.n["u"] = { desc = " " .. icons.ui.Gear .. " Utility" } lsp_mappings.n["uH"] = { function() vim.b[bufnr].inlay_hints_enabled = not vim.b[bufnr].inlay_hints_enabled if vim.lsp.inlay_hint then vim.lsp.inlay_hint.enable(bufnr, vim.b[bufnr].inlay_hints_enabled) end end, desc = "Toggle inlay hints", } end end if client.supports_method "textDocument/signatureHelp" then lsp_mappings.n["lh"] = { function() vim.lsp.buf.signature_help() end, desc = "Signature help", } end if M.is_available "telescope.nvim" then 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["lR"] then lsp_mappings.n["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["lG"] then lsp_mappings.n["lG"][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 "" end require("telescope.builtin").lsp_workspace_symbols { query = query, prompt_title = ("Find word (%s)"):format(query), } end end) end end end if not vim.tbl_isempty(lsp_mappings.v) then if lsp_mappings.v["l"] then lsp_mappings.v["l"] = { desc = " " .. icons.ui.Note .. " LSP" } end if lsp_mappings.v["u"] then lsp_mappings.v["u"] = { desc = " " .. icons.ui.Note .. " LSP" } end end M.set_mappings(lsp_mappings, { buffer = bufnr }) end return M