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