Skip to main content

st/
rename_project.rs

1//! šŸš— Project Rebranding Ritual - Elegant Identity Transition
2//!
3//! A context-aware project renaming system that understands:
4//! - Code semantics and identifier conventions
5//! - Configuration files and manifests
6//! - Documentation and brand consistency
7//! - Different naming conventions (snake_case, camelCase, kebab-case)
8
9use anyhow::Result;
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::fs;
14use std::path::{Path, PathBuf};
15
16use crate::{FileNode, Scanner, ScannerConfig};
17
18/// Different naming conventions we need to handle
19#[derive(Debug, Clone, Eq, PartialEq, Hash)]
20pub enum NamingConvention {
21    SnakeCase,  // bob_amazing_game
22    CamelCase,  // BobAmazingGame
23    PascalCase, // BobAmazingGame
24    KebabCase,  // bob-amazing-game
25    TitleCase,  // Bob Amazing Game
26    UpperCase,  // BOB_AMAZING_GAME
27    LowerCase,  // bob amazing game
28    DotCase,    // bob.amazing.game
29    PathCase,   // bob/amazing/game
30}
31
32/// Context where a name appears
33#[derive(Debug, Clone, PartialEq)]
34pub enum NameContext {
35    FunctionName,
36    VariableName,
37    ClassName,
38    ModuleName,
39    StringLiteral,
40    Comment,
41    ConfigKey,
42    ConfigValue,
43    DocumentationTitle,
44    FilePath,
45    Url,
46    PackageName,
47}
48
49/// A single renaming operation
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct RenameOperation {
52    pub file_path: PathBuf,
53    pub line: usize,
54    pub column: usize,
55    pub old_text: String,
56    pub new_text: String,
57    pub context: String,
58    pub confidence: f32,
59}
60
61/// Configuration for the rename operation
62#[derive(Debug, Clone)]
63pub struct RenameConfig {
64    pub old_name: String,
65    pub new_name: String,
66    pub dry_run: bool,
67    pub interactive: bool,
68    pub preserve_urls: bool,
69    pub update_comments: bool,
70    pub generate_logo: bool,
71    pub backup: bool,
72}
73
74/// The main project renamer
75pub struct ProjectRenamer {
76    config: RenameConfig,
77    operations: Vec<RenameOperation>,
78    name_variants: HashMap<NamingConvention, (String, String)>, // old -> new
79}
80
81impl ProjectRenamer {
82    pub fn new(config: RenameConfig) -> Self {
83        let name_variants = Self::generate_name_variants(&config.old_name, &config.new_name);
84
85        Self {
86            config,
87            operations: Vec::new(),
88            name_variants,
89        }
90    }
91
92    /// Generate all naming convention variants
93    fn generate_name_variants(
94        old_name: &str,
95        new_name: &str,
96    ) -> HashMap<NamingConvention, (String, String)> {
97        let mut variants = HashMap::new();
98
99        // Parse the names to extract words
100        let old_words = Self::extract_words(old_name);
101        let new_words = Self::extract_words(new_name);
102
103        // Generate all variants
104        variants.insert(
105            NamingConvention::SnakeCase,
106            (
107                Self::to_snake_case(&old_words),
108                Self::to_snake_case(&new_words),
109            ),
110        );
111
112        variants.insert(
113            NamingConvention::CamelCase,
114            (
115                Self::to_camel_case(&old_words),
116                Self::to_camel_case(&new_words),
117            ),
118        );
119
120        variants.insert(
121            NamingConvention::PascalCase,
122            (
123                Self::to_pascal_case(&old_words),
124                Self::to_pascal_case(&new_words),
125            ),
126        );
127
128        variants.insert(
129            NamingConvention::KebabCase,
130            (
131                Self::to_kebab_case(&old_words),
132                Self::to_kebab_case(&new_words),
133            ),
134        );
135
136        variants.insert(
137            NamingConvention::TitleCase,
138            (
139                Self::to_title_case(&old_words),
140                Self::to_title_case(&new_words),
141            ),
142        );
143
144        variants.insert(
145            NamingConvention::UpperCase,
146            (
147                Self::to_upper_case(&old_words),
148                Self::to_upper_case(&new_words),
149            ),
150        );
151
152        variants.insert(
153            NamingConvention::LowerCase,
154            (
155                Self::to_lower_case(&old_words),
156                Self::to_lower_case(&new_words),
157            ),
158        );
159
160        variants
161    }
162
163    /// Extract words from various naming formats
164    fn extract_words(name: &str) -> Vec<String> {
165        let mut words = Vec::new();
166        let mut current_word = String::new();
167        let mut prev_is_lower = false;
168
169        for ch in name.chars() {
170            if ch.is_uppercase() && prev_is_lower && !current_word.is_empty() {
171                // camelCase boundary
172                words.push(current_word.to_lowercase());
173                current_word = ch.to_string();
174                prev_is_lower = false;
175            } else if ch == '_' || ch == '-' || ch == ' ' || ch == '.' || ch == '/' {
176                // Separator
177                if !current_word.is_empty() {
178                    words.push(current_word.to_lowercase());
179                    current_word.clear();
180                }
181                prev_is_lower = false;
182            } else {
183                current_word.push(ch);
184                prev_is_lower = ch.is_lowercase();
185            }
186        }
187
188        if !current_word.is_empty() {
189            words.push(current_word.to_lowercase());
190        }
191
192        words
193    }
194
195    fn to_snake_case(words: &[String]) -> String {
196        words.join("_")
197    }
198
199    fn to_camel_case(words: &[String]) -> String {
200        words
201            .iter()
202            .enumerate()
203            .map(|(i, word)| {
204                if i == 0 {
205                    word.clone()
206                } else {
207                    Self::capitalize(word)
208                }
209            })
210            .collect()
211    }
212
213    fn to_pascal_case(words: &[String]) -> String {
214        words.iter().map(|word| Self::capitalize(word)).collect()
215    }
216
217    fn to_kebab_case(words: &[String]) -> String {
218        words.join("-")
219    }
220
221    fn to_title_case(words: &[String]) -> String {
222        words
223            .iter()
224            .map(|word| Self::capitalize(word))
225            .collect::<Vec<_>>()
226            .join(" ")
227    }
228
229    fn to_upper_case(words: &[String]) -> String {
230        words.join("_").to_uppercase()
231    }
232
233    fn to_lower_case(words: &[String]) -> String {
234        words.join(" ")
235    }
236
237    fn capitalize(word: &str) -> String {
238        let mut chars = word.chars();
239        match chars.next() {
240            None => String::new(),
241            Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
242        }
243    }
244
245    /// Scan the project for all occurrences
246    pub async fn scan_project(&mut self, project_path: &Path) -> Result<()> {
247        println!(
248            "šŸ”Ž Scanning for legacy references to \"{}\"...",
249            self.config.old_name
250        );
251
252        let scanner_config = ScannerConfig {
253            max_depth: 100,
254            follow_symlinks: false,
255            respect_gitignore: true,
256            show_hidden: false,
257            show_ignored: false,
258            find_pattern: None,
259            file_type_filter: None,
260            entry_type_filter: None,
261            min_size: None,
262            max_size: Some(10 * 1024 * 1024), // Skip files > 10MB
263            newer_than: None,
264            older_than: None,
265            use_default_ignores: true,
266            search_keyword: None,
267            show_filesystems: false,
268            sort_field: None,
269            top_n: None,
270            include_line_content: false,
271            // Smart scanning options (disabled for rename scan)
272            compute_interest: false,
273            security_scan: false,
274            min_interest: 0.0,
275            track_traversal: false,
276            changes_only: false,
277            compare_state: None,
278            smart_mode: false,
279        };
280
281        let scanner = Scanner::new(project_path, scanner_config)?;
282        let (nodes, _stats) = scanner.scan()?;
283
284        // Process each file
285        for node in nodes {
286            if !node.is_dir && !node.is_symlink {
287                self.scan_file(&node).await?;
288            }
289        }
290
291        Ok(())
292    }
293
294    /// Scan a single file for rename opportunities
295    async fn scan_file(&mut self, node: &FileNode) -> Result<()> {
296        let content = match fs::read_to_string(&node.path) {
297            Ok(content) => content,
298            Err(_) => return Ok(()), // Skip binary or unreadable files
299        };
300
301        let file_type = Self::detect_file_type(&node.path);
302
303        // Check each variant
304        let variants = self.name_variants.clone();
305        for (convention, (old_variant, new_variant)) in &variants {
306            self.find_occurrences_in_content(
307                &node.path,
308                &content,
309                old_variant,
310                new_variant,
311                &file_type,
312                convention,
313            )?;
314        }
315
316        Ok(())
317    }
318
319    /// Detect file type for context-aware replacements
320    fn detect_file_type(path: &Path) -> FileType {
321        let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
322        let filename = path.file_name().and_then(|f| f.to_str()).unwrap_or("");
323
324        match extension {
325            "rs" => FileType::Rust,
326            "py" => FileType::Python,
327            "js" | "jsx" | "ts" | "tsx" => FileType::JavaScript,
328            "go" => FileType::Go,
329            "java" => FileType::Java,
330            "toml" => FileType::Toml,
331            "yaml" | "yml" => FileType::Yaml,
332            "json" => FileType::Json,
333            "md" => FileType::Markdown,
334            "desktop" => FileType::Desktop,
335            _ => {
336                // Check filenames
337                match filename {
338                    "Cargo.toml" => FileType::Toml,
339                    "package.json" => FileType::Json,
340                    "README.md" | "README" => FileType::Markdown,
341                    _ => FileType::Unknown,
342                }
343            }
344        }
345    }
346
347    /// Find occurrences in content with context awareness
348    fn find_occurrences_in_content(
349        &mut self,
350        file_path: &Path,
351        content: &str,
352        old_variant: &str,
353        new_variant: &str,
354        file_type: &FileType,
355        _convention: &NamingConvention,
356    ) -> Result<()> {
357        // Build context-aware regex based on file type
358        let patterns = self.build_context_patterns(old_variant, file_type, _convention);
359
360        for (pattern, context) in patterns {
361            let re = Regex::new(&pattern)?;
362
363            for (line_no, line) in content.lines().enumerate() {
364                for mat in re.find_iter(line) {
365                    let operation = RenameOperation {
366                        file_path: file_path.to_path_buf(),
367                        line: line_no + 1,
368                        column: mat.start() + 1,
369                        old_text: mat.as_str().to_string(),
370                        new_text: self.calculate_replacement(
371                            mat.as_str(),
372                            old_variant,
373                            new_variant,
374                            &context,
375                        ),
376                        context: format!("{:?} in {:?}", context, file_type),
377                        confidence: self.calculate_confidence(&context, file_type),
378                    };
379
380                    self.operations.push(operation);
381                }
382            }
383        }
384
385        Ok(())
386    }
387
388    /// Build context-aware patterns based on file type
389    fn build_context_patterns(
390        &self,
391        variant: &str,
392        file_type: &FileType,
393        _convention: &NamingConvention,
394    ) -> Vec<(String, NameContext)> {
395        let escaped = regex::escape(variant);
396        let mut patterns = Vec::new();
397
398        match file_type {
399            FileType::Rust => {
400                // Function/method names
401                patterns.push((format!(r"\bfn\s+{}\b", escaped), NameContext::FunctionName));
402                // Struct/enum names
403                patterns.push((
404                    format!(r"\b(struct|enum|trait)\s+{}\b", escaped),
405                    NameContext::ClassName,
406                ));
407                // Variable names
408                patterns.push((
409                    format!(r"\b(let|const|static)\s+.*{}\b", escaped),
410                    NameContext::VariableName,
411                ));
412                // Module names
413                patterns.push((format!(r"\bmod\s+{}\b", escaped), NameContext::ModuleName));
414            }
415            FileType::Python => {
416                patterns.push((
417                    format!(r"\b(def|class)\s+{}\b", escaped),
418                    NameContext::FunctionName,
419                ));
420            }
421            FileType::Toml | FileType::Yaml | FileType::Json => {
422                // Package names
423                patterns.push((
424                    format!(r#"(name|package)\s*[=:]\s*"?{}"?"#, escaped),
425                    NameContext::PackageName,
426                ));
427            }
428            FileType::Markdown => {
429                // Titles
430                patterns.push((
431                    format!(r"^#+\s*.*{}", escaped),
432                    NameContext::DocumentationTitle,
433                ));
434            }
435            _ => {}
436        }
437
438        // Always check for string literals and comments
439        patterns.push((format!(r#""{}"#, escaped), NameContext::StringLiteral));
440        patterns.push((format!(r"//.*{}", escaped), NameContext::Comment));
441
442        patterns
443    }
444
445    /// Calculate the replacement based on context
446    fn calculate_replacement(
447        &self,
448        matched_text: &str,
449        old_variant: &str,
450        new_variant: &str,
451        _context: &NameContext,
452    ) -> String {
453        // For now, simple replacement
454        // TODO: Handle more complex cases like preserving quotes, etc.
455        matched_text.replace(old_variant, new_variant)
456    }
457
458    /// Calculate confidence score for the replacement
459    fn calculate_confidence(&self, context: &NameContext, _file_type: &FileType) -> f32 {
460        match context {
461            NameContext::FunctionName | NameContext::ClassName | NameContext::ModuleName => 0.95,
462            NameContext::VariableName => 0.85,
463            NameContext::StringLiteral => 0.8,
464            NameContext::PackageName => 0.9,
465            NameContext::DocumentationTitle => 0.9,
466            NameContext::Comment => 0.7,
467            _ => 0.6,
468        }
469    }
470
471    /// Apply all rename operations
472    pub async fn apply_renames(&self) -> Result<()> {
473        if self.operations.is_empty() {
474            println!("No renaming operations found.");
475            return Ok(());
476        }
477
478        // Group operations by file
479        let mut ops_by_file: HashMap<PathBuf, Vec<&RenameOperation>> = HashMap::new();
480        for op in &self.operations {
481            ops_by_file
482                .entry(op.file_path.clone())
483                .or_default()
484                .push(op);
485        }
486
487        for (file_path, ops) in ops_by_file {
488            self.apply_file_renames(&file_path, ops).await?;
489        }
490
491        Ok(())
492    }
493
494    /// Apply renames to a single file
495    async fn apply_file_renames(
496        &self,
497        file_path: &Path,
498        operations: Vec<&RenameOperation>,
499    ) -> Result<()> {
500        let content = fs::read_to_string(file_path)?;
501        let mut new_content = content.clone();
502
503        // Apply operations in reverse order to maintain positions
504        let mut sorted_ops = operations;
505        sorted_ops.sort_by(|a, b| b.line.cmp(&a.line).then(b.column.cmp(&a.column)));
506
507        for op in sorted_ops {
508            // Simple replacement for now
509            // TODO: Implement precise position-based replacement
510            new_content = new_content.replace(&op.old_text, &op.new_text);
511        }
512
513        if self.config.backup {
514            let backup_path = file_path.with_extension(format!(
515                "{}.bak",
516                file_path
517                    .extension()
518                    .unwrap_or_default()
519                    .to_str()
520                    .unwrap_or("")
521            ));
522            fs::copy(file_path, backup_path)?;
523        }
524
525        fs::write(file_path, new_content)?;
526        Ok(())
527    }
528
529    /// Show a summary of planned operations
530    pub fn show_summary(&self) {
531        println!("\nāœ… Found {} matches across:", self.operations.len());
532
533        // Group by file type
534        let mut file_counts: HashMap<String, usize> = HashMap::new();
535        for op in &self.operations {
536            let ext = op
537                .file_path
538                .extension()
539                .and_then(|e| e.to_str())
540                .unwrap_or("other");
541            *file_counts.entry(ext.to_string()).or_default() += 1;
542        }
543
544        for (ext, count) in file_counts {
545            println!("   - {} {} files", count, ext);
546        }
547
548        println!("\nšŸŽØ Context-aware replacements:");
549        println!(
550            "   • Identifiers → `{}`",
551            self.name_variants
552                .get(&NamingConvention::SnakeCase)
553                .unwrap()
554                .1
555        );
556        println!("   • Strings → \"{}\"", self.config.new_name);
557        println!(
558            "   • Titles → `{}`",
559            self.name_variants
560                .get(&NamingConvention::TitleCase)
561                .unwrap()
562                .1
563        );
564        println!("   • Comments → updated branding");
565
566        println!("\nšŸ›”ļø Safety net enabled: changes wrapped in diff mode");
567    }
568}
569
570/// File types we understand
571#[derive(Debug, Clone, PartialEq)]
572enum FileType {
573    Rust,
574    Python,
575    JavaScript,
576    Go,
577    Java,
578    Toml,
579    Yaml,
580    Json,
581    Markdown,
582    Desktop,
583    Unknown,
584}
585
586/// Interactive mode options
587pub enum UserChoice {
588    Preview,
589    Commit,
590    Edit,
591    Cancel,
592}
593
594impl ProjectRenamer {
595    /// Run interactive mode
596    pub async fn run_interactive(&mut self) -> Result<UserChoice> {
597        println!("\nWould you like to:");
598        println!("[1] Preview changes");
599        println!("[2] Commit rename");
600        println!("[3] Edit before apply");
601        println!("[4] Cancel");
602
603        // TODO: Implement actual user input
604        Ok(UserChoice::Preview)
605    }
606
607    /// Show preview of changes
608    pub fn show_preview(&self) {
609        for (i, op) in self.operations.iter().take(10).enumerate() {
610            println!("\n{}) {}:{}", i + 1, op.file_path.display(), op.line);
611            println!("   {} → {}", op.old_text, op.new_text);
612            println!(
613                "   Context: {} (confidence: {:.0}%)",
614                op.context,
615                op.confidence * 100.0
616            );
617        }
618
619        if self.operations.len() > 10 {
620            println!("\n... and {} more changes", self.operations.len() - 10);
621        }
622    }
623}
624
625/// Main entry point for the rename-project command
626pub async fn rename_project(old_name: &str, new_name: &str, options: RenameOptions) -> Result<()> {
627    println!("šŸš— Project Rebranding Ritual");
628    println!("═════════════════════════════");
629
630    let config = RenameConfig {
631        old_name: old_name.to_string(),
632        new_name: new_name.to_string(),
633        dry_run: options.dry_run,
634        interactive: options.interactive,
635        preserve_urls: options.preserve_urls,
636        update_comments: options.update_comments,
637        generate_logo: options.generate_logo,
638        backup: options.backup,
639    };
640
641    let mut renamer = ProjectRenamer::new(config);
642
643    // Scan the project
644    let project_path = std::env::current_dir()?;
645    renamer.scan_project(&project_path).await?;
646
647    // Show summary
648    renamer.show_summary();
649
650    // Interactive mode
651    if options.interactive {
652        match renamer.run_interactive().await? {
653            UserChoice::Preview => {
654                renamer.show_preview();
655            }
656            UserChoice::Commit => {
657                if !options.dry_run {
658                    renamer.apply_renames().await?;
659                    println!("\n✨ Project successfully rebranded!");
660                }
661            }
662            UserChoice::Edit => {
663                // TODO: Implement edit mode
664                println!("Edit mode not yet implemented");
665            }
666            UserChoice::Cancel => {
667                println!("Rename cancelled.");
668            }
669        }
670    } else if !options.dry_run {
671        renamer.apply_renames().await?;
672        println!("\n✨ Project successfully rebranded!");
673    }
674
675    // Generate logo if requested
676    if options.generate_logo {
677        generate_placeholder_logo(new_name)?;
678    }
679
680    Ok(())
681}
682
683/// Options for the rename command
684#[derive(Debug, Clone)]
685pub struct RenameOptions {
686    pub dry_run: bool,
687    pub interactive: bool,
688    pub preserve_urls: bool,
689    pub update_comments: bool,
690    pub generate_logo: bool,
691    pub backup: bool,
692}
693
694impl Default for RenameOptions {
695    fn default() -> Self {
696        Self {
697            dry_run: false,
698            interactive: true,
699            preserve_urls: true,
700            update_comments: true,
701            generate_logo: false,
702            backup: true,
703        }
704    }
705}
706
707/// Generate a placeholder SVG logo
708fn generate_placeholder_logo(project_name: &str) -> Result<()> {
709    let svg = format!(
710        r##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
711  <rect width="200" height="200" fill="#1a1a1a"/>
712  <text x="100" y="100" font-family="Arial, sans-serif" font-size="24" fill="#ffffff" text-anchor="middle" dominant-baseline="middle">
713    {}
714  </text>
715</svg>"##,
716        project_name
717    );
718
719    fs::write("assets/logo.svg", svg)?;
720    println!("\nšŸŽØ Generated placeholder logo at assets/logo.svg");
721
722    Ok(())
723}