1use crate::core::cache::FileCache;
4use crate::core::walker::FileInfo;
5use crate::utils::file_ext::FileType;
6use anyhow::Result;
7use std::collections::HashMap;
8use std::path::Path;
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
13pub struct ContextOptions {
14 pub max_tokens: Option<usize>,
16 pub include_tree: bool,
18 pub include_stats: bool,
20 pub group_by_type: bool,
22 pub sort_by_priority: bool,
24 pub file_header_template: String,
26 pub doc_header_template: String,
28 pub include_toc: bool,
30 pub enhanced_context: bool,
32}
33
34impl ContextOptions {
35 pub fn from_config(config: &crate::cli::Config) -> Result<Self> {
37 Ok(ContextOptions {
38 max_tokens: config.get_effective_context_tokens(),
39 include_tree: true,
40 include_stats: true,
41 group_by_type: false,
42 sort_by_priority: true,
43 file_header_template: "## {path}".to_string(),
44 doc_header_template: "# Code Context: {directory}".to_string(),
45 include_toc: true,
46 enhanced_context: config.enhanced_context,
47 })
48 }
49}
50
51impl Default for ContextOptions {
52 fn default() -> Self {
53 ContextOptions {
54 max_tokens: None,
55 include_tree: true,
56 include_stats: true,
57 group_by_type: false,
58 sort_by_priority: true,
59 file_header_template: "## {path}".to_string(),
60 doc_header_template: "# Code Context: {directory}".to_string(),
61 include_toc: true,
62 enhanced_context: false,
63 }
64 }
65}
66
67fn estimate_output_size(files: &[FileInfo], options: &ContextOptions, cache: &FileCache) -> usize {
69 let mut size = 0;
70
71 if !options.doc_header_template.is_empty() {
73 size += options.doc_header_template.len() + 50; }
75
76 if options.include_stats {
78 size += 500; size += files.len() * 50; }
81
82 if options.include_tree {
84 size += 100; size += files.len() * 100; }
87
88 if options.include_toc {
90 size += 50; size += files.len() * 100; }
93
94 for file in files {
96 size +=
98 options.file_header_template.len() + file.relative_path.to_string_lossy().len() + 20;
99
100 if let Ok(content) = cache.get_or_load(&file.path) {
102 size += content.len() + 20; } else {
104 size += file.size as usize; }
106 }
107
108 size + (size / 5)
110}
111
112pub fn generate_markdown(
114 files: Vec<FileInfo>,
115 options: ContextOptions,
116 cache: Arc<FileCache>,
117) -> Result<String> {
118 let estimated_size = estimate_output_size(&files, &options, &cache);
120 let mut output = String::with_capacity(estimated_size);
121
122 if !options.doc_header_template.is_empty() {
124 let header = options.doc_header_template.replace("{directory}", ".");
125 output.push_str(&header);
126 output.push_str("\n\n");
127 }
128
129 if options.include_stats {
131 let stats = generate_statistics(&files);
132 output.push_str(&stats);
133 output.push_str("\n\n");
134 }
135
136 if options.include_tree {
138 let tree = generate_file_tree(&files, &options);
139 output.push_str("## File Structure\n\n");
140 output.push_str("```\n");
141 output.push_str(&tree);
142 output.push_str("```\n\n");
143 }
144
145 let mut files = files;
147 if options.sort_by_priority {
148 files.sort_by(|a, b| {
149 b.priority
150 .partial_cmp(&a.priority)
151 .unwrap_or(std::cmp::Ordering::Equal)
152 .then_with(|| a.relative_path.cmp(&b.relative_path))
153 });
154 }
155
156 if options.include_toc {
158 output.push_str("## Table of Contents\n\n");
159 for file in &files {
160 let anchor = path_to_anchor(&file.relative_path);
161 output.push_str(&format!(
162 "- [{path}](#{anchor})\n",
163 path = file.relative_path.display(),
164 anchor = anchor
165 ));
166 }
167 output.push('\n');
168 }
169
170 if options.group_by_type {
172 let grouped = group_files_by_type(files);
173 for (file_type, group_files) in grouped {
174 output.push_str(&format!("## {} Files\n\n", file_type_display(&file_type)));
175 for file in group_files {
176 append_file_content(&mut output, &file, &options, &cache)?;
177 }
178 }
179 } else {
180 for file in files {
182 append_file_content(&mut output, &file, &options, &cache)?;
183 }
184 }
185
186 Ok(output)
187}
188
189fn append_file_content(
191 output: &mut String,
192 file: &FileInfo,
193 options: &ContextOptions,
194 cache: &FileCache,
195) -> Result<()> {
196 let content = match cache.get_or_load(&file.path) {
198 Ok(content) => content,
199 Err(e) => {
200 eprintln!(
201 "Warning: Could not read file {}: {}",
202 file.path.display(),
203 e
204 );
205 return Ok(());
206 }
207 };
208
209 let path_with_metadata = if options.enhanced_context {
211 format!(
212 "{} ({}, {})",
213 file.relative_path.display(),
214 format_size(file.size),
215 file_type_display(&file.file_type)
216 )
217 } else {
218 file.relative_path.display().to_string()
219 };
220
221 let header = options
222 .file_header_template
223 .replace("{path}", &path_with_metadata);
224 output.push_str(&header);
225 output.push_str("\n\n");
226
227 if !file.imports.is_empty() {
229 output.push_str("Imports: ");
230 let import_names: Vec<String> = file
231 .imports
232 .iter()
233 .map(|p| {
234 let filename = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
235
236 if filename == "__init__.py" {
238 p.parent()
239 .and_then(|parent| parent.file_name())
240 .and_then(|n| n.to_str())
241 .unwrap_or("unknown")
242 .to_string()
243 } else {
244 filename
246 .strip_suffix(".py")
247 .or_else(|| filename.strip_suffix(".rs"))
248 .or_else(|| filename.strip_suffix(".js"))
249 .or_else(|| filename.strip_suffix(".ts"))
250 .unwrap_or(filename)
251 .to_string()
252 }
253 })
254 .collect();
255 output.push_str(&format!("{}\n\n", import_names.join(", ")));
256 }
257
258 if !file.imported_by.is_empty() {
259 output.push_str("Imported by: ");
260 let imported_by_names: Vec<String> = file
261 .imported_by
262 .iter()
263 .map(|p| {
264 p.file_name()
265 .and_then(|n| n.to_str())
266 .unwrap_or_else(|| p.to_str().unwrap_or("unknown"))
267 .to_string()
268 })
269 .collect();
270 output.push_str(&format!("{}\n\n", imported_by_names.join(", ")));
271 }
272
273 if !file.function_calls.is_empty() {
274 output.push_str("Function calls: ");
275 let function_names: Vec<String> = file
276 .function_calls
277 .iter()
278 .map(|fc| {
279 if let Some(module) = &fc.module {
280 format!("{}.{}", module, fc.name)
281 } else {
282 fc.name.clone()
283 }
284 })
285 .collect();
286 output.push_str(&format!("{}\n\n", function_names.join(", ")));
287 }
288
289 if !file.type_references.is_empty() {
290 output.push_str("Type references: ");
291 let type_names: Vec<String> = file
292 .type_references
293 .iter()
294 .map(|tr| {
295 if let Some(module) = &tr.module {
296 format!("{}.{}", module, tr.name)
297 } else {
298 tr.name.clone()
299 }
300 })
301 .collect();
302 output.push_str(&format!("{}\n\n", type_names.join(", ")));
303 }
304
305 let language = get_language_hint(&file.file_type);
307 output.push_str(&format!("```{language}\n"));
308 output.push_str(&content);
309 if !content.ends_with('\n') {
310 output.push('\n');
311 }
312 output.push_str("```\n\n");
313
314 Ok(())
315}
316
317fn generate_statistics(files: &[FileInfo]) -> String {
319 let total_files = files.len();
320 let total_size: u64 = files.iter().map(|f| f.size).sum();
321
322 let mut type_counts: HashMap<FileType, usize> = HashMap::new();
324 for file in files {
325 *type_counts.entry(file.file_type.clone()).or_insert(0) += 1;
326 }
327
328 let mut stats = String::with_capacity(500 + type_counts.len() * 50);
330 stats.push_str("## Statistics\n\n");
331 stats.push_str(&format!("- Total files: {total_files}\n"));
332 stats.push_str(&format!(
333 "- Total size: {} bytes\n",
334 format_size(total_size)
335 ));
336 stats.push_str("\n### Files by type:\n");
337
338 let mut types: Vec<_> = type_counts.into_iter().collect();
339 types.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
340
341 for (file_type, count) in types {
342 stats.push_str(&format!("- {}: {}\n", file_type_display(&file_type), count));
343 }
344
345 stats
346}
347
348fn generate_file_tree(files: &[FileInfo], options: &ContextOptions) -> String {
350 use std::collections::{BTreeMap, HashMap};
351
352 #[derive(Default)]
353 struct TreeNode {
354 files: Vec<String>,
355 dirs: BTreeMap<String, TreeNode>,
356 }
357
358 let mut root = TreeNode::default();
359
360 let file_lookup: HashMap<String, &FileInfo> = files
362 .iter()
363 .map(|f| (f.relative_path.to_string_lossy().to_string(), f))
364 .collect();
365
366 for file in files {
368 let parts: Vec<_> = file
369 .relative_path
370 .components()
371 .map(|c| c.as_os_str().to_string_lossy().to_string())
372 .collect();
373
374 let mut current = &mut root;
375 for (i, part) in parts.iter().enumerate() {
376 if i == parts.len() - 1 {
377 current.files.push(part.clone());
379 } else {
380 current = current.dirs.entry(part.clone()).or_default();
382 }
383 }
384 }
385
386 #[allow(clippy::too_many_arguments)]
388 fn render_tree(
389 node: &TreeNode,
390 prefix: &str,
391 _is_last: bool,
392 current_path: &str,
393 file_lookup: &HashMap<String, &FileInfo>,
394 options: &ContextOptions,
395 ) -> String {
396 let estimated_size = (node.dirs.len() + node.files.len()) * 100;
398 let mut output = String::with_capacity(estimated_size);
399
400 let dir_count = node.dirs.len();
402 for (i, (name, child)) in node.dirs.iter().enumerate() {
403 let is_last_dir = i == dir_count - 1 && node.files.is_empty();
404 let connector = if is_last_dir {
405 "└── "
406 } else {
407 "├── "
408 };
409 let extension = if is_last_dir { " " } else { "│ " };
410
411 output.push_str(&format!("{prefix}{connector}{name}/\n"));
412 let child_path = if current_path.is_empty() {
413 name.clone()
414 } else {
415 format!("{current_path}/{name}")
416 };
417 output.push_str(&render_tree(
418 child,
419 &format!("{prefix}{extension}"),
420 is_last_dir,
421 &child_path,
422 file_lookup,
423 options,
424 ));
425 }
426
427 let file_count = node.files.len();
429 for (i, name) in node.files.iter().enumerate() {
430 let is_last_file = i == file_count - 1;
431 let connector = if is_last_file {
432 "└── "
433 } else {
434 "├── "
435 };
436
437 let file_path = if current_path.is_empty() {
438 name.clone()
439 } else {
440 format!("{current_path}/{name}")
441 };
442
443 let display_name = if options.enhanced_context {
445 if let Some(file_info) = file_lookup.get(&file_path) {
446 format!(
447 "{} ({}, {})",
448 name,
449 format_size(file_info.size),
450 file_type_display(&file_info.file_type)
451 )
452 } else {
453 name.clone()
454 }
455 } else {
456 name.clone()
457 };
458
459 output.push_str(&format!("{prefix}{connector}{display_name}\n"));
460 }
461
462 output
463 }
464
465 let mut output = String::with_capacity(files.len() * 100 + 10);
467 output.push_str(".\n");
468 output.push_str(&render_tree(&root, "", true, "", &file_lookup, options));
469 output
470}
471
472fn group_files_by_type(files: Vec<FileInfo>) -> Vec<(FileType, Vec<FileInfo>)> {
474 let mut groups: HashMap<FileType, Vec<FileInfo>> = HashMap::new();
475
476 for file in files {
477 groups.entry(file.file_type.clone()).or_default().push(file);
478 }
479
480 let mut result: Vec<_> = groups.into_iter().collect();
481 result.sort_by_key(|(file_type, _)| file_type_priority(file_type));
482 result
483}
484
485fn file_type_display(file_type: &FileType) -> &'static str {
487 match file_type {
488 FileType::Rust => "Rust",
489 FileType::Python => "Python",
490 FileType::JavaScript => "JavaScript",
491 FileType::TypeScript => "TypeScript",
492 FileType::Go => "Go",
493 FileType::Java => "Java",
494 FileType::Cpp => "C++",
495 FileType::C => "C",
496 FileType::CSharp => "C#",
497 FileType::Ruby => "Ruby",
498 FileType::Php => "PHP",
499 FileType::Swift => "Swift",
500 FileType::Kotlin => "Kotlin",
501 FileType::Scala => "Scala",
502 FileType::Haskell => "Haskell",
503 FileType::Dart => "Dart",
504 FileType::Lua => "Lua",
505 FileType::R => "R",
506 FileType::Julia => "Julia",
507 FileType::Elixir => "Elixir",
508 FileType::Elm => "Elm",
509 FileType::Markdown => "Markdown",
510 FileType::Json => "JSON",
511 FileType::Yaml => "YAML",
512 FileType::Toml => "TOML",
513 FileType::Xml => "XML",
514 FileType::Html => "HTML",
515 FileType::Css => "CSS",
516 FileType::Text => "Text",
517 FileType::Other => "Other",
518 }
519}
520
521fn get_language_hint(file_type: &FileType) -> &'static str {
523 match file_type {
524 FileType::Rust => "rust",
525 FileType::Python => "python",
526 FileType::JavaScript => "javascript",
527 FileType::TypeScript => "typescript",
528 FileType::Go => "go",
529 FileType::Java => "java",
530 FileType::Cpp => "cpp",
531 FileType::C => "c",
532 FileType::CSharp => "csharp",
533 FileType::Ruby => "ruby",
534 FileType::Php => "php",
535 FileType::Swift => "swift",
536 FileType::Kotlin => "kotlin",
537 FileType::Scala => "scala",
538 FileType::Haskell => "haskell",
539 FileType::Dart => "dart",
540 FileType::Lua => "lua",
541 FileType::R => "r",
542 FileType::Julia => "julia",
543 FileType::Elixir => "elixir",
544 FileType::Elm => "elm",
545 FileType::Markdown => "markdown",
546 FileType::Json => "json",
547 FileType::Yaml => "yaml",
548 FileType::Toml => "toml",
549 FileType::Xml => "xml",
550 FileType::Html => "html",
551 FileType::Css => "css",
552 FileType::Text => "text",
553 FileType::Other => "",
554 }
555}
556
557fn file_type_priority(file_type: &FileType) -> u8 {
559 match file_type {
560 FileType::Rust => 1,
561 FileType::Python => 2,
562 FileType::JavaScript => 3,
563 FileType::TypeScript => 3,
564 FileType::Go => 4,
565 FileType::Java => 5,
566 FileType::Cpp => 6,
567 FileType::C => 7,
568 FileType::CSharp => 8,
569 FileType::Ruby => 9,
570 FileType::Php => 10,
571 FileType::Swift => 11,
572 FileType::Kotlin => 12,
573 FileType::Scala => 13,
574 FileType::Haskell => 14,
575 FileType::Dart => 15,
576 FileType::Lua => 16,
577 FileType::R => 17,
578 FileType::Julia => 18,
579 FileType::Elixir => 19,
580 FileType::Elm => 20,
581 FileType::Markdown => 21,
582 FileType::Json => 22,
583 FileType::Yaml => 23,
584 FileType::Toml => 24,
585 FileType::Xml => 25,
586 FileType::Html => 26,
587 FileType::Css => 27,
588 FileType::Text => 28,
589 FileType::Other => 29,
590 }
591}
592
593fn path_to_anchor(path: &Path) -> String {
595 path.display()
596 .to_string()
597 .replace(['/', '\\', '.', ' '], "-")
598 .to_lowercase()
599}
600
601fn format_size(size: u64) -> String {
603 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
604 let mut size = size as f64;
605 let mut unit_index = 0;
606
607 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
608 size /= 1024.0;
609 unit_index += 1;
610 }
611
612 if unit_index == 0 {
613 format!("{} {}", size as u64, UNITS[unit_index])
614 } else {
615 format!("{:.2} {}", size, UNITS[unit_index])
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use std::path::PathBuf;
623
624 fn create_test_cache() -> Arc<FileCache> {
625 Arc::new(FileCache::new())
626 }
627
628 #[test]
629 fn test_format_size() {
630 assert_eq!(format_size(512), "512 B");
631 assert_eq!(format_size(1024), "1.00 KB");
632 assert_eq!(format_size(1536), "1.50 KB");
633 assert_eq!(format_size(1048576), "1.00 MB");
634 }
635
636 #[test]
637 fn test_path_to_anchor() {
638 assert_eq!(path_to_anchor(Path::new("src/main.rs")), "src-main-rs");
639 assert_eq!(path_to_anchor(Path::new("test file.txt")), "test-file-txt");
640 }
641
642 #[test]
643 fn test_file_type_display() {
644 assert_eq!(file_type_display(&FileType::Rust), "Rust");
645 assert_eq!(file_type_display(&FileType::Python), "Python");
646 }
647
648 #[test]
649 fn test_generate_statistics() {
650 let files = vec![
651 FileInfo {
652 path: PathBuf::from("test1.rs"),
653 relative_path: PathBuf::from("test1.rs"),
654 size: 100,
655 file_type: FileType::Rust,
656 priority: 1.0,
657 imports: Vec::new(),
658 imported_by: Vec::new(),
659 function_calls: Vec::new(),
660 type_references: Vec::new(),
661 },
662 FileInfo {
663 path: PathBuf::from("test2.py"),
664 relative_path: PathBuf::from("test2.py"),
665 size: 200,
666 file_type: FileType::Python,
667 priority: 0.9,
668 imports: Vec::new(),
669 imported_by: Vec::new(),
670 function_calls: Vec::new(),
671 type_references: Vec::new(),
672 },
673 ];
674
675 let stats = generate_statistics(&files);
676 assert!(stats.contains("Total files: 2"));
677 assert!(stats.contains("Total size: 300 B"));
678 assert!(stats.contains("Rust: 1"));
679 assert!(stats.contains("Python: 1"));
680 }
681
682 #[test]
683 fn test_generate_statistics_empty() {
684 let files = vec![];
685 let stats = generate_statistics(&files);
686 assert!(stats.contains("Total files: 0"));
687 assert!(stats.contains("Total size: 0 B"));
688 }
689
690 #[test]
691 fn test_generate_statistics_large_files() {
692 let files = vec![
693 FileInfo {
694 path: PathBuf::from("large.rs"),
695 relative_path: PathBuf::from("large.rs"),
696 size: 2_000_000, file_type: FileType::Rust,
698 priority: 1.0,
699 imports: Vec::new(),
700 imported_by: Vec::new(),
701 function_calls: Vec::new(),
702 type_references: Vec::new(),
703 },
704 FileInfo {
705 path: PathBuf::from("huge.py"),
706 relative_path: PathBuf::from("huge.py"),
707 size: 50_000_000, file_type: FileType::Python,
709 priority: 0.9,
710 imports: Vec::new(),
711 imported_by: Vec::new(),
712 function_calls: Vec::new(),
713 type_references: Vec::new(),
714 },
715 ];
716
717 let stats = generate_statistics(&files);
718 assert!(stats.contains("Total files: 2"));
719 assert!(stats.contains("MB bytes")); assert!(stats.contains("Python: 1"));
721 assert!(stats.contains("Rust: 1"));
722 }
723
724 #[test]
725 fn test_generate_file_tree_with_grouping() {
726 let files = vec![
727 FileInfo {
728 path: PathBuf::from("src/main.rs"),
729 relative_path: PathBuf::from("src/main.rs"),
730 size: 1000,
731 file_type: FileType::Rust,
732 priority: 1.5,
733 imports: Vec::new(),
734 imported_by: Vec::new(),
735 function_calls: Vec::new(),
736 type_references: Vec::new(),
737 },
738 FileInfo {
739 path: PathBuf::from("src/lib.rs"),
740 relative_path: PathBuf::from("src/lib.rs"),
741 size: 2000,
742 file_type: FileType::Rust,
743 priority: 1.2,
744 imports: Vec::new(),
745 imported_by: Vec::new(),
746 function_calls: Vec::new(),
747 type_references: Vec::new(),
748 },
749 FileInfo {
750 path: PathBuf::from("tests/test.rs"),
751 relative_path: PathBuf::from("tests/test.rs"),
752 size: 500,
753 file_type: FileType::Rust,
754 priority: 0.8,
755 imports: Vec::new(),
756 imported_by: Vec::new(),
757 function_calls: Vec::new(),
758 type_references: Vec::new(),
759 },
760 ];
761
762 let options = ContextOptions::default();
763 let tree = generate_file_tree(&files, &options);
764 assert!(tree.contains("src/"));
765 assert!(tree.contains("tests/"));
766 assert!(tree.contains("main.rs"));
767 assert!(tree.contains("lib.rs"));
768 assert!(tree.contains("test.rs"));
769 }
770
771 #[test]
772 fn test_context_options_from_config() {
773 use crate::cli::Config;
774 use tempfile::TempDir;
775
776 let temp_dir = TempDir::new().unwrap();
777 let config = Config {
778 paths: Some(vec![temp_dir.path().to_path_buf()]),
779 max_tokens: Some(100000),
780 ..Config::default()
781 };
782
783 let options = ContextOptions::from_config(&config).unwrap();
784 assert_eq!(options.max_tokens, Some(100000));
785 assert!(options.include_tree);
786 assert!(options.include_stats);
787 assert!(!options.group_by_type); }
789
790 #[test]
791 fn test_generate_markdown_structure_headers() {
792 let files = vec![];
793
794 let options = ContextOptions {
795 max_tokens: None,
796 include_tree: true,
797 include_stats: true,
798 group_by_type: true,
799 sort_by_priority: true,
800 file_header_template: "## {path}".to_string(),
801 doc_header_template: "# Code Context".to_string(),
802 include_toc: true,
803 enhanced_context: false,
804 };
805
806 let cache = create_test_cache();
807 let markdown = generate_markdown(files, options, cache).unwrap();
808
809 assert!(markdown.contains("# Code Context"));
811 assert!(markdown.contains("## Statistics"));
812 }
813
814 #[test]
815 fn test_enhanced_tree_generation_with_metadata() {
816 use crate::core::walker::FileInfo;
817 use crate::utils::file_ext::FileType;
818 use std::path::PathBuf;
819
820 let files = vec![
821 FileInfo {
822 path: PathBuf::from("src/main.rs"),
823 relative_path: PathBuf::from("src/main.rs"),
824 size: 145,
825 file_type: FileType::Rust,
826 priority: 1.5,
827 imports: Vec::new(),
828 imported_by: Vec::new(),
829 function_calls: Vec::new(),
830 type_references: Vec::new(),
831 },
832 FileInfo {
833 path: PathBuf::from("src/lib.rs"),
834 relative_path: PathBuf::from("src/lib.rs"),
835 size: 89,
836 file_type: FileType::Rust,
837 priority: 1.2,
838 imports: Vec::new(),
839 imported_by: Vec::new(),
840 function_calls: Vec::new(),
841 type_references: Vec::new(),
842 },
843 ];
844
845 let options = ContextOptions {
846 max_tokens: None,
847 include_tree: true,
848 include_stats: true,
849 group_by_type: false,
850 sort_by_priority: true,
851 file_header_template: "## {path}".to_string(),
852 doc_header_template: "# Code Context".to_string(),
853 include_toc: true,
854 enhanced_context: true,
855 };
856
857 let cache = create_test_cache();
858 let markdown = generate_markdown(files, options, cache).unwrap();
859
860 assert!(markdown.contains("main.rs (145 B, Rust)"));
862 assert!(markdown.contains("lib.rs (89 B, Rust)"));
863 }
864
865 #[test]
866 fn test_enhanced_file_headers_with_metadata() {
867 use crate::core::walker::FileInfo;
868 use crate::utils::file_ext::FileType;
869 use std::path::PathBuf;
870
871 let files = vec![FileInfo {
872 path: PathBuf::from("src/main.rs"),
873 relative_path: PathBuf::from("src/main.rs"),
874 size: 145,
875 file_type: FileType::Rust,
876 priority: 1.5,
877 imports: Vec::new(),
878 imported_by: Vec::new(),
879 function_calls: Vec::new(),
880 type_references: Vec::new(),
881 }];
882
883 let options = ContextOptions {
884 max_tokens: None,
885 include_tree: true,
886 include_stats: true,
887 group_by_type: false,
888 sort_by_priority: true,
889 file_header_template: "## {path}".to_string(),
890 doc_header_template: "# Code Context".to_string(),
891 include_toc: true,
892 enhanced_context: true,
893 };
894
895 let cache = create_test_cache();
896 let markdown = generate_markdown(files, options, cache).unwrap();
897
898 assert!(markdown.contains("## src/main.rs (145 B, Rust)"));
900 }
901
902 #[test]
903 fn test_basic_mode_unchanged() {
904 use crate::core::walker::FileInfo;
905 use crate::utils::file_ext::FileType;
906 use std::path::PathBuf;
907
908 let files = vec![FileInfo {
909 path: PathBuf::from("src/main.rs"),
910 relative_path: PathBuf::from("src/main.rs"),
911 size: 145,
912 file_type: FileType::Rust,
913 priority: 1.5,
914 imports: Vec::new(),
915 imported_by: Vec::new(),
916 function_calls: Vec::new(),
917 type_references: Vec::new(),
918 }];
919
920 let options = ContextOptions {
921 max_tokens: None,
922 include_tree: true,
923 include_stats: true,
924 group_by_type: false,
925 sort_by_priority: true,
926 file_header_template: "## {path}".to_string(),
927 doc_header_template: "# Code Context".to_string(),
928 include_toc: true,
929 enhanced_context: false,
930 };
931
932 let cache = create_test_cache();
933 let markdown = generate_markdown(files, options, cache).unwrap();
934
935 assert!(markdown.contains("## src/main.rs"));
937 assert!(!markdown.contains("## src/main.rs (145 B, Rust)"));
938 assert!(markdown.contains("main.rs") && !markdown.contains("main.rs (145 B, Rust)"));
939 }
940}