init.lua

  1--- ### General utils.
  2--
  3--  DESCRIPTION:
  4--  General utility functions to use within Nvim.
  5
  6--    Functions:
  7--      -> run_cmd                  → Run a shell command and return true/false.
  8--      -> add_autocmds_to_buffer   → Add autocmds to a bufnr.
  9--      -> del_autocmds_from_buffer → Delete autocmds from a bufnr.
 10--      -> get_icon                 → Return an icon from the icons directory.
 11--      -> get_mappings_template    → Return a empty mappings table.
 12--      -> is_available             → Return true if the plugin exist.
 13--      -> is_big_file              → Return true if the file is too big.
 14--      -> notify                   → Send a notification with a default title.
 15--      -> os_path                  → Converts a path to the current OS.
 16--      -> get_plugin_opts          → Return a plugin opts table.
 17--      -> set_mappings             → Set a list of mappings in a clean way.
 18--      -> set_url_effect           → Show an effect for urls.
 19--      -> open_with_program        → Open the file or URL under the cursor.
 20--      -> trigger_event            → Manually trigger a event.
 21--      -> which_key_register       → When setting a mapping, add it to whichkey.
 22
 23
 24local M = {}
 25
 26--- Run a shell command and capture the output and if the command
 27--- succeeded or failed.
 28---@param cmd string|string[] The terminal command to execute
 29---@param show_error? boolean If true, print errors if the command fail.
 30---@return string|nil # The result of a successfully executed command or nil
 31function M.run_cmd(cmd, show_error)
 32  if type(cmd) == "string" then cmd = vim.split(cmd, " ") end
 33  if vim.fn.has "win32" == 1 then cmd = vim.list_extend({ "cmd.exe", "/C" }, cmd) end
 34  local result = vim.fn.system(cmd)
 35  local success = vim.api.nvim_get_vvar "shell_error" == 0
 36  if not success and (show_error == nil or show_error) then
 37    vim.api.nvim_err_writeln(("Error running command %s\nError message:\n%s"):format(table.concat(cmd, " "), result))
 38  end
 39  return success and result:gsub("[\27\155][][()#;?%d]*[A-PRZcf-ntqry=><~]", "") or nil
 40end
 41
 42--- Adds autocmds to a specific buffer if they don't already exist.
 43---
 44--- @param augroup string       The name of the autocmd group to which the autocmds belong.
 45--- @param bufnr number         The buffer number to which the autocmds should be applied.
 46--- @param autocmds table|any  A table or a single autocmd definition containing the autocmds to add.
 47function M.add_autocmds_to_buffer(augroup, bufnr, autocmds)
 48  -- Check if autocmds is a list, if not convert it to a list
 49  if not vim.islist(autocmds) then autocmds = { autocmds } end
 50
 51  -- Attempt to retrieve existing autocmds associated with the specified augroup and bufnr
 52  local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
 53
 54  -- If no existing autocmds are found or the cmds_found call fails
 55  if not cmds_found or vim.tbl_isempty(cmds) then
 56    -- Create a new augroup if it doesn't already exist
 57    vim.api.nvim_create_augroup(augroup, { clear = false })
 58
 59    -- Iterate over each autocmd provided
 60    for _, autocmd in ipairs(autocmds) do
 61      -- Extract the events from the autocmd and remove the events key
 62      local events = autocmd.events
 63      autocmd.events = nil
 64
 65      -- Set the group and buffer keys for the autocmd
 66      autocmd.group = augroup
 67      autocmd.buffer = bufnr
 68
 69      -- Create the autocmd
 70      vim.api.nvim_create_autocmd(events, autocmd)
 71    end
 72  end
 73end
 74
 75--- Deletes autocmds associated with a specific buffer and autocmd group.
 76---
 77--- @param augroup string  The name of the autocmd group from which the autocmds should be removed.
 78--- @param bufnr number    The buffer number from which the autocmds should be removed.
 79function M.del_autocmds_from_buffer(augroup, bufnr)
 80  -- Attempt to retrieve existing autocmds associated with the specified augroup and bufnr
 81  local cmds_found, cmds = pcall(vim.api.nvim_get_autocmds, { group = augroup, buffer = bufnr })
 82
 83  -- If retrieval was successful
 84  if cmds_found then
 85    -- Map over each retrieved autocmd and delete it
 86    vim.tbl_map(function(cmd) vim.api.nvim_del_autocmd(cmd.id) end, cmds)
 87  end
 88end
 89
 90--- Get an icon from `lspkind` if it is available and return it.
 91---@param kind string The kind of icon in `lspkind` to retrieve.
 92---@return string icon.
 93function M.get_icon(kind, padding, no_fallback)
 94  if not vim.g.icons_enabled and no_fallback then return "" end
 95  local icon_pack = vim.g.icons_enabled and "icons" or "text_icons"
 96  if not M[icon_pack] then
 97    M.icons = require("base.icons.nerd_font")
 98    M.text_icons = require("base.icons.text")
 99  end
100  local icon = M[icon_pack] and M[icon_pack][kind]
101  return icon and icon .. string.rep(" ", padding or 0) or ""
102end
103
104--- Get an empty table of mappings with a key for each map mode.
105---@return table<string,table> # a table with entries for each map mode.
106function M.get_mappings_template()
107  local maps = {}
108  for _, mode in ipairs { "", "n", "v", "x", "s", "o", "!", "i", "l", "c", "t" } do
109    maps[mode] = {}
110  end
111  if vim.fn.has "nvim-0.10.0" == 1 then
112    for _, abbr_mode in ipairs { "ia", "ca", "!a" } do
113      maps[abbr_mode] = {}
114    end
115  end
116  return maps
117end
118
119--- Check if a plugin is defined in lazy. Useful with lazy loading
120--- when a plugin is not necessarily loaded yet.
121---@param plugin string The plugin to search for.
122---@return boolean available # Whether the plugin is available.
123function M.is_available(plugin)
124  local lazy_config_avail, lazy_config = pcall(require, "lazy.core.config")
125  return lazy_config_avail and lazy_config.spec.plugins[plugin] ~= nil
126end
127
128--- Returns true if the file is considered a big file,
129--- according to the criteria defined in `vim.g.big_file`.
130---@param bufnr number|nil buffer number. 0 by default, which means current buf.
131---@return boolean is_big_file true or false.
132function M.is_big_file(bufnr)
133  if bufnr == nil then bufnr = 0 end
134  local filesize = vim.fn.getfsize(vim.api.nvim_buf_get_name(bufnr))
135  local nlines = vim.api.nvim_buf_line_count(bufnr)
136  local is_big_file = (filesize > vim.g.big_file.size)
137      or (nlines > vim.g.big_file.lines)
138  return is_big_file
139end
140
141--- Sends a notification with 'Neovim' as default title.
142--- Same as using vim.notify, but it saves us typing the title every time.
143---@param msg string The notification body.
144---@param type number|nil The type of the notification (:help vim.log.levels).
145---@param opts? table The nvim-notify options to use (:help notify-options).
146function M.notify(msg, type, opts)
147  vim.schedule(function()
148    vim.notify(
149      msg, type, vim.tbl_deep_extend("force", { title = "Neovim" }, opts or {}))
150  end)
151end
152
153--- Convert a path to the path format of the current operative system.
154--- It converts 'slash' to 'inverted slash' if on windows, and vice versa on UNIX.
155---@param path string A path string.
156---@return string|nil,nil path A path string formatted for the current OS.
157function M.os_path(path)
158  if path == nil then return nil end
159  -- Get the platform-specific path separator
160  local separator = string.sub(package.config, 1, 1)
161  return string.gsub(path, '[/\\]', separator)
162end
163
164--- Get the options of a plugin managed by lazy.
165---@param plugin string The plugin to get options from
166---@return table opts # The plugin options, or empty table if no plugin.
167function M.get_plugin_opts(plugin)
168  local lazy_config_avail, lazy_config = pcall(require, "lazy.core.config")
169  local lazy_plugin_avail, lazy_plugin = pcall(require, "lazy.core.plugin")
170  local opts = {}
171  if lazy_config_avail and lazy_plugin_avail then
172    local spec = lazy_config.spec.plugins[plugin]
173    if spec then opts = lazy_plugin.values(spec, "opts") end
174  end
175  return opts
176end
177
178--- Set a table of mappings.
179---
180--- This wrapper prevents a  boilerplate code, and takes care of `whichkey.nvim`.
181---@param map_table table A nested table where the first key is the vim mode,
182---                       the second key is the key to map, and the value is
183---                       the function to set the mapping to.
184---@param base? table A base set of options to set on every keybinding.
185function M.set_mappings(map_table, base)
186  -- iterate over the first keys for each mode
187  base = base or {}
188  for mode, maps in pairs(map_table) do
189    -- iterate over each keybinding set in the current mode
190    for keymap, options in pairs(maps) do
191      -- build the options for the command accordingly
192      if options then
193        local cmd = options
194        local keymap_opts = base
195        if type(options) == "table" then
196          cmd = options[1]
197          keymap_opts = vim.tbl_deep_extend("force", keymap_opts, options)
198          keymap_opts[1] = nil
199        end
200        if not cmd or keymap_opts.name then -- if which-key mapping, queue it
201          if not keymap_opts.name then keymap_opts.name = keymap_opts.desc end
202          if not M.which_key_queue then M.which_key_queue = {} end
203          if not M.which_key_queue[mode] then M.which_key_queue[mode] = {} end
204          M.which_key_queue[mode][keymap] = keymap_opts
205        else -- if not which-key mapping, set it
206          vim.keymap.set(mode, keymap, cmd, keymap_opts)
207        end
208      end
209    end
210  end
211  -- if which-key is loaded already, register
212  if package.loaded["which-key"] then M.which_key_register() end
213end
214
215--- Add syntax matching rules for highlighting URLs/URIs.
216function M.set_url_effect()
217  --- regex used for matching a valid URL/URI string
218  local url_matcher =
219      "\\v\\c%(%(h?ttps?|ftp|file|ssh|git)://|[a-z]+[@][a-z]+[.][a-z]+:)" ..
220      "%([&:#*@~%_\\-=?!+;/0-9a-z]+%(%([.;/?]|[.][.]+)" ..
221      "[&:#*@~%_\\-=?!+/0-9a-z]+|:\\d+|,%(%(%(h?ttps?|ftp|file|ssh|git)://|" ..
222      "[a-z]+[@][a-z]+[.][a-z]+:)@![0-9a-z]+))*|\\([&:#*@~%_\\-=?!+;/.0-9a-z]*\\)" ..
223      "|\\[[&:#*@~%_\\-=?!+;/.0-9a-z]*\\]|\\{%([&:#*@~%_\\-=?!+;/.0-9a-z]*" ..
224      "|\\{[&:#*@~%_\\-=?!+;/.0-9a-z]*})\\})+"
225
226  M.delete_url_effect()
227  if vim.g.url_effect_enabled then
228    vim.fn.matchadd("HighlightURL", url_matcher, 15)
229  end
230end
231
232--- Delete the syntax matching rules for URLs/URIs if set.
233function M.delete_url_effect()
234  for _, match in ipairs(vim.fn.getmatches()) do
235    if match.group == "HighlightURL" then vim.fn.matchdelete(match.id) end
236  end
237end
238
239--- Open the file or url under the cursor.
240---@param path string The path of the file to open with the system opener.
241function M.open_with_program(path)
242  if vim.ui.open then return vim.ui.open(path) end
243  local cmd
244  if vim.fn.has "mac" == 1 then
245    cmd = { "open" }
246  elseif vim.fn.has "win32" == 1 then
247    if vim.fn.executable "rundll32" then
248      cmd = { "rundll32", "url.dll,FileProtocolHandler" }
249    else
250      cmd = { "cmd.exe", "/K", "explorer" }
251    end
252  elseif vim.fn.has "unix" == 1 then
253    if vim.fn.executable "explorer.exe" == 1 then -- available in WSL
254      cmd = { "explorer.exe" }
255    elseif vim.fn.executable "xdg-open" == 1 then
256      cmd = { "xdg-open" }
257    end
258  end
259  if not cmd then M.notify("Available system opening tool not found!", vim.log.levels.ERROR) end
260  if not path then
261    path = vim.fn.expand "<cfile>"
262  elseif not path:match "%w+:" then
263    path = vim.fn.expand(path)
264  end
265  vim.fn.jobstart(vim.list_extend(cmd, { path }), { detach = true })
266end
267
268--- Convenient wapper to save code when we Trigger events.
269--- To listen for a event triggered by this function you can use `autocmd`.
270---@param event string Name of the event.
271---@param is_urgent boolean|nil If true, trigger directly instead of scheduling. Useful for startup events.
272-- @usage To run a User event:   `trigger_event("User MyUserEvent")`
273-- @usage To run a Neovim event: `trigger_event("BufEnter")
274function M.trigger_event(event, is_urgent)
275  -- define behavior
276  local function trigger()
277    local is_user_event = string.match(event, "^User ") ~= nil
278    if is_user_event then
279      event = event:gsub("^User ", "")
280      vim.api.nvim_exec_autocmds("User", { pattern = event, modeline = false })
281    else
282      vim.api.nvim_exec_autocmds(event, { modeline = false })
283    end
284  end
285
286  -- execute
287  if is_urgent then
288    trigger()
289  else
290    vim.schedule(trigger)
291  end
292end
293
294--- Register queued which-key mappings.
295function M.which_key_register()
296  if M.which_key_queue then
297    local wk_avail, wk = pcall(require, "which-key")
298    if wk_avail then
299      for mode, registration in pairs(M.which_key_queue) do
300        wk.register(registration, { mode = mode })
301      end
302      M.which_key_queue = nil
303    end
304  end
305end
306
307return M