diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 6561627..d75a552 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -37,7 +37,6 @@ Using lazy.nvim: >lua { 'barrettruth/diffs.nvim', dependencies = { 'tpope/vim-fugitive' }, - opts = {}, } < The plugin works automatically with no configuration required. For @@ -46,6 +45,26 @@ customization, see |diffs-config|. ============================================================================== CONFIGURATION *diffs-config* +Configuration is done via `vim.g.diffs`. Set this before the plugin loads: +>lua + vim.g.diffs = { + debug = false, + debounce_ms = 0, + hide_prefix = false, + highlights = { + background = true, + gutter = true, + treesitter = { + enabled = true, + max_lines = 500, + }, + vim = { + enabled = false, + max_lines = 200, + }, + }, + } +< *diffs.Config* Fields: ~ {debug} (boolean, default: false) @@ -64,6 +83,21 @@ CONFIGURATION *diffs-config* is also enabled, the overlay inherits the line's background color. + {highlights} (table, default: see below) + Controls which highlight features are enabled. + See |diffs.Highlights| for fields. + + *diffs.Highlights* + Highlights table fields: ~ + {background} (boolean, default: true) + Apply background highlighting to `+`/`-` lines + using `DiffsAdd`/`DiffsDelete` groups (derived + from `DiffAdd`/`DiffDelete` backgrounds). + + {gutter} (boolean, default: true) + Highlight line numbers with matching colors. + Only visible if line numbers are enabled. + {treesitter} (table, default: see below) Treesitter highlighting options. See |diffs.TreesitterConfig| for fields. @@ -72,10 +106,6 @@ CONFIGURATION *diffs-config* Vim syntax highlighting options (experimental). See |diffs.VimConfig| for fields. - {highlights} (table, default: see below) - Controls which highlight features are enabled. - See |diffs.Highlights| for fields. - *diffs.TreesitterConfig* Treesitter config fields: ~ {enabled} (boolean, default: true) @@ -100,17 +130,6 @@ CONFIGURATION *diffs-config* this many lines. Lower than the treesitter default due to the per-character cost of |synID()|. - *diffs.Highlights* - Highlights table fields: ~ - {background} (boolean, default: true) - Apply background highlighting to `+`/`-` lines - using `DiffsAdd`/`DiffsDelete` groups (derived - from `DiffAdd`/`DiffDelete` backgrounds). - - {gutter} (boolean, default: true) - Highlight line numbers with matching colors. - Only visible if line numbers are enabled. - Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always highlighted with treesitter when a parser is available. @@ -122,12 +141,6 @@ CONFIGURATION *diffs-config* ============================================================================== API *diffs-api* -setup({opts}) *diffs.setup()* - Configure the plugin with `opts`. - - Parameters: ~ - {opts} (|diffs.Config|, optional) Configuration table. - attach({bufnr}) *diffs.attach()* Manually attach highlighting to a buffer. Called automatically for fugitive buffers via the `FileType fugitive` autocmd. @@ -183,11 +196,37 @@ which fires after vim-fugitive has already painted the buffer. Even with `debounce_ms = 0`, the re-painting goes through Neovim's event loop. To minimize the flash, use a low debounce value: >lua - require('diffs').setup({ + vim.g.diffs = { debounce_ms = 0, - }) + } < +Conflicting Diff Plugins ~ + *diffs-plugin-conflicts* +diffs.nvim may not interact well with other plugins that modify diff +highlighting or the sign column in diff views. Known plugins that may +conflict: + + - diffview.nvim (sindrets/diffview.nvim) + Provides its own diff highlighting and conflict resolution UI. + When using diffview.nvim for viewing diffs, you may want to disable + diffs.nvim's diff mode attachment or use one plugin exclusively. + + - mini.diff (echasnovski/mini.diff) + Visualizes buffer differences with its own highlighting system. + May override or conflict with diffs.nvim's background highlighting. + + - gitsigns.nvim (lewis6991/gitsigns.nvim) + Generally compatible for sign column decorations, but both plugins + modifying line highlights may produce unexpected results. + + - git-conflict.nvim (akinsho/git-conflict.nvim) + Provides conflict marker highlighting that may overlap with + diffs.nvim's highlighting in conflict scenarios. + +If you experience visual conflicts, try disabling the conflicting plugin's +diff-related features. + ============================================================================== HIGHLIGHT GROUPS *diffs-highlights* diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 7a2f799..e34931b 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -51,8 +51,6 @@ end ---@class diffs.HunkOpts ---@field hide_prefix boolean ----@field treesitter diffs.TreesitterConfig ----@field vim diffs.VimConfig ---@field highlights diffs.Highlights ---@param bufnr integer @@ -226,10 +224,10 @@ end ---@param hunk diffs.Hunk ---@param opts diffs.HunkOpts function M.highlight_hunk(bufnr, ns, hunk, opts) - local use_ts = hunk.lang and opts.treesitter.enabled - local use_vim = not use_ts and hunk.ft and opts.vim.enabled + local use_ts = hunk.lang and opts.highlights.treesitter.enabled + local use_vim = not use_ts and hunk.ft and opts.highlights.vim.enabled - local max_lines = use_ts and opts.treesitter.max_lines or opts.vim.max_lines + local max_lines = use_ts and opts.highlights.treesitter.max_lines or opts.highlights.vim.max_lines if (use_ts or use_vim) and #hunk.lines > max_lines then dbg( 'skipping hunk %s:%d (%d lines > %d max)', diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index c47beda..66a8098 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -1,7 +1,3 @@ ----@class diffs.Highlights ----@field background boolean ----@field gutter boolean - ---@class diffs.TreesitterConfig ---@field enabled boolean ---@field max_lines integer @@ -10,18 +6,21 @@ ---@field enabled boolean ---@field max_lines integer +---@class diffs.Highlights +---@field background boolean +---@field gutter boolean +---@field treesitter diffs.TreesitterConfig +---@field vim diffs.VimConfig + ---@class diffs.Config ---@field debug boolean ---@field debounce_ms integer ---@field hide_prefix boolean ----@field treesitter diffs.TreesitterConfig ----@field vim diffs.VimConfig ---@field highlights diffs.Highlights ---@class diffs ---@field attach fun(bufnr?: integer) ---@field refresh fun(bufnr?: integer) ----@field setup fun(opts?: diffs.Config) local M = {} local highlight = require('diffs.highlight') @@ -67,23 +66,25 @@ local default_config = { debug = false, debounce_ms = 0, hide_prefix = false, - treesitter = { - enabled = true, - max_lines = 500, - }, - vim = { - enabled = false, - max_lines = 200, - }, highlights = { background = true, gutter = true, + treesitter = { + enabled = true, + max_lines = 500, + }, + vim = { + enabled = false, + max_lines = 200, + }, }, } ---@type diffs.Config local config = vim.deepcopy(default_config) +local initialized = false + ---@type table local attached_buffers = {} @@ -111,8 +112,6 @@ local function highlight_buffer(bufnr) for _, hunk in ipairs(hunks) do highlight.highlight_hunk(bufnr, ns, hunk, { hide_prefix = config.hide_prefix, - treesitter = config.treesitter, - vim = config.vim, highlights = config.highlights, }) end @@ -148,8 +147,125 @@ local function create_debounced_highlight(bufnr) end end +local function compute_highlight_groups() + local normal = vim.api.nvim_get_hl(0, { name = 'Normal' }) + local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' }) + local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' }) + local diff_added = resolve_hl('diffAdded') + local diff_removed = resolve_hl('diffRemoved') + + local bg = normal.bg or 0x1e1e2e + local add_bg = diff_add.bg or 0x2e4a3a + local del_bg = diff_delete.bg or 0x4a2e3a + local add_fg = diff_added.fg or diff_add.fg or 0x80c080 + local del_fg = diff_removed.fg or diff_delete.fg or 0xc08080 + + local blended_add = blend_color(add_bg, bg, 0.4) + local blended_del = blend_color(del_bg, bg, 0.4) + + vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add }) + vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del }) + vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = add_fg, bg = blended_add }) + vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { default = true, fg = del_fg, bg = blended_del }) + + local diff_change = resolve_hl('DiffChange') + local diff_text = resolve_hl('DiffText') + + vim.api.nvim_set_hl(0, 'DiffsDiffAdd', { bg = diff_add.bg }) + vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { fg = diff_delete.fg, bg = diff_delete.bg }) + vim.api.nvim_set_hl(0, 'DiffsDiffChange', { bg = diff_change.bg }) + vim.api.nvim_set_hl(0, 'DiffsDiffText', { bg = diff_text.bg }) +end + +local function init() + if initialized then + return + end + initialized = true + + local opts = vim.g.diffs or {} + + vim.validate({ + debug = { opts.debug, 'boolean', true }, + debounce_ms = { opts.debounce_ms, 'number', true }, + hide_prefix = { opts.hide_prefix, 'boolean', true }, + highlights = { opts.highlights, 'table', true }, + }) + + if opts.highlights then + vim.validate({ + ['highlights.background'] = { opts.highlights.background, 'boolean', true }, + ['highlights.gutter'] = { opts.highlights.gutter, 'boolean', true }, + ['highlights.treesitter'] = { opts.highlights.treesitter, 'table', true }, + ['highlights.vim'] = { opts.highlights.vim, 'table', true }, + }) + + if opts.highlights.treesitter then + vim.validate({ + ['highlights.treesitter.enabled'] = { opts.highlights.treesitter.enabled, 'boolean', true }, + ['highlights.treesitter.max_lines'] = { + opts.highlights.treesitter.max_lines, + 'number', + true, + }, + }) + end + + if opts.highlights.vim then + vim.validate({ + ['highlights.vim.enabled'] = { opts.highlights.vim.enabled, 'boolean', true }, + ['highlights.vim.max_lines'] = { opts.highlights.vim.max_lines, 'number', true }, + }) + end + end + + if opts.debounce_ms and opts.debounce_ms < 0 then + error('diffs: debounce_ms must be >= 0') + end + if + opts.highlights + and opts.highlights.treesitter + and opts.highlights.treesitter.max_lines + and opts.highlights.treesitter.max_lines < 1 + then + error('diffs: highlights.treesitter.max_lines must be >= 1') + end + if + opts.highlights + and opts.highlights.vim + and opts.highlights.vim.max_lines + and opts.highlights.vim.max_lines < 1 + then + error('diffs: highlights.vim.max_lines must be >= 1') + end + + config = vim.tbl_deep_extend('force', default_config, opts) + log.set_enabled(config.debug) + + compute_highlight_groups() + + vim.api.nvim_create_autocmd('ColorScheme', { + callback = function() + compute_highlight_groups() + for bufnr, _ in pairs(attached_buffers) do + highlight_buffer(bufnr) + end + end, + }) + + vim.api.nvim_create_autocmd('WinClosed', { + callback = function(args) + local win = tonumber(args.match) + if win and diff_windows[win] then + diff_windows[win] = nil + end + end, + }) +end + ---@param bufnr? integer function M.attach(bufnr) + init() bufnr = bufnr or vim.api.nvim_get_current_buf() if attached_buffers[bufnr] then @@ -198,36 +314,6 @@ function M.refresh(bufnr) highlight_buffer(bufnr) end -local function compute_highlight_groups() - local normal = vim.api.nvim_get_hl(0, { name = 'Normal' }) - local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' }) - local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' }) - local diff_added = resolve_hl('diffAdded') - local diff_removed = resolve_hl('diffRemoved') - - local bg = normal.bg or 0x1e1e2e - local add_bg = diff_add.bg or 0x2e4a3a - local del_bg = diff_delete.bg or 0x4a2e3a - local add_fg = diff_added.fg or diff_add.fg or 0x80c080 - local del_fg = diff_removed.fg or diff_delete.fg or 0xc08080 - - local blended_add = blend_color(add_bg, bg, 0.4) - local blended_del = blend_color(del_bg, bg, 0.4) - - vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add }) - vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del }) - vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = add_fg, bg = blended_add }) - vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { default = true, fg = del_fg, bg = blended_del }) - - local diff_change = resolve_hl('DiffChange') - local diff_text = resolve_hl('DiffText') - - vim.api.nvim_set_hl(0, 'DiffsDiffAdd', { bg = diff_add.bg }) - vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { fg = diff_delete.fg, bg = diff_delete.bg }) - vim.api.nvim_set_hl(0, 'DiffsDiffChange', { bg = diff_change.bg }) - vim.api.nvim_set_hl(0, 'DiffsDiffText', { bg = diff_text.bg }) -end - local DIFF_WINHIGHLIGHT = table.concat({ 'DiffAdd:DiffsDiffAdd', 'DiffDelete:DiffsDiffDelete', @@ -236,6 +322,7 @@ local DIFF_WINHIGHLIGHT = table.concat({ }, ',') function M.attach_diff() + init() local tabpage = vim.api.nvim_get_current_tabpage() local wins = vim.api.nvim_tabpage_list_wins(tabpage) @@ -267,72 +354,4 @@ function M.detach_diff() end end ----@param opts? diffs.Config -function M.setup(opts) - opts = opts or {} - - vim.validate({ - debug = { opts.debug, 'boolean', true }, - debounce_ms = { opts.debounce_ms, 'number', true }, - hide_prefix = { opts.hide_prefix, 'boolean', true }, - treesitter = { opts.treesitter, 'table', true }, - vim = { opts.vim, 'table', true }, - highlights = { opts.highlights, 'table', true }, - }) - - if opts.treesitter then - vim.validate({ - ['treesitter.enabled'] = { opts.treesitter.enabled, 'boolean', true }, - ['treesitter.max_lines'] = { opts.treesitter.max_lines, 'number', true }, - }) - end - - if opts.vim then - vim.validate({ - ['vim.enabled'] = { opts.vim.enabled, 'boolean', true }, - ['vim.max_lines'] = { opts.vim.max_lines, 'number', true }, - }) - end - - if opts.highlights then - vim.validate({ - ['highlights.background'] = { opts.highlights.background, 'boolean', true }, - ['highlights.gutter'] = { opts.highlights.gutter, 'boolean', true }, - }) - end - - if opts.debounce_ms and opts.debounce_ms < 0 then - error('diffs: debounce_ms must be >= 0') - end - if opts.treesitter and opts.treesitter.max_lines and opts.treesitter.max_lines < 1 then - error('diffs: treesitter.max_lines must be >= 1') - end - if opts.vim and opts.vim.max_lines and opts.vim.max_lines < 1 then - error('diffs: vim.max_lines must be >= 1') - end - - config = vim.tbl_deep_extend('force', default_config, opts) - log.set_enabled(config.debug) - - compute_highlight_groups() - - vim.api.nvim_create_autocmd('ColorScheme', { - callback = function() - compute_highlight_groups() - for bufnr, _ in pairs(attached_buffers) do - highlight_buffer(bufnr) - end - end, - }) - - vim.api.nvim_create_autocmd('WinClosed', { - callback = function(args) - local win = tonumber(args.match) - if win and diff_windows[win] then - diff_windows[win] = nil - end - end, - }) -end - return M diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index ff5cee8..3d6df08 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -32,28 +32,25 @@ describe('highlight', function() local function default_opts(overrides) local opts = { hide_prefix = false, - treesitter = { - enabled = true, - max_lines = 500, - }, - vim = { - enabled = false, - max_lines = 200, - }, highlights = { background = false, gutter = false, + treesitter = { + enabled = true, + max_lines = 500, + }, + vim = { + enabled = false, + max_lines = 200, + }, }, } if overrides then - for k, v in pairs(overrides) do - if type(v) == 'table' and type(opts[k]) == 'table' then - for sk, sv in pairs(v) do - opts[k][sk] = sv - end - else - opts[k] = v - end + if overrides.highlights then + opts.highlights = vim.tbl_deep_extend('force', opts.highlights, overrides.highlights) + end + if overrides.hide_prefix ~= nil then + opts.hide_prefix = overrides.hide_prefix end end return opts @@ -484,7 +481,7 @@ describe('highlight', function() bufnr, ns, hunk, - default_opts({ treesitter = { enabled = false }, highlights = { background = true } }) + default_opts({ highlights = { treesitter = { enabled = false }, background = true } }) ) local extmarks = get_extmarks(bufnr) @@ -517,7 +514,7 @@ describe('highlight', function() bufnr, ns, hunk, - default_opts({ treesitter = { enabled = false }, highlights = { background = true } }) + default_opts({ highlights = { treesitter = { enabled = false }, background = true } }) ) local extmarks = get_extmarks(bufnr) @@ -560,7 +557,12 @@ describe('highlight', function() lines = { ' local x = 1', '+local y = 2' }, } - highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ vim = { enabled = true } })) + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { vim = { enabled = true } } }) + ) vim.fn.synID = orig_synID vim.fn.synIDtrans = orig_synIDtrans @@ -593,7 +595,12 @@ describe('highlight', function() lines = { ' local x = 1', '+local y = 2' }, } - highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ vim = { enabled = false } })) + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { vim = { enabled = false } } }) + ) local extmarks = get_extmarks(bufnr) local has_syntax_hl = false @@ -628,7 +635,7 @@ describe('highlight', function() bufnr, ns, hunk, - default_opts({ vim = { enabled = true, max_lines = 200 } }) + default_opts({ highlights = { vim = { enabled = true, max_lines = 200 } } }) ) local extmarks = get_extmarks(bufnr) @@ -655,7 +662,7 @@ describe('highlight', function() bufnr, ns, hunk, - default_opts({ vim = { enabled = true }, highlights = { background = true } }) + default_opts({ highlights = { vim = { enabled = true }, background = true } }) ) local extmarks = get_extmarks(bufnr) @@ -698,7 +705,12 @@ describe('highlight', function() lines = { ' local x = 1', '+local y = 2' }, } - highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ vim = { enabled = true } })) + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { vim = { enabled = true } } }) + ) vim.fn.synID = orig_synID vim.fn.synIDtrans = orig_synIDtrans diff --git a/spec/init_spec.lua b/spec/init_spec.lua index 19da8f7..1bf71d1 100644 --- a/spec/init_spec.lua +++ b/spec/init_spec.lua @@ -2,26 +2,33 @@ require('spec.helpers') local diffs = require('diffs') describe('diffs', function() - describe('setup', function() - it('accepts empty config', function() - assert.has_no.errors(function() - diffs.setup({}) - end) + describe('vim.g.diffs config', function() + after_each(function() + vim.g.diffs = nil end) it('accepts nil config', function() + vim.g.diffs = nil assert.has_no.errors(function() - diffs.setup() + diffs.attach() + end) + end) + + it('accepts empty config', function() + vim.g.diffs = {} + assert.has_no.errors(function() + diffs.attach() end) end) it('accepts full config', function() - assert.has_no.errors(function() - diffs.setup({ - enabled = false, - debug = true, - debounce_ms = 100, - hide_prefix = false, + vim.g.diffs = { + debug = true, + debounce_ms = 100, + hide_prefix = false, + highlights = { + background = true, + gutter = true, treesitter = { enabled = true, max_lines = 1000, @@ -30,19 +37,19 @@ describe('diffs', function() enabled = false, max_lines = 200, }, - highlights = { - background = true, - gutter = true, - }, - }) + }, + } + assert.has_no.errors(function() + diffs.attach() end) end) it('accepts partial config', function() + vim.g.diffs = { + debounce_ms = 25, + } assert.has_no.errors(function() - diffs.setup({ - debounce_ms = 25, - }) + diffs.attach() end) end) end) @@ -60,10 +67,6 @@ describe('diffs', function() end end - before_each(function() - diffs.setup({ enabled = true }) - end) - it('does not error on empty buffer', function() local bufnr = create_buffer({}) assert.has_no.errors(function() @@ -109,10 +112,6 @@ describe('diffs', function() end end - before_each(function() - diffs.setup({ enabled = true }) - end) - it('does not error on unattached buffer', function() local bufnr = create_buffer({}) assert.has_no.errors(function() @@ -168,10 +167,6 @@ describe('diffs', function() end end - before_each(function() - diffs.setup({ enabled = true }) - end) - describe('attach_diff', function() it('applies winhighlight to diff windows', function() local win, _ = create_diff_window()