Files
neovim-config/lua/data/func.lua
T

2424 lines
76 KiB
Lua

---@module "data.func"
--- This module contains utility functions used throughout the configuration.
-- ╭─────────────────────────────────────────────────────────╮
-- │ Utility Functions │
-- ╰─────────────────────────────────────────────────────────╯
-- Define a namespace for utility functions
local M = {}
---@function Check if the terminal is kitty
---@return boolean condition true if the terminal is kitty, false otherwise
function M.is_kitty()
local term = os.getenv("TERM") or ""
local kit = string.find(term, "kitty")
return kit ~= nil
end
---@function Check if using kitty-scrollback
---@return boolean condition true if using kitty-scrollback, false otherwise
function M.is_kitty_scrollback()
if vim.env.KITTY_SCROLLBACK_NVIM == "true" then
return true
end
return false
end
---@function Check if the terminal is alacritty
---@return boolean condition true if the terminal is alacritty, false otherwise
function M.is_alacritty()
local term = os.getenv("TERM") or ""
local alc = string.find(term, "alacritty")
return alc ~= nil
end
---@function Check if the terminal is tmux
---@return boolean condition true if the terminal is tmux, false otherwise
function M.is_tmux()
local tterm = os.getenv("TERM")
if tterm and string.find(tterm, "screen") then
if os.getenv("TMUX") then
return true
end
else
if tterm and string.find(tterm, "tmux") then
return true
end
end
return false
end
---@function Check if the terminal is wezterm
---@return boolean condition true if the terminal is wezterm, false otherwise
function M.is_wezterm()
local wterm = os.getenv("TERM_PROGRAM")
if wterm and string.find(wterm, "WezTerm") then
return true
end
return false
end
---@function Check if the terminal is neovide
---@return boolean condition true if the terminal is neovide, false otherwise
function M.is_neovide()
local neovide = vim.g.neovide
if neovide then
return true
end
return false
end
---@function Check if the terminal is ssh
---@return boolean condition true if the terminal is ssh, false otherwise
function M.is_ssh()
local ssh = os.getenv("SSH_TTY") or false
if ssh then
return true
end
return false
end
---@function Check if OS is Windows
---@return boolean condition true if the OS is Windows, false otherwise
function M.is_windows()
local win = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
if win then
return true
end
return false
end
---@function Check if OS is macOS
---@return boolean condition true if the OS is macOS, false otherwise
function M.is_mac()
local mac = vim.fn.has("macunix")
if mac == 1 then
return true
end
return false
end
---@function Check if OS is Linux
---@return boolean condition true if the OS is Linux, false otherwise
function M.is_linux()
local lin = vim.fn.has("unix")
if lin == 1 then
return true
end
return false
end
---@function Get the name of the OS.
---@param format string The format of the OS name.
--- Possible values:
--- - "verbose": Returns the full name of the OS.
--- - "short": Returns a short name or abbreviation.
--- - "code": Returns a code or identifier.
--- - "platform": Returns either "windows", "osx", or "linux".
---@return string os The name of the OS.
function M.get_os(format)
---@diagnostic disable-next-line: undefined-field
local uname = vim.loop and vim.loop.os_uname and vim.loop.os_uname() or {}
local os_name = uname.sysname or "unknown"
if os_name == "Windows_NT" then
if format == "platform" then
return "windows"
elseif format == "short" then
return "Win"
elseif format == "code" then
return "win"
else
return "Windows"
end
elseif os_name == "Darwin" then
if format == "platform" then
return "osx"
elseif format == "short" then
return "macOS"
elseif format == "code" then
return "osx"
else
return "macOS"
end
elseif os_name == "Linux" then
-- Check if the system is running Android
if vim.env.ANDROID_ROOT then
if format == "platform" then
return "linux"
elseif format == "short" then
return "Android"
elseif format == "code" then
return "android"
else
return "Android"
end
end
-- Determine the Linux distribution
local distro = "Linux"
local release_file = "/etc/os-release"
local fd = io.open(release_file, "r")
if fd then
for line in fd:lines() do
if line:match("^ID=") then
distro = line:gsub("ID=", ""):gsub('"', "")
break
end
end
fd:close()
end
if format == "platform" then
return "linux"
elseif format == "short" then
return "Linux"
elseif format == "code" then
return distro
else
return "Linux (" .. distro .. ")"
end
else
-- Falback tests
if M.is_mac() then
if format == "platform" then
return "osx"
elseif format == "short" then
return "macOS"
elseif format == "code" then
return "osx"
else
return "macOS"
end
elseif M.is_linux() then
if format == "platform" then
return "linux"
elseif format == "short" then
return "Linux"
elseif format == "code" then
return "linux"
else
return "Linux"
end
elseif M.is_windows() then
if format == "platform" then
return "windows"
elseif format == "short" then
return "Win"
elseif format == "code" then
return "win"
else
return "Windows"
end
else
-- Failed to determine OS
return "Unknown OS"
end
end
end
---@function Function to check if CWD is a git repo
---@param dir? string The directory to check, defaults to CWD
---@return boolean is_git_repo true if working directory is a git repository, false otherwise
function M.dir_is_git_repo(dir)
-- Default to the current working directory if no directory is specified
dir = dir or vim.fn.getcwd()
local result = vim
.system({ "git", "-C", dir, "rev-parse", "--is-inside-work-tree" })
:wait()
return result.code == 0 -- returns true if the command succeeded
end
---@function Function to send a notification
---@param message string The message to send
---@param level string|nil The level of the notification (default: "info")
---@param title string|nil The title of the notification
---@return boolean condition true if the notification was sent successfully, false otherwise
function M.notify(message, level, title)
level = level or "info"
if title then
vim.notify(message, vim.log.levels[level:upper()], { title = title })
else
vim.notify(message, vim.log.levels[level:upper()])
end
return true
end
---@function Function to generate a y/n confirmation prompt
---@param prompt string The prompt text to display
---@param action function|string The action to execute if the user confirms the prompt, or a Vim command as a string
---@return boolean condition true if the user confirms the prompt, false otherwise
function M.ConfirmPrompt(prompt, action)
-- Validate the action parameter
local function perform_action()
if type(action) == "function" then
action() -- Call the function
elseif type(action) == "string" then
vim.cmd(action) -- Run the Vim command
else
M.notify(
"Action must be a function or a string",
"ERROR",
"Configuration Error"
)
end
end
-- Create a new buffer
local buf = vim.api.nvim_create_buf(false, true) -- Create a new empty buffer
-- Set the prompt text in the buffer
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { prompt, "y/n: " })
-- Create a floating window to display the buffer
local win_height = 2 -- Height of floating window
local win_width = math.floor(vim.o.columns * 0.25) -- Width of floating window
local row = math.floor((vim.o.lines - win_height) / 2) -- Position row
local col = math.floor((vim.o.columns - win_width) / 2) -- Position column
local win_border = "rounded"
local style = "minimal"
-- Create a floating window
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
width = win_width,
height = win_height,
col = col,
row = row,
style = style,
border = win_border,
})
-- Move the cursor to the end of the buffer
vim.api.nvim_win_set_cursor(win, { 2, 5 })
-- Define the yes function
local yes = function()
vim.api.nvim_win_close(win, true)
perform_action() -- Perform the action
return true
end
-- Define the no function
local no = function()
vim.api.nvim_win_close(win, true)
M.notify("Action Canceled", "INFO", "Info")
end
-- Define buffer-specific key mappings
local keymaps = {
y = function()
yes()
end,
n = function()
no()
end,
q = function()
no()
end,
["<Esc>"] = function()
no()
end,
}
-- Set the key mappings
for key, callback in pairs(keymaps) do
vim.api.nvim_buf_set_keymap(buf, "n", key, "", {
noremap = true,
nowait = true,
callback = callback,
})
end
return false
end
---@function Function to generate an input prompt
---@param prompt string The prompt text to display
---@param callback function The function to call with the user input
function M.InputPrompt(prompt, callback)
-- Create a new buffer
local buf = vim.api.nvim_create_buf(false, true) -- Create a new empty buffer
-- Set the buffer name
vim.api.nvim_buf_set_name(buf, "Input")
-- Set the buffer filetype (e.g., for custom behavior or syntax highlighting)
vim.bo[buf].filetype = "input"
-- Set the buffer type to "nofile" to avoid editing or saving the buffer
vim.bo[buf].buftype = "nofile"
-- Set the prompt text in the buffer
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { prompt, "Input: " })
-- Create a floating window to display the buffer
local win_height = 2 -- Height of floating window
local win_width = math.floor(vim.o.columns * 0.25) -- Width of floating window
local row = math.floor((vim.o.lines - win_height) / 2) -- Position row
local col = math.floor((vim.o.columns - win_width) / 2) -- Position column
local win_border = "rounded"
local style = "minimal"
-- Create a floating window
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
width = win_width,
height = win_height,
col = col,
row = row,
style = style,
border = win_border,
})
-- Move the cursor to the end of the buffer
vim.api.nvim_win_set_cursor(win, { 2, 8 })
-- Set input mode
vim.api.nvim_command("startinsert")
-- Function to close the window
local function exit_win()
vim.api.nvim_command("stopinsert")
vim.api.nvim_win_close(win, true)
end
-- Function to handle input and close the window
local function handle_input()
-- Get the text after the "Input: " string
local input = vim.api.nvim_buf_get_lines(buf, 1, 2, false)[1]:sub(7) -- Adjust to trim "Input: "
return input
end
---@function Function to return user input
local function yes()
local user_input = handle_input() -- Get the input from the buffer
exit_win()
if callback then
callback(user_input) -- Call the callback with the user input
end
end
---@function Function to cancel user input
local function no()
exit_win()
if callback then
callback(nil) -- Call the callback with nil to indicate cancellation
end
end
-- Define buffer-specific key mappings
local keymaps = {
["<CR>"] = yes,
["<Esc>"] = no,
}
-- Set the key mappings
for key, keyback in pairs(keymaps) do
vim.api.nvim_buf_set_keymap(buf, "n", key, "", {
noremap = true,
nowait = true,
callback = keyback,
})
vim.api.nvim_buf_set_keymap(buf, "i", key, "", {
noremap = true,
nowait = true,
callback = keyback,
})
end
end
---@function Function to move blocks of text up and down
---@param up? boolean Whether to move the blocks up or down
--- Ex: Move selected block of text up: move_visual(true)
--- Move selected block of text down: move_visual(false)
---@param multiplier? integer The number of times to move the blocks
--- Ex: Move selected block of text up 3 times: move_visual(true, 3)
--- Move selected block of text down 2 times: move_visual(false, 2)
---@return nil
function M.move_visual(up, multiplier)
-- Handle parameter
local offset = 1
if up then
offset = -2
end
if multiplier then
offset = offset * multiplier
end
-- escape visual mode
vim.cmd("norm v")
-- GET REGION
-- eg region = { [103] = {0,-1}, [104] = {0,-1}, [105] = {0,-1}}
---@diagnostic disable-next-line: deprecated
local region = vim.region(0, "'<", "'>", vim.fn.visualmode(), true)
local function array_keys(array)
local keys = {}
for k in pairs(array) do
table.insert(keys, k)
end
return keys
end
-- GET LINES
-- eg lines = { 103, 104, 105 }
local lines = array_keys(region)
table.sort(lines)
-- GET TOP/BOTTOM LINE NUMBERS
local top = lines[1] + 1
local bottom = lines[#lines] + 1
-- EXECUTE
local new_pos = offset > 0 and bottom + offset or top + offset
vim.cmd(string.format("silent %d, %d move %d", top, bottom, new_pos))
-- eg :silent 104, 106 move 107
vim.cmd("norm gv")
end
---@function Function to paste over text with overwrite
---@return nil
function M.paste_overwrite()
-- Get the register contents and type
local register = vim.fn.getreginfo(vim.v.register or '"')
local regcontents = register.regcontents
-- Enter Virtual Replace Mode
vim.api.nvim_feedkeys("gR", "n", false)
-- Process each line in the register contents
for i, line in ipairs(regcontents) do
if i > 1 then
-- Handle formatting of multi-line pastes (except for the first line)
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>0gR", true, false, true),
"n",
false
)
end
-- Paste the current line; add a newline if it's not the last line
if i < #regcontents then
vim.api.nvim_feedkeys(line .. "\n", "n", false)
else
vim.api.nvim_feedkeys(line, "n", false)
end
end
-- Properly exit Virtual Replace Mode using <Esc>
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
"n",
false
)
end
---@function Function to dump the current buffer to a register and format as a Lua table
---@param register string? The name of the register to dump the buffer to (defaults to unnamed register if nil)
---@return nil
function M.dump_buffer_to_table(register)
register = register or "" -- Default to unnamed register if not provided
-- Get the lines from the current buffer
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
-- Process the buffer contents
local result = {}
for _, line in ipairs(lines) do
local entry = line:gsub("%.lua$", "") -- Remove ".lua" extension
table.insert(result, '"' .. entry .. '"')
end
-- Join the result into a single string and format as a Lua table
local output = "{ " .. table.concat(result, ", ") .. " }"
-- Dump the result into a register
vim.fn.setreg(register, output)
end
---@function Function to reload the user's Neovim configuration
---@return nil
function M.reload_config()
-- Notify the user about the reload process
M.notify("Reloading configuration...", "WARN", "Reloading Config")
-- 1. Save all buffers
vim.cmd("silent! wa")
-- 2. Record the list of open buffers to reopen later
local buffer_list = vim.api.nvim_list_bufs()
local buffers_to_reopen = {}
for _, buf in ipairs(buffer_list) do
if vim.api.nvim_buf_is_loaded(buf) and vim.fn.bufname(buf) ~= "" then
table.insert(
buffers_to_reopen,
{ buf = buf, file = vim.api.nvim_buf_get_name(buf) }
)
end
end
-- 3. Close all buffers
vim.cmd("silent! bufdo! bwipeout")
-- 4. Clear loaded lua modules related to your custom configuration
for name, _ in pairs(package.loaded) do
-- Replace 'userconfig' and 'plugin' with your actual config module names
if name:match("^userconfig") or name:match("^plugins") then
package.loaded[name] = nil
end
end
-- 5. Reload the vim script (init.lua)
vim.cmd("source $MYVIMRC")
-- 6. Reopen the buffers
for _, bufinfo in ipairs(buffers_to_reopen) do
local buf = vim.fn.bufadd(bufinfo.file)
vim.cmd("buffer " .. buf)
vim.api.nvim_buf_call(buf, function()
-- You could also restore the exact cursor position if desired
vim.cmd('silent! normal! g`"')
end)
end -- Notify the user that the config has been reloaded
M.notify("Configuration reloaded successfully!", "INFO", "Reloaded Config")
end
---@function Function to reload all plugins.
--- This is a messy operation. It's not recommended to use it.
--- If you do, please define the exclusion list in your config.lua file.
--- Suggested defaults:
--- vim.g.plugin_reloader_exclusion_list = {
--- ["lazy.nvim"] = true,
--- ["noice.nvim"] = true,
--- ["unception.nvim"] = true,
--- ["nvim-unception"] = true,
--- ["nui.nvim"] = true,
--- ["packer.nvim"] = true,
--- ["trouble.nvim"] = true,
--- ["which-key.nvim"] = true,
--- },
---
--- You should add any other plugins that won't handle a live reload
--- well to this exclusion list and define it in your configuration.
--- The exclusion list can be defined with:
--- 'vim.g.plugin_reloader_exclusion_list'
---@see data.types.plugin_reloader.exclusion_list
---@return nil
function M.reload_all_plugins()
-- Define the exclusion list
local exclude = {}
if vim.g.plugin_reloader.exclusion_list then
-- Use global variable if provided
exclude = vim.g.plugin_reloader.exclusion_list
elseif pcall(require, "data.types") then
-- Use data.types if available
exclude = require("data.types").plugin_reloader.exclusion_list
else
-- Fallback to default
exclude = {
["lazy.nvim"] = true,
["noice.nvim"] = true,
["unception.nvim"] = true,
["nvim-unception"] = true,
["nui.nvim"] = true,
["packer.nvim"] = true,
["trouble.nvim"] = true,
["which-key.nvim"] = true,
}
end
-- Try to require the lazy.core.config module
local ok, lazy_config = pcall(require, "lazy.core.config")
if not ok then
-- Handle the error; the module is not available
print("lazy.core.config not found")
else
-- Get the list of currently loaded plugins
local plugins = lazy_config.plugins
-- Iterate over each plugin and reload it if it's not in the exclusion list
for plugin_name, _ in pairs(plugins) do
if not exclude[plugin_name] then
vim.cmd("Lazy reload " .. plugin_name)
end
end
end
end
---@function Check if a plugin is installed.
---@param plugins string|table The name of the plugin module(s) to check.
--- Options:
--- - string: The name of the plugin module to check.
--- - table: A list of plugin modules to check.
---@param opts? table Additional options for the function.
--- - load (boolean): Whether to preload the plugin if found.
--- - simple (boolean): Whether to use a simple check (pcall(require, plugin)).
---@return boolean|table condition The installed state of the plugin(s).
--- - boolean: The installed state of the plugin.
--- - table: A list of installed states of the plugins.
--- The return type is determined based on the input type.
--- If the input is a table of plugin modules, the return type is a table.
function M.is_installed(plugins, opts)
opts = opts or {}
local is_single = type(plugins) == "string"
if type(plugins) ~= "table" then
plugins = { plugins }
end
local load_plug = opts.load or false
local simple = opts.simple or false
local installed_plugins = {}
for _, plugin in ipairs(plugins) do
local installed = false
local function check_plugin(name)
if simple then
return pcall(require, name)
else
local lazy_installed = pcall(require, "lazy")
if lazy_installed and require("lazy.core.config").plugins[name] then
return true
end
local packer_installed = pcall(require, "packer_plugins")
if
packer_installed
---@diagnostic disable-next-line: undefined-field
and _G.packer_plugins
---@diagnostic disable-next-line: undefined-field
and _G.packer_plugins[name]
then
return true
end
if vim.fn.exists("g:plugs") == 1 and vim.g.plugs[name] then
return true
end
return pcall(require, name)
end
end
-- Initial check with the plugin name as-is
installed = check_plugin(plugin)
-- If not installed, try replacing hyphens with underscores
if not installed and plugin:find("-") then
local underscored_name = plugin:gsub("-", "_")
installed = check_plugin(underscored_name)
end
installed_plugins[plugin] = installed
end
-- Optionally preload the plugin(s)
if load_plug then
local lazy_installed = pcall(require, "lazy")
if lazy_installed then
for _, plugin in ipairs(plugins) do
if installed_plugins[plugin] then
require("lazy").load({ plugins = { plugin } })
end
end
else
M.notify("Unable to preload plugin(s)", "ERROR", "Lazy not found")
end
end
return is_single and installed_plugins[plugins[1]] or installed_plugins
end
---@function Function to create a floating terminal window
function M.open_floating_terminal()
-- Create a scratch buffer specifically for the terminal
local buf = vim.api.nvim_create_buf(false, true) -- Create an unnamed, non-file, scratch buffer
if not buf or buf == 0 then
vim.notify("Failed to create buffer", vim.log.levels.ERROR)
return
end
-- Get the dimensions of the current editor window
-- This helps us center the floating window
local ui = vim.api.nvim_list_uis()[1]
local width = ui.width
local height = ui.height
local win_width = math.floor(width * 0.8)
local win_height = math.floor(height * 0.8)
-- Define settings of the floating window, sizing it to 80% of the full editor size
local win_opts = {
style = "minimal", -- Minimal UI, no status line or tab line
relative = "editor", -- Float relative to the whole editor UI
width = win_width, -- Set width to 80% of editor width
height = win_height, -- Set height to 80% of editor height
row = math.floor((height - win_height) / 2), -- Centered vertically
col = math.floor((width - win_width) / 2), -- Centered horizontally
border = "rounded", -- Add a border for aesthetics (can be 'single', 'double', etc.)
}
-- Open the floating window with our newly created buffer
local win = vim.api.nvim_open_win(buf, true, win_opts)
-- Check if the window was created successfully
if not win or win == 0 then
vim.notify("Failed to create floating window", vim.log.levels.ERROR)
return
end
-- Set very specific buffer and window configurations
vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) -- Auto-remove when the buffer is closed
vim.api.nvim_set_option_value("winblend", 10, { win = win }) -- Add slight transparency to the floating window
-- Now that the floating window is ready, we run the terminal shell in the created buffer
-- Open the terminal in the buffer when we're sure the buffer is set up in the float
vim.fn.termopen(vim.o.shell, {
on_exit = function()
-- Safety measure: Ensure the window still exists before trying to close it
if vim.api.nvim_win_is_valid(win) then
-- Notify the user that the terminal has closed
vim.notify("Terminal closed", vim.log.levels.INFO)
-- Close and wipe the associated floating window and its buffer
vim.api.nvim_win_close(win, true) -- Force close the terminal window
end
end,
})
-- Switch focus to the terminal window in the floating buffer and enter insert mode
vim.api.nvim_set_current_win(win)
vim.cmd("startinsert!") -- Automatically enter insert mode within the terminal
end
---@function Helper function to add keymaps with common properties
---@param lhs string|table The keybind (or list of keybinds)
--- This field can be the following:
--- - A string representing the keybind
--- - A list of strings representing a set of keybinds
--- - A table of multiple keybind specifications
--- This field is required.
---@param rhs string|function|nil The function to execute when the key is pressed
--- This field can be the following:
--- - A string representing the vimscript command
--- - A lua function (only when using which-key.nvim or global keymaps)
--- This field is required.
---@param desc string|nil The description of the keybind (optional)
--- This field can be the following:
--- - A string representing the description
--- The description will be displayed in the which-key menu
---@param mode string|table|nil The mode(s) in which the keybind should be added (optional)
--- This field can be the following:
--- - A string representing the mode
--- - A table of multiple modes (only when using which-key.nvim or global keymaps)
---@param icon string|nil The icon to use for the keybind (optional)
--- This field can be the following:
--- - A string representing the icon (only when defining a menu)
---@param group string|nil The group to add the keybind to (optional)
--- This field can be the following:
--- - A string representing the group (only when defining a menu)
---@param bufnr number|nil The buffer number to add the keymap to (optional)
--- This field can be the following:
--- - A number representing the buffer
--- This option is incompatible with some extended keymap options
---@param hidden boolean|nil Whether the keybind should be hidden (optional)
--- This field can be the following:
--- - A boolean representing whether to hide the keymap in which-key menus
--- This option is only compatible with which-key configurations
--- It will be ignored if which-key is not installed
--- The default value is false
---@return boolean condition true if the keybind was added, false otherwise
--- There are three main types of keymaps:
--- - Global keymaps
--- This is the most common type of keymap.
--- Ex:
--- add_keymap("<leader>ff", "lua require('telescope.builtin').find_files()")
--- - Buffer keymaps
--- This is used to add keymaps to specific buffers.
--- Ex:
--- add_keymap("<leader>ff", "lua require('telescope.builtin').find_files()", nil, nil, nil, 0)
--- - Which-key menus
--- This is used to add which-key menus.
--- Ex:
--- add_keymap("<leader>l", nil, nil, nil, "󰒲", "Lazy")
---@see which-key.nvim-which-key-mappings
---@see vim.api.nvim_buf_set_keymap
---@see vim.keymap.set
function M.add_keymap(
lhs, -- The keybind
rhs, -- Function to execute when the key is pressed
desc, -- Description of the keybind
mode, -- Mode(s) in which the keybind should be added
icon, -- Icon to use for the keybind menu
group, -- Group to use for the keybind menu
bufnr, -- Buffer number to add the keymap to
hidden
)
-- Check if which-key.nvim is installed
if M.is_installed("which-key.nvim") and not bufnr then
require("which-key").add({
-- stylua: ignore start
{
lhs, -- The keybind
rhs = rhs, -- Function to execute when the key is pressed
desc = desc, -- Description of the keybind
mode = mode or "n", -- Default to "n" (normal mode) if mode is not provided
icon = icon, -- Icon to use for the keybind
group = group, -- Group to add the keybind to
hidden = hidden, -- Hide the keybind in which-key menus
},
-- stylua: ignore end
})
return true
else
-- Handle the case where lhs is a table
if type(lhs) == "table" then
for _, keymap in ipairs(lhs) do
-- Set default values or use provided ones
local keymap_rhs = keymap.rhs or rhs
local keymap_desc = keymap.desc or desc
local keymap_mode = keymap.mode or mode or "n"
local keymap_icon = keymap.icon or icon
local keymap_group = keymap.group or group
local keymap_lhs = keymap.lhs
if not keymap_group and not keymap_icon then
if not bufnr then
-- Apply the keymap using vim.keymap.set
vim.keymap.set(
keymap_mode, -- Mode(s) in which the keybind should be added
keymap_lhs, -- The keybind
---@diagnostic disable-next-line: param-type-mismatch
keymap_rhs, -- Function to execute when the key is pressed
{ desc = keymap_desc } -- Description of the keybind
)
else
if -- Check if the keymap is compatible with buffer-based keymaps
type(keymap_mode) == "table" or type(keymap_rhs) == "function"
then
-- The keymap is incompatible with buffer-based keymaps
local msg = string.format(
"Mapping incompatible with buffer-based keymaps:\n%s%s",
vim.inspect(keymap_lhs), -- Convert lhs to a readable format
keymap_desc and ("\nDescription: " .. keymap_desc) or ""
)
M.notify(msg, "WARN")
return false
end
-- Apply the keymap using vim.api.nvim_buf_set_keymap
vim.api.nvim_buf_set_keymap(
bufnr,
keymap_mode,
keymap_lhs,
---@diagnostic disable-next-line: param-type-mismatch
keymap_rhs,
{ desc = keymap_desc }
)
end
else
-- The keymap requires which-key.nvim
local msg = string.format(
"Mapping requires which-key.nvim:\n%s%s",
vim.inspect(keymap_lhs), -- Convert lhs to a readable format
keymap_desc and ("\nDescription: " .. keymap_desc) or ""
)
M.notify(msg, "WARN")
return false
end
end
return true
else -- Handle the case where lhs is not a table
if not group and not icon then
if not bufnr then
vim.keymap.set(
mode or "n", -- Default to "n" (normal mode) if mode is not provided
lhs, -- The keybind
---@diagnostic disable-next-line: param-type-mismatch
rhs, -- Function to execute when the key is pressed
{ desc = desc } -- Description of the keybind and optional buffer number
)
else
if -- Check if the keymap is compatible with buffer-based keymaps
type(mode) == "table" or type(rhs) == "function"
then
-- The keymap is incompatible with buffer-based keymaps
local msg = string.format(
"Mapping incompatible with buffer-based keymaps:\n%s%s",
vim.inspect(lhs), -- Convert lhs to a readable format
desc and ("\nDescription: " .. desc) or ""
)
M.notify(msg, "WARN")
return false
end
-- Apply the keymap using vim.api.nvim_buf_set_keymap
vim.api.nvim_buf_set_keymap(
bufnr,
mode or "n",
lhs,
---@diagnostic disable-next-line: param-type-mismatch
rhs,
{ desc = desc }
)
end
return true
else
local msg = string.format(
"Mapping requires which-key.nvim:\n%s%s",
vim.inspect(lhs), -- Convert lhs to a readable format
desc and ("\nDescription: " .. desc) or ""
)
M.notify(msg, "WARN")
return false
end
end
end
end
---@function Helper function to remove keymaps
---@param lhs string The keybind
---@param mode string|nil The mode in which the keybind should be removed (optional)
---@param bufnr number|nil The buffer number to remove the keymap from (optional)
---@return boolean condition true if the keymap was removed, false otherwise
---@see vim.api.nvim_del_keymap
---@see vim.api.nvim_buf_del_keymap
function M.rm_keymap(
lhs, -- The keybind
mode, -- Mode(s) in which the keybind should be removed
bufnr -- Buffer number to remove the keymap from
)
mode = mode or "n" -- Default to "n" (normal mode) if mode is not provided
---@class Keymap
---@field lhs string The keybind
---@field rhs string|function The function or command associated with the keybind
if bufnr then
-- If a buffer number is provided, remove the keymap from the specified buffer
---@type Keymap[]
local keymaps = vim.api.nvim_buf_get_keymap(bufnr, mode)
for _, keymap in pairs(keymaps) do
if keymap.lhs and keymap.lhs == lhs then
vim.api.nvim_buf_del_keymap(bufnr, mode, lhs)
return true -- Indicate that the keymap was removed
end
end
else
-- If no buffer number is provided, remove the global keymap
---@type Keymap[]
local keymaps = vim.api.nvim_get_keymap(mode)
for _, keymap in pairs(keymaps) do
if keymap.lhs and keymap.lhs == lhs then
vim.api.nvim_del_keymap(mode, lhs)
return true -- Indicate that the keymap was removed
end
end
end
return false -- Indicate that the keymap did not exist
end
---@function Helper function to add a mark
---@param mark string The mark to add
---@param line integer The line to add the mark to
---@param col integer The column to add the mark to
---@return boolean condition true if the mark was added, false otherwise
function M.add_mark(mark, line, col)
-- Set the mark at the specified line and column
vim.api.nvim_buf_set_mark(0, mark, line, col, {})
return true -- Indicate that the mark was successfully added
end
---@function Helper function to remove a mark
---@param mark string The mark to remove
---@return boolean condition true if the mark was removed, false otherwise
function M.rm_mark(mark)
-- Get a list of marks in the current buffer
local marks = vim.fn.getmarklist(vim.fn.bufnr("%"))
-- Check if the mark exists
for _, m in ipairs(marks) do
if m.mark == mark then
-- Mark exists, remove it
vim.cmd("delmarks " .. mark)
return true -- Indicate that the mark was removed
end
end
return false -- Indicate that the mark did not exist
end
---@function Function to check if buffer is modified
---@return boolean condition true if buffer is modified, false otherwise
function M.is_buffer_modified()
return vim.bo.modified
end
---@function Function to check if buffer is empty
---@return boolean condition true if buffer is empty, false otherwise
function M.is_buffer_empty()
return vim.fn.empty(vim.fn.expand("%:t")) == 1
end
---@function Function to check if buffer is read-only
---@return boolean condition true if buffer is read-only, false otherwise
function M.is_buffer_readonly()
return vim.bo.readonly
end
---@function Function to check if file exists
---@param filepath string The path to the file
---@return boolean condition true if file exists, false otherwise
function M.file_exists(filepath)
local f = io.open(filepath, "r")
if f then
f:close()
end
return f ~= nil
end
---@function Function to get the current git branch
---@return string branch git branch name or "No branch"
function M.get_git_branch()
local branch = vim.fn.systemlist("git rev-parse --abbrev-ref HEAD")[1]
if branch and branch ~= "" then
return branch
else
return "No branch"
end
end
---@function Function to get the current git commit hash
---@return string The current git commit hash or "No commit hash"
function M.get_git_commit_hash()
local commit_hash = vim.fn.systemlist("git rev-parse --short HEAD")[1]
if commit_hash and commit_hash ~= "" then
return commit_hash
else
return "No commit hash"
end
end
---@function Function to run a shell command
---@param cmd string The command to run
---@return string|nil result The output of the command
function M.run_shell_command(cmd)
local handle = io.popen(cmd)
if handle then
local result = handle:read("*a")
handle:close()
return result
else
-- Handle the error case where `handle` is nil
M.notify("Failed to run the command: " .. cmd, "ERROR")
return nil
end
end
---@function Function to get the dimensions of the current window
---@param format string The format of the dimensions
---@return string|table dimensions dimensions of the current window
function M.get_ws_dimensions(format)
format = format or "verbose"
local dimensions = {
width = tonumber(vim.opt.columns:get()) or 0,
height = tonumber(vim.opt.lines:get()) or 0,
}
if format == "verbose" then
return string.format(
"Width: %d cells\nHeight: %d cells",
dimensions.width,
dimensions.height
)
elseif format == "basic" then
return string.format("%dx%d", dimensions.width, dimensions.height)
elseif format == "raw" then
return dimensions
else
-- Trigger an error if the format is not valid
error("Invalid format: " .. format)
end
end
---@function Function to get the cursor position
---@return integer row The current line number
---@return integer col The current column number
function M.get_cursor_position()
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
return row, col
end
---@function Function to check if a global variable is set
---@param var_name string The name of the global variable
---@param expected_value any The expected value of the global variable
---@param default_value any The default value of the global variable
---@return boolean condition true if the global variable is set to the expected value, false otherwise
function M.check_global_var(var_name, expected_value, default_value)
local actual_value = vim.g[var_name]
-- If the global variable is not set, use the default value (if provided)
if actual_value == nil and default_value ~= nil then
actual_value = default_value
end
-- Return whether the actual value matches the expected value
return actual_value == expected_value
end
---@function Function to split a string
---@param inputstr string The string to split
---@param sep string The separator
---@return table output The split string
function M.split_string(inputstr, sep)
if sep == nil then
sep = "%s"
end
local output = {}
for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
table.insert(output, str)
end
return output
end
---@function Function to convert RGB to hexadecimal
---@param rgb table The RGB color
---@return string hex The hexadecimal color
function M.rgb_to_hex(rgb)
return string.format("#%02x%02x%02x", rgb[1], rgb[2], rgb[3])
end
---@function Function to get the foreground color of a highlight group
---@param hlgroup string The name of the highlight group
---@return string|nil hex The foreground color of the highlight group
function M.get_fg_color(hlgroup)
local hl = vim.api.nvim_get_hl(0, { name = hlgroup, link = false })
local fg = hl.fg
if fg then
return M.rgb_to_hex({
bit.rshift(bit.band(fg, 0xFF0000), 16),
bit.rshift(bit.band(fg, 0x00FF00), 8),
bit.band(fg, 0x0000FF),
})
end
return nil
end
---@function Function to get the background color of a highlight group
---@param hlgroup string The name of the highlight group
---@return string|nil hex The background color of the highlight group
function M.get_bg_color(hlgroup)
local hl = vim.api.nvim_get_hl(0, { name = hlgroup, link = false })
local bg = hl.bg
if bg then
return M.rgb_to_hex({
bit.rshift(bit.band(bg, 0xFF0000), 16),
bit.rshift(bit.band(bg, 0x00FF00), 8),
bit.band(bg, 0x0000FF),
})
end
return nil
end
---@function Function to get the appropriate ordinal suffix for a given number.
---@param number number The number to check for the ordinal suffix.
---@return string The number with its ordinal suffix.
function M.get_ordinal_suffix(number)
-- Determine the last two digits to handle 'teen' cases correctly
local suffix = "th" -- Default suffix
local last_digit = number % 10
local last_two_digits = number % 100
if last_digit == 1 and last_two_digits ~= 11 then
suffix = "st"
elseif last_digit == 2 and last_two_digits ~= 12 then
suffix = "nd"
elseif last_digit == 3 and last_two_digits ~= 13 then
suffix = "rd"
end
return tostring(number) .. suffix
end
---@function Function to check if the window is wide enough
---@param width_limit number The minimum width of the window
---@return boolean condition true if the window is wide enough, false otherwise
function M.is_window_wide_enough(width_limit)
local width = vim.fn.winwidth(0)
return width >= width_limit
end
---@function Function to check if the window is tall enough
---@param height_limit number The minimum height of the window
---@return boolean condition true if the window is tall enough, false otherwise
function M.is_window_tall_enough(height_limit)
local height = vim.fn.winheight(0)
return height >= height_limit
end
---@function Function to get the current date
---@return string|osdate date The current date
function M.get_date()
return os.date("%Y-%m-%d")
end
---@function Function to exit neovim
---@return nil
function M.exit()
vim.api.nvim_command("wqall")
end
---@function Function to render the MultiCursor statusline
---@return table status The MultiCursor status object
function M.mc_statusline()
-- Define the default status object
local status = {
enabled = false,
icon = "󰘪 ",
short_text = "NO",
text = "SINGLE",
color = "lualine_a_normal",
cursors = 1,
disabled = 0,
installed = false,
}
local ok, mc = pcall(require, "multicursor-nvim")
if not ok then
-- Handle the case where the plugin is not installed
return status
end
status.installed = true
if mc.numEnabledCursors() > 1 then
status.enabled = true
status.cursors = mc.numEnabledCursors()
status.disabled = mc.numDisabledCursors()
if vim.fn.mode() == "v" then
-- status.icon = "󰚕 "
status.icon = "󰆿"
status.short_text = "V"
status.text = "VISUAL"
status.color = "lualine_a_visual"
else
-- status.icon = "󰬸 "
status.icon = "󰇀"
status.short_text = "N"
status.text = "NORMAL"
status.color = "lualine_a_normal"
end
end
-- Add the icon and text to the status object
status.icon_short_text = status.icon .. status.short_text
status.icon_text = status.icon .. status.text
-- Update the status object
if status.cursors > 1 and status.disabled > 0 then
status.count = status.cursors .. "/" .. status.disabled
elseif status.cursors > 1 and status.disabled <= 0 then
status.count = status.cursors
else
status.count = ""
end
return status
end
--- Configure conditional spellchecking
---@function This function conditionally enables spellcheck based on the provided parameters.
---@param spellcheck? boolean Whether to enable spellcheck (default: true)
---@param filetypes? table|boolean A list of filetypes to enable/disable spellcheck in
--- (default: nil)
--- - If true, toggle spellcheck for all filetypes
--- - If false, enable spellcheck for the current buffer
--- - If a table, enable spellcheck for the specified filetypes
--- - If nil, enable spellcheck for the current buffer
---@return nil
function M.spellcheck(spellcheck, filetypes)
-- Default to true if spellcheck is not provided
spellcheck = spellcheck == nil and true or spellcheck
-- Set up local toggler
local function toggle()
-- Handle local spellchecking
if spellcheck then -- Enable spellcheck for the current buffer
vim.opt_local.spell = true
else -- Disable spellcheck for the current buffer
vim.opt_local.spell = false
end
end
-- Handle filetype-specific spellchecking
if filetypes then
-- Set up description text
local desc = "Enable"
if not spellcheck then
desc = "Disable"
end
-- Make a comma-separated list of filetypes
local typedesc
if type(filetypes) == "table" then
typedesc = table.concat(filetypes, ",")
else -- Fallback to a string
typedesc = tostring(filetypes)
end
-- If filetypes is a boolean
if filetypes == true then
filetypes = { "*" } -- Apply to all filetypes
elseif filetypes == false then
toggle()
end
-- Create an autocommand for the specified filetypes to manage spellcheck
vim.api.nvim_create_autocmd("FileType", {
group = "Spellcheck",
pattern = filetypes,
callback = function()
vim.opt_local.spell = spellcheck
end,
desc = desc .. " spellcheck for " .. typedesc,
})
else
-- Handle local spellchecking
toggle()
end
end
---@function This function swaps the behavior of `p` and `P`
---@param default? boolean Whether to use the default behavior (default: true)
--- - true: swap the behavior of `p` and `P`
--- - false: revert the behavior of `p` and `P`
---@return nil
function M.swap_paste(default)
if default == nil then
default = true
end
if default then
-- Mapping to swap the behavior of `p` and `P`
-- `p` will now paste before the cursor (originally `P`)
-- `P` will now paste after the cursor (originally `p`)
-- Preserve the original actions of `p` and `P` using Vim commands to avoid interference
-- Set `p` to the original paste after cursor
vim.api.nvim_set_keymap("n", "p", '"_dP', { noremap = true, silent = true })
-- Set `P` to the original paste before cursor
vim.api.nvim_set_keymap(
"n",
"P",
'"_d"0p',
{ noremap = true, silent = true }
)
else
-- Mapping to revert the behavior of `p` and `P` to their original functions
-- `p` will now revert to pasting after the cursor (original behavior)
-- `P` will now revert to pasting before the cursor (original behavior)
-- Remap `p` to its original behavior
vim.api.nvim_set_keymap("n", "p", "p", { noremap = true, silent = true })
-- Remap `P` to its original behavior
vim.api.nvim_set_keymap("n", "P", "P", { noremap = true, silent = true })
end
end
---@function This function extracts text between two marked positions in a buffer,
--- removes empty lines, and trims indentation from the left.
---@return string? text The extracted and trimmed text
--- - nil if there is no text in the specified range
--- - string if the text is in the specified range
function M.trim_yank()
-- Execute the last command in normal mode
vim.cmd.normal("!<Esc>")
-- Get the start and finish positions of the selected text
local start = vim.fn.getpos("'<")
local finish = vim.fn.getpos("'>")
-- Get the lines in the specified range
local lines = vim.fn.getline(start[2], finish[2])
-- Ensure lines is a table even if only one line is selected
if type(lines) == "string" then
lines = { lines }
end
-- Drop empty lines from the selection
for i = #lines, 1, -1 do
if lines[i] == "" then
table.remove(lines, i)
end
end
-- Return early if there are no lines to process
if #lines == 0 then
return
end
-- Find the minimum whitespace (indentation) in the selected lines
local ws = 9999
for _, line in ipairs(lines) do
local lws = line:match("^%s*") -- Extract leading whitespace
if lws and #lws < ws then
ws = #lws
end
end
-- Trim the leading whitespace from each line according to the minimal whitespace found
for i, line in ipairs(lines) do
lines[i] = line:sub(ws + 1)
end
-- Return the resulting lines as a single concatenated string
return table.concat(lines, "\n")
end
---@function Function to open the lazygit popup in a floaterm
---@return nil
function M.open_lazygit_popup()
-- Set floaterm border characters
vim.g.floaterm_borderchars = "─│─│╭╮╯╰"
-- Floaterm configuration properties
local floaterm_props = {
width = "0.98", -- Width of the floaterm
height = "0.95", -- Height of the floaterm
autoclose = "1", -- Auto close the floaterm when finished
command = "lazygit", -- Command to run in the floaterm
name = "LazyGit", -- Name of the floaterm
title = "LazyGit", -- Title of the floaterm
titlepos = "center", -- Title position of the floaterm
}
-- Construct the command string for opening the floaterm with the specified settings
local cmd = string.format(
"FloatermNew --height=%s --width=%s --name=%s --title=%s --titleposition=%s --autoclose=%s %s",
floaterm_props.height,
floaterm_props.width,
floaterm_props.name,
floaterm_props.title,
floaterm_props.titlepos,
floaterm_props.autoclose,
floaterm_props.command
)
-- Execute the command
vim.cmd(cmd) -- Using vim.cmd to run the constructed command
-- Enter insert mode in the floaterm
vim.cmd("startinsert")
end
---@function Function to replace '...' with '…' (ellipsis)
---@return boolean condition true if the replacement was successful, false otherwise
function M.replace_ellipsis()
-- Get the current line and the cursor position
local current_line = vim.api.nvim_get_current_line()
local cursor_pos = vim.api.nvim_win_get_cursor(0) -- Get the current cursor position
-- Check if the current line ends with '...'
if current_line:sub(cursor_pos[2] - 2, cursor_pos[2]) == "..." then
-- Replace '...' with '…' using string manipulation
local new_line = current_line:sub(1, cursor_pos[2] - 3) .. "…"
-- Set the new line content
vim.api.nvim_set_current_line(new_line)
-- Move cursor to the new position after replacing
vim.api.nvim_win_set_cursor(0, { cursor_pos[1], cursor_pos[2] - 2 }) -- Adjust cursor position
return true
end
return false
end
---@function Function to automate replacing '...' with '…' (ellipsis)
---@param enable? boolean whether to enable the replacement on InsertLeave
---@return boolean result true if the replacement was enabled, false otherwise
function M.setup_replace_ellipsis(enable)
if enable == nil then
enable = true
end
-- Create an autogroup to handle the autocmds
vim.api.nvim_create_augroup("EllipsisReplace", { clear = true })
if enable then
-- Add an autocmd to handle the InsertLeave event
vim.api.nvim_create_autocmd("InsertLeave", {
group = "EllipsisReplace",
callback = M.replace_ellipsis,
})
-- Optionally, you could call the function on text change as well
vim.api.nvim_create_autocmd("TextChangedI", {
group = "EllipsisReplace",
callback = M.replace_ellipsis,
})
return true
else
-- Remove the autocmds
vim.api.nvim_del_augroup_by_id(
vim.api.nvim_create_augroup("EllipsisReplace", { clear = true })
)
end
return false
end
--- Function to append modeline after the last line in the buffer.
--- @return nil
function M.append_modeline()
local tabstop = vim.o.tabstop
local shiftwidth = vim.o.shiftwidth
local textwidth = vim.o.textwidth
local expandtab = vim.o.expandtab
-- Create the modeline string.
local modeline = string.format(
" vim: set ts=%d sw=%d tw=%d %set :",
tabstop,
shiftwidth,
textwidth,
expandtab and "" or "no"
)
-- Replace the placeholder in the comment string.
local commentstring = vim.o.commentstring
modeline = commentstring:gsub("%%s", modeline)
-- Append the modeline after the last line in the buffer.
vim.api.nvim_buf_set_lines(0, -1, -1, false, { modeline })
end
--- Function to remove buffer
--- Borrowed from LazyVim
--- https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/util/ui.lua
---@param buf number?
function M.bufremove(buf)
buf = buf or 0
buf = buf == 0 and vim.api.nvim_get_current_buf() or buf
if vim.bo.modified then
local choice = vim.fn.confirm(
("Save changes to %q?"):format(vim.fn.bufname()),
"&Yes\n&No\n&Cancel"
)
if choice == 0 or choice == 3 then -- 0 for <Esc>/<C-c> and 3 for Cancel
return
end
if choice == 1 then -- Yes
vim.cmd.write()
end
end
for _, win in ipairs(vim.fn.win_findbuf(buf)) do
vim.api.nvim_win_call(win, function()
if
not vim.api.nvim_win_is_valid(win)
or vim.api.nvim_win_get_buf(win) ~= buf
then
return
end
-- Try using alternate buffer
local alt = vim.fn.bufnr("#")
if alt ~= buf and vim.fn.buflisted(alt) == 1 then
vim.api.nvim_win_set_buf(win, alt)
return
end
-- Try using previous buffer
---@diagnostic disable-next-line: param-type-mismatch
local has_previous = pcall(vim.cmd, "bprevious")
if has_previous and buf ~= vim.api.nvim_win_get_buf(win) then
return
end
-- Create new listed buffer
local new_buf = vim.api.nvim_create_buf(true, false)
vim.api.nvim_win_set_buf(win, new_buf)
end)
end
if vim.api.nvim_buf_is_valid(buf) then
---@diagnostic disable-next-line: param-type-mismatch
pcall(vim.cmd, "bdelete! " .. buf)
end
end
--- Function to check if working directory is a git repository
---@param cwd? boolean|table Check if current working directory is a git repository
---@param pwd? boolean Check if project directory is a git repository
---@return boolean is_git_repo true if working directory or project directory is a git repository, false otherwise
function M.is_git_repo(cwd, pwd)
if type(cwd) == "table" then
pwd = cwd.pwd
cwd = cwd.cwd
end
if not pwd and not cwd then
cwd = true
pwd = true
end
local cwd_dir, pwd_dir
if cwd then
-- Get the current working directory
cwd_dir = vim.fn.getcwd()
end
if pwd then
-- Get the project directory, using Neovim's built-in LSP root detection or fallback to cwd
pwd_dir = vim.lsp.buf.list_workspace_folders()[1]
end
-- Helper function to check for a .git directory
local function has_git_dir(path)
local git_path = path .. "/.git"
---@diagnostic disable-next-line: undefined-field
local stat = vim.loop.fs_stat(git_path)
return stat and stat.type == "directory"
end
if cwd then
-- Check if the current working directory is a git repo
if has_git_dir(cwd_dir) then
return true
end
end
if pwd then
-- Check if the project directory is a git repo
if has_git_dir(pwd_dir) then
return true
end
end
-- Not a git repo
return false
end
---@function Select and run a picker command based on the given parameters.
--- If no provider is provided, it will use fzf-lua if available, else telescope.
--- The default provider can also be set with the `vim.g.lazyvim_picker` variable.
---@param cmd string|table? Command to run or a table with fields {cmd, provider, options} (optional)
--- When cmd is a table, it is destructured and the other fields are ignored
--- If cmd is nil, it will default to "files"
--- Special values for cmd:
--- "FILES": Open files using the file picker even if inside a git repo
--- "config_files": Open neovim config files using the files/git_files picker
---@param provider string? Name of the provider, either "fzf-lua" or "telescope" (optional)
--- If provider is nil, it will default to the value of `vim.g.lazyvim_picker`
--- If 'vim.g.lazyvim_picker' is not set, it will default to 'fzf-lua'
--- If 'fzf-lua' is not available, it will default to 'telescope'
---@param options table? Additional options to pass to the provider's function (optional)
--- This allows passing additional options to the provider function
---@return boolean success true if the picker command was successful, false otherwise
function M.pick(cmd, provider, options)
-- Handle case where `cmd` is a table (destructuring the table fields)
if type(cmd) == "table" then
provider = cmd.provider
options = cmd.options
cmd = cmd.cmd
end
-- Default the command to "files" if not provided
cmd = cmd or "files"
-- Determine the provider if one is not selected
-- (defaults to fzf-lua if available, else telescope)
-- Will use vim.g.lazyvim_picker if defined
if provider == nil then
if vim.g.lazyvim_picker == "fzf" then
provider = "fzf-lua"
elseif vim.g.lazyvim_picker == "telescope" then
provider = "telescope"
else
local has_fzf, _ = pcall(require, "fzf-lua")
if has_fzf then
provider = "fzf-lua"
else
provider = "telescope"
end
end
end
-- Handle special commands
if cmd == "config_files" then
cmd = "files"
options = { cwd = vim.fn.stdpath("config") }
end
if cmd == "files" and provider == "telescope" then
cmd = "find_files"
end
-- Helper function to check if a picker command exists for a given provider
local function has_picker(provide, commd)
local success, picker = pcall(function()
return require(provide)[commd]
end)
return success and type(picker) == "function"
end
-- Ensure options is always a table (to avoid errors if nil is passed)
options = options or {}
-- Define available providers in the order of fallback preference
local providers =
{ provider, provider == "fzf-lua" and "telescope" or "fzf-lua" }
-- Try running the picker for the first provider (or fallback to the second if not available)
for _, current_provider in ipairs(providers) do
if current_provider == "telescope" then
current_provider = "telescope.builtin"
end
if has_picker(current_provider, cmd) then
local picker = require(current_provider)[cmd]
picker(options)
return true -- Picker ran successfully, no need to check further
end
end
-- Handle special cases
if cmd == "file_browser" then
if provider == "mini" then
require("mini.files").open()
return true
elseif provider == "neotree" then
vim.cmd("Neotree reveal")
return true
elseif provider == "oil" then
require("oil").open()
return true
else
require("telescope").extensions.file_browser.file_browser()
return true
end
end
-- If no provider supports the cmd, throw an error
vim.notify(
"Unknown command: " .. cmd .. " for providers: fzf-lua, telescope",
vim.log.levels.ERROR
)
return false
end
--- Converts a Windows path to a Unix path
--- If the path is already in Unix format, it will return the original path.
--- You can pass any path whose format is uncertain to this function to ensure
--- the correct format is used.
---@param path string The path to convert
---@return string The converted path
function M.convert_path(path)
-- Check if the path is a Windows path (e.g., C:\ or D:\)
if path:match("^[A-Za-z]:\\") or path:match("^[A-Za-z]:/") then
-- Convert forward slashes (/) to backslashes (\)
local converted_path = path:gsub("/", "\\")
return converted_path
else
-- Return the original path if it's not a Windows path
return path
end
end
function M.char_on_pos(pos)
pos = pos or vim.fn.getpos(".")
return tostring(vim.fn.getline(pos[1])):sub(pos[2], pos[2])
end
-- From: https://neovim.discourse.group/t/how-do-you-work-with-strings-with-multibyte-characters-in-lua/2437/4
function M.char_byte_count(s, i)
if not s or s == "" then
return 1
end
local char = string.byte(s, i or 1)
-- Get byte count of unicode character (RFC 3629)
if char > 0 and char <= 127 then
return 1
elseif char >= 194 and char <= 223 then
return 2
elseif char >= 224 and char <= 239 then
return 3
elseif char >= 240 and char <= 244 then
return 4
end
end
function M.get_visual_range()
local sr, sc = unpack(vim.fn.getpos("v"), 2, 3)
local er, ec = unpack(vim.fn.getpos("."), 2, 3)
-- To correct work with non-single byte chars
local byte_c = M.char_byte_count(M.char_on_pos({ er, ec }))
ec = ec + (byte_c - 1)
local range = {}
if sr == er then
local cols = sc >= ec and { ec, sc } or { sc, ec }
range = { sr, cols[1] - 1, er, cols[2] }
elseif sr > er then
range = { er, ec - 1, sr, sc }
else
range = { sr, sc - 1, er, ec }
end
return range
end
function M.to_api_range(range)
local sr, sc, er, ec = unpack(range)
return sr - 1, sc, er - 1, ec
end
function M.number()
local nu = vim.opt.number:get()
local rnu = vim.opt.relativenumber:get()
local cur_line = vim.fn.line(".") == vim.v.lnum and vim.v.lnum or vim.v.relnum
local width = vim.opt.numberwidth:get()
local l_count_width = #tostring(vim.api.nvim_buf_line_count(0))
width = width >= l_count_width and width or l_count_width
local function pad_start(n)
local len = width - #tostring(n)
return len < 1 and n or (" "):rep(len) .. n
end
local v_hl = ""
local mode = vim.fn.strtrans(vim.fn.mode()):lower():gsub("%W", "")
if mode == "v" then
-- Define the custom highlight outside the function
local bg_color = M.get_bg_color("CursorLineNr")
local fg_color = M.get_bg_color("lualine_a_visual")
vim.api.nvim_set_hl(
0,
"StatusColumnVisualHighlight",
{ fg = fg_color, bg = bg_color }
)
local v_range = M.get_visual_range()
local is_in_range = vim.v.lnum >= v_range[1] and vim.v.lnum <= v_range[3]
v_hl = is_in_range and "%#StatusColumnVisualHighlight#" or ""
end
local line_display = ""
if nu and rnu then
line_display = pad_start(cur_line)
elseif nu then
line_display = pad_start(vim.v.lnum)
elseif rnu then
line_display = pad_start(vim.v.relnum)
end
-- Include `%s` for signs, followed by the custom line number output
return "%s" .. v_hl .. line_display
end
--- Function to search for the provided filetypes with pickers
---@param filetypes table A list of filetypes to search for
function M.pick_filetypes(filetypes)
if not pcall(require, "telescope") then
if not pcall(require, "fzf-lua") then
vim.notify("Picker not found", vim.log.levels.ERROR)
return
else
require("fzf-lua").files({
cmd = "fd --type f " .. table.concat(
vim.tbl_map(function(ft)
return "--extension " .. ft
end, filetypes),
" "
),
})
return
end
else
require("telescope.builtin").find_files({
find_command = {
"fd",
"--type",
"f",
unpack(vim
.iter(vim.tbl_map(function(ft)
return { "--extension", ft }
end, filetypes))
:flatten()
:totable()),
},
})
end
end
--- Function to search for cpp files with picker
function M.search_cpp_files()
M.pick_filetypes(require("data.types").picker_sets.cpp_files)
end
--- Function to search for python files with picker
function M.search_python_files()
M.pick_filetypes(require("data.types").picker_sets.python_files)
end
--- Function to search for nvim files with picker
function M.search_nvim_files()
M.pick_filetypes(require("data.types").picker_sets.nvim_files)
end
--- Function to search for vim files with picker
function M.search_vim_files()
M.pick_filetypes(require("data.types").picker_sets.vim_files)
end
--- Function to search for java files with picker
function M.search_java_files()
M.pick_filetypes(require("data.types").picker_sets.java_files)
end
--- Function to search for javascript files with picker
function M.search_js_files()
M.pick_filetypes(require("data.types").picker_sets.js_files)
end
--- Function to search for rust files with picker
function M.search_rust_files()
M.pick_filetypes(require("data.types").picker_sets.rust_files)
end
--- Function to search for HTML files with picker
function M.search_html_files()
M.pick_filetypes(require("data.types").picker_sets.html_files)
end
--- Function to apply the Caesar cipher to a given text
---@param text string The text to be transformed
---@param shift? number The shift value
---@return string shifted_text The transformed text
function M.caesar_cipher(text, shift)
shift = shift or 3 -- Default shift is 3 if not provided
local shifted_text = ""
for char in text:gmatch(".") do
-- Check if the character is a letter
if char:match("%a") then
local base = char:match("%u") and 65 or 97 -- Base ASCII for uppercase or lowercase letters
local new_char =
string.char(((string.byte(char) - base + shift) % 26) + base)
shifted_text = shifted_text .. new_char
else
shifted_text = shifted_text .. char -- Non-letter characters remain the same
end
end
return shifted_text
end
-- Function to apply the Caesar cipher to the current line
---@param shift number The shift value
function M.caesar_cipher_text(shift)
local shifted_line = M.caesar_cipher(vim.api.nvim_get_current_line(), shift)
vim.api.nvim_set_current_line(shifted_line)
end
--- Function to apply the Caesar cipher to the visual selection
---@param shift number The shift value
function M.caesar_cipher_visual(shift)
local Range = require("u.range")
local range = Range.from_vtext()
local text = range:text()
local shifted_text = ""
for char in text:gmatch(".") do
if char:match("%a") then
local base = char:match("%u") and 65 or 97
local new_char =
string.char(((string.byte(char) - base + shift) % 26) + base)
shifted_text = shifted_text .. new_char
else
shifted_text = shifted_text .. char
end
end
range:replace(shifted_text)
end
-- Function to encrypt the entire buffer contents using GPG
function M.encrypt_buffer_with_gpg()
-- Get the recipient email address from env variable or user input
local recipient = os.getenv("GPG_RECIPIENT")
if not recipient then
recipient = vim.fn.input("Enter recipient email: ")
end
-- Create a temporary file to hold the plaintext buffer content
local temp_filename = os.tmpname()
local temp_output_filename = os.tmpname() .. ".gpg" -- Temporary output file for encrypted content
local file = io.open(temp_filename, "w")
if file then
-- Get all lines in the current buffer and write them to the temporary file
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local content = table.concat(lines, "\n")
file:write(content)
file:close()
else
print("Failed to create temporary file")
return
end
-- Command to encrypt the file using GPG
local command = string.format(
"gpg -e -a -o %s -r %s %s",
temp_output_filename,
recipient,
temp_filename
)
-- Execute the GPG command
local exit_code = os.execute(command)
-- Check if the GPG command was successful
if exit_code == 0 then
-- Read the encrypted text from the output file
local encrypted_file = io.open(temp_output_filename, "r")
if encrypted_file then
local encrypted_text = encrypted_file:read("*a") -- Read the entire content
encrypted_file:close()
-- Replace the current buffer's contents with the encrypted text, split into lines
vim.api.nvim_buf_set_lines(
0,
0,
-1,
false,
vim.split(encrypted_text, "\n")
)
else
print("Failed to read the encrypted output file.")
end
else
print("GPG command failed. Check your GPG configuration.")
end
-- Clean up temporary files
os.remove(temp_filename)
os.remove(temp_output_filename)
end
-- Function to base64 encode the current line or visual selection
local function base64_encode(input)
local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local output = {}
local padding = ""
-- Add padding for any leftover bytes
local len = #input
if len % 3 == 1 then
input = input .. "\0\0"
padding = "=="
elseif len % 3 == 2 then
input = input .. "\0"
padding = "="
end
for i = 1, #input, 3 do
local b1, b2, b3 =
string.byte(input, i),
string.byte(input, i + 1),
string.byte(input, i + 2)
local enc1 = bit.rshift(b1, 2)
local enc2 = ((b1 % 4) * 16) + bit.rshift(b2, 4)
local enc3 = ((b2 % 16) * 4) + bit.rshift(b3, 6)
local enc4 = b3 % 64
table.insert(output, b64:sub(enc1 + 1, enc1 + 1))
table.insert(output, b64:sub(enc2 + 1, enc2 + 1))
table.insert(output, b64:sub(enc3 + 1, enc3 + 1))
table.insert(output, b64:sub(enc4 + 1, enc4 + 1))
end
return table.concat(output):sub(1, -1 - #padding) .. padding
end
function M.base64_encode_visual()
local Range = require("u.range")
local range = Range.from_vtext()
local selected_text = range:text()
local encoded_text = base64_encode(selected_text)
range:replace(encoded_text)
end
-- Function to encode the current line
function M.base64_encode_text()
-- Get the current line where the cursor is
local current_line = vim.api.nvim_get_current_line()
-- Base64 encode the current line using our function
local encoded_line = base64_encode(current_line)
-- Replace the current line with the encoded line
vim.api.nvim_set_current_line(encoded_line)
end
-- Function to base64 decode a string
local function base64_decode(input)
local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local output = {}
local padding = input:sub(-2) == "==" and 2
or (input:sub(-1) == "=" and 1 or 0)
-- Remove any padding characters
input = input:gsub("=", "")
for i = 1, #input, 4 do
local c1, c2, c3, c4 =
b64:find(input:sub(i, i)) - 1,
b64:find(input:sub(i + 1, i + 1)) - 1,
b64:find(input:sub(i + 2, i + 2)) - 1,
b64:find(input:sub(i + 3, i + 3)) - 1
local b1 = bit.lshift(c1, 2) + bit.rshift(c2, 4)
local b2 = bit.lshift(c2 % 16, 4) + bit.rshift(c3, 2)
local b3 = bit.lshift(c3 % 4, 6) + c4
table.insert(output, string.char(b1))
if i < #input - 2 or padding < 2 then
table.insert(output, string.char(b2))
end
if i < #input - 3 or padding < 1 then
table.insert(output, string.char(b3))
end
end
return table.concat(output)
end
-- Function to decode the current visual selection
function M.base64_decode_visual()
local Range = require("u.range")
local range = Range.from_vtext()
local selected_text = range:text()
local decoded_text = base64_decode(selected_text)
range:replace(decoded_text)
end
-- Function to decode the current line
function M.base64_decode_text()
-- Get the current line where the cursor is
local current_line = vim.api.nvim_get_current_line()
-- Base64 decode the current line using our function
local decoded_line = base64_decode(current_line)
-- Replace the current line with the decoded line
vim.api.nvim_set_current_line(decoded_line)
end
--- Applies the ROT47 cipher to the given string while preserving newline characters.
-- @param input_string The string to be transformed with the ROT47 cipher.
-- @return The transformed string after applying ROT47, maintaining the original structure.
local function rot47(input_string)
local result = {}
for i = 1, #input_string do
local char = input_string:sub(i, i)
local ascii = string.byte(char)
-- Check for newline characters
if char == "\n" then
-- Preserve newline characters without modification
table.insert(result, char)
elseif ascii >= 33 and ascii <= 126 then
-- Apply ROT47 cipher for characters in the range of printable ASCII
ascii = ((ascii - 33 + 47) % 94) + 33
table.insert(result, string.char(ascii))
else
-- Preserve other non-printable characters unchanged
table.insert(result, char)
end
end
return table.concat(result)
end
--- Applies the ROT47 cipher to the current line while preserving newline characters.
function M.rot47_text()
-- Get the current line where the cursor is
local current_line = vim.api.nvim_get_current_line()
-- Base64 encode the current line using our function
local encoded_line = rot47(current_line)
-- Replace the current line with the encoded line
vim.api.nvim_set_current_line(encoded_line)
end
--- Applies the ROT47 cipher to the visual selection while preserving newline characters.
function M.rot47_visual()
local Range = require("u.range")
local range = Range.from_vtext()
local selected_text = range:text()
local transformed_text = rot47(selected_text)
range:replace(transformed_text)
end
--- Converts a given string into its hexadecimal representation while preserving newlines.
-- @param input_string The string to be transformed into hexadecimal.
-- @return The hexadecimal encoded string, preserving the original structure.
local function hex_encode(input_string)
local result = {}
for i = 1, #input_string do
local char = input_string:sub(i, i)
-- Check for newline characters
if char == "\n" then
-- Preserve newline characters without modification
table.insert(result, char)
else
-- Convert each character to its hexadecimal representation
local hex = string.format("%02X", string.byte(char))
table.insert(result, hex)
end
end
return table.concat(result)
end
--- Decodes a given hexadecimal encoded string back to its original form while preserving newlines.
-- @param hex_string The hexadecimal encoded string to be decoded.
-- @return The decoded string, preserving the original structure.
local function hex_decode(hex_string)
local result = {}
local i = 1
while i <= #hex_string do
local char = hex_string:sub(i, i)
-- Check for newline characters
if char == "\n" then
-- Preserve newline characters without modification
table.insert(result, char)
i = i + 1
elseif i + 1 <= #hex_string then
-- Check for valid hexadecimal pairs
local hex_pair = hex_string:sub(i, i + 1)
-- Convert the hexadecimal pair to corresponding character
if hex_pair:match("^[0-9A-Fa-f][0-9A-Fa-f]$") then
local byte = tonumber(hex_pair, 16) -- Convert to number
table.insert(result, string.char(byte)) -- Convert number to character
i = i + 2 -- Move to the next pair
else
-- If not a valid pair, treat it as a single character and continue
table.insert(result, char)
i = i + 1
end
else
-- If this is the last character (without a pair), add it as is
table.insert(result, char)
i = i + 1
end
end
return table.concat(result)
end
--- Applies the hex encoding to the current line while preserving newline characters.
function M.hex_encode_text()
-- Get the current line where the cursor is
local current_line = vim.api.nvim_get_current_line()
-- Hex encode the current line using our function
local encoded_line = hex_encode(current_line)
-- Replace the current line with the encoded line
vim.api.nvim_set_current_line(encoded_line)
end
--- Applies the hex encoding to the visual selection while preserving newline characters.
function M.hex_encode_visual()
local Range = require("u.range")
local range = Range.from_vtext()
local selected_text = range:text()
local transformed_text = hex_encode(selected_text)
range:replace(transformed_text)
end
--- Reverses the hex encoding of the current line while preserving newline characters.
function M.hex_decode_text()
-- Get the current line where the cursor is
local current_line = vim.api.nvim_get_current_line()
-- Hex decode the current line using our function
local decoded_line = hex_decode(current_line)
-- Replace the current line with the decoded line
vim.api.nvim_set_current_line(decoded_line)
end
--- Reverses the hex encoding of the visual selection while preserving newline characters.
function M.hex_decode_visual()
local Range = require("u.range")
local range = Range.from_vtext()
local selected_text = range:text()
local transformed_text = hex_decode(selected_text)
range:replace(transformed_text)
end
--- Function to lookup selection in help docs
function M.help_lookup_visual()
local Range = require("u.range")
local range = Range.from_vtext()
local selected_text = range:text()
vim.api.nvim_command("help " .. selected_text)
end
--- Function to lookup word in help docs
function M.help_lookup_word()
local Range = require("u.range")
local range = Range.from_text_object("iw")
if range then
local selected_text = range:text()
vim.api.nvim_command("help " .. selected_text)
end
end
--- Function to look up a string in the help docs
---@param string string The string to be looked up
function M.help_lookup_string(string)
vim.api.nvim_command("help " .. string)
end
--- Function to add single-quotes around a string
---@param text string The text to be wrapped in single quotes
---@return string wrapped_text The text wrapped in single quotes
function M.wrap_in_quotes(text)
return "'" .. text .. "'"
end
--- Function to add double-quotes around a string
---@param text string The text to be wrapped in double quotes
---@return string wrapped_text The text wrapped in double quotes
function M.wrap_in_quotes_double(text)
return '"' .. text .. '"'
end
--- Function to lookup the word (quoted)
function M.help_lookup_quoted()
local Range = require("u.range")
local range = Range.from_text_object("iw")
if range then
local selected_text = range:text()
M.help_lookup_string(M.wrap_in_quotes(selected_text))
end
end
--- Function to lookup the selection (quoted)
function M.help_lookup_quoted_visual()
local Range = require("u.range")
local range = Range.from_vtext()
local selected_text = range:text()
M.help_lookup_string(M.wrap_in_quotes(selected_text))
end
-- Export the module
return M