Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions doc/diffs.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1174,22 +1174,80 @@ Save and quit after resolving conflicts. Use `:cq` to abort.
==============================================================================
API *diffs.nvim-api*

attach({bufnr}) *diffs.nvim.attach()*
Attach highlighting to a buffer. Called automatically when the buffer's
attach({bufnr}, {options}) *diffs.nvim.attach()*
Attach highlighting to a buffer. Called automatically when the buffer's
filetype matches `git`, `gitcommit`, an enabled integration filetype,
or any entry in the deprecated `extra_filetypes` option.
Direct calls work with any filetype. For automatic attachment to other
filetypes, see |diffs.nvim-extra-filetypes|.

Parameters: ~
{bufnr} (integer, optional) Buffer number. Defaults to current buffer.
{bufnr} (integer, optional) Buffer number. Omit or use 0 for current.
{options} (table, optional) ~
• {hunks} (function, optional) Receives the buffer number.
Returns |diffs.Hunk| tables in buffer order.
• {intra} (boolean, optional) Set false to disable intra-line
highlighting. Omit for automatic Difftastic detection.

Pass {options} to replace an attachment. Without it, repeated calls do
nothing. The {hunks} callback runs on attachment and on redraw after edits
or |diffs.nvim.refresh()|.

*diffs.Hunk*
Hunk fields: ~
{filename} (string) Source filename.
{start_line} (integer) One-based hunk header line. Use 0 when the
body starts on the first buffer line without a header.
{lines} (string[]) Diff body, including diff prefixes.
Element i is buffer line start_line + i (one-based).
Exclude display columns before the diff prefixes.
{column_offset} (integer, default: 0) Extra bytes before each prefix
in the displayed buffer.
{prefix_width} (integer, default: 1) Diff prefix width in bytes.
{quote_width} (integer, default: 0) Quote prefix width in bytes.
{ft} (string, optional) Vim syntax filetype.
{lang} (string, optional) Treesitter parser language.
{header_start_line} (integer, optional) First file header line
(one-based).
{header_lines} (string[], optional) File header lines.
{header_context} (string, optional) Code after the hunk header.
{header_context_col} (integer, optional) Zero-based byte column of
{header_context}, excluding {column_offset}.

detach({bufnr}) *diffs.nvim.detach()*
Stop highlighting the buffer and clear its highlights.

Parameters: ~
{bufnr} (integer, optional) Buffer number. Omit or use 0 for current.

refresh({bufnr}) *diffs.nvim.refresh()*
Manually refresh highlighting for a buffer. Useful after external changes
or for debugging.
Refresh highlighting on the next redraw. If supplied, the {hunks}
callback runs again even when the buffer text is unchanged.

Parameters: ~
{bufnr} (integer, optional) Buffer number. Defaults to current buffer.
{bufnr} (integer, optional) Buffer number. Omit or use 0 for current.

highlight_options() *diffs.nvim.highlight_options()*
Initialize highlight groups and return {hide_prefix} and {highlights}.
The {highlights.priorities} fields are {clear}, {syntax}, {line_bg},
and {char_bg}.

diff_options() *diffs.nvim.diff_options()*
Return options from 'diffopt': {algorithm}, {linematch}, {inline}, and
the whitespace flags {ignore_whitespace}, {ignore_whitespace_change},
{ignore_whitespace_change_at_eol}, and {ignore_blank_lines}.
Unset options are absent. An absent {inline} means "simple".

compute_intra_hunks({lines}) *diffs.nvim.compute_intra_hunks()*
Return {add_spans} and {del_spans} using 'diffopt', or nil when no spans apply.
{lines} contains diff body lines with one prefix byte per line.
Each span has a one-based {line} index into {lines}, and one-based byte
columns {col_start} and {col_end} after the prefix. {col_end} is exclusive.

whitespace_only_lines({lines}) *diffs.nvim.whitespace_only_lines()*
Return a table of one-based line indexes with value true for changes
ignored by the whitespace options in 'diffopt'. {lines} has the same
format as |diffs.nvim.compute_intra_hunks()|.

*diffs.ReviewSpec*
A `diffs.ReviewSpec` says what to review, as the arguments to |:Diff-review|
Expand Down
37 changes: 21 additions & 16 deletions lua/diffs/highlight.lua
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,13 @@ end
---@field highlights diffs.HunkHighlights
---@field defer_vim_syntax? boolean
---@field syntax_only? boolean
---@field intra? boolean

---@class diffs.HunkOptsOverride
---@field highlights? table
---@field defer_vim_syntax? boolean
---@field syntax_only? boolean
---@field intra? boolean

---@param config diffs.Config
---@param overrides? diffs.HunkOptsOverride
Expand Down Expand Up @@ -209,7 +211,8 @@ end
function M.highlight_hunk_prefixes(bufnr, ns, hunk, opts)
local p = opts.highlights.priorities
local pw = hunk.prefix_width or 1
local qw = hunk.quote_width or 0
local offset = hunk.column_offset or 0
local qw = (hunk.quote_width or 0) + offset
local extmark_count = 0

for i, line in ipairs(hunk.lines) do
Expand Down Expand Up @@ -395,7 +398,7 @@ local function apply_vim_syntax_spans(
leading_offset = leading_offset or 0

local hunk_line_count = #hunk.lines
local col_off = (hunk.prefix_width or 1) + (hunk.quote_width or 0) - 1
local col_off = (hunk.prefix_width or 1) + (hunk.quote_width or 0) + (hunk.column_offset or 0) - 1
local extmark_count = 0
for _, span in ipairs(spans) do
local adj = span.line - leading_offset
Expand Down Expand Up @@ -484,7 +487,8 @@ end
function M.highlight_hunk(bufnr, ns, hunk, opts)
local p = opts.highlights.priorities
local pw = hunk.prefix_width or 1
local qw = hunk.quote_width or 0
local offset = hunk.column_offset or 0
local qw = (hunk.quote_width or 0) + offset
local use_ts = hunk.lang and opts.highlights.treesitter
local use_vim = not use_ts and hunk.ft and opts.highlights.vim

Expand Down Expand Up @@ -559,7 +563,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
bufnr,
ns,
hunk,
hunk.header_context_col,
hunk.header_context_col + offset,
hunk.header_context,
hunk.lang,
new_code,
Expand Down Expand Up @@ -609,15 +613,16 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end

local at_raw_line
if (qw > 0 or pw > 1) and opts.highlights.treesitter then
if hunk.start_line > 0 and (qw > 0 or pw > 1) and opts.highlights.treesitter then
local at_buf_line = hunk.start_line - 1
at_raw_line = vim.api.nvim_buf_get_lines(bufnr, at_buf_line, at_buf_line + 1, false)[1]
end

---@type diffs.IntraChanges?
local intra = nil
local intra_cfg = opts.highlights.intra
local difft_active = vim.b[bufnr].diffs_difft_active == true
local difft_active = opts.intra == false
or (opts.intra == nil and vim.b[bufnr].diffs_difft_active == true)
if
not opts.syntax_only
and intra_cfg
Expand Down Expand Up @@ -679,7 +684,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
then
for i = 0, #hunk.header_lines - 1 do
local buf_line = hunk.header_start_line - 1 + i
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, offset, {
end_col = #hunk.header_lines[i + 1] + qw,
hl_group = 'DiffsClear',
priority = p.clear,
Expand Down Expand Up @@ -712,7 +717,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)

if (qw > 0 or pw > 1) and at_raw_line then
local at_buf_line = hunk.start_line - 1
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, at_buf_line, 0, {
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, at_buf_line, offset, {
end_col = #at_raw_line,
hl_group = 'DiffsClear',
priority = p.clear,
Expand All @@ -728,8 +733,8 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)

if use_ts and hunk.header_context and hunk.header_context_col then
local header_line = hunk.start_line - 1
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, header_line, hunk.header_context_col, {
end_col = hunk.header_context_col + #hunk.header_context,
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, header_line, hunk.header_context_col + offset, {
end_col = hunk.header_context_col + offset + #hunk.header_context,
hl_group = 'DiffsClear',
priority = p.clear,
})
Expand Down Expand Up @@ -768,7 +773,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
if not opts.syntax_only then
if opts.hide_prefix and (not hunk.rail_width or is_diff_line) then
local virt_hl = (opts.highlights.background and bg_hl) or nil
local rail_width = hunk.rail_width or 0
local rail_width = (hunk.rail_width or 0) + offset
local hide_width = math.max(0, pw + qw - rail_width)
if hide_width > 0 then
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, rail_width, {
Expand All @@ -786,7 +791,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
if raw_len and prefix_end > raw_len then
prefix_end = raw_len
end
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, offset, {
end_col = prefix_end,
hl_group = 'DiffsClear',
priority = p.clear,
Expand All @@ -796,7 +801,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
if raw_len and rail_end > raw_len then
rail_end = raw_len
end
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, offset, {
end_col = rail_end,
hl_group = 'DiffsRail',
priority = p.syntax,
Expand Down Expand Up @@ -855,15 +860,15 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end
end
elseif opts.highlights.background and is_diff_line then
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, offset, {
end_col = 1,
hl_group = prefix_hl,
priority = p.syntax,
})
end

if hunk.rail_width and is_diff_line and opts.highlights.background and not dehl then
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, offset, {
virt_text = { { '▏', bar_hl } },
virt_text_pos = 'overlay',
hl_mode = 'replace',
Expand All @@ -872,7 +877,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end

if opts.highlights.background and is_diff_line then
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, offset, {
end_row = buf_line + 1,
end_col = 0,
hl_group = bg_hl,
Expand Down
12 changes: 11 additions & 1 deletion lua/diffs/init.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
---@class diffs.PublicApi
---@field attach fun(bufnr?: integer)
---@field attach fun(bufnr?: integer, options?: diffs.AttachOptions)
---@field detach fun(bufnr?: integer)
---@field highlight_options fun(): diffs.HunkOpts
---@field diff_options fun(): diffs.DiffOpts
---@field compute_intra_hunks fun(lines: string[]): diffs.IntraChanges?
---@field whitespace_only_lines fun(lines: string[]): table<integer, true>
---@field open_review fun(spec?: diffs.ReviewSpec, opts?: diffs.ReviewOpts): integer?
---@field refresh fun(bufnr?: integer)
---@field review_files fun(bufnr?: integer): diffs.ReviewFile[]?
Expand All @@ -21,6 +26,11 @@ end
---@type diffs.PublicApi
return {
attach = runtime.attach,
detach = runtime.detach,
highlight_options = runtime.get_highlight_opts,
diff_options = require('diffs.diffopt').resolve,
compute_intra_hunks = require('diffs.diff').compute_intra_hunks,
whitespace_only_lines = require('diffs.diff').whitespace_only_lines,
open_review = command('open_review'),
refresh = runtime.refresh,
review_files = command('review_files'),
Expand Down
1 change: 1 addition & 0 deletions lua/diffs/parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
---@field header_lines string[]?
---@field prefix_width integer
---@field quote_width integer
---@field column_offset integer?
---@field rail_width integer?
---@field rail_style diffs.RailStyle?
---@field _hl_line_count integer?
Expand Down
80 changes: 54 additions & 26 deletions lua/diffs/runtime/attach.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,91 @@ local log = require('diffs.log')

local M = {}

---@class diffs.AttachOpts
---@class diffs.AttachOptions
---@field hunks? fun(bufnr: integer): diffs.Hunk[]
---@field intra? boolean

---@class diffs.Attachment: diffs.AttachOptions
---@field augroup integer

---@class diffs.DetachOpts
---@field cache diffs.Cache
---@field attached_buffers table<integer, boolean>
---@field attached_buffers table<integer, diffs.Attachment>

---@class diffs.AttachOpts: diffs.DetachOpts
---@field get_config fun(): diffs.Config
---@field refresh fun(bufnr?: integer)

---@param opts diffs.AttachOpts
---@param opts diffs.DetachOpts
---@param bufnr? integer
function M.attach(opts, bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
function M.detach(opts, bufnr)
if not bufnr or bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
local attachment = opts.attached_buffers[bufnr]
if not attachment then
return
end
opts.attached_buffers[bufnr] = nil
opts.cache:delete(bufnr)
vim.api.nvim_del_augroup_by_id(attachment.augroup)
if vim.api.nvim_buf_is_valid(bufnr) then
vim.api.nvim_buf_clear_namespace(bufnr, opts.cache.ns, 0, -1)
end
end

---@param opts diffs.AttachOpts
---@param bufnr? integer
---@param options? diffs.AttachOptions
function M.attach(opts, bufnr, options)
if not bufnr or bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end

if opts.attached_buffers[bufnr] then
return
if not options then
return
end
M.detach(opts, bufnr)
end
opts.attached_buffers[bufnr] = true

local attachment = {
hunks = options and options.hunks,
intra = options and options.intra,
augroup = vim.api.nvim_create_augroup('diffs_buffer_' .. bufnr, { clear = true }),
}
opts.attached_buffers[bufnr] = attachment
local config = opts.get_config()
local integration_augroup
for key, event in pairs({ neogit = 'NeogitDiffLoaded', neojj = 'NeojjDiffLoaded' }) do
for name, event in pairs({ neogit = 'NeogitDiffLoaded', neojj = 'NeojjDiffLoaded' }) do
if
integrations.is_enabled(config.integrations[key])
and integrations.matches_filetype(key, vim.bo[bufnr].filetype)
integrations.is_enabled(config.integrations[name])
and integrations.matches_filetype(name, vim.bo[bufnr].filetype)
then
vim.b[bufnr][key .. '_disable_hunk_highlight'] = true
integration_augroup =
vim.api.nvim_create_augroup('diffs_' .. key .. '_' .. bufnr, { clear = true })
vim.b[bufnr][name .. '_disable_hunk_highlight'] = true
vim.api.nvim_create_autocmd('User', {
pattern = event,
group = integration_augroup,
group = attachment.augroup,
callback = function()
if vim.api.nvim_buf_is_valid(bufnr) and opts.attached_buffers[bufnr] then
if vim.api.nvim_buf_is_valid(bufnr) and opts.attached_buffers[bufnr] == attachment then
opts.refresh(bufnr)
end
end,
})
break
end
end

log.dbg('attaching to buffer %d', bufnr)

opts.cache:ensure(bufnr)

vim.api.nvim_create_autocmd('BufWipeout', {
buffer = bufnr,
group = attachment.augroup,
callback = function()
opts.attached_buffers[bufnr] = nil
opts.cache:delete(bufnr)
if integration_augroup then
pcall(vim.api.nvim_del_augroup_by_id, integration_augroup)
if opts.attached_buffers[bufnr] == attachment then
M.detach(opts, bufnr)
end
end,
})
opts.cache:ensure(bufnr)
end

return M
Loading
Loading