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