@@ -1179,6 +1179,16 @@ function M.format_message(prompt, opts)
11791179
11801180 local parts = { { type = ' text' , text = prompt } }
11811181
1182+ -- recent_buffers synthetic context
1183+ if config .context and config .context .recent_buffers and config .context .recent_buffers .enabled then
1184+ local ok , recent = pcall (M .get_recent_buffers , prompt , config .context .recent_buffers )
1185+ if ok and recent and # recent > 0 then
1186+ for _ , rb in ipairs (recent ) do
1187+ table.insert (parts , rb )
1188+ end
1189+ end
1190+ end
1191+
11821192 for _ , path in ipairs (context .mentioned_files or {}) do
11831193 table.insert (parts , format_file_part (path , prompt ))
11841194 end
@@ -1368,4 +1378,162 @@ function M.extract_legacy_tag(tag, text)
13681378 return nil
13691379end
13701380
1381+ --- @param buf number
1382+ --- @return boolean
1383+ local function is_valid_buffer (buf )
1384+ if not vim .api .nvim_buf_is_valid (buf ) then
1385+ return false
1386+ end
1387+ if vim .bo [buf ].buftype ~= ' ' then
1388+ return false
1389+ end
1390+ if not vim .bo [buf ].modifiable then
1391+ return false
1392+ end
1393+ return true
1394+ end
1395+
1396+ --- @param client table
1397+ local function client_supports_symbols (client )
1398+ if not client or not client .server_capabilities then
1399+ return false
1400+ end
1401+ local caps = client .server_capabilities
1402+ return caps .documentSymbolProvider == true or (type (caps .documentSymbolProvider ) == ' table' )
1403+ end
1404+
1405+ --- @param bufnr number
1406+ --- @return table[] | nil
1407+ local function fetch_document_symbols (bufnr )
1408+ local params = { textDocument = vim .lsp .util .make_text_document_params (bufnr ) }
1409+ local results = {}
1410+ local clients = vim .lsp .get_active_clients ({ bufnr = bufnr })
1411+ local any = false
1412+ for _ , client in ipairs (clients ) do
1413+ if client_supports_symbols (client ) then
1414+ any = true
1415+ local ok , resp = pcall (function ()
1416+ return client .request_sync (' textDocument/documentSymbol' , params , 500 , bufnr )
1417+ end )
1418+ if ok and resp and resp .result then
1419+ if vim .tbl_islist (resp .result ) then
1420+ vim .list_extend (results , resp .result )
1421+ else
1422+ table.insert (results , resp .result )
1423+ end
1424+ end
1425+ end
1426+ end
1427+ if not any or # results == 0 then
1428+ return nil
1429+ end
1430+ return results
1431+ end
1432+
1433+ local function flatten_symbols (symbols , acc , parent )
1434+ acc = acc or {}
1435+ if not symbols then
1436+ return acc
1437+ end
1438+ for _ , s in ipairs (symbols ) do
1439+ local name = s .name or ' <anonymous>'
1440+ local kind = s .kind or 0
1441+ table.insert (acc , { name = name , kind = kind , parent = parent })
1442+ if s .children then
1443+ flatten_symbols (s .children , acc , name )
1444+ end
1445+ end
1446+ return acc
1447+ end
1448+
1449+ --- @param prompt string
1450+ --- @param opts { enabled : boolean , symbols_only : boolean , max : number }
1451+ --- @return OpencodeMessagePart[] | nil
1452+ function M .get_recent_buffers (prompt , opts )
1453+ if not opts or not opts .enabled then
1454+ return nil
1455+ end
1456+
1457+ local bufs = vim .api .nvim_list_bufs ()
1458+ local recent = {}
1459+
1460+ -- Collect candidate buffers (MRU ordering approximation by number)
1461+ for _ , b in ipairs (bufs ) do
1462+ if is_valid_buffer (b ) then
1463+ local line_count = vim .api .nvim_buf_line_count (b )
1464+ if line_count > 100 then
1465+ local clients = vim .lsp .get_active_clients ({ bufnr = b })
1466+ if # clients > 0 then
1467+ table.insert (recent , { bufnr = b , line_count = line_count })
1468+ end
1469+ end
1470+ end
1471+ end
1472+
1473+ if # recent == 0 then
1474+ return nil
1475+ end
1476+
1477+ table.sort (recent , function (a , b )
1478+ return a .bufnr > b .bufnr -- crude MRU heuristic
1479+ end )
1480+
1481+ local max_items = math.max (1 , opts .max or 5 )
1482+ local parts = {}
1483+ for i = 1 , math.min (# recent , max_items ) do
1484+ local b = recent [i ].bufnr
1485+ local path = vim .api .nvim_buf_get_name (b )
1486+ local rel_path = vim .fn .fnamemodify (path , ' :~:.' )
1487+ local mention = ' @' .. rel_path
1488+ local pos = prompt and prompt :find (mention )
1489+ pos = pos and pos - 1 or 0
1490+
1491+ local symbol_list
1492+ if opts .symbols_only then
1493+ local symbols = fetch_document_symbols (b )
1494+ if symbols then
1495+ local flat = flatten_symbols (symbols )
1496+ local names = {}
1497+ for _ , s in ipairs (flat ) do
1498+ table.insert (names , s .name )
1499+ end
1500+ symbol_list = names
1501+ end
1502+ -- Guarantee a symbols array exists (empty if none found) for a stable contract
1503+ if not symbol_list then
1504+ symbol_list = {}
1505+ end
1506+ end
1507+
1508+ local content
1509+ if not opts .symbols_only then
1510+ local first_lines = vim .api .nvim_buf_get_lines (b , 0 , math.min (200 , vim .api .nvim_buf_line_count (b )), false )
1511+ content = table.concat (first_lines , ' \n ' )
1512+ end
1513+
1514+ local data = {
1515+ context_type = ' recent-buffer' ,
1516+ path = path ,
1517+ relative = rel_path ,
1518+ line_count = recent [i ].line_count ,
1519+ symbols = symbol_list ,
1520+ preview = content and (' ```\n ' .. content .. ' \n ```' ) or nil ,
1521+ }
1522+
1523+ local part = {
1524+ type = ' text' ,
1525+ text = vim .json .encode (data ),
1526+ synthetic = true ,
1527+ source = {
1528+ path = path ,
1529+ type = ' file' ,
1530+ text = { start = pos , value = mention , [' end' ] = pos + # mention - 1 },
1531+ },
1532+ }
1533+ table.insert (parts , part )
1534+ end
1535+
1536+ return parts
1537+ end
1538+
13711539return M
0 commit comments