1use std::collections::{HashMap, HashSet};
2use std::io::Write;
3use std::process::Command;
4use std::sync::{mpsc, LazyLock};
5use std::time::Duration;
6
7use serde::Serialize;
8use sem_core::model::change::ChangeType;
9use sem_core::model::entity::SemanticEntity;
10use sem_core::model::identity::match_entities;
11use sem_core::parser::plugins::create_default_registry;
12use sem_core::parser::registry::ParserRegistry;
13
14static PARSER_REGISTRY: LazyLock<ParserRegistry> = LazyLock::new(create_default_registry);
17
18use crate::conflict::{classify_conflict, ConflictKind, EntityConflict, MarkerFormat, MergeStats};
19use crate::region::{extract_regions, EntityRegion, FileRegion};
20use crate::validate::SemanticWarning;
21use crate::reconstruct::reconstruct;
22
23#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ResolutionStrategy {
27 Unchanged,
28 OursOnly,
29 TheirsOnly,
30 ContentEqual,
31 DiffyMerged,
32 DecoratorMerged,
33 InnerMerged,
34 ConflictBothModified,
35 ConflictModifyDelete,
36 ConflictBothAdded,
37 ConflictRenameRename,
38 AddedOurs,
39 AddedTheirs,
40 Deleted,
41 Renamed { from: String, to: String },
42 Fallback,
43}
44
45#[derive(Debug, Clone, Serialize)]
47pub struct EntityAudit {
48 pub name: String,
49 #[serde(rename = "type")]
50 pub entity_type: String,
51 pub resolution: ResolutionStrategy,
52}
53
54#[derive(Debug)]
56pub struct MergeResult {
57 pub content: String,
58 pub conflicts: Vec<EntityConflict>,
59 pub warnings: Vec<SemanticWarning>,
60 pub stats: MergeStats,
61 pub audit: Vec<EntityAudit>,
62}
63
64impl MergeResult {
65 pub fn is_clean(&self) -> bool {
66 self.conflicts.is_empty()
67 && !self.content.lines().any(|l| l.starts_with("<<<<<<< ours"))
68 }
69}
70
71#[derive(Debug, Clone)]
73pub enum ResolvedEntity {
74 Clean(EntityRegion),
76 Conflict(EntityConflict),
78 ScopedConflict {
81 content: String,
82 conflict: EntityConflict,
83 },
84 Deleted,
86}
87
88pub fn entity_merge(
95 base: &str,
96 ours: &str,
97 theirs: &str,
98 file_path: &str,
99) -> MergeResult {
100 entity_merge_fmt(base, ours, theirs, file_path, &MarkerFormat::default())
101}
102
103pub fn entity_merge_fmt(
105 base: &str,
106 ours: &str,
107 theirs: &str,
108 file_path: &str,
109 marker_format: &MarkerFormat,
110) -> MergeResult {
111 let timeout_secs = std::env::var("WEAVE_TIMEOUT")
112 .ok()
113 .and_then(|v| v.parse::<u64>().ok())
114 .unwrap_or(5);
115
116 let base_owned = base.to_string();
119 let ours_owned = ours.to_string();
120 let theirs_owned = theirs.to_string();
121 let path_owned = file_path.to_string();
122 let fmt_owned = marker_format.clone();
123
124 let (tx, rx) = mpsc::channel();
125 std::thread::spawn(move || {
126 let result = entity_merge_with_registry(&base_owned, &ours_owned, &theirs_owned, &path_owned, &PARSER_REGISTRY, &fmt_owned);
127 let _ = tx.send(result);
128 });
129
130 match rx.recv_timeout(Duration::from_secs(timeout_secs)) {
131 Ok(result) => result,
132 Err(_) => {
133 eprintln!("weave: merge timed out after {}s for {}, falling back to git merge-file", timeout_secs, file_path);
134 let mut stats = MergeStats::default();
135 stats.used_fallback = true;
136 git_merge_file(base, ours, theirs, &mut stats)
137 }
138 }
139}
140
141pub fn entity_merge_with_registry(
142 base: &str,
143 ours: &str,
144 theirs: &str,
145 file_path: &str,
146 registry: &ParserRegistry,
147 marker_format: &MarkerFormat,
148) -> MergeResult {
149 if has_conflict_markers(base) || has_conflict_markers(ours) || has_conflict_markers(theirs) {
153 let mut stats = MergeStats::default();
154 stats.entities_conflicted = 1;
155 stats.used_fallback = true;
156 let content = if has_conflict_markers(ours) {
159 ours
160 } else if has_conflict_markers(theirs) {
161 theirs
162 } else {
163 base
164 };
165 let complexity = classify_conflict(Some(base), Some(ours), Some(theirs));
166 return MergeResult {
167 content: content.to_string(),
168 conflicts: vec![EntityConflict {
169 entity_name: "(file)".to_string(),
170 entity_type: "file".to_string(),
171 kind: ConflictKind::BothModified,
172 complexity,
173 ours_content: Some(ours.to_string()),
174 theirs_content: Some(theirs.to_string()),
175 base_content: Some(base.to_string()),
176 }],
177 warnings: vec![],
178 stats,
179 audit: vec![],
180 };
181 }
182
183 if ours == theirs {
185 return MergeResult {
186 content: ours.to_string(),
187 conflicts: vec![],
188 warnings: vec![],
189 stats: MergeStats::default(),
190 audit: vec![],
191 };
192 }
193
194 if base == ours {
196 return MergeResult {
197 content: theirs.to_string(),
198 conflicts: vec![],
199 warnings: vec![],
200 stats: MergeStats {
201 entities_theirs_only: 1,
202 ..Default::default()
203 },
204 audit: vec![],
205 };
206 }
207
208 if base == theirs {
210 return MergeResult {
211 content: ours.to_string(),
212 conflicts: vec![],
213 warnings: vec![],
214 stats: MergeStats {
215 entities_ours_only: 1,
216 ..Default::default()
217 },
218 audit: vec![],
219 };
220 }
221
222 if is_binary(base) || is_binary(ours) || is_binary(theirs) {
224 let mut stats = MergeStats::default();
225 stats.used_fallback = true;
226 return git_merge_file(base, ours, theirs, &mut stats);
227 }
228
229 if base.len() > 1_000_000 || ours.len() > 1_000_000 || theirs.len() > 1_000_000 {
231 return line_level_fallback(base, ours, theirs, file_path);
232 }
233
234 let plugin = match registry.get_plugin(file_path) {
240 Some(p) if p.id() != "fallback" => p,
241 _ => return line_level_fallback(base, ours, theirs, file_path),
242 };
243
244 let base_all = plugin.extract_entities(base, file_path);
247 let ours_all = plugin.extract_entities(ours, file_path);
248 let theirs_all = plugin.extract_entities(theirs, file_path);
249
250 let base_entities = filter_nested_entities(base_all.clone());
252 let ours_entities = filter_nested_entities(ours_all.clone());
253 let theirs_entities = filter_nested_entities(theirs_all.clone());
254
255 if base_entities.is_empty() && !base.trim().is_empty() {
257 return line_level_fallback(base, ours, theirs, file_path);
258 }
259 if ours_entities.is_empty() && !ours.trim().is_empty() && theirs_entities.is_empty() && !theirs.trim().is_empty() {
261 return line_level_fallback(base, ours, theirs, file_path);
262 }
263
264 if has_excessive_duplicates(&base_entities) || has_excessive_duplicates(&ours_entities) || has_excessive_duplicates(&theirs_entities) {
267 return line_level_fallback(base, ours, theirs, file_path);
268 }
269
270 let base_regions = extract_regions(base, &base_entities);
272 let ours_regions = extract_regions(ours, &ours_entities);
273 let theirs_regions = extract_regions(theirs, &theirs_entities);
274
275 let base_region_content = build_region_content_map(&base_regions);
278 let ours_region_content = build_region_content_map(&ours_regions);
279 let theirs_region_content = build_region_content_map(&theirs_regions);
280
281 let ours_changes = match_entities(&base_entities, &ours_entities, file_path, None, None, None);
283 let theirs_changes = match_entities(&base_entities, &theirs_entities, file_path, None, None, None);
284
285 let base_entity_map: HashMap<&str, &SemanticEntity> =
287 base_entities.iter().map(|e| (e.id.as_str(), e)).collect();
288 let ours_entity_map: HashMap<&str, &SemanticEntity> =
289 ours_entities.iter().map(|e| (e.id.as_str(), e)).collect();
290 let theirs_entity_map: HashMap<&str, &SemanticEntity> =
291 theirs_entities.iter().map(|e| (e.id.as_str(), e)).collect();
292
293 let mut ours_change_map: HashMap<String, ChangeType> = HashMap::new();
295 for change in &ours_changes.changes {
296 ours_change_map.insert(change.entity_id.clone(), change.change_type);
297 }
298 let mut theirs_change_map: HashMap<String, ChangeType> = HashMap::new();
299 for change in &theirs_changes.changes {
300 theirs_change_map.insert(change.entity_id.clone(), change.change_type);
301 }
302
303 let ours_rename_to_base = build_rename_map(&base_entities, &ours_entities);
307 let theirs_rename_to_base = build_rename_map(&base_entities, &theirs_entities);
308 let base_to_ours_rename: HashMap<String, String> = ours_rename_to_base
310 .iter()
311 .map(|(new, old)| (old.clone(), new.clone()))
312 .collect();
313 let base_to_theirs_rename: HashMap<String, String> = theirs_rename_to_base
314 .iter()
315 .map(|(new, old)| (old.clone(), new.clone()))
316 .collect();
317
318 let mut all_entity_ids: Vec<String> = Vec::new();
320 let mut seen: HashSet<String> = HashSet::new();
321 let mut skip_ids: HashSet<String> = HashSet::new();
323 for new_id in ours_rename_to_base.keys() {
325 skip_ids.insert(new_id.clone());
326 }
327 for new_id in theirs_rename_to_base.keys() {
328 skip_ids.insert(new_id.clone());
329 }
330
331 for entity in &ours_entities {
333 if skip_ids.contains(&entity.id) {
334 continue;
335 }
336 if seen.insert(entity.id.clone()) {
337 all_entity_ids.push(entity.id.clone());
338 }
339 }
340 for entity in &theirs_entities {
342 if skip_ids.contains(&entity.id) {
343 continue;
344 }
345 if seen.insert(entity.id.clone()) {
346 all_entity_ids.push(entity.id.clone());
347 }
348 }
349 for entity in &base_entities {
351 if seen.insert(entity.id.clone()) {
352 all_entity_ids.push(entity.id.clone());
353 }
354 }
355
356 let mut stats = MergeStats::default();
357 let mut conflicts: Vec<EntityConflict> = Vec::new();
358 let mut audit: Vec<EntityAudit> = Vec::new();
359 let mut resolved_entities: HashMap<String, ResolvedEntity> = HashMap::new();
360
361 let mut rename_conflict_ids: HashSet<String> = HashSet::new();
365 for (base_id, ours_new_id) in &base_to_ours_rename {
366 if let Some(theirs_new_id) = base_to_theirs_rename.get(base_id) {
367 if ours_new_id != theirs_new_id {
368 rename_conflict_ids.insert(base_id.clone());
369 }
370 }
371 }
372
373 for entity_id in &all_entity_ids {
374 if rename_conflict_ids.contains(entity_id) {
376 let ours_new_id = &base_to_ours_rename[entity_id];
377 let theirs_new_id = &base_to_theirs_rename[entity_id];
378 let base_entity = base_entity_map.get(entity_id.as_str());
379 let ours_entity = ours_entity_map.get(ours_new_id.as_str());
380 let theirs_entity = theirs_entity_map.get(theirs_new_id.as_str());
381 let base_name = base_entity.map(|e| e.name.as_str()).unwrap_or(entity_id);
382 let ours_name = ours_entity.map(|e| e.name.as_str()).unwrap_or(ours_new_id);
383 let theirs_name = theirs_entity.map(|e| e.name.as_str()).unwrap_or(theirs_new_id);
384
385 let base_rc = base_entity.map(|e| base_region_content.get(e.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| e.content.clone()));
386 let ours_rc = ours_entity.map(|e| ours_region_content.get(e.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| e.content.clone()));
387 let theirs_rc = theirs_entity.map(|e| theirs_region_content.get(e.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| e.content.clone()));
388
389 stats.entities_conflicted += 1;
390 let conflict = EntityConflict {
391 entity_name: base_name.to_string(),
392 entity_type: base_entity.map(|e| e.entity_type.clone()).unwrap_or_default(),
393 kind: ConflictKind::RenameRename {
394 base_name: base_name.to_string(),
395 ours_name: ours_name.to_string(),
396 theirs_name: theirs_name.to_string(),
397 },
398 complexity: crate::conflict::ConflictComplexity::Syntax,
399 ours_content: ours_rc,
400 theirs_content: theirs_rc,
401 base_content: base_rc,
402 };
403 conflicts.push(conflict.clone());
404 audit.push(EntityAudit {
405 name: base_name.to_string(),
406 entity_type: base_entity.map(|e| e.entity_type.clone()).unwrap_or_default(),
407 resolution: ResolutionStrategy::ConflictRenameRename,
408 });
409 let resolution = ResolvedEntity::Conflict(conflict);
410 resolved_entities.insert(entity_id.clone(), resolution.clone());
411 resolved_entities.insert(ours_new_id.clone(), resolution);
412 resolved_entities.insert(theirs_new_id.clone(), ResolvedEntity::Deleted);
415 continue;
416 }
417
418 let in_base = base_entity_map.get(entity_id.as_str());
419 let ours_id = base_to_ours_rename.get(entity_id.as_str()).map(|s| s.as_str()).unwrap_or(entity_id.as_str());
421 let theirs_id = base_to_theirs_rename.get(entity_id.as_str()).map(|s| s.as_str()).unwrap_or(entity_id.as_str());
422 let in_ours = ours_entity_map.get(ours_id).or_else(|| ours_entity_map.get(entity_id.as_str()));
423 let in_theirs = theirs_entity_map.get(theirs_id).or_else(|| theirs_entity_map.get(entity_id.as_str()));
424
425 let ours_change = ours_change_map.get(entity_id);
426 let theirs_change = theirs_change_map.get(entity_id);
427
428 let (resolution, strategy) = resolve_entity(
429 entity_id,
430 in_base,
431 in_ours,
432 in_theirs,
433 ours_change,
434 theirs_change,
435 &base_region_content,
436 &ours_region_content,
437 &theirs_region_content,
438 &base_all,
439 &ours_all,
440 &theirs_all,
441 &mut stats,
442 marker_format,
443 );
444
445 let entity_name = in_ours.map(|e| e.name.as_str())
447 .or_else(|| in_theirs.map(|e| e.name.as_str()))
448 .or_else(|| in_base.map(|e| e.name.as_str()))
449 .unwrap_or(entity_id)
450 .to_string();
451 let entity_type = in_ours.map(|e| e.entity_type.as_str())
452 .or_else(|| in_theirs.map(|e| e.entity_type.as_str()))
453 .or_else(|| in_base.map(|e| e.entity_type.as_str()))
454 .unwrap_or("")
455 .to_string();
456 audit.push(EntityAudit {
457 name: entity_name,
458 entity_type,
459 resolution: strategy,
460 });
461
462 match &resolution {
463 ResolvedEntity::Conflict(ref c) => conflicts.push(c.clone()),
464 ResolvedEntity::ScopedConflict { conflict, .. } => conflicts.push(conflict.clone()),
465 _ => {}
466 }
467
468 resolved_entities.insert(entity_id.clone(), resolution.clone());
469 if let Some(ours_renamed_id) = base_to_ours_rename.get(entity_id.as_str()) {
471 resolved_entities.insert(ours_renamed_id.clone(), resolution.clone());
472 }
473 if let Some(theirs_renamed_id) = base_to_theirs_rename.get(entity_id.as_str()) {
474 resolved_entities.insert(theirs_renamed_id.clone(), resolution);
475 }
476 }
477
478 let (merged_interstitials, interstitial_conflicts) =
480 merge_interstitials(&base_regions, &ours_regions, &theirs_regions, marker_format);
481 stats.entities_conflicted += interstitial_conflicts.len();
482 conflicts.extend(interstitial_conflicts);
483
484 let content = reconstruct(
486 &ours_regions,
487 &theirs_regions,
488 &theirs_entities,
489 &ours_entity_map,
490 &resolved_entities,
491 &merged_interstitials,
492 marker_format,
493 );
494
495 let content = post_merge_cleanup(&content);
497
498 let mut warnings = vec![];
501 if conflicts.is_empty() && stats.entities_both_changed_merged > 0 {
502 let merged_entities = plugin.extract_entities(&content, file_path);
503 if merged_entities.is_empty() && !content.trim().is_empty() {
504 warnings.push(crate::validate::SemanticWarning {
505 entity_name: "(file)".to_string(),
506 entity_type: "file".to_string(),
507 file_path: file_path.to_string(),
508 kind: crate::validate::WarningKind::ParseFailedAfterMerge,
509 related: vec![],
510 });
511 }
512 }
513
514 let entity_result = MergeResult {
515 content,
516 conflicts,
517 warnings,
518 stats: stats.clone(),
519 audit,
520 };
521
522 let entity_markers = entity_result.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
526 if entity_markers > 0 {
527 let git_result = git_merge_file(base, ours, theirs, &mut stats);
528 let git_markers = git_result.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
529 if entity_markers > git_markers {
530 return git_result;
531 }
532 }
533
534 if entity_markers == 0 {
538 let merged_len = entity_result.content.len();
539 let min_input_len = ours.len().min(theirs.len());
540 if min_input_len > 200 && merged_len < min_input_len * 80 / 100 {
542 return git_merge_file(base, ours, theirs, &mut stats);
543 }
544 }
545
546 entity_result
547}
548
549fn resolve_entity(
550 _entity_id: &str,
551 in_base: Option<&&SemanticEntity>,
552 in_ours: Option<&&SemanticEntity>,
553 in_theirs: Option<&&SemanticEntity>,
554 _ours_change: Option<&ChangeType>,
555 _theirs_change: Option<&ChangeType>,
556 base_region_content: &HashMap<&str, &str>,
557 ours_region_content: &HashMap<&str, &str>,
558 theirs_region_content: &HashMap<&str, &str>,
559 base_all: &[SemanticEntity],
560 ours_all: &[SemanticEntity],
561 theirs_all: &[SemanticEntity],
562 stats: &mut MergeStats,
563 marker_format: &MarkerFormat,
564) -> (ResolvedEntity, ResolutionStrategy) {
565 let region_content = |entity: &SemanticEntity, map: &HashMap<&str, &str>| -> String {
567 map.get(entity.id.as_str()).map(|s| s.to_string()).unwrap_or_else(|| entity.content.clone())
568 };
569
570 match (in_base, in_ours, in_theirs) {
571 (Some(base), Some(ours), Some(theirs)) => {
573 let base_rc_lazy = || region_content(base, base_region_content);
577 let ours_rc_lazy = || region_content(ours, ours_region_content);
578 let theirs_rc_lazy = || region_content(theirs, theirs_region_content);
579
580 let ours_modified = ours.content_hash != base.content_hash
581 || ours_rc_lazy() != base_rc_lazy();
582 let theirs_modified = theirs.content_hash != base.content_hash
583 || theirs_rc_lazy() != base_rc_lazy();
584
585 match (ours_modified, theirs_modified) {
586 (false, false) => {
587 stats.entities_unchanged += 1;
589 (ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::Unchanged)
590 }
591 (true, false) => {
592 stats.entities_ours_only += 1;
594 (ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::OursOnly)
595 }
596 (false, true) => {
597 stats.entities_theirs_only += 1;
599 (ResolvedEntity::Clean(entity_to_region_with_content(theirs, ®ion_content(theirs, theirs_region_content))), ResolutionStrategy::TheirsOnly)
600 }
601 (true, true) => {
602 if ours.content_hash == theirs.content_hash {
604 stats.entities_both_changed_merged += 1;
606 (ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::ContentEqual)
607 } else {
608 let base_rc = region_content(base, base_region_content);
610 let ours_rc = region_content(ours, ours_region_content);
611 let theirs_rc = region_content(theirs, theirs_region_content);
612
613 if is_whitespace_only_diff(&base_rc, &ours_rc) {
618 stats.entities_theirs_only += 1;
619 return (ResolvedEntity::Clean(entity_to_region_with_content(theirs, &theirs_rc)), ResolutionStrategy::TheirsOnly);
620 }
621 if is_whitespace_only_diff(&base_rc, &theirs_rc) {
622 stats.entities_ours_only += 1;
623 return (ResolvedEntity::Clean(entity_to_region_with_content(ours, &ours_rc)), ResolutionStrategy::OursOnly);
624 }
625
626 match diffy_merge(&base_rc, &ours_rc, &theirs_rc) {
627 Some(merged) => {
628 stats.entities_both_changed_merged += 1;
629 stats.resolved_via_diffy += 1;
630 (ResolvedEntity::Clean(EntityRegion {
631 entity_id: ours.id.clone(),
632 entity_name: ours.name.clone(),
633 entity_type: ours.entity_type.clone(),
634 content: merged,
635 start_line: ours.start_line,
636 end_line: ours.end_line,
637 }), ResolutionStrategy::DiffyMerged)
638 }
639 None => {
640 if let Some(merged) = try_decorator_aware_merge(&base_rc, &ours_rc, &theirs_rc) {
643 stats.entities_both_changed_merged += 1;
644 stats.resolved_via_diffy += 1;
645 return (ResolvedEntity::Clean(EntityRegion {
646 entity_id: ours.id.clone(),
647 entity_name: ours.name.clone(),
648 entity_type: ours.entity_type.clone(),
649 content: merged,
650 start_line: ours.start_line,
651 end_line: ours.end_line,
652 }), ResolutionStrategy::DecoratorMerged);
653 }
654
655 if is_container_entity_type(&ours.entity_type) {
658 let base_children = in_base
659 .map(|b| get_child_entities(b, base_all))
660 .unwrap_or_default();
661 let ours_children = get_child_entities(ours, ours_all);
662 let theirs_children = in_theirs
663 .map(|t| get_child_entities(t, theirs_all))
664 .unwrap_or_default();
665 let base_start = in_base.map(|b| b.start_line).unwrap_or(1);
666 let ours_start = ours.start_line;
667 let theirs_start = in_theirs.map(|t| t.start_line).unwrap_or(1);
668 if let Some(inner) = try_inner_entity_merge(
669 &base_rc, &ours_rc, &theirs_rc,
670 &base_children, &ours_children, &theirs_children,
671 base_start, ours_start, theirs_start,
672 marker_format,
673 ) {
674 if inner.has_conflicts {
675 stats.entities_conflicted += 1;
679 stats.resolved_via_inner_merge += 1;
680 let complexity = classify_conflict(Some(&base_rc), Some(&ours_rc), Some(&theirs_rc));
681 return (ResolvedEntity::ScopedConflict {
682 content: inner.content,
683 conflict: EntityConflict {
684 entity_name: ours.name.clone(),
685 entity_type: ours.entity_type.clone(),
686 kind: ConflictKind::BothModified,
687 complexity,
688 ours_content: Some(ours_rc),
689 theirs_content: Some(theirs_rc),
690 base_content: Some(base_rc),
691 },
692 }, ResolutionStrategy::InnerMerged);
693 } else {
694 stats.entities_both_changed_merged += 1;
695 stats.resolved_via_inner_merge += 1;
696 return (ResolvedEntity::Clean(EntityRegion {
697 entity_id: ours.id.clone(),
698 entity_name: ours.name.clone(),
699 entity_type: ours.entity_type.clone(),
700 content: inner.content,
701 start_line: ours.start_line,
702 end_line: ours.end_line,
703 }), ResolutionStrategy::InnerMerged);
704 }
705 }
706 }
707 stats.entities_conflicted += 1;
708 let complexity = classify_conflict(Some(&base_rc), Some(&ours_rc), Some(&theirs_rc));
709 (ResolvedEntity::Conflict(EntityConflict {
710 entity_name: ours.name.clone(),
711 entity_type: ours.entity_type.clone(),
712 kind: ConflictKind::BothModified,
713 complexity,
714 ours_content: Some(ours_rc),
715 theirs_content: Some(theirs_rc),
716 base_content: Some(base_rc),
717 }), ResolutionStrategy::ConflictBothModified)
718 }
719 }
720 }
721 }
722 }
723 }
724
725 (Some(_base), Some(ours), None) => {
727 let ours_modified = ours.content_hash != _base.content_hash;
728 if ours_modified {
729 stats.entities_conflicted += 1;
731 let ours_rc = region_content(ours, ours_region_content);
732 let base_rc = region_content(_base, base_region_content);
733 let complexity = classify_conflict(Some(&base_rc), Some(&ours_rc), None);
734 (ResolvedEntity::Conflict(EntityConflict {
735 entity_name: ours.name.clone(),
736 entity_type: ours.entity_type.clone(),
737 kind: ConflictKind::ModifyDelete {
738 modified_in_ours: true,
739 },
740 complexity,
741 ours_content: Some(ours_rc),
742 theirs_content: None,
743 base_content: Some(base_rc),
744 }), ResolutionStrategy::ConflictModifyDelete)
745 } else {
746 stats.entities_deleted += 1;
748 (ResolvedEntity::Deleted, ResolutionStrategy::Deleted)
749 }
750 }
751
752 (Some(_base), None, Some(theirs)) => {
754 let theirs_modified = theirs.content_hash != _base.content_hash;
755 if theirs_modified {
756 stats.entities_conflicted += 1;
758 let theirs_rc = region_content(theirs, theirs_region_content);
759 let base_rc = region_content(_base, base_region_content);
760 let complexity = classify_conflict(Some(&base_rc), None, Some(&theirs_rc));
761 (ResolvedEntity::Conflict(EntityConflict {
762 entity_name: theirs.name.clone(),
763 entity_type: theirs.entity_type.clone(),
764 kind: ConflictKind::ModifyDelete {
765 modified_in_ours: false,
766 },
767 complexity,
768 ours_content: None,
769 theirs_content: Some(theirs_rc),
770 base_content: Some(base_rc),
771 }), ResolutionStrategy::ConflictModifyDelete)
772 } else {
773 stats.entities_deleted += 1;
775 (ResolvedEntity::Deleted, ResolutionStrategy::Deleted)
776 }
777 }
778
779 (None, Some(ours), None) => {
781 stats.entities_added_ours += 1;
782 (ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::AddedOurs)
783 }
784
785 (None, None, Some(theirs)) => {
787 stats.entities_added_theirs += 1;
788 (ResolvedEntity::Clean(entity_to_region_with_content(theirs, ®ion_content(theirs, theirs_region_content))), ResolutionStrategy::AddedTheirs)
789 }
790
791 (None, Some(ours), Some(theirs)) => {
793 if ours.content_hash == theirs.content_hash {
794 stats.entities_added_ours += 1;
796 (ResolvedEntity::Clean(entity_to_region_with_content(ours, ®ion_content(ours, ours_region_content))), ResolutionStrategy::ContentEqual)
797 } else {
798 stats.entities_conflicted += 1;
800 let ours_rc = region_content(ours, ours_region_content);
801 let theirs_rc = region_content(theirs, theirs_region_content);
802 let complexity = classify_conflict(None, Some(&ours_rc), Some(&theirs_rc));
803 (ResolvedEntity::Conflict(EntityConflict {
804 entity_name: ours.name.clone(),
805 entity_type: ours.entity_type.clone(),
806 kind: ConflictKind::BothAdded,
807 complexity,
808 ours_content: Some(ours_rc),
809 theirs_content: Some(theirs_rc),
810 base_content: None,
811 }), ResolutionStrategy::ConflictBothAdded)
812 }
813 }
814
815 (Some(_), None, None) => {
817 stats.entities_deleted += 1;
818 (ResolvedEntity::Deleted, ResolutionStrategy::Deleted)
819 }
820
821 (None, None, None) => (ResolvedEntity::Deleted, ResolutionStrategy::Deleted),
823 }
824}
825
826fn entity_to_region_with_content(entity: &SemanticEntity, content: &str) -> EntityRegion {
827 EntityRegion {
828 entity_id: entity.id.clone(),
829 entity_name: entity.name.clone(),
830 entity_type: entity.entity_type.clone(),
831 content: content.to_string(),
832 start_line: entity.start_line,
833 end_line: entity.end_line,
834 }
835}
836
837fn build_region_content_map(regions: &[FileRegion]) -> HashMap<&str, &str> {
841 regions
842 .iter()
843 .filter_map(|r| match r {
844 FileRegion::Entity(e) => Some((e.entity_id.as_str(), e.content.as_str())),
845 _ => None,
846 })
847 .collect()
848}
849
850fn is_whitespace_only_diff(a: &str, b: &str) -> bool {
853 if a == b {
854 return true; }
856 let a_normalized: Vec<&str> = a.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
857 let b_normalized: Vec<&str> = b.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
858 a_normalized == b_normalized
859}
860
861fn is_decorator_line(line: &str) -> bool {
864 let trimmed = line.trim();
865 trimmed.starts_with('@')
866 && !trimmed.starts_with("@param")
867 && !trimmed.starts_with("@return")
868 && !trimmed.starts_with("@type")
869 && !trimmed.starts_with("@see")
870}
871
872fn split_decorators(content: &str) -> (Vec<&str>, &str) {
874 let mut decorator_end = 0;
875 let mut byte_offset = 0;
876 for line in content.lines() {
877 if is_decorator_line(line) || line.trim().is_empty() {
878 decorator_end += 1;
879 byte_offset += line.len() + 1; } else {
881 break;
882 }
883 }
884 let lines: Vec<&str> = content.lines().collect();
886 while decorator_end > 0 && lines.get(decorator_end - 1).map_or(false, |l| l.trim().is_empty()) {
887 byte_offset -= lines[decorator_end - 1].len() + 1;
888 decorator_end -= 1;
889 }
890 let decorators: Vec<&str> = lines[..decorator_end]
891 .iter()
892 .filter(|l| is_decorator_line(l))
893 .copied()
894 .collect();
895 let body = &content[byte_offset.min(content.len())..];
896 (decorators, body)
897}
898
899fn try_decorator_aware_merge(base: &str, ours: &str, theirs: &str) -> Option<String> {
905 let (base_decorators, base_body) = split_decorators(base);
906 let (ours_decorators, ours_body) = split_decorators(ours);
907 let (theirs_decorators, theirs_body) = split_decorators(theirs);
908
909 if ours_decorators.is_empty() && theirs_decorators.is_empty() {
911 return None;
912 }
913
914 let merged_body = if base_body == ours_body && base_body == theirs_body {
916 base_body.to_string()
917 } else if base_body == ours_body {
918 theirs_body.to_string()
919 } else if base_body == theirs_body {
920 ours_body.to_string()
921 } else {
922 diffy_merge(base_body, ours_body, theirs_body)?
924 };
925
926 let base_set: HashSet<&str> = base_decorators.iter().copied().collect();
928 let ours_set: HashSet<&str> = ours_decorators.iter().copied().collect();
929 let theirs_set: HashSet<&str> = theirs_decorators.iter().copied().collect();
930
931 let ours_deleted: HashSet<&str> = base_set.difference(&ours_set).copied().collect();
933 let theirs_deleted: HashSet<&str> = base_set.difference(&theirs_set).copied().collect();
934
935 let mut merged_decorators: Vec<&str> = base_decorators
937 .iter()
938 .filter(|d| !ours_deleted.contains(**d) && !theirs_deleted.contains(**d))
939 .copied()
940 .collect();
941
942 for d in &ours_decorators {
944 if !base_set.contains(d) && !merged_decorators.contains(d) {
945 merged_decorators.push(d);
946 }
947 }
948 for d in &theirs_decorators {
950 if !base_set.contains(d) && !merged_decorators.contains(d) {
951 merged_decorators.push(d);
952 }
953 }
954
955 let mut result = String::new();
957 for d in &merged_decorators {
958 result.push_str(d);
959 result.push('\n');
960 }
961 result.push_str(&merged_body);
962
963 Some(result)
964}
965
966fn diffy_merge(base: &str, ours: &str, theirs: &str) -> Option<String> {
968 let result = diffy::merge(base, ours, theirs);
969 match result {
970 Ok(merged) => Some(merged),
971 Err(_conflicted) => None,
972 }
973}
974
975fn git_merge_string(base: &str, ours: &str, theirs: &str) -> Option<String> {
979 let dir = tempfile::tempdir().ok()?;
980 let base_path = dir.path().join("base");
981 let ours_path = dir.path().join("ours");
982 let theirs_path = dir.path().join("theirs");
983
984 std::fs::write(&base_path, base).ok()?;
985 std::fs::write(&ours_path, ours).ok()?;
986 std::fs::write(&theirs_path, theirs).ok()?;
987
988 let output = Command::new("git")
989 .arg("merge-file")
990 .arg("-p")
991 .arg(&ours_path)
992 .arg(&base_path)
993 .arg(&theirs_path)
994 .output()
995 .ok()?;
996
997 if output.status.success() {
998 String::from_utf8(output.stdout).ok()
999 } else {
1000 None
1001 }
1002}
1003
1004fn merge_interstitials(
1009 base_regions: &[FileRegion],
1010 ours_regions: &[FileRegion],
1011 theirs_regions: &[FileRegion],
1012 marker_format: &MarkerFormat,
1013) -> (HashMap<String, String>, Vec<EntityConflict>) {
1014 let base_map: HashMap<&str, &str> = base_regions
1015 .iter()
1016 .filter_map(|r| match r {
1017 FileRegion::Interstitial(i) => Some((i.position_key.as_str(), i.content.as_str())),
1018 _ => None,
1019 })
1020 .collect();
1021
1022 let ours_map: HashMap<&str, &str> = ours_regions
1023 .iter()
1024 .filter_map(|r| match r {
1025 FileRegion::Interstitial(i) => Some((i.position_key.as_str(), i.content.as_str())),
1026 _ => None,
1027 })
1028 .collect();
1029
1030 let theirs_map: HashMap<&str, &str> = theirs_regions
1031 .iter()
1032 .filter_map(|r| match r {
1033 FileRegion::Interstitial(i) => Some((i.position_key.as_str(), i.content.as_str())),
1034 _ => None,
1035 })
1036 .collect();
1037
1038 let mut all_keys: HashSet<&str> = HashSet::new();
1039 all_keys.extend(base_map.keys());
1040 all_keys.extend(ours_map.keys());
1041 all_keys.extend(theirs_map.keys());
1042
1043 let mut merged: HashMap<String, String> = HashMap::new();
1044 let mut interstitial_conflicts: Vec<EntityConflict> = Vec::new();
1045
1046 for key in all_keys {
1047 let base_content = base_map.get(key).copied().unwrap_or("");
1048 let ours_content = ours_map.get(key).copied().unwrap_or("");
1049 let theirs_content = theirs_map.get(key).copied().unwrap_or("");
1050
1051 if ours_content == theirs_content {
1053 merged.insert(key.to_string(), ours_content.to_string());
1054 } else if base_content == ours_content {
1055 merged.insert(key.to_string(), theirs_content.to_string());
1056 } else if base_content == theirs_content {
1057 merged.insert(key.to_string(), ours_content.to_string());
1058 } else {
1059 if is_import_region(base_content)
1061 || is_import_region(ours_content)
1062 || is_import_region(theirs_content)
1063 {
1064 let result = merge_imports_commutatively(base_content, ours_content, theirs_content);
1066 merged.insert(key.to_string(), result);
1067 } else {
1068 match diffy::merge(base_content, ours_content, theirs_content) {
1070 Ok(m) => {
1071 merged.insert(key.to_string(), m);
1072 }
1073 Err(_conflicted) => {
1074 let complexity = classify_conflict(
1077 Some(base_content),
1078 Some(ours_content),
1079 Some(theirs_content),
1080 );
1081 let conflict = EntityConflict {
1082 entity_name: key.to_string(),
1083 entity_type: "interstitial".to_string(),
1084 kind: ConflictKind::BothModified,
1085 complexity,
1086 ours_content: Some(ours_content.to_string()),
1087 theirs_content: Some(theirs_content.to_string()),
1088 base_content: Some(base_content.to_string()),
1089 };
1090 merged.insert(key.to_string(), conflict.to_conflict_markers(marker_format));
1091 interstitial_conflicts.push(conflict);
1092 }
1093 }
1094 }
1095 }
1096 }
1097
1098 (merged, interstitial_conflicts)
1099}
1100
1101fn is_import_region(content: &str) -> bool {
1105 let lines: Vec<&str> = content
1106 .lines()
1107 .filter(|l| !l.trim().is_empty())
1108 .collect();
1109 if lines.is_empty() {
1110 return false;
1111 }
1112 let mut import_count = 0;
1113 let mut in_multiline_import = false;
1114 for line in &lines {
1115 if in_multiline_import {
1116 import_count += 1;
1117 let trimmed = line.trim();
1118 if trimmed.starts_with('}') || trimmed.ends_with(')') {
1119 in_multiline_import = false;
1120 }
1121 } else if is_import_line(line) {
1122 import_count += 1;
1123 let trimmed = line.trim();
1124 if (trimmed.contains('{') && !trimmed.contains('}'))
1126 || (trimmed.starts_with("import (") && !trimmed.contains(')'))
1127 {
1128 in_multiline_import = true;
1129 }
1130 }
1131 }
1132 import_count * 2 > lines.len()
1134}
1135
1136fn post_merge_cleanup(content: &str) -> String {
1144 let lines: Vec<&str> = content.lines().collect();
1145 let mut result: Vec<&str> = Vec::with_capacity(lines.len());
1146
1147 for line in &lines {
1151 if line.trim().is_empty() {
1152 result.push(line);
1153 continue;
1154 }
1155 if let Some(prev) = result.last() {
1156 if !prev.trim().is_empty() && *prev == *line && looks_like_declaration(line) {
1157 continue; }
1159 }
1160 result.push(line);
1161 }
1162
1163 let mut final_lines: Vec<&str> = Vec::with_capacity(result.len());
1165 let mut consecutive_blanks = 0;
1166 for line in &result {
1167 if line.trim().is_empty() {
1168 consecutive_blanks += 1;
1169 if consecutive_blanks <= 2 {
1170 final_lines.push(line);
1171 }
1172 } else {
1173 consecutive_blanks = 0;
1174 final_lines.push(line);
1175 }
1176 }
1177
1178 let mut out = final_lines.join("\n");
1179 if content.ends_with('\n') && !out.ends_with('\n') {
1180 out.push('\n');
1181 }
1182 out
1183}
1184
1185fn looks_like_declaration(line: &str) -> bool {
1189 let trimmed = line.trim();
1190 trimmed.starts_with("import ")
1191 || trimmed.starts_with("from ")
1192 || trimmed.starts_with("use ")
1193 || trimmed.starts_with("export ")
1194 || trimmed.starts_with("require(")
1195 || trimmed.starts_with("#include")
1196 || trimmed.starts_with("typedef ")
1197 || trimmed.starts_with("using ")
1198 || (trimmed.starts_with("pub ") && trimmed.contains("mod "))
1199}
1200
1201fn is_import_line(line: &str) -> bool {
1206 if line.starts_with(' ') || line.starts_with('\t') {
1208 return false;
1209 }
1210 let trimmed = line.trim();
1211 trimmed.starts_with("import ")
1212 || trimmed.starts_with("from ")
1213 || trimmed.starts_with("use ")
1214 || trimmed.starts_with("require(")
1215 || trimmed.starts_with("const ") && trimmed.contains("require(")
1216 || trimmed.starts_with("package ")
1217 || trimmed.starts_with("#include ")
1218 || trimmed.starts_with("using ")
1219}
1220
1221#[derive(Debug, Clone)]
1223struct ImportStatement {
1224 lines: Vec<String>,
1226 source: String,
1228 specifiers: Vec<String>,
1230 is_multiline: bool,
1232}
1233
1234fn parse_import_statements(content: &str) -> (Vec<ImportStatement>, Vec<String>) {
1236 let mut imports: Vec<ImportStatement> = Vec::new();
1237 let mut non_import_lines: Vec<String> = Vec::new();
1238 let lines: Vec<&str> = content.lines().collect();
1239 let mut i = 0;
1240
1241 while i < lines.len() {
1242 let line = lines[i];
1243
1244 if line.trim().is_empty() {
1245 non_import_lines.push(line.to_string());
1246 i += 1;
1247 continue;
1248 }
1249
1250 if is_import_line(line) {
1251 let trimmed = line.trim();
1252 let starts_multiline = (trimmed.contains('{') && !trimmed.contains('}'))
1254 || (trimmed.starts_with("import (") && !trimmed.contains(')'));
1255
1256 if starts_multiline {
1257 let mut block_lines = vec![line.to_string()];
1258 let mut specifiers = Vec::new();
1259 let close_char = if trimmed.contains('{') { '}' } else { ')' };
1260 i += 1;
1261
1262 while i < lines.len() {
1264 let inner = lines[i];
1265 block_lines.push(inner.to_string());
1266 let inner_trimmed = inner.trim();
1267
1268 if inner_trimmed.starts_with(close_char) {
1269 break;
1271 } else if !inner_trimmed.is_empty() {
1272 let spec = inner_trimmed.trim_end_matches(',').trim().to_string();
1274 if !spec.is_empty() {
1275 specifiers.push(spec);
1276 }
1277 }
1278 i += 1;
1279 }
1280
1281 let full_text = block_lines.join("\n");
1282 let source = import_source_prefix(&full_text).to_string();
1283 imports.push(ImportStatement {
1284 lines: block_lines,
1285 source,
1286 specifiers,
1287 is_multiline: true,
1288 });
1289 } else {
1290 let source = import_source_prefix(line).to_string();
1292 imports.push(ImportStatement {
1293 lines: vec![line.to_string()],
1294 source,
1295 specifiers: Vec::new(),
1296 is_multiline: false,
1297 });
1298 }
1299 } else {
1300 non_import_lines.push(line.to_string());
1301 }
1302 i += 1;
1303 }
1304
1305 (imports, non_import_lines)
1306}
1307
1308fn merge_imports_commutatively(base: &str, ours: &str, theirs: &str) -> String {
1314 let (base_imports, _) = parse_import_statements(base);
1315 let (ours_imports, _) = parse_import_statements(ours);
1316 let (theirs_imports, _) = parse_import_statements(theirs);
1317
1318 let has_multiline = base_imports.iter().any(|i| i.is_multiline)
1319 || ours_imports.iter().any(|i| i.is_multiline)
1320 || theirs_imports.iter().any(|i| i.is_multiline);
1321
1322 if has_multiline {
1323 return merge_imports_with_multiline(base, ours, theirs,
1324 &base_imports, &ours_imports, &theirs_imports);
1325 }
1326
1327 let base_lines: HashSet<&str> = base.lines().filter(|l| is_import_line(l)).collect();
1329 let ours_lines: HashSet<&str> = ours.lines().filter(|l| is_import_line(l)).collect();
1330
1331 let theirs_deleted: HashSet<&str> = base_lines.difference(
1332 &theirs.lines().filter(|l| is_import_line(l)).collect::<HashSet<&str>>()
1333 ).copied().collect();
1334
1335 let theirs_added: Vec<&str> = theirs
1336 .lines()
1337 .filter(|l| is_import_line(l) && !base_lines.contains(l) && !ours_lines.contains(l))
1338 .collect();
1339
1340 let mut groups: Vec<Vec<&str>> = Vec::new();
1341 let mut current_group: Vec<&str> = Vec::new();
1342
1343 for line in ours.lines() {
1344 if line.trim().is_empty() {
1345 if !current_group.is_empty() {
1346 groups.push(current_group);
1347 current_group = Vec::new();
1348 }
1349 } else if is_import_line(line) {
1350 if theirs_deleted.contains(line) {
1351 continue;
1352 }
1353 current_group.push(line);
1354 } else {
1355 current_group.push(line);
1356 }
1357 }
1358 if !current_group.is_empty() {
1359 groups.push(current_group);
1360 }
1361
1362 for add in &theirs_added {
1363 let prefix = import_source_prefix(add);
1364 let mut best_group = if groups.is_empty() { 0 } else { groups.len() - 1 };
1365 for (i, group) in groups.iter().enumerate() {
1366 if group.iter().any(|l| {
1367 is_import_line(l) && import_source_prefix(l) == prefix
1368 }) {
1369 best_group = i;
1370 break;
1371 }
1372 }
1373 if best_group < groups.len() {
1374 groups[best_group].push(add);
1375 } else {
1376 groups.push(vec![add]);
1377 }
1378 }
1379
1380 let mut result_lines: Vec<&str> = Vec::new();
1381 for (i, group) in groups.iter().enumerate() {
1382 if i > 0 {
1383 result_lines.push("");
1384 }
1385 result_lines.extend(group);
1386 }
1387
1388 let mut result = result_lines.join("\n");
1389 let ours_trailing = ours.len() - ours.trim_end_matches('\n').len();
1390 let result_trailing = result.len() - result.trim_end_matches('\n').len();
1391 for _ in result_trailing..ours_trailing {
1392 result.push('\n');
1393 }
1394 result
1395}
1396
1397fn merge_imports_with_multiline(
1400 _base_raw: &str,
1401 ours_raw: &str,
1402 _theirs_raw: &str,
1403 base_imports: &[ImportStatement],
1404 ours_imports: &[ImportStatement],
1405 theirs_imports: &[ImportStatement],
1406) -> String {
1407 let base_specs: HashMap<&str, HashSet<&str>> = base_imports.iter().map(|imp| {
1409 let specs: HashSet<&str> = imp.specifiers.iter().map(|s| s.as_str()).collect();
1410 (imp.source.as_str(), specs)
1411 }).collect();
1412
1413 let theirs_specs: HashMap<&str, HashSet<&str>> = theirs_imports.iter().map(|imp| {
1414 let specs: HashSet<&str> = imp.specifiers.iter().map(|s| s.as_str()).collect();
1415 (imp.source.as_str(), specs)
1416 }).collect();
1417
1418 let base_single: HashSet<String> = base_imports.iter()
1420 .filter(|i| !i.is_multiline)
1421 .map(|i| i.lines[0].clone())
1422 .collect();
1423 let theirs_single: HashSet<String> = theirs_imports.iter()
1424 .filter(|i| !i.is_multiline)
1425 .map(|i| i.lines[0].clone())
1426 .collect();
1427 let theirs_deleted_single: HashSet<&str> = base_single.iter()
1428 .filter(|l| !theirs_single.contains(l.as_str()))
1429 .map(|l| l.as_str())
1430 .collect();
1431
1432 let mut result_parts: Vec<String> = Vec::new();
1434 let mut handled_theirs_sources: HashSet<&str> = HashSet::new();
1435
1436 let lines: Vec<&str> = ours_raw.lines().collect();
1438 let mut i = 0;
1439 let mut ours_imp_idx = 0;
1440
1441 while i < lines.len() {
1442 let line = lines[i];
1443
1444 if line.trim().is_empty() {
1445 result_parts.push(line.to_string());
1446 i += 1;
1447 continue;
1448 }
1449
1450 if is_import_line(line) {
1451 let trimmed = line.trim();
1452 let starts_multiline = (trimmed.contains('{') && !trimmed.contains('}'))
1453 || (trimmed.starts_with("import (") && !trimmed.contains(')'));
1454
1455 if starts_multiline && ours_imp_idx < ours_imports.len() {
1456 let imp = &ours_imports[ours_imp_idx];
1457 let source = imp.source.as_str();
1459 handled_theirs_sources.insert(source);
1460
1461 let base_spec_set = base_specs.get(source).cloned().unwrap_or_default();
1463 let theirs_spec_set = theirs_specs.get(source).cloned().unwrap_or_default();
1464 let theirs_added: HashSet<&str> = theirs_spec_set.difference(&base_spec_set).copied().collect();
1466 let theirs_removed: HashSet<&str> = base_spec_set.difference(&theirs_spec_set).copied().collect();
1468
1469 let mut final_specs: Vec<&str> = imp.specifiers.iter()
1471 .map(|s| s.as_str())
1472 .filter(|s| !theirs_removed.contains(s))
1473 .collect();
1474 for added in &theirs_added {
1475 if !final_specs.contains(added) {
1476 final_specs.push(added);
1477 }
1478 }
1479
1480 let indent = if imp.lines.len() > 1 {
1482 let second = &imp.lines[1];
1483 &second[..second.len() - second.trim_start().len()]
1484 } else {
1485 " "
1486 };
1487
1488 result_parts.push(imp.lines[0].clone()); for spec in &final_specs {
1491 result_parts.push(format!("{}{},", indent, spec));
1492 }
1493 if let Some(last) = imp.lines.last() {
1495 result_parts.push(last.clone());
1496 }
1497
1498 let close_char = if trimmed.contains('{') { '}' } else { ')' };
1500 i += 1;
1501 while i < lines.len() {
1502 if lines[i].trim().starts_with(close_char) {
1503 i += 1;
1504 break;
1505 }
1506 i += 1;
1507 }
1508 ours_imp_idx += 1;
1509 continue;
1510 } else {
1511 if ours_imp_idx < ours_imports.len() {
1513 let imp = &ours_imports[ours_imp_idx];
1514 handled_theirs_sources.insert(imp.source.as_str());
1515 ours_imp_idx += 1;
1516 }
1517 if !theirs_deleted_single.contains(line) {
1519 result_parts.push(line.to_string());
1520 }
1521 }
1522 } else {
1523 result_parts.push(line.to_string());
1524 }
1525 i += 1;
1526 }
1527
1528 for imp in theirs_imports {
1530 if handled_theirs_sources.contains(imp.source.as_str()) {
1531 continue;
1532 }
1533 if base_specs.contains_key(imp.source.as_str()) {
1535 continue;
1536 }
1537 for line in &imp.lines {
1539 result_parts.push(line.clone());
1540 }
1541 }
1542
1543 let mut result = result_parts.join("\n");
1544 let ours_trailing = ours_raw.len() - ours_raw.trim_end_matches('\n').len();
1545 let result_trailing = result.len() - result.trim_end_matches('\n').len();
1546 for _ in result_trailing..ours_trailing {
1547 result.push('\n');
1548 }
1549 result
1550}
1551
1552fn import_source_prefix(line: &str) -> &str {
1557 for l in line.lines() {
1560 let trimmed = l.trim();
1561 if let Some(rest) = trimmed.strip_prefix("from ") {
1563 return rest.split_whitespace().next().unwrap_or("");
1564 }
1565 if trimmed.starts_with('}') && trimmed.contains("from ") {
1567 if let Some(quote_start) = trimmed.find(|c: char| c == '\'' || c == '"') {
1568 let after = &trimmed[quote_start + 1..];
1569 if let Some(quote_end) = after.find(|c: char| c == '\'' || c == '"') {
1570 return &after[..quote_end];
1571 }
1572 }
1573 }
1574 if trimmed.starts_with("import ") {
1576 if let Some(quote_start) = trimmed.find(|c: char| c == '\'' || c == '"') {
1577 let after = &trimmed[quote_start + 1..];
1578 if let Some(quote_end) = after.find(|c: char| c == '\'' || c == '"') {
1579 return &after[..quote_end];
1580 }
1581 }
1582 }
1583 if let Some(rest) = trimmed.strip_prefix("use ") {
1585 return rest.split("::").next().unwrap_or("").trim_end_matches(';');
1586 }
1587 }
1588 line.trim()
1589}
1590
1591fn line_level_fallback(base: &str, ours: &str, theirs: &str, file_path: &str) -> MergeResult {
1603 let mut stats = MergeStats::default();
1604 stats.used_fallback = true;
1605
1606 let skip = skip_sesame(file_path);
1608
1609 if skip {
1610 return git_merge_file(base, ours, theirs, &mut stats);
1614 }
1615
1616 let base_expanded = expand_separators(base);
1619 let ours_expanded = expand_separators(ours);
1620 let theirs_expanded = expand_separators(theirs);
1621
1622 let sesame_result = match diffy::merge(&base_expanded, &ours_expanded, &theirs_expanded) {
1623 Ok(merged) => {
1624 let content = collapse_separators(&merged, base);
1625 Some(MergeResult {
1626 content: post_merge_cleanup(&content),
1627 conflicts: vec![],
1628 warnings: vec![],
1629 stats: stats.clone(),
1630 audit: vec![],
1631 })
1632 }
1633 Err(_) => {
1634 match diffy::merge(base, ours, theirs) {
1636 Ok(merged) => Some(MergeResult {
1637 content: merged,
1638 conflicts: vec![],
1639 warnings: vec![],
1640 stats: stats.clone(),
1641 audit: vec![],
1642 }),
1643 Err(conflicted) => {
1644 let _markers = conflicted.lines().filter(|l| l.starts_with("<<<<<<<")).count();
1645 let mut s = stats.clone();
1646 s.entities_conflicted = 1;
1647 Some(MergeResult {
1648 content: conflicted,
1649 conflicts: vec![EntityConflict {
1650 entity_name: "(file)".to_string(),
1651 entity_type: "file".to_string(),
1652 kind: ConflictKind::BothModified,
1653 complexity: classify_conflict(Some(base), Some(ours), Some(theirs)),
1654 ours_content: Some(ours.to_string()),
1655 theirs_content: Some(theirs.to_string()),
1656 base_content: Some(base.to_string()),
1657 }],
1658 warnings: vec![],
1659 stats: s,
1660 audit: vec![],
1661 })
1662 }
1663 }
1664 }
1665 };
1666
1667 let git_result = git_merge_file(base, ours, theirs, &mut stats);
1669
1670 match sesame_result {
1672 Some(sesame) if sesame.conflicts.is_empty() && !git_result.conflicts.is_empty() => {
1673 sesame
1675 }
1676 Some(sesame) if !sesame.conflicts.is_empty() && !git_result.conflicts.is_empty() => {
1677 let sesame_markers = sesame.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
1679 let git_markers = git_result.content.lines().filter(|l| l.starts_with("<<<<<<<")).count();
1680 if sesame_markers <= git_markers { sesame } else { git_result }
1681 }
1682 _ => git_result,
1683 }
1684}
1685
1686fn git_merge_file(base: &str, ours: &str, theirs: &str, stats: &mut MergeStats) -> MergeResult {
1692 let dir = match tempfile::tempdir() {
1693 Ok(d) => d,
1694 Err(_) => return diffy_fallback(base, ours, theirs, stats),
1695 };
1696
1697 let base_path = dir.path().join("base");
1698 let ours_path = dir.path().join("ours");
1699 let theirs_path = dir.path().join("theirs");
1700
1701 let write_ok = (|| -> std::io::Result<()> {
1702 std::fs::File::create(&base_path)?.write_all(base.as_bytes())?;
1703 std::fs::File::create(&ours_path)?.write_all(ours.as_bytes())?;
1704 std::fs::File::create(&theirs_path)?.write_all(theirs.as_bytes())?;
1705 Ok(())
1706 })();
1707
1708 if write_ok.is_err() {
1709 return diffy_fallback(base, ours, theirs, stats);
1710 }
1711
1712 let output = Command::new("git")
1714 .arg("merge-file")
1715 .arg("-p") .arg("--diff3") .arg("-L").arg("ours")
1718 .arg("-L").arg("base")
1719 .arg("-L").arg("theirs")
1720 .arg(&ours_path)
1721 .arg(&base_path)
1722 .arg(&theirs_path)
1723 .output();
1724
1725 match output {
1726 Ok(result) => {
1727 let content = String::from_utf8_lossy(&result.stdout).into_owned();
1728 if result.status.success() {
1729 MergeResult {
1731 content: post_merge_cleanup(&content),
1732 conflicts: vec![],
1733 warnings: vec![],
1734 stats: stats.clone(),
1735 audit: vec![],
1736 }
1737 } else {
1738 stats.entities_conflicted = 1;
1740 MergeResult {
1741 content,
1742 conflicts: vec![EntityConflict {
1743 entity_name: "(file)".to_string(),
1744 entity_type: "file".to_string(),
1745 kind: ConflictKind::BothModified,
1746 complexity: classify_conflict(Some(base), Some(ours), Some(theirs)),
1747 ours_content: Some(ours.to_string()),
1748 theirs_content: Some(theirs.to_string()),
1749 base_content: Some(base.to_string()),
1750 }],
1751 warnings: vec![],
1752 stats: stats.clone(),
1753 audit: vec![],
1754 }
1755 }
1756 }
1757 Err(_) => diffy_fallback(base, ours, theirs, stats),
1759 }
1760}
1761
1762fn diffy_fallback(base: &str, ours: &str, theirs: &str, stats: &mut MergeStats) -> MergeResult {
1764 match diffy::merge(base, ours, theirs) {
1765 Ok(merged) => {
1766 let content = post_merge_cleanup(&merged);
1767 MergeResult {
1768 content,
1769 conflicts: vec![],
1770 warnings: vec![],
1771 stats: stats.clone(),
1772 audit: vec![],
1773 }
1774 }
1775 Err(conflicted) => {
1776 stats.entities_conflicted = 1;
1777 MergeResult {
1778 content: conflicted,
1779 conflicts: vec![EntityConflict {
1780 entity_name: "(file)".to_string(),
1781 entity_type: "file".to_string(),
1782 kind: ConflictKind::BothModified,
1783 complexity: classify_conflict(Some(base), Some(ours), Some(theirs)),
1784 ours_content: Some(ours.to_string()),
1785 theirs_content: Some(theirs.to_string()),
1786 base_content: Some(base.to_string()),
1787 }],
1788 warnings: vec![],
1789 stats: stats.clone(),
1790 audit: vec![],
1791 }
1792 }
1793 }
1794}
1795
1796fn has_excessive_duplicates(entities: &[SemanticEntity]) -> bool {
1804 let threshold = std::env::var("WEAVE_MAX_DUPLICATES")
1805 .ok()
1806 .and_then(|v| v.parse::<usize>().ok())
1807 .unwrap_or(10);
1808 let mut counts: HashMap<&str, usize> = HashMap::new();
1809 for e in entities {
1810 *counts.entry(&e.name).or_default() += 1;
1811 }
1812 counts.values().any(|&c| c >= threshold)
1813}
1814
1815fn filter_nested_entities(mut entities: Vec<SemanticEntity>) -> Vec<SemanticEntity> {
1818 if entities.len() <= 1 {
1819 return entities;
1820 }
1821
1822 entities.sort_by(|a, b| {
1825 a.start_line.cmp(&b.start_line).then(b.end_line.cmp(&a.end_line))
1826 });
1827
1828 let mut result: Vec<SemanticEntity> = Vec::with_capacity(entities.len());
1830 let mut max_end: usize = 0;
1831
1832 for entity in entities {
1833 if entity.start_line > max_end || max_end == 0 {
1834 max_end = entity.end_line;
1836 result.push(entity);
1837 } else if entity.start_line == result.last().map_or(0, |e| e.start_line)
1838 && entity.end_line == result.last().map_or(0, |e| e.end_line)
1839 {
1840 result.push(entity);
1842 }
1843 }
1845
1846 result
1847}
1848
1849fn get_child_entities<'a>(
1851 parent: &SemanticEntity,
1852 all_entities: &'a [SemanticEntity],
1853) -> Vec<&'a SemanticEntity> {
1854 let mut children: Vec<&SemanticEntity> = all_entities
1855 .iter()
1856 .filter(|e| e.parent_id.as_deref() == Some(&parent.id))
1857 .collect();
1858 children.sort_by_key(|e| e.start_line);
1859 children
1860}
1861
1862fn body_hash(entity: &SemanticEntity) -> u64 {
1870 use std::collections::hash_map::DefaultHasher;
1871 use std::hash::{Hash, Hasher};
1872 let normalized = replace_at_word_boundaries(&entity.content, &entity.name, "__ENTITY__");
1873 let mut hasher = DefaultHasher::new();
1874 normalized.hash(&mut hasher);
1875 hasher.finish()
1876}
1877
1878fn replace_at_word_boundaries(content: &str, needle: &str, replacement: &str) -> String {
1882 if needle.is_empty() {
1883 return content.to_string();
1884 }
1885 let bytes = content.as_bytes();
1886 let mut result = String::with_capacity(content.len());
1887 let mut i = 0;
1888 while i < content.len() {
1889 if content.is_char_boundary(i) && content[i..].starts_with(needle) {
1890 let before_ok = i == 0 || {
1891 let prev_idx = content[..i]
1892 .char_indices()
1893 .next_back()
1894 .map(|(idx, _)| idx)
1895 .unwrap_or(0);
1896 !is_ident_char(bytes[prev_idx])
1897 };
1898 let after_idx = i + needle.len();
1899 let after_ok = after_idx >= content.len()
1900 || (content.is_char_boundary(after_idx)
1901 && !is_ident_char(bytes[after_idx]));
1902 if before_ok && after_ok {
1903 result.push_str(replacement);
1904 i += needle.len();
1905 continue;
1906 }
1907 }
1908 if content.is_char_boundary(i) {
1909 let ch = content[i..].chars().next().unwrap();
1910 result.push(ch);
1911 i += ch.len_utf8();
1912 } else {
1913 i += 1;
1914 }
1915 }
1916 result
1917}
1918
1919fn is_ident_char(b: u8) -> bool {
1920 b.is_ascii_alphanumeric() || b == b'_'
1921}
1922
1923fn build_rename_map(
1930 base_entities: &[SemanticEntity],
1931 branch_entities: &[SemanticEntity],
1932) -> HashMap<String, String> {
1933 let mut rename_map: HashMap<String, String> = HashMap::new();
1934
1935 let base_ids: HashSet<&str> = base_entities.iter().map(|e| e.id.as_str()).collect();
1936
1937 let mut base_by_body: HashMap<u64, Vec<&SemanticEntity>> = HashMap::new();
1939 for entity in base_entities {
1940 base_by_body.entry(body_hash(entity)).or_default().push(entity);
1941 }
1942
1943 let mut base_by_structural: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
1945 for entity in base_entities {
1946 if let Some(ref sh) = entity.structural_hash {
1947 base_by_structural.entry(sh.as_str()).or_default().push(entity);
1948 }
1949 }
1950
1951 struct RenameCandidate<'a> {
1953 branch: &'a SemanticEntity,
1954 base: &'a SemanticEntity,
1955 confidence: f64,
1956 }
1957 let mut candidates: Vec<RenameCandidate> = Vec::new();
1958
1959 for branch_entity in branch_entities {
1960 if base_ids.contains(branch_entity.id.as_str()) {
1961 continue;
1962 }
1963
1964 let bh = body_hash(branch_entity);
1965
1966 if let Some(base_entities_for_hash) = base_by_body.get(&bh) {
1968 for &base_entity in base_entities_for_hash {
1969 let same_type = base_entity.entity_type == branch_entity.entity_type;
1970 let same_parent = base_entity.parent_id == branch_entity.parent_id;
1971 let confidence = match (same_type, same_parent) {
1972 (true, true) => 0.95,
1973 (true, false) => 0.8,
1974 (false, _) => 0.6,
1975 };
1976 candidates.push(RenameCandidate { branch: branch_entity, base: base_entity, confidence });
1977 }
1978 }
1979
1980 if let Some(ref sh) = branch_entity.structural_hash {
1982 if let Some(base_entities_for_sh) = base_by_structural.get(sh.as_str()) {
1983 for &base_entity in base_entities_for_sh {
1984 if candidates.iter().any(|c| c.branch.id == branch_entity.id && c.base.id == base_entity.id) {
1986 continue;
1987 }
1988 candidates.push(RenameCandidate { branch: branch_entity, base: base_entity, confidence: 0.6 });
1989 }
1990 }
1991 }
1992 }
1993
1994 candidates.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
1996
1997 let mut used_base_ids: HashSet<String> = HashSet::new();
1998 let mut used_branch_ids: HashSet<String> = HashSet::new();
1999
2000 for candidate in &candidates {
2001 if candidate.confidence < 0.6 {
2002 break;
2003 }
2004 if used_base_ids.contains(&candidate.base.id) || used_branch_ids.contains(&candidate.branch.id) {
2005 continue;
2006 }
2007 let base_id_in_branch = branch_entities.iter().any(|e| e.id == candidate.base.id);
2009 if base_id_in_branch {
2010 continue;
2011 }
2012 rename_map.insert(candidate.branch.id.clone(), candidate.base.id.clone());
2013 used_base_ids.insert(candidate.base.id.clone());
2014 used_branch_ids.insert(candidate.branch.id.clone());
2015 }
2016
2017 rename_map
2018}
2019
2020fn is_container_entity_type(entity_type: &str) -> bool {
2022 matches!(
2023 entity_type,
2024 "class" | "interface" | "enum" | "impl" | "trait" | "module" | "impl_item" | "trait_item"
2025 | "struct" | "union" | "namespace" | "struct_item" | "struct_specifier"
2026 | "variable" | "export"
2027 )
2028}
2029
2030#[derive(Debug, Clone)]
2032struct MemberChunk {
2033 name: String,
2035 content: String,
2037}
2038
2039struct InnerMergeResult {
2041 content: String,
2043 has_conflicts: bool,
2045}
2046
2047fn children_to_chunks(
2053 children: &[&SemanticEntity],
2054 container_content: &str,
2055 container_start_line: usize,
2056) -> Vec<MemberChunk> {
2057 if children.is_empty() {
2058 return Vec::new();
2059 }
2060
2061 let lines: Vec<&str> = container_content.lines().collect();
2062 let mut chunks = Vec::new();
2063
2064 for (i, child) in children.iter().enumerate() {
2065 let child_start_idx = child.start_line.saturating_sub(container_start_line);
2066 let child_end_idx = child.end_line.saturating_sub(container_start_line) + 1;
2068
2069 if child_end_idx > lines.len() + 1 || child_start_idx >= lines.len() {
2070 chunks.push(MemberChunk {
2072 name: child.name.clone(),
2073 content: child.content.clone(),
2074 });
2075 continue;
2076 }
2077 let child_end_idx = child_end_idx.min(lines.len());
2078
2079 let floor = if i > 0 {
2081 children[i - 1].end_line.saturating_sub(container_start_line) + 1
2082 } else {
2083 let header_end = lines
2086 .iter()
2087 .position(|l| l.contains('{') || l.trim().ends_with(':'))
2088 .map(|p| p + 1)
2089 .unwrap_or(0);
2090 header_end
2091 };
2092
2093 let mut content_start = child_start_idx;
2095 while content_start > floor {
2096 let prev = content_start - 1;
2097 let trimmed = lines[prev].trim();
2098 if trimmed.starts_with('@')
2099 || trimmed.starts_with("#[")
2100 || trimmed.starts_with("//")
2101 || trimmed.starts_with("///")
2102 || trimmed.starts_with("/**")
2103 || trimmed.starts_with("* ")
2104 || trimmed == "*/"
2105 {
2106 content_start = prev;
2107 } else if trimmed.is_empty() && content_start > floor + 1 {
2108 content_start = prev;
2110 } else {
2111 break;
2112 }
2113 }
2114
2115 while content_start < child_start_idx && lines[content_start].trim().is_empty() {
2117 content_start += 1;
2118 }
2119
2120 let chunk_content: String = lines[content_start..child_end_idx].join("\n");
2121 chunks.push(MemberChunk {
2122 name: child.name.clone(),
2123 content: chunk_content,
2124 });
2125 }
2126
2127 chunks
2128}
2129
2130fn scoped_conflict_marker(
2132 name: &str,
2133 base: Option<&str>,
2134 ours: Option<&str>,
2135 theirs: Option<&str>,
2136 ours_deleted: bool,
2137 theirs_deleted: bool,
2138 fmt: &MarkerFormat,
2139) -> String {
2140 let open = "<".repeat(fmt.marker_length);
2141 let sep = "=".repeat(fmt.marker_length);
2142 let close = ">".repeat(fmt.marker_length);
2143
2144 let o = ours.unwrap_or("");
2145 let t = theirs.unwrap_or("");
2146
2147 let ours_lines: Vec<&str> = o.lines().collect();
2149 let theirs_lines: Vec<&str> = t.lines().collect();
2150 let (prefix_len, suffix_len) = if ours.is_some() && theirs.is_some() {
2151 crate::conflict::narrow_conflict_lines(&ours_lines, &theirs_lines)
2152 } else {
2153 (0, 0)
2154 };
2155 let has_narrowing = prefix_len > 0 || suffix_len > 0;
2156 let ours_mid = &ours_lines[prefix_len..ours_lines.len() - suffix_len];
2157 let theirs_mid = &theirs_lines[prefix_len..theirs_lines.len() - suffix_len];
2158
2159 let mut out = String::new();
2160
2161 if has_narrowing {
2163 for line in &ours_lines[..prefix_len] {
2164 out.push_str(line);
2165 out.push('\n');
2166 }
2167 }
2168
2169 if fmt.enhanced {
2171 if ours_deleted {
2172 out.push_str(&format!("{} ours ({} deleted)\n", open, name));
2173 } else {
2174 out.push_str(&format!("{} ours ({})\n", open, name));
2175 }
2176 } else {
2177 out.push_str(&format!("{} ours\n", open));
2178 }
2179
2180 if ours.is_some() {
2182 if has_narrowing {
2183 for line in ours_mid {
2184 out.push_str(line);
2185 out.push('\n');
2186 }
2187 } else {
2188 out.push_str(o);
2189 if !o.ends_with('\n') {
2190 out.push('\n');
2191 }
2192 }
2193 }
2194
2195 if !fmt.enhanced {
2197 let base_marker = "|".repeat(fmt.marker_length);
2198 out.push_str(&format!("{} base\n", base_marker));
2199 let b = base.unwrap_or("");
2200 if has_narrowing {
2201 let base_lines: Vec<&str> = b.lines().collect();
2202 let base_prefix = prefix_len.min(base_lines.len());
2203 let base_suffix = suffix_len.min(base_lines.len().saturating_sub(base_prefix));
2204 for line in &base_lines[base_prefix..base_lines.len() - base_suffix] {
2205 out.push_str(line);
2206 out.push('\n');
2207 }
2208 } else {
2209 out.push_str(b);
2210 if !b.is_empty() && !b.ends_with('\n') {
2211 out.push('\n');
2212 }
2213 }
2214 }
2215
2216 out.push_str(&format!("{}\n", sep));
2218
2219 if theirs.is_some() {
2221 if has_narrowing {
2222 for line in theirs_mid {
2223 out.push_str(line);
2224 out.push('\n');
2225 }
2226 } else {
2227 out.push_str(t);
2228 if !t.ends_with('\n') {
2229 out.push('\n');
2230 }
2231 }
2232 }
2233
2234 if fmt.enhanced {
2236 if theirs_deleted {
2237 out.push_str(&format!("{} theirs ({} deleted)\n", close, name));
2238 } else {
2239 out.push_str(&format!("{} theirs ({})\n", close, name));
2240 }
2241 } else {
2242 out.push_str(&format!("{} theirs\n", close));
2243 }
2244
2245 if has_narrowing {
2247 for line in &ours_lines[ours_lines.len() - suffix_len..] {
2248 out.push_str(line);
2249 out.push('\n');
2250 }
2251 }
2252
2253 out
2254}
2255
2256fn try_inner_entity_merge(
2265 base: &str,
2266 ours: &str,
2267 theirs: &str,
2268 base_children: &[&SemanticEntity],
2269 ours_children: &[&SemanticEntity],
2270 theirs_children: &[&SemanticEntity],
2271 base_start_line: usize,
2272 ours_start_line: usize,
2273 theirs_start_line: usize,
2274 marker_format: &MarkerFormat,
2275) -> Option<InnerMergeResult> {
2276 let use_children = !ours_children.is_empty() || !theirs_children.is_empty();
2282 let (base_chunks, ours_chunks, theirs_chunks) = if use_children {
2283 (
2284 children_to_chunks(base_children, base, base_start_line),
2285 children_to_chunks(ours_children, ours, ours_start_line),
2286 children_to_chunks(theirs_children, theirs, theirs_start_line),
2287 )
2288 } else {
2289 (
2290 extract_member_chunks(base)?,
2291 extract_member_chunks(ours)?,
2292 extract_member_chunks(theirs)?,
2293 )
2294 };
2295
2296 if base_chunks.is_empty() && ours_chunks.is_empty() && theirs_chunks.is_empty() {
2299 return None;
2300 }
2301
2302 let base_map: HashMap<&str, &str> = base_chunks
2304 .iter()
2305 .map(|c| (c.name.as_str(), c.content.as_str()))
2306 .collect();
2307 let ours_map: HashMap<&str, &str> = ours_chunks
2308 .iter()
2309 .map(|c| (c.name.as_str(), c.content.as_str()))
2310 .collect();
2311 let theirs_map: HashMap<&str, &str> = theirs_chunks
2312 .iter()
2313 .map(|c| (c.name.as_str(), c.content.as_str()))
2314 .collect();
2315
2316 let mut all_names: Vec<String> = Vec::new();
2318 let mut seen: HashSet<String> = HashSet::new();
2319 for chunk in &ours_chunks {
2321 if seen.insert(chunk.name.clone()) {
2322 all_names.push(chunk.name.clone());
2323 }
2324 }
2325 for chunk in &theirs_chunks {
2327 if seen.insert(chunk.name.clone()) {
2328 all_names.push(chunk.name.clone());
2329 }
2330 }
2331
2332 let (ours_header, ours_footer) = extract_container_wrapper(ours)?;
2334
2335 let mut merged_members: Vec<String> = Vec::new();
2336 let mut has_conflict = false;
2337
2338 for name in &all_names {
2339 let in_base = base_map.get(name.as_str());
2340 let in_ours = ours_map.get(name.as_str());
2341 let in_theirs = theirs_map.get(name.as_str());
2342
2343 match (in_base, in_ours, in_theirs) {
2344 (Some(b), Some(o), Some(t)) => {
2346 if o == t {
2347 merged_members.push(o.to_string());
2348 } else if b == o {
2349 merged_members.push(t.to_string());
2350 } else if b == t {
2351 merged_members.push(o.to_string());
2352 } else {
2353 if let Some(merged) = diffy_merge(b, o, t) {
2355 merged_members.push(merged);
2356 } else if let Some(merged) = git_merge_string(b, o, t) {
2357 merged_members.push(merged);
2358 } else if let Some(merged) = try_decorator_aware_merge(b, o, t) {
2359 merged_members.push(merged);
2360 } else {
2361 has_conflict = true;
2363 merged_members.push(scoped_conflict_marker(name, Some(b), Some(o), Some(t), false, false, marker_format));
2364 }
2365 }
2366 }
2367 (Some(b), Some(o), None) => {
2369 if *b == *o {
2370 } else {
2372 has_conflict = true;
2374 merged_members.push(scoped_conflict_marker(name, Some(b), Some(o), None, false, true, marker_format));
2375 }
2376 }
2377 (Some(b), None, Some(t)) => {
2379 if *b == *t {
2380 } else {
2382 has_conflict = true;
2384 merged_members.push(scoped_conflict_marker(name, Some(b), None, Some(t), true, false, marker_format));
2385 }
2386 }
2387 (None, Some(o), None) => {
2389 merged_members.push(o.to_string());
2390 }
2391 (None, None, Some(t)) => {
2393 merged_members.push(t.to_string());
2394 }
2395 (None, Some(o), Some(t)) => {
2397 if o == t {
2398 merged_members.push(o.to_string());
2399 } else {
2400 has_conflict = true;
2401 merged_members.push(scoped_conflict_marker(name, None, Some(o), Some(t), false, false, marker_format));
2402 }
2403 }
2404 (Some(_), None, None) => {}
2406 (None, None, None) => {}
2407 }
2408 }
2409
2410 let mut result = String::new();
2412 result.push_str(ours_header);
2413 if !ours_header.ends_with('\n') {
2414 result.push('\n');
2415 }
2416
2417 let has_multiline_members = merged_members.iter().any(|m| m.contains('\n'));
2419 let original_has_blank_separators = {
2421 let body = ours_header.len()..ours.rfind(ours_footer).unwrap_or(ours.len());
2422 let body_content = &ours[body];
2423 body_content.contains("\n\n")
2424 };
2425
2426 for (i, member) in merged_members.iter().enumerate() {
2427 result.push_str(member);
2428 if !member.ends_with('\n') {
2429 result.push('\n');
2430 }
2431 if i < merged_members.len() - 1 && has_multiline_members && original_has_blank_separators && !member.ends_with("\n\n") {
2433 result.push('\n');
2434 }
2435 }
2436
2437 result.push_str(ours_footer);
2438 if !ours_footer.ends_with('\n') && ours.ends_with('\n') {
2439 result.push('\n');
2440 }
2441
2442 if has_conflict && use_children {
2446 if let (Some(bc), Some(oc), Some(tc)) = (
2447 extract_member_chunks(base),
2448 extract_member_chunks(ours),
2449 extract_member_chunks(theirs),
2450 ) {
2451 if !bc.is_empty() || !oc.is_empty() || !tc.is_empty() {
2452 let fallback = try_inner_merge_with_chunks(
2453 &bc, &oc, &tc, ours, ours_header, ours_footer,
2454 has_multiline_members, marker_format,
2455 );
2456 if let Some(fb) = fallback {
2457 if !fb.has_conflicts {
2458 return Some(fb);
2459 }
2460 }
2461 }
2462 }
2463 }
2464
2465 Some(InnerMergeResult {
2466 content: result,
2467 has_conflicts: has_conflict,
2468 })
2469}
2470
2471fn try_inner_merge_with_chunks(
2473 base_chunks: &[MemberChunk],
2474 ours_chunks: &[MemberChunk],
2475 theirs_chunks: &[MemberChunk],
2476 ours: &str,
2477 ours_header: &str,
2478 ours_footer: &str,
2479 has_multiline_hint: bool,
2480 marker_format: &MarkerFormat,
2481) -> Option<InnerMergeResult> {
2482 let base_map: HashMap<&str, &str> = base_chunks.iter().map(|c| (c.name.as_str(), c.content.as_str())).collect();
2483 let ours_map: HashMap<&str, &str> = ours_chunks.iter().map(|c| (c.name.as_str(), c.content.as_str())).collect();
2484 let theirs_map: HashMap<&str, &str> = theirs_chunks.iter().map(|c| (c.name.as_str(), c.content.as_str())).collect();
2485
2486 let mut all_names: Vec<String> = Vec::new();
2487 let mut seen: HashSet<String> = HashSet::new();
2488 for chunk in ours_chunks {
2489 if seen.insert(chunk.name.clone()) {
2490 all_names.push(chunk.name.clone());
2491 }
2492 }
2493 for chunk in theirs_chunks {
2494 if seen.insert(chunk.name.clone()) {
2495 all_names.push(chunk.name.clone());
2496 }
2497 }
2498
2499 let mut merged_members: Vec<String> = Vec::new();
2500 let mut has_conflict = false;
2501
2502 for name in &all_names {
2503 let in_base = base_map.get(name.as_str());
2504 let in_ours = ours_map.get(name.as_str());
2505 let in_theirs = theirs_map.get(name.as_str());
2506
2507 match (in_base, in_ours, in_theirs) {
2508 (Some(b), Some(o), Some(t)) => {
2509 if o == t {
2510 merged_members.push(o.to_string());
2511 } else if b == o {
2512 merged_members.push(t.to_string());
2513 } else if b == t {
2514 merged_members.push(o.to_string());
2515 } else if let Some(merged) = diffy_merge(b, o, t) {
2516 merged_members.push(merged);
2517 } else if let Some(merged) = git_merge_string(b, o, t) {
2518 merged_members.push(merged);
2519 } else {
2520 has_conflict = true;
2521 merged_members.push(scoped_conflict_marker(name, Some(b), Some(o), Some(t), false, false, marker_format));
2522 }
2523 }
2524 (Some(b), Some(o), None) => {
2525 if *b != *o { merged_members.push(o.to_string()); }
2526 }
2527 (Some(b), None, Some(t)) => {
2528 if *b != *t { merged_members.push(t.to_string()); }
2529 }
2530 (None, Some(o), None) => merged_members.push(o.to_string()),
2531 (None, None, Some(t)) => merged_members.push(t.to_string()),
2532 (None, Some(o), Some(t)) => {
2533 if o == t {
2534 merged_members.push(o.to_string());
2535 } else {
2536 has_conflict = true;
2537 merged_members.push(scoped_conflict_marker(name, None, Some(o), Some(t), false, false, marker_format));
2538 }
2539 }
2540 (Some(_), None, None) | (None, None, None) => {}
2541 }
2542 }
2543
2544 let has_multiline_members = has_multiline_hint || merged_members.iter().any(|m| m.contains('\n'));
2545 let mut result = String::new();
2546 result.push_str(ours_header);
2547 if !ours_header.ends_with('\n') { result.push('\n'); }
2548 for (i, member) in merged_members.iter().enumerate() {
2549 result.push_str(member);
2550 if !member.ends_with('\n') { result.push('\n'); }
2551 if i < merged_members.len() - 1 && has_multiline_members && !member.ends_with("\n\n") {
2552 result.push('\n');
2553 }
2554 }
2555 result.push_str(ours_footer);
2556 if !ours_footer.ends_with('\n') && ours.ends_with('\n') { result.push('\n'); }
2557
2558 Some(InnerMergeResult {
2559 content: result,
2560 has_conflicts: has_conflict,
2561 })
2562}
2563
2564fn extract_container_wrapper(content: &str) -> Option<(&str, &str)> {
2567 let lines: Vec<&str> = content.lines().collect();
2568 if lines.len() < 2 {
2569 return None;
2570 }
2571
2572 let is_python_style = lines.iter().any(|l| {
2574 let trimmed = l.trim();
2575 (trimmed.starts_with("class ") || trimmed.starts_with("def "))
2576 && trimmed.ends_with(':')
2577 }) && !lines.iter().any(|l| l.contains('{'));
2578
2579 if is_python_style {
2580 let header_end = lines.iter().position(|l| l.trim().ends_with(':'))?;
2582 let header_byte_end: usize = lines[..=header_end]
2583 .iter()
2584 .map(|l| l.len() + 1)
2585 .sum();
2586 let header = &content[..header_byte_end.min(content.len())];
2587 let footer = &content[content.len()..];
2589 Some((header, footer))
2590 } else {
2591 let header_end = lines.iter().position(|l| l.contains('{'))?;
2593 let header_byte_end = lines[..=header_end]
2594 .iter()
2595 .map(|l| l.len() + 1)
2596 .sum::<usize>();
2597 let header = &content[..header_byte_end.min(content.len())];
2598
2599 let footer_start = lines.iter().rposition(|l| {
2600 let trimmed = l.trim();
2601 trimmed == "}" || trimmed == "};"
2602 })?;
2603
2604 let footer_byte_start: usize = lines[..footer_start]
2605 .iter()
2606 .map(|l| l.len() + 1)
2607 .sum();
2608 let footer = &content[footer_byte_start.min(content.len())..];
2609
2610 Some((header, footer))
2611 }
2612}
2613
2614fn extract_member_chunks(content: &str) -> Option<Vec<MemberChunk>> {
2620 let lines: Vec<&str> = content.lines().collect();
2621 if lines.len() < 2 {
2622 return None;
2623 }
2624
2625 let is_python_style = lines.iter().any(|l| {
2627 let trimmed = l.trim();
2628 (trimmed.starts_with("class ") || trimmed.starts_with("def "))
2629 && trimmed.ends_with(':')
2630 }) && !lines.iter().any(|l| l.contains('{'));
2631
2632 let body_start = if is_python_style {
2634 lines.iter().position(|l| l.trim().ends_with(':'))? + 1
2635 } else {
2636 lines.iter().position(|l| l.contains('{'))? + 1
2637 };
2638 let body_end = if is_python_style {
2639 lines.len()
2641 } else {
2642 lines.iter().rposition(|l| {
2643 let trimmed = l.trim();
2644 trimmed == "}" || trimmed == "};"
2645 })?
2646 };
2647
2648 if body_start >= body_end {
2649 return None;
2650 }
2651
2652 let member_indent = lines[body_start..body_end]
2654 .iter()
2655 .find(|l| !l.trim().is_empty())
2656 .map(|l| l.len() - l.trim_start().len())?;
2657
2658 let mut chunks: Vec<MemberChunk> = Vec::new();
2659 let mut current_chunk_lines: Vec<&str> = Vec::new();
2660 let mut current_name: Option<String> = None;
2661
2662 for line in &lines[body_start..body_end] {
2663 let trimmed = line.trim();
2664 if trimmed.is_empty() {
2665 if current_name.is_some() {
2667 current_chunk_lines.push(line);
2669 }
2670 continue;
2671 }
2672
2673 let indent = line.len() - line.trim_start().len();
2674
2675 if indent == member_indent
2678 && !trimmed.starts_with("//")
2679 && !trimmed.starts_with("/*")
2680 && !trimmed.starts_with("*")
2681 && !trimmed.starts_with("#")
2682 && !trimmed.starts_with("@")
2683 && !trimmed.starts_with("}")
2684 && trimmed != ","
2685 {
2686 if let Some(name) = current_name.take() {
2688 while current_chunk_lines.last().map_or(false, |l| l.trim().is_empty()) {
2690 current_chunk_lines.pop();
2691 }
2692 if !current_chunk_lines.is_empty() {
2693 chunks.push(MemberChunk {
2694 name,
2695 content: current_chunk_lines.join("\n"),
2696 });
2697 }
2698 current_chunk_lines.clear();
2699 }
2700
2701 let name = extract_member_name(trimmed);
2703 current_name = Some(name);
2704 current_chunk_lines.push(line);
2705 } else if current_name.is_some() {
2706 current_chunk_lines.push(line);
2708 } else {
2709 current_chunk_lines.push(line);
2712 }
2713 }
2714
2715 if let Some(name) = current_name {
2717 while current_chunk_lines.last().map_or(false, |l| l.trim().is_empty()) {
2718 current_chunk_lines.pop();
2719 }
2720 if !current_chunk_lines.is_empty() {
2721 chunks.push(MemberChunk {
2722 name,
2723 content: current_chunk_lines.join("\n"),
2724 });
2725 }
2726 }
2727
2728 for chunk in &mut chunks {
2732 if chunk.name == "{" || chunk.name == "{}" {
2733 if let Some(better) = derive_name_from_struct_literal(&chunk.content) {
2734 chunk.name = better;
2735 }
2736 }
2737 }
2738
2739 if chunks.is_empty() {
2740 None
2741 } else {
2742 Some(chunks)
2743 }
2744}
2745
2746fn extract_member_name(line: &str) -> String {
2748 let trimmed = line.trim();
2749
2750 if trimmed.starts_with("func ") && trimmed.get(5..6) == Some("(") {
2752 if let Some(recv_close) = trimmed.find(')') {
2754 let after_recv = &trimmed[recv_close + 1..];
2755 if let Some(paren_pos) = after_recv.find('(') {
2756 let before = after_recv[..paren_pos].trim();
2757 let name: String = before
2758 .chars()
2759 .rev()
2760 .take_while(|c| c.is_alphanumeric() || *c == '_')
2761 .collect::<Vec<_>>()
2762 .into_iter()
2763 .rev()
2764 .collect();
2765 if !name.is_empty() {
2766 return name;
2767 }
2768 }
2769 }
2770 }
2771
2772 if let Some(paren_pos) = trimmed.find('(') {
2777 let before = trimmed[..paren_pos].trim_end();
2778 let name: String = before
2779 .chars()
2780 .rev()
2781 .take_while(|c| c.is_alphanumeric() || *c == '_')
2782 .collect::<Vec<_>>()
2783 .into_iter()
2784 .rev()
2785 .collect();
2786 if !name.is_empty() {
2787 return name;
2788 }
2789 }
2790
2791 let mut s = trimmed;
2794 for keyword in &[
2795 "export ", "public ", "private ", "protected ", "static ",
2796 "abstract ", "async ", "override ", "readonly ",
2797 "pub ", "pub(crate) ", "fn ", "def ", "get ", "set ",
2798 ] {
2799 if s.starts_with(keyword) {
2800 s = &s[keyword.len()..];
2801 }
2802 }
2803 if s.starts_with("fn ") {
2804 s = &s[3..];
2805 }
2806
2807 let name: String = s
2808 .chars()
2809 .take_while(|c| c.is_alphanumeric() || *c == '_')
2810 .collect();
2811
2812 if name.is_empty() {
2813 trimmed.chars().take(20).collect()
2814 } else {
2815 name
2816 }
2817}
2818
2819fn derive_name_from_struct_literal(content: &str) -> Option<String> {
2823 for line in content.lines().skip(1) {
2824 let trimmed = line.trim().trim_end_matches(',');
2825 if let Some(colon_pos) = trimmed.find(':') {
2827 let value = trimmed[colon_pos + 1..].trim();
2828 let value = value.trim_matches('"').trim_matches('\'');
2830 if !value.is_empty() {
2831 return Some(value.to_string());
2832 }
2833 }
2834 }
2835 None
2836}
2837
2838fn is_binary(content: &str) -> bool {
2846 content.as_bytes().iter().take(8192).any(|&b| b == 0)
2847}
2848
2849fn has_conflict_markers(content: &str) -> bool {
2852 content.contains("<<<<<<<") && content.contains(">>>>>>>")
2853}
2854
2855fn skip_sesame(file_path: &str) -> bool {
2856 let path_lower = file_path.to_lowercase();
2857 let extensions = [
2858 ".json", ".yaml", ".yml", ".toml", ".lock", ".xml", ".csv", ".tsv",
2860 ".ini", ".cfg", ".conf", ".properties", ".env",
2861 ".md", ".markdown", ".txt", ".rst", ".svg", ".html", ".htm",
2863 ];
2864 extensions.iter().any(|ext| path_lower.ends_with(ext))
2865}
2866
2867fn expand_separators(content: &str) -> String {
2873 let bytes = content.as_bytes();
2874 let mut result = Vec::with_capacity(content.len() * 2);
2875 let mut in_string = false;
2876 let mut escape_next = false;
2877 let mut string_char = b'"';
2878
2879 for &b in bytes {
2880 if escape_next {
2881 result.push(b);
2882 escape_next = false;
2883 continue;
2884 }
2885 if b == b'\\' && in_string {
2886 result.push(b);
2887 escape_next = true;
2888 continue;
2889 }
2890 if !in_string && (b == b'"' || b == b'\'' || b == b'`') {
2891 in_string = true;
2892 string_char = b;
2893 result.push(b);
2894 continue;
2895 }
2896 if in_string && b == string_char {
2897 in_string = false;
2898 result.push(b);
2899 continue;
2900 }
2901
2902 if !in_string && (b == b'{' || b == b'}' || b == b';') {
2903 if result.last() != Some(&b'\n') && !result.is_empty() {
2904 result.push(b'\n');
2905 }
2906 result.push(b);
2907 result.push(b'\n');
2908 } else {
2909 result.push(b);
2910 }
2911 }
2912
2913 unsafe { String::from_utf8_unchecked(result) }
2915}
2916
2917fn collapse_separators(merged: &str, _base: &str) -> String {
2920 let lines: Vec<&str> = merged.lines().collect();
2922 let mut result = String::new();
2923 let mut i = 0;
2924
2925 while i < lines.len() {
2926 let trimmed = lines[i].trim();
2927 if (trimmed == "{" || trimmed == "}" || trimmed == ";") && trimmed.len() == 1 {
2928 if !result.is_empty() && !result.ends_with('\n') {
2931 if trimmed == "{" {
2933 result.push(' ');
2934 result.push_str(trimmed);
2935 result.push('\n');
2936 } else if trimmed == "}" {
2937 result.push('\n');
2938 result.push_str(trimmed);
2939 result.push('\n');
2940 } else {
2941 result.push_str(trimmed);
2942 result.push('\n');
2943 }
2944 } else {
2945 result.push_str(lines[i]);
2946 result.push('\n');
2947 }
2948 } else {
2949 result.push_str(lines[i]);
2950 result.push('\n');
2951 }
2952 i += 1;
2953 }
2954
2955 while result.ends_with("\n\n") {
2957 result.pop();
2958 }
2959
2960 result
2961}
2962
2963#[cfg(test)]
2964mod tests {
2965 use super::*;
2966
2967 #[test]
2968 fn test_replace_at_word_boundaries() {
2969 assert_eq!(replace_at_word_boundaries("fn get() {}", "get", "__E__"), "fn __E__() {}");
2971 assert_eq!(replace_at_word_boundaries("fn getAll() {}", "get", "__E__"), "fn getAll() {}");
2973 assert_eq!(replace_at_word_boundaries("fn _get() {}", "get", "__E__"), "fn _get() {}");
2974 assert_eq!(
2976 replace_at_word_boundaries("pub enum Source { Source }", "Source", "__E__"),
2977 "pub enum __E__ { __E__ }"
2978 );
2979 assert_eq!(
2981 replace_at_word_boundaries("SourceManager isSource", "Source", "__E__"),
2982 "SourceManager isSource"
2983 );
2984 assert_eq!(
2986 replace_at_word_boundaries("❌ get ✅", "get", "__E__"),
2987 "❌ __E__ ✅"
2988 );
2989 assert_eq!(
2990 replace_at_word_boundaries("fn 名前() { get }", "get", "__E__"),
2991 "fn 名前() { __E__ }"
2992 );
2993 assert_eq!(
2995 replace_at_word_boundaries("🎉🚀✨", "get", "__E__"),
2996 "🎉🚀✨"
2997 );
2998 }
2999
3000 #[test]
3001 fn test_fast_path_identical() {
3002 let content = "hello world";
3003 let result = entity_merge(content, content, content, "test.ts");
3004 assert!(result.is_clean());
3005 assert_eq!(result.content, content);
3006 }
3007
3008 #[test]
3009 fn test_fast_path_only_ours_changed() {
3010 let base = "hello";
3011 let ours = "hello world";
3012 let result = entity_merge(base, ours, base, "test.ts");
3013 assert!(result.is_clean());
3014 assert_eq!(result.content, ours);
3015 }
3016
3017 #[test]
3018 fn test_fast_path_only_theirs_changed() {
3019 let base = "hello";
3020 let theirs = "hello world";
3021 let result = entity_merge(base, base, theirs, "test.ts");
3022 assert!(result.is_clean());
3023 assert_eq!(result.content, theirs);
3024 }
3025
3026 #[test]
3027 fn test_different_functions_no_conflict() {
3028 let base = r#"export function existing() {
3030 return 1;
3031}
3032"#;
3033 let ours = r#"export function existing() {
3034 return 1;
3035}
3036
3037export function agentA() {
3038 return "added by agent A";
3039}
3040"#;
3041 let theirs = r#"export function existing() {
3042 return 1;
3043}
3044
3045export function agentB() {
3046 return "added by agent B";
3047}
3048"#;
3049 let result = entity_merge(base, ours, theirs, "test.ts");
3050 assert!(
3051 result.is_clean(),
3052 "Should auto-resolve: different functions added. Conflicts: {:?}",
3053 result.conflicts
3054 );
3055 assert!(
3056 result.content.contains("agentA"),
3057 "Should contain agentA function"
3058 );
3059 assert!(
3060 result.content.contains("agentB"),
3061 "Should contain agentB function"
3062 );
3063 }
3064
3065 #[test]
3066 fn test_same_function_modified_by_both_conflict() {
3067 let base = r#"export function shared() {
3068 return "original";
3069}
3070"#;
3071 let ours = r#"export function shared() {
3072 return "modified by ours";
3073}
3074"#;
3075 let theirs = r#"export function shared() {
3076 return "modified by theirs";
3077}
3078"#;
3079 let result = entity_merge(base, ours, theirs, "test.ts");
3080 assert!(
3082 !result.is_clean(),
3083 "Should conflict when both modify same function differently"
3084 );
3085 assert_eq!(result.conflicts.len(), 1);
3086 assert_eq!(result.conflicts[0].entity_name, "shared");
3087 }
3088
3089 #[test]
3090 fn test_fallback_for_unknown_filetype() {
3091 let base = "line 1\nline 2\nline 3\nline 4\nline 5\n";
3093 let ours = "line 1 modified\nline 2\nline 3\nline 4\nline 5\n";
3094 let theirs = "line 1\nline 2\nline 3\nline 4\nline 5 modified\n";
3095 let result = entity_merge(base, ours, theirs, "test.xyz");
3096 assert!(
3097 result.is_clean(),
3098 "Non-adjacent changes should merge cleanly. Conflicts: {:?}",
3099 result.conflicts,
3100 );
3101 }
3102
3103 #[test]
3104 fn test_line_level_fallback() {
3105 let base = "a\nb\nc\nd\ne\n";
3107 let ours = "A\nb\nc\nd\ne\n";
3108 let theirs = "a\nb\nc\nd\nE\n";
3109 let result = line_level_fallback(base, ours, theirs, "test.rs");
3110 assert!(result.is_clean());
3111 assert!(result.stats.used_fallback);
3112 assert_eq!(result.content, "A\nb\nc\nd\nE\n");
3113 }
3114
3115 #[test]
3116 fn test_line_level_fallback_conflict() {
3117 let base = "a\nb\nc\n";
3119 let ours = "X\nb\nc\n";
3120 let theirs = "Y\nb\nc\n";
3121 let result = line_level_fallback(base, ours, theirs, "test.rs");
3122 assert!(!result.is_clean());
3123 assert!(result.stats.used_fallback);
3124 }
3125
3126 #[test]
3127 fn test_expand_separators() {
3128 let code = "function foo() { return 1; }";
3129 let expanded = expand_separators(code);
3130 assert!(expanded.contains("{\n"), "Opening brace should have newline after");
3132 assert!(expanded.contains(";\n"), "Semicolons should have newline after");
3133 assert!(expanded.contains("\n}"), "Closing brace should have newline before");
3134 }
3135
3136 #[test]
3137 fn test_expand_separators_preserves_strings() {
3138 let code = r#"let x = "hello { world };";"#;
3139 let expanded = expand_separators(code);
3140 assert!(
3142 expanded.contains("\"hello { world };\""),
3143 "Separators in strings should be preserved: {}",
3144 expanded
3145 );
3146 }
3147
3148 #[test]
3149 fn test_is_import_region() {
3150 assert!(is_import_region("import foo from 'foo';\nimport bar from 'bar';\n"));
3151 assert!(is_import_region("use std::io;\nuse std::fs;\n"));
3152 assert!(!is_import_region("let x = 1;\nlet y = 2;\n"));
3153 assert!(!is_import_region("import foo from 'foo';\nlet x = 1;\nlet y = 2;\n"));
3155 assert!(!is_import_region(""));
3157 }
3158
3159 #[test]
3160 fn test_is_import_line() {
3161 assert!(is_import_line("import foo from 'foo';"));
3163 assert!(is_import_line("import { bar } from 'bar';"));
3164 assert!(is_import_line("from typing import List"));
3165 assert!(is_import_line("use std::io::Read;"));
3167 assert!(is_import_line("#include <stdio.h>"));
3169 assert!(is_import_line("const fs = require('fs');"));
3171 assert!(!is_import_line("let x = 1;"));
3173 assert!(!is_import_line("function foo() {}"));
3174 }
3175
3176 #[test]
3177 fn test_commutative_import_merge_both_add_different() {
3178 let base = "import a from 'a';\nimport b from 'b';\n";
3180 let ours = "import a from 'a';\nimport b from 'b';\nimport c from 'c';\n";
3181 let theirs = "import a from 'a';\nimport b from 'b';\nimport d from 'd';\n";
3182 let result = merge_imports_commutatively(base, ours, theirs);
3183 assert!(result.contains("import a from 'a';"));
3184 assert!(result.contains("import b from 'b';"));
3185 assert!(result.contains("import c from 'c';"));
3186 assert!(result.contains("import d from 'd';"));
3187 }
3188
3189 #[test]
3190 fn test_commutative_import_merge_one_removes() {
3191 let base = "import a from 'a';\nimport b from 'b';\nimport c from 'c';\n";
3193 let ours = "import a from 'a';\nimport c from 'c';\n";
3194 let theirs = "import a from 'a';\nimport b from 'b';\nimport c from 'c';\n";
3195 let result = merge_imports_commutatively(base, ours, theirs);
3196 assert!(result.contains("import a from 'a';"));
3197 assert!(!result.contains("import b from 'b';"), "Removed import should stay removed");
3198 assert!(result.contains("import c from 'c';"));
3199 }
3200
3201 #[test]
3202 fn test_commutative_import_merge_both_add_same() {
3203 let base = "import a from 'a';\n";
3205 let ours = "import a from 'a';\nimport b from 'b';\n";
3206 let theirs = "import a from 'a';\nimport b from 'b';\n";
3207 let result = merge_imports_commutatively(base, ours, theirs);
3208 let count = result.matches("import b from 'b';").count();
3209 assert_eq!(count, 1, "Duplicate import should be deduplicated");
3210 }
3211
3212 #[test]
3213 fn test_inner_entity_merge_different_methods() {
3214 let base = r#"export class Calculator {
3217 add(a: number, b: number): number {
3218 return a + b;
3219 }
3220
3221 subtract(a: number, b: number): number {
3222 return a - b;
3223 }
3224}
3225"#;
3226 let ours = r#"export class Calculator {
3227 add(a: number, b: number): number {
3228 // Added logging
3229 console.log("adding", a, b);
3230 return a + b;
3231 }
3232
3233 subtract(a: number, b: number): number {
3234 return a - b;
3235 }
3236}
3237"#;
3238 let theirs = r#"export class Calculator {
3239 add(a: number, b: number): number {
3240 return a + b;
3241 }
3242
3243 subtract(a: number, b: number): number {
3244 // Added validation
3245 if (b > a) throw new Error("negative");
3246 return a - b;
3247 }
3248}
3249"#;
3250 let result = entity_merge(base, ours, theirs, "test.ts");
3251 assert!(
3252 result.is_clean(),
3253 "Different methods modified should auto-merge via inner entity merge. Conflicts: {:?}",
3254 result.conflicts,
3255 );
3256 assert!(result.content.contains("console.log"), "Should contain ours changes");
3257 assert!(result.content.contains("negative"), "Should contain theirs changes");
3258 }
3259
3260 #[test]
3261 fn test_inner_entity_merge_both_add_different_methods() {
3262 let base = r#"export class Calculator {
3264 add(a: number, b: number): number {
3265 return a + b;
3266 }
3267}
3268"#;
3269 let ours = r#"export class Calculator {
3270 add(a: number, b: number): number {
3271 return a + b;
3272 }
3273
3274 multiply(a: number, b: number): number {
3275 return a * b;
3276 }
3277}
3278"#;
3279 let theirs = r#"export class Calculator {
3280 add(a: number, b: number): number {
3281 return a + b;
3282 }
3283
3284 divide(a: number, b: number): number {
3285 return a / b;
3286 }
3287}
3288"#;
3289 let result = entity_merge(base, ours, theirs, "test.ts");
3290 assert!(
3291 result.is_clean(),
3292 "Both adding different methods should auto-merge. Conflicts: {:?}",
3293 result.conflicts,
3294 );
3295 assert!(result.content.contains("multiply"), "Should contain ours's new method");
3296 assert!(result.content.contains("divide"), "Should contain theirs's new method");
3297 }
3298
3299 #[test]
3300 fn test_inner_entity_merge_same_method_modified_still_conflicts() {
3301 let base = r#"export class Calculator {
3303 add(a: number, b: number): number {
3304 return a + b;
3305 }
3306
3307 subtract(a: number, b: number): number {
3308 return a - b;
3309 }
3310}
3311"#;
3312 let ours = r#"export class Calculator {
3313 add(a: number, b: number): number {
3314 return a + b + 1;
3315 }
3316
3317 subtract(a: number, b: number): number {
3318 return a - b;
3319 }
3320}
3321"#;
3322 let theirs = r#"export class Calculator {
3323 add(a: number, b: number): number {
3324 return a + b + 2;
3325 }
3326
3327 subtract(a: number, b: number): number {
3328 return a - b;
3329 }
3330}
3331"#;
3332 let result = entity_merge(base, ours, theirs, "test.ts");
3333 assert!(
3334 !result.is_clean(),
3335 "Both modifying same method differently should still conflict"
3336 );
3337 }
3338
3339 #[test]
3340 fn test_extract_member_chunks() {
3341 let class_body = r#"export class Foo {
3342 bar() {
3343 return 1;
3344 }
3345
3346 baz() {
3347 return 2;
3348 }
3349}
3350"#;
3351 let chunks = extract_member_chunks(class_body).unwrap();
3352 assert_eq!(chunks.len(), 2, "Should find 2 members, found {:?}", chunks.iter().map(|c| &c.name).collect::<Vec<_>>());
3353 assert_eq!(chunks[0].name, "bar");
3354 assert_eq!(chunks[1].name, "baz");
3355 }
3356
3357 #[test]
3358 fn test_extract_member_name() {
3359 assert_eq!(extract_member_name("add(a, b) {"), "add");
3360 assert_eq!(extract_member_name("fn add(&self, a: i32) -> i32 {"), "add");
3361 assert_eq!(extract_member_name("def add(self, a, b):"), "add");
3362 assert_eq!(extract_member_name("public static getValue(): number {"), "getValue");
3363 assert_eq!(extract_member_name("async fetchData() {"), "fetchData");
3364 }
3365
3366 #[test]
3367 fn test_commutative_import_merge_rust_use() {
3368 let base = "use std::io;\nuse std::fs;\n";
3369 let ours = "use std::io;\nuse std::fs;\nuse std::path::Path;\n";
3370 let theirs = "use std::io;\nuse std::fs;\nuse std::collections::HashMap;\n";
3371 let result = merge_imports_commutatively(base, ours, theirs);
3372 assert!(result.contains("use std::path::Path;"));
3373 assert!(result.contains("use std::collections::HashMap;"));
3374 assert!(result.contains("use std::io;"));
3375 assert!(result.contains("use std::fs;"));
3376 }
3377
3378 #[test]
3379 fn test_is_whitespace_only_diff_true() {
3380 assert!(is_whitespace_only_diff(
3382 " return 1;\n return 2;\n",
3383 " return 1;\n return 2;\n"
3384 ));
3385 assert!(is_whitespace_only_diff(
3387 "return 1;\nreturn 2;\n",
3388 "return 1;\n\nreturn 2;\n"
3389 ));
3390 }
3391
3392 #[test]
3393 fn test_is_whitespace_only_diff_false() {
3394 assert!(!is_whitespace_only_diff(
3396 " return 1;\n",
3397 " return 2;\n"
3398 ));
3399 assert!(!is_whitespace_only_diff(
3401 "return 1;\n",
3402 "return 1;\nconsole.log('x');\n"
3403 ));
3404 }
3405
3406 #[test]
3407 fn test_ts_interface_both_add_different_fields() {
3408 let base = "interface Config {\n name: string;\n}\n";
3409 let ours = "interface Config {\n name: string;\n age: number;\n}\n";
3410 let theirs = "interface Config {\n name: string;\n email: string;\n}\n";
3411 let result = entity_merge(base, ours, theirs, "test.ts");
3412 eprintln!("TS interface: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3413 eprintln!("Content: {:?}", result.content);
3414 assert!(
3415 result.is_clean(),
3416 "Both adding different fields to TS interface should merge. Conflicts: {:?}",
3417 result.conflicts,
3418 );
3419 assert!(result.content.contains("age"));
3420 assert!(result.content.contains("email"));
3421 }
3422
3423 #[test]
3424 fn test_rust_enum_both_add_different_variants() {
3425 let base = "enum Color {\n Red,\n Blue,\n}\n";
3426 let ours = "enum Color {\n Red,\n Blue,\n Green,\n}\n";
3427 let theirs = "enum Color {\n Red,\n Blue,\n Yellow,\n}\n";
3428 let result = entity_merge(base, ours, theirs, "test.rs");
3429 eprintln!("Rust enum: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3430 eprintln!("Content: {:?}", result.content);
3431 assert!(
3432 result.is_clean(),
3433 "Both adding different enum variants should merge. Conflicts: {:?}",
3434 result.conflicts,
3435 );
3436 assert!(result.content.contains("Green"));
3437 assert!(result.content.contains("Yellow"));
3438 }
3439
3440 #[test]
3441 fn test_python_both_add_different_decorators() {
3442 let base = "def foo():\n return 1\n\ndef bar():\n return 2\n";
3444 let ours = "@cache\ndef foo():\n return 1\n\ndef bar():\n return 2\n";
3445 let theirs = "@deprecated\ndef foo():\n return 1\n\ndef bar():\n return 2\n";
3446 let result = entity_merge(base, ours, theirs, "test.py");
3447 assert!(
3448 result.is_clean(),
3449 "Both adding different decorators should merge. Conflicts: {:?}",
3450 result.conflicts,
3451 );
3452 assert!(result.content.contains("@cache"));
3453 assert!(result.content.contains("@deprecated"));
3454 assert!(result.content.contains("def foo()"));
3455 }
3456
3457 #[test]
3458 fn test_decorator_plus_body_change() {
3459 let base = "def foo():\n return 1\n";
3461 let ours = "@cache\ndef foo():\n return 1\n";
3462 let theirs = "def foo():\n return 42\n";
3463 let result = entity_merge(base, ours, theirs, "test.py");
3464 assert!(
3465 result.is_clean(),
3466 "Decorator + body change should merge. Conflicts: {:?}",
3467 result.conflicts,
3468 );
3469 assert!(result.content.contains("@cache"));
3470 assert!(result.content.contains("return 42"));
3471 }
3472
3473 #[test]
3474 fn test_ts_class_decorator_merge() {
3475 let base = "class Foo {\n bar() {\n return 1;\n }\n}\n";
3477 let ours = "class Foo {\n @Injectable()\n bar() {\n return 1;\n }\n}\n";
3478 let theirs = "class Foo {\n @Deprecated()\n bar() {\n return 1;\n }\n}\n";
3479 let result = entity_merge(base, ours, theirs, "test.ts");
3480 assert!(
3481 result.is_clean(),
3482 "Both adding different decorators to same method should merge. Conflicts: {:?}",
3483 result.conflicts,
3484 );
3485 assert!(result.content.contains("@Injectable()"));
3486 assert!(result.content.contains("@Deprecated()"));
3487 assert!(result.content.contains("bar()"));
3488 }
3489
3490 #[test]
3491 fn test_non_adjacent_intra_function_changes() {
3492 let base = r#"export function process(data: any) {
3493 const validated = validate(data);
3494 const transformed = transform(validated);
3495 const saved = save(transformed);
3496 return saved;
3497}
3498"#;
3499 let ours = r#"export function process(data: any) {
3500 const validated = validate(data);
3501 const transformed = transform(validated);
3502 const saved = save(transformed);
3503 console.log("saved", saved);
3504 return saved;
3505}
3506"#;
3507 let theirs = r#"export function process(data: any) {
3508 console.log("input", data);
3509 const validated = validate(data);
3510 const transformed = transform(validated);
3511 const saved = save(transformed);
3512 return saved;
3513}
3514"#;
3515 let result = entity_merge(base, ours, theirs, "test.ts");
3516 assert!(
3517 result.is_clean(),
3518 "Non-adjacent changes within same function should merge via diffy. Conflicts: {:?}",
3519 result.conflicts,
3520 );
3521 assert!(result.content.contains("console.log(\"saved\""));
3522 assert!(result.content.contains("console.log(\"input\""));
3523 }
3524
3525 #[test]
3526 fn test_method_reordering_with_modification() {
3527 let base = r#"class Service {
3530 getUser(id: string) {
3531 return db.find(id);
3532 }
3533
3534 createUser(data: any) {
3535 return db.create(data);
3536 }
3537
3538 deleteUser(id: string) {
3539 return db.delete(id);
3540 }
3541}
3542"#;
3543 let ours = r#"class Service {
3545 getUser(id: string) {
3546 return db.find(id);
3547 }
3548
3549 deleteUser(id: string) {
3550 return db.delete(id);
3551 }
3552
3553 createUser(data: any) {
3554 return db.create(data);
3555 }
3556}
3557"#;
3558 let theirs = r#"class Service {
3560 getUser(id: string) {
3561 console.log("fetching", id);
3562 return db.find(id);
3563 }
3564
3565 createUser(data: any) {
3566 return db.create(data);
3567 }
3568
3569 deleteUser(id: string) {
3570 return db.delete(id);
3571 }
3572}
3573"#;
3574 let result = entity_merge(base, ours, theirs, "test.ts");
3575 eprintln!("Method reorder: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3576 eprintln!("Content:\n{}", result.content);
3577 assert!(
3578 result.is_clean(),
3579 "Method reordering + modification should merge. Conflicts: {:?}",
3580 result.conflicts,
3581 );
3582 assert!(result.content.contains("console.log(\"fetching\""), "Should contain theirs modification");
3583 assert!(result.content.contains("deleteUser"), "Should have deleteUser");
3584 assert!(result.content.contains("createUser"), "Should have createUser");
3585 }
3586
3587 #[test]
3588 fn test_doc_comment_plus_body_change() {
3589 let base = r#"export function calculate(a: number, b: number): number {
3592 return a + b;
3593}
3594"#;
3595 let ours = r#"/**
3596 * Calculate the sum of two numbers.
3597 * @param a - First number
3598 * @param b - Second number
3599 */
3600export function calculate(a: number, b: number): number {
3601 return a + b;
3602}
3603"#;
3604 let theirs = r#"export function calculate(a: number, b: number): number {
3605 const result = a + b;
3606 console.log("result:", result);
3607 return result;
3608}
3609"#;
3610 let result = entity_merge(base, ours, theirs, "test.ts");
3611 eprintln!("Doc comment + body: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3612 eprintln!("Content:\n{}", result.content);
3613 }
3615
3616 #[test]
3617 fn test_both_add_different_guard_clauses() {
3618 let base = r#"export function processOrder(order: Order): Result {
3620 const total = calculateTotal(order);
3621 return { success: true, total };
3622}
3623"#;
3624 let ours = r#"export function processOrder(order: Order): Result {
3625 if (!order) throw new Error("Order required");
3626 const total = calculateTotal(order);
3627 return { success: true, total };
3628}
3629"#;
3630 let theirs = r#"export function processOrder(order: Order): Result {
3631 if (order.items.length === 0) throw new Error("Empty order");
3632 const total = calculateTotal(order);
3633 return { success: true, total };
3634}
3635"#;
3636 let result = entity_merge(base, ours, theirs, "test.ts");
3637 eprintln!("Guard clauses: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3638 eprintln!("Content:\n{}", result.content);
3639 }
3641
3642 #[test]
3643 fn test_both_modify_different_enum_variants() {
3644 let base = r#"enum Status {
3646 Active = "active",
3647 Inactive = "inactive",
3648 Pending = "pending",
3649}
3650"#;
3651 let ours = r#"enum Status {
3652 Active = "active",
3653 Inactive = "disabled",
3654 Pending = "pending",
3655}
3656"#;
3657 let theirs = r#"enum Status {
3658 Active = "active",
3659 Inactive = "inactive",
3660 Pending = "pending",
3661 Deleted = "deleted",
3662}
3663"#;
3664 let result = entity_merge(base, ours, theirs, "test.ts");
3665 eprintln!("Enum modify+add: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3666 eprintln!("Content:\n{}", result.content);
3667 assert!(
3668 result.is_clean(),
3669 "Modify variant + add new variant should merge. Conflicts: {:?}",
3670 result.conflicts,
3671 );
3672 assert!(result.content.contains("\"disabled\""), "Should have modified Inactive");
3673 assert!(result.content.contains("Deleted"), "Should have new Deleted variant");
3674 }
3675
3676 #[test]
3677 fn test_config_object_field_additions() {
3678 let base = r#"export const config = {
3680 timeout: 5000,
3681 retries: 3,
3682};
3683"#;
3684 let ours = r#"export const config = {
3685 timeout: 5000,
3686 retries: 3,
3687 maxConnections: 10,
3688};
3689"#;
3690 let theirs = r#"export const config = {
3691 timeout: 5000,
3692 retries: 3,
3693 logLevel: "info",
3694};
3695"#;
3696 let result = entity_merge(base, ours, theirs, "test.ts");
3697 eprintln!("Config fields: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3698 eprintln!("Content:\n{}", result.content);
3699 }
3702
3703 #[test]
3704 fn test_rust_impl_block_both_add_methods() {
3705 let base = r#"impl Calculator {
3707 fn add(&self, a: i32, b: i32) -> i32 {
3708 a + b
3709 }
3710}
3711"#;
3712 let ours = r#"impl Calculator {
3713 fn add(&self, a: i32, b: i32) -> i32 {
3714 a + b
3715 }
3716
3717 fn multiply(&self, a: i32, b: i32) -> i32 {
3718 a * b
3719 }
3720}
3721"#;
3722 let theirs = r#"impl Calculator {
3723 fn add(&self, a: i32, b: i32) -> i32 {
3724 a + b
3725 }
3726
3727 fn divide(&self, a: i32, b: i32) -> i32 {
3728 a / b
3729 }
3730}
3731"#;
3732 let result = entity_merge(base, ours, theirs, "test.rs");
3733 eprintln!("Rust impl: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3734 eprintln!("Content:\n{}", result.content);
3735 assert!(
3736 result.is_clean(),
3737 "Both adding methods to Rust impl should merge. Conflicts: {:?}",
3738 result.conflicts,
3739 );
3740 assert!(result.content.contains("multiply"), "Should have multiply");
3741 assert!(result.content.contains("divide"), "Should have divide");
3742 }
3743
3744 #[test]
3745 fn test_rust_impl_same_trait_different_types() {
3746 let base = r#"struct Foo;
3750struct Bar;
3751
3752impl Stream for Foo {
3753 type Item = i32;
3754 fn poll_next(&self) -> Option<i32> {
3755 Some(1)
3756 }
3757}
3758
3759impl Stream for Bar {
3760 type Item = String;
3761 fn poll_next(&self) -> Option<String> {
3762 Some("hello".into())
3763 }
3764}
3765
3766fn other() {}
3767"#;
3768 let ours = r#"struct Foo;
3769struct Bar;
3770
3771impl Stream for Foo {
3772 type Item = i32;
3773 fn poll_next(&self) -> Option<i32> {
3774 let x = compute();
3775 Some(x + 1)
3776 }
3777}
3778
3779impl Stream for Bar {
3780 type Item = String;
3781 fn poll_next(&self) -> Option<String> {
3782 Some("hello".into())
3783 }
3784}
3785
3786fn other() {}
3787"#;
3788 let theirs = r#"struct Foo;
3789struct Bar;
3790
3791impl Stream for Foo {
3792 type Item = i32;
3793 fn poll_next(&self) -> Option<i32> {
3794 Some(1)
3795 }
3796}
3797
3798impl Stream for Bar {
3799 type Item = String;
3800 fn poll_next(&self) -> Option<String> {
3801 let s = format!("hello {}", name);
3802 Some(s)
3803 }
3804}
3805
3806fn other() {}
3807"#;
3808 let result = entity_merge(base, ours, theirs, "test.rs");
3809 assert!(
3810 result.is_clean(),
3811 "Same trait, different types should not conflict. Conflicts: {:?}",
3812 result.conflicts,
3813 );
3814 assert!(result.content.contains("impl Stream for Foo"), "Should have Foo impl");
3815 assert!(result.content.contains("impl Stream for Bar"), "Should have Bar impl");
3816 assert!(result.content.contains("compute()"), "Should have ours' Foo change");
3817 assert!(result.content.contains("format!"), "Should have theirs' Bar change");
3818 }
3819
3820 #[test]
3821 fn test_rust_doc_comment_plus_body_change() {
3822 let base = r#"fn add(a: i32, b: i32) -> i32 {
3825 a + b
3826}
3827
3828fn subtract(a: i32, b: i32) -> i32 {
3829 a - b
3830}
3831"#;
3832 let ours = r#"/// Adds two numbers together.
3833fn add(a: i32, b: i32) -> i32 {
3834 a + b
3835}
3836
3837fn subtract(a: i32, b: i32) -> i32 {
3838 a - b
3839}
3840"#;
3841 let theirs = r#"fn add(a: i32, b: i32) -> i32 {
3842 a + b
3843}
3844
3845fn subtract(a: i32, b: i32) -> i32 {
3846 a - b - 1
3847}
3848"#;
3849 let result = entity_merge(base, ours, theirs, "test.rs");
3850 assert!(
3851 result.is_clean(),
3852 "Rust doc comment + body change should merge. Conflicts: {:?}",
3853 result.conflicts,
3854 );
3855 assert!(result.content.contains("/// Adds two numbers"), "Should have ours doc comment");
3856 assert!(result.content.contains("a - b - 1"), "Should have theirs body change");
3857 }
3858
3859 #[test]
3860 fn test_both_add_different_doc_comments() {
3861 let base = r#"fn add(a: i32, b: i32) -> i32 {
3863 a + b
3864}
3865
3866fn subtract(a: i32, b: i32) -> i32 {
3867 a - b
3868}
3869"#;
3870 let ours = r#"/// Adds two numbers.
3871fn add(a: i32, b: i32) -> i32 {
3872 a + b
3873}
3874
3875fn subtract(a: i32, b: i32) -> i32 {
3876 a - b
3877}
3878"#;
3879 let theirs = r#"fn add(a: i32, b: i32) -> i32 {
3880 a + b
3881}
3882
3883/// Subtracts b from a.
3884fn subtract(a: i32, b: i32) -> i32 {
3885 a - b
3886}
3887"#;
3888 let result = entity_merge(base, ours, theirs, "test.rs");
3889 assert!(
3890 result.is_clean(),
3891 "Both adding doc comments to different functions should merge. Conflicts: {:?}",
3892 result.conflicts,
3893 );
3894 assert!(result.content.contains("/// Adds two numbers"), "Should have add's doc comment");
3895 assert!(result.content.contains("/// Subtracts b from a"), "Should have subtract's doc comment");
3896 }
3897
3898 #[test]
3899 fn test_go_import_block_both_add_different() {
3900 let base = "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n";
3902 let ours = "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n";
3903 let theirs = "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"io\"\n)\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n";
3904 let result = entity_merge(base, ours, theirs, "main.go");
3905 eprintln!("Go import block: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3906 eprintln!("Content:\n{}", result.content);
3907 }
3909
3910 #[test]
3911 fn test_python_class_both_add_methods() {
3912 let base = "class Calculator:\n def add(self, a, b):\n return a + b\n";
3914 let ours = "class Calculator:\n def add(self, a, b):\n return a + b\n\n def multiply(self, a, b):\n return a * b\n";
3915 let theirs = "class Calculator:\n def add(self, a, b):\n return a + b\n\n def divide(self, a, b):\n return a / b\n";
3916 let result = entity_merge(base, ours, theirs, "test.py");
3917 eprintln!("Python class: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
3918 eprintln!("Content:\n{}", result.content);
3919 assert!(
3920 result.is_clean(),
3921 "Both adding methods to Python class should merge. Conflicts: {:?}",
3922 result.conflicts,
3923 );
3924 assert!(result.content.contains("multiply"), "Should have multiply");
3925 assert!(result.content.contains("divide"), "Should have divide");
3926 }
3927
3928 #[test]
3929 fn test_interstitial_conflict_not_silently_embedded() {
3930 let base = r#"export { alpha } from "./alpha";
3941
3942// Section: data utilities
3943// TODO: add more exports here
3944
3945export { beta } from "./beta";
3946"#;
3947 let ours = r#"export { alpha } from "./alpha";
3948
3949// Section: data utilities (sorting)
3950// Sorting helpers for list views
3951
3952export { beta } from "./beta";
3953"#;
3954 let theirs = r#"export { alpha } from "./alpha";
3955
3956// Section: data utilities (filtering)
3957// Filtering helpers for search views
3958
3959export { beta } from "./beta";
3960"#;
3961 let result = entity_merge(base, ours, theirs, "index.ts");
3962
3963 let has_markers = result.content.contains("<<<<<<<") || result.content.contains(">>>>>>>");
3966 if has_markers {
3967 assert!(
3968 !result.is_clean(),
3969 "BUG: is_clean()=true but merged content has conflict markers!\n\
3970 stats: {}\nconflicts: {:?}\ncontent:\n{}",
3971 result.stats, result.conflicts, result.content
3972 );
3973 assert!(
3974 result.stats.entities_conflicted > 0,
3975 "entities_conflicted should be > 0 when markers are present"
3976 );
3977 }
3978
3979 if result.is_clean() {
3981 assert!(
3982 !has_markers,
3983 "Clean merge should not contain conflict markers!\ncontent:\n{}",
3984 result.content
3985 );
3986 }
3987 }
3988
3989 #[test]
3990 fn test_pre_conflicted_input_not_treated_as_clean() {
3991 let base = "";
3994 let theirs = "";
3995 let ours = r#"/**
3996 * MIT License
3997 */
3998
3999<<<<<<<< HEAD:src/lib/exports/index.ts
4000export { renderDocToBuffer } from "./doc-exporter";
4001export type { ExportOptions, ExportMetadata, RenderContext } from "./types";
4002========
4003export * from "./editor";
4004export * from "./types";
4005>>>>>>>> feature:packages/core/src/editor/index.ts
4006"#;
4007 let result = entity_merge(base, ours, theirs, "index.ts");
4008
4009 assert!(
4010 !result.is_clean(),
4011 "Pre-conflicted input must not be reported as clean!\n\
4012 stats: {}\nconflicts: {:?}",
4013 result.stats, result.conflicts,
4014 );
4015 assert!(result.stats.entities_conflicted > 0);
4016 assert!(!result.conflicts.is_empty());
4017 }
4018
4019 #[test]
4020 fn test_multi_line_signature_classified_as_syntax() {
4021 let base = "function process(\n a: number,\n b: string\n) {\n return a;\n}\n";
4023 let ours = "function process(\n a: number,\n b: string,\n c: boolean\n) {\n return a;\n}\n";
4024 let theirs = "function process(\n a: number,\n b: number\n) {\n return a;\n}\n";
4025 let complexity = crate::conflict::classify_conflict(Some(base), Some(ours), Some(theirs));
4026 assert_eq!(
4027 complexity,
4028 crate::conflict::ConflictComplexity::Syntax,
4029 "Multi-line signature change should be classified as Syntax, got {:?}",
4030 complexity
4031 );
4032 }
4033
4034 #[test]
4035 fn test_grouped_import_merge_preserves_groups() {
4036 let base = "import os\nimport sys\n\nfrom collections import OrderedDict\nfrom typing import List\n";
4037 let ours = "import os\nimport sys\nimport json\n\nfrom collections import OrderedDict\nfrom typing import List\n";
4038 let theirs = "import os\nimport sys\n\nfrom collections import OrderedDict\nfrom collections import defaultdict\nfrom typing import List\n";
4039 let result = merge_imports_commutatively(base, ours, theirs);
4040 let lines: Vec<&str> = result.lines().collect();
4042 let json_idx = lines.iter().position(|l| l.contains("json"));
4043 let blank_idx = lines.iter().position(|l| l.trim().is_empty());
4044 let defaultdict_idx = lines.iter().position(|l| l.contains("defaultdict"));
4045 assert!(json_idx.is_some(), "json import should be present");
4046 assert!(blank_idx.is_some(), "blank line separator should be present");
4047 assert!(defaultdict_idx.is_some(), "defaultdict import should be present");
4048 assert!(json_idx.unwrap() < blank_idx.unwrap(), "json should be in first group");
4050 assert!(defaultdict_idx.unwrap() > blank_idx.unwrap(), "defaultdict should be in second group");
4051 }
4052
4053 #[test]
4054 fn test_configurable_duplicate_threshold() {
4055 let entities: Vec<SemanticEntity> = (0..15).map(|i| SemanticEntity {
4057 id: format!("test::function::test_{}", i),
4058 file_path: "test.ts".to_string(),
4059 entity_type: "function".to_string(),
4060 name: "test".to_string(),
4061 parent_id: None,
4062 content: format!("function test() {{ return {}; }}", i),
4063 content_hash: format!("hash_{}", i),
4064 structural_hash: None,
4065 start_line: i * 3 + 1,
4066 end_line: i * 3 + 3,
4067 metadata: None,
4068 }).collect();
4069 assert!(has_excessive_duplicates(&entities));
4071 std::env::set_var("WEAVE_MAX_DUPLICATES", "20");
4073 assert!(!has_excessive_duplicates(&entities));
4074 std::env::remove_var("WEAVE_MAX_DUPLICATES");
4075 }
4076
4077 #[test]
4078 fn test_ts_multiline_import_consolidation() {
4079 let base = "\
4082import type { Foo } from \"./foo\"
4083import {
4084 type a,
4085 type b,
4086 type c,
4087} from \"./foo\"
4088
4089export function bar() {
4090 return 1;
4091}
4092";
4093 let ours = base;
4094 let theirs = "\
4095import {
4096 type Foo,
4097 type a,
4098 type b,
4099 type c,
4100} from \"./foo\"
4101
4102export function bar() {
4103 return 1;
4104}
4105";
4106 let result = entity_merge(base, ours, theirs, "test.ts");
4107 eprintln!("TS import consolidation: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
4108 eprintln!("Content:\n{}", result.content);
4109 assert!(result.content.contains("import {"), "import {{ must not be dropped");
4111 assert!(result.content.contains("type Foo,"), "type Foo must be present");
4112 assert!(result.content.contains("} from \"./foo\""), "closing must be present");
4113 assert!(!result.content.contains("import type { Foo }"), "old separate import should be removed");
4114 }
4115
4116 #[test]
4117 fn test_ts_multiline_import_both_modify() {
4118 let base = "\
4120import type { Foo } from \"./foo\"
4121import {
4122 type a,
4123 type b,
4124 type c,
4125} from \"./foo\"
4126
4127export function bar() {
4128 return 1;
4129}
4130";
4131 let ours = "\
4133import {
4134 type Foo,
4135 type a,
4136 type b,
4137 type c,
4138 type d,
4139} from \"./foo\"
4140
4141export function bar() {
4142 return 1;
4143}
4144";
4145 let theirs = "\
4147import {
4148 type Foo,
4149 type a,
4150 type b,
4151 type c,
4152 type e,
4153} from \"./foo\"
4154
4155export function bar() {
4156 return 1;
4157}
4158";
4159 let result = entity_merge(base, ours, theirs, "test.ts");
4160 eprintln!("TS import both modify: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
4161 eprintln!("Content:\n{}", result.content);
4162 assert!(result.content.contains("import {"), "import {{ must not be dropped");
4163 assert!(result.content.contains("type Foo,"), "type Foo must be present");
4164 assert!(result.content.contains("type d,"), "ours addition must be present");
4165 assert!(result.content.contains("type e,"), "theirs addition must be present");
4166 assert!(result.content.contains("} from \"./foo\""), "closing must be present");
4167 }
4168
4169 #[test]
4170 fn test_ts_multiline_import_no_entities() {
4171 let base = "\
4173import type { Foo } from \"./foo\"
4174import {
4175 type a,
4176 type b,
4177 type c,
4178} from \"./foo\"
4179";
4180 let ours = base;
4181 let theirs = "\
4182import {
4183 type Foo,
4184 type a,
4185 type b,
4186 type c,
4187} from \"./foo\"
4188";
4189 let result = entity_merge(base, ours, theirs, "test.ts");
4190 eprintln!("TS import no entities: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
4191 eprintln!("Content:\n{}", result.content);
4192 assert!(result.content.contains("import {"), "import {{ must not be dropped");
4193 assert!(result.content.contains("type Foo,"), "type Foo must be present");
4194 }
4195
4196 #[test]
4197 fn test_ts_multiline_import_export_variable() {
4198 let base = "\
4200import type { Foo } from \"./foo\"
4201import {
4202 type a,
4203 type b,
4204 type c,
4205} from \"./foo\"
4206
4207export const X = 1;
4208
4209export function bar() {
4210 return 1;
4211}
4212";
4213 let ours = "\
4214import type { Foo } from \"./foo\"
4215import {
4216 type a,
4217 type b,
4218 type c,
4219 type d,
4220} from \"./foo\"
4221
4222export const X = 1;
4223
4224export function bar() {
4225 return 1;
4226}
4227";
4228 let theirs = "\
4229import {
4230 type Foo,
4231 type a,
4232 type b,
4233 type c,
4234} from \"./foo\"
4235
4236export const X = 2;
4237
4238export function bar() {
4239 return 1;
4240}
4241";
4242 let result = entity_merge(base, ours, theirs, "test.ts");
4243 eprintln!("TS import + export var: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
4244 eprintln!("Content:\n{}", result.content);
4245 assert!(result.content.contains("import {"), "import {{ must not be dropped");
4246 }
4247
4248 #[test]
4249 fn test_ts_multiline_import_adjacent_to_entity() {
4250 let base = "\
4252import type { Foo } from \"./foo\"
4253import {
4254 type a,
4255 type b,
4256 type c,
4257} from \"./foo\"
4258export function bar() {
4259 return 1;
4260}
4261";
4262 let ours = base;
4263 let theirs = "\
4264import {
4265 type Foo,
4266 type a,
4267 type b,
4268 type c,
4269} from \"./foo\"
4270export function bar() {
4271 return 1;
4272}
4273";
4274 let result = entity_merge(base, ours, theirs, "test.ts");
4275 eprintln!("TS import adjacent: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
4276 eprintln!("Content:\n{}", result.content);
4277 assert!(result.content.contains("import {"), "import {{ must not be dropped");
4278 assert!(result.content.contains("type Foo,"), "type Foo must be present");
4279 }
4280
4281 #[test]
4282 fn test_ts_multiline_import_both_consolidate_differently() {
4283 let base = "\
4285import type { Foo } from \"./foo\"
4286import {
4287 type a,
4288 type b,
4289} from \"./foo\"
4290
4291export function bar() {
4292 return 1;
4293}
4294";
4295 let ours = "\
4296import {
4297 type Foo,
4298 type a,
4299 type b,
4300 type c,
4301} from \"./foo\"
4302
4303export function bar() {
4304 return 1;
4305}
4306";
4307 let theirs = "\
4308import {
4309 type Foo,
4310 type a,
4311 type b,
4312 type d,
4313} from \"./foo\"
4314
4315export function bar() {
4316 return 1;
4317}
4318";
4319 let result = entity_merge(base, ours, theirs, "test.ts");
4320 eprintln!("TS both consolidate: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
4321 eprintln!("Content:\n{}", result.content);
4322 assert!(result.content.contains("import {"), "import {{ must not be dropped");
4323 assert!(result.content.contains("type Foo,"), "type Foo must be present");
4324 assert!(result.content.contains("} from \"./foo\""), "closing must be present");
4325 }
4326
4327 #[test]
4328 fn test_ts_multiline_import_ours_adds_theirs_consolidates() {
4329 let base = "\
4331import type { Foo } from \"./foo\"
4332import {
4333 type a,
4334 type b,
4335 type c,
4336} from \"./foo\"
4337
4338export function bar() {
4339 return 1;
4340}
4341";
4342 let ours = "\
4344import type { Foo } from \"./foo\"
4345import {
4346 type a,
4347 type b,
4348 type c,
4349 type d,
4350} from \"./foo\"
4351
4352export function bar() {
4353 return 1;
4354}
4355";
4356 let theirs = "\
4358import {
4359 type Foo,
4360 type a,
4361 type b,
4362 type c,
4363} from \"./foo\"
4364
4365export function bar() {
4366 return 1;
4367}
4368";
4369 let result = entity_merge(base, ours, theirs, "test.ts");
4370 eprintln!("TS import ours-adds theirs-consolidates: clean={}, conflicts={:?}", result.is_clean(), result.conflicts);
4371 eprintln!("Content:\n{}", result.content);
4372 assert!(result.content.contains("import {"), "import {{ must not be dropped");
4373 assert!(result.content.contains("type d,"), "ours addition must be present");
4374 assert!(result.content.contains("} from \"./foo\""), "closing must be present");
4375 }
4376}