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