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