422 lines
17 KiB
Lua
422 lines
17 KiB
Lua
--- ### General utils.
|
|
|
|
-- DESCRIPTION:
|
|
-- General utility functions to use within Nvim.
|
|
|
|
-- Functions:
|
|
-- -> run_cmd → Run a shell command and return true/false.
|
|
-- -> add_autocmds_to_buffer → Add autocmds to a bufnr.
|
|
-- -> apply_default_lsp_settings → Apply Default LSP settings.
|
|
-- -> apply_user_lsp_mappings → Apply user lsp mappings to a lsp client.
|
|
-- -> del_autocmds_from_buffer → Delete autocmds from a bufnr.
|
|
-- -> get_icon → Return an icon from the icons directory.
|
|
-- -> get_mappings_template → Return a empty mappings table.
|
|
-- -> is_available → Return true if the plugin exist.
|
|
-- -> is_big_file → Return true if the file is too big.
|
|
-- -> notify → Send a notification with a default title.
|
|
-- -> os_path → Converts a path to the current OS.
|
|
-- -> get_plugin_opts → Return a plugin opts table.
|
|
-- -> set_mappings → Set a list of mappings in a clean way.
|
|
-- -> set_url_effect → Show an effect for urls.
|
|
-- -> open_with_program → Open the file or URL under the cursor.
|
|
-- -> trigger_event → Manually trigger an event.
|
|
-- -> which_key_register → When setting a mapping, add it to whichkey.
|
|
|
|
|
|
local M = {}
|
|
|
|
--- Run a shell command and capture the output and whether the command
|
|
--- succeeded or failed.
|
|
--- @param cmd string|string[] The terminal command to execute.
|
|
--- @param show_error? boolean If true, print errors if the command fails.
|
|
--- @return string|nil # The result of a successfully executed command, or nil if it failed.
|
|
function M.run_cmd(cmd, show_error)
|
|
-- Split cmd string into a list, if needed.
|
|
if type(cmd) == "string" then
|
|
cmd = vim.split(cmd, " ")
|
|
end
|
|
|
|
-- If windows, and prepend cmd.exe
|
|
if vim.fn.has("win32") == 1 then
|
|
cmd = vim.list_extend({ "cmd.exe", "/C" }, cmd)
|
|
end
|
|
|
|
-- Execute cmd and store result (output or error message)
|
|
local result = vim.fn.system(cmd)
|
|
local success = vim.api.nvim_get_vvar("shell_error") == 0
|
|
|
|
-- If the command failed and show_error is true or not provided, print error.
|
|
if not success and (show_error == nil or show_error) then
|
|
vim.api.nvim_echo({{
|
|
("Error running command %s\nError message:\n%s"):format(
|
|
table.concat(cmd, " "), -- Convert the cmd back to string.
|
|
result -- Show the error result
|
|
)}}, true, { err = true }
|
|
)
|
|
end
|
|
|
|
-- strip out terminal escape sequences and control characters.
|
|
local cleaned_result = result:gsub("[\27\155][][()#;?%d]*[A-PRZcf-ntqry=><~]", "")
|
|
|
|
-- Return the cleaned result if the command succeeded, or nil if it failed
|
|
return (success and cleaned_result) or nil
|
|
end
|
|
|
|
--- Adds autocmds to a specific buffer if they don't already exist.
|
|
---
|
|
--- @param augroup string The name of the autocmd group to which the autocmds belong.
|
|
--- @param bufnr number The buffer number to which the autocmds should be applied.
|
|
--- @param autocmds table|any A table or a single autocmd definition containing the autocmds to add.
|
|
function M.add_autocmds_to_buffer(augroup, bufnr, autocmds)
|
|
-- Check if autocmds is a list, if not convert it to a list
|
|
if not vim.islist(autocmds) then autocmds = { autocmds } end
|
|
|
|
-- Attempt to retrieve existing autocmds associated with the specified augroup and bufnr
|
|
local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
|
|
|
|
-- If no existing autocmds are found or the cmds_found call fails
|
|
if not cmds_found or vim.tbl_isempty(cmds) then
|
|
-- Create a new augroup if it doesn't already exist
|
|
vim.api.nvim_create_augroup(augroup, { clear = false })
|
|
|
|
-- Iterate over each autocmd provided
|
|
for _, autocmd in ipairs(autocmds) do
|
|
-- Extract the events from the autocmd and remove the events key
|
|
local events = autocmd.events
|
|
autocmd.events = nil
|
|
|
|
-- Set the group and buffer keys for the autocmd
|
|
autocmd.group = augroup
|
|
autocmd.buffer = bufnr
|
|
|
|
-- Create the autocmd
|
|
vim.api.nvim_create_autocmd(events, autocmd)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- This function define how LSP diagnostics will look.
|
|
M.apply_lsp_diagnostic_defaults = function()
|
|
|
|
-- Define LSP diagnostics icons defined in ../icons/icons.lua
|
|
local signs = {
|
|
{ name = "DiagnosticSignError", text = M.get_icon("DiagnosticError"), texthl = "DiagnosticSignError" },
|
|
{ name = "DiagnosticSignWarn", text = M.get_icon("DiagnosticWarn"), texthl = "DiagnosticSignWarn" },
|
|
{ name = "DiagnosticSignHint", text = M.get_icon("DiagnosticHint"), texthl = "DiagnosticSignHint" },
|
|
{ name = "DiagnosticSignInfo", text = M.get_icon("DiagnosticInfo"), texthl = "DiagnosticSignInfo" },
|
|
{ name = "DapStopped", text = M.get_icon("DapStopped"), texthl = "DiagnosticWarn" },
|
|
{ name = "DapBreakpoint", text = M.get_icon("DapBreakpoint"), texthl = "DiagnosticInfo" },
|
|
{ name = "DapBreakpointRejected", text = M.get_icon("DapBreakpointRejected"), texthl = "DiagnosticError" },
|
|
{ name = "DapBreakpointCondition", text = M.get_icon("DapBreakpointCondition"), texthl = "DiagnosticInfo" },
|
|
{ name = "DapLogPoint", text = M.get_icon("DapLogPoint"), texthl = "DiagnosticInfo" }
|
|
}
|
|
for _, sign in ipairs(signs) do
|
|
vim.fn.sign_define(sign.name, sign)
|
|
end
|
|
|
|
-- Define diagnostic opts
|
|
local diagnostics_opts = {
|
|
virtual_text = true,
|
|
signs = {
|
|
text = {
|
|
[vim.diagnostic.severity.ERROR] = M.get_icon("DiagnosticError"),
|
|
[vim.diagnostic.severity.HINT] = M.get_icon("DiagnosticHint"),
|
|
[vim.diagnostic.severity.WARN] = M.get_icon("DiagnosticWarn"),
|
|
[vim.diagnostic.severity.INFO] = M.get_icon("DiagnosticInfo"),
|
|
},
|
|
active = signs,
|
|
},
|
|
update_in_insert = true,
|
|
underline = true,
|
|
severity_sort = true,
|
|
float = {
|
|
focused = false,
|
|
style = "minimal",
|
|
border = "rounded",
|
|
source = "always",
|
|
header = "",
|
|
prefix = "",
|
|
},
|
|
}
|
|
|
|
-- Define the table of options used by vim.g.diagnostics_mode in ../1-options.lua
|
|
M.diagnostics_enum = {
|
|
-- diagnostics off
|
|
[0] = vim.tbl_deep_extend(
|
|
"force",
|
|
diagnostics_opts,
|
|
{ underline = false, virtual_text = false, signs = false, update_in_insert = false }
|
|
),
|
|
-- status only
|
|
vim.tbl_deep_extend("force", diagnostics_opts, { virtual_text = false, signs = false }),
|
|
-- virtual text off, signs on
|
|
vim.tbl_deep_extend("force", diagnostics_opts, { virtual_text = false }),
|
|
-- all diagnostics on
|
|
diagnostics_opts,
|
|
}
|
|
|
|
-- Apply the settings defined in this function
|
|
vim.diagnostic.config(M.diagnostics_enum[vim.g.diagnostics_mode])
|
|
end
|
|
|
|
--- Applies the user lsp mappings to a lsp client.
|
|
--- This function must be called every time a lsp client is attached.
|
|
--- (currently on the config of the plugin `lspconfig`)
|
|
--- @param client string The lsp client to attach the lsp mappings to.
|
|
--- @param bufnr number The bufnr to attach the lsp mappings to.
|
|
function M.apply_user_lsp_mappings(client, bufnr)
|
|
local lsp_mappings = require("base.4-mappings").lsp_mappings(client, bufnr)
|
|
if not vim.tbl_isempty(lsp_mappings.v) then
|
|
lsp_mappings.v["<leader>l"] = { desc = M.get_icon("ActiveLSP", true) .. "LSP" }
|
|
end
|
|
M.set_mappings(lsp_mappings, { buffer = bufnr })
|
|
end
|
|
|
|
--- Deletes autocmds associated with a specific buffer and autocmd group.
|
|
--- @param augroup string The name of the autocmd group from which the autocmds should be removed.
|
|
--- @param bufnr number The buffer number from which the autocmds should be removed.
|
|
function M.del_autocmds_from_buffer(augroup, bufnr)
|
|
-- Attempt to retrieve existing autocmds associated with the specified augroup and bufnr
|
|
local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
|
|
|
|
-- If retrieval was successful
|
|
if cmds_found then
|
|
-- Map over each retrieved autocmd and delete it
|
|
vim.tbl_map(function(cmd) vim.api.nvim_del_autocmd(cmd.id) end, cmds)
|
|
end
|
|
end
|
|
|
|
--- Get an icon from given its icon name.
|
|
--- if vim.g.fallback_icons_enabled = true, it will return a fallback icon
|
|
--- unless specified otherwise.
|
|
--- @param icon_name string Name of the icon to retrieve.
|
|
--- @param fallback_to_empty_string boolean|nil If this parameter is true, when `vim.g.fallback_icons_enabled = true` then `get_icon()` will return empty string.
|
|
--- @return string icon.
|
|
function M.get_icon(icon_name, fallback_to_empty_string)
|
|
-- guard clause
|
|
if fallback_to_empty_string and vim.g.fallback_icons_enabled then return "" end
|
|
|
|
-- get icon_pack
|
|
local icon_pack = (vim.g.fallback_icons_enabled and "fallback_icons") or "icons"
|
|
|
|
-- cache icon_pack into M
|
|
if not M[icon_pack] then -- only if not cached already.
|
|
if icon_pack == "icons" then
|
|
M.icons = require("base.icons.icons")
|
|
elseif icon_pack =="fallback_icons" then
|
|
M.fallback_icons = require("base.icons.fallback_icons")
|
|
end
|
|
end
|
|
|
|
-- return specified icon
|
|
local icon = M[icon_pack] and M[icon_pack][icon_name]
|
|
return icon
|
|
end
|
|
|
|
--- Get an empty table of mappings with a key for each map mode.
|
|
--- @return table<string,table> # a table with entries for each map mode.
|
|
function M.get_mappings_template()
|
|
local maps = {}
|
|
for _, mode in ipairs {
|
|
"", "n", "v", "x", "s", "o", "!", "i", "l", "c", "t", "ia", "ca", "!a"
|
|
} do maps[mode] = {} end
|
|
return maps
|
|
end
|
|
|
|
--- Check if a plugin is defined in lazy. Useful with lazy loading
|
|
--- when a plugin is not necessarily loaded yet.
|
|
--- @param plugin string The plugin to search for.
|
|
--- @return boolean available # Whether the plugin is available.
|
|
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
|
|
|
|
--- Returns true if the file is considered a big file,
|
|
--- according to the criteria defined in `vim.g.big_file`.
|
|
--- @param bufnr number|nil buffer number. 0 by default, which means current buf.
|
|
--- @return boolean is_big_file true or false.
|
|
function M.is_big_file(bufnr)
|
|
if bufnr == nil then bufnr = 0 end
|
|
local filesize = vim.fn.getfsize(vim.api.nvim_buf_get_name(bufnr))
|
|
local nlines = vim.api.nvim_buf_line_count(bufnr)
|
|
local is_big_file = (filesize > vim.g.big_file.size)
|
|
or (nlines > vim.g.big_file.lines)
|
|
return is_big_file
|
|
end
|
|
|
|
--- Sends a notification with 'Neovim' as default title.
|
|
--- Same as using vim.notify, but it saves us typing the title every time.
|
|
--- @param msg string The notification body.
|
|
--- @param type number|nil The type of the notification (:help vim.log.levels).
|
|
--- @param opts? table The nvim-notify options to use (:help notify-options).
|
|
function M.notify(msg, type, opts)
|
|
vim.schedule(function()
|
|
vim.notify(
|
|
msg, type, vim.tbl_deep_extend("force", { title = "Neovim" }, opts or {}))
|
|
end)
|
|
end
|
|
|
|
--- Convert a path to the path format of the current operative system.
|
|
--- It converts 'slash' to 'inverted slash' if on windows, and vice versa on UNIX.
|
|
--- @param path string A path string.
|
|
--- @return string|nil,nil path A path string formatted for the current OS.
|
|
function M.os_path(path)
|
|
if path == nil then return nil end
|
|
-- Get the platform-specific path separator
|
|
local separator = string.sub(package.config, 1, 1)
|
|
return string.gsub(path, '[/\\]', separator)
|
|
end
|
|
|
|
--- Get the options of a plugin managed by lazy.
|
|
--- @param plugin string The plugin to get options from
|
|
--- @return table opts # The plugin options, or empty table if no plugin.
|
|
function M.get_plugin_opts(plugin)
|
|
local lazy_config_avail, lazy_config = pcall(require, "lazy.core.config")
|
|
local lazy_plugin_avail, lazy_plugin = pcall(require, "lazy.core.plugin")
|
|
local opts = {}
|
|
if lazy_config_avail and lazy_plugin_avail then
|
|
local spec = lazy_config.spec.plugins[plugin]
|
|
if spec then opts = lazy_plugin.values(spec, "opts") end
|
|
end
|
|
return opts
|
|
end
|
|
|
|
--- Set a table of mappings.
|
|
--- This wrapper prevents boilerplate code, and takes care of `whichkey.nvim`.
|
|
--- @param map_table table A nested table where the first key is the vim mode,
|
|
--- the second key is the key to map, and the value is
|
|
--- the function to set the mapping to.
|
|
--- @param base? table A base set of options to set on every keybinding.
|
|
function M.set_mappings(map_table, base)
|
|
-- iterate over the first keys for each mode
|
|
for mode, maps in pairs(map_table) do
|
|
-- iterate over each keybinding set in the current mode
|
|
for keymap, options in pairs(maps) do
|
|
-- build the options for the command accordingly
|
|
if options then
|
|
local cmd
|
|
local keymap_opts = base or {}
|
|
if type(options) == "string" or type(options) == "function" then
|
|
cmd = options
|
|
else
|
|
cmd = options[1]
|
|
keymap_opts = vim.tbl_deep_extend("force", keymap_opts, options)
|
|
keymap_opts[1] = nil
|
|
end
|
|
if not cmd then -- if which-key mapping, queue it
|
|
keymap_opts[1], keymap_opts.mode = keymap, mode
|
|
if not keymap_opts.group then keymap_opts.group = keymap_opts.desc end
|
|
if not M.which_key_queue then M.which_key_queue = {} end
|
|
table.insert(M.which_key_queue, keymap_opts)
|
|
else -- if not which-key mapping, set it
|
|
vim.keymap.set(mode, keymap, cmd, keymap_opts)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- if which-key is loaded already, register
|
|
if package.loaded["which-key"] then M.which_key_register() end
|
|
end
|
|
|
|
|
|
--- Add syntax matching rules for highlighting URLs/URIs.
|
|
function M.set_url_effect()
|
|
--- regex used for matching a valid URL/URI string
|
|
local url_matcher =
|
|
"\\v\\c%(%(h?ttps?|ftp|file|ssh|git)://|[a-z]+[@][a-z]+[.][a-z]+:)" ..
|
|
"%([&:#*@~%_\\-=?!+;/0-9a-z]+%(%([.;/?]|[.][.]+)" ..
|
|
"[&:#*@~%_\\-=?!+/0-9a-z]+|:\\d+|,%(%(%(h?ttps?|ftp|file|ssh|git)://|" ..
|
|
"[a-z]+[@][a-z]+[.][a-z]+:)@![0-9a-z]+))*|\\([&:#*@~%_\\-=?!+;/.0-9a-z]*\\)" ..
|
|
"|\\[[&:#*@~%_\\-=?!+;/.0-9a-z]*\\]|\\{%([&:#*@~%_\\-=?!+;/.0-9a-z]*" ..
|
|
"|\\{[&:#*@~%_\\-=?!+;/.0-9a-z]*})\\})+"
|
|
|
|
M.delete_url_effect()
|
|
if vim.g.url_effect_enabled then
|
|
vim.fn.matchadd("HighlightURL", url_matcher, 15)
|
|
end
|
|
end
|
|
|
|
--- Delete the syntax matching rules for URLs/URIs if set.
|
|
function M.delete_url_effect()
|
|
for _, match in ipairs(vim.fn.getmatches()) do
|
|
if match.group == "HighlightURL" then vim.fn.matchdelete(match.id) end
|
|
end
|
|
end
|
|
|
|
--- Open the file or url under the cursor.
|
|
--- @param path string The path of the file to open with the system opener.
|
|
function M.open_with_program(path)
|
|
-- guard clause: If a opener already exists, use it.
|
|
if vim.ui.open then return vim.ui.open(path) end
|
|
|
|
-- command to run
|
|
local cmd
|
|
|
|
-- cmd is different depending the OS
|
|
if vim.fn.has("mac") == 1 then
|
|
cmd = { "open" }
|
|
elseif vim.fn.has("win32") == 1 then
|
|
if vim.fn.executable "rundll32" then
|
|
cmd = { "rundll32", "url.dll,FileProtocolHandler" }
|
|
else
|
|
cmd = { "cmd.exe", "/K", "explorer" }
|
|
end
|
|
elseif vim.fn.has("unix") == 1 then
|
|
if vim.fn.executable("explorer.exe") == 1 then -- available in WSL
|
|
cmd = { "explorer.exe" }
|
|
elseif vim.fn.executable("xdg-open") == 1 then
|
|
cmd = { "xdg-open" }
|
|
end
|
|
end
|
|
if not cmd then M.notify("Available system opening tool not found!", vim.log.levels.ERROR) end
|
|
|
|
-- No path provided? use the file under the cursor; else, expand the path.
|
|
if not path then
|
|
path = vim.fn.expand("<cfile>")
|
|
elseif not path:match "%w+:" then
|
|
path = vim.fn.expand(path)
|
|
end
|
|
|
|
-- start job (detached)
|
|
vim.fn.jobstart(vim.list_extend(cmd, { path }), { detach = true })
|
|
end
|
|
|
|
--- Convenient wapper to save code when we Trigger events.
|
|
--- To listen for an event triggered by this function you can use `autocmd`.
|
|
--- @param event string Name of the event.
|
|
--- @param is_urgent boolean|nil If true, trigger directly instead of scheduling. Useful for startup events.
|
|
-- @usage To run a User event: `trigger_event("User MyUserEvent")`
|
|
-- @usage To run a Neovim event: `trigger_event("BufEnter")
|
|
function M.trigger_event(event, is_urgent)
|
|
-- define behavior
|
|
local function trigger()
|
|
local is_user_event = string.match(event, "^User ") ~= nil
|
|
if is_user_event then
|
|
event = event:gsub("^User ", "")
|
|
vim.api.nvim_exec_autocmds("User", { pattern = event, modeline = false })
|
|
else
|
|
vim.api.nvim_exec_autocmds(event, { modeline = false })
|
|
end
|
|
end
|
|
|
|
-- execute
|
|
if is_urgent then
|
|
trigger()
|
|
else
|
|
vim.schedule(trigger)
|
|
end
|
|
end
|
|
|
|
--- Register queued which-key mappings.
|
|
function M.which_key_register()
|
|
if M.which_key_queue then
|
|
local wk_avail, wk = pcall(require, "which-key")
|
|
if wk_avail then
|
|
wk.add(M.which_key_queue)
|
|
M.which_key_queue = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
return M
|