@@ -308,6 +308,16 @@ function M.format_message(prompt, opts)
308308
309309 local parts = { { type = ' text' , text = prompt } }
310310
311+ -- recent_buffers synthetic context
312+ if config .context and config .context .recent_buffers and config .context .recent_buffers .enabled then
313+ local ok , recent = pcall (M .get_recent_buffers , prompt , config .context .recent_buffers )
314+ if ok and recent and # recent > 0 then
315+ for _ , rb in ipairs (recent ) do
316+ table.insert (parts , rb )
317+ end
318+ end
319+ end
320+
311321 for _ , path in ipairs (context .mentioned_files or {}) do
312322 table.insert (parts , format_file_part (path , prompt ))
313323 end
@@ -416,4 +426,162 @@ function M.extract_legacy_tag(tag, text)
416426 return nil
417427end
418428
429+ --- @param buf number
430+ --- @return boolean
431+ local function is_valid_buffer (buf )
432+ if not vim .api .nvim_buf_is_valid (buf ) then
433+ return false
434+ end
435+ if vim .bo [buf ].buftype ~= ' ' then
436+ return false
437+ end
438+ if not vim .bo [buf ].modifiable then
439+ return false
440+ end
441+ return true
442+ end
443+
444+ --- @param client table
445+ local function client_supports_symbols (client )
446+ if not client or not client .server_capabilities then
447+ return false
448+ end
449+ local caps = client .server_capabilities
450+ return caps .documentSymbolProvider == true or (type (caps .documentSymbolProvider ) == ' table' )
451+ end
452+
453+ --- @param bufnr number
454+ --- @return table[] | nil
455+ local function fetch_document_symbols (bufnr )
456+ local params = { textDocument = vim .lsp .util .make_text_document_params (bufnr ) }
457+ local results = {}
458+ local clients = vim .lsp .get_active_clients ({ bufnr = bufnr })
459+ local any = false
460+ for _ , client in ipairs (clients ) do
461+ if client_supports_symbols (client ) then
462+ any = true
463+ local ok , resp = pcall (function ()
464+ return client .request_sync (' textDocument/documentSymbol' , params , 500 , bufnr )
465+ end )
466+ if ok and resp and resp .result then
467+ if vim .tbl_islist (resp .result ) then
468+ vim .list_extend (results , resp .result )
469+ else
470+ table.insert (results , resp .result )
471+ end
472+ end
473+ end
474+ end
475+ if not any or # results == 0 then
476+ return nil
477+ end
478+ return results
479+ end
480+
481+ local function flatten_symbols (symbols , acc , parent )
482+ acc = acc or {}
483+ if not symbols then
484+ return acc
485+ end
486+ for _ , s in ipairs (symbols ) do
487+ local name = s .name or ' <anonymous>'
488+ local kind = s .kind or 0
489+ table.insert (acc , { name = name , kind = kind , parent = parent })
490+ if s .children then
491+ flatten_symbols (s .children , acc , name )
492+ end
493+ end
494+ return acc
495+ end
496+
497+ --- @param prompt string
498+ --- @param opts { enabled : boolean , symbols_only : boolean , max : number }
499+ --- @return OpencodeMessagePart[] | nil
500+ function M .get_recent_buffers (prompt , opts )
501+ if not opts or not opts .enabled then
502+ return nil
503+ end
504+
505+ local bufs = vim .api .nvim_list_bufs ()
506+ local recent = {}
507+
508+ -- Collect candidate buffers (MRU ordering approximation by number)
509+ for _ , b in ipairs (bufs ) do
510+ if is_valid_buffer (b ) then
511+ local line_count = vim .api .nvim_buf_line_count (b )
512+ if line_count > 100 then
513+ local clients = vim .lsp .get_active_clients ({ bufnr = b })
514+ if # clients > 0 then
515+ table.insert (recent , { bufnr = b , line_count = line_count })
516+ end
517+ end
518+ end
519+ end
520+
521+ if # recent == 0 then
522+ return nil
523+ end
524+
525+ table.sort (recent , function (a , b )
526+ return a .bufnr > b .bufnr -- crude MRU heuristic
527+ end )
528+
529+ local max_items = math.max (1 , opts .max or 5 )
530+ local parts = {}
531+ for i = 1 , math.min (# recent , max_items ) do
532+ local b = recent [i ].bufnr
533+ local path = vim .api .nvim_buf_get_name (b )
534+ local rel_path = vim .fn .fnamemodify (path , ' :~:.' )
535+ local mention = ' @' .. rel_path
536+ local pos = prompt and prompt :find (mention )
537+ pos = pos and pos - 1 or 0
538+
539+ local symbol_list
540+ if opts .symbols_only then
541+ local symbols = fetch_document_symbols (b )
542+ if symbols then
543+ local flat = flatten_symbols (symbols )
544+ local names = {}
545+ for _ , s in ipairs (flat ) do
546+ table.insert (names , s .name )
547+ end
548+ symbol_list = names
549+ end
550+ -- Guarantee a symbols array exists (empty if none found) for a stable contract
551+ if not symbol_list then
552+ symbol_list = {}
553+ end
554+ end
555+
556+ local content
557+ if not opts .symbols_only then
558+ local first_lines = vim .api .nvim_buf_get_lines (b , 0 , math.min (200 , vim .api .nvim_buf_line_count (b )), false )
559+ content = table.concat (first_lines , ' \n ' )
560+ end
561+
562+ local data = {
563+ context_type = ' recent-buffer' ,
564+ path = path ,
565+ relative = rel_path ,
566+ line_count = recent [i ].line_count ,
567+ symbols = symbol_list ,
568+ preview = content and (' ```\n ' .. content .. ' \n ```' ) or nil ,
569+ }
570+
571+ local part = {
572+ type = ' text' ,
573+ text = vim .json .encode (data ),
574+ synthetic = true ,
575+ source = {
576+ path = path ,
577+ type = ' file' ,
578+ text = { start = pos , value = mention , [' end' ] = pos + # mention - 1 },
579+ },
580+ }
581+ table.insert (parts , part )
582+ end
583+
584+ return parts
585+ end
586+
419587return M
0 commit comments