From 6a86a75ed5ecf6061e97389e8a7d6acc56279747 Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Mon, 27 Apr 2026 16:02:14 +0200 Subject: [PATCH] refactor(git): convert blocking subprocess calls to async --- lua/git/cmd.lua | 63 +++++++++++---------- lua/git/commit.lua | 34 ++++++----- lua/git/diff.lua | 79 +++++++++++++++----------- lua/git/init.lua | 27 +++++++++ lua/git/log_win.lua | 55 +++++++++--------- lua/git/repo.lua | 100 ++++++++++++++++----------------- lua/git/show.lua | 109 ++++++++++++++++++++++-------------- lua/git/status_win.lua | 124 ++++++++++++++++++++--------------------- 8 files changed, 338 insertions(+), 253 deletions(-) diff --git a/lua/git/cmd.lua b/lua/git/cmd.lua index 91b94e4..27864ec 100644 --- a/lua/git/cmd.lua +++ b/lua/git/cmd.lua @@ -1,3 +1,4 @@ +local git = require("git") local log = require("log") local repo = require("git.repo") local util = require("util") @@ -39,11 +40,7 @@ local function prefetch_cmds() vim.system( { "git", "--list-cmds=main,others,alias" }, { text = true }, - function(result) - vim.schedule(function() - populate_cached_cmds(result) - end) - end + vim.schedule_wrap(populate_cached_cmds) ) end @@ -75,33 +72,41 @@ end ---@param args string[] ---@param conf ow.Git.SplitHandler local function run_in_split(worktree, args, conf) - vim.cmd("new") - local buf = vim.api.nvim_get_current_buf() - vim.bo[buf].buftype = "nofile" - vim.bo[buf].bufhidden = "hide" - vim.bo[buf].swapfile = false - vim.bo[buf].modifiable = false + local buf = git.new_scratch() vim.b[buf].git_worktree = worktree if conf.needs_ref then local user_ref = first_positional(args, 2) or "HEAD" - local sha = repo.rev_parse(worktree, user_ref, true) - if sha then - vim.b[buf].git_ref = sha - vim.b[buf].git_parent_ref = - repo.rev_parse(worktree, user_ref .. "^", true) + local function apply_name(label) + if not vim.api.nvim_buf_is_valid(buf) then + return + end + pcall(vim.api.nvim_buf_set_name, buf, "git://" .. label .. "/") + vim.bo[buf].filetype = conf.ft end - pcall( - vim.api.nvim_buf_set_name, - buf, - "git://" .. (sha or user_ref) .. "/" - ) + repo.rev_parse(worktree, user_ref, true, function(sha) + if not sha then + apply_name(user_ref) + return + end + repo.rev_parse(worktree, user_ref .. "^", true, function(parent) + if not vim.api.nvim_buf_is_valid(buf) then + return + end + vim.b[buf].git_ref = sha + vim.b[buf].git_parent_ref = parent + apply_name(sha) + end) + end) + else + vim.bo[buf].filetype = conf.ft end - vim.bo[buf].filetype = conf.ft local cmd = { "git" } vim.list_extend(cmd, args) - vim.system(cmd, { cwd = worktree, text = true }, function(obj) - vim.schedule(function() + vim.system( + cmd, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(obj) if not vim.api.nvim_buf_is_valid(buf) then return end @@ -124,7 +129,7 @@ local function run_in_split(worktree, args, conf) ) end end) - end) + ) end ---@param worktree string @@ -132,8 +137,10 @@ end local function run_to_messages(worktree, args) local cmd = { "git" } vim.list_extend(cmd, args) - vim.system(cmd, { cwd = worktree, text = true }, function(obj) - vim.schedule(function() + vim.system( + cmd, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(obj) local out = vim.trim(obj.stdout or "") local err = vim.trim(obj.stderr or "") local chunks = {} @@ -156,7 +163,7 @@ local function run_to_messages(worktree, args) vim.api.nvim_echo(chunks, true, {}) end end) - end) + ) end ---@param args string[] diff --git a/lua/git/commit.lua b/lua/git/commit.lua index ebccde1..fd74343 100644 --- a/lua/git/commit.lua +++ b/lua/git/commit.lua @@ -67,20 +67,26 @@ function M.commit(opts) if amend then table.insert(cmd, "--amend") end - local result = vim.system(cmd, { cwd = worktree, text = true }) - :wait() - if result.code ~= 0 then - log.error( - "git commit failed: %s", - vim.trim(result.stderr or "") - ) - return - end - local out = vim.trim(result.stdout or "") - if out ~= "" then - log.info("%s", out) - end - vim.api.nvim_buf_delete(buf, { force = true }) + vim.system( + cmd, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(result) + if result.code ~= 0 then + log.error( + "git commit failed: %s", + vim.trim(result.stderr or "") + ) + return + end + local out = vim.trim(result.stdout or "") + if out ~= "" then + log.info("%s", out) + end + if vim.api.nvim_buf_is_valid(buf) then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end) + ) end, }) end diff --git a/lua/git/diff.lua b/lua/git/diff.lua index 6374fc6..89f1607 100644 --- a/lua/git/diff.lua +++ b/lua/git/diff.lua @@ -59,47 +59,62 @@ local function attach_index_writer(buf, worktree, path) }) end +---Run `git show ` asynchronously and call `callback(lines?)` on the +---main loop. `lines` is nil on failure (the error is logged). ---@param worktree string ---@param revspec string anything `git show` accepts (e.g. `HEAD:foo`, `:foo`, blob SHA) ----@return string[] -local function read_show(worktree, revspec) - local result = vim.system( +---@param callback fun(lines: string[]?) +local function read_show_async(worktree, revspec, callback) + vim.system( { "git", "show", revspec }, - { cwd = worktree, text = true } + { cwd = worktree, text = true }, + vim.schedule_wrap(function(result) + if result.code ~= 0 then + log.error( + "git show %s failed: %s", + revspec, + vim.trim(result.stderr or "") + ) + callback(nil) + return + end + callback(util.split_lines(result.stdout or "")) + end) ) - :wait() - if result.code ~= 0 then - log.error( - "git show %s failed: %s", - revspec, - vim.trim(result.stderr or "") - ) - return {} - end - return util.split_lines(result.stdout or "") end ----Internal builder: run `git show `, drop the result into a fresh ----scratch buffer, and (when `is_index` is true) wire up the BufWriteCmd ----that writes back to the git index for `index_path`. +---Internal builder: create a scratch buffer immediately and asynchronously +---fill it with the content of `git show `. Returning the buffer +---synchronously lets callers wire up windows / `:diffthis` right away; the +---diff updates when the content arrives. The buffer starts non-modifiable +---and the index `BufWriteCmd` is only attached after a successful load, so +---a premature `:w` can't blow away the index entry with empty content. ---@param worktree string ---@param revspec string ---@param is_index boolean ---@param index_path string? required when is_index is true ---@return integer local function build_show_buf(worktree, revspec, is_index, index_path) - local lines = read_show(worktree, revspec) local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) - vim.bo[buf].buftype = is_index and "acwrite" or "nofile" + vim.bo[buf].buftype = "nofile" vim.bo[buf].bufhidden = "wipe" vim.bo[buf].swapfile = false - if is_index then - attach_index_writer(buf, worktree, assert(index_path)) - else - vim.bo[buf].modifiable = false - end + vim.bo[buf].modifiable = false vim.bo[buf].modified = false + read_show_async(worktree, revspec, function(lines) + if not vim.api.nvim_buf_is_valid(buf) then + return + end + vim.bo[buf].modifiable = true + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines or {}) + if is_index then + vim.bo[buf].buftype = "acwrite" + attach_index_writer(buf, worktree, assert(index_path)) + else + vim.bo[buf].modifiable = false + end + vim.bo[buf].modified = false + end) return buf end @@ -192,12 +207,14 @@ function M.split(opts) local label = is_index and "index" or opts.ref M.set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel) - local split_cmd = opts.vertical and "leftabove vertical sbuffer " - or "leftabove sbuffer " - vim.cmd(split_cmd .. other) - vim.cmd("diffthis") - vim.cmd("wincmd p") - vim.cmd("diffthis") + local cur_win = vim.api.nvim_get_current_win() + local other_win = vim.api.nvim_open_win(other, true, { + split = opts.vertical and "left" or "above", + win = cur_win, + }) + vim.wo[other_win].diff = true + vim.api.nvim_set_current_win(cur_win) + vim.wo[cur_win].diff = true end return M diff --git a/lua/git/init.lua b/lua/git/init.lua index 4af5f42..b9a8bc2 100644 --- a/lua/git/init.lua +++ b/lua/git/init.lua @@ -25,6 +25,33 @@ function M.head(path) return repo.head(path) end +---@class ow.Git.NewScratchOpts +---@field name string? +---@field bufhidden ("hide"|"wipe")? defaults to "hide" +---@field split ("above"|"below"|"left"|"right")? defaults to splitbelow-aware horizontal + +---Open a split with a fresh non-modifiable scratch buffer. Default split +---direction is horizontal, honouring `splitbelow`. Caller flips +---`modifiable`, fills the buffer, and sets `filetype` once content lands. +---@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) + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = opts.bufhidden or "hide" + 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 + local split = opts.split or (vim.o.splitbelow and "below" or "above") + local win = vim.api.nvim_open_win(buf, true, { split = split }) + return buf, win +end + function M.setup() for name, link in pairs(HIGHLIGHTS) do vim.api.nvim_set_hl(0, name, { link = link, default = true }) diff --git a/lua/git/log_win.lua b/lua/git/log_win.lua index ac4f321..3ea68c1 100644 --- a/lua/git/log_win.lua +++ b/lua/git/log_win.lua @@ -1,3 +1,4 @@ +local git = require("git") local log = require("log") local repo = require("git.repo") local util = require("util") @@ -13,32 +14,36 @@ function M.show() return end - local result = vim.system({ - "git", - "log", - "--graph", - "--all", - "--decorate", - "--date=short", - "--format=format:" .. LOG_FORMAT, - }, { cwd = worktree, text = true }):wait() - if result.code ~= 0 then - log.error("git log failed: %s", result.stderr or "") - return - end - - local lines = util.split_lines(result.stdout or "") - - vim.cmd("new") - local buf = vim.api.nvim_get_current_buf() - vim.bo[buf].buftype = "nofile" - vim.bo[buf].bufhidden = "hide" - vim.bo[buf].swapfile = false - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) - vim.bo[buf].modifiable = false - vim.bo[buf].modified = false + local buf = git.new_scratch() vim.b[buf].git_worktree = worktree - vim.bo[buf].filetype = "gitlog" + + vim.system( + { + "git", + "log", + "--graph", + "--all", + "--decorate", + "--date=short", + "--format=format:" .. LOG_FORMAT, + }, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(result) + if not vim.api.nvim_buf_is_valid(buf) then + return + end + if result.code ~= 0 then + log.error("git log failed: %s", vim.trim(result.stderr or "")) + return + end + local lines = util.split_lines(result.stdout or "") + vim.bo[buf].modifiable = true + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.bo[buf].modifiable = false + vim.bo[buf].modified = false + vim.bo[buf].filetype = "gitlog" + end) + ) end return M diff --git a/lua/git/repo.lua b/lua/git/repo.lua index 20cfe29..b2dca0c 100644 --- a/lua/git/repo.lua +++ b/lua/git/repo.lua @@ -1,7 +1,9 @@ local log = require("log") local util = require("util") -local UNMERGED = { +local M = {} + +M.UNMERGED = { DD = true, AU = true, UD = true, @@ -14,7 +16,7 @@ local UNMERGED = { ---@param code string porcelain v1 XY code ---@return string? char ---@return string? hl_group -local function indicator(code) +function M.indicator(code) if code == "" then return nil end @@ -24,7 +26,7 @@ local function indicator(code) if code == "!!" then return "!", "GitIgnored" end - if UNMERGED[code] then + if M.UNMERGED[code] then return "U", "GitUnmerged" end local x, y = code:sub(1, 1), code:sub(2, 2) @@ -43,7 +45,7 @@ end ---@param code string ---@return string? local function format(code) - local char, hl = indicator(code) + local char, hl = M.indicator(code) if not char then return nil end @@ -53,7 +55,7 @@ end ---@param path string ---@return string? gitdir ---@return string? worktree -local function resolve(path) +function M.resolve(path) local found = vim.fs.find(".git", { upward = true, path = path })[1] if not found then return nil @@ -88,12 +90,12 @@ end ---both when not inside a git repo. ---@return string? gitdir ---@return string? worktree -local function resolve_cwd() +function M.resolve_cwd() local path = vim.api.nvim_buf_get_name(0) if path == "" then path = vim.fn.getcwd() end - return resolve(path) + return M.resolve(path) end ---@class ow.Git.Repo @@ -152,15 +154,17 @@ local function do_refresh(repo) -- path lines, so it ignores `##` lines below. Running with `--branch` -- lets the sidebar reuse this single subprocess via the GitRefresh -- data payload instead of spawning its own. - vim.system({ - "git", - "-c", - "core.quotePath=false", - "status", - "--porcelain=v1", - "--branch", - }, { cwd = repo.worktree, text = true }, function(obj) - vim.schedule(function() + vim.system( + { + "git", + "-c", + "core.quotePath=false", + "status", + "--porcelain=v1", + "--branch", + }, + { cwd = repo.worktree, text = true }, + vim.schedule_wrap(function(obj) local statuses = {} if obj.code == 0 then for line in (obj.stdout or ""):gmatch("[^\r\n]+") do @@ -210,7 +214,7 @@ local function do_refresh(repo) }, }) end) - end) + ) end ---@param gitdir string @@ -244,7 +248,7 @@ local function register(buf) if path == "" then return nil end - local gitdir, worktree = resolve(path) + local gitdir, worktree = M.resolve(path) if not gitdir or not worktree then return nil end @@ -259,7 +263,7 @@ local function register(buf) end ---@param buf integer -local function unregister(buf) +function M.unregister(buf) local repo = repo_by_buf[buf] if not repo then return @@ -273,7 +277,7 @@ local function unregister(buf) end ---@param buf integer -local function refresh_buf(buf) +function M.refresh_buf(buf) if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then return end @@ -285,7 +289,7 @@ local function refresh_buf(buf) repo:refresh() end -local function stop_all() +function M.stop_all() for _, repo in pairs(repo_by_gitdir) do repo:stop_watcher() end @@ -293,8 +297,8 @@ end ---@param path string ---@return string? -local function head(path) - local gitdir = resolve(path) +function M.head(path) + local gitdir = M.resolve(path) if not gitdir then return nil end @@ -318,39 +322,35 @@ local function head(path) return nil end ----Resolve a git revision to its object SHA. Returns nil if the ref can't be ----resolved (root-commit's `^`, blob's `^`, malformed ref, etc.). When `short` ----is true, the result is abbreviated via `core.abbrev` (auto-extended by git ----to keep the prefix unique in the current repo). +---Resolve a git revision to its object SHA. Calls `callback(sha?)` on the +---main loop with nil if the ref can't be parsed (root-commit's `^`, blob's +---`^`, malformed ref, etc.). When `short` is true, the result is abbreviated +---via `core.abbrev` (auto-extended by git to keep the prefix unique in the +---current repo). ---@param worktree string ---@param ref string ----@param short? boolean ----@return string? -local function rev_parse(worktree, ref, short) +---@param short boolean +---@param callback fun(sha: string?) +function M.rev_parse(worktree, ref, short, callback) local cmd = { "git", "rev-parse", "--verify", "--quiet" } if short then table.insert(cmd, "--short") end table.insert(cmd, ref) - local result = vim.system(cmd, { cwd = worktree, text = true }):wait() - if result.code ~= 0 then - return nil - end - local sha = vim.trim(result.stdout or "") - if sha == "" then - return nil - end - return sha + vim.system( + cmd, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(result) + local sha + if result.code == 0 then + local trimmed = vim.trim(result.stdout or "") + if trimmed ~= "" then + sha = trimmed + end + end + callback(sha) + end) + ) end -return { - UNMERGED = UNMERGED, - head = head, - indicator = indicator, - refresh_buf = refresh_buf, - resolve = resolve, - resolve_cwd = resolve_cwd, - rev_parse = rev_parse, - stop_all = stop_all, - unregister = unregister, -} +return M diff --git a/lua/git/show.lua b/lua/git/show.lua index 05ed999..c8bb916 100644 --- a/lua/git/show.lua +++ b/lua/git/show.lua @@ -1,4 +1,5 @@ local diff = require("git.diff") +local git = require("git") local log = require("log") local repo = require("git.repo") local util = require("util") @@ -111,8 +112,8 @@ end ---@param ref string local function show_blob(worktree, blob, path, ref) local buf = blob_buf(worktree, blob, path, ref) - vim.cmd("normal! m'") - vim.cmd("buffer " .. buf) + vim.cmd.normal({ "m'", bang = true }) + vim.api.nvim_set_current_buf(buf) end ---@param ctx ow.Git.ShowContext @@ -127,53 +128,75 @@ local function show_diff(ctx, section) blob_buf(ctx.worktree, section.pre_blob, section.pre_path, parent) local right = blob_buf(ctx.worktree, section.post_blob, section.post_path, ctx.ref) - vim.cmd("normal! m'") - vim.cmd("buffer " .. left) - vim.cmd("diffthis") - vim.cmd("rightbelow vertical sbuffer " .. right) - vim.cmd("diffthis") - vim.cmd("wincmd p") + vim.cmd.normal({ "m'", bang = true }) + local left_win = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_buf(left) + vim.wo[left_win].diff = true + local right_win = + vim.api.nvim_open_win(right, true, { split = "right", win = left_win }) + vim.wo[right_win].diff = true + vim.api.nvim_set_current_win(left_win) end ---@param worktree string ---@param ref string function M.open_commit(worktree, ref) - local sha = repo.rev_parse(worktree, ref, true) or ref - local name = "git://" .. sha .. "/" - -- Reuse a previously-opened buffer for the same commit; commit SHAs - -- are immutable so the content is stable. - local existing = vim.fn.bufnr(name) - if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then - vim.cmd("normal! m'") - vim.cmd("buffer " .. existing) - return - end + repo.rev_parse(worktree, ref, true, function(resolved) + local sha = resolved or ref + local name = "git://" .. sha .. "/" + -- Reuse a previously-opened buffer for the same commit; commit SHAs + -- are immutable so the content is stable. + local existing = vim.fn.bufnr(name) + if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then + vim.api.nvim_open_win(existing, true, { + split = vim.o.splitbelow and "below" or "above", + }) + return + end - local result = vim.system( - { "git", "show", ref }, - { cwd = worktree, text = true } - ) - :wait() - if result.code ~= 0 then - log.error("git show %s failed: %s", ref, result.stderr or "") - return - end - local lines = util.split_lines(result.stdout or "") - local parent = repo.rev_parse(worktree, ref .. "^", true) - local buf = vim.api.nvim_create_buf(false, true) - vim.bo[buf].buftype = "nofile" - vim.bo[buf].bufhidden = "hide" - vim.bo[buf].swapfile = false - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) - vim.bo[buf].modifiable = false - vim.bo[buf].modified = false - pcall(vim.api.nvim_buf_set_name, buf, name) - vim.b[buf].git_worktree = worktree - vim.b[buf].git_ref = sha - vim.b[buf].git_parent_ref = parent - vim.bo[buf].filetype = "git" - vim.cmd("normal! m'") - vim.cmd("buffer " .. buf) + local buf, win = git.new_scratch({ name = name }) + vim.b[buf].git_worktree = worktree + vim.b[buf].git_ref = sha + + vim.system( + { "git", "show", ref }, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(result) + if result.code ~= 0 then + log.error( + "git show %s failed: %s", + ref, + vim.trim(result.stderr or "") + ) + -- Tear down the empty placeholder window+buffer so a + -- retry runs a fresh fetch instead of hitting the + -- cached-buffer branch and reopening an empty pane. + if vim.api.nvim_win_is_valid(win) then + pcall(vim.api.nvim_win_close, win, true) + end + if vim.api.nvim_buf_is_valid(buf) then + pcall(vim.api.nvim_buf_delete, buf, { force = true }) + end + return + end + if not vim.api.nvim_buf_is_valid(buf) then + return + end + local lines = util.split_lines(result.stdout or "") + repo.rev_parse(worktree, ref .. "^", true, function(parent) + if not vim.api.nvim_buf_is_valid(buf) then + return + end + vim.b[buf].git_parent_ref = parent + vim.bo[buf].modifiable = true + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.bo[buf].modifiable = false + vim.bo[buf].modified = false + vim.bo[buf].filetype = "git" + end) + end) + ) + end) end ---@return boolean dispatched true if the cursor was on an actionable line diff --git a/lua/git/status_win.lua b/lua/git/status_win.lua index e45a142..375eb16 100644 --- a/lua/git/status_win.lua +++ b/lua/git/status_win.lua @@ -1,4 +1,5 @@ local diff = require("git.diff") +local git = require("git") local log = require("log") local repo = require("git.repo") @@ -240,13 +241,15 @@ local function enrich_with_log(worktree, branch, groups, callback) end local pending = #fetches for _, f in ipairs(fetches) do - vim.system({ - "git", - "log", - "--format=%h %s", - f.range, - }, { cwd = worktree, text = true }, function(log_obj) - vim.schedule(function() + vim.system( + { + "git", + "log", + "--format=%h %s", + f.range, + }, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(log_obj) if log_obj.code == 0 then for line in (log_obj.stdout or ""):gmatch("[^\r\n]+") do local sha, subject = line:match("^(%S+)%s+(.+)$") @@ -270,7 +273,7 @@ local function enrich_with_log(worktree, branch, groups, callback) callback(branch, groups) end end) - end) + ) end end @@ -287,15 +290,17 @@ local function fetch_status(worktree, prefetched_stdout, callback) enrich_with_log(worktree, branch, groups, callback) return end - vim.system({ - "git", - "-c", - "core.quotePath=false", - "status", - "--porcelain=v1", - "--branch", - }, { cwd = worktree, text = true }, function(obj) - vim.schedule(function() + vim.system( + { + "git", + "-c", + "core.quotePath=false", + "status", + "--porcelain=v1", + "--branch", + }, + { cwd = worktree, text = true }, + vim.schedule_wrap(function(obj) if obj.code ~= 0 then log.error("git status failed: %s", vim.trim(obj.stderr or "")) local branch = { ahead = 0, behind = 0 } @@ -313,7 +318,7 @@ local function fetch_status(worktree, prefetched_stdout, callback) local branch, groups = parse_porcelain(obj.stdout or "") enrich_with_log(worktree, branch, groups, callback) end) - end) + ) end ---@param bufnr integer @@ -598,9 +603,7 @@ end ---@param win integer ---@param enabled boolean local function set_diff(win, enabled) - vim.api.nvim_win_call(win, function() - vim.cmd(enabled and "diffthis" or "diffoff") - end) + vim.wo[win].diff = enabled if enabled then vim.wo[win].foldenable = true vim.wo[win].foldlevel = 0 @@ -642,6 +645,21 @@ local function entry_key(entry) return entry.section .. "|" .. entry.path .. "|" .. (entry.orig or "") end +---Split `target_win` and put its buffer into the new window. Matches the +---semantics of `:set_current_win(target); :rightbelow/leftabove vertical +---split` (the new window inherits the target's buffer; caller swaps it +---afterwards via `nvim_win_set_buf`). +---@param target_win integer +---@param dir "left"|"right" +---@return integer +local function vsplit_at(target_win, dir) + return vim.api.nvim_open_win( + vim.api.nvim_win_get_buf(target_win), + true, + { split = dir, win = target_win } + ) +end + ---@param s ow.Git.StatusState ---@param entry ow.Git.StatusEntry ---@param focus_left boolean @@ -673,23 +691,17 @@ local function show_diff(s, entry, focus_left) end if left_win and not right_win then - vim.api.nvim_set_current_win(left_win) - vim.cmd("rightbelow vertical split") - right_win = vim.api.nvim_get_current_win() + right_win = vsplit_at(left_win, "right") reset_diff_win(right_win) elseif right_win and not left_win then - vim.api.nvim_set_current_win(right_win) - vim.cmd("leftabove vertical split") - left_win = vim.api.nvim_get_current_win() + left_win = vsplit_at(right_win, "left") reset_diff_win(left_win) elseif not (left_win or right_win) then local default_main = find_default_main_win(sidebar_win) if default_main then right_win = default_main reset_diff_win(right_win) - vim.api.nvim_set_current_win(default_main) - vim.cmd("leftabove vertical split") - left_win = vim.api.nvim_get_current_win() + left_win = vsplit_at(right_win, "left") reset_diff_win(left_win) else -- No reusable default-empty window. Open the diff pair by @@ -697,12 +709,9 @@ local function show_diff(s, entry, focus_left) -- when there are other windows to absorb the split; if the -- sidebar is the only window in the tab, the split has to take -- from the sidebar itself, so restore the width explicitly. - vim.api.nvim_set_current_win(sidebar_win) - vim.cmd("rightbelow vertical split") - right_win = vim.api.nvim_get_current_win() + right_win = vsplit_at(sidebar_win, "right") reset_diff_win(right_win) - vim.cmd("leftabove vertical split") - left_win = vim.api.nvim_get_current_win() + left_win = vsplit_at(right_win, "left") reset_diff_win(left_win) vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH) end @@ -756,13 +765,11 @@ local function action_stage() vim.system( { "git", "add", "--", entry.path }, { cwd = s.worktree }, - function(obj) + vim.schedule_wrap(function(obj) if obj.code ~= 0 then - vim.schedule(function() - log.error("git add failed: %s", vim.trim(obj.stderr or "")) - end) + log.error("git add failed: %s", vim.trim(obj.stderr or "")) end - end + end) ) end @@ -780,16 +787,18 @@ local function action_unstage() table.insert(cmd, entry.orig) end table.insert(cmd, entry.path) - vim.system(cmd, { cwd = s.worktree }, function(obj) - if obj.code ~= 0 then - vim.schedule(function() + vim.system( + cmd, + { cwd = s.worktree }, + vim.schedule_wrap(function(obj) + if obj.code ~= 0 then log.error( "git restore --staged failed: %s", vim.trim(obj.stderr or "") ) - end) - end - end) + end + end) + ) end local function action_discard() @@ -828,16 +837,14 @@ local function action_discard() vim.system( { "git", "checkout", "--", entry.path }, { cwd = s.worktree }, - function(obj) + vim.schedule_wrap(function(obj) if obj.code ~= 0 then - vim.schedule(function() - log.error( - "git checkout failed: %s", - vim.trim(obj.stderr or "") - ) - end) + log.error( + "git checkout failed: %s", + vim.trim(obj.stderr or "") + ) end - end + end) ) end else @@ -875,15 +882,8 @@ local function open(worktree) end local previous_win = vim.api.nvim_get_current_win() - vim.cmd("leftabove vertical new") - local win = vim.api.nvim_get_current_win() - local bufnr = vim.api.nvim_get_current_buf() - - vim.bo[bufnr].buftype = "nofile" - vim.bo[bufnr].bufhidden = "wipe" - vim.bo[bufnr].swapfile = false + local bufnr, win = git.new_scratch({ split = "left", bufhidden = "wipe" }) vim.bo[bufnr].filetype = "gitstatus" - vim.bo[bufnr].modifiable = false vim.wo[win].number = false vim.wo[win].relativenumber = false