Skip to main content

yek/
models.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::sync::OnceLock;
4
5use crate::category::FileCategory;
6
7/// Represents a processed file with its metadata and content
8#[derive(Debug, Serialize, Deserialize)]
9pub struct ProcessedFile {
10    /// Priority score for file ordering
11    pub priority: i32,
12    /// Index within the same priority group for stable sorting
13    pub file_index: usize,
14    /// Relative path from the repository root
15    pub rel_path: String,
16    /// File content as string
17    pub content: String,
18    /// File size in bytes
19    pub size_bytes: usize,
20    /// Token count (computed lazily with caching)
21    #[serde(skip)]
22    pub token_count: OnceLock<usize>,
23    /// Cached formatted content (for line numbers)
24    pub formatted_content: Option<String>,
25    /// File category for improved sorting and organization
26    pub category: FileCategory,
27}
28
29impl Clone for ProcessedFile {
30    fn clone(&self) -> Self {
31        Self {
32            priority: self.priority,
33            file_index: self.file_index,
34            rel_path: self.rel_path.clone(),
35            content: self.content.clone(),
36            size_bytes: self.size_bytes,
37            token_count: OnceLock::new(),
38            formatted_content: self.formatted_content.clone(),
39            category: self.category,
40        }
41    }
42}
43
44impl ProcessedFile {
45    /// Create a new ProcessedFile with basic information
46    pub fn new(rel_path: String, content: String, priority: i32, file_index: usize) -> Self {
47        let category = crate::category::categorize_file(&rel_path);
48        let size_bytes = content.len();
49        Self {
50            priority,
51            file_index,
52            rel_path,
53            content,
54            size_bytes,
55            token_count: OnceLock::new(),
56            formatted_content: None,
57            category,
58        }
59    }
60
61    /// Create a new ProcessedFile with explicit category
62    pub fn new_with_category(
63        rel_path: String,
64        content: String,
65        priority: i32,
66        file_index: usize,
67        category: FileCategory,
68    ) -> Self {
69        let size_bytes = content.len();
70        Self {
71            priority,
72            file_index,
73            rel_path,
74            content,
75            size_bytes,
76            token_count: OnceLock::new(),
77            formatted_content: None,
78            category,
79        }
80    }
81
82    /// Get token count, computing it lazily if not already computed
83    pub fn get_token_count(&self) -> usize {
84        *self.token_count.get_or_init(|| self.compute_token_count())
85    }
86
87    /// Get formatted content with line numbers if requested
88    pub fn get_formatted_content(&self, include_line_numbers: bool) -> &str {
89        if !include_line_numbers {
90            return &self.content;
91        }
92
93        self.formatted_content.as_deref().unwrap_or("")
94    }
95
96    /// Compute token count for the content
97    fn compute_token_count(&self) -> usize {
98        // If we have formatted content cached, use that for token counting
99        // as it represents the final output format
100        if let Some(ref formatted) = self.formatted_content {
101            crate::count_tokens(formatted)
102        } else {
103            // Only count tokens if we actually need them (lazy evaluation)
104            // This avoids expensive tokenization for files that won't be included
105            crate::count_tokens(&self.content)
106        }
107    }
108
109    /// Format content with line numbers
110    #[allow(dead_code)]
111    fn format_content_with_line_numbers(&self) -> String {
112        if self.content.is_empty() {
113            return String::new();
114        }
115
116        let lines: Vec<&str> = self.content.lines().collect();
117        let total_lines = lines.len();
118
119        // Calculate the width needed for the largest line number, with minimum width of 3
120        let width = if total_lines == 0 {
121            3
122        } else {
123            std::cmp::max(3, total_lines.to_string().len())
124        };
125
126        // Use String::with_capacity for better memory allocation
127        let mut result = String::with_capacity(self.content.len() + total_lines * (width + 3));
128
129        for (i, line) in lines.iter().enumerate() {
130            result.push_str(&format!("{:width$} | {}\n", i + 1, line, width = width));
131        }
132
133        // Remove trailing newline
134        if result.ends_with('\n') {
135            result.pop();
136        }
137
138        result
139    }
140
141    /// Get the size in the specified mode (bytes or tokens)
142    pub fn get_size(&self, token_mode: bool, include_line_numbers: bool) -> usize {
143        if token_mode {
144            self.get_token_count()
145        } else {
146            // Use formatted content size if line numbers are requested
147            if include_line_numbers {
148                self.get_formatted_content(true).len()
149            } else {
150                self.size_bytes
151            }
152        }
153    }
154
155    /// Check if file would exceed size limit
156    pub fn exceeds_limit(
157        &self,
158        limit: usize,
159        token_mode: bool,
160        include_line_numbers: bool,
161    ) -> bool {
162        self.get_size(token_mode, include_line_numbers) > limit
163    }
164
165    /// Clear caches to free memory
166    pub fn clear_caches(&mut self) {
167        self.token_count = OnceLock::new();
168        self.formatted_content = None;
169    }
170}
171
172/// Represents file priority information
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct FilePriority {
175    /// Base priority from rules
176    pub rule_priority: i32,
177    /// Boost from git history recency
178    pub git_boost: i32,
179    /// Final combined priority
180    pub combined: i32,
181}
182
183impl FilePriority {
184    pub fn new(rule_priority: i32, git_boost: i32) -> Self {
185        Self {
186            rule_priority,
187            git_boost,
188            combined: rule_priority + git_boost,
189        }
190    }
191}
192
193/// Represents repository information
194#[derive(Debug, Clone)]
195pub struct RepositoryInfo {
196    /// Root path of the repository
197    pub root_path: PathBuf,
198    /// Whether this is a git repository
199    pub is_git_repo: bool,
200    /// Git commit times for files (path -> timestamp)
201    pub commit_times: std::collections::HashMap<String, u64>,
202}
203
204impl RepositoryInfo {
205    pub fn new(root_path: PathBuf, is_git_repo: bool) -> Self {
206        Self {
207            root_path,
208            is_git_repo,
209            commit_times: std::collections::HashMap::new(),
210        }
211    }
212}
213
214/// Configuration for input processing
215#[derive(Debug, Clone)]
216pub struct InputConfig {
217    /// Input file and directory paths
218    pub input_paths: Vec<String>,
219    /// Ignore patterns (compiled globs)
220    pub ignore_patterns: Vec<glob::Pattern>,
221    /// Binary file extensions to skip
222    pub binary_extensions: std::collections::HashSet<String>,
223    /// Maximum depth for git history traversal
224    pub max_git_depth: i32,
225    /// Maximum git boost value
226    pub git_boost_max: Option<i32>,
227}
228
229impl Default for InputConfig {
230    fn default() -> Self {
231        Self {
232            input_paths: Vec::new(),
233            ignore_patterns: Vec::new(),
234            binary_extensions: std::collections::HashSet::new(),
235            max_git_depth: 100,
236            git_boost_max: Some(100),
237        }
238    }
239}
240
241/// Configuration for output processing
242#[derive(Debug, Clone)]
243pub struct OutputConfig {
244    /// Maximum size limit (bytes or tokens)
245    pub max_size: String,
246    /// Whether to use token mode instead of byte mode
247    pub token_mode: bool,
248    /// Token limit when in token mode
249    pub token_limit: Option<String>,
250    /// Output template string
251    pub output_template: String,
252    /// Whether to include line numbers
253    pub line_numbers: bool,
254    /// Whether to enable JSON output
255    pub json_output: bool,
256    /// Whether to include tree header
257    pub tree_header: bool,
258    /// Whether to show only tree (no content)
259    pub tree_only: bool,
260    /// Output directory (if not streaming)
261    pub output_dir: Option<String>,
262    /// Output filename (if not streaming)
263    pub output_name: Option<String>,
264    /// Whether to stream output to stdout
265    pub stream: bool,
266}
267
268impl Default for OutputConfig {
269    fn default() -> Self {
270        Self {
271            max_size: "10MB".to_string(),
272            token_mode: false,
273            token_limit: None,
274            output_template: ">>>> FILE_PATH\nFILE_CONTENT".to_string(),
275            line_numbers: false,
276            json_output: false,
277            tree_header: false,
278            tree_only: false,
279            output_dir: None,
280            output_name: None,
281            stream: false,
282        }
283    }
284}
285
286/// Configuration for processing behavior
287#[derive(Debug, Clone)]
288pub struct ProcessingConfig {
289    /// Priority rules for file ordering
290    pub priority_rules: Vec<crate::priority::PriorityRule>,
291    /// Category-based priority weights
292    pub category_weights: crate::category::CategoryWeights,
293    /// Whether to enable debug output
294    pub debug: bool,
295    /// Whether to enable parallel processing
296    pub parallel: bool,
297    /// Maximum number of concurrent threads
298    pub max_threads: Option<usize>,
299    /// Memory limit for processing
300    pub memory_limit_mb: Option<usize>,
301    /// Batch size for processing
302    pub batch_size: usize,
303}
304
305impl Default for ProcessingConfig {
306    fn default() -> Self {
307        Self {
308            priority_rules: Vec::new(),
309            category_weights: crate::category::CategoryWeights::default(),
310            debug: false,
311            parallel: true,
312            max_threads: None,
313            memory_limit_mb: None,
314            batch_size: 1000,
315        }
316    }
317}
318
319/// Processing statistics for monitoring and optimization
320#[derive(Debug, Clone, Default)]
321pub struct ProcessingStats {
322    /// Total number of files processed
323    pub files_processed: usize,
324    /// Total number of files skipped
325    pub files_skipped: usize,
326    /// Total bytes processed
327    pub bytes_processed: usize,
328    /// Total tokens processed
329    pub tokens_processed: usize,
330    /// Processing time in milliseconds
331    pub processing_time_ms: u128,
332    /// Memory usage in bytes
333    pub memory_usage_bytes: usize,
334    /// Cache hit rate (0.0 to 1.0)
335    pub cache_hit_rate: f64,
336}
337
338impl ProcessingStats {
339    /// Create a new stats instance
340    pub fn new() -> Self {
341        Self::default()
342    }
343
344    /// Add file processing statistics
345    pub fn add_file(&mut self, file: &ProcessedFile, was_cached: bool) {
346        self.files_processed += 1;
347        self.bytes_processed += file.size_bytes;
348        if let Some(token_count) = file.token_count.get() {
349            self.tokens_processed += *token_count;
350        }
351        if was_cached {
352            // This is a simplified cache hit tracking
353            // In a real implementation, you'd track actual cache hits
354        }
355    }
356
357    /// Add skipped file statistics
358    pub fn add_skipped_file(&mut self, size_bytes: usize) {
359        self.files_skipped += 1;
360        self.bytes_processed += size_bytes;
361    }
362}