Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/pivi/diff.lua

Raw
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', '<Plug>(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,
}