Skip to content

Commit 5d2a86a

Browse files
feat(api): add public inline diff rendering API
Expose codediff's inline diff rendering as a reusable Lua API for both standalone use and plugin consumers (e.g. gitsigns.nvim). Low-level API (re-exports, no state): - codediff.diff() — pure diff computation - codediff.render_inline_diff() — render on any buffer - codediff.clear_inline_diff() — clear decorations High-level API (standalone with git integration): - codediff.render_inline() — toggle inline diff per-buffer or globally - codediff.change_base() — change base revision for comparison
1 parent 93cd80c commit 5d2a86a

4 files changed

Lines changed: 283 additions & 8 deletions

File tree

lua/codediff/core/git.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,22 @@ function M.get_file_content(revision, git_root, rel_path, callback)
302302
end)
303303
end
304304

305+
-- Get file content for a buffer's file at a given revision (async convenience)
306+
-- Chains get_git_root -> get_relative_path -> get_file_content
307+
-- callback: function(err, lines, git_root, rel_path)
308+
function M.get_buf_file_content(file_path, revision, callback)
309+
M.get_git_root(file_path, function(err_root, git_root)
310+
if err_root then
311+
callback(err_root, nil)
312+
return
313+
end
314+
local rel_path = M.get_relative_path(file_path, git_root)
315+
M.get_file_content(revision, git_root, rel_path, function(err_content, lines)
316+
callback(err_content, lines, git_root, rel_path)
317+
end)
318+
end)
319+
end
320+
305321
-- Check if a git status code indicates a merge conflict
306322
-- Git uses these status codes for conflicts:
307323
-- U = unmerged (both modified, added by us/them, deleted by us/them)

lua/codediff/init.lua

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,36 @@ function M.prev_file()
4040
return navigation.prev_file()
4141
end
4242

43+
-- ============================================================================
44+
-- Inline diff public API
45+
-- ============================================================================
46+
47+
-- Low-level: re-export core diff and inline rendering functions
48+
-- See codediff.core.diff and codediff.ui.inline for full signatures
49+
M.diff = function(...)
50+
return require("codediff.core.diff").compute_diff(...)
51+
end
52+
M.render_inline_diff = function(...)
53+
return require("codediff.ui.inline").render_inline_diff(...)
54+
end
55+
M.clear_inline_diff = function(...)
56+
return require("codediff.ui.inline").clear(...)
57+
end
58+
59+
-- Toggle inline diff rendering (standalone mode with git integration)
60+
-- @param show nil|bool: nil = toggle, true = show, false = hide
61+
-- @param global bool|nil: if true, applies to all git-tracked buffers
62+
function M.render_inline(show, global)
63+
local inline_render = require("codediff.inline_render")
64+
inline_render.toggle(show, global)
65+
end
66+
67+
-- Change the base revision for diff comparison
68+
-- @param base string|nil: revision (e.g. "HEAD", "~1", "main"), nil = reset to index
69+
-- @param global bool|nil: if true, applies to all buffers
70+
function M.change_base(base, global)
71+
local inline_render = require("codediff.inline_render")
72+
inline_render.change_base(base, global)
73+
end
74+
4375
return M

lua/codediff/inline_render.lua

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
-- Inline diff rendering orchestrator
2+
-- Manages per-buffer state, async git pipeline, and global mode autocmds.
3+
-- Completely independent from :CodeDiff sessions (uses separate state).
4+
local M = {}
5+
6+
local DEFAULT_BASE = ":0" -- git index (staged content), matches gitsigns default
7+
8+
-- Per-buffer state: keyed by bufnr
9+
local inline_state = {}
10+
11+
-- Global mode state
12+
local global_state = {
13+
enabled = false,
14+
base = nil, -- nil = use DEFAULT_BASE
15+
augroup = nil,
16+
}
17+
18+
-- Guard for lazy highlight initialization
19+
local highlights_initialized = false
20+
21+
local function ensure_highlights()
22+
if not highlights_initialized then
23+
local highlights = require("codediff.ui.highlights")
24+
highlights.setup()
25+
highlights_initialized = true
26+
end
27+
end
28+
29+
-- Check if a buffer is part of an active :CodeDiff session.
30+
-- The codediff-inline namespace is shared, so rendering on a buffer
31+
-- that already has a :CodeDiff inline view would cause conflicts.
32+
local function is_in_codediff_session(bufnr)
33+
local ok, lifecycle = pcall(require, "codediff.ui.lifecycle")
34+
if not ok then
35+
return false
36+
end
37+
-- find_tabpage_by_buffer returns tabpage if buffer is in an active session
38+
return lifecycle.find_tabpage_by_buffer(bufnr) ~= nil
39+
end
40+
41+
-- Internal: fetch git content, compute diff, and render
42+
function M.render_buf(bufnr, base)
43+
base = base or global_state.base or DEFAULT_BASE
44+
45+
if not vim.api.nvim_buf_is_valid(bufnr) then
46+
return
47+
end
48+
49+
local file_path = vim.api.nvim_buf_get_name(bufnr)
50+
if file_path == "" then
51+
return
52+
end
53+
54+
if is_in_codediff_session(bufnr) then
55+
return
56+
end
57+
58+
local git = require("codediff.core.git")
59+
git.get_buf_file_content(file_path, base, function(err, original_lines, git_root, rel_path)
60+
vim.schedule(function()
61+
if not vim.api.nvim_buf_is_valid(bufnr) then
62+
return
63+
end
64+
65+
if err then
66+
-- Non-git buffer or git error: silent no-op
67+
if err:match("Not in a git repository") then
68+
return
69+
end
70+
-- File not in base revision (new file): treat as all-added
71+
if err:match("not found in revision") then
72+
original_lines = {}
73+
else
74+
vim.notify("[codediff] " .. err, vim.log.levels.WARN)
75+
return
76+
end
77+
end
78+
79+
ensure_highlights()
80+
81+
local modified_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
82+
local config = require("codediff.config")
83+
local diff_result = require("codediff.core.diff").compute_diff(original_lines, modified_lines, {
84+
max_computation_time_ms = config.options.diff.max_computation_time_ms,
85+
ignore_trim_whitespace = config.options.diff.ignore_trim_whitespace,
86+
})
87+
88+
require("codediff.ui.inline").render_inline_diff(bufnr, diff_result, original_lines, modified_lines)
89+
90+
inline_state[bufnr] = { active = true, base = base, git_root = git_root, rel_path = rel_path }
91+
end)
92+
end)
93+
end
94+
95+
-- Clear inline diff decorations and remove state for a buffer
96+
function M.clear_buf(bufnr)
97+
local inline = require("codediff.ui.inline")
98+
inline.clear(bufnr)
99+
inline_state[bufnr] = nil
100+
end
101+
102+
-- Toggle inline diff rendering
103+
-- @param show boolean|nil: true = show, false = hide, nil = toggle
104+
-- @param global boolean|nil: if true, applies to all git-tracked buffers
105+
function M.toggle(show, global)
106+
if global then
107+
-- Resolve toggle
108+
if show == nil then
109+
show = not global_state.enabled
110+
end
111+
if show then
112+
M.enable_global()
113+
else
114+
M.disable_global()
115+
end
116+
else
117+
local bufnr = vim.api.nvim_get_current_buf()
118+
-- Resolve toggle
119+
if show == nil then
120+
show = not (inline_state[bufnr] and inline_state[bufnr].active)
121+
end
122+
if show then
123+
M.render_buf(bufnr)
124+
else
125+
M.clear_buf(bufnr)
126+
end
127+
end
128+
end
129+
130+
-- Change base revision for inline diff
131+
-- @param base string|nil: revision (e.g. "HEAD", "~1", "main"), nil = reset to index
132+
-- @param global boolean|nil: if true, applies to all buffers
133+
function M.change_base(base, global)
134+
base = base or DEFAULT_BASE
135+
136+
if global then
137+
global_state.base = base
138+
for buf, state in pairs(inline_state) do
139+
if state.active and vim.api.nvim_buf_is_valid(buf) then
140+
M.render_buf(buf, base)
141+
end
142+
end
143+
else
144+
local bufnr = vim.api.nvim_get_current_buf()
145+
if inline_state[bufnr] and inline_state[bufnr].active then
146+
M.render_buf(bufnr, base)
147+
end
148+
end
149+
end
150+
151+
-- Enable global mode: render inline diffs on all file buffers, auto-render on BufEnter
152+
function M.enable_global()
153+
if global_state.enabled then
154+
return
155+
end
156+
global_state.enabled = true
157+
158+
local augroup = vim.api.nvim_create_augroup("codediff_inline_global", { clear = true })
159+
global_state.augroup = augroup
160+
161+
-- Render current buffer immediately
162+
local bufnr = vim.api.nvim_get_current_buf()
163+
if vim.bo[bufnr].buftype == "" then
164+
M.render_buf(bufnr)
165+
end
166+
167+
vim.api.nvim_create_autocmd("BufEnter", {
168+
group = augroup,
169+
callback = function(args)
170+
if vim.bo[args.buf].buftype == "" and not (inline_state[args.buf] and inline_state[args.buf].active) then
171+
M.render_buf(args.buf)
172+
end
173+
end,
174+
})
175+
176+
vim.api.nvim_create_autocmd("BufWritePost", {
177+
group = augroup,
178+
callback = function(args)
179+
if inline_state[args.buf] and inline_state[args.buf].active then
180+
M.render_buf(args.buf)
181+
end
182+
end,
183+
})
184+
185+
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
186+
group = augroup,
187+
callback = function(args)
188+
inline_state[args.buf] = nil
189+
end,
190+
})
191+
192+
vim.api.nvim_create_autocmd("ColorScheme", {
193+
group = augroup,
194+
callback = function()
195+
highlights_initialized = false
196+
for buf, state in pairs(inline_state) do
197+
if state.active and vim.api.nvim_buf_is_valid(buf) then
198+
M.render_buf(buf)
199+
end
200+
end
201+
end,
202+
})
203+
end
204+
205+
-- Disable global mode: clear all inline diffs and remove autocmds
206+
function M.disable_global()
207+
if not global_state.enabled then
208+
return
209+
end
210+
211+
local inline = require("codediff.ui.inline")
212+
for buf, _ in pairs(inline_state) do
213+
if vim.api.nvim_buf_is_valid(buf) then
214+
inline.clear(buf)
215+
end
216+
end
217+
inline_state = {}
218+
219+
if global_state.augroup then
220+
vim.api.nvim_del_augroup_by_id(global_state.augroup)
221+
global_state.augroup = nil
222+
end
223+
224+
global_state.enabled = false
225+
end
226+
227+
return M

lua/codediff/ui/inline.lua

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ end
279279
-- Apply char-level highlights on modified buffer lines
280280
-- ============================================================================
281281

282-
local function apply_modified_char_highlights(bufnr, inner_changes, modified_lines)
282+
local function apply_modified_char_highlights(bufnr, inner_changes, modified_lines, char_priority)
283283
if not inner_changes then
284284
return
285285
end
@@ -321,7 +321,7 @@ local function apply_modified_char_highlights(bufnr, inner_changes, modified_lin
321321
pcall(vim.api.nvim_buf_set_extmark, bufnr, M.ns_inline, line_idx, start_col - 1, {
322322
end_col = end_col - 1,
323323
hl_group = "CodeDiffCharInsert",
324-
priority = 200,
324+
priority = char_priority,
325325
})
326326
end
327327
else
@@ -332,7 +332,7 @@ local function apply_modified_char_highlights(bufnr, inner_changes, modified_lin
332332
end_line = first_idx + 1,
333333
end_col = 0,
334334
hl_group = "CodeDiffCharInsert",
335-
priority = 200,
335+
priority = char_priority,
336336
})
337337
end
338338
for line = start_line + 1, end_line - 1 do
@@ -342,7 +342,7 @@ local function apply_modified_char_highlights(bufnr, inner_changes, modified_lin
342342
end_line = idx + 1,
343343
end_col = 0,
344344
hl_group = "CodeDiffCharInsert",
345-
priority = 200,
345+
priority = char_priority,
346346
})
347347
end
348348
end
@@ -352,7 +352,7 @@ local function apply_modified_char_highlights(bufnr, inner_changes, modified_lin
352352
pcall(vim.api.nvim_buf_set_extmark, bufnr, M.ns_inline, last_idx, 0, {
353353
end_col = end_col - 1,
354354
hl_group = "CodeDiffCharInsert",
355-
priority = 200,
355+
priority = char_priority,
356356
})
357357
end
358358
end
@@ -374,7 +374,7 @@ end
374374
-- @param diff_result table: The diff result from core/diff.compute_diff
375375
-- @param original_lines string[]: Lines from the original (reference) content
376376
-- @param modified_lines string[]: Lines from the modified buffer
377-
-- @param opts? table: { filetype?: string } for syntax highlighting on virt_lines
377+
-- @param opts? table: { filetype?: string, priority?: number } for syntax highlighting on virt_lines
378378
function M.render_inline_diff(bufnr, diff_result, original_lines, modified_lines, opts)
379379
-- Clear previous inline decorations
380380
vim.api.nvim_buf_clear_namespace(bufnr, M.ns_inline, 0, -1)
@@ -390,7 +390,7 @@ function M.render_inline_diff(bufnr, diff_result, original_lines, modified_lines
390390
local syntax_hls = M.compute_syntax_highlights(original_lines, filetype)
391391

392392
local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
393-
local highlight_priority = config.options.diff.highlight_priority
393+
local highlight_priority = (opts and opts.priority) or config.options.diff.highlight_priority
394394

395395
for _, mapping in ipairs(diff_result.changes) do
396396
local orig_start = mapping.original.start_line
@@ -455,7 +455,7 @@ function M.render_inline_diff(bufnr, diff_result, original_lines, modified_lines
455455

456456
-- Step 3: Character-level highlights on modified buffer lines
457457
if has_modified and mapping.inner_changes then
458-
apply_modified_char_highlights(bufnr, mapping.inner_changes, modified_lines)
458+
apply_modified_char_highlights(bufnr, mapping.inner_changes, modified_lines, highlight_priority + 100)
459459
end
460460
end
461461
end

0 commit comments

Comments
 (0)