1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::path::Path;
3
4use crate::models::{
5 BlastRadiusResult, ContextSlice, ImpactedSymbol, ReferenceSite, SearchExplain, Symbol,
6 SymbolSearchResult,
7};
8
9pub fn format_file_skeleton(
11 file_path: &str,
12 symbols: &[Symbol],
13 line_count: Option<usize>,
14 parse_errors: usize,
15) -> String {
16 let mut out = String::new();
17 let lines_str = match line_count {
18 Some(c) => format!(" (Lines 1-{c})"),
19 None => String::new(),
20 };
21 out.push_str(&format!("// File: {file_path}{lines_str}\n\n"));
22
23 if parse_errors > 0 {
24 let noun = if parse_errors == 1 { "error" } else { "errors" };
25 out.push_str(&format!(
26 "// {parse_errors} parse {noun}: symbols may be incomplete\n\n"
27 ));
28 }
29
30 if symbols.is_empty() {
31 out.push_str("// No exported symbols indexed.\n");
32 return out;
33 }
34
35 let in_file: HashSet<&str> = symbols.iter().map(|s| s.symbol_id.as_str()).collect();
37 let mut children_map: HashMap<Option<String>, Vec<&Symbol>> = HashMap::new();
38 for s in symbols {
39 let parent = s
40 .parent_symbol_id
41 .clone()
42 .filter(|parent| in_file.contains(parent.as_str()));
43 children_map.entry(parent).or_default().push(s);
44 }
45
46 if let Some(roots) = children_map.get(&None) {
48 for root in roots {
49 render_symbol_skeleton(&mut out, root, None, &children_map, 0);
50 }
51 } else {
52 for s in symbols {
54 render_symbol_skeleton(&mut out, s, None, &children_map, 0);
55 }
56 }
57
58 out
59}
60
61fn is_container_kind(kind: &str) -> bool {
62 let hides_its_body = matches!(
63 kind,
64 "function" | "method" | "constructor" | "destructor" | "operator"
65 );
66 !is_skippable_kind(kind) && !hides_its_body
67}
68
69pub(crate) fn display_kind(sym: &Symbol) -> &str {
73 match (sym.language.as_str(), sym.kind.as_str()) {
74 ("html", "class") => "element",
75 ("sql", "class") => "table",
76 ("markdown", "module") => "section",
77 ("markdown", "import") => "link",
78 (language, "property")
79 if language != "fsharp"
80 && sym.signature.as_deref().is_some_and(|sig| {
81 ["self.", "this.", "cls."]
82 .iter()
83 .any(|p| sig.starts_with(p))
84 }) =>
85 {
86 "attribute"
87 }
88 (_, kind) => kind,
89 }
90}
91
92fn is_skippable_kind(kind: &str) -> bool {
93 matches!(kind, "variable" | "parameter" | "import")
94}
95
96fn value_row_signature(sig: &str) -> std::borrow::Cow<'_, str> {
99 let sig = sig.trim_end().trim_end_matches(';').trim_end();
100 if !sig.contains('\n') && sig.chars().count() <= 120 {
101 return sig.into();
102 }
103 let joined = one_line(sig);
104 let Some(equals) = value_equals(&joined) else {
105 if joined.chars().count() <= 120 {
106 return joined.into();
107 }
108 let cut: String = joined.chars().take(119).collect();
109 return format!("{}…", cut.trim_end()).into();
110 };
111 let (declaration, value) = joined.split_at(equals);
112 let budget = 120usize.saturating_sub(declaration.chars().count()).max(40);
113 if value.chars().count() <= budget {
114 return joined.into();
115 }
116 let cut: String = value.chars().take(budget - 1).collect();
117 format!("{declaration}{}…", cut.trim_end()).into()
118}
119
120fn one_line(sig: &str) -> String {
122 let mut joined = sig
123 .lines()
124 .map(|line| {
125 let code = [" # ", " // "]
126 .iter()
127 .filter_map(|marker| line.find(marker))
128 .min()
129 .map_or(line, |at| &line[..at]);
130 code.trim().trim_end_matches('\\').trim_end()
131 })
132 .filter(|line| !line.is_empty())
133 .collect::<Vec<_>>()
134 .join(" ");
135 for (from, to) in [
136 ("( ", "("),
137 ("[ ", "["),
138 ("{ ", "{"),
139 (", )", ")"),
140 (", ]", "]"),
141 (", }", "}"),
142 (" )", ")"),
143 (" ]", "]"),
144 (" }", "}"),
145 ] {
146 joined = joined.replace(from, to);
147 }
148 joined
149}
150
151fn assigns_a_value(sig: &str) -> bool {
154 value_equals(sig).is_some()
155}
156
157fn value_equals(sig: &str) -> Option<usize> {
159 let bytes = sig.as_bytes();
160 let mut depth = 0usize;
161 for (i, &byte) in bytes.iter().enumerate() {
162 match byte {
163 b'(' | b'[' => depth += 1,
164 b')' | b']' => depth = depth.saturating_sub(1),
165 b'{' if depth == 0 => return None,
166 b'=' if depth == 0 => {
167 let before = i.checked_sub(1).map(|j| bytes[j]);
168 let after = bytes.get(i + 1).copied();
169 let operator_before = before.is_some_and(|b| b"<>!=^*~|$+-/%&:?".contains(&b));
170 let operator_after = after.is_some_and(|b| b == b'=' || b == b'>');
171 if !operator_before && !operator_after {
172 return Some(i);
173 }
174 }
175 _ => {}
176 }
177 }
178 None
179}
180
181fn sanitize_skeleton_sig<'a>(sig: &'a str, sym: &'a Symbol) -> std::borrow::Cow<'a, str> {
182 if matches!(
183 sym.kind.as_str(),
184 "variable" | "constant" | "property" | "field" | "enum_member"
185 ) && assigns_a_value(sig)
186 {
187 return value_row_signature(sig);
188 }
189 container_signature(sig, sym)
190}
191
192fn container_signature<'a>(sig: &'a str, sym: &'a Symbol) -> std::borrow::Cow<'a, str> {
194 let expression_arrow = if sym.language == "csharp"
195 && matches!(
196 sym.kind.as_str(),
197 "method" | "function" | "constructor" | "operator"
198 ) {
199 sig.match_indices("=>")
200 .find_map(|(idx, _)| sig[..idx].trim_end().ends_with(')').then_some(idx))
201 } else {
202 None
203 };
204 let cutoff = sig
205 .find('{')
206 .into_iter()
207 .chain(expression_arrow)
208 .min()
209 .unwrap_or(sig.len());
210 let clean = sig[..cutoff].trim_end();
211 let trimmed = clean.trim_end_matches(';').trim_end();
212 if trimmed.is_empty() {
213 sym.name.as_str().into()
214 } else {
215 trimmed.into()
216 }
217}
218
219fn signature_without_duplicate_body<'a>(signature: &'a str, body: &str) -> &'a str {
220 let body = body.trim();
221 if body.is_empty() {
222 return signature;
223 }
224
225 let trimmed_signature = signature.trim_end();
226 trimmed_signature
227 .strip_suffix(body)
228 .map(str::trim_end)
229 .unwrap_or(signature)
230}
231
232fn unspelled_kind(sym: &Symbol) -> &'static str {
236 if sym.kind != "event" {
237 return "";
238 }
239 let spelled = sym.signature.as_deref().is_some_and(|sig| {
240 sig.split_whitespace()
241 .any(|word| word == "signal" || word == "event")
242 });
243 if spelled { "" } else { "event " }
244}
245
246fn assigning_method(
249 sym: &Symbol,
250 parent: Option<&Symbol>,
251 children_map: &HashMap<Option<String>, Vec<&Symbol>>,
252) -> String {
253 if !sym
254 .signature
255 .as_deref()
256 .is_some_and(|sig| sig.starts_with("self."))
257 {
258 return String::new();
259 }
260 parent
261 .and_then(|parent| children_map.get(&Some(parent.symbol_id.clone())))
262 .and_then(|siblings| {
263 siblings.iter().find(|method| {
264 matches!(method.kind.as_str(), "method" | "function" | "constructor")
265 && method.start_line <= sym.start_line
266 && sym.end_line <= method.end_line
267 })
268 })
269 .map_or_else(String::new, |method| format!("in {}(), ", method.name))
270}
271
272fn is_shown(sym: &Symbol, parent: Option<&Symbol>) -> bool {
275 let python_declaration = sym.kind == "variable"
276 && sym.language == "python"
277 && parent.is_none_or(|parent| parent.kind == "class");
278 !is_skippable_kind(&sym.kind) || python_declaration
279}
280
281fn is_misparsed_c_function(child: &Symbol) -> bool {
286 matches!(child.language.as_str(), "c" | "cpp")
287 && matches!(child.kind.as_str(), "function" | "method")
288 && !child.is_test
289 && !child.test_container
290}
291
292fn nested_definitions(children: &[&Symbol]) -> String {
293 let names: Vec<String> = children
294 .iter()
295 .filter(|child| {
296 matches!(
297 child.kind.as_str(),
298 "function" | "method" | "class" | "struct"
299 )
300 })
301 .filter(|child| {
302 !child
303 .signature
304 .as_deref()
305 .is_some_and(|sig| sig.starts_with("lambda"))
306 })
307 .filter(|child| !is_misparsed_c_function(child))
308 .map(|child| format!("`{}`", child.name))
309 .fold(Vec::new(), |mut names, name| {
310 if !names.contains(&name) {
311 names.push(name);
312 }
313 names
314 });
315 match names.len() {
316 0 => String::new(),
317 1..=5 => format!("; defines {}", names.join(", ")),
318 n => format!("; defines {}, +{} more", names[..5].join(", "), n - 5),
319 }
320}
321
322fn render_symbol_skeleton(
323 out: &mut String,
324 sym: &Symbol,
325 parent: Option<&Symbol>,
326 children_map: &HashMap<Option<String>, Vec<&Symbol>>,
327 indent_level: usize,
328) {
329 if !is_shown(sym, parent) {
330 return;
331 }
332
333 let indent = " ".repeat(indent_level);
334
335 if let Some(ref doc) = sym.doc_comment {
336 let document = crate::queries::DOCUMENTATION_LANGUAGES.contains(&sym.language.as_str());
337 let lines: Vec<_> = if document {
338 doc.lines().collect()
339 } else {
340 doc.lines()
341 .map(doc_line_text)
342 .filter(|line| !line.is_empty())
343 .collect()
344 };
345 let cap = 3;
346 for line in lines.iter().take(cap) {
347 out.push_str(&format!("{indent}/// {line}\n"));
348 }
349 if lines.len() > cap {
350 out.push_str(&format!(
351 "{indent}/// ... ({} more lines)\n",
352 lines.len() - cap
353 ));
354 }
355 }
356
357 let span_str = format!("L{}-{}", sym.start_line, sym.end_line);
358 let leaf_note = format!(
359 "{}{}{span_str}",
360 unspelled_kind(sym),
361 assigning_method(sym, parent, children_map)
362 );
363
364 let children = children_map.get(&Some(sym.symbol_id.clone()));
365
366 let spans_multiple_lines = sym.end_line > sym.start_line;
367
368 if is_container_kind(&sym.kind)
369 && children.is_some_and(|list| list.iter().any(|child| is_shown(child, Some(sym))))
370 && (spans_multiple_lines || sym.kind == "enum")
371 {
372 let raw_sig = sym.signature.as_deref().unwrap_or(&sym.name);
373 let sig = container_signature(raw_sig, sym);
374 out.push_str(&format!("{indent}{sig} {{\n"));
375 if let Some(child_list) = children {
376 for child in child_list {
377 render_symbol_skeleton(out, child, Some(sym), children_map, indent_level + 1);
378 }
379 }
380 out.push_str(&format!("{indent}}} // {span_str}\n\n"));
381 } else {
382 if let Some(count) = sym.hidden_body_line_count() {
383 let raw_sig = sym.signature.as_deref().unwrap_or(&sym.name);
384 let sig = if count > 1 {
385 container_signature(raw_sig, sym)
386 } else {
387 sanitize_skeleton_sig(raw_sig, sym)
388 };
389 let b_start = sym.body_start_line.unwrap_or(sym.start_line);
390 let b_end = sym.body_end_line.unwrap_or(sym.end_line);
391
392 if count > 1 {
393 let defines = nested_definitions(children.map(Vec::as_slice).unwrap_or_default());
394 out.push_str(&format!(
395 "{indent}{sig} {{ /* {count} lines hidden: L{b_start}-L{b_end}{defines} */ }}\n"
396 ));
397 } else {
398 out.push_str(&format!("{indent}{sig}; // {leaf_note}\n"));
399 }
400 } else if let Some(ref raw_sig) = sym.signature {
401 let sig = sanitize_skeleton_sig(raw_sig, sym);
402 out.push_str(&format!("{indent}{sig}; // {leaf_note}\n"));
403 } else {
404 out.push_str(&format!(
405 "{indent}{} {sym_name}; // {span_str}\n",
406 display_kind(sym),
407 sym_name = sym.name
408 ));
409 }
410 if sym.kind == "namespace"
411 && let Some(child_list) = children
412 {
413 for child in child_list {
414 render_symbol_skeleton(out, child, Some(sym), children_map, indent_level);
415 }
416 }
417 }
418}
419
420#[derive(Default)]
422pub struct OutlineNode {
423 pub files: BTreeMap<String, Vec<String>>, pub subdirs: BTreeMap<String, OutlineNode>,
425 pub hidden_files: usize,
427 pub plain_files: Vec<String>,
429 pub unsupported_files: usize,
431}
432
433fn outline_components(file_path: &str, norm_filter: &str) -> Option<Vec<String>> {
436 let normalized = file_path.replace('\\', "/");
437 let rel_path_str = if norm_filter.is_empty() {
438 normalized.as_str()
439 } else if normalized.eq_ignore_ascii_case(norm_filter) {
440 Path::new(&normalized)
441 .file_name()
442 .and_then(|n| n.to_str())
443 .unwrap_or(&normalized)
444 } else if normalized.len() > norm_filter.len()
445 && normalized.as_bytes()[norm_filter.len()] == b'/'
446 && normalized[..norm_filter.len()].eq_ignore_ascii_case(norm_filter)
447 {
448 &normalized[norm_filter.len() + 1..]
449 } else {
450 return None;
451 };
452
453 let path = Path::new(rel_path_str);
454 let components: Vec<String> = path
455 .components()
456 .map(|c| c.as_os_str().to_str().unwrap_or(""))
457 .filter(|s| !s.is_empty())
458 .map(str::to_string)
459 .collect();
460
461 if components.is_empty() {
462 return None;
463 }
464
465 Some(components)
466}
467
468pub fn add_unsupported_to_outline(
471 root_node: &mut OutlineNode,
472 file_path: &str,
473 max_depth: usize,
474 norm_filter: &str,
475) {
476 let Some(components) = outline_components(file_path, norm_filter) else {
477 return;
478 };
479 let mut curr = root_node;
480 for (i, comp) in components
481 .iter()
482 .take(components.len() - 1)
483 .take(max_depth)
484 .enumerate()
485 {
486 if i > 0 && !curr.subdirs.contains_key(comp) {
487 break;
488 }
489 curr = curr.subdirs.entry(comp.clone()).or_default();
490 }
491 curr.unsupported_files += 1;
492}
493
494pub fn add_path_to_outline(
496 root_node: &mut OutlineNode,
497 file_path: &str,
498 symbols_by_file: &HashMap<String, Vec<Symbol>>,
499 counts: &HashMap<String, crate::queries::OutlineCounts>,
500 max_depth: usize,
501 norm_filter: &str,
502) {
503 let Some(components) = outline_components(file_path, norm_filter) else {
504 return;
505 };
506 let normalized = file_path.replace('\\', "/");
507
508 let mut curr = root_node;
509 let depth = components.len();
510
511 for (i, comp) in components.iter().enumerate() {
512 if i == depth - 1 {
513 if depth > max_depth {
514 curr.hidden_files += 1;
515 } else {
516 let mut sym_tags: Vec<String> = symbols_by_file
517 .get(&normalized)
518 .into_iter()
519 .flatten()
520 .filter(|s| !s.is_test && !s.test_container)
521 .take(5)
522 .map(|s| format!("{} {}", display_kind(s), s.name))
523 .collect();
524 let count = counts.get(&normalized).copied().unwrap_or_default();
525 if count.definitions > sym_tags.len() {
526 sym_tags.push(format!("+{} more", count.definitions - sym_tags.len()));
527 }
528 if count.tests > 0 {
529 sym_tags.push(format!("{} {}", count.tests, plural(count.tests, "test")));
530 }
531 if count.fixtures > 0 {
532 sym_tags.push(format!(
533 "{} {}",
534 count.fixtures,
535 plural(count.fixtures, "fixture")
536 ));
537 }
538 if sym_tags.is_empty() {
539 curr.plain_files.push(comp.to_string());
540 } else {
541 curr.files.insert(comp.to_string(), sym_tags);
542 }
543 }
544 } else if i < max_depth {
545 curr = curr.subdirs.entry(comp.to_string()).or_default();
546 } else {
547 curr.hidden_files += 1;
548 break;
549 }
550 }
551}
552
553const FILES_LISTED_PER_FOLDER: usize = 40;
555
556pub fn render_outline_tree(
557 out: &mut String,
558 node: &OutlineNode,
559 prefix: &str,
560 depth: usize,
561 max_depth: usize,
562) {
563 if depth >= max_depth {
564 return;
565 }
566
567 let listed = if depth == 0 {
568 node.files.len()
569 } else {
570 node.files.len().min(FILES_LISTED_PER_FOLDER)
571 };
572 let more_files = node.files.len() - listed;
573 let total_items = node.subdirs.len()
574 + listed
575 + usize::from(more_files > 0)
576 + usize::from(!node.plain_files.is_empty());
577 let mut index = 0;
578
579 for (name, sub) in &node.subdirs {
581 index += 1;
582 let is_last = index == total_items;
583 let branch = if is_last { "└── " } else { "├── " };
584 let next_prefix = format!("{}{}", prefix, if is_last { " " } else { "│ " });
585
586 let hidden = match (sub.hidden_files, sub.unsupported_files) {
587 (0, 0) => String::new(),
588 (0, m) => format!(" ({m} unsupported {})", plural(m, "file")),
589 (n, 0) => format!(" ({n} indexed {})", plural(n, "file")),
590 (n, m) => format!(" ({n} indexed {}, {m} unsupported)", plural(n, "file")),
591 };
592 out.push_str(&format!("{prefix}{branch}{name}/{hidden}\n"));
593 render_outline_tree(out, sub, &next_prefix, depth + 1, max_depth);
594 }
595
596 for (file_name, syms) in node.files.iter().take(listed) {
598 index += 1;
599 let is_last = index == total_items;
600 let branch = if is_last { "└── " } else { "├── " };
601
602 let sym_suffix = if !syms.is_empty() {
603 format!(" [{}]", syms.join(", "))
604 } else {
605 String::new()
606 };
607
608 out.push_str(&format!("{prefix}{branch}{file_name}{sym_suffix}\n"));
609 }
610
611 if more_files > 0 {
612 index += 1;
613 let branch = if index == total_items {
614 "└── "
615 } else {
616 "├── "
617 };
618 out.push_str(&format!(
619 "{prefix}{branch}(+{more_files} more {} with functions or classes; pass this folder as the path to list them)\n",
620 plural(more_files, "file")
621 ));
622 }
623
624 if !node.plain_files.is_empty() {
625 let count = node.plain_files.len();
626 let names = if count <= 5 {
627 node.plain_files.join(", ")
628 } else {
629 format!("{}, +{} more", node.plain_files[..3].join(", "), count - 3)
630 };
631 out.push_str(&format!(
632 "{prefix}└── ({count} {} without functions or classes: {names})\n",
633 plural(count, "file")
634 ));
635 }
636}
637
638pub fn format_symbol_body(symbol: &Symbol, source: &str) -> String {
641 let mut out = format!(
642 "// {}:{}-{} ({})\n{source}",
643 symbol.path, symbol.start_line, symbol.end_line, symbol.name
644 );
645 if !source.ends_with('\n') {
646 out.push('\n');
647 }
648 out
649}
650
651pub fn format_symbol_bodies(bodies: &[(Symbol, String)]) -> String {
653 let mut out = overloads_header(bodies.iter().map(|(symbol, _)| symbol));
654 let parts: Vec<String> = bodies
655 .iter()
656 .map(|(symbol, source)| format_symbol_body(symbol, source))
657 .collect();
658 out.push_str(&parts.join("\n"));
659 out
660}
661
662pub fn format_context_slices(slices: &[ContextSlice]) -> String {
664 let mut out = overloads_header(slices.iter().map(|slice| &slice.target_symbol));
665 let parts: Vec<String> = slices.iter().map(format_context_slice).collect();
666 out.push_str(&parts.join("\n"));
667 out
668}
669
670fn overloads_header<'a>(symbols: impl ExactSizeIterator<Item = &'a Symbol>) -> String {
671 let count = symbols.len();
672 let mut symbols = symbols;
673 match symbols.next() {
674 Some(first) if count > 1 => format!(
675 "`{}` has {count} overloads in {}; each one follows.\n\n",
676 first.name, first.path
677 ),
678 _ => String::new(),
679 }
680}
681
682pub fn format_context_slice(slice: &ContextSlice) -> String {
684 let sym = &slice.target_symbol;
685 let mut out = String::new();
686 out.push_str(&format!(
687 "### Target: `{}` ({}:{}-{})\n\n",
688 sym.name, sym.path, sym.start_line, sym.end_line
689 ));
690
691 if let Some(ref sig) = sym.signature {
692 let sig = signature_without_duplicate_body(sig, &slice.target_body);
693 if !sig.trim().is_empty() {
694 out.push_str(&format!("Signature: `{sig}`\n\n"));
695 }
696 }
697
698 out.push_str(&format!("```{}\n", sym.language));
699 out.push_str(&slice.target_body);
700 if !slice.target_body.ends_with('\n') {
701 out.push('\n');
702 }
703 out.push_str("```\n\n");
704
705 if !slice.callee_signatures.is_empty() {
706 out.push_str("### Dependencies (Signatures):\n");
707 for callee in &slice.callee_signatures {
708 out.push_str(&format!("- {callee}\n"));
709 }
710 if slice.callee_signatures.len() >= 10 {
711 out.push_str("[Showing 10 dependencies (limit reached)]\n");
712 }
713 out.push('\n');
714 }
715
716 if !slice.related_types.is_empty() {
717 out.push_str("### Types:\n");
718 for t in &slice.related_types {
719 out.push_str(&format!("- {t}\n"));
720 }
721 out.push('\n');
722 }
723
724 if !slice.related_tests.is_empty() {
725 out.push_str("### Related Tests:\n");
726 for test in &slice.related_tests {
727 out.push_str(&format!(
728 "- `{}` ({}:{})\n",
729 test.name, test.path, test.start_line
730 ));
731 }
732 if slice.related_tests.len() >= 5 {
733 out.push_str("[Showing 5 tests (limit reached)]\n");
734 }
735 out.push_str("These tests call, use, or name this symbol; blast_radius also lists tests that can reach it through callers.\n\n");
736 } else {
737 out.push_str(
738 "### Related Tests:\nNo test calls, uses, or names this symbol; blast_radius lists tests that can reach it through callers.\n",
739 );
740 }
741
742 out
743}
744
745fn cap_notice(shown: usize, limit: usize) -> String {
746 let advice = if limit >= crate::queries::MAX_RESULT_LIMIT {
747 "narrow the query to see more"
748 } else {
749 "increase limit to see more"
750 };
751 format!("\n[Showing {shown} results (limit reached); {advice}.]\n")
752}
753
754fn match_line(snippet: &str, signature: &str) -> Option<String> {
757 let line = snippet
758 .lines()
759 .enumerate()
760 .map(|(i, text)| match i {
761 0 => text.trim(),
762 _ => text.trim().trim_start_matches(['/', '*', '!']).trim(),
763 })
764 .flat_map(str::split_whitespace)
765 .collect::<Vec<_>>()
766 .join(" ");
767 let plain = |text: &str| {
768 text.split_whitespace()
769 .collect::<String>()
770 .replace(['[', ']'], "")
771 };
772 let matched = plain(line.trim_start_matches("...").trim_end_matches("..."));
773 (!line.is_empty() && !plain(signature).contains(&matched)).then_some(line)
774}
775
776fn callee_name(name: &str) -> String {
779 let line = name.split_whitespace().collect::<Vec<_>>().join(" ");
780 if line.chars().count() <= 60 || !line.contains('(') {
781 return line;
782 }
783 match line.rsplit_once('.') {
784 Some((_, last)) if !last.is_empty() => format!("….{last}"),
785 _ => line,
786 }
787}
788
789pub fn format_import_summary(sites: &[(String, usize)]) -> String {
792 if sites.is_empty() {
793 return String::new();
794 }
795 let files: std::collections::BTreeSet<&str> =
796 sites.iter().map(|(path, _)| path.as_str()).collect();
797 let first: Vec<String> = sites
798 .iter()
799 .take(3)
800 .map(|(path, line)| format!("{path}:{line}"))
801 .collect();
802 let more = if sites.len() > 3 { ", …" } else { "" };
803 format!(
804 "Imported {} {} in {} {}: {}{more} (lookup_symbol with kind=\"import\" lists them)\n",
805 sites.len(),
806 plural(sites.len(), "time"),
807 files.len(),
808 plural(files.len(), "file"),
809 first.join(", ")
810 )
811}
812
813pub fn format_references(
814 target_name: &str,
815 refs: &[ReferenceSite],
816 direction: &str,
817 limit: usize,
818) -> String {
819 let mut out = String::new();
820 let dir_label = if direction == "callers" {
821 "Callers of"
822 } else {
823 "Callees called by"
824 };
825 out.push_str(&format!(
826 "{dir_label} `{target_name}` ({} found):\n",
827 refs.len()
828 ));
829
830 if refs.is_empty() {
831 out.push_str(" (none)\n");
832 return out;
833 }
834
835 for r in refs {
836 let line_info = match r.start_line {
837 Some(l) => format!(":{l}"),
838 None => String::new(),
839 };
840 let other = if direction == "callers" {
841 if r.from_symbol_name.is_empty() && r.enclosing_symbol_name.is_none() {
842 "(no enclosing symbol)".to_string()
843 } else {
844 format!(
845 "`{}`",
846 displayed_symbol_name(&r.from_symbol_name, r.enclosing_symbol_name.as_deref())
847 )
848 }
849 } else {
850 format!("`{}`", callee_name(&r.to_symbol_name))
851 };
852 let in_file = match r.occurrences {
853 Some(n) => format!(", {n} in file"),
854 None => String::new(),
855 };
856 let target = match &r.target {
857 Some(target) if direction != "callers" => format!(" → {target}"),
858 _ => String::new(),
859 };
860 out.push_str(&format!(
861 "- {other} [{}{line_info}] (kind: {}{in_file}){target}\n",
862 r.path, r.kind
863 ));
864 }
865
866 if refs.len() >= limit {
867 out.push_str(&cap_notice(refs.len(), limit));
868 }
869
870 out
871}
872
873fn displayed_symbol_name(name: &str, enclosing_symbol_name: Option<&str>) -> String {
874 if let Some(enclosing_symbol_name) = enclosing_symbol_name {
875 format!("<lambda> in {enclosing_symbol_name}")
876 } else {
877 name.to_string()
878 }
879}
880
881fn other_names_line(query: &str, others: &[&Symbol]) -> String {
885 let mut groups: Vec<(String, &str, usize)> = Vec::new();
886 for s in others {
887 let kind = display_kind(s);
888 match groups
889 .iter_mut()
890 .find(|(name, k, _)| *name == s.name && *k == kind)
891 {
892 Some(group) => group.2 += 1,
893 None => groups.push((s.name.clone(), kind, 1)),
894 }
895 }
896 let listed: Vec<String> = groups
897 .iter()
898 .take(8)
899 .map(|(name, kind, count)| match count {
900 1 => format!("`{name}` ({kind})"),
901 n => format!("`{name}` ({kind}, {n}×)"),
902 })
903 .collect();
904 let more = match groups.len().saturating_sub(8) {
905 0 => String::new(),
906 n => format!(", +{n} more"),
907 };
908 format!(
909 "- {} other {} `{query}`, ignoring case: {}{more} (lookup_symbol with one of these names lists its rows)\n",
910 others.len(),
911 if others.len() == 1 {
912 "row starts with or contains"
913 } else {
914 "rows start with or contain"
915 },
916 listed.join(", ")
917 )
918}
919
920fn doc_line_text(line: &str) -> &str {
923 let mut text = line.trim();
924 for marker in [
925 "///", "//!", "//", "/**", "/*!", "/*", "##", "#'", "#", "--", "*", "'''", "'",
926 ] {
927 if let Some(rest) = text.strip_prefix(marker) {
928 text = rest.trim_start();
929 break;
930 }
931 }
932 for tag in ["<summary>", "</summary>"] {
933 text = text.strip_prefix(tag).unwrap_or(text).trim_start();
934 }
935 for suffix in ["*/", "</summary>"] {
936 text = text.strip_suffix(suffix).unwrap_or(text).trim_end();
937 }
938 text
939}
940
941fn sentence_end(text: &str) -> Option<usize> {
944 let mut depth = 0usize;
945 for (index, c) in text.char_indices() {
946 match c {
947 '(' => depth += 1,
948 ')' => depth = depth.saturating_sub(1),
949 '.' if depth == 0 => {
950 let rest = &text[index + 1..];
951 if rest.starts_with(' ') && !rest[1..].starts_with(|next: char| next.is_lowercase())
952 {
953 return Some(index);
954 }
955 }
956 _ => {}
957 }
958 }
959 None
960}
961
962fn doc_summary(doc: &str) -> Option<String> {
965 let paragraph = doc
966 .lines()
967 .map(doc_line_text)
968 .skip_while(|line| line.is_empty())
969 .take_while(|line| !line.is_empty())
970 .collect::<Vec<_>>()
971 .join(" ");
972 let summary = match sentence_end(¶graph) {
973 Some(end) => paragraph[..=end].to_string(),
974 None if paragraph.chars().count() > 200 => {
975 format!("{}…", paragraph.chars().take(200).collect::<String>())
976 }
977 None => paragraph,
978 };
979 (!summary.is_empty()).then_some(summary)
980}
981
982pub fn format_find_symbol_results(
983 query: &str,
984 exact_matches: &[Symbol],
985 fts_matches: &[SymbolSearchResult],
986 limit: usize,
987) -> String {
988 if !exact_matches.is_empty() {
989 let dotted = query.replace("::", ".");
990 let is_exact = |s: &Symbol| s.name == dotted || s.name.ends_with(&format!(".{dotted}"));
991 let named_exactly = exact_matches.iter().filter(|s| is_exact(s)).count();
992 let exact_note = if named_exactly < exact_matches.len() {
993 format!(" ({named_exactly} named exactly `{query}`; the rest start with or contain it)")
994 } else {
995 String::new()
996 };
997 let owner = query
998 .strip_suffix("::")
999 .or_else(|| query.strip_suffix('.'))
1000 .filter(|owner| !owner.is_empty());
1001 let mut out = match owner {
1002 Some(owner) => format!("Found {} members of `{owner}`:\n\n", exact_matches.len()),
1003 None => format!(
1004 "Found {} symbols matching \"{query}\"{exact_note}:\n\n",
1005 exact_matches.len()
1006 ),
1007 };
1008 let has_definition = exact_matches.iter().any(|s| s.kind != "import");
1009 let folds = |s: &Symbol| crate::queries::folds_into_import_line(query, s, has_definition);
1010 let imports: Vec<&Symbol> = exact_matches.iter().filter(|s| folds(s)).collect();
1011 let (shown, others): (Vec<&Symbol>, Vec<&Symbol>) = exact_matches
1012 .iter()
1013 .filter(|s| !folds(s))
1014 .partition(|s| named_exactly == 0 || is_exact(s));
1015 for s in shown {
1016 let sig = s.signature.as_deref().unwrap_or(&s.name);
1017 out.push_str(&format!(
1018 "- {} `{}` [{}:{}-{}] id={}\n",
1019 display_kind(s),
1020 s.name,
1021 s.path,
1022 s.start_line,
1023 s.end_line,
1024 s.symbol_id
1025 ));
1026 out.push_str(&format!(" Signature: {sig}\n"));
1027 if let Some(doc) = s.doc_comment.as_deref().and_then(doc_summary) {
1028 out.push_str(&format!(" Doc: {doc}\n"));
1029 }
1030 }
1031 if !imports.is_empty() {
1032 let shown = imports
1033 .iter()
1034 .take(3)
1035 .map(|s| format!("{}:{}", s.path, s.start_line))
1036 .collect::<Vec<_>>()
1037 .join(", ");
1038 let more = if imports.len() > 3 { ", …" } else { "" };
1039 let cut_inside_exact_names = exact_matches
1040 .last()
1041 .is_some_and(|s| s.name.eq_ignore_ascii_case(query) || folds(s));
1042 let capped = if exact_matches.len() - imports.len() >= limit && cut_inside_exact_names {
1043 "at least "
1044 } else {
1045 ""
1046 };
1047 out.push_str(&format!(
1048 "- {capped}{} {} of `{query}`: {shown}{more} (lookup_symbol with kind=\"import\" lists them)\n",
1049 imports.len(),
1050 plural(imports.len(), "import")
1051 ));
1052 }
1053 if !others.is_empty() {
1054 out.push_str(&other_names_line(query, &others));
1055 }
1056 if exact_matches.len() - imports.len() >= limit {
1057 out.push_str(&cap_notice(exact_matches.len() - imports.len(), limit));
1058 }
1059 out
1060 } else if !fts_matches.is_empty() {
1061 let mut out = format!(
1062 "No exact name match; {} full-text matches for \"{query}\":\n\n",
1063 fts_matches.len()
1064 );
1065 for r in fts_matches {
1066 let s = &r.symbol;
1067 let sig = s.signature.as_deref().unwrap_or(&s.name);
1068 out.push_str(&format!(
1069 "- {} `{}` [{}:{}-{}] (score: {:.2}) id={}\n",
1070 display_kind(s),
1071 s.name,
1072 s.path,
1073 s.start_line,
1074 s.end_line,
1075 r.score,
1076 s.symbol_id
1077 ));
1078 out.push_str(&format!(" Signature: {sig}\n"));
1079 if let Some(line) = r.snippet.as_deref().and_then(|m| match_line(m, sig)) {
1080 out.push_str(&format!(" Match: {line}\n"));
1081 } else if let Some(doc) = s.doc_comment.as_deref().and_then(doc_summary) {
1082 out.push_str(&format!(" Doc: {doc}\n"));
1083 }
1084 }
1085 if fts_matches.len() >= limit {
1086 out.push_str(&cap_notice(fts_matches.len(), limit));
1087 }
1088 out
1089 } else {
1090 format!("No symbols found matching \"{query}\".\n")
1091 }
1092}
1093
1094pub fn no_facts_heading(category: &str, path_filter: Option<&str>) -> String {
1097 match path_filter {
1098 Some(path) => format!("No facts match '{category}' under `{path}`."),
1099 None => format!("No facts match '{category}' in this repository."),
1100 }
1101}
1102
1103pub fn format_no_facts(
1107 category: &str,
1108 path_filter: Option<&str>,
1109 categories: &[(String, usize)],
1110) -> String {
1111 let mut out = format!("{}\n\n", no_facts_heading(category, path_filter));
1112 if !crate::queries::is_category_alias(category) {
1113 out.push_str(&format_fact_categories(categories));
1114 return out;
1115 }
1116 if matches!(category.to_ascii_lowercase().as_str(), "import" | "imports") {
1117 out.push_str("Most languages record imports as symbols: lookup_symbol with kind=\"import\" lists them.\n\n");
1118 }
1119 out.push_str(&alias_summary(categories));
1120 out
1121}
1122
1123fn alias_summary(categories: &[(String, usize)]) -> String {
1124 let aliases = crate::queries::alias_fact_counts(categories);
1125 if aliases.is_empty() {
1126 return String::new();
1127 }
1128 let parts: Vec<String> = aliases
1129 .iter()
1130 .map(|(alias, patterns, facts)| {
1131 format!(
1132 "{alias} ({patterns} {}, {facts} {})",
1133 plural(*patterns, "pattern"),
1134 plural(*facts, "fact")
1135 )
1136 })
1137 .collect();
1138 format!("Aliases: {}\n\n", parts.join(", "))
1139}
1140
1141pub fn format_fact_categories(categories: &[(String, usize)]) -> String {
1142 if categories.is_empty() {
1143 return "No structural facts or literals indexed in this repository.".to_string();
1144 }
1145 let mut out = String::new();
1146 out.push_str(&alias_summary(categories));
1147 let (patterns, literal_kinds): (Vec<_>, Vec<_>) = categories
1149 .iter()
1150 .filter(|(name, _)| !name.ends_with(".decorated_definition.v1"))
1151 .partition(|(name, _)| name.contains('.'));
1152 out.push_str(&format!(
1153 "Available structural fact categories ({} found):\n\n",
1154 patterns.len()
1155 ));
1156 for (name, count) in &patterns {
1157 out.push_str(&format!(
1158 "- `{name}` ({count} {})\n",
1159 plural(*count, "fact")
1160 ));
1161 }
1162 if !literal_kinds.is_empty() {
1163 out.push_str("\nString literal kinds (the same command lists the literals of a kind):\n\n");
1164 for (name, count) in &literal_kinds {
1165 out.push_str(&format!(
1166 "- `{name}` ({count} {})\n",
1167 plural(*count, "literal")
1168 ));
1169 }
1170 }
1171 out
1172}
1173
1174fn plural(count: usize, word: &str) -> String {
1175 if count == 1 {
1176 word.to_string()
1177 } else {
1178 format!("{word}s")
1179 }
1180}
1181
1182pub fn format_structural_facts(
1184 facts: &[crate::models::StructuralFact],
1185 literals: &[crate::models::LiteralFact],
1186 category: &str,
1187 limit: usize,
1188) -> String {
1189 let mut out = format!(
1190 "Structural facts for '{category}' ({} found):\n",
1191 facts.len()
1192 );
1193 for f in facts {
1194 let label = f.key.as_deref().unwrap_or(&f.capture_name);
1195 let details = format!("{}{}", qt_property_details(f), flask_route_details(f));
1196 let api_style = f
1197 .metadata
1198 .as_ref()
1199 .and_then(|m| m.get("api_style"))
1200 .and_then(|v| v.as_str());
1201 let place = match api_style {
1202 Some("decorator_routing") => "handler",
1203 Some("call_routing") => "registered in",
1204 _ => "in",
1205 };
1206 let parent = f
1207 .containing_symbol_name
1208 .as_deref()
1209 .map(|p| format!(", {place}: {p}"))
1210 .unwrap_or_default();
1211 out.push_str(&format!(
1212 "- {label} [{}:{}] (pattern: {}{details}{parent})\n",
1213 f.path, f.start_line, f.pattern_id
1214 ));
1215 }
1216 if limit > 0 && facts.len() >= limit {
1217 out.push_str(&cap_notice(facts.len(), limit));
1218 }
1219 if facts
1220 .iter()
1221 .any(|f| f.pattern_id.starts_with("flask.route"))
1222 {
1223 out.push_str("Routes as declared in the source. By default Flask also answers HEAD for GET and OPTIONS for every rule, and serves `/static/<path:filename>`; the app or a rule can turn these off.\n");
1224 }
1225 if !literals.is_empty() {
1226 out.push_str(&format!(
1227 "\nMatching literals ({} found):\n",
1228 literals.len()
1229 ));
1230 for l in literals {
1231 out.push_str(&format!(
1232 "- \"{}\" [{}:{}] (kind: {})\n",
1233 l.literal_text, l.path, l.start_line, l.kind
1234 ));
1235 }
1236 if limit > 0 && literals.len() >= limit {
1237 out.push_str(&cap_notice(literals.len(), limit));
1238 }
1239 }
1240 out
1241}
1242
1243fn flask_route_details(fact: &crate::models::StructuralFact) -> String {
1246 if !fact.pattern_id.starts_with("flask.route") {
1247 return String::new();
1248 }
1249 let Some(metadata) = fact.metadata.as_ref() else {
1250 return String::new();
1251 };
1252 let text = |key: &str| metadata.get(key).and_then(|v| v.as_str());
1253 let mut out = String::new();
1254 if let Some(endpoint) = text("endpoint") {
1255 out.push_str(&format!(", endpoint: {endpoint}"));
1256 }
1257 if text("api_style") == Some("call_routing") {
1258 match text("view_target") {
1259 Some(view) => out.push_str(&format!(", view: {view}")),
1260 None => out.push_str(", no view function"),
1261 }
1262 }
1263 out
1264}
1265
1266fn qt_property_details(fact: &crate::models::StructuralFact) -> String {
1267 if fact.pattern_id != "cpp.qt_property.v1" {
1268 return String::new();
1269 }
1270 let Some(metadata) = fact.metadata.as_ref() else {
1271 return String::new();
1272 };
1273 [
1274 "property_type",
1275 "read",
1276 "write",
1277 "notify",
1278 "designable",
1279 "scriptable",
1280 "stored",
1281 "user",
1282 "revision",
1283 ]
1284 .into_iter()
1285 .filter_map(|key| metadata.get(key).map(|value| (key, value)))
1286 .map(|(key, value)| {
1287 let value = value
1288 .as_str()
1289 .map(str::to_owned)
1290 .unwrap_or_else(|| value.to_string());
1291 format!(", {key}: {value}")
1292 })
1293 .collect()
1294}
1295
1296pub fn format_search_results(query: &str, results: &[SymbolSearchResult], limit: usize) -> String {
1298 if results.is_empty() {
1299 return format!("No symbols found matching concept \"{query}\".");
1300 }
1301
1302 let mut out = format!(
1303 "Found {} symbols matching concept \"{query}\":\n",
1304 results.len()
1305 );
1306 if let Some(explain) = results.first().and_then(|r| r.explain.as_ref()) {
1307 out.push_str(&format!(
1308 "rerank: {} candidates in {} µs",
1309 explain.candidates, explain.rerank_us
1310 ));
1311 if !explain.word_weights.is_empty() {
1312 let words: Vec<String> = explain
1313 .word_weights
1314 .iter()
1315 .map(|(word, weight)| format!("{word} {weight:.2}"))
1316 .collect();
1317 out.push_str(&format!("; words {}", words.join(", ")));
1318 }
1319 out.push('\n');
1320 }
1321 out.push('\n');
1322 for r in results {
1323 let s = &r.symbol;
1324 let sig = s.signature.as_deref().unwrap_or(&s.name);
1325 out.push_str(&format!(
1326 "- {} `{}` [{}:{}-{}] (score: {:.2}) id={}\n",
1327 display_kind(s),
1328 s.name,
1329 s.path,
1330 s.start_line,
1331 s.end_line,
1332 r.score,
1333 s.symbol_id
1334 ));
1335 out.push_str(&format!(" Signature: {sig}\n"));
1336 if let Some(line) = r.snippet.as_deref().and_then(|m| match_line(m, sig)) {
1337 out.push_str(&format!(" Match: {line}\n"));
1338 } else if let Some(doc) = s.doc_comment.as_deref().and_then(doc_summary) {
1339 out.push_str(&format!(" Doc: {doc}\n"));
1340 }
1341 if let Some(explain) = &r.explain {
1342 out.push_str(&format!(" explain: {}\n", explain_line(r.score, explain)));
1343 }
1344 }
1345
1346 if results.len() >= limit {
1347 out.push_str(&cap_notice(results.len(), limit));
1348 }
1349
1350 out
1351}
1352
1353fn explain_line(score: f64, e: &SearchExplain) -> String {
1354 let mut line = format!(
1355 "score {score:.1} = terms {:.1} + name {}({}) {:.1} + kind {:.1} + path {:.1}",
1356 e.term_score, e.name_tier, e.name_strength, e.name_bonus, e.kind_prior, e.path_role,
1357 );
1358 if e.field_diversity_bonus != 0.0 {
1359 line.push_str(&format!(" + field coverage {:.1}", e.field_diversity_bonus));
1360 }
1361 if e.documentation != 0.0 {
1362 line.push_str(&format!(" + doc {:.1}", e.documentation));
1363 }
1364 if e.test_intent != 0.0 {
1365 line.push_str(&format!(" + test {:.1}", e.test_intent));
1366 }
1367 if e.nested != 0.0 {
1368 line.push_str(&format!(" + nested {:.1}", e.nested));
1369 }
1370 line.push_str(&format!(" [{}]", e.branches.join(",")));
1371 if let Some(bm25) = e.bm25 {
1372 line.push_str(&format!(" bm25 {bm25:.2}"));
1373 }
1374 if !e.terms.is_empty() {
1375 let terms: Vec<String> = e
1376 .terms
1377 .iter()
1378 .map(|(term, field, credit)| format!("{term}={field}:{credit}"))
1379 .collect();
1380 line.push_str(&format!(" terms {}", terms.join(" ")));
1381 }
1382 line
1383}
1384
1385pub fn format_blast_radius(result: &BlastRadiusResult) -> String {
1387 if result.seed_type == "none" {
1388 return "No uncommitted changes detected in git working tree. Pass a 'symbol' or 'file' parameter to analyze blast radius.".to_string();
1389 }
1390
1391 let mut out = String::new();
1392 let seed_label = if result.seed_type == "file" {
1393 format!("Files: {}", result.seeds.join(", "))
1394 } else if result.seed_type == "symbol" {
1395 format!("Symbol: {}", result.seeds.join(", "))
1396 } else {
1397 format!("Seeds: {}", result.seeds.join(", "))
1398 };
1399
1400 out.push_str(&format!("## Blast Radius & Test Impact ({seed_label})\n\n"));
1401
1402 let more = if result.limit_at_maximum {
1403 "limit is at its maximum, so narrow the target to a symbol or fewer files"
1404 } else {
1405 "raise limit to see the rest"
1406 };
1407 let shown_of = |shown: usize, found: usize, noun: &str| {
1408 if found > shown {
1409 format!("Showing {shown} of {found} {noun}; {more}.\n\n")
1410 } else {
1411 format!("Requested limit hid additional {noun}; {more}.\n\n")
1412 }
1413 };
1414 if result.likely_tests_truncated {
1415 out.push_str(&shown_of(
1416 result.likely_tests.len(),
1417 result.likely_tests_found,
1418 "likely tests",
1419 ));
1420 }
1421 if result.impacted_symbols_truncated {
1422 out.push_str(&shown_of(
1423 result.impacted_symbols.len(),
1424 result.impacted_symbols_found,
1425 "impacted symbols",
1426 ));
1427 }
1428 if result.traversal_ceiling_reached {
1429 out.push_str("Traversal stopped at the 200-row discovery ceiling; narrow the target because increasing limit cannot raise this ceiling.\n\n");
1430 }
1431 if result.test_file_ceiling_reached {
1432 out.push_str("Name-matched test discovery hit its fixed ceiling; narrow the target because increasing limit cannot raise it.\n\n");
1433 }
1434
1435 if !result.likely_tests.is_empty() {
1436 let total = result.likely_tests.len();
1437 out.push_str(&format!("### Likely Tests to Run ({} returned)\n", total));
1438
1439 let mut previous_path = None;
1441 for t in &result.likely_tests {
1442 if previous_path != Some(t.path.as_str()) {
1443 out.push_str(&format!("{}:\n", t.path));
1444 previous_path = Some(t.path.as_str());
1445 }
1446 out.push_str(&format!(
1447 " - `{}` [line {}] ({})\n",
1448 t.name, t.line, t.reason
1449 ));
1450 }
1451
1452 if result
1453 .likely_tests
1454 .iter()
1455 .any(|t| t.reason.starts_with("possible:"))
1456 {
1457 out.push_str("`possible` rows build the class and use a test client, so a runtime call to `__call__` may reach the target. Tests that register an error handler or call `abort` rank ahead of name-only matches; other rows are ranked by shared words. The index cannot prove a request reaches the target; run the whole suite for full coverage.\n");
1458 }
1459 out.push('\n');
1460 } else if result.likely_tests_truncated
1461 || result.traversal_ceiling_reached
1462 || result.test_file_ceiling_reached
1463 {
1464 out.push_str("### Likely Tests to Run (0 returned)\n\n");
1465 } else {
1466 out.push_str(
1467 "### Likely Tests to Run (0 returned)\nNo direct or name-matched tests found.\n\n",
1468 );
1469 }
1470
1471 if !result.impacted_symbols.is_empty() {
1472 let total = result.impacted_symbols.len();
1473 let mut visible = Vec::new();
1474 let mut low_signal_count = 0;
1475 for s in &result.impacted_symbols {
1476 if matches!(s.kind.as_str(), "import" | "module" | "namespace") {
1477 low_signal_count += 1;
1478 } else {
1479 visible.push(s);
1480 }
1481 }
1482
1483 out.push_str(&format!("### Downstream Impact ({} returned)\n", total));
1484
1485 if visible.is_empty() && low_signal_count > 0 {
1486 let row_word = if low_signal_count == 1 {
1487 "row (import/module)"
1488 } else {
1489 "rows (imports/modules)"
1490 };
1491 out.push_str(&format!(
1492 "All impacted symbols are imports/modules; {low_signal_count} low-signal {row_word} hidden; available in CLI --json.\n"
1493 ));
1494 } else {
1495 let mut syms_by_file: std::collections::BTreeMap<&str, Vec<&ImpactedSymbol>> =
1496 std::collections::BTreeMap::new();
1497 let mut file_order = Vec::new();
1498 for s in &visible {
1499 if !syms_by_file.contains_key(s.path.as_str()) {
1500 file_order.push(s.path.as_str());
1501 }
1502 syms_by_file.entry(s.path.as_str()).or_default().push(s);
1503 }
1504
1505 for path in file_order {
1506 out.push_str(&format!("{path}:\n"));
1507 if let Some(syms) = syms_by_file.get(path) {
1508 for s in syms {
1509 let name =
1510 displayed_symbol_name(&s.name, s.enclosing_symbol_name.as_deref());
1511 out.push_str(&format!(
1512 " - [depth {}] {} `{}` [line {}]\n",
1513 s.depth, s.kind, name, s.line
1514 ));
1515 }
1516 }
1517 }
1518
1519 if low_signal_count > 0 {
1520 let row_word = if low_signal_count == 1 {
1521 "row (import/module)"
1522 } else {
1523 "rows (imports/modules)"
1524 };
1525 out.push_str(&format!(
1526 "... {low_signal_count} low-signal {row_word} hidden; available in CLI --json.\n"
1527 ));
1528 }
1529 }
1530 } else if result.impacted_symbols_truncated || result.traversal_ceiling_reached {
1531 out.push_str("### Downstream Impact (0 returned)\n");
1532 } else {
1533 out.push_str(
1534 "### Downstream Impact (0 returned)\nNo downstream callers found within depth.\n",
1535 );
1536 }
1537
1538 out
1539}
1540
1541#[cfg(test)]
1542mod tests {
1543 use super::*;
1544 use crate::models::{ImpactedSymbol, TestTarget};
1545
1546 fn structural_fact(key: Option<&str>) -> crate::models::StructuralFact {
1547 crate::models::StructuralFact {
1548 structural_fact_id: "sf1".into(),
1549 path: ".codex/config.toml".into(),
1550 language: "toml".into(),
1551 pattern_id: "toml.key_value.v1".into(),
1552 capture_name: "key_value".into(),
1553 node_kind: "table".into(),
1554 key: key.map(str::to_string),
1555 metadata: None,
1556 containing_symbol_name: None,
1557 start_line: 2,
1558 end_line: 2,
1559 confidence: 1.0,
1560 }
1561 }
1562
1563 #[test]
1564 fn format_structural_facts_prints_key_and_falls_back_to_capture_name() {
1565 let with_key = format_structural_facts(
1566 &[structural_fact(Some("mcp_servers.code-kb.command"))],
1567 &[],
1568 "config",
1569 30,
1570 );
1571 assert!(with_key.contains(
1572 "- mcp_servers.code-kb.command [.codex/config.toml:2] (pattern: toml.key_value.v1)"
1573 ));
1574
1575 let without_key = format_structural_facts(&[structural_fact(None)], &[], "config", 30);
1576 assert!(
1577 without_key.contains("- key_value [.codex/config.toml:2] (pattern: toml.key_value.v1)")
1578 );
1579 }
1580
1581 #[test]
1582 fn format_structural_facts_renders_selected_qt_property_metadata() {
1583 let mut fact = structural_fact(Some("index"));
1584 fact.pattern_id = "cpp.qt_property.v1".into();
1585 fact.metadata = Some(serde_json::json!({
1586 "property_type": "int",
1587 "designable": false,
1588 "scriptable": true,
1589 "stored": false,
1590 "user": true,
1591 "revision": 2,
1592 }));
1593
1594 let output = format_structural_facts(&[fact], &[], "property", 30);
1595
1596 assert!(output.contains(
1597 "property_type: int, designable: false, scriptable: true, stored: false, user: true, revision: 2"
1598 ));
1599 }
1600
1601 #[test]
1602 fn format_structural_facts_names_the_endpoint_and_view_of_a_flask_route() {
1603 let route = |metadata| crate::models::StructuralFact {
1604 pattern_id: "flask.route.v1".into(),
1605 metadata: Some(metadata),
1606 ..structural_fact(Some("GET /"))
1607 };
1608 let alias = route(serde_json::json!({"api_style": "call_routing", "endpoint": "index"}));
1609 let rule = route(serde_json::json!({"api_style": "call_routing", "view_target": "ping"}));
1610 let decorated = route(serde_json::json!({"api_style": "decorator_routing"}));
1611
1612 let output = format_structural_facts(&[alias, rule, decorated], &[], "route", 30);
1613
1614 assert!(
1615 output.contains("(pattern: flask.route.v1, endpoint: index, no view function)"),
1616 "{output}"
1617 );
1618 assert!(
1619 output.contains("(pattern: flask.route.v1, view: ping)"),
1620 "{output}"
1621 );
1622 assert!(output.contains("(pattern: flask.route.v1)"), "{output}");
1623 }
1624
1625 #[test]
1626 fn doc_summary_ends_at_the_first_sentence_of_the_first_paragraph() {
1627 assert_eq!(
1628 doc_summary(
1629 "The actual WSGI application. This is not implemented in\n:meth:`__call__`."
1630 )
1631 .as_deref(),
1632 Some("The actual WSGI application.")
1633 );
1634 assert_eq!(
1635 doc_summary("\nCreate a runner\nfor tests\n\nMore.").as_deref(),
1636 Some("Create a runner for tests")
1637 );
1638 assert_eq!(
1639 doc_summary("Find facts by category (e.g. route, query). More.").as_deref(),
1640 Some("Find facts by category (e.g. route, query).")
1641 );
1642 assert_eq!(
1643 doc_summary("Use a name, e.g. foo. Then more.").as_deref(),
1644 Some("Use a name, e.g. foo.")
1645 );
1646 assert_eq!(
1647 doc_summary("/// <summary>\n/// Goes to previous page.\n/// </summary>").as_deref(),
1648 Some("Goes to previous page.")
1649 );
1650 assert_eq!(
1651 doc_summary("/**\n * Render the view.\n *\n * @param x\n */").as_deref(),
1652 Some("Render the view.")
1653 );
1654 assert_eq!(
1655 doc_summary("# Given a +hash+ returns the settings\n# for the environment.").as_deref(),
1656 Some("Given a +hash+ returns the settings for the environment.")
1657 );
1658 assert_eq!(
1659 doc_summary("#' A person record.").as_deref(),
1660 Some("A person record.")
1661 );
1662 assert_eq!(doc_summary(" \n"), None);
1663 assert!(doc_summary(&"word ".repeat(60)).unwrap().ends_with('…'));
1664 }
1665
1666 #[test]
1667 fn format_references_reports_the_occurrence_count_of_a_grouped_row() {
1668 let site = |occurrences| ReferenceSite {
1669 from_symbol_name: "Button".into(),
1670 from_symbol_id: "s1".into(),
1671 to_symbol_name: "background".into(),
1672 kind: "member_access".into(),
1673 path: "Ui/Button.qml".into(),
1674 start_line: Some(5),
1675 start_column: Some(4),
1676 occurrences,
1677 target: None,
1678 enclosing_symbol_name: None,
1679 };
1680
1681 let grouped = format_references("Color", &[site(Some(6))], "callers", 30);
1682 assert!(
1683 grouped.contains("- `Button` [Ui/Button.qml:5] (kind: member_access, 6 in file)"),
1684 "{grouped}"
1685 );
1686
1687 let single = format_references("Color", &[site(None)], "callers", 30);
1688 assert!(
1689 single.contains("- `Button` [Ui/Button.qml:5] (kind: member_access)"),
1690 "{single}"
1691 );
1692 }
1693
1694 #[test]
1695 fn a_match_snippet_joins_its_lines_and_is_dropped_when_it_repeats_the_signature() {
1696 assert_eq!(
1697 match_line("...path up to\n the [root] (the [root] excluded)", "fn f()"),
1698 Some("...path up to the [root] (the [root] excluded)".into())
1699 );
1700 assert_eq!(
1701 match_line("// [HelpFunc] returns the\n// [function] set", "fn f()"),
1702 Some("// [HelpFunc] returns the [function] set".into())
1703 );
1704 assert_eq!(
1705 match_line(
1706 "fn [index]_facts(workspace_[root]: &Path) -> Option<[IndexFacts]>",
1707 "fn index_facts(workspace_root: &Path) -> Option<IndexFacts>"
1708 ),
1709 None
1710 );
1711 assert_eq!(
1712 match_line(
1713 "...[root]: &[PathBuf]) -> bool",
1714 "fn f(root: &[PathBuf]) -> bool"
1715 ),
1716 None
1717 );
1718 }
1719
1720 #[test]
1721 fn a_long_unresolved_call_chain_shows_only_its_last_call() {
1722 assert_eq!(
1723 callee_name(
1724 "[\"a\", \"b\"]\n .into_iter()\n .find_map(|key| args.get(key)).ok_or_else"
1725 ),
1726 "….ok_or_else"
1727 );
1728 assert_eq!(
1729 callee_name("workspace.resolve_path"),
1730 "workspace.resolve_path"
1731 );
1732 let selector = "html.dash .header, html.dash .breadcrumbs, html.dash .navigation";
1733 assert_eq!(callee_name(selector), selector);
1734 }
1735
1736 #[test]
1737 fn file_skeleton_reports_parse_errors() {
1738 let two = format_file_skeleton("src/lib.rs", &[], Some(35), 2);
1739 assert!(two.contains("// 2 parse errors: symbols may be incomplete"));
1740
1741 let one = format_file_skeleton("src/lib.rs", &[], Some(35), 1);
1742 assert!(one.contains("// 1 parse error: symbols may be incomplete"));
1743
1744 let none = format_file_skeleton("src/lib.rs", &[], Some(35), 0);
1745 assert!(!none.contains("parse error"));
1746 }
1747
1748 #[test]
1749 fn test_format_file_skeleton() {
1750 let syms = vec![Symbol {
1751 symbol_id: "s1".into(),
1752 file_id: "f1".into(),
1753 path: "src/lib.rs".into(),
1754 language: "rust".into(),
1755 name: "do_work".into(),
1756 kind: "function".into(),
1757 signature: Some("pub fn do_work() -> Result<()>".into()),
1758 doc_comment: Some("Performs core work.".into()),
1759 visibility: Some("pub".into()),
1760 parent_symbol_id: None,
1761 start_line: 10,
1762 start_column: 0,
1763 end_line: 30,
1764 end_column: 1,
1765 start_byte: 100,
1766 end_byte: 300,
1767 body_start_line: Some(11),
1768 body_start_column: Some(0),
1769 body_end_line: Some(29),
1770 body_end_column: Some(1),
1771 body_start_byte: Some(130),
1772 body_end_byte: Some(298),
1773 body_hash: None,
1774 semantic_group: None,
1775 is_test: false,
1776 test_container: false,
1777 }];
1778
1779 let skeleton = format_file_skeleton("src/lib.rs", &syms, Some(35), 0);
1780 assert!(skeleton.contains("/// Performs core work."));
1781
1782 let mut commented = syms.clone();
1783 commented[0].doc_comment = Some("/**\n * Performs core work.\n */".into());
1784 let skeleton = format_file_skeleton("src/lib.rs", &commented, Some(35), 0);
1785 assert!(skeleton.contains("/// Performs core work.\n"), "{skeleton}");
1786 assert!(!skeleton.contains("/// /**"), "{skeleton}");
1787 assert!(skeleton.contains("19 lines hidden: L11-L29"));
1788 }
1789
1790 #[test]
1791 fn format_symbol_body_prints_the_source_under_its_location() {
1792 let mut symbol = sample_symbol("plain");
1793 symbol.signature = Some("pub fn plain()".into());
1794
1795 assert_eq!(
1796 format_symbol_body(&symbol, "pub fn plain() {\n 1\n}"),
1797 "// src/lib.rs:1-10 (plain)\npub fn plain() {\n 1\n}\n"
1798 );
1799 }
1800
1801 #[test]
1802 fn test_format_search_results() {
1803 let results = vec![SymbolSearchResult {
1804 symbol: Symbol {
1805 symbol_id: "s1".into(),
1806 file_id: "f1".into(),
1807 path: "src/parser.rs".into(),
1808 language: "rust".into(),
1809 name: "parse_tokens".into(),
1810 kind: "function".into(),
1811 signature: Some("pub fn parse_tokens()".into()),
1812 doc_comment: Some("Parses tokens from stream.".into()),
1813 visibility: Some("pub".into()),
1814 parent_symbol_id: None,
1815 start_line: 15,
1816 start_column: 0,
1817 end_line: 25,
1818 end_column: 1,
1819 start_byte: 100,
1820 end_byte: 250,
1821 body_start_line: None,
1822 body_start_column: None,
1823 body_end_line: None,
1824 body_end_column: None,
1825 body_start_byte: None,
1826 body_end_byte: None,
1827 body_hash: None,
1828 semantic_group: None,
1829 is_test: false,
1830 test_container: false,
1831 },
1832 score: -1.85,
1833 snippet: Some("Parses [tokens] from stream.".into()),
1834 explain: None,
1835 }];
1836
1837 let formatted = format_search_results("tokens", &results, 20);
1838 assert!(formatted.contains("Found 1 symbols matching concept \"tokens\":\n\n- "));
1839 assert!(
1840 formatted.contains("- function `parse_tokens` [src/parser.rs:15-25] (score: -1.85)")
1841 );
1842 assert!(formatted.contains("Match: Parses [tokens] from stream."));
1843 assert!(!formatted.contains("explain"));
1844 assert!(!formatted.contains("rerank"));
1845 }
1846
1847 #[test]
1848 fn exact_lookup_folds_import_rows_into_one_line_when_a_definition_exists() {
1849 let class = Symbol {
1850 kind: "class".into(),
1851 path: "src/flask/app.py".into(),
1852 ..sample_symbol("Flask")
1853 };
1854 let import = |path: &str, line: usize| Symbol {
1855 kind: "import".into(),
1856 path: path.into(),
1857 start_line: line,
1858 symbol_id: format!("id_{path}"),
1859 ..sample_symbol("Flask")
1860 };
1861 let rows = vec![
1862 class,
1863 import("src/flask/cli.py", 34),
1864 import("src/flask/ctx.py", 21),
1865 import("tests/conftest.py", 6),
1866 import("examples/app.py", 1),
1867 ];
1868
1869 let folded = format_find_symbol_results("Flask", &rows, &[], 20);
1870
1871 assert!(
1872 folded.contains("- class `Flask` [src/flask/app.py:"),
1873 "{folded}"
1874 );
1875 assert!(
1876 folded.contains(
1877 "- 4 imports of `Flask`: src/flask/cli.py:34, src/flask/ctx.py:21, tests/conftest.py:6, … (lookup_symbol with kind=\"import\" lists them)"
1878 ),
1879 "{folded}"
1880 );
1881 assert!(!folded.contains("- import `Flask`"), "{folded}");
1882
1883 let capped = format_find_symbol_results("Flask", &rows, &[], 1);
1884 assert!(
1885 capped.contains("- at least 4 imports of `Flask`"),
1886 "{capped}"
1887 );
1888
1889 let mut with_prefix_rows = rows.clone();
1890 with_prefix_rows.push(Symbol {
1891 kind: "class".into(),
1892 ..sample_symbol("FlaskGroup")
1893 });
1894 let cut_after_exact = format_find_symbol_results("Flask", &with_prefix_rows, &[], 6);
1895 assert!(
1896 cut_after_exact.contains("- 4 imports of `Flask`"),
1897 "{cut_after_exact}"
1898 );
1899
1900 let imports_only = format_find_symbol_results("Flask", &rows[1..], &[], 20);
1901 assert_eq!(imports_only.matches("- import `Flask`").count(), 4);
1902 }
1903
1904 #[test]
1905 fn skeleton_shows_python_class_and_module_variables_with_one_line_values() {
1906 let leaf = |name: &str, kind: &str, line: usize, sig: &str| Symbol {
1907 kind: kind.into(),
1908 language: "python".into(),
1909 path: "app.py".into(),
1910 parent_symbol_id: Some("id_App".into()),
1911 start_line: line,
1912 end_line: line,
1913 body_start_line: None,
1914 body_end_line: None,
1915 signature: Some(sig.into()),
1916 ..sample_symbol(name)
1917 };
1918 let class = Symbol {
1919 kind: "class".into(),
1920 language: "python".into(),
1921 path: "app.py".into(),
1922 start_line: 1,
1923 end_line: 20,
1924 body_start_line: None,
1925 body_end_line: None,
1926 signature: Some("class App".into()),
1927 ..sample_symbol("App")
1928 };
1929 let module_variable = Symbol {
1930 kind: "variable".into(),
1931 language: "python".into(),
1932 path: "app.py".into(),
1933 start_line: 30,
1934 end_line: 30,
1935 body_start_line: None,
1936 body_end_line: None,
1937 signature: Some("app = App()".into()),
1938 ..sample_symbol("app")
1939 };
1940 let symbols = vec![
1941 class,
1942 leaf(
1943 "request_class",
1944 "variable",
1945 2,
1946 "request_class: type[Request] = Request",
1947 ),
1948 leaf(
1949 "default_config",
1950 "variable",
1951 3,
1952 "default_config = ImmutableDict(\n {\n \"DEBUG\": None,\n }\n)",
1953 ),
1954 leaf(
1955 "blueprints",
1956 "property",
1957 5,
1958 "self.blueprints: dict[str, Blueprint] = {}",
1959 ),
1960 leaf(
1961 "error_handler_spec",
1962 "property",
1963 7,
1964 "self.error_handler_spec: dict[\n ft.AppOrBlueprintKey,\n dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]],\n] = defaultdict(lambda: defaultdict(dict), default_factory_argument_that_is_long) # type: ignore",
1965 ),
1966 module_variable,
1967 ];
1968
1969 let out = format_file_skeleton("app.py", &symbols, None, 0);
1970
1971 assert!(
1972 out.contains(" request_class: type[Request] = Request; // L2-2"),
1973 "{out}"
1974 );
1975 assert!(
1976 out.contains(" default_config = ImmutableDict({\"DEBUG\": None}); // L3-3"),
1977 "{out}"
1978 );
1979 assert!(
1980 out.contains(" self.error_handler_spec: dict[ft.AppOrBlueprintKey, dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]]] = defaultdict(lambda: defaultdict(dict)…; // L7-7"),
1981 "{out}"
1982 );
1983 assert!(
1984 out.contains(" self.blueprints: dict[str, Blueprint] = {}; // L5-5"),
1985 "{out}"
1986 );
1987 assert!(out.contains("app = App(); // L30-30"), "{out}");
1988 }
1989
1990 #[test]
1991 fn skeleton_names_the_method_that_assigns_an_attribute() {
1992 let member = |name: &str, kind: &str, start: usize, end: usize, sig: &str| Symbol {
1993 kind: kind.into(),
1994 language: "python".into(),
1995 parent_symbol_id: Some("id_Flask".into()),
1996 start_line: start,
1997 end_line: end,
1998 body_start_line: None,
1999 body_end_line: None,
2000 signature: Some(sig.into()),
2001 ..sample_symbol(name)
2002 };
2003 let class = Symbol {
2004 kind: "class".into(),
2005 language: "python".into(),
2006 start_line: 1,
2007 end_line: 30,
2008 body_start_line: None,
2009 body_end_line: None,
2010 signature: Some("class Flask(App)".into()),
2011 ..sample_symbol("Flask")
2012 };
2013 let symbols = vec![
2014 class,
2015 member("__init__", "method", 2, 10, "def __init__(self)"),
2016 member("cli", "property", 3, 3, "self.cli = cli.AppGroup()"),
2017 member("run", "method", 12, 20, "def run(self)"),
2018 member("debug", "property", 14, 14, "self.debug = get_debug_flag()"),
2019 ];
2020
2021 let out = format_file_skeleton("app.py", &symbols, None, 0);
2022
2023 assert!(
2024 out.contains("self.cli = cli.AppGroup(); // in __init__(), L3-3"),
2025 "{out}"
2026 );
2027 assert!(
2028 out.contains("self.debug = get_debug_flag(); // in run(), L14-14"),
2029 "{out}"
2030 );
2031 }
2032
2033 #[test]
2034 fn skeleton_hides_variables_that_are_not_python_class_attributes() {
2035 let parent = |name: &str, kind: &str, language: &str| Symbol {
2036 kind: kind.into(),
2037 language: language.into(),
2038 start_line: 1,
2039 end_line: 20,
2040 body_start_line: None,
2041 body_end_line: None,
2042 signature: Some(format!("{kind} {name}")),
2043 ..sample_symbol(name)
2044 };
2045 let child = |name: &str, parent: &str, language: &str, sig: &str| Symbol {
2046 kind: "variable".into(),
2047 language: language.into(),
2048 parent_symbol_id: Some(format!("id_{parent}")),
2049 start_line: 2,
2050 end_line: 2,
2051 body_start_line: None,
2052 body_end_line: None,
2053 signature: Some(sig.into()),
2054 ..sample_symbol(name)
2055 };
2056 let symbols = vec![
2057 parent("Adapters", "class", "java"),
2058 child(
2059 "accessible",
2060 "Adapters",
2061 "java",
2062 "boolean accessible = false",
2063 ),
2064 parent("body", "class", "html"),
2065 child(
2066 "img",
2067 "body",
2068 "html",
2069 "<img class=\"carat\" src=\"carat.png\" alt>",
2070 ),
2071 parent("section", "module", "yaml"),
2072 child("title", "section", "yaml", "title: Invoking jq"),
2073 ];
2074
2075 let out = format_file_skeleton("mixed", &symbols, None, 0);
2076
2077 for hidden in ["accessible", "<img", "title:"] {
2078 assert!(!out.contains(hidden), "{out}");
2079 }
2080 }
2081
2082 #[test]
2083 fn a_multi_line_macro_row_drops_its_line_continuations_and_is_cut_to_one_line() {
2084 let short = "#define PAIR(a, b) \\\n a, \\\n b";
2085 assert_eq!(value_row_signature(short), "#define PAIR(a, b) a, b");
2086
2087 let long = format!(
2088 "#define WIDE(x) \\\n{}",
2089 " x + x + x + x + x + x + x + x \\\n".repeat(8)
2090 );
2091 let row = value_row_signature(&long);
2092 assert!(row.starts_with("#define WIDE(x) x + x"), "{row}");
2093 assert!(row.ends_with('…'), "{row}");
2094 assert!(!row.contains('\\'), "{row}");
2095 assert!(row.chars().count() <= 120, "{row}");
2096 }
2097
2098 #[test]
2099 fn skeleton_keeps_values_only_for_real_assignments() {
2100 let row =
2101 |name: &str, kind: &str, line: usize, sig: &str, body: Option<(usize, usize)>| Symbol {
2102 kind: kind.into(),
2103 start_line: line,
2104 end_line: body.map_or(line, |(_, end)| end),
2105 body_start_line: body.map(|(start, _)| start),
2106 body_end_line: body.map(|(_, end)| end),
2107 signature: Some(sig.into()),
2108 ..sample_symbol(name)
2109 };
2110 let symbols = vec![
2111 row(
2112 "img",
2113 "property",
2114 1,
2115 "[class^=rz-] img,[class^=rz-] svg { vertical-align:middle }",
2116 None,
2117 ),
2118 row(
2119 "Coordinates",
2120 "property",
2121 2,
2122 "[JsonProperty(ItemConverterType = typeof(IntToFloatConverter))] public int[,,] Coordinates { get; set; }",
2123 None,
2124 ),
2125 row(
2126 "VERSION_CHECK",
2127 "constant",
2128 3,
2129 "#define VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor)))\n",
2130 None,
2131 ),
2132 row(
2133 "TABLE",
2134 "constant",
2135 4,
2136 "TABLE = {\n '\"' => '%22',\n '\\r' => '%0D',\n}.freeze",
2137 Some((4, 8)),
2138 ),
2139 row(
2140 "LIMIT",
2141 "field",
2142 10,
2143 "private static final int LIMIT = 5;",
2144 None,
2145 ),
2146 ];
2147
2148 let out = format_file_skeleton("mixed", &symbols, None, 0);
2149
2150 assert!(
2151 out.contains("[class^=rz-] img,[class^=rz-] svg; // L1-1"),
2152 "{out}"
2153 );
2154 assert!(out.contains("public int[,,] Coordinates; // L2-2"), "{out}");
2155 assert!(
2156 out.contains(
2157 "#define VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))); // L3-3\n"
2158 ),
2159 "{out}"
2160 );
2161 assert!(
2162 out.contains("TABLE = { /* 5 lines hidden: L4-L8 */ }"),
2163 "{out}"
2164 );
2165 assert!(
2166 out.contains("private static final int LIMIT = 5; // L10-10"),
2167 "{out}"
2168 );
2169 }
2170
2171 #[test]
2172 fn skeleton_cuts_a_field_with_members_at_its_brace() {
2173 let field = Symbol {
2174 kind: "field".into(),
2175 language: "java".into(),
2176 start_line: 1,
2177 end_line: 9,
2178 body_start_line: None,
2179 body_end_line: None,
2180 signature: Some(
2181 "public static final TypeAdapter<Class> CLASS = new TypeAdapter<Class>() {\n @Override\n}"
2182 .into(),
2183 ),
2184 ..sample_symbol("CLASS")
2185 };
2186 let member = Symbol {
2187 kind: "method".into(),
2188 language: "java".into(),
2189 parent_symbol_id: Some("id_CLASS".into()),
2190 start_line: 2,
2191 end_line: 4,
2192 signature: Some("public void write(JsonWriter out, Class value)".into()),
2193 ..sample_symbol("write")
2194 };
2195
2196 let out = format_file_skeleton("TypeAdapters.java", &[field, member], None, 0);
2197
2198 assert!(
2199 out.contains(
2200 "public static final TypeAdapter<Class> CLASS = new TypeAdapter<Class>() {\n"
2201 ),
2202 "{out}"
2203 );
2204 assert!(!out.contains('…'), "{out}");
2205 }
2206
2207 #[test]
2208 fn import_summary_counts_sites_and_files_and_lists_three() {
2209 let sites = vec![
2210 ("src/flask/__init__.py".to_string(), 2),
2211 ("src/flask/cli.py".to_string(), 34),
2212 ("src/flask/cli.py".to_string(), 45),
2213 ("tests/conftest.py".to_string(), 6),
2214 ];
2215
2216 assert_eq!(
2217 format_import_summary(&sites),
2218 "Imported 4 times in 3 files: src/flask/__init__.py:2, src/flask/cli.py:34, src/flask/cli.py:45, … (lookup_symbol with kind=\"import\" lists them)\n"
2219 );
2220 assert_eq!(format_import_summary(&[]), "");
2221 }
2222
2223 #[test]
2224 fn no_facts_for_an_alias_points_imports_at_lookup_and_skips_the_raw_list() {
2225 let categories = vec![("flask.route.v1".to_string(), 3)];
2226
2227 let imports = format_no_facts("import", None, &categories);
2228 let unknown = format_no_facts("widgets", None, &categories);
2229
2230 assert!(
2231 imports.contains("lookup_symbol with kind=\"import\""),
2232 "{imports}"
2233 );
2234 assert!(!imports.contains("`flask.route.v1`"), "{imports}");
2235 assert!(unknown.contains("`flask.route.v1` (3 facts)"), "{unknown}");
2236 }
2237
2238 #[test]
2239 fn markup_and_data_rows_show_their_own_kind_words() {
2240 let row = |language: &str, kind: &str| Symbol {
2241 language: language.into(),
2242 kind: kind.into(),
2243 ..sample_symbol("x")
2244 };
2245 assert_eq!(display_kind(&row("html", "class")), "element");
2246 assert_eq!(display_kind(&row("sql", "class")), "table");
2247 assert_eq!(display_kind(&row("markdown", "module")), "section");
2248 assert_eq!(display_kind(&row("markdown", "import")), "link");
2249 let attribute = Symbol {
2250 kind: "property".into(),
2251 signature: Some("self.extensions = {}".into()),
2252 ..sample_symbol("extensions")
2253 };
2254 assert_eq!(display_kind(&attribute), "attribute");
2255 let fsharp_member = Symbol {
2256 language: "fsharp".into(),
2257 signature: Some("this.Total = decimal this.Qty * this.Price".into()),
2258 ..attribute
2259 };
2260 assert_eq!(display_kind(&fsharp_member), "property");
2261 assert_eq!(display_kind(&row("python", "class")), "class");
2262 }
2263
2264 #[test]
2265 fn no_facts_heading_names_the_path_filter() {
2266 assert_eq!(
2267 no_facts_heading("config", Some("src/flask")),
2268 "No facts match 'config' under `src/flask`."
2269 );
2270 assert_eq!(
2271 no_facts_heading("config", None),
2272 "No facts match 'config' in this repository."
2273 );
2274 }
2275
2276 #[test]
2277 fn outline_says_how_many_definitions_it_left_out_and_counts_tests() {
2278 let mut root = OutlineNode::default();
2279 let symbol = |name: &str, is_test: bool| Symbol {
2280 kind: "function".into(),
2281 path: "tests/test_basic.py".into(),
2282 is_test,
2283 ..sample_symbol(name)
2284 };
2285 let symbols = HashMap::from([(
2286 "tests/test_basic.py".to_string(),
2287 vec![symbol("helper", false), symbol("test_one", true)],
2288 )]);
2289 let counts = HashMap::from([(
2290 "tests/test_basic.py".to_string(),
2291 crate::queries::OutlineCounts {
2292 definitions: 3,
2293 tests: 90,
2294 fixtures: 2,
2295 },
2296 )]);
2297 add_path_to_outline(&mut root, "tests/test_basic.py", &symbols, &counts, 3, "");
2298
2299 let mut out = String::new();
2300 render_outline_tree(&mut out, &root, "", 0, 3);
2301
2302 assert!(
2303 out.contains("test_basic.py [function helper, +2 more, 90 tests, 2 fixtures]"),
2304 "{out}"
2305 );
2306 }
2307
2308 #[test]
2309 fn outline_counts_the_files_below_the_depth_limit() {
2310 let mut root = OutlineNode::default();
2311 let no_symbols = HashMap::new();
2312 for path in [
2313 "src/flask/app.py",
2314 "src/flask/json/tag.py",
2315 "src/flask/cli.py",
2316 "tests/conftest.py",
2317 ] {
2318 add_path_to_outline(&mut root, path, &no_symbols, &HashMap::new(), 2, "");
2319 }
2320
2321 let mut out = String::new();
2322 render_outline_tree(&mut out, &root, "", 0, 2);
2323
2324 assert!(out.contains("└── flask/ (3 indexed files)"), "{out}");
2325 assert!(!out.contains("tests/ ("), "{out}");
2326 }
2327
2328 #[test]
2329 fn a_lookup_qualified_with_double_colons_names_the_member_exactly() {
2330 let rows = vec![sample_symbol("Flask.handle_user_exception")];
2331
2332 let out = format_find_symbol_results("Flask::handle_user_exception", &rows, &[], 20);
2333
2334 assert!(!out.contains("named exactly"), "{out}");
2335 }
2336
2337 #[test]
2338 fn exact_lookup_folds_only_imports_named_exactly_like_the_query() {
2339 let rows = vec![
2340 Symbol {
2341 kind: "function".into(),
2342 ..sample_symbol("read")
2343 },
2344 Symbol {
2345 kind: "import".into(),
2346 path: "src/flask/cli.py".into(),
2347 start_line: 1034,
2348 ..sample_symbol("readline")
2349 },
2350 Symbol {
2351 kind: "import".into(),
2352 path: "src/Types.kt".into(),
2353 start_line: 18,
2354 ..sample_symbol("com.example.internal.read")
2355 },
2356 ];
2357
2358 let out = format_find_symbol_results("read", &rows, &[], 20);
2359
2360 assert!(
2361 out.contains(
2362 "- 1 other row starts with or contains `read`, ignoring case: `readline` (import) (lookup_symbol"
2363 ),
2364 "{out}"
2365 );
2366 assert!(
2367 out.contains(
2368 "- 1 import of `read`: src/Types.kt:18 (lookup_symbol with kind=\"import\" lists them)"
2369 ),
2370 "{out}"
2371 );
2372 }
2373
2374 #[test]
2375 fn discovery_formatters_append_ids_to_metadata_rows() {
2376 let exact = vec![sample_symbol("exact")];
2377 let fallback = vec![SymbolSearchResult {
2378 symbol: sample_symbol("fallback"),
2379 score: 1.0,
2380 snippet: None,
2381 explain: None,
2382 }];
2383
2384 let outputs = [
2385 (
2386 format_find_symbol_results("exact", &exact, &[], 20),
2387 "id_exact",
2388 ),
2389 (
2390 format_find_symbol_results("fallback query", &[], &fallback, 20),
2391 "id_fallback",
2392 ),
2393 (
2394 format_search_results("fallback", &fallback, 20),
2395 "id_fallback",
2396 ),
2397 ];
2398
2399 for (formatted, id) in outputs {
2400 assert!(
2401 formatted
2402 .lines()
2403 .any(|line| line.starts_with("- ") && line.contains(&format!("id={id}"))),
2404 "{formatted}"
2405 );
2406 assert!(!formatted.contains("\n id="), "{formatted}");
2407 }
2408 }
2409
2410 #[test]
2411 fn every_printed_part_of_the_explain_line_sums_to_the_score() {
2412 let e = SearchExplain {
2413 bm25: Some(-3.21),
2414 branches: vec!["word".into()],
2415 name_tier: "all".into(),
2416 name_strength: 6,
2417 term_score: 24.5,
2418 field_diversity_bonus: 6.0,
2419 name_bonus: 60.0,
2420 kind_prior: 4.0,
2421 path_role: -10.0,
2422 documentation: -200.0,
2423 test_intent: 5.0,
2424 nested: 0.0,
2425 terms: vec![("sha".into(), "name".into(), 3.0)],
2426 word_weights: vec![("sha".into(), 2.6)],
2427 candidates: 1,
2428 rerank_us: 1,
2429 };
2430 let score = e.term_score
2431 + e.field_diversity_bonus
2432 + e.name_bonus
2433 + e.kind_prior
2434 + e.path_role
2435 + e.documentation
2436 + e.test_intent;
2437
2438 let line = explain_line(score, &e);
2439 let parts: f64 = line
2440 .split(" = ")
2441 .nth(1)
2442 .unwrap()
2443 .split(" [")
2444 .next()
2445 .unwrap()
2446 .split(" + ")
2447 .map(|part| part.rsplit(' ').next().unwrap().parse::<f64>().unwrap())
2448 .sum();
2449
2450 assert!(line.starts_with(&format!(
2451 "score {score:.1} = terms 24.5 + name all(6) 60.0 "
2452 )));
2453 assert!(line.contains("+ field coverage 6.0"));
2454 assert_eq!(format!("{parts:.1}"), format!("{score:.1}"));
2455 }
2456
2457 #[test]
2458 fn test_format_search_results_prints_the_explain_breakdown_when_present() {
2459 let mut result = SymbolSearchResult {
2460 symbol: sample_symbol("parseSha256Sidecar"),
2461 score: 71.6,
2462 snippet: Some("parse[Sha256]Sidecar".into()),
2463 explain: Some(SearchExplain {
2464 bm25: Some(-3.21),
2465 branches: vec!["word".into(), "name".into()],
2466 name_tier: "all".into(),
2467 name_strength: 6,
2468 terms: vec![
2469 ("sha".into(), "name".into(), 3.0),
2470 ("256".into(), "name".into(), 3.0),
2471 ],
2472 term_score: 12.6,
2473 field_diversity_bonus: 0.0,
2474 name_bonus: 60.0,
2475 kind_prior: 4.0,
2476 path_role: -10.0,
2477 documentation: 0.0,
2478 test_intent: 5.0,
2479 nested: 0.0,
2480 word_weights: vec![("sha".into(), 2.6), ("256".into(), 0.97)],
2481 candidates: 37,
2482 rerank_us: 180,
2483 }),
2484 };
2485
2486 let formatted = format_search_results("sha256", std::slice::from_ref(&result), 20);
2487 assert!(formatted.contains(
2488 "Found 1 symbols matching concept \"sha256\":\nrerank: 37 candidates in 180 µs; words sha 2.60, 256 0.97\n\n- "
2489 ));
2490 assert!(formatted.contains(
2491 " explain: score 71.6 = terms 12.6 + name all(6) 60.0 + kind 4.0 + path -10.0 + test 5.0 [word,name] bm25 -3.21 terms sha=name:3 256=name:3\n"
2492 ));
2493
2494 result.explain = None;
2495 let silent = format_search_results("sha256", std::slice::from_ref(&result), 20);
2496 assert!(!silent.contains("explain"));
2497 assert!(!silent.contains("rerank"));
2498 }
2499
2500 #[test]
2501 fn test_format_search_results_discloses_a_reached_limit() {
2502 let results = vec![SymbolSearchResult {
2503 symbol: sample_symbol("parse_tokens"),
2504 score: 0.0,
2505 snippet: None,
2506 explain: None,
2507 }];
2508
2509 let formatted = format_search_results("tokens", &results, 1);
2510 assert!(
2511 formatted.contains("[Showing 1 results (limit reached); increase limit to see more.]")
2512 );
2513
2514 let at_ceiling =
2515 format_search_results("tokens", &results, crate::queries::MAX_RESULT_LIMIT);
2516 assert!(!at_ceiling.contains("limit reached"));
2517
2518 let full: Vec<SymbolSearchResult> = (0..crate::queries::MAX_RESULT_LIMIT)
2519 .map(|_| SymbolSearchResult {
2520 symbol: sample_symbol("parse_tokens"),
2521 score: 0.0,
2522 snippet: None,
2523 explain: None,
2524 })
2525 .collect();
2526 let capped = format_search_results("tokens", &full, crate::queries::MAX_RESULT_LIMIT);
2527 assert!(capped.contains("(limit reached); narrow the query to see more.]"));
2528 }
2529
2530 #[test]
2531 fn test_format_blast_radius() {
2532 let res = BlastRadiusResult {
2533 seed_type: "symbol".into(),
2534 seeds: vec!["do_work".into()],
2535 likely_tests: vec![TestTarget {
2536 name: "test_do_work".into(),
2537 path: "tests/work_test.rs".into(),
2538 line: 15,
2539 reason: "direct caller".into(),
2540 }],
2541 impacted_symbols: vec![ImpactedSymbol {
2542 name: "caller_fn".into(),
2543 kind: "function".into(),
2544 path: "src/caller.rs".into(),
2545 line: 42,
2546 depth: 1,
2547 enclosing_symbol_name: None,
2548 }],
2549 traversal_ceiling_reached: false,
2550 likely_tests_truncated: false,
2551 impacted_symbols_truncated: false,
2552 test_file_ceiling_reached: false,
2553 limit_at_maximum: false,
2554 likely_tests_found: 0,
2555 impacted_symbols_found: 0,
2556 };
2557
2558 let formatted = format_blast_radius(&res);
2559 assert!(formatted.contains("## Blast Radius & Test Impact (Symbol: do_work)"));
2560 assert!(formatted.contains("### Likely Tests to Run (1 returned)"));
2561 assert!(
2562 formatted.contains("tests/work_test.rs:\n - `test_do_work` [line 15] (direct caller)")
2563 );
2564 assert!(formatted.contains("src/caller.rs:\n - [depth 1] function `caller_fn` [line 42]"));
2565 }
2566
2567 #[test]
2568 fn format_blast_radius_groups_every_returned_test_by_file() {
2569 let mut likely_tests = Vec::new();
2570 for i in 1..=25 {
2571 likely_tests.push(TestTarget {
2572 name: format!("test_{i}"),
2573 path: format!("tests/test_{}.rs", (i % 3) + 1),
2574 line: i * 10,
2575 reason: "direct caller".into(),
2576 });
2577 }
2578
2579 let impacted_symbols = vec![
2580 ImpactedSymbol {
2581 name: "use_foo".into(),
2582 kind: "import".into(),
2583 path: "src/service.rs".into(),
2584 line: 1,
2585 depth: 1,
2586 enclosing_symbol_name: None,
2587 },
2588 ImpactedSymbol {
2589 name: "service_fn".into(),
2590 kind: "function".into(),
2591 path: "src/service.rs".into(),
2592 line: 20,
2593 depth: 1,
2594 enclosing_symbol_name: None,
2595 },
2596 ImpactedSymbol {
2597 name: "api_handler".into(),
2598 kind: "function".into(),
2599 path: "src/api.rs".into(),
2600 line: 45,
2601 depth: 2,
2602 enclosing_symbol_name: None,
2603 },
2604 ];
2605
2606 let res = BlastRadiusResult {
2607 seed_type: "file".into(),
2608 seeds: vec!["src/lib.rs".into()],
2609 likely_tests,
2610 impacted_symbols,
2611 traversal_ceiling_reached: false,
2612 likely_tests_truncated: false,
2613 impacted_symbols_truncated: false,
2614 test_file_ceiling_reached: false,
2615 limit_at_maximum: false,
2616 likely_tests_found: 0,
2617 impacted_symbols_found: 0,
2618 };
2619
2620 let formatted = format_blast_radius(&res);
2621
2622 assert!(formatted.contains("### Likely Tests to Run (25 returned)"));
2623 assert!(
2624 formatted.contains(" - `test_25` [line 250]"),
2625 "{formatted}"
2626 );
2627 assert!(!formatted.contains("hidden by the compact"));
2628
2629 assert!(formatted.contains("tests/test_1.rs:\n"));
2630 assert!(formatted.contains(" - `test_"));
2631
2632 assert!(!formatted.contains("use_foo"));
2633 assert!(
2634 formatted
2635 .contains("... 1 low-signal row (import/module) hidden; available in CLI --json.")
2636 );
2637 assert!(formatted.contains("src/service.rs:\n"));
2638 assert!(formatted.contains(" - [depth 1] function `service_fn` [line 20]"));
2639 }
2640
2641 fn sample_symbol(name: &str) -> Symbol {
2642 Symbol {
2643 symbol_id: format!("id_{name}"),
2644 file_id: "f1".into(),
2645 path: "src/lib.rs".into(),
2646 language: "rust".into(),
2647 name: name.into(),
2648 kind: "function".into(),
2649 signature: Some(format!("pub fn {name}()")),
2650 doc_comment: None,
2651 visibility: Some("pub".into()),
2652 parent_symbol_id: None,
2653 start_line: 1,
2654 start_column: 0,
2655 end_line: 10,
2656 end_column: 1,
2657 start_byte: 0,
2658 end_byte: 100,
2659 body_start_line: Some(2),
2660 body_start_column: Some(0),
2661 body_end_line: Some(9),
2662 body_end_column: Some(1),
2663 body_start_byte: Some(10),
2664 body_end_byte: Some(99),
2665 body_hash: None,
2666 semantic_group: None,
2667 is_test: false,
2668 test_container: false,
2669 }
2670 }
2671
2672 fn sample_context_slice() -> ContextSlice {
2673 ContextSlice {
2674 target_symbol: sample_symbol("target_fn"),
2675 target_body: " println!(\"hello\");\n".into(),
2676 callee_signatures: Vec::new(),
2677 related_types: Vec::new(),
2678 related_tests: Vec::new(),
2679 }
2680 }
2681
2682 #[test]
2683 fn format_context_slice_does_not_repeat_expression_already_in_signature() {
2684 let body = "=>\n transport.SendAsync<Response>(\n HttpMethod.Get,\n $\"items/{id}\",\n cancellationToken: cancellationToken)";
2685 let mut slice = sample_context_slice();
2686 slice.target_symbol.language = "csharp".into();
2687 slice.target_symbol.signature = Some(format!(
2688 "public static Task<Response?> GetByIdAsync(\n int id,\n CancellationToken cancellationToken = default){body}"
2689 ));
2690 slice.target_body = body.into();
2691
2692 let formatted = format_context_slice(&slice);
2693
2694 assert_eq!(
2695 formatted.matches("transport.SendAsync").count(),
2696 1,
2697 "{formatted}"
2698 );
2699 }
2700
2701 #[test]
2702 fn context_slice_leaves_out_a_signature_that_the_body_already_holds() {
2703 let mut slice = sample_context_slice();
2704 slice.target_symbol.signature = Some("type Person = { Name: string; Age: int }".into());
2705 slice.target_body = "type Person = { Name: string; Age: int }\n".into();
2706
2707 let text = format_context_slice(&slice);
2708
2709 assert!(!text.contains("Signature:"), "{text}");
2710 assert_eq!(text.matches("type Person").count(), 1, "{text}");
2711 }
2712
2713 #[test]
2714 fn context_slice_says_when_no_test_was_found() {
2715 let text = format_context_slice(&sample_context_slice());
2716
2717 assert!(
2718 text.ends_with("### Related Tests:\nNo test calls, uses, or names this symbol; blast_radius lists tests that can reach it through callers.\n"),
2719 "{text}"
2720 );
2721 }
2722
2723 #[test]
2724 fn test_context_slice_shows_truncation_notice_when_caps_hit() {
2725 let mut slice = sample_context_slice();
2726 slice.callee_signatures = (1..=10).map(|i| format!("fn callee_{i}()")).collect();
2727 let text = format_context_slice(&slice);
2728 assert!(text.contains("[Showing 10 dependencies (limit reached)]"));
2729
2730 let mut slice_tests = sample_context_slice();
2731 slice_tests.related_tests = (1..=5)
2732 .map(|i| {
2733 let mut sym = sample_symbol(&format!("test_fn_{i}"));
2734 sym.path = format!("tests/test_{i}.rs");
2735 sym.is_test = true;
2736 sym
2737 })
2738 .collect();
2739 let text_tests = format_context_slice(&slice_tests);
2740 assert!(text_tests.contains("[Showing 5 tests (limit reached)]"));
2741 }
2742
2743 #[test]
2744 fn test_blast_radius_shows_traversal_ceiling_at_200_symbols() {
2745 let impacted_symbols = (1..=200)
2746 .map(|i| ImpactedSymbol {
2747 name: format!("sym_{i}"),
2748 kind: "function".into(),
2749 path: format!("src/mod_{}.rs", i % 10),
2750 line: i,
2751 depth: 1,
2752 enclosing_symbol_name: None,
2753 })
2754 .collect();
2755
2756 let res = BlastRadiusResult {
2757 seed_type: "symbol".into(),
2758 seeds: vec!["root_fn".into()],
2759 likely_tests: Vec::new(),
2760 impacted_symbols,
2761 traversal_ceiling_reached: true,
2762 likely_tests_truncated: false,
2763 impacted_symbols_truncated: false,
2764 test_file_ceiling_reached: false,
2765 limit_at_maximum: false,
2766 likely_tests_found: 0,
2767 impacted_symbols_found: 0,
2768 };
2769
2770 let formatted = format_blast_radius(&res);
2771 assert!(formatted.contains("### Downstream Impact (200 returned)\n"));
2772 }
2773
2774 #[test]
2775 fn blast_radius_does_not_claim_no_results_after_discovery_ceiling() {
2776 let res = BlastRadiusResult {
2777 seed_type: "file".into(),
2778 seeds: vec!["src/widget.rs".into()],
2779 likely_tests: Vec::new(),
2780 impacted_symbols: Vec::new(),
2781 likely_tests_truncated: false,
2782 impacted_symbols_truncated: false,
2783 traversal_ceiling_reached: true,
2784 test_file_ceiling_reached: true,
2785 limit_at_maximum: false,
2786 likely_tests_found: 0,
2787 impacted_symbols_found: 0,
2788 };
2789
2790 let formatted = format_blast_radius(&res);
2791 assert!(formatted.contains("Likely Tests to Run (0 returned)"));
2792 assert!(formatted.contains("Downstream Impact (0 returned)"));
2793 assert!(formatted.contains("Name-matched test discovery hit its fixed ceiling"));
2794 assert!(!formatted.contains("No direct or name-matched tests found"));
2795 assert!(!formatted.contains("No downstream callers found within depth"));
2796
2797 let no_ceiling = BlastRadiusResult {
2798 traversal_ceiling_reached: false,
2799 test_file_ceiling_reached: false,
2800 limit_at_maximum: false,
2801 ..res.clone()
2802 };
2803 assert!(format_blast_radius(&no_ceiling).contains("No direct or name-matched tests found"));
2804
2805 let traversal_only = BlastRadiusResult {
2806 traversal_ceiling_reached: true,
2807 test_file_ceiling_reached: false,
2808 limit_at_maximum: false,
2809 ..res
2810 };
2811 let traversal_only_text = format_blast_radius(&traversal_only);
2812 assert!(!traversal_only_text.contains("No direct or name-matched tests found"));
2813 }
2814
2815 fn skeleton_row(
2816 id: &str,
2817 parent: Option<&str>,
2818 kind: &str,
2819 name: &str,
2820 signature: &str,
2821 lines: (usize, usize),
2822 body: Option<(usize, usize)>,
2823 ) -> Symbol {
2824 Symbol {
2825 symbol_id: id.into(),
2826 file_id: "f1".into(),
2827 path: "src/lib.rs".into(),
2828 language: "rust".into(),
2829 name: name.into(),
2830 kind: kind.into(),
2831 signature: Some(signature.into()),
2832 doc_comment: None,
2833 visibility: None,
2834 parent_symbol_id: parent.map(str::to_string),
2835 start_line: lines.0,
2836 start_column: 0,
2837 end_line: lines.1,
2838 end_column: 1,
2839 start_byte: 0,
2840 end_byte: 0,
2841 body_start_line: body.map(|b| b.0),
2842 body_start_column: None,
2843 body_end_line: body.map(|b| b.1),
2844 body_end_column: None,
2845 body_start_byte: None,
2846 body_end_byte: None,
2847 body_hash: None,
2848 semantic_group: None,
2849 is_test: false,
2850 test_container: false,
2851 }
2852 }
2853
2854 #[test]
2855 fn skeleton_nests_an_object_under_the_field_that_declares_it() {
2856 let syms = vec![
2857 skeleton_row(
2858 "root",
2859 None,
2860 "class",
2861 "shell",
2862 "extends ShellRoot",
2863 (1, 8),
2864 None,
2865 ),
2866 skeleton_row(
2867 "timer",
2868 Some("root"),
2869 "field",
2870 "localPluginReloadTimer",
2871 "localPluginReloadTimer: Timer",
2872 (2, 7),
2873 Some((2, 7)),
2874 ),
2875 skeleton_row(
2876 "interval",
2877 Some("timer"),
2878 "property",
2879 "interval",
2880 "interval: 150",
2881 (3, 3),
2882 None,
2883 ),
2884 skeleton_row(
2885 "fire",
2886 Some("timer"),
2887 "function",
2888 "fire",
2889 "function fire()",
2890 (5, 7),
2891 Some((6, 7)),
2892 ),
2893 ];
2894
2895 assert_eq!(
2896 format_file_skeleton("shell/shell.qml", &syms, Some(8), 0),
2897 "// File: shell/shell.qml (Lines 1-8)\n\
2898 \n\
2899 extends ShellRoot {\n\
2900 \x20 localPluginReloadTimer: Timer {\n\
2901 \x20 interval: 150; // L3-3\n\
2902 \x20 function fire() { /* 2 lines hidden: L6-L7 */ }\n\
2903 \x20 } // L2-7\n\
2904 \n\
2905 } // L1-8\n\
2906 \n"
2907 );
2908 }
2909
2910 #[test]
2911 fn skeleton_renders_a_single_line_symbol_with_children_as_a_leaf() {
2912 let syms = vec![
2913 skeleton_row(
2914 "rusqlite",
2915 None,
2916 "field",
2917 "rusqlite",
2918 "rusqlite = { workspace = true }",
2919 (15, 15),
2920 None,
2921 ),
2922 skeleton_row(
2923 "workspace",
2924 Some("rusqlite"),
2925 "property",
2926 "workspace",
2927 "workspace = true",
2928 (15, 15),
2929 None,
2930 ),
2931 ];
2932
2933 assert_eq!(
2934 format_file_skeleton("Cargo.toml", &syms, Some(15), 0),
2935 "// File: Cargo.toml (Lines 1-15)\n\
2936 \n\
2937 rusqlite = { workspace = true }; // L15-15\n"
2938 );
2939 }
2940
2941 #[test]
2942 fn skeleton_renders_members_of_a_single_line_enum() {
2943 let mut syms = vec![
2944 skeleton_row(
2945 "mode",
2946 None,
2947 "enum",
2948 "DispatchMode",
2949 "public enum DispatchMode",
2950 (21, 21),
2951 None,
2952 ),
2953 skeleton_row(
2954 "provision",
2955 Some("mode"),
2956 "enum_member",
2957 "Provision",
2958 "Provision",
2959 (21, 21),
2960 None,
2961 ),
2962 skeleton_row(
2963 "deprovision",
2964 Some("mode"),
2965 "enum_member",
2966 "Deprovision",
2967 "Deprovision",
2968 (21, 21),
2969 None,
2970 ),
2971 skeleton_row(
2972 "extension",
2973 Some("mode"),
2974 "enum_member",
2975 "Extension",
2976 "Extension",
2977 (21, 21),
2978 None,
2979 ),
2980 ];
2981 for symbol in &mut syms {
2982 symbol.language = "csharp".into();
2983 }
2984
2985 let skeleton = format_file_skeleton("DispatchContext.cs", &syms, Some(21), 0);
2986
2987 for member in ["Provision", "Deprovision", "Extension"] {
2988 assert!(skeleton.contains(member), "{skeleton}");
2989 }
2990 }
2991
2992 #[test]
2993 fn a_hidden_body_names_the_functions_defined_inside_it_but_not_lambdas() {
2994 let mut syms = vec![
2995 skeleton_row(
2996 "factory",
2997 None,
2998 "function",
2999 "create_app",
3000 "def create_app()",
3001 (1, 9),
3002 Some((2, 9)),
3003 ),
3004 skeleton_row(
3005 "hello",
3006 Some("factory"),
3007 "function",
3008 "hello",
3009 "def hello()",
3010 (4, 5),
3011 Some((5, 5)),
3012 ),
3013 skeleton_row(
3014 "lambda",
3015 Some("factory"),
3016 "function",
3017 "lambda_7",
3018 "lambda v: v",
3019 (7, 7),
3020 None,
3021 ),
3022 ];
3023 for symbol in &mut syms {
3024 symbol.language = "python".into();
3025 }
3026
3027 let skeleton = format_file_skeleton("app.py", &syms, Some(9), 0);
3028
3029 assert!(
3030 skeleton.contains("def create_app() { /* 8 lines hidden: L2-L9; defines `hello` */ }"),
3031 "{skeleton}"
3032 );
3033
3034 for symbol in &mut syms {
3035 symbol.language = "cpp".into();
3036 }
3037 let cpp = format_file_skeleton("app.cpp", &syms, Some(9), 0);
3038 assert!(!cpp.contains("defines"), "{cpp}");
3039 syms[1].test_container = true;
3040 let cpp_section = format_file_skeleton("app.cpp", &syms, Some(9), 0);
3041 assert!(cpp_section.contains("defines `hello`"), "{cpp_section}");
3042 }
3043
3044 #[test]
3045 fn skeleton_keeps_plain_fields_and_function_locals_unchanged() {
3046 let syms = vec![
3047 skeleton_row(
3048 "cfg",
3049 None,
3050 "struct",
3051 "Config",
3052 "pub struct Config",
3053 (1, 3),
3054 None,
3055 ),
3056 skeleton_row(
3057 "retries",
3058 Some("cfg"),
3059 "field",
3060 "retries",
3061 "pub retries: u32",
3062 (2, 2),
3063 None,
3064 ),
3065 skeleton_row(
3066 "run",
3067 None,
3068 "function",
3069 "run",
3070 "pub fn run()",
3071 (5, 9),
3072 Some((6, 8)),
3073 ),
3074 skeleton_row(
3075 "tmp",
3076 Some("run"),
3077 "variable",
3078 "tmp",
3079 "let tmp",
3080 (7, 7),
3081 None,
3082 ),
3083 ];
3084
3085 assert_eq!(
3086 format_file_skeleton("src/lib.rs", &syms, Some(9), 0),
3087 "// File: src/lib.rs (Lines 1-9)\n\
3088 \n\
3089 pub struct Config {\n\
3090 \x20 pub retries: u32; // L2-2\n\
3091 } // L1-3\n\
3092 \n\
3093 pub fn run() { /* 3 lines hidden: L6-L8 */ }\n"
3094 );
3095 }
3096
3097 #[test]
3098 fn skeleton_marks_an_event_row_whose_signature_does_not_spell_it() {
3099 let syms = vec![
3100 skeleton_row(
3101 "cls",
3102 None,
3103 "class",
3104 "ColumnViewAttached",
3105 "class ColumnViewAttached : public QObject",
3106 (11, 52),
3107 None,
3108 ),
3109 skeleton_row(
3110 "sig",
3111 Some("cls"),
3112 "event",
3113 "indexChanged",
3114 "void indexChanged()",
3115 (47, 47),
3116 None,
3117 ),
3118 skeleton_row(
3119 "qml",
3120 None,
3121 "event",
3122 "clicked",
3123 "signal clicked()",
3124 (60, 60),
3125 None,
3126 ),
3127 ];
3128
3129 assert_eq!(
3130 format_file_skeleton("src/columnview.h", &syms, Some(60), 0),
3131 "// File: src/columnview.h (Lines 1-60)\n\
3132 \n\
3133 class ColumnViewAttached : public QObject {\n\
3134 \x20 void indexChanged(); // event L47-47\n\
3135 } // L11-52\n\
3136 \n\
3137 signal clicked(); // L60-60\n"
3138 );
3139 }
3140}