1use crate::cli::OutputFormat;
4use crate::core::cache::FileCache;
5use crate::core::walker::FileInfo;
6use crate::formatters::{create_formatter, DigestData};
7use crate::utils::file_ext::FileType;
8use anyhow::Result;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use tracing::warn;
13
14#[derive(Debug, Clone)]
16pub struct ContextOptions {
17 pub max_tokens: Option<usize>,
19 pub include_tree: bool,
21 pub include_stats: bool,
23 pub group_by_type: bool,
25 pub sort_by_priority: bool,
27 pub file_header_template: String,
29 pub doc_header_template: String,
31 pub include_toc: bool,
33 pub enhanced_context: bool,
35}
36
37impl ContextOptions {
38 pub fn from_config(config: &crate::cli::Config) -> Result<Self> {
40 Ok(ContextOptions {
41 max_tokens: config.get_effective_context_tokens(),
42 include_tree: true,
43 include_stats: true,
44 group_by_type: false,
45 sort_by_priority: true,
46 file_header_template: "## {path}".to_string(),
47 doc_header_template: "# Code Context: {directory}".to_string(),
48 include_toc: true,
49 enhanced_context: config.enhanced_context,
50 })
51 }
52}
53
54impl Default for ContextOptions {
55 fn default() -> Self {
56 ContextOptions {
57 max_tokens: None,
58 include_tree: true,
59 include_stats: true,
60 group_by_type: false,
61 sort_by_priority: true,
62 file_header_template: "## {path}".to_string(),
63 doc_header_template: "# Code Context: {directory}".to_string(),
64 include_toc: true,
65 enhanced_context: false,
66 }
67 }
68}
69
70fn estimate_output_size(files: &[FileInfo], options: &ContextOptions, cache: &FileCache) -> usize {
72 let mut size = 0;
73
74 if !options.doc_header_template.is_empty() {
76 size += options.doc_header_template.len() + 50; }
78
79 if options.include_stats {
81 size += 500; size += files.len() * 50; }
84
85 if options.include_tree {
87 size += 100; size += files.len() * 100; }
90
91 if options.include_toc {
93 size += 50; size += files.len() * 100; }
96
97 for file in files {
99 size +=
101 options.file_header_template.len() + file.relative_path.to_string_lossy().len() + 20;
102
103 if let Ok(content) = cache.get_or_load(&file.path) {
105 size += content.len() + 20; } else {
107 size += file.size as usize; }
109 }
110
111 size + (size / 5)
113}
114
115pub fn generate_markdown(
117 files: Vec<FileInfo>,
118 options: ContextOptions,
119 cache: Arc<FileCache>,
120) -> Result<String> {
121 let mut output = create_output_buffer(&files, &options, &cache);
122
123 add_document_header(&mut output, &options);
124 add_statistics_section(&mut output, &files, &options);
125 add_file_tree_section(&mut output, &files, &options);
126
127 let sorted_files = sort_files_by_priority(files, &options);
128 add_table_of_contents(&mut output, &sorted_files, &options);
129 add_file_contents(&mut output, sorted_files, &options, &cache)?;
130
131 Ok(output)
132}
133
134fn create_output_buffer(
137 files: &[FileInfo],
138 options: &ContextOptions,
139 cache: &Arc<FileCache>,
140) -> String {
141 let estimated_size = estimate_output_size(files, options, cache);
142 String::with_capacity(estimated_size)
143}
144
145fn add_document_header(output: &mut String, options: &ContextOptions) {
146 if !options.doc_header_template.is_empty() {
147 let header = options.doc_header_template.replace("{directory}", ".");
148 output.push_str(&header);
149 output.push_str("\n\n");
150 }
151}
152
153fn add_statistics_section(output: &mut String, files: &[FileInfo], options: &ContextOptions) {
154 if options.include_stats {
155 let stats = generate_statistics(files);
156 output.push_str(&stats);
157 output.push_str("\n\n");
158 }
159}
160
161fn add_file_tree_section(output: &mut String, files: &[FileInfo], options: &ContextOptions) {
162 if options.include_tree {
163 let tree = generate_file_tree(files, options);
164 output.push_str("## File Structure\n\n");
165 output.push_str("```\n");
166 output.push_str(&tree);
167 output.push_str("```\n\n");
168 }
169}
170
171fn sort_files_by_priority(mut files: Vec<FileInfo>, options: &ContextOptions) -> Vec<FileInfo> {
172 if options.sort_by_priority {
173 files.sort_by(|a, b| {
174 b.priority
175 .partial_cmp(&a.priority)
176 .unwrap_or(std::cmp::Ordering::Equal)
177 .then_with(|| a.relative_path.cmp(&b.relative_path))
178 });
179 }
180 files
181}
182
183fn add_table_of_contents(output: &mut String, files: &[FileInfo], options: &ContextOptions) {
184 if options.include_toc {
185 output.push_str("## Table of Contents\n\n");
186 for file in files {
187 add_toc_entry(output, file);
188 }
189 output.push('\n');
190 }
191}
192
193fn add_toc_entry(output: &mut String, file: &FileInfo) {
194 let anchor = path_to_anchor(&file.relative_path);
195 output.push_str(&format!(
196 "- [{path}](#{anchor})\n",
197 path = file.relative_path.display(),
198 anchor = anchor
199 ));
200}
201
202fn add_file_contents(
203 output: &mut String,
204 files: Vec<FileInfo>,
205 options: &ContextOptions,
206 cache: &Arc<FileCache>,
207) -> Result<()> {
208 if options.group_by_type {
209 add_grouped_files(output, files, options, cache)
210 } else {
211 add_ungrouped_files(output, files, options, cache)
212 }
213}
214
215fn add_grouped_files(
216 output: &mut String,
217 files: Vec<FileInfo>,
218 options: &ContextOptions,
219 cache: &Arc<FileCache>,
220) -> Result<()> {
221 let grouped = group_files_by_type(files);
222 for (file_type, group_files) in grouped {
223 output.push_str(&format!("## {} Files\n\n", file_type_display(&file_type)));
224 for file in group_files {
225 append_file_content(output, &file, options, cache)?;
226 }
227 }
228 Ok(())
229}
230
231fn add_ungrouped_files(
232 output: &mut String,
233 files: Vec<FileInfo>,
234 options: &ContextOptions,
235 cache: &Arc<FileCache>,
236) -> Result<()> {
237 for file in files {
238 append_file_content(output, &file, options, cache)?;
239 }
240 Ok(())
241}
242
243pub fn generate_digest(
245 files: Vec<FileInfo>,
246 options: ContextOptions,
247 cache: Arc<FileCache>,
248 output_format: OutputFormat,
249 base_directory: &str,
250) -> Result<String> {
251 let mut formatter = create_formatter(output_format);
253
254 let data = DigestData {
256 files: &files,
257 options: &options,
258 cache: &cache,
259 base_directory,
260 };
261
262 formatter.render_header(&data)?;
264 formatter.render_statistics(&data)?;
265 formatter.render_file_tree(&data)?;
266 formatter.render_toc(&data)?;
267
268 for file in &files {
270 formatter.render_file_details(file, &data)?;
271 }
272
273 Ok(formatter.finalize())
275}
276
277fn append_file_content(
279 output: &mut String,
280 file: &FileInfo,
281 options: &ContextOptions,
282 cache: &FileCache,
283) -> Result<()> {
284 let content = load_file_content(file, cache)?;
285 add_file_header(output, file, options);
286 add_semantic_info(output, file);
287 add_file_body(output, &content, &file.file_type);
288 Ok(())
289}
290
291fn load_file_content(file: &FileInfo, cache: &FileCache) -> Result<String> {
292 match cache.get_or_load(&file.path) {
293 Ok(content) => Ok(content.to_string()),
294 Err(e) => {
295 warn!("Could not read file {}: {}", file.path.display(), e);
296 Ok(String::new())
297 }
298 }
299}
300
301fn add_file_header(output: &mut String, file: &FileInfo, options: &ContextOptions) {
302 let path_with_metadata = format_path_with_metadata(file, options);
303 let header = options
304 .file_header_template
305 .replace("{path}", &path_with_metadata);
306 output.push_str(&header);
307 output.push_str("\n\n");
308}
309
310pub fn format_path_with_metadata(file: &FileInfo, options: &ContextOptions) -> String {
311 if options.enhanced_context {
312 format!(
313 "{} ({}, {})",
314 file.relative_path.display(),
315 format_size(file.size),
316 file_type_display(&file.file_type)
317 )
318 } else {
319 file.relative_path.display().to_string()
320 }
321}
322
323fn add_semantic_info(output: &mut String, file: &FileInfo) {
324 add_imports_info(output, &file.imports);
325 add_imported_by_info(output, &file.imported_by);
326 add_function_calls_info(output, &file.function_calls);
327 add_type_references_info(output, &file.type_references);
328}
329
330fn add_imports_info(output: &mut String, imports: &[PathBuf]) {
331 if !imports.is_empty() {
332 output.push_str("Imports: ");
333 let names = format_import_names(imports);
334 output.push_str(&format!("{}\n\n", names.join(", ")));
335 }
336}
337
338pub fn format_import_names(imports: &[PathBuf]) -> Vec<String> {
339 imports.iter().map(|p| format_import_name(p)).collect()
340}
341
342fn format_import_name(path: &Path) -> String {
343 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
344
345 if filename == "__init__.py" {
346 get_python_module_name(path)
347 } else {
348 strip_common_extensions(filename).to_string()
349 }
350}
351
352fn get_python_module_name(path: &Path) -> String {
353 path.parent()
354 .and_then(|parent| parent.file_name())
355 .and_then(|n| n.to_str())
356 .unwrap_or("unknown")
357 .to_string()
358}
359
360fn strip_common_extensions(filename: &str) -> &str {
361 filename
362 .strip_suffix(".py")
363 .or_else(|| filename.strip_suffix(".rs"))
364 .or_else(|| filename.strip_suffix(".js"))
365 .or_else(|| filename.strip_suffix(".ts"))
366 .unwrap_or(filename)
367}
368
369fn add_imported_by_info(output: &mut String, imported_by: &[PathBuf]) {
370 if !imported_by.is_empty() {
371 output.push_str("Imported by: ");
372 let names = format_imported_by_names(imported_by);
373 output.push_str(&format!("{}\n\n", names.join(", ")));
374 }
375}
376
377pub fn format_imported_by_names(imported_by: &[PathBuf]) -> Vec<String> {
378 imported_by
379 .iter()
380 .map(|p| {
381 p.file_name()
382 .and_then(|n| n.to_str())
383 .unwrap_or_else(|| p.to_str().unwrap_or("unknown"))
384 .to_string()
385 })
386 .collect()
387}
388
389fn add_function_calls_info(
390 output: &mut String,
391 calls: &[crate::core::semantic::analyzer::FunctionCall],
392) {
393 if !calls.is_empty() {
394 output.push_str("Function calls: ");
395 let names = format_function_call_names(calls);
396 output.push_str(&format!("{}\n\n", names.join(", ")));
397 }
398}
399
400fn format_function_call_names(
401 calls: &[crate::core::semantic::analyzer::FunctionCall],
402) -> Vec<String> {
403 calls
404 .iter()
405 .map(|fc| {
406 if let Some(module) = &fc.module {
407 format!("{}.{}", module, fc.name)
408 } else {
409 fc.name.clone()
410 }
411 })
412 .collect()
413}
414
415fn add_type_references_info(
416 output: &mut String,
417 refs: &[crate::core::semantic::analyzer::TypeReference],
418) {
419 if !refs.is_empty() {
420 output.push_str("Type references: ");
421 let names: Vec<String> = refs
422 .iter()
423 .map(|tr| {
424 if let Some(module) = &tr.module {
425 if module.ends_with(&format!("::{}", tr.name)) {
427 module.clone()
428 } else {
429 format!("{}.{}", module, tr.name)
430 }
431 } else {
432 tr.name.clone()
433 }
434 })
435 .collect();
436 output.push_str(&format!("{}\n\n", names.join(", ")));
437 }
438}
439
440fn add_file_body(output: &mut String, content: &str, file_type: &FileType) {
441 let language = get_language_hint(file_type);
442 output.push_str(&format!("```{language}\n"));
443 output.push_str(content);
444 if !content.ends_with('\n') {
445 output.push('\n');
446 }
447 output.push_str("```\n\n");
448}
449
450pub fn generate_statistics(files: &[FileInfo]) -> String {
452 let mut stats = create_stats_buffer(files.len());
453 add_stats_header(&mut stats);
454 add_file_count(&mut stats, files.len());
455 add_total_size(&mut stats, calculate_total_size(files));
456 add_file_type_breakdown(&mut stats, count_file_types(files));
457 stats
458}
459
460fn create_stats_buffer(file_count: usize) -> String {
461 String::with_capacity(500 + file_count * 50)
462}
463
464fn add_stats_header(stats: &mut String) {
465 stats.push_str("## Statistics\n\n");
466}
467
468fn add_file_count(stats: &mut String, count: usize) {
469 stats.push_str(&format!("- Total files: {count}\n"));
470}
471
472fn add_total_size(stats: &mut String, size: u64) {
473 stats.push_str(&format!("- Total size: {} bytes\n", format_size(size)));
474}
475
476fn calculate_total_size(files: &[FileInfo]) -> u64 {
477 files.iter().map(|f| f.size).sum()
478}
479
480fn count_file_types(files: &[FileInfo]) -> Vec<(FileType, usize)> {
481 let mut type_counts: HashMap<FileType, usize> = HashMap::new();
482 for file in files {
483 *type_counts.entry(file.file_type.clone()).or_insert(0) += 1;
484 }
485 let mut types: Vec<_> = type_counts.into_iter().collect();
486 types.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
487 types
488}
489
490fn add_file_type_breakdown(stats: &mut String, types: Vec<(FileType, usize)>) {
491 stats.push_str("\n### Files by type:\n");
492 for (file_type, count) in types {
493 stats.push_str(&format!("- {}: {}\n", file_type_display(&file_type), count));
494 }
495}
496
497pub fn generate_file_tree(files: &[FileInfo], options: &ContextOptions) -> String {
499 use std::collections::{BTreeMap, HashMap};
500
501 #[derive(Default)]
502 struct TreeNode {
503 files: Vec<String>,
504 dirs: BTreeMap<String, TreeNode>,
505 }
506
507 let mut root = TreeNode::default();
508
509 let file_lookup: HashMap<String, &FileInfo> = files
511 .iter()
512 .map(|f| (f.relative_path.to_string_lossy().to_string(), f))
513 .collect();
514
515 for file in files {
517 let parts: Vec<_> = file
518 .relative_path
519 .components()
520 .map(|c| c.as_os_str().to_string_lossy().to_string())
521 .collect();
522
523 let mut current = &mut root;
524 for (i, part) in parts.iter().enumerate() {
525 if i == parts.len() - 1 {
526 current.files.push(part.clone());
528 } else {
529 current = current.dirs.entry(part.clone()).or_default();
531 }
532 }
533 }
534
535 #[allow(clippy::too_many_arguments)]
537 fn render_tree(
538 node: &TreeNode,
539 prefix: &str,
540 _is_last: bool,
541 current_path: &str,
542 file_lookup: &HashMap<String, &FileInfo>,
543 options: &ContextOptions,
544 ) -> String {
545 let estimated_size = (node.dirs.len() + node.files.len()) * 100;
547 let mut output = String::with_capacity(estimated_size);
548
549 let dir_count = node.dirs.len();
551 for (i, (name, child)) in node.dirs.iter().enumerate() {
552 let is_last_dir = i == dir_count - 1 && node.files.is_empty();
553 let connector = if is_last_dir {
554 "└── "
555 } else {
556 "├── "
557 };
558 let extension = if is_last_dir { " " } else { "│ " };
559
560 output.push_str(&format!("{prefix}{connector}{name}/\n"));
561 let child_path = if current_path.is_empty() {
562 name.clone()
563 } else {
564 format!("{current_path}/{name}")
565 };
566 output.push_str(&render_tree(
567 child,
568 &format!("{prefix}{extension}"),
569 is_last_dir,
570 &child_path,
571 file_lookup,
572 options,
573 ));
574 }
575
576 let file_count = node.files.len();
578 for (i, name) in node.files.iter().enumerate() {
579 let is_last_file = i == file_count - 1;
580 let connector = if is_last_file {
581 "└── "
582 } else {
583 "├── "
584 };
585
586 let file_path = if current_path.is_empty() {
587 name.clone()
588 } else {
589 format!("{current_path}/{name}")
590 };
591
592 let display_name = if options.enhanced_context {
594 if let Some(file_info) = file_lookup.get(&file_path) {
595 format!(
596 "{} ({}, {})",
597 name,
598 format_size(file_info.size),
599 file_type_display(&file_info.file_type)
600 )
601 } else {
602 name.clone()
603 }
604 } else {
605 name.clone()
606 };
607
608 output.push_str(&format!("{prefix}{connector}{display_name}\n"));
609 }
610
611 output
612 }
613
614 let mut output = String::with_capacity(files.len() * 100 + 10);
616 output.push_str(".\n");
617 output.push_str(&render_tree(&root, "", true, "", &file_lookup, options));
618 output
619}
620
621fn group_files_by_type(files: Vec<FileInfo>) -> Vec<(FileType, Vec<FileInfo>)> {
623 let mut groups: HashMap<FileType, Vec<FileInfo>> = HashMap::new();
624
625 for file in files {
626 groups.entry(file.file_type.clone()).or_default().push(file);
627 }
628
629 let mut result: Vec<_> = groups.into_iter().collect();
630 result.sort_by_key(|(file_type, _)| file_type_priority(file_type));
631 result
632}
633
634pub fn file_type_display(file_type: &FileType) -> &'static str {
636 match file_type {
637 FileType::Rust => "Rust",
638 FileType::Python => "Python",
639 FileType::JavaScript => "JavaScript",
640 FileType::TypeScript => "TypeScript",
641 FileType::Go => "Go",
642 FileType::Java => "Java",
643 FileType::Cpp => "C++",
644 FileType::C => "C",
645 FileType::CSharp => "C#",
646 FileType::Ruby => "Ruby",
647 FileType::Php => "PHP",
648 FileType::Swift => "Swift",
649 FileType::Kotlin => "Kotlin",
650 FileType::Scala => "Scala",
651 FileType::Haskell => "Haskell",
652 FileType::Dart => "Dart",
653 FileType::Lua => "Lua",
654 FileType::R => "R",
655 FileType::Julia => "Julia",
656 FileType::Elixir => "Elixir",
657 FileType::Elm => "Elm",
658 FileType::Markdown => "Markdown",
659 FileType::Json => "JSON",
660 FileType::Yaml => "YAML",
661 FileType::Toml => "TOML",
662 FileType::Xml => "XML",
663 FileType::Html => "HTML",
664 FileType::Css => "CSS",
665 FileType::Text => "Text",
666 FileType::Other => "Other",
667 }
668}
669
670pub fn get_language_hint(file_type: &FileType) -> &'static str {
672 match file_type {
673 FileType::Rust => "rust",
674 FileType::Python => "python",
675 FileType::JavaScript => "javascript",
676 FileType::TypeScript => "typescript",
677 FileType::Go => "go",
678 FileType::Java => "java",
679 FileType::Cpp => "cpp",
680 FileType::C => "c",
681 FileType::CSharp => "csharp",
682 FileType::Ruby => "ruby",
683 FileType::Php => "php",
684 FileType::Swift => "swift",
685 FileType::Kotlin => "kotlin",
686 FileType::Scala => "scala",
687 FileType::Haskell => "haskell",
688 FileType::Dart => "dart",
689 FileType::Lua => "lua",
690 FileType::R => "r",
691 FileType::Julia => "julia",
692 FileType::Elixir => "elixir",
693 FileType::Elm => "elm",
694 FileType::Markdown => "markdown",
695 FileType::Json => "json",
696 FileType::Yaml => "yaml",
697 FileType::Toml => "toml",
698 FileType::Xml => "xml",
699 FileType::Html => "html",
700 FileType::Css => "css",
701 FileType::Text => "text",
702 FileType::Other => "",
703 }
704}
705
706fn file_type_priority(file_type: &FileType) -> u8 {
708 match file_type {
709 FileType::Rust => 1,
710 FileType::Python => 2,
711 FileType::JavaScript => 3,
712 FileType::TypeScript => 3,
713 FileType::Go => 4,
714 FileType::Java => 5,
715 FileType::Cpp => 6,
716 FileType::C => 7,
717 FileType::CSharp => 8,
718 FileType::Ruby => 9,
719 FileType::Php => 10,
720 FileType::Swift => 11,
721 FileType::Kotlin => 12,
722 FileType::Scala => 13,
723 FileType::Haskell => 14,
724 FileType::Dart => 15,
725 FileType::Lua => 16,
726 FileType::R => 17,
727 FileType::Julia => 18,
728 FileType::Elixir => 19,
729 FileType::Elm => 20,
730 FileType::Markdown => 21,
731 FileType::Json => 22,
732 FileType::Yaml => 23,
733 FileType::Toml => 24,
734 FileType::Xml => 25,
735 FileType::Html => 26,
736 FileType::Css => 27,
737 FileType::Text => 28,
738 FileType::Other => 29,
739 }
740}
741
742pub fn path_to_anchor(path: &Path) -> String {
744 path.display()
745 .to_string()
746 .replace(['/', '\\', '.', ' '], "-")
747 .to_lowercase()
748}
749
750fn format_size(size: u64) -> String {
752 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
753 let mut size = size as f64;
754 let mut unit_index = 0;
755
756 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
757 size /= 1024.0;
758 unit_index += 1;
759 }
760
761 if unit_index == 0 {
762 format!("{} {}", size as u64, UNITS[unit_index])
763 } else {
764 format!("{:.2} {}", size, UNITS[unit_index])
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771 use std::path::PathBuf;
772
773 fn create_test_cache() -> Arc<FileCache> {
774 Arc::new(FileCache::new())
775 }
776
777 #[test]
778 fn test_format_size() {
779 assert_eq!(format_size(512), "512 B");
780 assert_eq!(format_size(1024), "1.00 KB");
781 assert_eq!(format_size(1536), "1.50 KB");
782 assert_eq!(format_size(1048576), "1.00 MB");
783 }
784
785 #[test]
786 fn test_path_to_anchor() {
787 assert_eq!(path_to_anchor(Path::new("src/main.rs")), "src-main-rs");
788 assert_eq!(path_to_anchor(Path::new("test file.txt")), "test-file-txt");
789 }
790
791 #[test]
792 fn test_file_type_display() {
793 assert_eq!(file_type_display(&FileType::Rust), "Rust");
794 assert_eq!(file_type_display(&FileType::Python), "Python");
795 }
796
797 #[test]
798 fn test_generate_statistics() {
799 let files = vec![
800 FileInfo {
801 path: PathBuf::from("test1.rs"),
802 relative_path: PathBuf::from("test1.rs"),
803 size: 100,
804 file_type: FileType::Rust,
805 priority: 1.0,
806 imports: Vec::new(),
807 imported_by: Vec::new(),
808 function_calls: Vec::new(),
809 type_references: Vec::new(),
810 exported_functions: Vec::new(),
811 },
812 FileInfo {
813 path: PathBuf::from("test2.py"),
814 relative_path: PathBuf::from("test2.py"),
815 size: 200,
816 file_type: FileType::Python,
817 priority: 0.9,
818 imports: Vec::new(),
819 imported_by: Vec::new(),
820 function_calls: Vec::new(),
821 type_references: Vec::new(),
822 exported_functions: Vec::new(),
823 },
824 ];
825
826 let stats = generate_statistics(&files);
827 assert!(stats.contains("Total files: 2"));
828 assert!(stats.contains("Total size: 300 B"));
829 assert!(stats.contains("Rust: 1"));
830 assert!(stats.contains("Python: 1"));
831 }
832
833 #[test]
834 fn test_generate_statistics_empty() {
835 let files = vec![];
836 let stats = generate_statistics(&files);
837 assert!(stats.contains("Total files: 0"));
838 assert!(stats.contains("Total size: 0 B"));
839 }
840
841 #[test]
842 fn test_generate_statistics_large_files() {
843 let files = vec![
844 FileInfo {
845 path: PathBuf::from("large.rs"),
846 relative_path: PathBuf::from("large.rs"),
847 size: 2_000_000, file_type: FileType::Rust,
849 priority: 1.0,
850 imports: Vec::new(),
851 imported_by: Vec::new(),
852 function_calls: Vec::new(),
853 type_references: Vec::new(),
854 exported_functions: Vec::new(),
855 },
856 FileInfo {
857 path: PathBuf::from("huge.py"),
858 relative_path: PathBuf::from("huge.py"),
859 size: 50_000_000, file_type: FileType::Python,
861 priority: 0.9,
862 imports: Vec::new(),
863 imported_by: Vec::new(),
864 function_calls: Vec::new(),
865 type_references: Vec::new(),
866 exported_functions: Vec::new(),
867 },
868 ];
869
870 let stats = generate_statistics(&files);
871 assert!(stats.contains("Total files: 2"));
872 assert!(stats.contains("MB bytes")); assert!(stats.contains("Python: 1"));
874 assert!(stats.contains("Rust: 1"));
875 }
876
877 #[test]
878 fn test_generate_file_tree_with_grouping() {
879 let files = vec![
880 FileInfo {
881 path: PathBuf::from("src/main.rs"),
882 relative_path: PathBuf::from("src/main.rs"),
883 size: 1000,
884 file_type: FileType::Rust,
885 priority: 1.5,
886 imports: Vec::new(),
887 imported_by: Vec::new(),
888 function_calls: Vec::new(),
889 type_references: Vec::new(),
890 exported_functions: Vec::new(),
891 },
892 FileInfo {
893 path: PathBuf::from("src/lib.rs"),
894 relative_path: PathBuf::from("src/lib.rs"),
895 size: 2000,
896 file_type: FileType::Rust,
897 priority: 1.2,
898 imports: Vec::new(),
899 imported_by: Vec::new(),
900 function_calls: Vec::new(),
901 type_references: Vec::new(),
902 exported_functions: Vec::new(),
903 },
904 FileInfo {
905 path: PathBuf::from("tests/test.rs"),
906 relative_path: PathBuf::from("tests/test.rs"),
907 size: 500,
908 file_type: FileType::Rust,
909 priority: 0.8,
910 imports: Vec::new(),
911 imported_by: Vec::new(),
912 function_calls: Vec::new(),
913 type_references: Vec::new(),
914 exported_functions: Vec::new(),
915 },
916 ];
917
918 let options = ContextOptions::default();
919 let tree = generate_file_tree(&files, &options);
920 assert!(tree.contains("src/"));
921 assert!(tree.contains("tests/"));
922 assert!(tree.contains("main.rs"));
923 assert!(tree.contains("lib.rs"));
924 assert!(tree.contains("test.rs"));
925 }
926
927 #[test]
928 fn test_context_options_from_config() {
929 use crate::cli::Config;
930 use tempfile::TempDir;
931
932 let temp_dir = TempDir::new().unwrap();
933 let config = Config {
934 paths: Some(vec![temp_dir.path().to_path_buf()]),
935 max_tokens: Some(100000),
936 ..Config::default()
937 };
938
939 let options = ContextOptions::from_config(&config).unwrap();
940 assert_eq!(options.max_tokens, Some(100000));
941 assert!(options.include_tree);
942 assert!(options.include_stats);
943 assert!(!options.group_by_type); }
945
946 #[test]
947 fn test_generate_markdown_structure_headers() {
948 let files = vec![];
949
950 let options = ContextOptions {
951 max_tokens: None,
952 include_tree: true,
953 include_stats: true,
954 group_by_type: true,
955 sort_by_priority: true,
956 file_header_template: "## {path}".to_string(),
957 doc_header_template: "# Code Context".to_string(),
958 include_toc: true,
959 enhanced_context: false,
960 };
961
962 let cache = create_test_cache();
963 let markdown = generate_markdown(files, options, cache).unwrap();
964
965 assert!(markdown.contains("# Code Context"));
967 assert!(markdown.contains("## Statistics"));
968 }
969
970 #[test]
971 fn test_enhanced_tree_generation_with_metadata() {
972 use crate::core::walker::FileInfo;
973 use crate::utils::file_ext::FileType;
974 use std::path::PathBuf;
975
976 let files = vec![
977 FileInfo {
978 path: PathBuf::from("src/main.rs"),
979 relative_path: PathBuf::from("src/main.rs"),
980 size: 145,
981 file_type: FileType::Rust,
982 priority: 1.5,
983 imports: Vec::new(),
984 imported_by: Vec::new(),
985 function_calls: Vec::new(),
986 type_references: Vec::new(),
987 exported_functions: Vec::new(),
988 },
989 FileInfo {
990 path: PathBuf::from("src/lib.rs"),
991 relative_path: PathBuf::from("src/lib.rs"),
992 size: 89,
993 file_type: FileType::Rust,
994 priority: 1.2,
995 imports: Vec::new(),
996 imported_by: Vec::new(),
997 function_calls: Vec::new(),
998 type_references: Vec::new(),
999 exported_functions: Vec::new(),
1000 },
1001 ];
1002
1003 let options = ContextOptions {
1004 max_tokens: None,
1005 include_tree: true,
1006 include_stats: true,
1007 group_by_type: false,
1008 sort_by_priority: true,
1009 file_header_template: "## {path}".to_string(),
1010 doc_header_template: "# Code Context".to_string(),
1011 include_toc: true,
1012 enhanced_context: true,
1013 };
1014
1015 let cache = create_test_cache();
1016 let markdown = generate_markdown(files, options, cache).unwrap();
1017
1018 assert!(markdown.contains("main.rs (145 B, Rust)"));
1020 assert!(markdown.contains("lib.rs (89 B, Rust)"));
1021 }
1022
1023 #[test]
1024 fn test_enhanced_file_headers_with_metadata() {
1025 use crate::core::walker::FileInfo;
1026 use crate::utils::file_ext::FileType;
1027 use std::path::PathBuf;
1028
1029 let files = vec![FileInfo {
1030 path: PathBuf::from("src/main.rs"),
1031 relative_path: PathBuf::from("src/main.rs"),
1032 size: 145,
1033 file_type: FileType::Rust,
1034 priority: 1.5,
1035 imports: Vec::new(),
1036 imported_by: Vec::new(),
1037 function_calls: Vec::new(),
1038 type_references: Vec::new(),
1039 exported_functions: Vec::new(),
1040 }];
1041
1042 let options = ContextOptions {
1043 max_tokens: None,
1044 include_tree: true,
1045 include_stats: true,
1046 group_by_type: false,
1047 sort_by_priority: true,
1048 file_header_template: "## {path}".to_string(),
1049 doc_header_template: "# Code Context".to_string(),
1050 include_toc: true,
1051 enhanced_context: true,
1052 };
1053
1054 let cache = create_test_cache();
1055 let markdown = generate_markdown(files, options, cache).unwrap();
1056
1057 assert!(markdown.contains("## src/main.rs (145 B, Rust)"));
1059 }
1060
1061 #[test]
1062 fn test_basic_mode_unchanged() {
1063 use crate::core::walker::FileInfo;
1064 use crate::utils::file_ext::FileType;
1065 use std::path::PathBuf;
1066
1067 let files = vec![FileInfo {
1068 path: PathBuf::from("src/main.rs"),
1069 relative_path: PathBuf::from("src/main.rs"),
1070 size: 145,
1071 file_type: FileType::Rust,
1072 priority: 1.5,
1073 imports: Vec::new(),
1074 imported_by: Vec::new(),
1075 function_calls: Vec::new(),
1076 type_references: Vec::new(),
1077 exported_functions: Vec::new(),
1078 }];
1079
1080 let options = ContextOptions {
1081 max_tokens: None,
1082 include_tree: true,
1083 include_stats: true,
1084 group_by_type: false,
1085 sort_by_priority: true,
1086 file_header_template: "## {path}".to_string(),
1087 doc_header_template: "# Code Context".to_string(),
1088 include_toc: true,
1089 enhanced_context: false,
1090 };
1091
1092 let cache = create_test_cache();
1093 let markdown = generate_markdown(files, options, cache).unwrap();
1094
1095 assert!(markdown.contains("## src/main.rs"));
1097 assert!(!markdown.contains("## src/main.rs (145 B, Rust)"));
1098 assert!(markdown.contains("main.rs") && !markdown.contains("main.rs (145 B, Rust)"));
1099 }
1100}