@@ -451,6 +451,27 @@ fn is_heading_zoom_language(lang: Option<LangId>) -> bool {
451451 matches ! ( lang, Some ( LangId :: Markdown | LangId :: Html ) )
452452}
453453
454+ /// Parse a JSON-stringified array of symbol names (`"[\"A\", \"B\"]"`).
455+ ///
456+ /// Returns `None` unless the string is a well-formed JSON array whose elements
457+ /// are all strings, so ordinary symbol text (including bracketed code like
458+ /// `[derive]` or heading text starting with `[`) is never misparsed.
459+ fn parse_stringified_symbol_array ( raw : & str ) -> Option < Vec < String > > {
460+ let trimmed = raw. trim ( ) ;
461+ if !trimmed. starts_with ( '[' ) || !trimmed. ends_with ( ']' ) {
462+ return None ;
463+ }
464+ let values: Vec < serde_json:: Value > = serde_json:: from_str ( trimmed) . ok ( ) ?;
465+ let mut names = Vec :: with_capacity ( values. len ( ) ) ;
466+ for value in values {
467+ let name = value. as_str ( ) ?. trim ( ) ;
468+ if !name. is_empty ( ) {
469+ names. push ( name. to_string ( ) ) ;
470+ }
471+ }
472+ Some ( names)
473+ }
474+
454475/// Normalize `symbol` / `symbols` into one or more lookup names.
455476///
456477/// For code files, a single string containing internal whitespace is split on `\s+`.
@@ -473,6 +494,14 @@ fn parse_zoom_symbol_names(
473494 return Ok ( Vec :: new ( ) ) ;
474495 } ;
475496
497+ // Some models JSON-stringify the array form ("[\"A\", \"B\"]"). Absorb it
498+ // instead of treating the serialized text as one symbol name; without this
499+ // the lookup fails with a not-found whose suggestions are the very names
500+ // the caller sent.
501+ if let Some ( names) = parse_stringified_symbol_array ( raw) {
502+ return Ok ( names) ;
503+ }
504+
476505 if is_heading_zoom_language ( lang) {
477506 let trimmed = raw. trim ( ) ;
478507 if trimmed. is_empty ( ) {
@@ -1449,6 +1478,40 @@ mod tests {
14491478 assert_eq ! ( names, vec![ "A" , "B" , "C" ] ) ;
14501479 }
14511480
1481+ #[ test]
1482+ fn parse_zoom_symbol_names_absorbs_stringified_array_for_headings ( ) {
1483+ // Models sometimes JSON-stringify the array form; headings keep spaces.
1484+ let params =
1485+ serde_json:: json!( { "symbols" : "[\" 2. Identity material\" , \" 3. Enrollment\" ]" } ) ;
1486+ let names = parse_zoom_symbol_names ( & params, Some ( LangId :: Markdown ) ) . expect ( "parse" ) ;
1487+ assert_eq ! ( names, vec![ "2. Identity material" , "3. Enrollment" ] ) ;
1488+ }
1489+
1490+ #[ test]
1491+ fn parse_zoom_symbol_names_absorbs_stringified_array_for_code ( ) {
1492+ let params = serde_json:: json!( { "symbol" : "[\" alpha\" , \" beta\" ]" } ) ;
1493+ let names = parse_zoom_symbol_names ( & params, Some ( LangId :: Rust ) ) . expect ( "parse" ) ;
1494+ assert_eq ! ( names, vec![ "alpha" , "beta" ] ) ;
1495+ }
1496+
1497+ #[ test]
1498+ fn parse_zoom_symbol_names_bracketed_heading_not_misparsed ( ) {
1499+ // A real heading that starts with '[' but is not a JSON array must
1500+ // stay a single lookup name.
1501+ let params = serde_json:: json!( { "symbols" : "[Draft] Rollout plan" } ) ;
1502+ let names = parse_zoom_symbol_names ( & params, Some ( LangId :: Markdown ) ) . expect ( "parse" ) ;
1503+ assert_eq ! ( names, vec![ "[Draft] Rollout plan" ] ) ;
1504+ }
1505+
1506+ #[ test]
1507+ fn parse_zoom_symbol_names_non_string_json_array_not_absorbed ( ) {
1508+ // "[1, 2]" parses as JSON but not as symbol names; fall through to
1509+ // ordinary handling rather than returning numbers-as-names.
1510+ let params = serde_json:: json!( { "symbols" : "[1, 2]" } ) ;
1511+ let names = parse_zoom_symbol_names ( & params, Some ( LangId :: Rust ) ) . expect ( "parse" ) ;
1512+ assert_eq ! ( names, vec![ "[1," , "2]" ] ) ;
1513+ }
1514+
14521515 // --- Call extraction tests ---
14531516
14541517 #[ test]
0 commit comments