feat: add the entire plugin
This commit is contained in:
parent
7eade50d05
commit
21b8cfb470
7 changed files with 382 additions and 5 deletions
16
README.md
16
README.md
|
|
@ -4,7 +4,9 @@ Treesitter syntax highlighting for vim-fugitive diff views.
|
|||
|
||||
## Problem
|
||||
|
||||
vim-fugitive uses regex-based `syntax/diff.vim` for highlighting expanded diffs in the status buffer. This means code inside diffs has no language-aware highlighting:
|
||||
vim-fugitive uses regex-based `syntax/diff.vim` for highlighting expanded diffs
|
||||
in the status buffer. This means code inside diffs has no language-aware
|
||||
highlighting:
|
||||
|
||||
```
|
||||
Unstaged (1)
|
||||
|
|
@ -17,7 +19,9 @@ M lua/mymodule.lua
|
|||
|
||||
## Solution
|
||||
|
||||
Hook into fugitive's buffer, detect diff hunks, extract the language from filenames, and apply treesitter highlights as extmarks on top of fugitive's existing highlighting.
|
||||
Hook into fugitive's buffer, detect diff hunks, extract the language from
|
||||
filenames, and apply treesitter highlights as extmarks on top of fugitive's
|
||||
existing highlighting.
|
||||
|
||||
```
|
||||
Unstaged (1)
|
||||
|
|
@ -66,6 +70,7 @@ Staged (N)
|
|||
```
|
||||
|
||||
Pattern to detect:
|
||||
|
||||
- Filename: `^[MADRC?] .+%.(%w+)$` → captures extension
|
||||
- Hunk header: `^@@ .+ @@`
|
||||
- Code lines: after hunk header, lines starting with ` `, `+`, or `-`
|
||||
|
|
@ -154,7 +159,8 @@ end
|
|||
|
||||
### 6. Re-highlight on Buffer Change
|
||||
|
||||
Fugitive modifies the buffer when user expands/collapses diffs. Need to re-parse:
|
||||
Fugitive modifies the buffer when user expands/collapses diffs. Need to
|
||||
re-parse:
|
||||
|
||||
```lua
|
||||
vim.api.nvim_create_autocmd({"TextChanged", "TextChangedI"}, {
|
||||
|
|
@ -226,5 +232,5 @@ require("fugitive-ts").setup({
|
|||
|
||||
- [Neovim Treesitter API](https://neovim.io/doc/user/treesitter.html)
|
||||
- [vim-fugitive User events](https://github.com/tpope/vim-fugitive/blob/master/doc/fugitive.txt)
|
||||
- [nvim_buf_set_extmark](https://neovim.io/doc/user/api.html#nvim_buf_set_extmark())
|
||||
- [vim.treesitter.get_string_parser](https://neovim.io/doc/user/treesitter.html#vim.treesitter.get_string_parser())
|
||||
- [nvim_buf_set_extmark](<https://neovim.io/doc/user/api.html#nvim_buf_set_extmark()>)
|
||||
- [vim.treesitter.get_string_parser](<https://neovim.io/doc/user/treesitter.html#vim.treesitter.get_string_parser()>)
|
||||
|
|
|
|||
69
doc/fugitive-ts.nvim.txt
Normal file
69
doc/fugitive-ts.nvim.txt
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
*fugitive-ts.nvim.txt* Treesitter highlighting for vim-fugitive diffs
|
||||
|
||||
Author: Barrett Ruth <br.barrettruth@gmail.com>
|
||||
License: MIT
|
||||
|
||||
==============================================================================
|
||||
INTRODUCTION *fugitive-ts.nvim*
|
||||
|
||||
fugitive-ts.nvim adds treesitter-based syntax highlighting to vim-fugitive
|
||||
diff views. It overlays language-aware highlights on top of fugitive's
|
||||
default regex-based diff highlighting.
|
||||
|
||||
==============================================================================
|
||||
REQUIREMENTS *fugitive-ts-requirements*
|
||||
|
||||
- Neovim 0.9.0+
|
||||
- vim-fugitive
|
||||
- Treesitter parsers for languages you want highlighted
|
||||
|
||||
==============================================================================
|
||||
SETUP *fugitive-ts-setup*
|
||||
|
||||
>lua
|
||||
require('fugitive-ts').setup({
|
||||
-- Enable/disable highlighting (default: true)
|
||||
enabled = true,
|
||||
|
||||
-- Custom filename -> language mappings (optional)
|
||||
languages = {},
|
||||
|
||||
-- Debounce delay in ms (default: 50)
|
||||
debounce_ms = 50,
|
||||
})
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
COMMANDS *fugitive-ts-commands*
|
||||
|
||||
This plugin works automatically when you open a fugitive buffer. No commands
|
||||
are required.
|
||||
|
||||
==============================================================================
|
||||
API *fugitive-ts-api*
|
||||
|
||||
*fugitive-ts.setup()*
|
||||
setup({opts})
|
||||
Configure the plugin. See |fugitive-ts-setup| for options.
|
||||
|
||||
*fugitive-ts.attach()*
|
||||
attach({bufnr})
|
||||
Manually attach highlighting to a buffer. Called automatically for
|
||||
fugitive buffers.
|
||||
|
||||
*fugitive-ts.refresh()*
|
||||
refresh({bufnr})
|
||||
Manually refresh highlighting for a buffer.
|
||||
|
||||
==============================================================================
|
||||
ROADMAP *fugitive-ts-roadmap*
|
||||
|
||||
Planned features and improvements:
|
||||
|
||||
- Vim syntax fallback: For languages without treesitter parsers, fall back
|
||||
to vim's built-in syntax highlighting via scratch buffers. This would
|
||||
provide highlighting coverage for more languages at the cost of
|
||||
implementation complexity.
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
41
lua/fugitive-ts/health.lua
Normal file
41
lua/fugitive-ts/health.lua
Normal 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
|
||||
52
lua/fugitive-ts/highlight.lua
Normal file
52
lua/fugitive-ts/highlight.lua
Normal 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
109
lua/fugitive-ts/init.lua
Normal 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
|
||||
89
lua/fugitive-ts/parser.lua
Normal file
89
lua/fugitive-ts/parser.lua
Normal 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
|
||||
11
plugin/fugitive-ts.lua
Normal file
11
plugin/fugitive-ts.lua
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
if vim.g.loaded_fugitive_ts then
|
||||
return
|
||||
end
|
||||
vim.g.loaded_fugitive_ts = 1
|
||||
|
||||
vim.api.nvim_create_autocmd('FileType', {
|
||||
pattern = 'fugitive',
|
||||
callback = function(args)
|
||||
require('fugitive-ts').attach(args.buf)
|
||||
end,
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue