local entry = require 'bugabinga.pivi.entry' local navigate = require 'bugabinga.pivi.navigate' local tool = require 'bugabinga.pivi.tool' local state = { window = nil, buffer = nil, } --- Reads a reported change back into the lines it added and removed. --- The tool numbers every changed line, so the change can be undone on the --- content it produced. local parse = function( change ) local removed, added = {}, {} for _, line in ipairs( vim.split( change or '', '\n', { plain = true, } ) ) do local mark = line:sub( 1, 1 ) local number, text = line:sub( 2 ):match '^%s*(%d+) (.*)$' if number then local record = { line = tonumber( number ), text = text, } if mark == '+' then table.insert( added, record ) elseif mark == '-' then table.insert( removed, record ) end end end return removed, added end --- The content that existed before a change, rebuilt from the change itself. --- @return string[] lines --- @return boolean faithful whether the content still matches what was changed local before = function( current, change ) local removed, added = parse( change ) local lines = vim.deepcopy( current ) local faithful = true for _, record in ipairs( added ) do if lines[record.line] ~= record.text then faithful = false end end local descending = vim.deepcopy( added ) table.sort( descending, function( left, right ) return left.line > right.line end ) for _, record in ipairs( descending ) do if lines[record.line] ~= nil then table.remove( lines, record.line ) end end local ascending = vim.deepcopy( removed ) table.sort( ascending, function( left, right ) return left.line < right.line end ) for _, record in ipairs( ascending ) do table.insert( lines, math.min( record.line, #lines + 1 ), record.text ) end return lines, faithful end local close = function() if state.window and vim.api.nvim_win_is_valid( state.window ) then pcall( vim.api.nvim_win_call, state.window, function() vim.cmd 'diffoff' end ) pcall( vim.api.nvim_win_close, state.window, true ) end if state.buffer and vim.api.nvim_buf_is_valid( state.buffer ) then pcall( vim.api.nvim_buf_delete, state.buffer, { force = true, } ) end state.window, state.buffer = nil, nil end --- Opens a completed change as an editor comparison. --- The change is compared against the content the tool produced, so ordinary --- diff motions and diff folds apply. --- @return number? changed the window showing the file --- @return number? original the window showing the content before the change --- @return boolean? faithful whether the file still matches what was changed local open = function( bufnr, line_number ) local item = entry.at( bufnr, line_number or vim.api.nvim_win_get_cursor( 0 )[1] ) if not item or item.kind ~= 'tool' then return nil end local change = tool.diff( item.meta ) local location = navigate.location_of( item ) if not change or not location then return nil end local working = vim.api.nvim_get_current_win() local changed = navigate.locate( bufnr, item.first ) if not changed then return nil end close() local target = vim.api.nvim_win_get_buf( changed ) local current = vim.api.nvim_buf_get_lines( target, 0, -1, false ) local original, faithful = before( current, change ) state.buffer = vim.api.nvim_create_buf( false, true ) vim.bo[state.buffer].buftype = 'nofile' vim.bo[state.buffer].bufhidden = 'wipe' vim.bo[state.buffer].swapfile = false vim.api.nvim_buf_set_lines( state.buffer, 0, -1, false, original ) vim.bo[state.buffer].modifiable = false vim.bo[state.buffer].filetype = vim.bo[target].filetype local label = 'pivi://' .. vim.fs.basename( location.path ) .. '/before' if not faithful then label = label .. ' (the file has moved on)' end pcall( vim.api.nvim_buf_set_name, state.buffer, label ) local opened = pcall( vim.api.nvim_win_call, changed, function() vim.cmd 'diffthis' vim.cmd 'leftabove vsplit' vim.api.nvim_win_set_buf( 0, state.buffer ) vim.cmd 'diffthis' state.window = vim.api.nvim_get_current_win() end ) if not opened then close() return nil end if vim.api.nvim_win_is_valid( working ) then vim.api.nvim_set_current_win( working ) end return changed, state.window, faithful end local bind = function( bufnr ) vim.keymap.set( 'n', '(pivi-compare-change)', function() open( bufnr ) end, { buffer = bufnr, desc = 'pivi compare the change of the entry', } ) end require( 'bugabinga.pivi.transcript' ).on_buffer( bind ) return { parse = parse, before = before, open = open, close = close, bind = bind, }