nvim/lua/utils.lua

287 lines
8.5 KiB
Lua

local M = {}
function M.safe_require(name)
local ok, mod = pcall(require, name)
if ok then return mod end
return nil
end
-- Number of parent directories to show in full in the tabline
-- The rest will be shortened according to tabline_shorten_length
-- Example: with full_parents = 1, /path/to/my/project/src/file.txt becomes pat/to/my/project/src/file.txt
-- Example: with full_parents = 2, it becomes pat/to/my/project/src/file.txt
M.tabline_full_parents = 1
-- Number of characters to show for shortened directory names in the tabline
-- Example: with shorten_length = 3, /path/to/my becomes pat/to/my
-- Example: with shorten_length = 1, /path/to/my becomes p/t/m
M.tabline_shorten_length = 3
-- Get all modified, deleted, and untracked Git files with status
-- Returns a list suitable for setqflist() or nil on error
function M.git_changed_files()
-- Use git status --porcelain to get all changes with status indicators
-- Format: "XY filename" where X=index status, Y=worktree status
-- Status codes: M=modified, D=deleted, A=added, ??=untracked, etc.
local handle = io.popen('git status --porcelain 2>/dev/null')
if not handle then
vim.notify('Failed to run git status', vim.log.levels.ERROR)
return nil
end
local result = handle:read('*a')
handle:close()
if result == '' then
vim.notify('No git changes found', vim.log.levels.INFO)
return nil
end
local qf_list = {}
local status_map = {
['M'] = 'Modified',
['A'] = 'Added',
['D'] = 'Deleted',
['R'] = 'Renamed',
['C'] = 'Copied',
['U'] = 'Unmerged',
['?'] = 'Untracked',
}
for line in result:gmatch('[^\n]+') do
-- Parse porcelain format: "XY filename" or "XY original -> renamed"
local index_status = line:sub(1, 1)
local work_status = line:sub(2, 2)
local filename = line:sub(4) -- Skip "XY " prefix
-- Determine status text (worktree takes precedence over index)
local status_code = work_status ~= ' ' and work_status or index_status
local status_text = status_map[status_code] or 'Changed'
table.insert(qf_list, {
filename = filename,
lnum = 1,
text = status_text,
})
end
return qf_list
end
-- Show highlight group and color information under cursor
-- Returns nothing, displays results in a floating window
function M.show_highlight_info()
local cursor_pos = vim.api.nvim_win_get_cursor(0)
local row, col = cursor_pos[1] - 1, cursor_pos[2]
-- Get all highlight groups at cursor position
local ts_hl = vim.treesitter.get_captures_at_pos(0, row, col)
local synID = vim.fn.synID(row + 1, col + 1, 1)
local synName = vim.fn.synIDattr(synID, 'name')
local synTrans = vim.fn.synIDattr(vim.fn.synIDtrans(synID), 'name')
-- Helper to resolve highlight links
local function resolve_hl(name)
local hl = vim.api.nvim_get_hl(0, { name = name })
local max_depth = 10
local depth = 0
while hl.link and depth < max_depth do
name = hl.link
hl = vim.api.nvim_get_hl(0, { name = name })
depth = depth + 1
end
return hl, name
end
local lines = {
'=== Highlight Info Under Cursor ===',
'',
'Position: row=' .. row .. ' col=' .. col,
'',
}
-- TreeSitter captures
if #ts_hl > 0 then
table.insert(lines, 'TreeSitter Captures:')
for _, capture in ipairs(ts_hl) do
local cap_name = '@' .. capture.capture
local hl, resolved_name = resolve_hl(cap_name)
table.insert(lines, string.format(' %s', cap_name))
if resolved_name ~= cap_name then
table.insert(lines, string.format(' → resolves to: %s', resolved_name))
end
if hl.fg then
table.insert(lines, string.format(' fg: #%06x', hl.fg))
end
if hl.bg then
table.insert(lines, string.format(' bg: #%06x', hl.bg))
end
local styles = {}
if hl.bold then table.insert(styles, 'bold') end
if hl.italic then table.insert(styles, 'italic') end
if hl.underline then table.insert(styles, 'underline') end
if #styles > 0 then
table.insert(lines, ' style: ' .. table.concat(styles, ', '))
end
end
table.insert(lines, '')
end
-- Syntax group
if synName ~= '' then
table.insert(lines, 'Syntax Group: ' .. synName)
if synTrans ~= synName and synTrans ~= '' then
table.insert(lines, 'Translates to: ' .. synTrans)
end
local hl, resolved_name = resolve_hl(synTrans ~= '' and synTrans or synName)
if hl.fg then
table.insert(lines, string.format(' fg: #%06x', hl.fg))
end
if hl.bg then
table.insert(lines, string.format(' bg: #%06x', hl.bg))
end
table.insert(lines, '')
end
-- Final applied highlight (use TreeSitter if available, otherwise syntax)
local final_hl_name = nil
if #ts_hl > 0 then
final_hl_name = '@' .. ts_hl[1].capture
elseif synTrans ~= '' then
final_hl_name = synTrans
elseif synName ~= '' then
final_hl_name = synName
end
if final_hl_name then
local final_hl, final_resolved = resolve_hl(final_hl_name)
table.insert(lines, 'Applied Highlight: ' .. final_resolved)
if final_hl.fg then
table.insert(lines, string.format(' fg: #%06x', final_hl.fg))
else
table.insert(lines, ' fg: NONE')
end
if final_hl.bg then
table.insert(lines, string.format(' bg: #%06x', final_hl.bg))
else
table.insert(lines, ' bg: NONE')
end
local styles = {}
if final_hl.bold then table.insert(styles, 'bold') end
if final_hl.italic then table.insert(styles, 'italic') end
if final_hl.underline then table.insert(styles, 'underline') end
if final_hl.undercurl then table.insert(styles, 'undercurl') end
if #styles > 0 then
table.insert(lines, ' style: ' .. table.concat(styles, ', '))
end
end
-- Show in a floating window
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
local width = 0
for _, line in ipairs(lines) do
width = math.max(width, #line)
end
width = math.min(width + 2, vim.o.columns - 4)
local height = #lines
local opts = {
relative = 'cursor',
width = width,
height = height,
row = 1,
col = 0,
style = 'minimal',
border = 'rounded',
}
vim.api.nvim_open_win(buf, false, opts)
end
-- Custom tabline function
-- Shows configurable number of full parent directories, shortens the rest
function M.custom_tabline()
local tabline = ''
local num_tabs = vim.fn.tabpagenr('$')
for i = 1, num_tabs do
local buflist = vim.fn.tabpagebuflist(i)
local winnr = vim.fn.tabpagewinnr(i)
local bufnr = buflist[winnr]
local bufname = vim.fn.bufname(bufnr)
local bufmodified = vim.fn.getbufvar(bufnr, "&modified")
-- Highlight for the tab
if i == vim.fn.tabpagenr() then
tabline = tabline .. '%#TabLineSel#'
else
tabline = tabline .. '%#TabLine#'
end
-- Tab number
tabline = tabline .. ' ' .. i .. ' '
-- Format the filename with smart path shortening
local filename
if bufname == '' then
filename = '[No Name]'
else
-- Get the full path relative to cwd if possible
local path = vim.fn.fnamemodify(bufname, ':~:.')
-- Split path into components
local parts = vim.split(path, '/', { plain = true })
if #parts > M.tabline_full_parents + 1 then
-- We have enough parts to do smart shortening
local result = {}
-- Shorten the leading directories (all but the last full_parents + filename)
local num_to_shorten = #parts - M.tabline_full_parents - 1
for j = 1, num_to_shorten do
table.insert(result, parts[j]:sub(1, M.tabline_shorten_length))
end
-- Add the full parent directories
for j = num_to_shorten + 1, #parts - 1 do
table.insert(result, parts[j])
end
-- Add the filename
table.insert(result, parts[#parts])
filename = table.concat(result, '/')
else
-- Path is short enough, just use it as-is
filename = path
end
end
-- Add modified flag
if bufmodified == 1 then
filename = filename .. ' [+]'
end
tabline = tabline .. filename .. ' '
end
-- Fill the rest with TabLineFill
tabline = tabline .. '%#TabLineFill#%T'
-- Right-align: show tab page count if more than one tab
if num_tabs > 1 then
tabline = tabline .. '%=%#TabLine# ' .. num_tabs .. ' tabs '
end
return tabline
end
return M