---@class DiffResult ---@field content string[] ---@field highlights table[]? ---@field raw_diff string? ---@class DiffBackend ---@field name string ---@field render fun(expected: string, actual: string): DiffResult local M = {} ---Vim's built-in diff backend using diffthis ---@type DiffBackend local vim_backend = { name = 'vim', render = function(_, actual) local actual_lines = vim.split(actual, '\n', { plain = true, trimempty = true }) return { content = actual_lines, highlights = nil, -- diffthis handles highlighting } end, } ---Git word-diff backend for character-level precision ---@type DiffBackend local git_backend = { name = 'git', render = function(expected, actual) -- Create temporary files for git diff local tmp_expected = vim.fn.tempname() local tmp_actual = vim.fn.tempname() vim.fn.writefile(vim.split(expected, '\n', { plain = true }), tmp_expected) vim.fn.writefile(vim.split(actual, '\n', { plain = true }), tmp_actual) local cmd = { 'git', 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix', tmp_expected, tmp_actual, } local result = vim.system(cmd, { text = true }):wait() -- Clean up temp files vim.fn.delete(tmp_expected) vim.fn.delete(tmp_actual) if result.code == 0 then return { content = vim.split(actual, '\n', { plain = true, trimempty = true }), highlights = {}, } else -- Parse git diff output to extract content and highlights local diff_content = result.stdout or '' local lines = {} local highlights = {} local line_num = 0 -- Extract content lines that start with space, +, or - for line in diff_content:gmatch('[^\n]*') do if line:match('^[%s%+%-]') or (not line:match('^[@%-+]') and not line:match('^index') and not line:match('^diff')) then -- This is content, not metadata local clean_line = line if line:match('^[%+%-]') then clean_line = line:sub(2) -- Remove +/- prefix end -- Parse diff markers in the line local col_pos = 0 local processed_line = '' local i = 1 while i <= #clean_line do local removed_start, removed_end = clean_line:find('%[%-[^%-]*%-]', i) local added_start, added_end = clean_line:find('{%+[^%+]*%+}', i) local next_marker_start = nil local marker_type = nil if removed_start and (not added_start or removed_start < added_start) then next_marker_start = removed_start marker_type = 'removed' elseif added_start then next_marker_start = added_start marker_type = 'added' end if next_marker_start then -- Add text before marker if next_marker_start > i then local before_text = clean_line:sub(i, next_marker_start - 1) processed_line = processed_line .. before_text col_pos = col_pos + #before_text end -- Extract and add marker content with highlighting local marker_end = (marker_type == 'removed') and removed_end or added_end local marker_text = clean_line:sub(next_marker_start, marker_end) local content_text if marker_type == 'removed' then content_text = marker_text:sub(3, -3) -- Remove [- and -] table.insert(highlights, { line = line_num, col_start = col_pos, col_end = col_pos + #content_text, highlight_group = 'DiffDelete', }) else -- added content_text = marker_text:sub(3, -3) -- Remove {+ and +} table.insert(highlights, { line = line_num, col_start = col_pos, col_end = col_pos + #content_text, highlight_group = 'DiffAdd', }) end processed_line = processed_line .. content_text col_pos = col_pos + #content_text i = marker_end + 1 else -- No more markers, add rest of line local rest = clean_line:sub(i) processed_line = processed_line .. rest break end end table.insert(lines, processed_line) line_num = line_num + 1 end end return { content = lines, highlights = highlights, raw_diff = diff_content, } end end, } ---Available diff backends ---@type table local backends = { vim = vim_backend, git = git_backend, } ---Get available backend names ---@return string[] function M.get_available_backends() return vim.tbl_keys(backends) end ---Get a diff backend by name ---@param name string ---@return DiffBackend? function M.get_backend(name) return backends[name] end ---Check if git backend is available ---@return boolean function M.is_git_available() local result = vim.system({ 'git', '--version' }, { text = true }):wait() return result.code == 0 end ---Get the best available backend based on config and system availability ---@param preferred_backend? string ---@return DiffBackend function M.get_best_backend(preferred_backend) if preferred_backend and backends[preferred_backend] then if preferred_backend == 'git' and not M.is_git_available() then return backends.vim end return backends[preferred_backend] end return backends.vim end ---Render diff using specified backend ---@param expected string ---@param actual string ---@param backend_name? string ---@return DiffResult function M.render_diff(expected, actual, backend_name) local backend = M.get_best_backend(backend_name) return backend.render(expected, actual) end return M