mirror of
https://github.com/zoriya/flake.git
synced 2026-08-15 18:43:42 +00:00
259 lines
6.3 KiB
Lua
259 lines
6.3 KiB
Lua
-- mini.diff source backed by jj.
|
|
|
|
local M = {}
|
|
|
|
local function resolve(buf_id)
|
|
local name = vim.api.nvim_buf_get_name(buf_id)
|
|
if name == "" then
|
|
return nil
|
|
end
|
|
|
|
local change_id, rel = name:match("^jj://([^/]+)/(.+)$")
|
|
if change_id then
|
|
-- The path in the uri is repo-relative; jj resolves it from anywhere in
|
|
-- the repo, so run from the current working directory.
|
|
return { rev = change_id .. "-", from = change_id, file = rel, rel = rel, cwd = vim.fn.getcwd() }
|
|
end
|
|
|
|
local path = vim.uv.fs_realpath(name)
|
|
if path == nil then
|
|
return nil
|
|
end
|
|
local root = vim.fs.root(path, ".jj")
|
|
return {
|
|
rev = "@-",
|
|
from = "@",
|
|
file = path,
|
|
rel = root and path:sub(#root + 2),
|
|
cwd = vim.fs.dirname(path),
|
|
}
|
|
end
|
|
|
|
local function set_ref_text(buf_id)
|
|
if not vim.api.nvim_buf_is_valid(buf_id) then
|
|
return
|
|
end
|
|
local target = resolve(buf_id)
|
|
if target == nil then
|
|
return
|
|
end
|
|
vim.system(
|
|
{ "jj", "file", "show", "--ignore-working-copy", "-r", target.rev, target.file },
|
|
{ cwd = target.cwd, text = true },
|
|
vim.schedule_wrap(function(obj)
|
|
if not vim.api.nvim_buf_is_valid(buf_id) then
|
|
return
|
|
end
|
|
local text = obj.code == 0 and obj.stdout or ""
|
|
pcall(MiniDiff.set_ref_text, buf_id, text)
|
|
end)
|
|
)
|
|
end
|
|
|
|
local function group_name(buf_id)
|
|
return "MiniDiffSourceJj" .. buf_id
|
|
end
|
|
|
|
-- One filesystem watcher per repo root, tracking the buffers attached to it.
|
|
-- jj rewrites `.jj/repo/op_heads/heads/` on every operation (edit, squash, …),
|
|
-- so watching it lets us refresh signs when jj state changes out from under us.
|
|
local watchers = {}
|
|
|
|
local function refresh_watcher(root)
|
|
local watcher = watchers[root]
|
|
if watcher == nil then
|
|
return
|
|
end
|
|
for buf_id in pairs(watcher.buffers) do
|
|
if vim.api.nvim_buf_is_valid(buf_id) then
|
|
set_ref_text(buf_id)
|
|
else
|
|
watcher.buffers[buf_id] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local function start_watcher(root)
|
|
local watcher = { buffers = {} }
|
|
local heads = root .. "/.jj/repo/op_heads/heads"
|
|
local handle = vim.uv.new_fs_event()
|
|
if handle ~= nil then
|
|
local ok = handle:start(heads, {}, function(err)
|
|
if err then
|
|
return
|
|
end
|
|
-- Debounce: an operation removes the old head and adds the new one,
|
|
-- firing several events; coalesce them into a single refresh.
|
|
if watcher.timer == nil then
|
|
watcher.timer = vim.uv.new_timer()
|
|
watcher.timer:start(
|
|
50,
|
|
0,
|
|
vim.schedule_wrap(function()
|
|
if watcher.timer ~= nil then
|
|
watcher.timer:close()
|
|
watcher.timer = nil
|
|
end
|
|
refresh_watcher(root)
|
|
end)
|
|
)
|
|
end
|
|
end)
|
|
if ok then
|
|
watcher.handle = handle
|
|
else
|
|
handle:close()
|
|
end
|
|
end
|
|
watchers[root] = watcher
|
|
return watcher
|
|
end
|
|
|
|
local function stop_watcher(root)
|
|
local watcher = watchers[root]
|
|
if watcher == nil then
|
|
return
|
|
end
|
|
if watcher.timer ~= nil then
|
|
watcher.timer:close()
|
|
watcher.timer = nil
|
|
end
|
|
if watcher.handle ~= nil then
|
|
watcher.handle:stop()
|
|
watcher.handle:close()
|
|
end
|
|
watchers[root] = nil
|
|
end
|
|
|
|
local buf_roots = {}
|
|
|
|
local function attach(buf_id)
|
|
local target = resolve(buf_id)
|
|
if target == nil then
|
|
return false
|
|
end
|
|
|
|
local root = vim.fs.root(target.cwd, ".jj")
|
|
if root == nil then
|
|
return false
|
|
end
|
|
|
|
local group = vim.api.nvim_create_augroup(group_name(buf_id), { clear = true })
|
|
vim.api.nvim_create_autocmd({ "BufWritePost", "BufReadPost", "BufEnter", "FocusGained", "FileChangedShellPost" }, {
|
|
group = group,
|
|
buffer = buf_id,
|
|
desc = "Refresh jj diff reference",
|
|
callback = function()
|
|
set_ref_text(buf_id)
|
|
end,
|
|
})
|
|
|
|
local watcher = watchers[root] or start_watcher(root)
|
|
watcher.buffers[buf_id] = true
|
|
buf_roots[buf_id] = root
|
|
|
|
set_ref_text(buf_id)
|
|
end
|
|
|
|
local function detach(buf_id)
|
|
pcall(vim.api.nvim_del_augroup_by_name, group_name(buf_id))
|
|
|
|
local root = buf_roots[buf_id]
|
|
buf_roots[buf_id] = nil
|
|
local watcher = root and watchers[root]
|
|
if watcher ~= nil then
|
|
watcher.buffers[buf_id] = nil
|
|
if next(watcher.buffers) == nil then
|
|
stop_watcher(root)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Squash the given hunks into the revision we diff against. jj has no index to
|
|
-- stage into, so instead we rebuild what the parent should look like (the
|
|
-- reference text with only those hunks applied) and hand it to `jj squash -i`
|
|
-- through a diff editor that just copies our file over jj's right side.
|
|
local function apply_hunks(buf_id, hunks)
|
|
local target = resolve(buf_id)
|
|
if target == nil or target.rel == nil then
|
|
return
|
|
end
|
|
|
|
local ref = vim.split(MiniDiff.get_buf_data(buf_id).ref_text, "\n")
|
|
-- mini.diff always terminates the reference text with a newline
|
|
if ref[#ref] == "" then
|
|
table.remove(ref)
|
|
end
|
|
local buf = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
|
|
|
|
-- Bottom-up so earlier hunks keep their reference line numbers.
|
|
local sorted = vim.deepcopy(hunks)
|
|
table.sort(sorted, function(a, b) return a.ref_start > b.ref_start end)
|
|
for _, hunk in ipairs(sorted) do
|
|
-- "add" hunks have no reference line, `ref_start` is the one above them
|
|
local from = hunk.ref_start + (hunk.ref_count == 0 and 1 or 0)
|
|
local to = from + hunk.ref_count - 1
|
|
local out = vim.list_slice(ref, 1, from - 1)
|
|
vim.list_extend(out, buf, hunk.buf_start, hunk.buf_start + hunk.buf_count - 1)
|
|
vim.list_extend(out, ref, to + 1, #ref)
|
|
ref = out
|
|
end
|
|
|
|
local tmp = vim.fn.tempname()
|
|
vim.fn.writefile(ref, tmp)
|
|
vim.system({
|
|
"jj",
|
|
"squash",
|
|
"--from",
|
|
target.from,
|
|
"--into",
|
|
target.rev,
|
|
"--use-destination-message",
|
|
"--tool",
|
|
"minidiff",
|
|
"--config",
|
|
'merge-tools.minidiff.program="cp"',
|
|
"--config",
|
|
string.format(
|
|
"merge-tools.minidiff.edit-args=[%s, %s]",
|
|
vim.json.encode(tmp),
|
|
vim.json.encode("$right/" .. target.rel)
|
|
),
|
|
"--",
|
|
target.rel,
|
|
}, { cwd = target.cwd, text = true }, vim.schedule_wrap(function(obj)
|
|
vim.fn.delete(tmp)
|
|
if obj.code ~= 0 then
|
|
vim.notify(obj.stderr, vim.log.levels.ERROR)
|
|
end
|
|
end))
|
|
end
|
|
|
|
M.source = {
|
|
name = "jj",
|
|
attach = attach,
|
|
detach = detach,
|
|
apply_hunks = apply_hunks,
|
|
}
|
|
|
|
-- support jj:// buffers
|
|
function M.setup()
|
|
local group = vim.api.nvim_create_augroup("MiniDiffJjRevisionBuffers", { clear = true })
|
|
vim.api.nvim_create_autocmd({ "BufWinEnter", "BufReadPost" }, {
|
|
group = group,
|
|
desc = "Enable mini.diff on jj revision buffers",
|
|
callback = function(ev)
|
|
vim.schedule(function()
|
|
if not vim.api.nvim_buf_is_valid(ev.buf) then
|
|
return
|
|
end
|
|
if vim.api.nvim_buf_get_name(ev.buf):match("^jj://") then
|
|
pcall(require("mini.diff").enable, ev.buf)
|
|
end
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
|
|
return M
|