if not pcall(require, "el") then return end local job = require "plenary.job" local el_sub = require "el.subscribe" local M = {} function M.extract_hl(spec) if not spec or vim.tbl_isempty(spec) then return end local hl_name, hl_opts = { "El" }, {} for attr, val in pairs(spec) do if type(val) == "table" then table.insert(hl_name, attr) assert(vim.tbl_count(val) == 1) local hl, what = next(val) local hlID = vim.fn.hlID(hl) if hlID > 0 then table.insert(hl_name, hl) local col = vim.fn.synIDattr(hlID, what) if col and #col > 0 then table.insert(hl_name, what) hl_opts[attr] = col end end else -- bold, underline, etc hl_opts[attr] = val end end hl_name = table.concat(hl_name, "_") -- if highlight exists, verify it has -- the correct colorscheme highlights local newID = vim.fn.hlID(hl_name) if newID > 0 then for what, expected in pairs(hl_opts) do local res = vim.fn.synIDattr(newID, what) if type(expected) == "boolean" then -- synIDattr returns '1' for boolean res = res and res == "1" and true end if res ~= expected then -- need to regen the highlight -- print("color mismatch", hl_name, what, "e:", expected, "c:", res) newID = 0 end end end if newID == 0 then vim.api.nvim_set_hl(0, hl_name, hl_opts) end return hl_name end local function set_hl(hls, s) if not hls or not s then return s end hls = type(hls) == "string" and { hls } or hls for _, hl in ipairs(hls) do if vim.fn.hlID(hl) > 0 then return ("%%#%s#%s%%0*"):format(hl, s) end end return s end local function wrap_fnc(opts, fn) return function(window, buffer) -- buf_autocmd doesn't send win if not window and buffer then window = { win_id = vim.fn.bufwinid(buffer.bufnr) } end if opts.hide_inactive and window and window.win_id ~= vim.api.nvim_get_current_win() then return "" end return fn(window, buffer) end end function M.mode(opts) opts = opts or {} return wrap_fnc(opts, function(_, _) local fmt = opts.fmt or "%s%s" local mode = vim.api.nvim_get_mode().mode local mode_data = opts.modes and opts.modes[mode] local hls = mode_data and mode_data[3] local icon = opts.hl_icon_only and set_hl(hls, opts.icon) or opts.icon mode = mode_data and mode_data[1]:upper() or mode mode = (fmt):format(icon or "", mode) return not opts.hl_icon_only and set_hl(hls, mode) or mode end) end function M.git_branch(opts) opts = opts or {} return el_sub.buf_autocmd("el_git_branch", "BufEnter", wrap_fnc(opts, function(_, buffer) -- Try fugitive first as it's most reliable local branch = vim.g.loaded_fugitive == 1 and vim.fn.FugitiveHead() or nil -- buffer can be null and code will crash with: -- E5108: Error executing lua ... 'attempt to index a nil value' if not buffer or not (buffer.bufnr > 0) then return end -- fugitive is empty or not loaded, try gitsigns if not branch or #branch == 0 then local ok, res = pcall(vim.api.nvim_buf_get_var, buffer.bufnr, "gitsigns_head") if ok then branch = res end end -- last resort run git command if not branch then local j = job:new { command = "git", args = { "branch", "--show-current" }, cwd = vim.fn.fnamemodify(buffer.name, ":h"), } local ok, result = pcall(function() return vim.trim(j:sync()[1]) end) if ok then branch = result end end if branch and #branch > 0 then local fmt = opts.fmt or "%s %s" local icon = opts.icon or "" return set_hl(opts.hl, (fmt):format(icon, branch)) end end)) end local function git_changes_formatter(opts) local specs = { insert = { regex = "(%d+) insertions?", icon = opts.icon_insert or "+", hl = opts.hl_insert, }, change = { regex = "(%d+) files? changed", icon = opts.icon_change or "~", hl = opts.hl_change, }, delete = { regex = "(%d+) deletions?", icon = opts.icon_delete or "-", hl = opts.hl_delete, }, } return function(_, _, s) local result = {} for k, v in pairs(specs) do local count = nil if type(s) == "string" then -- 'git diff --shortstat' output -- from 'git_changes_all' count = tonumber(string.match(s, v.regex)) else -- map from 'git_changes_buf' count = s[k] end if count and count > 0 then table.insert(result, set_hl(v.hl, ("%s%d"):format(v.icon, count))) end end return table.concat(result, " ") end end -- requires gitsigns function M.git_changes_buf(opts) opts = opts or {} local formatter = opts.formatter or git_changes_formatter(opts) return wrap_fnc(opts, function(window, buffer) local stats = {} if buffer and buffer.bufnr > 0 then local ok, res = pcall(vim.api.nvim_buf_get_var, buffer.bufnr, "vgit_status") if ok then stats = res end end if buffer and buffer.bufnr > 0 then local ok, res = pcall(vim.api.nvim_buf_get_var, buffer.bufnr, "gitsigns_status_dict") if ok then stats = res end end local counts = { insert = stats.added > 0 and stats.added or nil, change = stats.changed > 0 and stats.changed or nil, delete = stats.removed > 0 and stats.removed or nil, } if not vim.tbl_isempty(counts) then local fmt = opts.fmt or "%s" local out = formatter(window, buffer, counts) return out and fmt:format(out) or nil else -- el functions that return a -- string must not return nil return "" end end) end function M.git_changes_all(opts) opts = opts or {} local formatter = opts.formatter or git_changes_formatter(opts) return el_sub.buf_autocmd("el_git_changes", "BufWritePost", wrap_fnc(opts, function(window, buffer) if not buffer or not (buffer.bufnr > 0) or vim.bo[buffer.bufnr].bufhidden ~= "" or vim.bo[buffer.bufnr].buftype == "nofile" or vim.fn.filereadable(buffer.name) ~= 1 then return end local j = job:new { command = "git", args = { "diff", "--shortstat" }, -- makes no sense to run for one file as -- 'file(s) changed' will always be 1 -- args = { "diff", "--shortstat", buffer.name }, cwd = vim.fn.fnamemodify(buffer.name, ":h"), } local ok, git_changes = pcall(function() return formatter(window, buffer, vim.trim(j:sync()[1])) end) if ok then local fmt = opts.fmt or "%s" return git_changes and fmt:format(git_changes) or nil end end)) end function M.lsp_srvname(opts) local fmt = opts.fmt or "%s" local icon = opts.icon or "" local names = "" vim.lsp.for_each_buffer_client(0, function(client) if names ~= "" then names = names..", " end names = names..client.name end) return icon..fmt:format(names) end local function diag_formatter(opts) return function(_, buffer, counts) local items = {} local icons = { ["errors"] = { opts.icon_err or "x", opts.hl_err or "DiagnosticError"}, ["warnings"] = { opts.icon_warn or "!", opts.hl_warn or "DiagnosticWarn"}, ["infos"] = { opts.icon_info or "i", opts.hl_info or "DiagnosticInfo"}, ["hints"] = { opts.icon_hint or "h", opts.hl_hint or "DiagnosticHint"}, } for _, k in ipairs({ "errors", "warnings", "infos", "hints" }) do if counts[k] > 0 then table.insert(items, set_hl(icons[k][2], ("%s:%s"):format(icons[k][1], counts[k]))) end end local fmt = opts.fmt or "%s" if vim.tbl_isempty(items) then return "" else return fmt:format(table.concat(items, " ")) end end end local function get_buffer_counts(diagnostic, _, buffer) local counts = { 0, 0, 0, 0 } local diags = diagnostic.get(buffer.bufnr) if diags and not vim.tbl_isempty(diags) then for _, d in ipairs(diags) do if tonumber(d.severity) then counts[d.severity] = counts[d.severity] + 1 end end end return { errors = counts[1], warnings = counts[2], infos = counts[3], hints = counts[4], } end function M.diagnostics(opts) opts = opts or {} local formatter = opts.formatter or diag_formatter(opts) return el_sub.buf_autocmd("el_buf_diagnostic", "LspAttach,DiagnosticChanged", wrap_fnc(opts, function(window, buffer) return formatter(window, buffer, get_buffer_counts(vim.diagnostic, window, buffer)) end)) end function M.line(opts) opts = opts or {} local fmt = opts.fmt or "%s" return wrap_fnc(opts, function(_, _) local al = vim.api.nvim_buf_line_count(0) local cl = vim.api.nvim_win_get_cursor(0)[1] return (fmt):format(cl.."/"..al.." "..math.floor((cl / al) * 100).."%%") end) end return M