From 00bf84cb05d357e864b56354b7a809dd0212b4f4 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 1 Feb 2026 23:17:20 -0600 Subject: [PATCH 1/5] feat: highlights --- doc/fugitive-ts.nvim.txt | 40 ++++- lua/fugitive-ts/highlight.lua | 68 +++++-- lua/fugitive-ts/init.lua | 33 ++-- spec/highlight_spec.lua | 328 +++++++++++++++++++++++++++++++++- spec/init_spec.lua | 9 +- 5 files changed, 437 insertions(+), 41 deletions(-) diff --git a/doc/fugitive-ts.nvim.txt b/doc/fugitive-ts.nvim.txt index 9d5e5de..19a8bbc 100644 --- a/doc/fugitive-ts.nvim.txt +++ b/doc/fugitive-ts.nvim.txt @@ -56,11 +56,6 @@ CONFIGURATION *fugitive-ts-config* Example: >lua disabled_languages = { 'markdown', 'text' } < - {highlight_headers} (boolean, default: true) - Highlight function context in hunk headers. - The context portion of `@@ -10,3 +10,4 @@ func()` - will receive treesitter highlighting. - {debounce_ms} (integer, default: 50) Debounce delay in milliseconds for re-highlighting after buffer changes. Lower values feel snappier @@ -70,6 +65,37 @@ CONFIGURATION *fugitive-ts-config* Skip treesitter highlighting for hunks larger than this many lines. Prevents lag on massive diffs. + {conceal_prefixes} (boolean, default: true) + Hide diff prefixes (`+`/`-`/` `) using extmark + conceal. Makes code appear without the leading + diff character. + + {highlights} (table, default: see below) + Controls which highlight features are enabled. + See |fugitive-ts.Highlights| for fields. + + *fugitive-ts.Highlights* + Highlights table fields: ~ + {treesitter} (boolean, default: true) + Apply treesitter syntax highlighting to code. + + {headers} (boolean, default: true) + Highlight function context in hunk headers. + The context portion of `@@ -10,3 +10,4 @@ func()` + will receive treesitter highlighting. + + {background} (boolean, default: true) + Apply `DiffAdd` background to `+` lines and + `DiffDelete` background to `-` lines. + + {linenr} (boolean, default: true) + Highlight line numbers with `DiffAdd`/`DiffDelete` + colors matching the line background. + + {vim} (boolean, default: false) + Experimental: Use vim syntax highlighting as + fallback when no treesitter parser is available. + ============================================================================== API *fugitive-ts-api* @@ -103,8 +129,10 @@ IMPLEMENTATION *fugitive-ts-implementation* - Language is detected from the filename using |vim.filetype.match()| - Diff prefixes (`+`/`-`/` `) are stripped from code lines - Code is parsed with |vim.treesitter.get_string_parser()| + - Background extmarks (`DiffAdd`/`DiffDelete`) applied at priority 198 + - `Normal` extmarks at priority 199 clear underlying diff foreground - Treesitter highlights are applied as extmarks at priority 200 - - A `Normal` extmark at priority 199 clears underlying diff colors + - Conceal extmarks hide diff prefixes when `conceal_prefixes` is enabled 4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events ============================================================================== diff --git a/lua/fugitive-ts/highlight.lua b/lua/fugitive-ts/highlight.lua index 6286f10..31878b6 100644 --- a/lua/fugitive-ts/highlight.lua +++ b/lua/fugitive-ts/highlight.lua @@ -55,26 +55,30 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, debug) return extmark_count end +---@class fugitive-ts.HunkOpts +---@field max_lines integer +---@field conceal_prefixes boolean +---@field highlights fugitive-ts.Highlights +---@field debug boolean + ---@param bufnr integer ---@param ns integer ---@param hunk fugitive-ts.Hunk ----@param max_lines integer ----@param highlight_headers boolean ----@param debug? boolean -function M.highlight_hunk(bufnr, ns, hunk, max_lines, highlight_headers, debug) +---@param opts fugitive-ts.HunkOpts +function M.highlight_hunk(bufnr, ns, hunk, opts) local lang = hunk.lang if not lang then return end - if #hunk.lines > max_lines then - if debug then + if #hunk.lines > opts.max_lines then + if opts.debug then dbg( 'skipping hunk %s:%d (%d lines > %d max)', hunk.filename, hunk.start_line, #hunk.lines, - max_lines + opts.max_lines ) end return @@ -93,7 +97,7 @@ function M.highlight_hunk(bufnr, ns, hunk, max_lines, highlight_headers, debug) local ok, parser_obj = pcall(vim.treesitter.get_string_parser, code, lang) if not ok or not parser_obj then - if debug then + if opts.debug then dbg('failed to create parser for lang: %s', lang) end return @@ -101,7 +105,7 @@ function M.highlight_hunk(bufnr, ns, hunk, max_lines, highlight_headers, debug) local trees = parser_obj:parse() if not trees or #trees == 0 then - if debug then + if opts.debug then dbg('parse returned no trees for lang: %s', lang) end return @@ -109,22 +113,29 @@ function M.highlight_hunk(bufnr, ns, hunk, max_lines, highlight_headers, debug) local query = vim.treesitter.query.get(lang, 'highlights') if not query then - if debug then + if opts.debug then dbg('no highlights query for lang: %s', lang) end return end - if highlight_headers and hunk.header_context and hunk.header_context_col then + if opts.highlights.headers 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, hl_group = 'Normal', priority = 199, }) - local header_extmarks = - highlight_text(bufnr, ns, hunk, hunk.header_context_col, hunk.header_context, lang, debug) - if debug and header_extmarks > 0 then + local header_extmarks = highlight_text( + bufnr, + ns, + hunk, + hunk.header_context_col, + hunk.header_context, + lang, + opts.debug + ) + if opts.debug and header_extmarks > 0 then dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks) end end @@ -132,7 +143,28 @@ function M.highlight_hunk(bufnr, ns, hunk, max_lines, highlight_headers, debug) for i, line in ipairs(hunk.lines) do local buf_line = hunk.start_line + i - 1 local line_len = #line - if line_len > 1 then + local prefix = line:sub(1, 1) + + if opts.conceal_prefixes then + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { + end_col = 1, + conceal = '', + }) + end + + if opts.highlights.background and (prefix == '+' or prefix == '-') then + local line_hl = prefix == '+' and 'DiffAdd' or 'DiffDelete' + local extmark_opts = { + line_hl_group = line_hl, + priority = 198, + } + if opts.highlights.linenr then + extmark_opts.number_hl_group = line_hl + end + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, extmark_opts) + end + + if line_len > 1 and opts.highlights.treesitter then pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, { end_col = line_len, hl_group = 'Normal', @@ -141,6 +173,10 @@ function M.highlight_hunk(bufnr, ns, hunk, max_lines, highlight_headers, debug) end end + if not opts.highlights.treesitter then + return + end + local extmark_count = 0 for id, node, _ in query:iter_captures(trees[1]:root(), code) do local capture_name = '@' .. query.captures[id] @@ -160,7 +196,7 @@ function M.highlight_hunk(bufnr, ns, hunk, max_lines, highlight_headers, debug) extmark_count = extmark_count + 1 end - if debug then + if opts.debug then dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count) end end diff --git a/lua/fugitive-ts/init.lua b/lua/fugitive-ts/init.lua index 31084a2..b659342 100644 --- a/lua/fugitive-ts/init.lua +++ b/lua/fugitive-ts/init.lua @@ -1,11 +1,19 @@ +---@class fugitive-ts.Highlights +---@field treesitter boolean +---@field headers boolean +---@field background boolean +---@field linenr boolean +---@field vim boolean + ---@class fugitive-ts.Config ---@field enabled boolean ---@field debug boolean ---@field languages table ---@field disabled_languages string[] ----@field highlight_headers boolean ---@field debounce_ms integer ---@field max_lines_per_hunk integer +---@field conceal_prefixes boolean +---@field highlights fugitive-ts.Highlights ---@class fugitive-ts ---@field attach fun(bufnr?: integer) @@ -24,9 +32,16 @@ local default_config = { debug = false, languages = {}, disabled_languages = {}, - highlight_headers = true, debounce_ms = 50, max_lines_per_hunk = 500, + conceal_prefixes = true, + highlights = { + treesitter = true, + headers = true, + background = true, + linenr = true, + vim = false, + }, } ---@type fugitive-ts.Config @@ -61,14 +76,12 @@ local function highlight_buffer(bufnr) parser.parse_buffer(bufnr, config.languages, config.disabled_languages, config.debug) dbg('found %d hunks in buffer %d', #hunks, bufnr) for _, hunk in ipairs(hunks) do - highlight.highlight_hunk( - bufnr, - ns, - hunk, - config.max_lines_per_hunk, - config.highlight_headers, - config.debug - ) + highlight.highlight_hunk(bufnr, ns, hunk, { + max_lines = config.max_lines_per_hunk, + conceal_prefixes = config.conceal_prefixes, + highlights = config.highlights, + debug = config.debug, + }) end end diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 62d57b8..3f800f7 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -25,6 +25,33 @@ describe('highlight', function() return vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true }) end + local function default_opts(overrides) + local opts = { + max_lines = 500, + conceal_prefixes = false, + highlights = { + treesitter = true, + headers = false, + background = false, + linenr = false, + vim = false, + }, + debug = false, + } + if overrides then + for k, v in pairs(overrides) do + if k == 'highlights' then + for hk, hv in pairs(v) do + opts.highlights[hk] = hv + end + else + opts[k] = v + end + end + end + return opts + end + it('applies extmarks for lua code', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -39,7 +66,7 @@ describe('highlight', function() lines = { ' local x = 1', '+local y = 2' }, } - highlight.highlight_hunk(bufnr, ns, hunk, 500, false, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) assert.is_true(#extmarks > 0) @@ -60,7 +87,7 @@ describe('highlight', function() lines = { ' local x = 1', '+local y = 2' }, } - highlight.highlight_hunk(bufnr, ns, hunk, 500, false, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) local has_normal = false @@ -90,7 +117,7 @@ describe('highlight', function() lines = hunk_lines, } - highlight.highlight_hunk(bufnr, ns, hunk, 500, false, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) assert.are.equal(0, #extmarks) @@ -111,7 +138,7 @@ describe('highlight', function() lines = { ' some content', '+more content' }, } - highlight.highlight_hunk(bufnr, ns, hunk, 500, false, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) assert.are.equal(0, #extmarks) @@ -134,7 +161,7 @@ describe('highlight', function() lines = { ' local x = 1', '+local y = 2' }, } - highlight.highlight_hunk(bufnr, ns, hunk, 500, true, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ highlights = { headers = true } })) local extmarks = get_extmarks(bufnr) local has_header_extmark = false @@ -163,7 +190,7 @@ describe('highlight', function() lines = { ' local x = 1' }, } - highlight.highlight_hunk(bufnr, ns, hunk, 500, false, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) local header_extmarks = 0 @@ -189,7 +216,7 @@ describe('highlight', function() } assert.has_no.errors(function() - highlight.highlight_hunk(bufnr, ns, hunk, 500, false, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) end) delete_buffer(bufnr) end) @@ -209,9 +236,294 @@ describe('highlight', function() } assert.has_no.errors(function() - highlight.highlight_hunk(bufnr, ns, hunk, 500, false, false) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) end) delete_buffer(bufnr) end) + + it('applies conceal extmarks when conceal_prefixes enabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ conceal_prefixes = true })) + + local extmarks = get_extmarks(bufnr) + local conceal_count = 0 + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].conceal == '' then + conceal_count = conceal_count + 1 + end + end + assert.are.equal(2, conceal_count) + delete_buffer(bufnr) + end) + + it('does not apply conceal extmarks when conceal_prefixes disabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ conceal_prefixes = false })) + + local extmarks = get_extmarks(bufnr) + local conceal_count = 0 + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].conceal == '' then + conceal_count = conceal_count + 1 + end + end + assert.are.equal(0, conceal_count) + delete_buffer(bufnr) + end) + + it('applies DiffAdd background to + lines when background enabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true } }) + ) + + local extmarks = get_extmarks(bufnr) + local has_diff_add = false + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].line_hl_group == 'DiffAdd' then + has_diff_add = true + break + end + end + assert.is_true(has_diff_add) + delete_buffer(bufnr) + end) + + it('applies DiffDelete background to - lines when background enabled', function() + local bufnr = create_buffer({ + '@@ -1,2 +1,1 @@', + ' local x = 1', + '-local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '-local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true } }) + ) + + local extmarks = get_extmarks(bufnr) + local has_diff_delete = false + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].line_hl_group == 'DiffDelete' then + has_diff_delete = true + break + end + end + assert.is_true(has_diff_delete) + delete_buffer(bufnr) + end) + + it('does not apply background when background disabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = false } }) + ) + + local extmarks = get_extmarks(bufnr) + local has_line_hl = false + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].line_hl_group then + has_line_hl = true + break + end + end + assert.is_false(has_line_hl) + delete_buffer(bufnr) + end) + + it('applies number_hl_group when linenr enabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true, linenr = true } }) + ) + + local extmarks = get_extmarks(bufnr) + local has_number_hl = false + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].number_hl_group then + has_number_hl = true + break + end + end + assert.is_true(has_number_hl) + delete_buffer(bufnr) + end) + + it('does not apply number_hl_group when linenr disabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true, linenr = false } }) + ) + + local extmarks = get_extmarks(bufnr) + local has_number_hl = false + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].number_hl_group then + has_number_hl = true + break + end + end + assert.is_false(has_number_hl) + delete_buffer(bufnr) + end) + + it('skips treesitter highlights when treesitter disabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { treesitter = false, background = true } }) + ) + + local extmarks = get_extmarks(bufnr) + local has_ts_highlight = false + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@') then + has_ts_highlight = true + break + end + end + assert.is_false(has_ts_highlight) + delete_buffer(bufnr) + end) + + it('still applies background when treesitter disabled', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { treesitter = false, background = true } }) + ) + + local extmarks = get_extmarks(bufnr) + local has_diff_add = false + for _, mark in ipairs(extmarks) do + if mark[4] and mark[4].line_hl_group == 'DiffAdd' then + has_diff_add = true + break + end + end + assert.is_true(has_diff_add) + delete_buffer(bufnr) + end) end) end) diff --git a/spec/init_spec.lua b/spec/init_spec.lua index 5e7a0ff..77c3db0 100644 --- a/spec/init_spec.lua +++ b/spec/init_spec.lua @@ -22,9 +22,16 @@ describe('fugitive-ts', function() debug = true, languages = { ['.envrc'] = 'bash' }, disabled_languages = { 'markdown' }, - highlight_headers = false, debounce_ms = 100, max_lines_per_hunk = 1000, + conceal_prefixes = false, + highlights = { + treesitter = true, + headers = false, + background = true, + linenr = true, + vim = false, + }, }) end) end) From 935eb8f7ed7e5a0b5b0ab7b0062f0d31a427c534 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 2 Feb 2026 00:22:44 -0500 Subject: [PATCH 2/5] feat: highlights, debug fixes, and config validation --- lua/fugitive-ts/highlight.lua | 59 +++++++++++++++-------------------- lua/fugitive-ts/init.lua | 28 +++++++++++++++-- lua/fugitive-ts/parser.lua | 38 +++++++++++----------- spec/highlight_spec.lua | 1 - 4 files changed, 69 insertions(+), 57 deletions(-) diff --git a/lua/fugitive-ts/highlight.lua b/lua/fugitive-ts/highlight.lua index 31878b6..8c821fb 100644 --- a/lua/fugitive-ts/highlight.lua +++ b/lua/fugitive-ts/highlight.lua @@ -1,8 +1,18 @@ local M = {} +local debug_enabled = false + +---@param enabled boolean +function M.set_debug(enabled) + debug_enabled = enabled +end + ---@param msg string ---@param ... any local function dbg(msg, ...) + if not debug_enabled then + return + end local formatted = string.format(msg, ...) vim.notify('[fugitive-ts] ' .. formatted, vim.log.levels.DEBUG) end @@ -13,9 +23,8 @@ end ---@param col_offset integer ---@param text string ---@param lang string ----@param debug? boolean ---@return integer -local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, debug) +local function highlight_text(bufnr, ns, hunk, col_offset, text, lang) local ok, parser_obj = pcall(vim.treesitter.get_string_parser, text, lang) if not ok or not parser_obj then return 0 @@ -59,7 +68,6 @@ end ---@field max_lines integer ---@field conceal_prefixes boolean ---@field highlights fugitive-ts.Highlights ----@field debug boolean ---@param bufnr integer ---@param ns integer @@ -72,15 +80,13 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) end if #hunk.lines > opts.max_lines then - if opts.debug then - dbg( - 'skipping hunk %s:%d (%d lines > %d max)', - hunk.filename, - hunk.start_line, - #hunk.lines, - opts.max_lines - ) - end + dbg( + 'skipping hunk %s:%d (%d lines > %d max)', + hunk.filename, + hunk.start_line, + #hunk.lines, + opts.max_lines + ) return end @@ -97,25 +103,19 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) local ok, parser_obj = pcall(vim.treesitter.get_string_parser, code, lang) if not ok or not parser_obj then - if opts.debug then - dbg('failed to create parser for lang: %s', lang) - end + dbg('failed to create parser for lang: %s', lang) return end local trees = parser_obj:parse() if not trees or #trees == 0 then - if opts.debug then - dbg('parse returned no trees for lang: %s', lang) - end + dbg('parse returned no trees for lang: %s', lang) return end local query = vim.treesitter.query.get(lang, 'highlights') if not query then - if opts.debug then - dbg('no highlights query for lang: %s', lang) - end + dbg('no highlights query for lang: %s', lang) return end @@ -126,16 +126,9 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) hl_group = 'Normal', priority = 199, }) - local header_extmarks = highlight_text( - bufnr, - ns, - hunk, - hunk.header_context_col, - hunk.header_context, - lang, - opts.debug - ) - if opts.debug and header_extmarks > 0 then + local header_extmarks = + highlight_text(bufnr, ns, hunk, hunk.header_context_col, hunk.header_context, lang) + if header_extmarks > 0 then dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks) end end @@ -196,9 +189,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) extmark_count = extmark_count + 1 end - if opts.debug then - dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count) - end + dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count) end return M diff --git a/lua/fugitive-ts/init.lua b/lua/fugitive-ts/init.lua index b659342..1e19c03 100644 --- a/lua/fugitive-ts/init.lua +++ b/lua/fugitive-ts/init.lua @@ -72,15 +72,13 @@ local function highlight_buffer(bufnr) vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) - local hunks = - parser.parse_buffer(bufnr, config.languages, config.disabled_languages, config.debug) + local hunks = parser.parse_buffer(bufnr, config.languages, config.disabled_languages) dbg('found %d hunks in buffer %d', #hunks, bufnr) for _, hunk in ipairs(hunks) do highlight.highlight_hunk(bufnr, ns, hunk, { max_lines = config.max_lines_per_hunk, conceal_prefixes = config.conceal_prefixes, highlights = config.highlights, - debug = config.debug, }) end end @@ -160,7 +158,31 @@ end ---@param opts? fugitive-ts.Config function M.setup(opts) opts = opts or {} + + vim.validate({ + enabled = { opts.enabled, 'boolean', true }, + debug = { opts.debug, 'boolean', true }, + languages = { opts.languages, 'table', true }, + disabled_languages = { opts.disabled_languages, 'table', true }, + debounce_ms = { opts.debounce_ms, 'number', true }, + max_lines_per_hunk = { opts.max_lines_per_hunk, 'number', true }, + conceal_prefixes = { opts.conceal_prefixes, 'boolean', true }, + highlights = { opts.highlights, 'table', true }, + }) + + if opts.highlights then + vim.validate({ + ['highlights.treesitter'] = { opts.highlights.treesitter, 'boolean', true }, + ['highlights.headers'] = { opts.highlights.headers, 'boolean', true }, + ['highlights.background'] = { opts.highlights.background, 'boolean', true }, + ['highlights.linenr'] = { opts.highlights.linenr, 'boolean', true }, + ['highlights.vim'] = { opts.highlights.vim, 'boolean', true }, + }) + end + config = vim.tbl_deep_extend('force', default_config, opts) + parser.set_debug(config.debug) + highlight.set_debug(config.debug) end return M diff --git a/lua/fugitive-ts/parser.lua b/lua/fugitive-ts/parser.lua index 8cd119b..fa0bd16 100644 --- a/lua/fugitive-ts/parser.lua +++ b/lua/fugitive-ts/parser.lua @@ -8,9 +8,19 @@ local M = {} +local debug_enabled = false + +---@param enabled boolean +function M.set_debug(enabled) + debug_enabled = enabled +end + ---@param msg string ---@param ... any local function dbg(msg, ...) + if not debug_enabled then + return + end local formatted = string.format(msg, ...) vim.notify('[fugitive-ts] ' .. formatted, vim.log.levels.DEBUG) end @@ -18,15 +28,12 @@ end ---@param filename string ---@param custom_langs? table ---@param disabled_langs? string[] ----@param debug? boolean ---@return string? -local function get_lang_from_filename(filename, custom_langs, disabled_langs, debug) +local function get_lang_from_filename(filename, custom_langs, disabled_langs) if custom_langs and custom_langs[filename] then local lang = custom_langs[filename] if disabled_langs and vim.tbl_contains(disabled_langs, lang) then - if debug then - dbg('lang disabled: %s', lang) - end + dbg('lang disabled: %s', lang) return nil end return lang @@ -34,28 +41,22 @@ local function get_lang_from_filename(filename, custom_langs, disabled_langs, de local ft = vim.filetype.match({ filename = filename }) if not ft then - if debug then - dbg('no filetype for: %s', filename) - end + dbg('no filetype for: %s', filename) return nil end local lang = vim.treesitter.language.get_lang(ft) if lang then if disabled_langs and vim.tbl_contains(disabled_langs, lang) then - if debug then - dbg('lang disabled: %s', lang) - end + dbg('lang disabled: %s', lang) return nil end local ok = pcall(vim.treesitter.language.inspect, lang) if ok then return lang end - if debug then - dbg('no parser for lang: %s (ft: %s)', lang, ft) - end - elseif debug then + dbg('no parser for lang: %s (ft: %s)', lang, ft) + else dbg('no ts lang for filetype: %s', ft) end @@ -65,9 +66,8 @@ end ---@param bufnr integer ---@param custom_langs? table ---@param disabled_langs? string[] ----@param debug? boolean ---@return fugitive-ts.Hunk[] -function M.parse_buffer(bufnr, custom_langs, disabled_langs, debug) +function M.parse_buffer(bufnr, custom_langs, disabled_langs) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) ---@type fugitive-ts.Hunk[] local hunks = {} @@ -107,8 +107,8 @@ function M.parse_buffer(bufnr, custom_langs, disabled_langs, debug) if filename then flush_hunk() current_filename = filename - current_lang = get_lang_from_filename(filename, custom_langs, disabled_langs, debug) - if debug and current_lang then + current_lang = get_lang_from_filename(filename, custom_langs, disabled_langs) + if current_lang then dbg('file: %s -> lang: %s', filename, current_lang) end elseif line:match('^@@.-@@') then diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 3f800f7..38743f6 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -36,7 +36,6 @@ describe('highlight', function() linenr = false, vim = false, }, - debug = false, } if overrides then for k, v in pairs(overrides) do From b3e687c954c3080f1696b62a8aaf719815810a8c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 2 Feb 2026 00:35:08 -0500 Subject: [PATCH 3/5] fix: proper background higlights --- lua/fugitive-ts/highlight.lua | 2 +- lua/fugitive-ts/init.lua | 5 +++++ spec/highlight_spec.lua | 10 +++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lua/fugitive-ts/highlight.lua b/lua/fugitive-ts/highlight.lua index 8c821fb..f8cc026 100644 --- a/lua/fugitive-ts/highlight.lua +++ b/lua/fugitive-ts/highlight.lua @@ -146,7 +146,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) end if opts.highlights.background and (prefix == '+' or prefix == '-') then - local line_hl = prefix == '+' and 'DiffAdd' or 'DiffDelete' + local line_hl = prefix == '+' and 'FugitiveTsAdd' or 'FugitiveTsDelete' local extmark_opts = { line_hl_group = line_hl, priority = 198, diff --git a/lua/fugitive-ts/init.lua b/lua/fugitive-ts/init.lua index 1e19c03..e3f053e 100644 --- a/lua/fugitive-ts/init.lua +++ b/lua/fugitive-ts/init.lua @@ -183,6 +183,11 @@ function M.setup(opts) config = vim.tbl_deep_extend('force', default_config, opts) parser.set_debug(config.debug) highlight.set_debug(config.debug) + + local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' }) + local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' }) + vim.api.nvim_set_hl(0, 'FugitiveTsAdd', { bg = diff_add.bg }) + vim.api.nvim_set_hl(0, 'FugitiveTsDelete', { bg = diff_delete.bg }) end return M diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 38743f6..93c8784 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -7,6 +7,10 @@ describe('highlight', function() before_each(function() ns = vim.api.nvim_create_namespace('fugitive_ts_test') + local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' }) + local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' }) + vim.api.nvim_set_hl(0, 'FugitiveTsAdd', { bg = diff_add.bg }) + vim.api.nvim_set_hl(0, 'FugitiveTsDelete', { bg = diff_delete.bg }) end) local function create_buffer(lines) @@ -318,7 +322,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group == 'DiffAdd' then + if mark[4] and mark[4].line_hl_group == 'FugitiveTsAdd' then has_diff_add = true break end @@ -351,7 +355,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_delete = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group == 'DiffDelete' then + if mark[4] and mark[4].line_hl_group == 'FugitiveTsDelete' then has_diff_delete = true break end @@ -516,7 +520,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group == 'DiffAdd' then + if mark[4] and mark[4].line_hl_group == 'FugitiveTsAdd' then has_diff_add = true break end From d7537e93d0561e4821043aa864b4f245d7f7ba03 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 2 Feb 2026 00:57:41 -0500 Subject: [PATCH 4/5] feat: fix highlighting --- doc/fugitive-ts.nvim.txt | 29 +++++++++++++++-------------- lua/fugitive-ts/highlight.lua | 14 +++++++++----- lua/fugitive-ts/init.lua | 3 --- spec/highlight_spec.lua | 29 +++++++++++++---------------- spec/init_spec.lua | 1 - 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/doc/fugitive-ts.nvim.txt b/doc/fugitive-ts.nvim.txt index 19a8bbc..4945f0c 100644 --- a/doc/fugitive-ts.nvim.txt +++ b/doc/fugitive-ts.nvim.txt @@ -66,9 +66,11 @@ CONFIGURATION *fugitive-ts-config* this many lines. Prevents lag on massive diffs. {conceal_prefixes} (boolean, default: true) - Hide diff prefixes (`+`/`-`/` `) using extmark - conceal. Makes code appear without the leading - diff character. + Hide diff prefixes (`+`/`-`/` `) using virtual + text overlay. Makes code appear without the + leading diff character. When `highlights.background` + is also enabled, the overlay inherits the line's + background color. {highlights} (table, default: see below) Controls which highlight features are enabled. @@ -79,23 +81,22 @@ CONFIGURATION *fugitive-ts-config* {treesitter} (boolean, default: true) Apply treesitter syntax highlighting to code. - {headers} (boolean, default: true) - Highlight function context in hunk headers. - The context portion of `@@ -10,3 +10,4 @@ func()` - will receive treesitter highlighting. - {background} (boolean, default: true) - Apply `DiffAdd` background to `+` lines and - `DiffDelete` background to `-` lines. + Apply background highlighting to `+`/`-` lines + using `FugitiveTsAdd`/`FugitiveTsDelete` groups + (derived from `DiffAdd`/`DiffDelete` backgrounds). {linenr} (boolean, default: true) - Highlight line numbers with `DiffAdd`/`DiffDelete` - colors matching the line background. + Highlight line numbers with matching colors. + Only visible if line numbers are enabled. {vim} (boolean, default: false) Experimental: Use vim syntax highlighting as fallback when no treesitter parser is available. + Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always + highlighted with treesitter when a parser is available. + ============================================================================== API *fugitive-ts-api* @@ -129,10 +130,10 @@ IMPLEMENTATION *fugitive-ts-implementation* - Language is detected from the filename using |vim.filetype.match()| - Diff prefixes (`+`/`-`/` `) are stripped from code lines - Code is parsed with |vim.treesitter.get_string_parser()| - - Background extmarks (`DiffAdd`/`DiffDelete`) applied at priority 198 + - Background extmarks (`FugitiveTsAdd`/`FugitiveTsDelete`) at priority 198 - `Normal` extmarks at priority 199 clear underlying diff foreground - Treesitter highlights are applied as extmarks at priority 200 - - Conceal extmarks hide diff prefixes when `conceal_prefixes` is enabled + - Virtual text overlays hide diff prefixes when `conceal_prefixes` is enabled 4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events ============================================================================== diff --git a/lua/fugitive-ts/highlight.lua b/lua/fugitive-ts/highlight.lua index f8cc026..8c95ceb 100644 --- a/lua/fugitive-ts/highlight.lua +++ b/lua/fugitive-ts/highlight.lua @@ -119,7 +119,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) return end - if opts.highlights.headers and hunk.header_context and hunk.header_context_col then + if 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, @@ -138,15 +138,19 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) local line_len = #line local prefix = line:sub(1, 1) + local is_diff_line = prefix == '+' or prefix == '-' + local line_hl = is_diff_line and (prefix == '+' and 'FugitiveTsAdd' or 'FugitiveTsDelete') + or nil + if opts.conceal_prefixes then + local virt_hl = (opts.highlights.background and line_hl) or nil pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { - end_col = 1, - conceal = '', + virt_text = { { ' ', virt_hl } }, + virt_text_pos = 'overlay', }) end - if opts.highlights.background and (prefix == '+' or prefix == '-') then - local line_hl = prefix == '+' and 'FugitiveTsAdd' or 'FugitiveTsDelete' + if opts.highlights.background and is_diff_line then local extmark_opts = { line_hl_group = line_hl, priority = 198, diff --git a/lua/fugitive-ts/init.lua b/lua/fugitive-ts/init.lua index e3f053e..37e129c 100644 --- a/lua/fugitive-ts/init.lua +++ b/lua/fugitive-ts/init.lua @@ -1,6 +1,5 @@ ---@class fugitive-ts.Highlights ---@field treesitter boolean ----@field headers boolean ---@field background boolean ---@field linenr boolean ---@field vim boolean @@ -37,7 +36,6 @@ local default_config = { conceal_prefixes = true, highlights = { treesitter = true, - headers = true, background = true, linenr = true, vim = false, @@ -173,7 +171,6 @@ function M.setup(opts) if opts.highlights then vim.validate({ ['highlights.treesitter'] = { opts.highlights.treesitter, 'boolean', true }, - ['highlights.headers'] = { opts.highlights.headers, 'boolean', true }, ['highlights.background'] = { opts.highlights.background, 'boolean', true }, ['highlights.linenr'] = { opts.highlights.linenr, 'boolean', true }, ['highlights.vim'] = { opts.highlights.vim, 'boolean', true }, diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 93c8784..fcdf8dd 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -35,7 +35,6 @@ describe('highlight', function() conceal_prefixes = false, highlights = { treesitter = true, - headers = false, background = false, linenr = false, vim = false, @@ -164,7 +163,7 @@ describe('highlight', function() lines = { ' local x = 1', '+local y = 2' }, } - highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ highlights = { headers = true } })) + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) local has_header_extmark = false @@ -178,9 +177,9 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('does not highlight header when disabled', function() + it('does not highlight header when no header_context', function() local bufnr = create_buffer({ - '@@ -10,3 +10,4 @@ function hello()', + '@@ -10,3 +10,4 @@', ' local x = 1', }) @@ -188,8 +187,6 @@ describe('highlight', function() filename = 'test.lua', lang = 'lua', start_line = 1, - header_context = 'function hello()', - header_context_col = 18, lines = { ' local x = 1' }, } @@ -244,7 +241,7 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('applies conceal extmarks when conceal_prefixes enabled', function() + it('applies overlay extmarks when conceal_prefixes enabled', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', ' local x = 1', @@ -261,17 +258,17 @@ describe('highlight', function() highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ conceal_prefixes = true })) local extmarks = get_extmarks(bufnr) - local conceal_count = 0 + local overlay_count = 0 for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].conceal == '' then - conceal_count = conceal_count + 1 + if mark[4] and mark[4].virt_text_pos == 'overlay' then + overlay_count = overlay_count + 1 end end - assert.are.equal(2, conceal_count) + assert.are.equal(2, overlay_count) delete_buffer(bufnr) end) - it('does not apply conceal extmarks when conceal_prefixes disabled', function() + it('does not apply overlay extmarks when conceal_prefixes disabled', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', ' local x = 1', @@ -288,13 +285,13 @@ describe('highlight', function() highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ conceal_prefixes = false })) local extmarks = get_extmarks(bufnr) - local conceal_count = 0 + local overlay_count = 0 for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].conceal == '' then - conceal_count = conceal_count + 1 + if mark[4] and mark[4].virt_text_pos == 'overlay' then + overlay_count = overlay_count + 1 end end - assert.are.equal(0, conceal_count) + assert.are.equal(0, overlay_count) delete_buffer(bufnr) end) diff --git a/spec/init_spec.lua b/spec/init_spec.lua index 77c3db0..5f3b8e8 100644 --- a/spec/init_spec.lua +++ b/spec/init_spec.lua @@ -27,7 +27,6 @@ describe('fugitive-ts', function() conceal_prefixes = false, highlights = { treesitter = true, - headers = false, background = true, linenr = true, vim = false, From 851cdb02145f106e5f2ec1af530a7a301a58afc9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 2 Feb 2026 01:00:40 -0500 Subject: [PATCH 5/5] fix race condition --- lua/fugitive-ts/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/fugitive-ts/init.lua b/lua/fugitive-ts/init.lua index 37e129c..bc965d4 100644 --- a/lua/fugitive-ts/init.lua +++ b/lua/fugitive-ts/init.lua @@ -101,9 +101,9 @@ local function create_debounced_highlight(bufnr) config.debounce_ms, 0, vim.schedule_wrap(function() - t:close() if timer == t then timer = nil + t:close() end highlight_buffer(bufnr) end)