feat(highlight): add character-level intra-line diff highlighting
Line-level backgrounds (DiffsAdd/DiffsDelete) now get a second tier:
changed characters within modified lines receive an intense background
overlay (DiffsAddText/DiffsDeleteText at 70% alpha vs 40% for lines).
Treesitter foreground colors show through since the extmarks only set bg.
diff.lua extracts contiguous -/+ change groups from hunk lines and diffs
each group byte-by-byte using vim.diff(). An optional libvscodediff FFI
backend (lib.lua) auto-downloads the .so from codediff.nvim releases and
falls back to native if unavailable.
New config: highlights.intra.{enabled, algorithm, max_lines}. Gated by
max_lines (default 200) to avoid stalling on huge hunks. Priority 201
sits above treesitter (200) so the character bg always wins.
Closes #60
This commit is contained in:
parent
294cbad749
commit
997bc49f8b
7 changed files with 842 additions and 0 deletions
214
lua/diffs/lib.lua
Normal file
214
lua/diffs/lib.lua
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
local M = {}
|
||||
|
||||
local dbg = require('diffs.log').dbg
|
||||
|
||||
---@type table?
|
||||
local cached_handle = nil
|
||||
|
||||
---@type boolean
|
||||
local download_in_progress = false
|
||||
|
||||
---@return string
|
||||
local function get_os()
|
||||
local os_name = jit.os:lower()
|
||||
if os_name == 'osx' then
|
||||
return 'macos'
|
||||
end
|
||||
return os_name
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function get_arch()
|
||||
return jit.arch:lower()
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function get_ext()
|
||||
local os_name = jit.os:lower()
|
||||
if os_name == 'windows' then
|
||||
return 'dll'
|
||||
elseif os_name == 'osx' then
|
||||
return 'dylib'
|
||||
end
|
||||
return 'so'
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function lib_dir()
|
||||
return vim.fn.stdpath('data') .. '/diffs/lib'
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function lib_path()
|
||||
return lib_dir() .. '/libvscode_diff.' .. get_ext()
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function version_path()
|
||||
return lib_dir() .. '/version'
|
||||
end
|
||||
|
||||
local EXPECTED_VERSION = '2.18.0'
|
||||
|
||||
---@return boolean
|
||||
function M.has_lib()
|
||||
if cached_handle then
|
||||
return true
|
||||
end
|
||||
return vim.fn.filereadable(lib_path()) == 1
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.lib_path()
|
||||
return lib_path()
|
||||
end
|
||||
|
||||
---@return table?
|
||||
function M.load()
|
||||
if cached_handle then
|
||||
return cached_handle
|
||||
end
|
||||
|
||||
local path = lib_path()
|
||||
if vim.fn.filereadable(path) ~= 1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ffi = require('ffi')
|
||||
|
||||
ffi.cdef([[
|
||||
typedef struct {
|
||||
int start_line;
|
||||
int end_line;
|
||||
} DiffsLineRange;
|
||||
|
||||
typedef struct {
|
||||
int start_line;
|
||||
int start_col;
|
||||
int end_line;
|
||||
int end_col;
|
||||
} DiffsCharRange;
|
||||
|
||||
typedef struct {
|
||||
DiffsCharRange original;
|
||||
DiffsCharRange modified;
|
||||
} DiffsRangeMapping;
|
||||
|
||||
typedef struct {
|
||||
DiffsLineRange original;
|
||||
DiffsLineRange modified;
|
||||
DiffsRangeMapping* inner_changes;
|
||||
int inner_change_count;
|
||||
} DiffsDetailedMapping;
|
||||
|
||||
typedef struct {
|
||||
DiffsDetailedMapping* mappings;
|
||||
int count;
|
||||
int capacity;
|
||||
} DiffsDetailedMappingArray;
|
||||
|
||||
typedef struct {
|
||||
DiffsLineRange original;
|
||||
DiffsLineRange modified;
|
||||
} DiffsMovedText;
|
||||
|
||||
typedef struct {
|
||||
DiffsMovedText* moves;
|
||||
int count;
|
||||
int capacity;
|
||||
} DiffsMovedTextArray;
|
||||
|
||||
typedef struct {
|
||||
DiffsDetailedMappingArray changes;
|
||||
DiffsMovedTextArray moves;
|
||||
bool hit_timeout;
|
||||
} DiffsLinesDiff;
|
||||
|
||||
typedef struct {
|
||||
bool ignore_trim_whitespace;
|
||||
int max_computation_time_ms;
|
||||
bool compute_moves;
|
||||
bool extend_to_subwords;
|
||||
} DiffsDiffOptions;
|
||||
|
||||
DiffsLinesDiff* compute_diff(
|
||||
const char** original_lines,
|
||||
int original_count,
|
||||
const char** modified_lines,
|
||||
int modified_count,
|
||||
const DiffsDiffOptions* options
|
||||
);
|
||||
|
||||
void free_lines_diff(DiffsLinesDiff* diff);
|
||||
]])
|
||||
|
||||
local ok, handle = pcall(ffi.load, path)
|
||||
if not ok then
|
||||
dbg('failed to load libvscode_diff: %s', handle)
|
||||
return nil
|
||||
end
|
||||
|
||||
cached_handle = handle
|
||||
return handle
|
||||
end
|
||||
|
||||
---@param callback fun(handle: table?)
|
||||
function M.ensure(callback)
|
||||
if cached_handle then
|
||||
callback(cached_handle)
|
||||
return
|
||||
end
|
||||
|
||||
if M.has_lib() then
|
||||
callback(M.load())
|
||||
return
|
||||
end
|
||||
|
||||
if download_in_progress then
|
||||
dbg('download already in progress')
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
|
||||
download_in_progress = true
|
||||
|
||||
local dir = lib_dir()
|
||||
vim.fn.mkdir(dir, 'p')
|
||||
|
||||
local os_name = get_os()
|
||||
local arch = get_arch()
|
||||
local ext = get_ext()
|
||||
local filename = ('libvscode_diff_%s_%s_%s.%s'):format(os_name, arch, EXPECTED_VERSION, ext)
|
||||
local url = ('https://github.com/esmuellert/vscode-diff.nvim/releases/download/v%s/%s'):format(
|
||||
EXPECTED_VERSION,
|
||||
filename
|
||||
)
|
||||
|
||||
local dest = lib_path()
|
||||
vim.notify('[diffs] downloading libvscode_diff...', vim.log.levels.INFO)
|
||||
|
||||
local cmd = { 'curl', '-fSL', '-o', dest, url }
|
||||
|
||||
vim.system(cmd, {}, function(result)
|
||||
download_in_progress = false
|
||||
vim.schedule(function()
|
||||
if result.code ~= 0 then
|
||||
vim.notify('[diffs] failed to download libvscode_diff', vim.log.levels.WARN)
|
||||
dbg('curl failed: %s', result.stderr or '')
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
|
||||
local f = io.open(version_path(), 'w')
|
||||
if f then
|
||||
f:write(EXPECTED_VERSION)
|
||||
f:close()
|
||||
end
|
||||
|
||||
vim.notify('[diffs] libvscode_diff downloaded', vim.log.levels.INFO)
|
||||
callback(M.load())
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue