local M = {} ---@class ow.Git.ScratchOpts ---@field name string? ---@field bufhidden ("hide"|"wipe")? ---@param buf integer ---@param opts ow.Git.ScratchOpts local function setup_scratch(buf, opts) vim.bo[buf].buftype = "nofile" vim.bo[buf].bufhidden = opts.bufhidden or "wipe" vim.bo[buf].swapfile = false vim.bo[buf].modifiable = false vim.bo[buf].modified = 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.NewScratchOpts : ow.Git.ScratchOpts ---@field split (false|"above"|"below"|"left"|"right")? ---@param opts ow.Git.NewScratchOpts? ---@return integer buf ---@return integer win function M.new_scratch(opts) opts = opts or {} local buf = vim.api.nvim_create_buf(false, true) 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 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 = assert(vim.uv.new_timer()) 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.ExecOpts ---@field cwd string? ---@field stdin string? ---@field silent boolean? ---@field on_done fun(stdout: string?)? ---@param cmd string[] ---@param opts ow.Git.ExecOpts? ---@return string? function M.exec(cmd, opts) opts = opts or {} local sys_opts = { cwd = opts.cwd, stdin = opts.stdin, text = true } local function handle(result) 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 if opts.on_done then vim.system( cmd, sys_opts, vim.schedule_wrap(function(result) opts.on_done(handle(result)) end) ) return nil end return handle(vim.system(cmd, sys_opts):wait()) end return M