repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi/context.lua
Rawlocal SURROUNDING_LINES = 40
local VISIBLE_LINES = 120
local MESSAGE_LINES = 20
local freeze = function( value )
if type( value ) ~= 'table' then return value end
return setmetatable( {}, {
__index = value,
__newindex = function() error 'pivi context snapshots are immutable' end,
__len = function() return #value end,
__pairs = function() return pairs( value ) end,
} )
end
local function deep_freeze( value )
if type( value ) ~= 'table' then return value end
local frozen = {}
for key, item in pairs( value ) do
frozen[key] = deep_freeze( item )
end
return freeze( frozen )
end
--- Resolves the current or last visual selection into a line and column range.
local selection_range = function()
local mode = vim.api.nvim_get_mode().mode
local visual = mode:sub( 1, 1 ) == 'v' or mode:sub( 1, 1 ) == 'V' or mode:sub( 1, 1 ) == '\22'
local start_position = vim.fn.getpos( visual and 'v' or "'<" )
local end_position = vim.fn.getpos( visual and '.' or "'>" )
if start_position[2] == 0 or end_position[2] == 0 then return nil end
local first = { start_position[2], start_position[3] - 1, }
local last = { end_position[2], end_position[3] - 1, }
if first[1] > last[1] or ( first[1] == last[1] and first[2] > last[2] ) then
first, last = last, first
end
return {
kind = visual and mode or 'mark',
first = first,
last = last,
}
end
local text_between = function( bufnr, range )
local lines = vim.api.nvim_buf_get_lines( bufnr, range.first[1] - 1, range.last[1], false )
return table.concat( lines, '\n' )
end
--- Captures an immutable snapshot of the submission's originating editor state.
--- @param source table? bufnr and window override
local snapshot = function( source )
source = source or {}
local bufnr = source.bufnr or vim.api.nvim_get_current_buf()
if not vim.api.nvim_buf_is_valid( bufnr ) then return nil end
if vim.bo[bufnr].buftype ~= '' and vim.bo[bufnr].buftype ~= 'acwrite' then return nil end
local win = source.win or vim.api.nvim_get_current_win()
local cursor = { 1, 0, }
if vim.api.nvim_win_is_valid( win ) and vim.api.nvim_win_get_buf( win ) == bufnr then
cursor = vim.api.nvim_win_get_cursor( win )
end
local total = vim.api.nvim_buf_line_count( bufnr )
local first = math.max( 1, cursor[1] - math.floor( SURROUNDING_LINES / 2 ) )
local last = math.min( total, first + SURROUNDING_LINES - 1 )
local surrounding = table.concat( vim.api.nvim_buf_get_lines( bufnr, first - 1, last, false ), '\n' )
local range = source.range
local selected
if range then selected = text_between( bufnr, range ) end
return deep_freeze {
buffer = bufnr,
path = vim.api.nvim_buf_get_name( bufnr ),
filetype = vim.bo[bufnr].filetype,
revision = vim.api.nvim_buf_get_changedtick( bufnr ),
modified = vim.bo[bufnr].modified,
line_count = total,
cursor = { cursor[1], cursor[2], },
range = range,
selection_kind = range and range.kind or nil,
selected_text = selected,
surrounding = surrounding,
surrounding_first_line = first,
surrounding_last_line = last,
}
end
--- Renders a snapshot as a stable prompt preamble.
local render = function( shot )
if not shot then return '' end
local lines = { '<editor-context>', }
table.insert( lines, 'path: ' .. ( shot.path ~= '' and shot.path or '[No Name]' ) )
table.insert( lines, 'revision: ' .. tostring( shot.revision ) )
table.insert( lines, 'cursor: ' .. tostring( shot.cursor[1] ) .. ':' .. tostring( shot.cursor[2] ) )
if shot.modified then table.insert( lines, 'unsaved: true' ) end
if shot.selected_text then
table.insert( lines, 'selection (' .. tostring( shot.selection_kind ) .. '):' )
table.insert( lines, shot.selected_text )
else
table.insert( lines, 'around cursor (lines ' ..
tostring( shot.surrounding_first_line ) .. '-' .. tostring( shot.surrounding_last_line ) .. '):' )
table.insert( lines, shot.surrounding )
end
table.insert( lines, '</editor-context>' )
table.insert( lines, '' )
return table.concat( lines, '\n' )
end
--- Ambient core editor state, fetched on demand rather than attached to every prompt.
local editor = function()
local buffers = {}
for _, bufnr in ipairs( vim.api.nvim_list_bufs() ) do
if vim.api.nvim_buf_is_loaded( bufnr ) then
table.insert( buffers, {
buffer = bufnr,
path = vim.api.nvim_buf_get_name( bufnr ),
filetype = vim.bo[bufnr].filetype,
modified = vim.bo[bufnr].modified,
line_count = vim.api.nvim_buf_line_count( bufnr ),
revision = vim.api.nvim_buf_get_changedtick( bufnr ),
} )
end
end
local windows = {}
for _, win in ipairs( vim.api.nvim_list_wins() ) do
table.insert( windows, {
window = win,
buffer = vim.api.nvim_win_get_buf( win ),
cursor = vim.api.nvim_win_get_cursor( win ),
tabpage = vim.api.nvim_win_get_tabpage( win ),
} )
end
return {
cwd = vim.uv.cwd(),
mode = vim.api.nvim_get_mode().mode,
current_buffer = vim.api.nvim_get_current_buf(),
current_window = vim.api.nvim_get_current_win(),
buffers = buffers,
windows = windows,
}
end
local messages = function()
local ok, text = pcall( vim.api.nvim_exec2, 'messages', { output = true, } )
if not ok or type( text.output ) ~= 'string' then return {} end
local lines = vim.split( text.output, '\n', { plain = true, } )
local first = math.max( 1, #lines - MESSAGE_LINES + 1 )
return vim.list_slice( lines, first, #lines )
end
local window_entry = function( win, current )
local bufnr = vim.api.nvim_win_get_buf( win )
local name = vim.api.nvim_buf_get_name( bufnr )
return {
window = win,
buffer = bufnr,
path = name ~= '' and name or nil,
filetype = vim.bo[bufnr].filetype,
current = win == current,
}
end
--- Captures what the user is actually looking at right now.
--- Only the visible region of the current window is read, never whole buffers,
--- so an ephemeral question stays small.
local visible = function()
local win = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf( win )
local view = vim.api.nvim_win_call( win, function()
return { vim.fn.line 'w0', vim.fn.line 'w$', }
end )
local count = vim.api.nvim_buf_line_count( bufnr )
local first, last = view[1], view[2]
first = math.max( 1, first or 1 )
last = math.min( count, math.max( first, last or first ) )
if last - first + 1 > VISIBLE_LINES then last = first + VISIBLE_LINES - 1 end
local windows = {}
for _, other in ipairs( vim.api.nvim_tabpage_list_wins( 0 ) ) do
table.insert( windows, window_entry( other, win ) )
end
local range = selection_range()
local selected
if range and vim.api.nvim_buf_is_valid( bufnr ) then
local ok, text = pcall( text_between, bufnr, range )
if ok then selected = text end
end
local name = vim.api.nvim_buf_get_name( bufnr )
return {
cwd = vim.uv.cwd(),
mode = vim.api.nvim_get_mode().mode,
buffer = bufnr,
path = name ~= '' and name or nil,
filetype = vim.bo[bufnr].filetype,
modified = vim.bo[bufnr].modified,
cursor = vim.api.nvim_win_get_cursor( win ),
first_line = first,
last_line = last,
line_count = count,
lines = vim.api.nvim_buf_get_lines( bufnr, first - 1, last, false ),
selection_kind = range and range.kind or nil,
selected_text = selected,
windows = windows,
tabpages = #vim.api.nvim_list_tabpages(),
messages = messages(),
error = vim.v.errmsg ~= '' and vim.v.errmsg or nil,
}
end
--- Renders the visible state as a bounded preamble for an ephemeral question.
local render_visible = function( state )
if not state then return '' end
local lines = { '<editor-view>', }
table.insert( lines, 'cwd: ' .. tostring( state.cwd ) )
table.insert( lines, 'mode: ' .. tostring( state.mode ) )
table.insert( lines, 'buffer: ' .. ( state.path or '[No Name]' ) ..
' (' .. ( state.filetype ~= '' and state.filetype or 'no filetype' ) .. ')' ..
( state.modified and ' unsaved' or '' ) )
table.insert( lines, 'cursor: ' .. tostring( state.cursor[1] ) .. ':' .. tostring( state.cursor[2] ) )
if #state.windows > 1 or state.tabpages > 1 then
local described = {}
for _, entry in ipairs( state.windows ) do
local label = entry.path and vim.fs.basename( entry.path ) or ( entry.filetype ~= '' and entry.filetype or 'scratch' )
table.insert( described, entry.current and ( '[' .. label .. ']' ) or label )
end
table.insert( lines, 'windows: ' .. table.concat( described, ' ' ) ..
( state.tabpages > 1 and ( ' tabs: ' .. tostring( state.tabpages ) ) or '' ) )
end
if state.selected_text then
table.insert( lines, 'selection (' .. tostring( state.selection_kind ) .. '):' )
table.insert( lines, state.selected_text )
end
table.insert( lines, 'on screen (lines ' .. tostring( state.first_line ) ..
'-' .. tostring( state.last_line ) .. ' of ' .. tostring( state.line_count ) .. '):' )
for offset, line in ipairs( state.lines ) do
table.insert( lines, string.format( '%d\t%s', state.first_line + offset - 1, line ) )
end
if state.error then table.insert( lines, 'last error: ' .. state.error ) end
local recent = {}
for _, message in ipairs( state.messages ) do
if message ~= '' then table.insert( recent, message ) end
end
if #recent > 0 then
table.insert( lines, 'recent messages:' )
for _, message in ipairs( recent ) do
table.insert( lines, ' ' .. message )
end
end
table.insert( lines, '</editor-view>' )
table.insert( lines, '' )
return table.concat( lines, '\n' )
end
return {
visible_lines = VISIBLE_LINES,
snapshot = snapshot,
render = render,
visible = visible,
render_visible = render_visible,
editor = editor,
selection_range = selection_range,
}