Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/pivi/context.lua

Raw
local 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,
}