repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi/activity.lua
Rawlocal publish = require 'bugabinga.pivi.publish'
local TRAIL_NAMESPACE = 'pivi-trail'
local GHOST_NAMESPACE = 'pivi-ghost'
local DIAGNOSTIC_NAMESPACE = 'pivi-diagnostic'
local SIGN_GROUP = 'pivi'
local namespaces = {}
local namespace = function( name )
if not namespaces[name] then
namespaces[name] = vim.api.nvim_create_namespace( name )
end
return namespaces[name]
end
local diagnostic_namespace = function() return namespace( DIAGNOSTIC_NAMESPACE ) end
local trail = {}
local revisions = {}
local groups = {}
local clear_trail
local unwatch = function( bufnr )
revisions[bufnr] = nil
if groups[bufnr] then
pcall( vim.api.nvim_del_augroup_by_id, groups[bufnr] )
groups[bufnr] = nil
end
end
--- Records the current revision as agent-owned.
--- Whatever the edit triggers in the same loop iteration, such as a formatter
--- on write, still belongs to the agent's operation rather than to the user.
local touch = function( bufnr )
if not vim.api.nvim_buf_is_valid( bufnr ) then return end
revisions[bufnr] = vim.api.nvim_buf_get_changedtick( bufnr )
vim.schedule( function()
if revisions[bufnr] and vim.api.nvim_buf_is_valid( bufnr ) then
revisions[bufnr] = vim.api.nvim_buf_get_changedtick( bufnr )
end
end )
end
--- Clears a buffer's trail as soon as the user changes that buffer themselves.
--- Agent edits record their own revision, so only a foreign change clears it.
local watch = function( bufnr )
touch( bufnr )
if groups[bufnr] then return end
groups[bufnr] = vim.api.nvim_create_augroup( 'pivi_trail_' .. tostring( bufnr ), { clear = true, } )
vim.api.nvim_create_autocmd( { 'TextChanged', 'TextChangedI', }, {
group = groups[bufnr],
buffer = bufnr,
desc = 'Clear the pivi edit trail once the user changes the buffer.',
callback = function()
if not vim.api.nvim_buf_is_valid( bufnr ) then
unwatch( bufnr )
return true
end
if vim.api.nvim_buf_get_changedtick( bufnr ) == revisions[bufnr] then return end
clear_trail( bufnr )
return true
end,
} )
end
--- Marks changed lines so the agent's work stays navigable.
--- Marks live in pivi's own namespace and never touch diagnostic or
--- language server namespaces.
local mark_edit = function( bufnr, ranges )
if not vim.api.nvim_buf_is_valid( bufnr ) then return end
local ns = namespace( TRAIL_NAMESPACE )
for _, range in ipairs( ranges or {} ) do
local first = math.max( 0, range.first )
local last = math.max( first + 1, range.last )
local count = vim.api.nvim_buf_line_count( bufnr )
for line = first, math.min( last, count ) - 1 do
pcall( vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
line_hl_group = 'DiffChange',
sign_text = '│',
sign_hl_group = 'DiffChange',
priority = 90,
} )
end
table.insert( trail, { buffer = bufnr, first = first + 1, last = math.min( last, count ), } )
end
watch( bufnr )
publish.activity { kind = 'edit', buffer = bufnr, ranges = ranges, }
end
local marks = function( bufnr )
if not vim.api.nvim_buf_is_valid( bufnr ) then return {} end
return vim.api.nvim_buf_get_extmarks( bufnr, namespace( TRAIL_NAMESPACE ), 0, -1, {} )
end
clear_trail = function( bufnr )
if bufnr and vim.api.nvim_buf_is_valid( bufnr ) then
vim.api.nvim_buf_clear_namespace( bufnr, namespace( TRAIL_NAMESPACE ), 0, -1 )
end
if bufnr then
unwatch( bufnr )
else
for buffer in pairs( groups ) do unwatch( buffer ) end
end
trail = {}
end
local trail_entries = function() return vim.deepcopy( trail ) end
--- Shows streaming edit intent without touching buffer contents.
local ghost = function( bufnr, line, text )
if not vim.api.nvim_buf_is_valid( bufnr ) then return end
if type( text ) ~= 'string' or text == '' then return end
local ns = namespace( GHOST_NAMESPACE )
local count = vim.api.nvim_buf_line_count( bufnr )
local target = math.min( math.max( 0, line ), math.max( 0, count - 1 ) )
vim.api.nvim_buf_clear_namespace( bufnr, ns, 0, -1 )
local chunks = {}
for _, piece in ipairs( vim.split( text, '\n', { plain = true, } ) ) do
table.insert( chunks, { { piece, 'Comment', }, } )
end
pcall( vim.api.nvim_buf_set_extmark, bufnr, ns, target, 0, {
virt_lines = chunks,
virt_lines_above = false,
priority = 100,
} )
end
local ghost_marks = function( bufnr )
if not vim.api.nvim_buf_is_valid( bufnr ) then return {} end
return vim.api.nvim_buf_get_extmarks( bufnr, namespace( GHOST_NAMESPACE ), 0, -1, {} )
end
local ghost_clear = function( bufnr )
if not vim.api.nvim_buf_is_valid( bufnr ) then return end
vim.api.nvim_buf_clear_namespace( bufnr, namespace( GHOST_NAMESPACE ), 0, -1 )
end
local to_quickfix_item = function( item )
return {
filename = item.path,
bufnr = item.path and nil or item.buffer,
lnum = item.line or 1,
col = ( item.column or 0 ) + 1,
text = item.text or '',
type = item.severity == 'error' and 'E' or item.severity == 'warning' and 'W' or 'I',
}
end
--- Cross-file findings use the quickfix list.
local findings = function( items, title )
local entries = vim.tbl_map( to_quickfix_item, items or {} )
vim.fn.setqflist( {}, ' ', { title = title or 'pivi', items = entries, } )
publish.activity { kind = 'findings', count = #entries, }
return entries
end
--- Window associated findings use the location list.
local window_findings = function( win, items, title )
local entries = vim.tbl_map( to_quickfix_item, items or {} )
vim.fn.setloclist( win or 0, {}, ' ', { title = title or 'pivi', items = entries, } )
return entries
end
local severity_of = function( value )
if value == 'error' then return vim.diagnostic.severity.ERROR end
if value == 'warning' then return vim.diagnostic.severity.WARN end
if value == 'hint' then return vim.diagnostic.severity.HINT end
return vim.diagnostic.severity.INFO
end
--- Agent diagnostics stay distinguishable from compiler and language server output.
local diagnose = function( bufnr, items )
if not vim.api.nvim_buf_is_valid( bufnr ) then return end
local diagnostics = {}
for _, item in ipairs( items or {} ) do
table.insert( diagnostics, {
lnum = math.max( 0, ( item.line or 1 ) - 1 ),
col = item.column or 0,
message = item.text or '',
severity = severity_of( item.severity ),
source = 'pi',
} )
end
vim.diagnostic.set( diagnostic_namespace(), bufnr, diagnostics )
end
local diagnostics = function( bufnr )
return vim.diagnostic.get( bufnr, { namespace = diagnostic_namespace(), } )
end
local clear_diagnostics = function( bufnr )
vim.diagnostic.reset( diagnostic_namespace(), bufnr )
end
local clear_all = function( bufnr )
ghost_clear( bufnr )
clear_trail( bufnr )
clear_diagnostics( bufnr )
end
return {
sign_group = SIGN_GROUP,
namespace = namespace,
touch = touch,
trail_namespace = TRAIL_NAMESPACE,
ghost_namespace = GHOST_NAMESPACE,
diagnostic_namespace = diagnostic_namespace,
mark_edit = mark_edit,
marks = marks,
trail = trail_entries,
clear_trail = clear_trail,
ghost = ghost,
ghost_marks = ghost_marks,
ghost_clear = ghost_clear,
findings = findings,
window_findings = window_findings,
diagnose = diagnose,
diagnostics = diagnostics,
clear_diagnostics = clear_diagnostics,
clear_all = clear_all,
}