Skip to content

Commit 520bbde

Browse files
committed
fix: absorb JSON-stringified symbols arrays in aft_zoom
Models sometimes send symbols as a serialized array string ('["A", "B"]'); zoom treated the whole string as one symbol name and failed with a not-found whose suggestions were the very names the caller sent. Parse well-formed JSON string arrays into individual lookups at the shared chokepoint; bracketed headings and non-string JSON fall through unchanged.
1 parent 18b8aaf commit 520bbde

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

crates/aft/src/commands/zoom.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)