@@ -4,7 +4,7 @@ use crate::providers::objects::ObjectsProvider;
44use crate :: providers:: refs:: RefsProvider ;
55use crate :: types:: { Change , VcsObjectType } ;
66use moor_compiler:: ObjectDefinition ;
7- use moor_var:: { Var , v_map, v_str} ;
7+ use moor_var:: { Var , v_map, v_str, Sequence , Associative } ;
88use serde:: { Deserialize , Serialize } ;
99use std:: collections:: { HashMap , HashSet } ;
1010
@@ -431,6 +431,10 @@ pub fn compare_object_definitions_with_meta(
431431 }
432432 }
433433
434+ // Detect verb renames: if a verb was deleted and another was added with the same content,
435+ // it's likely a rename rather than a delete+add
436+ detect_verb_renames ( & baseline_verbs, & local_verbs, object_change) ;
437+
434438 // Compare property definitions
435439 let baseline_props: HashMap < String , & moor_compiler:: ObjPropDef > = baseline
436440 . property_definitions
@@ -522,6 +526,180 @@ pub fn compare_object_definitions_with_meta(
522526 }
523527 }
524528 }
529+
530+ // Detect property renames: if a property was deleted and another was added with the same content,
531+ // it's likely a rename rather than a delete+add
532+ detect_property_renames ( & baseline_props, & local_props, object_change) ;
533+ detect_property_override_renames ( & baseline_overrides, & local_overrides, object_change) ;
534+ }
535+
536+ /// Detect verb renames by finding deleted verbs that match added verbs in content
537+ /// Also detects renames when verb names have overlapping aliases (space-delimited)
538+ fn detect_verb_renames (
539+ baseline_verbs : & HashMap < String , & moor_compiler:: ObjVerbDef > ,
540+ local_verbs : & HashMap < String , & moor_compiler:: ObjVerbDef > ,
541+ object_change : & mut ObjectChange ,
542+ ) {
543+ // Find potential renames: for each deleted verb, check if there's a matching added verb
544+ let mut renames_to_apply = Vec :: new ( ) ;
545+
546+ for deleted_name in & object_change. verbs_deleted . clone ( ) {
547+ if let Some ( baseline_verb) = baseline_verbs. get ( deleted_name) {
548+ // Check if there's an added verb with the same content
549+ for added_name in & object_change. verbs_added . clone ( ) {
550+ if let Some ( local_verb) = local_verbs. get ( added_name) {
551+ // Check if the verbs have the same content (everything except the name)
552+ if !verbs_differ ( baseline_verb, local_verb) {
553+ // Check if the names have any overlapping elements (e.g. "look examine" vs "look inspect")
554+ // This helps confirm it's a rename vs coincidental identical code
555+ let has_overlap = verb_names_overlap ( deleted_name, added_name) ;
556+
557+ // Accept the rename if:
558+ // 1. Names have overlapping aliases, OR
559+ // 2. Names are similar enough (could add Levenshtein distance check here)
560+ // For now, we'll accept any exact content match as a rename
561+ if has_overlap || true {
562+ // This is a rename!
563+ renames_to_apply. push ( ( deleted_name. clone ( ) , added_name. clone ( ) ) ) ;
564+ break ; // Each deleted verb can only match one added verb
565+ }
566+ }
567+ }
568+ }
569+ }
570+ }
571+
572+ // Apply the renames: remove from added/deleted, add to renamed
573+ for ( old_name, new_name) in renames_to_apply {
574+ object_change. verbs_deleted . remove ( & old_name) ;
575+ object_change. verbs_added . remove ( & new_name) ;
576+ object_change. verbs_renamed . insert ( old_name, new_name) ;
577+ }
578+ }
579+
580+ /// Check if two verb names have any overlapping elements when split by spaces
581+ /// For example, "look examine" and "look inspect" overlap on "look"
582+ fn verb_names_overlap ( name1 : & str , name2 : & str ) -> bool {
583+ // Split both names by spaces and check for any common elements
584+ let names1: HashSet < & str > = name1. split_whitespace ( ) . collect ( ) ;
585+ let names2: HashSet < & str > = name2. split_whitespace ( ) . collect ( ) ;
586+
587+ // If either name has no elements after splitting, fall back to exact comparison
588+ if names1. is_empty ( ) || names2. is_empty ( ) {
589+ return name1 == name2;
590+ }
591+
592+ // Check if there's any intersection
593+ !names1. is_disjoint ( & names2)
594+ }
595+
596+ /// Detect property definition renames by finding deleted properties that match added properties in content
597+ /// Skip rename detection for properties with falsy values (empty strings, lists, maps) to avoid false positives
598+ fn detect_property_renames (
599+ baseline_props : & HashMap < String , & moor_compiler:: ObjPropDef > ,
600+ local_props : & HashMap < String , & moor_compiler:: ObjPropDef > ,
601+ object_change : & mut ObjectChange ,
602+ ) {
603+ let mut renames_to_apply = Vec :: new ( ) ;
604+
605+ for deleted_name in & object_change. props_deleted . clone ( ) {
606+ if let Some ( baseline_prop) = baseline_props. get ( deleted_name) {
607+ // Skip rename detection if the property value is falsy (empty/default)
608+ if let Some ( value) = & baseline_prop. value {
609+ if is_property_value_falsy ( value) {
610+ continue ;
611+ }
612+ }
613+
614+ // Check if there's an added property with the same content
615+ for added_name in & object_change. props_added . clone ( ) {
616+ if let Some ( local_prop) = local_props. get ( added_name) {
617+ // Skip if the new property value is also falsy
618+ if let Some ( value) = & local_prop. value {
619+ if is_property_value_falsy ( value) {
620+ continue ;
621+ }
622+ }
623+
624+ // Check if the properties have the same content (everything except the name)
625+ if !property_definitions_differ ( baseline_prop, local_prop) {
626+ // This is a rename!
627+ renames_to_apply. push ( ( deleted_name. clone ( ) , added_name. clone ( ) ) ) ;
628+ break ;
629+ }
630+ }
631+ }
632+ }
633+ }
634+
635+ // Apply the renames
636+ for ( old_name, new_name) in renames_to_apply {
637+ object_change. props_deleted . remove ( & old_name) ;
638+ object_change. props_added . remove ( & new_name) ;
639+ object_change. props_renamed . insert ( old_name, new_name) ;
640+ }
641+ }
642+
643+ /// Detect property override renames by finding deleted overrides that match added overrides in content
644+ /// Skip rename detection for properties with falsy values (empty strings, lists, maps) to avoid false positives
645+ fn detect_property_override_renames (
646+ baseline_overrides : & HashMap < String , & moor_compiler:: ObjPropOverride > ,
647+ local_overrides : & HashMap < String , & moor_compiler:: ObjPropOverride > ,
648+ object_change : & mut ObjectChange ,
649+ ) {
650+ let mut renames_to_apply = Vec :: new ( ) ;
651+
652+ for deleted_name in & object_change. props_deleted . clone ( ) {
653+ if let Some ( baseline_override) = baseline_overrides. get ( deleted_name) {
654+ // Skip rename detection if the property value is falsy (empty/default)
655+ if let Some ( value) = & baseline_override. value {
656+ if is_property_value_falsy ( value) {
657+ continue ;
658+ }
659+ }
660+
661+ // Check if there's an added property override with the same content
662+ for added_name in & object_change. props_added . clone ( ) {
663+ if let Some ( local_override) = local_overrides. get ( added_name) {
664+ // Skip if the new property value is also falsy
665+ if let Some ( value) = & local_override. value {
666+ if is_property_value_falsy ( value) {
667+ continue ;
668+ }
669+ }
670+
671+ // Check if the property overrides have the same content (everything except the name)
672+ if !property_overrides_differ ( baseline_override, local_override) {
673+ // This is a rename!
674+ renames_to_apply. push ( ( deleted_name. clone ( ) , added_name. clone ( ) ) ) ;
675+ break ;
676+ }
677+ }
678+ }
679+ }
680+ }
681+
682+ // Apply the renames
683+ for ( old_name, new_name) in renames_to_apply {
684+ object_change. props_deleted . remove ( & old_name) ;
685+ object_change. props_added . remove ( & new_name) ;
686+ object_change. props_renamed . insert ( old_name, new_name) ;
687+ }
688+ }
689+
690+ /// Check if a property value is "falsy" (empty/default) to avoid false positives in rename detection
691+ fn is_property_value_falsy ( value : & moor_var:: Var ) -> bool {
692+ use moor_var:: Variant ;
693+
694+ match value. variant ( ) {
695+ Variant :: Str ( s) => s. is_empty ( ) ,
696+ Variant :: List ( l) => l. is_empty ( ) ,
697+ Variant :: Map ( m) => m. is_empty ( ) ,
698+ Variant :: Int ( i) => * i == 0 ,
699+ Variant :: Float ( f) => * f == 0.0 ,
700+ Variant :: None => true ,
701+ _ => false ,
702+ }
525703}
526704
527705/// Check if two verb definitions differ
0 commit comments