repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi/edit.lua
Rawlocal activity = require 'bugabinga.pivi.activity'
local is_file_buffer = function( bufnr )
return vim.api.nvim_buf_is_valid( bufnr )
and vim.bo[bufnr].buftype == ''
and vim.api.nvim_buf_get_name( bufnr ) ~= ''
end
--- Finds the loaded buffer for a path, if any.
local buffer_for = function( path )
local wanted = vim.fs.normalize( vim.fn.fnamemodify( path, ':p' ) )
for _, bufnr in ipairs( vim.api.nvim_list_bufs() ) do
if vim.api.nvim_buf_is_loaded( bufnr ) then
local name = vim.api.nvim_buf_get_name( bufnr )
if name ~= '' and vim.fs.normalize( name ) == wanted then return bufnr end
end
end
return nil
end
--- Reads authoritative content: the loaded buffer wins over disk.
local read = function( path )
local bufnr = buffer_for( path )
if bufnr then
return table.concat( vim.api.nvim_buf_get_lines( bufnr, 0, -1, false ), '\n' ), 'buffer'
end
local ok, lines = pcall( vim.fn.readfile, path )
if not ok then return nil, 'missing' end
return table.concat( lines, '\n' ), 'disk'
end
--- Writes a loaded buffer to disk so later filesystem readers observe agent edits.
--- pivi starts no filesystem watcher; visibility is part of the edit itself.
local persist = function( bufnr )
if not is_file_buffer( bufnr ) then return false end
if not vim.bo[bufnr].modified then return true end
local ok = pcall( vim.api.nvim_buf_call, bufnr, function()
vim.cmd 'silent! noautocmd write'
end )
-- Writing stays part of the agent's operation, so the trail survives it.
activity.touch( bufnr )
return ok and not vim.bo[bufnr].modified
end
--- Applies line replacements as one coherent undoable change.
--- Replacements are zero based, end exclusive, and are applied bottom up.
--- When Neovim refuses to join undo blocks the edit still lands.
--- @return boolean applied
--- @return boolean joined whether the change stayed a single undo block
local apply = function( bufnr, replacements )
vim.validate( 'bufnr', bufnr, 'number' )
vim.validate( 'replacements', replacements, 'table' )
if not vim.api.nvim_buf_is_valid( bufnr ) then return false, false end
local ordered = vim.deepcopy( replacements )
table.sort( ordered, function( left, right ) return left.first > right.first end )
local joined = true
local touched = {}
for index, replacement in ipairs( ordered ) do
if index > 1 then
local ok = pcall( vim.api.nvim_buf_call, bufnr, function() vim.cmd.undojoin() end )
if not ok then joined = false end
end
vim.api.nvim_buf_set_lines(
bufnr,
replacement.first,
replacement.last,
false,
replacement.lines
)
table.insert( touched, {
first = replacement.first,
last = replacement.first + #replacement.lines,
} )
end
activity.mark_edit( bufnr, touched )
return true, joined
end
--- Brings a buffer to the given content by rewriting only what differs.
--- A whole-buffer replacement would mark every line as agent work and destroy
--- marks and folds for an edit that touched one line.
local set_content = function( bufnr, content )
vim.validate( 'content', content, 'string' )
if not vim.api.nvim_buf_is_valid( bufnr ) then return false, false end
local target = vim.split( content, '\n', { plain = true, } )
local current = table.concat( vim.api.nvim_buf_get_lines( bufnr, 0, -1, false ), '\n' )
if current == content then return true, true end
local whole = { { first = 0, last = -1, lines = target, }, }
local ok, hunks = pcall( vim.text.diff, current .. '\n', content .. '\n', { result_type = 'indices', } )
if not ok or type( hunks ) ~= 'table' or #hunks == 0 then
return apply( bufnr, whole )
end
local replacements = {}
for _, hunk in ipairs( hunks ) do
local start_a, count_a, start_b, count_b = hunk[1], hunk[2], hunk[3], hunk[4]
local first = count_a > 0 and start_a - 1 or start_a
local lines = {}
for index = start_b, start_b + count_b - 1 do
table.insert( lines, target[index] )
end
table.insert( replacements, { first = first, last = first + count_a, lines = lines, } )
end
return apply( bufnr, replacements )
end
local find_text = function( content, needle )
local first = content:find( needle, 1, true )
if not first then return nil end
local second = content:find( needle, first + 1, true )
if second then return nil, 'text is not unique' end
return first
end
--- Replaces one unique text occurrence in a loaded buffer.
--- Only the lines the replacement actually spans are rewritten, so undo,
--- marks, folds, and the activity trail stay proportional to the change.
local replace = function( bufnr, old_text, new_text )
vim.validate( 'old_text', old_text, 'string' )
vim.validate( 'new_text', new_text, 'string' )
if not vim.api.nvim_buf_is_valid( bufnr ) then return false, 'invalid buffer' end
local lines = vim.api.nvim_buf_get_lines( bufnr, 0, -1, false )
local content = table.concat( lines, '\n' )
local found, failure = find_text( content, old_text )
if not found then return false, failure or 'text not found' end
local first = #vim.split( content:sub( 1, found - 1 ), '\n', { plain = true, } )
local last = first + #vim.split( old_text, '\n', { plain = true, } ) - 1
local region = table.concat( vim.list_slice( lines, first, last ), '\n' )
local offset = region:find( old_text, 1, true )
if not offset then return false, 'text not found' end
local replaced = region:sub( 1, offset - 1 ) .. new_text .. region:sub( offset + #old_text )
return apply( bufnr, { {
first = first - 1,
last = last,
lines = vim.split( replaced, '\n', { plain = true, } ),
}, } )
end
return {
buffer_for = buffer_for,
is_file_buffer = is_file_buffer,
read = read,
persist = persist,
apply = apply,
set_content = set_content,
replace = replace,
}