local M = {} ---@class ow.Git.Util.ScratchOpts ---@field name string? ---@field bufhidden ("hide"|"wipe"|"delete")? ---@field buftype ("nofile"|"acwrite"|"nowrite")? ---@field modifiable boolean? ---@param buf integer ---@param opts ow.Git.Util.ScratchOpts function M.setup_scratch(buf, opts) vim.bo[buf].buftype = opts.buftype or "nofile" vim.bo[buf].bufhidden = opts.bufhidden or "wipe" vim.bo[buf].swapfile = false vim.bo[buf].modifiable = opts.modifiable == true vim.bo[buf].modified = false vim.bo[buf].buflisted = false if opts.name then pcall(vim.api.nvim_buf_set_name, buf, opts.name) end end ---@param buf integer ---@param name string function M.set_buf_name(buf, name) pcall(vim.api.nvim_buf_set_name, buf, name) local ft = vim.filetype.match({ buf = buf }) if ft then vim.bo[buf].filetype = ft end end ---@param buf integer ---@param split (false|"above"|"below"|"left"|"right")? ---@return integer win function M.place_buf(buf, split) if split == false then vim.cmd.normal({ "m'", bang = true }) vim.api.nvim_set_current_buf(buf) return vim.api.nvim_get_current_win() end local win = vim.api.nvim_open_win(buf, true, { split = split or (vim.o.splitbelow and "below" or "above"), }) vim.cmd.clearjumps() return win end ---@class ow.Git.Util.NewScratchOpts : ow.Git.Util.ScratchOpts ---@field split (false|"above"|"below"|"left"|"right")? ---@param opts ow.Git.Util.NewScratchOpts? ---@return integer buf ---@return integer win function M.new_scratch(opts) opts = opts or {} local buf = vim.api.nvim_create_buf(false, true) M.setup_scratch(buf, opts) return buf, M.place_buf(buf, opts.split) end ---@param fmt string ---@param ... any function M.error(fmt, ...) vim.notify(fmt:format(...), vim.log.levels.ERROR) end ---@param fmt string ---@param ... any function M.warning(fmt, ...) vim.notify(fmt:format(...), vim.log.levels.WARN) end ---@param fmt string ---@param ... any function M.info(fmt, ...) vim.notify(fmt:format(...), vim.log.levels.INFO) end ---@param fmt string ---@param ... any function M.debug(fmt, ...) vim.notify(fmt:format(...), vim.log.levels.DEBUG) end ---@param buf integer ---@param start integer ---@param end_ integer ---@param lines string[] function M.set_buf_lines(buf, start, end_, lines) if not vim.api.nvim_buf_is_loaded(buf) then return end local was_modifiable = vim.bo[buf].modifiable vim.bo[buf].modifiable = true vim.api.nvim_buf_set_lines(buf, start, end_, true, lines) vim.bo[buf].modifiable = was_modifiable vim.bo[buf].modified = false end ---@param content string ---@return string[] function M.split_lines(content) local lines = vim.split(content, "\n", { plain = true, trimempty = false }) if #lines > 0 and lines[#lines] == "" then table.remove(lines) end return lines end ---@class ow.Git.Util.DebounceHandle ---@field cancel fun() ---@field flush fun() ---@field pending fun(): boolean ---@field close fun() ---@generic F: fun(...) ---@param fn F ---@param delay integer ---@return F, ow.Git.Util.DebounceHandle function M.debounce(fn, delay) local timer, err = vim.uv.new_timer() if not timer then M.warning("git: failed to create timer: %s", err) local noop = function() end return fn, { cancel = noop, flush = noop, pending = function() return false end, close = noop, } end local args ---@type table? local gen = 0 local fired_gen = 0 local cb_main = vim.schedule_wrap(function() -- Identity check: the libuv fire may have been superseded by -- a re-arm or a cancel between the timer firing and this -- scheduled callback running. if fired_gen ~= gen or args == nil then return end local a = args args = nil fn(vim.F.unpack_len(a)) end) local cb_uv = function() fired_gen = gen cb_main() end local function call(...) args = vim.F.pack_len(...) gen = gen + 1 timer:start(delay, 0, cb_uv) end return call, { cancel = function() timer:stop() args = nil end, flush = function() if args == nil then return end timer:stop() local a = args args = nil fn(vim.F.unpack_len(a)) end, pending = function() return args ~= nil end, close = function() timer:stop() if not timer:is_closing() then timer:close() end args = nil end, } end ---@class ow.Git.Util.ExecOpts ---@field cwd string? ---@field stdin string? ---@field silent boolean? ---@field on_exit fun(result: vim.SystemCompleted)? ---@param cmd string[] ---@param opts ow.Git.Util.ExecOpts? ---@return string? function M.exec(cmd, opts) opts = opts or {} local sys_opts = { cwd = opts.cwd, stdin = opts.stdin, text = true } if opts.on_exit then vim.system(cmd, sys_opts, vim.schedule_wrap(opts.on_exit)) return nil end local result = vim.system(cmd, sys_opts):wait() if result.code ~= 0 then if not opts.silent then local label = cmd[2] and (cmd[1] .. " " .. cmd[2]) or cmd[1] or "?" M.error("%s failed: %s", label, vim.trim(result.stderr or "")) end return nil end return result.stdout or "" end ---@param args string[] ---@param opts ow.Git.Util.ExecOpts? ---@return string? function M.git(args, opts) local cmd = { "git" } vim.list_extend(cmd, args) return M.exec(cmd, opts) end ---@class ow.Git.Util.Emitter ---@field private _listeners table local Emitter = {} Emitter.__index = Emitter ---@return ow.Git.Util.Emitter function Emitter.new() return setmetatable({ _listeners = {} }, Emitter) end ---@param event T ---@param fn fun(...) ---@return fun() unsubscribe function Emitter:on(event, fn) local list = self._listeners[event] or {} self._listeners[event] = list table.insert(list, fn) return function() for i, f in ipairs(list) do if f == fn then table.remove(list, i) return end end end end ---@param event T function Emitter:emit(event, ...) for _, fn in ipairs(self._listeners[event] or {}) do fn(...) end end M.Emitter = Emitter return M