commit
6e5a144aa3
9 changed files with 703 additions and 226 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
Enhance the great `vim-fugitive` with syntax-aware code to easily work with
|
Enhance the great `vim-fugitive` with syntax-aware code to easily work with
|
||||||
diffs.
|
diffs.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,45 +42,56 @@ CONFIGURATION *fugitive-ts-config*
|
||||||
Enable debug logging to |:messages| with
|
Enable debug logging to |:messages| with
|
||||||
`[fugitive-ts]` prefix.
|
`[fugitive-ts]` prefix.
|
||||||
|
|
||||||
{languages} (table<string, string>, default: {})
|
|
||||||
Custom filename to treesitter language mappings.
|
|
||||||
Useful for non-standard file extensions.
|
|
||||||
Example: >lua
|
|
||||||
languages = {
|
|
||||||
['.envrc'] = 'bash',
|
|
||||||
['Justfile'] = 'just',
|
|
||||||
}
|
|
||||||
<
|
|
||||||
{disabled_languages} (string[], default: {})
|
|
||||||
Treesitter language names to skip highlighting.
|
|
||||||
Example: >lua
|
|
||||||
disabled_languages = { 'markdown', 'text' }
|
|
||||||
<
|
|
||||||
{debounce_ms} (integer, default: 0)
|
{debounce_ms} (integer, default: 0)
|
||||||
Debounce delay in milliseconds for re-highlighting
|
Debounce delay in milliseconds for re-highlighting
|
||||||
after buffer changes. Lower values feel snappier
|
after buffer changes. Lower values feel snappier
|
||||||
but use more CPU.
|
but use more CPU.
|
||||||
|
|
||||||
{max_lines_per_hunk} (integer, default: 500)
|
{hide_prefix} (boolean, default: false)
|
||||||
Skip treesitter highlighting for hunks larger than
|
|
||||||
this many lines. Prevents lag on massive diffs.
|
|
||||||
|
|
||||||
{hide_prefix} (boolean, default: true)
|
|
||||||
Hide diff prefixes (`+`/`-`/` `) using virtual
|
Hide diff prefixes (`+`/`-`/` `) using virtual
|
||||||
text overlay. Makes code appear without the
|
text overlay. Makes code appear without the
|
||||||
leading diff character. When `highlights.background`
|
leading diff character. When `highlights.background`
|
||||||
is also enabled, the overlay inherits the line's
|
is also enabled, the overlay inherits the line's
|
||||||
background color.
|
background color.
|
||||||
|
|
||||||
|
{treesitter} (table, default: see below)
|
||||||
|
Treesitter highlighting options.
|
||||||
|
See |fugitive-ts.TreesitterConfig| for fields.
|
||||||
|
|
||||||
|
{vim} (table, default: see below)
|
||||||
|
Vim syntax highlighting options (experimental).
|
||||||
|
See |fugitive-ts.VimConfig| for fields.
|
||||||
|
|
||||||
{highlights} (table, default: see below)
|
{highlights} (table, default: see below)
|
||||||
Controls which highlight features are enabled.
|
Controls which highlight features are enabled.
|
||||||
See |fugitive-ts.Highlights| for fields.
|
See |fugitive-ts.Highlights| for fields.
|
||||||
|
|
||||||
*fugitive-ts.Highlights*
|
*fugitive-ts.TreesitterConfig*
|
||||||
Highlights table fields: ~
|
Treesitter config fields: ~
|
||||||
{treesitter} (boolean, default: true)
|
{enabled} (boolean, default: true)
|
||||||
Apply treesitter syntax highlighting to code.
|
Apply treesitter syntax highlighting to code.
|
||||||
|
|
||||||
|
{max_lines} (integer, default: 500)
|
||||||
|
Skip treesitter highlighting for hunks larger than
|
||||||
|
this many lines. Prevents lag on massive diffs.
|
||||||
|
|
||||||
|
*fugitive-ts.VimConfig*
|
||||||
|
Vim config fields: ~
|
||||||
|
{enabled} (boolean, default: false)
|
||||||
|
Use vim syntax highlighting as fallback when no
|
||||||
|
treesitter parser is available for a language.
|
||||||
|
Creates a scratch buffer, sets the filetype, and
|
||||||
|
queries |synID()| per character to extract
|
||||||
|
highlight groups. Slower than treesitter but
|
||||||
|
covers languages without a TS parser installed.
|
||||||
|
|
||||||
|
{max_lines} (integer, default: 200)
|
||||||
|
Skip vim syntax highlighting for hunks larger than
|
||||||
|
this many lines. Lower than the treesitter default
|
||||||
|
due to the per-character cost of |synID()|.
|
||||||
|
|
||||||
|
*fugitive-ts.Highlights*
|
||||||
|
Highlights table fields: ~
|
||||||
{background} (boolean, default: true)
|
{background} (boolean, default: true)
|
||||||
Apply background highlighting to `+`/`-` lines
|
Apply background highlighting to `+`/`-` lines
|
||||||
using `FugitiveTsAdd`/`FugitiveTsDelete` groups
|
using `FugitiveTsAdd`/`FugitiveTsDelete` groups
|
||||||
|
|
@ -90,13 +101,14 @@ CONFIGURATION *fugitive-ts-config*
|
||||||
Highlight line numbers with matching colors.
|
Highlight line numbers with matching colors.
|
||||||
Only visible if line numbers are enabled.
|
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
|
Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always
|
||||||
highlighted with treesitter when a parser is available.
|
highlighted with treesitter when a parser is available.
|
||||||
|
|
||||||
|
Language detection uses Neovim's built-in |vim.filetype.match()| and
|
||||||
|
|vim.treesitter.language.get_lang()|. To customize filetype detection
|
||||||
|
or register treesitter parsers for custom filetypes, use
|
||||||
|
|vim.filetype.add()| and |vim.treesitter.language.register()|.
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
API *fugitive-ts-api*
|
API *fugitive-ts-api*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,101 +65,43 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class fugitive-ts.HunkOpts
|
---@class fugitive-ts.HunkOpts
|
||||||
---@field max_lines integer
|
|
||||||
---@field hide_prefix boolean
|
---@field hide_prefix boolean
|
||||||
|
---@field treesitter fugitive-ts.TreesitterConfig
|
||||||
|
---@field vim fugitive-ts.VimConfig
|
||||||
---@field highlights fugitive-ts.Highlights
|
---@field highlights fugitive-ts.Highlights
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param ns integer
|
---@param ns integer
|
||||||
---@param hunk fugitive-ts.Hunk
|
---@param hunk fugitive-ts.Hunk
|
||||||
---@param opts fugitive-ts.HunkOpts
|
---@param code_lines string[]
|
||||||
function M.highlight_hunk(bufnr, ns, hunk, opts)
|
---@return integer
|
||||||
|
local function highlight_treesitter(bufnr, ns, hunk, code_lines)
|
||||||
local lang = hunk.lang
|
local lang = hunk.lang
|
||||||
if not lang then
|
if not lang then
|
||||||
return
|
return 0
|
||||||
end
|
|
||||||
|
|
||||||
if #hunk.lines > opts.max_lines then
|
|
||||||
dbg(
|
|
||||||
'skipping hunk %s:%d (%d lines > %d max)',
|
|
||||||
hunk.filename,
|
|
||||||
hunk.start_line,
|
|
||||||
#hunk.lines,
|
|
||||||
opts.max_lines
|
|
||||||
)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
for i, line in ipairs(hunk.lines) do
|
|
||||||
local buf_line = hunk.start_line + i - 1
|
|
||||||
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
|
|
||||||
local number_hl = is_diff_line and (prefix == '+' and 'FugitiveTsAddNr' or 'FugitiveTsDeleteNr')
|
|
||||||
or nil
|
|
||||||
|
|
||||||
if opts.hide_prefix then
|
|
||||||
local virt_hl = (opts.highlights.background and line_hl) or nil
|
|
||||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
|
|
||||||
virt_text = { { ' ', virt_hl } },
|
|
||||||
virt_text_pos = 'overlay',
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
if opts.highlights.background and is_diff_line then
|
|
||||||
local extmark_opts = {
|
|
||||||
line_hl_group = line_hl,
|
|
||||||
priority = 198,
|
|
||||||
}
|
|
||||||
if opts.highlights.gutter then
|
|
||||||
extmark_opts.number_hl_group = number_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',
|
|
||||||
priority = 199,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not opts.highlights.treesitter then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type string[]
|
|
||||||
local code_lines = {}
|
|
||||||
for _, line in ipairs(hunk.lines) do
|
|
||||||
table.insert(code_lines, line:sub(2))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local code = table.concat(code_lines, '\n')
|
local code = table.concat(code_lines, '\n')
|
||||||
if code == '' then
|
if code == '' then
|
||||||
return
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, parser_obj = pcall(vim.treesitter.get_string_parser, code, lang)
|
local ok, parser_obj = pcall(vim.treesitter.get_string_parser, code, lang)
|
||||||
if not ok or not parser_obj then
|
if not ok or not parser_obj then
|
||||||
dbg('failed to create parser for lang: %s', lang)
|
dbg('failed to create parser for lang: %s', lang)
|
||||||
return
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local trees = parser_obj:parse()
|
local trees = parser_obj:parse()
|
||||||
if not trees or #trees == 0 then
|
if not trees or #trees == 0 then
|
||||||
dbg('parse returned no trees for lang: %s', lang)
|
dbg('parse returned no trees for lang: %s', lang)
|
||||||
return
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = vim.treesitter.query.get(lang, 'highlights')
|
local query = vim.treesitter.query.get(lang, 'highlights')
|
||||||
if not query then
|
if not query then
|
||||||
dbg('no highlights query for lang: %s', lang)
|
dbg('no highlights query for lang: %s', lang)
|
||||||
return
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
if hunk.header_context and hunk.header_context_col then
|
if hunk.header_context and hunk.header_context_col then
|
||||||
|
|
@ -195,6 +137,184 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
||||||
extmark_count = extmark_count + 1
|
extmark_count = extmark_count + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return extmark_count
|
||||||
|
end
|
||||||
|
|
||||||
|
---@alias fugitive-ts.SyntaxQueryFn fun(line: integer, col: integer): integer, string
|
||||||
|
|
||||||
|
---@param query_fn fugitive-ts.SyntaxQueryFn
|
||||||
|
---@param code_lines string[]
|
||||||
|
---@return {line: integer, col_start: integer, col_end: integer, hl_name: string}[]
|
||||||
|
function M.coalesce_syntax_spans(query_fn, code_lines)
|
||||||
|
local spans = {}
|
||||||
|
for i, line in ipairs(code_lines) do
|
||||||
|
local col = 1
|
||||||
|
local line_len = #line
|
||||||
|
|
||||||
|
while col <= line_len do
|
||||||
|
local syn_id, hl_name = query_fn(i, col)
|
||||||
|
if syn_id == 0 then
|
||||||
|
col = col + 1
|
||||||
|
else
|
||||||
|
local span_start = col
|
||||||
|
|
||||||
|
col = col + 1
|
||||||
|
while col <= line_len do
|
||||||
|
local next_id, next_name = query_fn(i, col)
|
||||||
|
if next_id == 0 or next_name ~= hl_name then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
col = col + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if hl_name ~= '' then
|
||||||
|
table.insert(spans, {
|
||||||
|
line = i,
|
||||||
|
col_start = span_start,
|
||||||
|
col_end = col,
|
||||||
|
hl_name = hl_name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return spans
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param ns integer
|
||||||
|
---@param hunk fugitive-ts.Hunk
|
||||||
|
---@param code_lines string[]
|
||||||
|
---@return integer
|
||||||
|
local function highlight_vim_syntax(bufnr, ns, hunk, code_lines)
|
||||||
|
local ft = hunk.ft
|
||||||
|
if not ft then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
if #code_lines == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local scratch = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(scratch, 0, -1, false, code_lines)
|
||||||
|
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = scratch })
|
||||||
|
|
||||||
|
local spans = {}
|
||||||
|
|
||||||
|
vim.api.nvim_buf_call(scratch, function()
|
||||||
|
vim.cmd('setlocal syntax=' .. ft)
|
||||||
|
vim.cmd('redraw')
|
||||||
|
|
||||||
|
---@param line integer
|
||||||
|
---@param col integer
|
||||||
|
---@return integer, string
|
||||||
|
local function query_fn(line, col)
|
||||||
|
local syn_id = vim.fn.synID(line, col, 1)
|
||||||
|
if syn_id == 0 then
|
||||||
|
return 0, ''
|
||||||
|
end
|
||||||
|
return syn_id, vim.fn.synIDattr(vim.fn.synIDtrans(syn_id), 'name')
|
||||||
|
end
|
||||||
|
|
||||||
|
spans = M.coalesce_syntax_spans(query_fn, code_lines)
|
||||||
|
end)
|
||||||
|
|
||||||
|
vim.api.nvim_buf_delete(scratch, { force = true })
|
||||||
|
|
||||||
|
local extmark_count = 0
|
||||||
|
for _, span in ipairs(spans) do
|
||||||
|
local buf_line = hunk.start_line + span.line - 1
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
||||||
|
end_col = span.col_end,
|
||||||
|
hl_group = span.hl_name,
|
||||||
|
priority = 200,
|
||||||
|
})
|
||||||
|
extmark_count = extmark_count + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return extmark_count
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param ns integer
|
||||||
|
---@param hunk fugitive-ts.Hunk
|
||||||
|
---@param opts fugitive-ts.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 max_lines = use_ts and opts.treesitter.max_lines or opts.vim.max_lines
|
||||||
|
if (use_ts or use_vim) and #hunk.lines > max_lines then
|
||||||
|
dbg(
|
||||||
|
'skipping hunk %s:%d (%d lines > %d max)',
|
||||||
|
hunk.filename,
|
||||||
|
hunk.start_line,
|
||||||
|
#hunk.lines,
|
||||||
|
max_lines
|
||||||
|
)
|
||||||
|
use_ts = false
|
||||||
|
use_vim = false
|
||||||
|
end
|
||||||
|
|
||||||
|
local apply_syntax = use_ts or use_vim
|
||||||
|
|
||||||
|
---@type string[]
|
||||||
|
local code_lines = {}
|
||||||
|
if apply_syntax then
|
||||||
|
for _, line in ipairs(hunk.lines) do
|
||||||
|
table.insert(code_lines, line:sub(2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local extmark_count = 0
|
||||||
|
if use_ts then
|
||||||
|
extmark_count = highlight_treesitter(bufnr, ns, hunk, code_lines)
|
||||||
|
elseif use_vim then
|
||||||
|
extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
local syntax_applied = extmark_count > 0
|
||||||
|
|
||||||
|
for i, line in ipairs(hunk.lines) do
|
||||||
|
local buf_line = hunk.start_line + i - 1
|
||||||
|
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
|
||||||
|
local number_hl = is_diff_line and (prefix == '+' and 'FugitiveTsAddNr' or 'FugitiveTsDeleteNr')
|
||||||
|
or nil
|
||||||
|
|
||||||
|
if opts.hide_prefix then
|
||||||
|
local virt_hl = (opts.highlights.background and line_hl) or nil
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
|
||||||
|
virt_text = { { ' ', virt_hl } },
|
||||||
|
virt_text_pos = 'overlay',
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.highlights.background and is_diff_line then
|
||||||
|
local extmark_opts = {
|
||||||
|
line_hl_group = line_hl,
|
||||||
|
priority = 198,
|
||||||
|
}
|
||||||
|
if opts.highlights.gutter then
|
||||||
|
extmark_opts.number_hl_group = number_hl
|
||||||
|
end
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, extmark_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
if line_len > 1 and syntax_applied then
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, {
|
||||||
|
end_col = line_len,
|
||||||
|
hl_group = 'Normal',
|
||||||
|
priority = 199,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count)
|
dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
---@class fugitive-ts.Highlights
|
---@class fugitive-ts.Highlights
|
||||||
---@field treesitter boolean
|
|
||||||
---@field background boolean
|
---@field background boolean
|
||||||
---@field gutter boolean
|
---@field gutter boolean
|
||||||
---@field vim boolean
|
|
||||||
|
---@class fugitive-ts.TreesitterConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field max_lines integer
|
||||||
|
|
||||||
|
---@class fugitive-ts.VimConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field max_lines integer
|
||||||
|
|
||||||
---@class fugitive-ts.Config
|
---@class fugitive-ts.Config
|
||||||
---@field enabled boolean
|
---@field enabled boolean
|
||||||
---@field debug boolean
|
---@field debug boolean
|
||||||
---@field languages table<string, string>
|
|
||||||
---@field disabled_languages string[]
|
|
||||||
---@field debounce_ms integer
|
---@field debounce_ms integer
|
||||||
---@field max_lines_per_hunk integer
|
|
||||||
---@field hide_prefix boolean
|
---@field hide_prefix boolean
|
||||||
|
---@field treesitter fugitive-ts.TreesitterConfig
|
||||||
|
---@field vim fugitive-ts.VimConfig
|
||||||
---@field highlights fugitive-ts.Highlights
|
---@field highlights fugitive-ts.Highlights
|
||||||
|
|
||||||
---@class fugitive-ts
|
---@class fugitive-ts
|
||||||
|
|
@ -61,16 +66,19 @@ end
|
||||||
local default_config = {
|
local default_config = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
debug = false,
|
debug = false,
|
||||||
languages = {},
|
|
||||||
disabled_languages = {},
|
|
||||||
debounce_ms = 0,
|
debounce_ms = 0,
|
||||||
max_lines_per_hunk = 500,
|
|
||||||
hide_prefix = false,
|
hide_prefix = false,
|
||||||
|
treesitter = {
|
||||||
|
enabled = true,
|
||||||
|
max_lines = 500,
|
||||||
|
},
|
||||||
|
vim = {
|
||||||
|
enabled = false,
|
||||||
|
max_lines = 200,
|
||||||
|
},
|
||||||
highlights = {
|
highlights = {
|
||||||
treesitter = true,
|
|
||||||
background = true,
|
background = true,
|
||||||
gutter = true,
|
gutter = true,
|
||||||
vim = false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,12 +110,13 @@ local function highlight_buffer(bufnr)
|
||||||
|
|
||||||
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
||||||
|
|
||||||
local hunks = parser.parse_buffer(bufnr, config.languages, config.disabled_languages)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
dbg('found %d hunks in buffer %d', #hunks, bufnr)
|
dbg('found %d hunks in buffer %d', #hunks, bufnr)
|
||||||
for _, hunk in ipairs(hunks) do
|
for _, hunk in ipairs(hunks) do
|
||||||
highlight.highlight_hunk(bufnr, ns, hunk, {
|
highlight.highlight_hunk(bufnr, ns, hunk, {
|
||||||
max_lines = config.max_lines_per_hunk,
|
|
||||||
hide_prefix = config.hide_prefix,
|
hide_prefix = config.hide_prefix,
|
||||||
|
treesitter = config.treesitter,
|
||||||
|
vim = config.vim,
|
||||||
highlights = config.highlights,
|
highlights = config.highlights,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
@ -171,6 +180,14 @@ function M.attach(bufnr)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd('BufReadPost', {
|
||||||
|
buffer = bufnr,
|
||||||
|
callback = function()
|
||||||
|
dbg('BufReadPost event, re-highlighting buffer %d', bufnr)
|
||||||
|
highlight_buffer(bufnr)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd('BufWipeout', {
|
vim.api.nvim_create_autocmd('BufWipeout', {
|
||||||
buffer = bufnr,
|
buffer = bufnr,
|
||||||
callback = function()
|
callback = function()
|
||||||
|
|
@ -185,34 +202,7 @@ function M.refresh(bufnr)
|
||||||
highlight_buffer(bufnr)
|
highlight_buffer(bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param opts? fugitive-ts.Config
|
local function compute_highlight_groups()
|
||||||
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 },
|
|
||||||
hide_prefix = { opts.hide_prefix, 'boolean', true },
|
|
||||||
highlights = { opts.highlights, 'table', true },
|
|
||||||
})
|
|
||||||
|
|
||||||
if opts.highlights then
|
|
||||||
vim.validate({
|
|
||||||
['highlights.treesitter'] = { opts.highlights.treesitter, 'boolean', true },
|
|
||||||
['highlights.background'] = { opts.highlights.background, 'boolean', true },
|
|
||||||
['highlights.gutter'] = { opts.highlights.gutter, '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)
|
|
||||||
|
|
||||||
local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
|
local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
|
||||||
local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' })
|
local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' })
|
||||||
local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' })
|
local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' })
|
||||||
|
|
@ -234,4 +224,55 @@ function M.setup(opts)
|
||||||
vim.api.nvim_set_hl(0, 'FugitiveTsDeleteNr', { fg = del_fg, bg = blended_del })
|
vim.api.nvim_set_hl(0, 'FugitiveTsDeleteNr', { fg = del_fg, bg = blended_del })
|
||||||
end
|
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 },
|
||||||
|
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
|
||||||
|
|
||||||
|
config = vim.tbl_deep_extend('force', default_config, opts)
|
||||||
|
parser.set_debug(config.debug)
|
||||||
|
highlight.set_debug(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,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---@class fugitive-ts.Hunk
|
---@class fugitive-ts.Hunk
|
||||||
---@field filename string
|
---@field filename string
|
||||||
---@field lang string
|
---@field ft string?
|
||||||
|
---@field lang string?
|
||||||
---@field start_line integer
|
---@field start_line integer
|
||||||
---@field header_context string?
|
---@field header_context string?
|
||||||
---@field header_context_col integer?
|
---@field header_context_col integer?
|
||||||
|
|
@ -26,31 +27,20 @@ local function dbg(msg, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param filename string
|
---@param filename string
|
||||||
---@param custom_langs? table<string, string>
|
|
||||||
---@param disabled_langs? string[]
|
|
||||||
---@return string?
|
---@return string?
|
||||||
local function get_lang_from_filename(filename, custom_langs, disabled_langs)
|
local function get_ft_from_filename(filename)
|
||||||
if custom_langs and custom_langs[filename] then
|
|
||||||
local lang = custom_langs[filename]
|
|
||||||
if disabled_langs and vim.tbl_contains(disabled_langs, lang) then
|
|
||||||
dbg('lang disabled: %s', lang)
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return lang
|
|
||||||
end
|
|
||||||
|
|
||||||
local ft = vim.filetype.match({ filename = filename })
|
local ft = vim.filetype.match({ filename = filename })
|
||||||
if not ft then
|
if not ft then
|
||||||
dbg('no filetype for: %s', filename)
|
dbg('no filetype for: %s', filename)
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
return ft
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ft string
|
||||||
|
---@return string?
|
||||||
|
local function get_lang_from_ft(ft)
|
||||||
local lang = vim.treesitter.language.get_lang(ft)
|
local lang = vim.treesitter.language.get_lang(ft)
|
||||||
if lang then
|
if lang then
|
||||||
if disabled_langs and vim.tbl_contains(disabled_langs, lang) then
|
|
||||||
dbg('lang disabled: %s', lang)
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local ok = pcall(vim.treesitter.language.inspect, lang)
|
local ok = pcall(vim.treesitter.language.inspect, lang)
|
||||||
if ok then
|
if ok then
|
||||||
return lang
|
return lang
|
||||||
|
|
@ -59,15 +49,12 @@ local function get_lang_from_filename(filename, custom_langs, disabled_langs)
|
||||||
else
|
else
|
||||||
dbg('no ts lang for filetype: %s', ft)
|
dbg('no ts lang for filetype: %s', ft)
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param custom_langs? table<string, string>
|
|
||||||
---@param disabled_langs? string[]
|
|
||||||
---@return fugitive-ts.Hunk[]
|
---@return fugitive-ts.Hunk[]
|
||||||
function M.parse_buffer(bufnr, custom_langs, disabled_langs)
|
function M.parse_buffer(bufnr)
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
---@type fugitive-ts.Hunk[]
|
---@type fugitive-ts.Hunk[]
|
||||||
local hunks = {}
|
local hunks = {}
|
||||||
|
|
@ -75,6 +62,8 @@ function M.parse_buffer(bufnr, custom_langs, disabled_langs)
|
||||||
---@type string?
|
---@type string?
|
||||||
local current_filename = nil
|
local current_filename = nil
|
||||||
---@type string?
|
---@type string?
|
||||||
|
local current_ft = nil
|
||||||
|
---@type string?
|
||||||
local current_lang = nil
|
local current_lang = nil
|
||||||
---@type integer?
|
---@type integer?
|
||||||
local hunk_start = nil
|
local hunk_start = nil
|
||||||
|
|
@ -86,9 +75,10 @@ function M.parse_buffer(bufnr, custom_langs, disabled_langs)
|
||||||
local hunk_lines = {}
|
local hunk_lines = {}
|
||||||
|
|
||||||
local function flush_hunk()
|
local function flush_hunk()
|
||||||
if hunk_start and #hunk_lines > 0 and current_lang then
|
if hunk_start and #hunk_lines > 0 and (current_lang or current_ft) then
|
||||||
table.insert(hunks, {
|
table.insert(hunks, {
|
||||||
filename = current_filename,
|
filename = current_filename,
|
||||||
|
ft = current_ft,
|
||||||
lang = current_lang,
|
lang = current_lang,
|
||||||
start_line = hunk_start,
|
start_line = hunk_start,
|
||||||
header_context = hunk_header_context,
|
header_context = hunk_header_context,
|
||||||
|
|
@ -107,9 +97,12 @@ function M.parse_buffer(bufnr, custom_langs, disabled_langs)
|
||||||
if filename then
|
if filename then
|
||||||
flush_hunk()
|
flush_hunk()
|
||||||
current_filename = filename
|
current_filename = filename
|
||||||
current_lang = get_lang_from_filename(filename, custom_langs, disabled_langs)
|
current_ft = get_ft_from_filename(filename)
|
||||||
|
current_lang = current_ft and get_lang_from_ft(current_ft) or nil
|
||||||
if current_lang then
|
if current_lang then
|
||||||
dbg('file: %s -> lang: %s', filename, current_lang)
|
dbg('file: %s -> lang: %s', filename, current_lang)
|
||||||
|
elseif current_ft then
|
||||||
|
dbg('file: %s -> ft: %s (no ts parser)', filename, current_ft)
|
||||||
end
|
end
|
||||||
elseif line:match('^@@.-@@') then
|
elseif line:match('^@@.-@@') then
|
||||||
flush_hunk()
|
flush_hunk()
|
||||||
|
|
@ -126,6 +119,7 @@ function M.parse_buffer(bufnr, custom_langs, disabled_langs)
|
||||||
elseif line == '' or line:match('^[MADRC%?!]%s+') or line:match('^%a') then
|
elseif line == '' or line:match('^[MADRC%?!]%s+') or line:match('^%a') then
|
||||||
flush_hunk()
|
flush_hunk()
|
||||||
current_filename = nil
|
current_filename = nil
|
||||||
|
current_ft = nil
|
||||||
current_lang = nil
|
current_lang = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
65
scripts/ci.sh
Executable file
65
scripts/ci.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
tmpdir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
|
||||||
|
run_job() {
|
||||||
|
local name=$1
|
||||||
|
shift
|
||||||
|
local log="$tmpdir/$name.log"
|
||||||
|
if "$@" >"$log" 2>&1; then
|
||||||
|
echo -e "${GREEN}✓${RESET} $name"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${RESET} $name"
|
||||||
|
cat "$log"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e "${BOLD}Running CI jobs in parallel...${RESET}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
pids=()
|
||||||
|
jobs_names=()
|
||||||
|
|
||||||
|
run_job "stylua" stylua --check . &
|
||||||
|
pids+=($!); jobs_names+=("stylua")
|
||||||
|
|
||||||
|
run_job "selene" selene --display-style quiet . &
|
||||||
|
pids+=($!); jobs_names+=("selene")
|
||||||
|
|
||||||
|
run_job "prettier" prettier --check . &
|
||||||
|
pids+=($!); jobs_names+=("prettier")
|
||||||
|
|
||||||
|
run_job "busted" env \
|
||||||
|
LUA_PATH="/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;/usr/lib/lua/5.1/?.lua;/usr/lib/lua/5.1/?/init.lua;;" \
|
||||||
|
LUA_CPATH="/usr/lib/lua/5.1/?.so;;" \
|
||||||
|
nvim -l /usr/lib/luarocks/rocks-5.1/busted/2.3.0-1/bin/busted --verbose spec/ &
|
||||||
|
pids+=($!); jobs_names+=("busted")
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
for i in "${!pids[@]}"; do
|
||||||
|
if ! wait "${pids[$i]}"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$failed" -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}${BOLD}All jobs passed.${RESET}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}${BOLD}Some jobs failed.${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -31,20 +31,25 @@ describe('highlight', function()
|
||||||
|
|
||||||
local function default_opts(overrides)
|
local function default_opts(overrides)
|
||||||
local opts = {
|
local opts = {
|
||||||
max_lines = 500,
|
|
||||||
hide_prefix = false,
|
hide_prefix = false,
|
||||||
|
treesitter = {
|
||||||
|
enabled = true,
|
||||||
|
max_lines = 500,
|
||||||
|
},
|
||||||
|
vim = {
|
||||||
|
enabled = false,
|
||||||
|
max_lines = 200,
|
||||||
|
},
|
||||||
highlights = {
|
highlights = {
|
||||||
treesitter = true,
|
|
||||||
background = false,
|
background = false,
|
||||||
gutter = false,
|
gutter = false,
|
||||||
vim = false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if overrides then
|
if overrides then
|
||||||
for k, v in pairs(overrides) do
|
for k, v in pairs(overrides) do
|
||||||
if k == 'highlights' then
|
if type(v) == 'table' and type(opts[k]) == 'table' then
|
||||||
for hk, hv in pairs(v) do
|
for sk, sv in pairs(v) do
|
||||||
opts.highlights[hk] = hv
|
opts[k][sk] = sv
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
opts[k] = v
|
opts[k] = v
|
||||||
|
|
@ -126,7 +131,7 @@ describe('highlight', function()
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('does nothing for nil lang', function()
|
it('does nothing for nil lang and nil ft', function()
|
||||||
local bufnr = create_buffer({
|
local bufnr = create_buffer({
|
||||||
'@@ -1,1 +1,2 @@',
|
'@@ -1,1 +1,2 @@',
|
||||||
' some content',
|
' some content',
|
||||||
|
|
@ -135,6 +140,7 @@ describe('highlight', function()
|
||||||
|
|
||||||
local hunk = {
|
local hunk = {
|
||||||
filename = 'test.unknown',
|
filename = 'test.unknown',
|
||||||
|
ft = nil,
|
||||||
lang = nil,
|
lang = nil,
|
||||||
start_line = 1,
|
start_line = 1,
|
||||||
lines = { ' some content', '+more content' },
|
lines = { ' some content', '+more content' },
|
||||||
|
|
@ -478,7 +484,7 @@ describe('highlight', function()
|
||||||
bufnr,
|
bufnr,
|
||||||
ns,
|
ns,
|
||||||
hunk,
|
hunk,
|
||||||
default_opts({ highlights = { treesitter = false, background = true } })
|
default_opts({ treesitter = { enabled = false }, highlights = { background = true } })
|
||||||
)
|
)
|
||||||
|
|
||||||
local extmarks = get_extmarks(bufnr)
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
|
@ -511,7 +517,7 @@ describe('highlight', function()
|
||||||
bufnr,
|
bufnr,
|
||||||
ns,
|
ns,
|
||||||
hunk,
|
hunk,
|
||||||
default_opts({ highlights = { treesitter = false, background = true } })
|
default_opts({ treesitter = { enabled = false }, highlights = { background = true } })
|
||||||
)
|
)
|
||||||
|
|
||||||
local extmarks = get_extmarks(bufnr)
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
|
@ -525,5 +531,242 @@ describe('highlight', function()
|
||||||
assert.is_true(has_diff_add)
|
assert.is_true(has_diff_add)
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('applies vim syntax extmarks when vim.enabled and no TS parser', function()
|
||||||
|
local orig_synID = vim.fn.synID
|
||||||
|
local orig_synIDtrans = vim.fn.synIDtrans
|
||||||
|
local orig_synIDattr = vim.fn.synIDattr
|
||||||
|
vim.fn.synID = function(_line, _col, _trans)
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
vim.fn.synIDtrans = function(id)
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
vim.fn.synIDattr = function(_id, _what)
|
||||||
|
return 'Identifier'
|
||||||
|
end
|
||||||
|
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'@@ -1,1 +1,2 @@',
|
||||||
|
' local x = 1',
|
||||||
|
'+local y = 2',
|
||||||
|
})
|
||||||
|
|
||||||
|
local hunk = {
|
||||||
|
filename = 'test.lua',
|
||||||
|
ft = 'lua',
|
||||||
|
lang = nil,
|
||||||
|
start_line = 1,
|
||||||
|
lines = { ' local x = 1', '+local y = 2' },
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ vim = { enabled = true } }))
|
||||||
|
|
||||||
|
vim.fn.synID = orig_synID
|
||||||
|
vim.fn.synIDtrans = orig_synIDtrans
|
||||||
|
vim.fn.synIDattr = orig_synIDattr
|
||||||
|
|
||||||
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
local has_syntax_hl = false
|
||||||
|
for _, mark in ipairs(extmarks) do
|
||||||
|
if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'Normal' then
|
||||||
|
has_syntax_hl = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(has_syntax_hl)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('skips vim fallback when vim.enabled is false', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'@@ -1,1 +1,2 @@',
|
||||||
|
' local x = 1',
|
||||||
|
'+local y = 2',
|
||||||
|
})
|
||||||
|
|
||||||
|
local hunk = {
|
||||||
|
filename = 'test.lua',
|
||||||
|
ft = 'lua',
|
||||||
|
lang = nil,
|
||||||
|
start_line = 1,
|
||||||
|
lines = { ' local x = 1', '+local y = 2' },
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ vim = { enabled = false } }))
|
||||||
|
|
||||||
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
local has_syntax_hl = false
|
||||||
|
for _, mark in ipairs(extmarks) do
|
||||||
|
if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'Normal' then
|
||||||
|
has_syntax_hl = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_false(has_syntax_hl)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('respects vim.max_lines', function()
|
||||||
|
local lines = { '@@ -1,100 +1,101 @@' }
|
||||||
|
local hunk_lines = {}
|
||||||
|
for i = 1, 250 do
|
||||||
|
table.insert(lines, ' line ' .. i)
|
||||||
|
table.insert(hunk_lines, ' line ' .. i)
|
||||||
|
end
|
||||||
|
|
||||||
|
local bufnr = create_buffer(lines)
|
||||||
|
local hunk = {
|
||||||
|
filename = 'test.lua',
|
||||||
|
ft = 'lua',
|
||||||
|
lang = nil,
|
||||||
|
start_line = 1,
|
||||||
|
lines = hunk_lines,
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight.highlight_hunk(
|
||||||
|
bufnr,
|
||||||
|
ns,
|
||||||
|
hunk,
|
||||||
|
default_opts({ vim = { enabled = true, max_lines = 200 } })
|
||||||
|
)
|
||||||
|
|
||||||
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
assert.are.equal(0, #extmarks)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('applies background for vim fallback hunks', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'@@ -1,1 +1,2 @@',
|
||||||
|
' local x = 1',
|
||||||
|
'+local y = 2',
|
||||||
|
})
|
||||||
|
|
||||||
|
local hunk = {
|
||||||
|
filename = 'test.lua',
|
||||||
|
ft = 'lua',
|
||||||
|
lang = nil,
|
||||||
|
start_line = 1,
|
||||||
|
lines = { ' local x = 1', '+local y = 2' },
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight.highlight_hunk(
|
||||||
|
bufnr,
|
||||||
|
ns,
|
||||||
|
hunk,
|
||||||
|
default_opts({ vim = { enabled = true }, 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 == 'FugitiveTsAdd' then
|
||||||
|
has_diff_add = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(has_diff_add)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('applies Normal blanking for vim fallback hunks', function()
|
||||||
|
local orig_synID = vim.fn.synID
|
||||||
|
local orig_synIDtrans = vim.fn.synIDtrans
|
||||||
|
local orig_synIDattr = vim.fn.synIDattr
|
||||||
|
vim.fn.synID = function(_line, _col, _trans)
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
vim.fn.synIDtrans = function(id)
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
vim.fn.synIDattr = function(_id, _what)
|
||||||
|
return 'Identifier'
|
||||||
|
end
|
||||||
|
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'@@ -1,1 +1,2 @@',
|
||||||
|
' local x = 1',
|
||||||
|
'+local y = 2',
|
||||||
|
})
|
||||||
|
|
||||||
|
local hunk = {
|
||||||
|
filename = 'test.lua',
|
||||||
|
ft = 'lua',
|
||||||
|
lang = nil,
|
||||||
|
start_line = 1,
|
||||||
|
lines = { ' local x = 1', '+local y = 2' },
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ vim = { enabled = true } }))
|
||||||
|
|
||||||
|
vim.fn.synID = orig_synID
|
||||||
|
vim.fn.synIDtrans = orig_synIDtrans
|
||||||
|
vim.fn.synIDattr = orig_synIDattr
|
||||||
|
|
||||||
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
local has_normal = false
|
||||||
|
for _, mark in ipairs(extmarks) do
|
||||||
|
if mark[4] and mark[4].hl_group == 'Normal' then
|
||||||
|
has_normal = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(has_normal)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('coalesce_syntax_spans', function()
|
||||||
|
it('coalesces adjacent chars with same hl group', function()
|
||||||
|
local function query_fn(_line, _col)
|
||||||
|
return 1, 'Keyword'
|
||||||
|
end
|
||||||
|
local spans = highlight.coalesce_syntax_spans(query_fn, { 'hello' })
|
||||||
|
assert.are.equal(1, #spans)
|
||||||
|
assert.are.equal(1, spans[1].col_start)
|
||||||
|
assert.are.equal(6, spans[1].col_end)
|
||||||
|
assert.are.equal('Keyword', spans[1].hl_name)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('splits spans at hl group boundaries', function()
|
||||||
|
local function query_fn(_line, col)
|
||||||
|
if col <= 3 then
|
||||||
|
return 1, 'Keyword'
|
||||||
|
end
|
||||||
|
return 2, 'String'
|
||||||
|
end
|
||||||
|
local spans = highlight.coalesce_syntax_spans(query_fn, { 'abcdef' })
|
||||||
|
assert.are.equal(2, #spans)
|
||||||
|
assert.are.equal('Keyword', spans[1].hl_name)
|
||||||
|
assert.are.equal(1, spans[1].col_start)
|
||||||
|
assert.are.equal(4, spans[1].col_end)
|
||||||
|
assert.are.equal('String', spans[2].hl_name)
|
||||||
|
assert.are.equal(4, spans[2].col_start)
|
||||||
|
assert.are.equal(7, spans[2].col_end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('skips syn_id 0 gaps', function()
|
||||||
|
local function query_fn(_line, col)
|
||||||
|
if col == 2 or col == 3 then
|
||||||
|
return 0, ''
|
||||||
|
end
|
||||||
|
return 1, 'Identifier'
|
||||||
|
end
|
||||||
|
local spans = highlight.coalesce_syntax_spans(query_fn, { 'abcd' })
|
||||||
|
assert.are.equal(2, #spans)
|
||||||
|
assert.are.equal(1, spans[1].col_start)
|
||||||
|
assert.are.equal(2, spans[1].col_end)
|
||||||
|
assert.are.equal(4, spans[2].col_start)
|
||||||
|
assert.are.equal(5, spans[2].col_end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('skips empty hl_name spans', function()
|
||||||
|
local function query_fn(_line, _col)
|
||||||
|
return 1, ''
|
||||||
|
end
|
||||||
|
local spans = highlight.coalesce_syntax_spans(query_fn, { 'abc' })
|
||||||
|
assert.are.equal(0, #spans)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,19 @@ describe('fugitive-ts', function()
|
||||||
fugitive_ts.setup({
|
fugitive_ts.setup({
|
||||||
enabled = false,
|
enabled = false,
|
||||||
debug = true,
|
debug = true,
|
||||||
languages = { ['.envrc'] = 'bash' },
|
|
||||||
disabled_languages = { 'markdown' },
|
|
||||||
debounce_ms = 100,
|
debounce_ms = 100,
|
||||||
max_lines_per_hunk = 1000,
|
|
||||||
hide_prefix = false,
|
hide_prefix = false,
|
||||||
|
treesitter = {
|
||||||
|
enabled = true,
|
||||||
|
max_lines = 1000,
|
||||||
|
},
|
||||||
|
vim = {
|
||||||
|
enabled = false,
|
||||||
|
max_lines = 200,
|
||||||
|
},
|
||||||
highlights = {
|
highlights = {
|
||||||
treesitter = true,
|
|
||||||
background = true,
|
background = true,
|
||||||
gutter = true,
|
gutter = true,
|
||||||
vim = false,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,9 @@ describe('parser', function()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local test_langs = {
|
|
||||||
['lua/test.lua'] = 'lua',
|
|
||||||
['lua/foo.lua'] = 'lua',
|
|
||||||
['src/bar.py'] = 'python',
|
|
||||||
['test.lua'] = 'lua',
|
|
||||||
['test.py'] = 'python',
|
|
||||||
['other.lua'] = 'lua',
|
|
||||||
['.envrc'] = 'bash',
|
|
||||||
}
|
|
||||||
|
|
||||||
it('returns empty table for empty buffer', function()
|
it('returns empty table for empty buffer', function()
|
||||||
local bufnr = create_buffer({})
|
local bufnr = create_buffer({})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
assert.are.same({}, hunks)
|
assert.are.same({}, hunks)
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end)
|
end)
|
||||||
|
|
@ -40,7 +30,7 @@ describe('parser', function()
|
||||||
'Unstaged (1)',
|
'Unstaged (1)',
|
||||||
'M lua/test.lua',
|
'M lua/test.lua',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
assert.are.same({}, hunks)
|
assert.are.same({}, hunks)
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end)
|
end)
|
||||||
|
|
@ -54,10 +44,11 @@ describe('parser', function()
|
||||||
'+local new = true',
|
'+local new = true',
|
||||||
' return M',
|
' return M',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
assert.are.equal(1, #hunks)
|
||||||
assert.are.equal('lua/test.lua', hunks[1].filename)
|
assert.are.equal('lua/test.lua', hunks[1].filename)
|
||||||
|
assert.are.equal('lua', hunks[1].ft)
|
||||||
assert.are.equal('lua', hunks[1].lang)
|
assert.are.equal('lua', hunks[1].lang)
|
||||||
assert.are.equal(3, hunks[1].start_line)
|
assert.are.equal(3, hunks[1].start_line)
|
||||||
assert.are.equal(3, #hunks[1].lines)
|
assert.are.equal(3, #hunks[1].lines)
|
||||||
|
|
@ -76,7 +67,7 @@ describe('parser', function()
|
||||||
'+ print("hello")',
|
'+ print("hello")',
|
||||||
' end',
|
' end',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
assert.are.equal(2, #hunks)
|
assert.are.equal(2, #hunks)
|
||||||
assert.are.equal(2, hunks[1].start_line)
|
assert.are.equal(2, hunks[1].start_line)
|
||||||
|
|
@ -85,6 +76,25 @@ describe('parser', function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('detects hunks across multiple files', function()
|
it('detects hunks across multiple files', function()
|
||||||
|
local orig_get_lang = vim.treesitter.language.get_lang
|
||||||
|
local orig_inspect = vim.treesitter.language.inspect
|
||||||
|
vim.treesitter.language.get_lang = function(ft)
|
||||||
|
local result = orig_get_lang(ft)
|
||||||
|
if result then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
if ft == 'python' then
|
||||||
|
return 'python'
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
vim.treesitter.language.inspect = function(lang)
|
||||||
|
if lang == 'python' then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return orig_inspect(lang)
|
||||||
|
end
|
||||||
|
|
||||||
local bufnr = create_buffer({
|
local bufnr = create_buffer({
|
||||||
'M lua/foo.lua',
|
'M lua/foo.lua',
|
||||||
'@@ -1,1 +1,2 @@',
|
'@@ -1,1 +1,2 @@',
|
||||||
|
|
@ -95,7 +105,10 @@ describe('parser', function()
|
||||||
' def hello():',
|
' def hello():',
|
||||||
'+ pass',
|
'+ pass',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
|
vim.treesitter.language.get_lang = orig_get_lang
|
||||||
|
vim.treesitter.language.inspect = orig_inspect
|
||||||
|
|
||||||
assert.are.equal(2, #hunks)
|
assert.are.equal(2, #hunks)
|
||||||
assert.are.equal('lua/foo.lua', hunks[1].filename)
|
assert.are.equal('lua/foo.lua', hunks[1].filename)
|
||||||
|
|
@ -113,7 +126,7 @@ describe('parser', function()
|
||||||
'+print(msg)',
|
'+print(msg)',
|
||||||
' end',
|
' end',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
assert.are.equal(1, #hunks)
|
||||||
assert.are.equal('function M.hello()', hunks[1].header_context)
|
assert.are.equal('function M.hello()', hunks[1].header_context)
|
||||||
|
|
@ -128,46 +141,13 @@ describe('parser', function()
|
||||||
' local M = {}',
|
' local M = {}',
|
||||||
'+local x = 1',
|
'+local x = 1',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
assert.are.equal(1, #hunks)
|
||||||
assert.is_nil(hunks[1].header_context)
|
assert.is_nil(hunks[1].header_context)
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('respects custom language mappings', function()
|
|
||||||
local bufnr = create_buffer({
|
|
||||||
'M .envrc',
|
|
||||||
'@@ -1,1 +1,2 @@',
|
|
||||||
' export FOO=bar',
|
|
||||||
'+export BAZ=qux',
|
|
||||||
})
|
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal('bash', hunks[1].lang)
|
|
||||||
delete_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('respects disabled_languages', function()
|
|
||||||
local bufnr = create_buffer({
|
|
||||||
'M test.lua',
|
|
||||||
'@@ -1,1 +1,2 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local x = 1',
|
|
||||||
'M test.py',
|
|
||||||
'@@ -1,1 +1,2 @@',
|
|
||||||
' def foo():',
|
|
||||||
'+ pass',
|
|
||||||
})
|
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, { 'lua' }, false)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal('test.py', hunks[1].filename)
|
|
||||||
assert.are.equal('python', hunks[1].lang)
|
|
||||||
delete_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles all git status prefixes', function()
|
it('handles all git status prefixes', function()
|
||||||
local prefixes = { 'M', 'A', 'D', 'R', 'C', '?', '!' }
|
local prefixes = { 'M', 'A', 'D', 'R', 'C', '?', '!' }
|
||||||
for _, prefix in ipairs(prefixes) do
|
for _, prefix in ipairs(prefixes) do
|
||||||
|
|
@ -177,7 +157,7 @@ describe('parser', function()
|
||||||
' local x = 1',
|
' local x = 1',
|
||||||
'+local y = 2',
|
'+local y = 2',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
assert.are.equal(1, #hunks, 'Failed for prefix: ' .. prefix)
|
assert.are.equal(1, #hunks, 'Failed for prefix: ' .. prefix)
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end
|
end
|
||||||
|
|
@ -192,13 +172,32 @@ describe('parser', function()
|
||||||
'',
|
'',
|
||||||
'Some other content',
|
'Some other content',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
assert.are.equal(1, #hunks)
|
||||||
assert.are.equal(2, #hunks[1].lines)
|
assert.are.equal(2, #hunks[1].lines)
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('emits hunk with ft when no ts parser available', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'M test.xyz_no_parser',
|
||||||
|
'@@ -1,1 +1,2 @@',
|
||||||
|
' some content',
|
||||||
|
'+more content',
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.filetype.add({ extension = { xyz_no_parser = 'xyz_no_parser_ft' } })
|
||||||
|
|
||||||
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
|
assert.are.equal(1, #hunks)
|
||||||
|
assert.are.equal('xyz_no_parser_ft', hunks[1].ft)
|
||||||
|
assert.is_nil(hunks[1].lang)
|
||||||
|
assert.are.equal(2, #hunks[1].lines)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
it('stops hunk at next file header', function()
|
it('stops hunk at next file header', function()
|
||||||
local bufnr = create_buffer({
|
local bufnr = create_buffer({
|
||||||
'M test.lua',
|
'M test.lua',
|
||||||
|
|
@ -209,7 +208,7 @@ describe('parser', function()
|
||||||
'@@ -1,1 +1,1 @@',
|
'@@ -1,1 +1,1 @@',
|
||||||
' local z = 3',
|
' local z = 3',
|
||||||
})
|
})
|
||||||
local hunks = parser.parse_buffer(bufnr, test_langs, {}, false)
|
local hunks = parser.parse_buffer(bufnr)
|
||||||
|
|
||||||
assert.are.equal(2, #hunks)
|
assert.are.equal(2, #hunks)
|
||||||
assert.are.equal(2, #hunks[1].lines)
|
assert.are.equal(2, #hunks[1].lines)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue