feat(neojj): add neojj (jujutsu) integration

Problem: diffs.nvim has no support for neojj, the neogit-like TUI for
jujutsu VCS. Users switching to jj get no syntax highlighting in neojj
status/diff buffers.

Solution: Mirror the existing neogit integration pattern for neojj.
Register `NeojjStatus`, `NeojjCommitView`, `NeojjDiffView` filetypes,
set `vim.b.neojj_disable_hunk_highlight` on attach, listen for
`User NeojjDiffLoaded` events, and detect jj repo root via
`neojj.lib.jj.repo.worktree_root`. Add parser patterns for neojj's
`added`, `updated`, `changed`, and `unmerged` filename labels.
This commit is contained in:
Barrett Ruth 2026-03-11 14:01:28 -04:00
parent de04381298
commit 5da3929480
Signed by: barrett
GPG key ID: A6C96C9349D2FC81
5 changed files with 265 additions and 6 deletions

View file

@ -9,7 +9,10 @@ highlighting driven by treesitter.
## Features
- Treesitter syntax highlighting in vim-fugitive, Neogit, and `diff` filetype
- Treesitter syntax highlighting in
[vim-fugitive](https://github.com/tpope/vim-fugitive),
[Neogit](https://github.com/NeogitOrg/neogit), builtin `diff` filetype, and
more!
- Character-level intra-line diff highlighting (with optional
[vscode-diff](https://github.com/esmuellert/codediff.nvim) FFI backend for
word-level accuracy)
@ -58,15 +61,18 @@ luarocks install diffs.nvim
Do not lazy load `diffs.nvim` with `event`, `lazy`, `ft`, `config`, or `keys` to
control loading - `diffs.nvim` lazy-loads itself.
**Q: Does diffs.nvim support vim-fugitive/Neogit/gitsigns?**
**Q: Does diffs.nvim support vim-fugitive/Neogit/neojj/gitsigns?**
Yes. Enable integrations in your config:
```lua
vim.g.diffs = {
fugitive = true,
neogit = true,
gitsigns = true,
integrations = {
fugitive = true,
neogit = true,
neojj = true,
gitsigns = true,
}
}
```

View file

@ -40,6 +40,7 @@ CONTENTS *diffs-contents*
7. Integrations ..................................... |diffs-integrations|
Fugitive .......................................... |diffs-fugitive|
Neogit .............................................. |diffs-neogit|
Neojj ............................................... |diffs-neojj|
Gitsigns .......................................... |diffs-gitsigns|
Telescope ........................................ |diffs-telescope|
8. Conflict Resolution .................................... |diffs-conflict|
@ -84,6 +85,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
integrations = {
fugitive = false,
neogit = false,
neojj = false,
gitsigns = false,
committia = false,
telescope = false,
@ -180,6 +182,14 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
integrations = { neogit = true }
<
{neojj} (boolean|table, default: false)
Enable neojj integration. When active,
`NeojjStatus`, `NeojjCommitView`, and
`NeojjDiffView` filetypes are registered.
See |diffs-neojj|. >lua
integrations = { neojj = true }
<
{gitsigns} (boolean|table, default: false)
Enable gitsigns.nvim blame popup highlighting.
See |diffs-gitsigns|. >lua
@ -536,6 +546,7 @@ each integration's filetypes and attaches automatically.
integrations = {
fugitive = true,
neogit = true,
neojj = true,
gitsigns = true,
},
}
@ -624,6 +635,17 @@ Expanding a diff in a Neogit buffer (e.g., TAB on a file in the status
view) applies treesitter syntax highlighting and intra-line diffs to the
hunk lines.
------------------------------------------------------------------------------
NEOJJ *diffs-neojj*
Enable neojj (https://github.com/NicholasZolton/neojj) support: >lua
vim.g.diffs = { integrations = { neojj = true } }
<
Expanding a diff in a neojj buffer (e.g., TAB on a file in the status
view) applies treesitter syntax highlighting and intra-line diffs to the
hunk lines.
------------------------------------------------------------------------------
GITSIGNS *diffs-gitsigns*

View file

@ -39,6 +39,8 @@
---@class diffs.NeogitConfig
---@class diffs.NeojjConfig
---@class diffs.GitsignsConfig
---@class diffs.CommittiaConfig
@ -65,6 +67,7 @@
---@class diffs.IntegrationsConfig
---@field fugitive diffs.FugitiveConfig|false
---@field neogit diffs.NeogitConfig|false
---@field neojj diffs.NeojjConfig|false
---@field gitsigns diffs.GitsignsConfig|false
---@field committia diffs.CommittiaConfig|false
---@field telescope diffs.TelescopeConfig|false
@ -77,6 +80,7 @@
---@field integrations diffs.IntegrationsConfig
---@field fugitive? diffs.FugitiveConfig|false deprecated: use integrations.fugitive
---@field neogit? diffs.NeogitConfig|false deprecated: use integrations.neogit
---@field neojj? diffs.NeojjConfig|false deprecated: use integrations.neojj
---@field gitsigns? diffs.GitsignsConfig|false deprecated: use integrations.gitsigns
---@field committia? diffs.CommittiaConfig|false deprecated: use integrations.committia
---@field telescope? diffs.TelescopeConfig|false deprecated: use integrations.telescope
@ -161,6 +165,7 @@ local default_config = {
integrations = {
fugitive = false,
neogit = false,
neojj = false,
gitsigns = false,
committia = false,
telescope = false,
@ -239,6 +244,15 @@ function M.compute_filetypes(opts)
table.insert(fts, 'NeogitCommitView')
table.insert(fts, 'NeogitDiffView')
end
local njj = intg.neojj
if njj == nil then
njj = opts.neojj
end
if njj == true or type(njj) == 'table' then
table.insert(fts, 'NeojjStatus')
table.insert(fts, 'NeojjCommitView')
table.insert(fts, 'NeojjDiffView')
end
if type(opts.extra_filetypes) == 'table' then
for _, ft in ipairs(opts.extra_filetypes) do
table.insert(fts, ft)
@ -610,7 +624,7 @@ local function compute_highlight_groups(is_default)
end
end
local integration_keys = { 'fugitive', 'neogit', 'gitsigns', 'committia', 'telescope' }
local integration_keys = { 'fugitive', 'neogit', 'neojj', 'gitsigns', 'committia', 'telescope' }
local function migrate_integrations(opts)
if opts.integrations then
@ -674,6 +688,10 @@ local function init()
intg.neogit = {}
end
if intg.neojj == true then
intg.neojj = {}
end
if intg.gitsigns == true then
intg.gitsigns = {}
end
@ -1032,6 +1050,21 @@ function M.attach(bufnr)
})
end
local neojj_augroup = nil
if config.integrations.neojj and vim.bo[bufnr].filetype:match('^Neojj') then
vim.b[bufnr].neojj_disable_hunk_highlight = true
neojj_augroup = vim.api.nvim_create_augroup('diffs_neojj_' .. bufnr, { clear = true })
vim.api.nvim_create_autocmd('User', {
pattern = 'NeojjDiffLoaded',
group = neojj_augroup,
callback = function()
if vim.api.nvim_buf_is_valid(bufnr) and attached_buffers[bufnr] then
M.refresh(bufnr)
end
end,
})
end
dbg('attaching to buffer %d', bufnr)
ensure_cache(bufnr)
@ -1045,6 +1078,9 @@ function M.attach(bufnr)
if neogit_augroup then
pcall(vim.api.nvim_del_augroup_by_id, neogit_augroup)
end
if neojj_augroup then
pcall(vim.api.nvim_del_augroup_by_id, neojj_augroup)
end
end,
})
end
@ -1101,6 +1137,12 @@ function M.get_fugitive_config()
return config.integrations.fugitive
end
---@return diffs.NeojjConfig|false
function M.get_neojj_config()
init()
return config.integrations.neojj
end
---@return diffs.CommittiaConfig|false
function M.get_committia_config()
init()

View file

@ -129,6 +129,18 @@ local function get_repo_root(bufnr)
return vim.fn.fnamemodify(neogit_git_dir, ':h')
end
if vim.bo[bufnr].filetype:match('^Neojj') then
local jj_ok, jj_mod = pcall(require, 'neojj.lib.jj')
if jj_ok then
local rok, repo = pcall(function()
return jj_mod.repo
end)
if rok and repo and repo.worktree_root then
return repo.worktree_root
end
end
end
local cwd = vim.fn.getcwd()
local git = require('diffs.git')
return git.get_repo_root(cwd .. '/.')
@ -244,6 +256,10 @@ function M.parse_buffer(bufnr)
or (not logical:match('^deleted file mode') and logical:match('^deleted%s+(.+)$'))
or logical:match('^renamed%s+(.+)$')
or logical:match('^copied%s+(.+)$')
or logical:match('^added%s+(.+)$')
or logical:match('^updated%s+(.+)$')
or logical:match('^changed%s+(.+)$')
or logical:match('^unmerged%s+(.+)$')
local bare_file = not hunk_start and logical:match('^([^%s]+%.[^%s]+)$')
local filename = logical:match('^[MADRCU%?!]%s+(.+)$')
or diff_git_file

View file

@ -0,0 +1,173 @@
require('spec.helpers')
vim.g.diffs = { integrations = { neojj = true } }
local diffs = require('diffs')
local parser = require('diffs.parser')
local function create_buffer(lines)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
return bufnr
end
local function delete_buffer(bufnr)
if vim.api.nvim_buf_is_valid(bufnr) then
vim.api.nvim_buf_delete(bufnr, { force = true })
end
end
describe('neojj_integration', function()
describe('neojj_disable_hunk_highlight', function()
it('sets neojj_disable_hunk_highlight on NeojjStatus buffer after attach', function()
local bufnr = create_buffer({
'modified test.lua',
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
vim.api.nvim_set_option_value('filetype', 'NeojjStatus', { buf = bufnr })
diffs.attach(bufnr)
assert.is_true(vim.b[bufnr].neojj_disable_hunk_highlight)
delete_buffer(bufnr)
end)
it('does not set neojj_disable_hunk_highlight on non-Neojj buffer', function()
local bufnr = create_buffer({})
vim.api.nvim_set_option_value('filetype', 'git', { buf = bufnr })
diffs.attach(bufnr)
assert.is_not_true(vim.b[bufnr].neojj_disable_hunk_highlight)
delete_buffer(bufnr)
end)
end)
describe('NeojjStatus buffer attach', function()
it('populates hunk_cache for NeojjStatus buffer with diff content', function()
local bufnr = create_buffer({
'modified hello.lua',
'@@ -1,2 +1,3 @@',
' local M = {}',
'+local x = 1',
' return M',
})
vim.api.nvim_set_option_value('filetype', 'NeojjStatus', { buf = bufnr })
diffs.attach(bufnr)
local entry = diffs._test.hunk_cache[bufnr]
assert.is_not_nil(entry)
assert.is_table(entry.hunks)
assert.are.equal(1, #entry.hunks)
assert.are.equal('hello.lua', entry.hunks[1].filename)
delete_buffer(bufnr)
end)
it('populates hunk_cache for NeojjDiffView buffer', function()
local bufnr = create_buffer({
'new file newmod.lua',
'@@ -0,0 +1,2 @@',
'+local M = {}',
'+return M',
})
vim.api.nvim_set_option_value('filetype', 'NeojjDiffView', { buf = bufnr })
diffs.attach(bufnr)
local entry = diffs._test.hunk_cache[bufnr]
assert.is_not_nil(entry)
assert.is_table(entry.hunks)
assert.are.equal(1, #entry.hunks)
delete_buffer(bufnr)
end)
end)
describe('parser neojj patterns', function()
it('detects added prefix via parser', function()
local bufnr = create_buffer({
'added utils.py',
'@@ -0,0 +1,2 @@',
'+def hello():',
'+ pass',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal('utils.py', hunks[1].filename)
delete_buffer(bufnr)
end)
it('detects updated prefix via parser', function()
local bufnr = create_buffer({
'updated config.toml',
'@@ -1,2 +1,3 @@',
' [section]',
'+key = "val"',
' other = 1',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal('config.toml', hunks[1].filename)
delete_buffer(bufnr)
end)
it('detects changed prefix via parser', function()
local bufnr = create_buffer({
'changed main.rs',
'@@ -1,1 +1,2 @@',
' fn main() {}',
'+fn helper() {}',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal('main.rs', hunks[1].filename)
delete_buffer(bufnr)
end)
it('detects unmerged prefix via parser', function()
local bufnr = create_buffer({
'unmerged conflict.lua',
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal('conflict.lua', hunks[1].filename)
delete_buffer(bufnr)
end)
it('parses multi-file neojj buffer with modified and added', function()
local bufnr = create_buffer({
'modified test.lua',
'@@ -1,2 +1,3 @@',
' local M = {}',
'+local x = 1',
' return M',
'added utils.py',
'@@ -0,0 +1,2 @@',
'+def hello():',
'+ pass',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(2, #hunks)
assert.are.equal('test.lua', hunks[1].filename)
assert.are.equal('utils.py', hunks[2].filename)
delete_buffer(bufnr)
end)
end)
describe('compute_filetypes', function()
it('includes Neojj filetypes when neojj integration is enabled', function()
local fts = diffs.compute_filetypes({ integrations = { neojj = true } })
assert.is_true(vim.tbl_contains(fts, 'NeojjStatus'))
assert.is_true(vim.tbl_contains(fts, 'NeojjCommitView'))
assert.is_true(vim.tbl_contains(fts, 'NeojjDiffView'))
end)
it('excludes Neojj filetypes when neojj integration is disabled', function()
local fts = diffs.compute_filetypes({ integrations = { neojj = false } })
assert.is_false(vim.tbl_contains(fts, 'NeojjStatus'))
assert.is_false(vim.tbl_contains(fts, 'NeojjCommitView'))
assert.is_false(vim.tbl_contains(fts, 'NeojjDiffView'))
end)
end)
end)