feat: add the entire plugin

This commit is contained in:
Barrett Ruth 2026-02-01 17:38:32 -05:00
parent 7eade50d05
commit 21b8cfb470
7 changed files with 382 additions and 5 deletions

View file

@ -0,0 +1,41 @@
local M = {}
function M.check()
vim.health.start('fugitive-ts.nvim')
if vim.fn.has('nvim-0.9.0') == 1 then
vim.health.ok('Neovim 0.9.0+ detected')
else
vim.health.error('fugitive-ts.nvim requires Neovim 0.9.0+')
end
local fugitive_loaded = vim.fn.exists(':Git') == 2
if fugitive_loaded then
vim.health.ok('vim-fugitive detected')
else
vim.health.warn('vim-fugitive not detected (required for this plugin to be useful)')
end
local common_langs = { 'lua', 'python', 'javascript', 'typescript', 'rust', 'go', 'c', 'cpp' }
local available = {}
local missing = {}
for _, lang in ipairs(common_langs) do
local ok = pcall(vim.treesitter.language.inspect, lang)
if ok then
table.insert(available, lang)
else
table.insert(missing, lang)
end
end
if #available > 0 then
vim.health.ok('Treesitter parsers available: ' .. table.concat(available, ', '))
end
if #missing > 0 then
vim.health.info('Treesitter parsers not installed: ' .. table.concat(missing, ', '))
end
end
return M

View file

@ -0,0 +1,52 @@
local M = {}
function M.highlight_hunk(bufnr, ns, hunk)
local lang = hunk.lang
if not lang then
return
end
local code_lines = {}
for _, line in ipairs(hunk.lines) do
table.insert(code_lines, line:sub(2))
end
local code = table.concat(code_lines, '\n')
if code == '' then
return
end
local ok, parser_obj = pcall(vim.treesitter.get_string_parser, code, lang)
if not ok or not parser_obj then
return
end
local trees = parser_obj:parse()
if not trees or #trees == 0 then
return
end
local query = vim.treesitter.query.get(lang, 'highlights')
if not query then
return
end
for id, node, _ in query:iter_captures(trees[1]:root(), code) do
local capture_name = '@' .. query.captures[id]
local sr, sc, er, ec = node:range()
local buf_sr = hunk.start_line + sr
local buf_er = hunk.start_line + er
local buf_sc = sc + 1
local buf_ec = ec + 1
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
end_row = buf_er,
end_col = buf_ec,
hl_group = capture_name,
priority = 200,
})
end
end
return M

109
lua/fugitive-ts/init.lua Normal file
View file

@ -0,0 +1,109 @@
---@class fugitive-ts.Config
---@field enabled boolean
---@field languages table<string, string>
---@field debounce_ms integer
---@class fugitive-ts
---@field attach fun(bufnr?: integer)
---@field refresh fun(bufnr?: integer)
---@field setup fun(opts?: fugitive-ts.Config)
local M = {}
local highlight = require('fugitive-ts.highlight')
local parser = require('fugitive-ts.parser')
local ns = vim.api.nvim_create_namespace('fugitive_ts')
---@type fugitive-ts.Config
local default_config = {
enabled = true,
languages = {},
debounce_ms = 50,
}
---@type fugitive-ts.Config
local config = vim.deepcopy(default_config)
---@type table<integer, boolean>
local attached_buffers = {}
---@param bufnr integer
local function highlight_buffer(bufnr)
if not config.enabled then
return
end
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
local hunks = parser.parse_buffer(bufnr, config.languages)
for _, hunk in ipairs(hunks) do
highlight.highlight_hunk(bufnr, ns, hunk)
end
end
---@param bufnr integer
---@return fun()
local function create_debounced_highlight(bufnr)
---@type uv_timer_t?
local timer = nil
return function()
if timer then
timer:stop()
timer:close()
end
timer = vim.uv.new_timer()
timer:start(
config.debounce_ms,
0,
vim.schedule_wrap(function()
timer:close()
timer = nil
highlight_buffer(bufnr)
end)
)
end
end
---@param bufnr? integer
function M.attach(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
if attached_buffers[bufnr] then
return
end
attached_buffers[bufnr] = true
local debounced = create_debounced_highlight(bufnr)
highlight_buffer(bufnr)
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
buffer = bufnr,
callback = debounced,
})
vim.api.nvim_create_autocmd('BufWipeout', {
buffer = bufnr,
callback = function()
attached_buffers[bufnr] = nil
end,
})
end
---@param bufnr? integer
function M.refresh(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
highlight_buffer(bufnr)
end
---@param opts? fugitive-ts.Config
function M.setup(opts)
opts = opts or {}
config = vim.tbl_deep_extend('force', default_config, opts)
end
return M

View file

@ -0,0 +1,89 @@
---@class fugitive-ts.Hunk
---@field filename string
---@field lang string
---@field start_line integer
---@field lines string[]
local M = {}
---@param filename string
---@param custom_langs? table<string, string>
---@return string?
local function get_lang_from_filename(filename, custom_langs)
if custom_langs and custom_langs[filename] then
return custom_langs[filename]
end
local ft = vim.filetype.match({ filename = filename })
if not ft then
return nil
end
local lang = vim.treesitter.language.get_lang(ft)
if lang then
local ok = pcall(vim.treesitter.language.inspect, lang)
if ok then
return lang
end
end
return nil
end
---@param bufnr integer
---@param custom_langs? table<string, string>
---@return fugitive-ts.Hunk[]
function M.parse_buffer(bufnr, custom_langs)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
---@type fugitive-ts.Hunk[]
local hunks = {}
---@type string?
local current_filename = nil
---@type string?
local current_lang = nil
---@type integer?
local hunk_start = nil
---@type string[]
local hunk_lines = {}
local function flush_hunk()
if hunk_start and #hunk_lines > 0 and current_lang then
table.insert(hunks, {
filename = current_filename,
lang = current_lang,
start_line = hunk_start,
lines = hunk_lines,
})
end
hunk_start = nil
hunk_lines = {}
end
for i, line in ipairs(lines) do
local filename = line:match('^[MADRC%?!]%s+(.+)$')
if filename then
flush_hunk()
current_filename = filename
current_lang = get_lang_from_filename(filename, custom_langs)
elseif line:match('^@@.-@@') then
flush_hunk()
hunk_start = i
elseif hunk_start then
local prefix = line:sub(1, 1)
if prefix == ' ' or prefix == '+' or prefix == '-' then
table.insert(hunk_lines, line)
elseif line == '' or line:match('^[MADRC%?!]%s+') or line:match('^%a') then
flush_hunk()
current_filename = nil
current_lang = nil
end
end
end
flush_hunk()
return hunks
end
return M