context_creator/core/
context_builder.rs

1//! Context creation functionality for LLM consumption
2
3use 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/// Options for generating context for LLM consumption
15#[derive(Debug, Clone)]
16pub struct ContextOptions {
17    /// Maximum tokens allowed in the output
18    pub max_tokens: Option<usize>,
19    /// Include file tree in output
20    pub include_tree: bool,
21    /// Include token count statistics
22    pub include_stats: bool,
23    /// Group files by type
24    pub group_by_type: bool,
25    /// Sort files by priority
26    pub sort_by_priority: bool,
27    /// Template for file headers
28    pub file_header_template: String,
29    /// Template for the document header
30    pub doc_header_template: String,
31    /// Include table of contents
32    pub include_toc: bool,
33    /// Enable enhanced context with file metadata
34    pub enhanced_context: bool,
35}
36
37impl ContextOptions {
38    /// Create ContextOptions from CLI config
39    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
70/// Estimate the total size of the markdown output
71fn estimate_output_size(files: &[FileInfo], options: &ContextOptions, cache: &FileCache) -> usize {
72    let mut size = 0;
73
74    // Document header
75    if !options.doc_header_template.is_empty() {
76        size += options.doc_header_template.len() + 50; // Extra for replacements and newlines
77    }
78
79    // Statistics section
80    if options.include_stats {
81        size += 500; // Estimated size for stats
82        size += files.len() * 50; // For file type listing
83    }
84
85    // File tree
86    if options.include_tree {
87        size += 100; // Headers
88        size += files.len() * 100; // Estimated per-file in tree
89    }
90
91    // Table of contents
92    if options.include_toc {
93        size += 50; // Header
94        size += files.len() * 100; // Per-file TOC entry
95    }
96
97    // File contents
98    for file in files {
99        // Header template
100        size +=
101            options.file_header_template.len() + file.relative_path.to_string_lossy().len() + 20;
102
103        // File content + code fence
104        if let Ok(content) = cache.get_or_load(&file.path) {
105            size += content.len() + 20; // Content + fence markers
106        } else {
107            size += file.size as usize; // Fallback to file size
108        }
109    }
110
111    // Add 20% buffer for formatting and unexpected overhead
112    size + (size / 5)
113}
114
115/// Generate markdown from a list of files
116pub 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
134// Helper functions - each 10 lines or less
135
136fn 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
243/// Generate digest using the appropriate formatter
244pub 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    // Create formatter based on output format
252    let mut formatter = create_formatter(output_format);
253
254    // Create digest data
255    let data = DigestData {
256        files: &files,
257        options: &options,
258        cache: &cache,
259        base_directory,
260    };
261
262    // Render all sections
263    formatter.render_header(&data)?;
264    formatter.render_statistics(&data)?;
265    formatter.render_file_tree(&data)?;
266    formatter.render_toc(&data)?;
267
268    // Render file details
269    for file in &files {
270        formatter.render_file_details(file, &data)?;
271    }
272
273    // Finalize and return
274    Ok(formatter.finalize())
275}
276
277/// Append a single file's content to the output
278fn 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                    // Check if module already ends with the type name to avoid duplication
426                    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
450/// Generate statistics about the files
451pub 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
497/// Generate a file tree representation
498pub 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    // Create a lookup map from relative path to FileInfo for metadata
510    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    // Build tree structure
516    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                // File
527                current.files.push(part.clone());
528            } else {
529                // Directory
530                current = current.dirs.entry(part.clone()).or_default();
531            }
532        }
533    }
534
535    // Render tree
536    #[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        // Pre-allocate with estimated size
546        let estimated_size = (node.dirs.len() + node.files.len()) * 100;
547        let mut output = String::with_capacity(estimated_size);
548
549        // Render directories
550        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        // Render files
577        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            // Include metadata if enhanced context is enabled
593            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    // Pre-allocate output string
615    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
621/// Group files by their type
622fn 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
634/// Get display name for file type
635pub 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
670/// Get language hint for syntax highlighting
671pub 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
706/// Get priority for file type ordering
707fn 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
742/// Convert path to anchor-friendly string
743pub fn path_to_anchor(path: &Path) -> String {
744    path.display()
745        .to_string()
746        .replace(['/', '\\', '.', ' '], "-")
747        .to_lowercase()
748}
749
750/// Format file size in human-readable format
751fn 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, // 2MB
848                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, // 50MB
860                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")); // Just check that it's in MB
873        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); // Default is false according to implementation
944    }
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        // Check that main structure is present even with no files
966        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        // Should include file sizes and types in tree
1019        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        // Should include metadata in file headers
1058        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        // Should NOT include metadata - backward compatibility
1096        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}