Skip to main content

weave_core/
reconstruct.rs

1use std::collections::HashMap;
2
3use sem_core::model::entity::SemanticEntity;
4
5use crate::conflict::MarkerFormat;
6use crate::merge::ResolvedEntity;
7use crate::region::FileRegion;
8
9/// Reconstruct a merged file from resolved entities and merged interstitials.
10///
11/// Uses "ours" region ordering as the skeleton. Inserts theirs-only additions
12/// at their relative position (after the entity that precedes them in theirs).
13pub fn reconstruct(
14    ours_regions: &[FileRegion],
15    theirs_regions: &[FileRegion],
16    theirs_entities: &[SemanticEntity],
17    ours_entity_map: &HashMap<&str, &SemanticEntity>,
18    resolved_entities: &HashMap<String, ResolvedEntity>,
19    merged_interstitials: &HashMap<String, String>,
20    marker_format: &MarkerFormat,
21) -> String {
22    let mut output = String::new();
23
24    // Track which entity IDs we've emitted (from ours skeleton)
25    let mut emitted_entities: std::collections::HashSet<String> = std::collections::HashSet::new();
26
27    // Identify theirs-only entities (not in ours)
28    let theirs_only: Vec<&SemanticEntity> = theirs_entities
29        .iter()
30        .filter(|e| !ours_entity_map.contains_key(e.id.as_str()))
31        .collect();
32
33    // Build a map of theirs-only entities by what precedes them in theirs ordering
34    let mut theirs_insertions: HashMap<Option<String>, Vec<&SemanticEntity>> = HashMap::new();
35    for entity in &theirs_only {
36        let predecessor = find_predecessor_in_regions(theirs_regions, &entity.id);
37        theirs_insertions
38            .entry(predecessor)
39            .or_default()
40            .push(entity);
41    }
42
43    // Walk ours regions as skeleton
44    for region in ours_regions {
45        match region {
46            FileRegion::Interstitial(interstitial) => {
47                // Use merged interstitial if available, otherwise ours
48                if let Some(merged) = merged_interstitials.get(&interstitial.position_key) {
49                    output.push_str(merged);
50                } else {
51                    output.push_str(&interstitial.content);
52                }
53            }
54            FileRegion::Entity(entity_region) => {
55                // Before emitting ours entity, check if there are theirs-only insertions
56                // that should go before this entity (predecessor is the entity before this one)
57
58                // Emit the resolved entity
59                if let Some(resolved) = resolved_entities.get(&entity_region.entity_id) {
60                    match resolved {
61                        ResolvedEntity::Clean(region) => {
62                            output.push_str(&region.content);
63                            if !region.content.is_empty() && !region.content.ends_with('\n') {
64                                output.push('\n');
65                            }
66                        }
67                        ResolvedEntity::Conflict(conflict) => {
68                            output.push_str(&conflict.to_conflict_markers(marker_format));
69                        }
70                        ResolvedEntity::ScopedConflict { content, .. } => {
71                            output.push_str(content);
72                            if !content.is_empty() && !content.ends_with('\n') {
73                                output.push('\n');
74                            }
75                        }
76                        ResolvedEntity::Deleted => {
77                            // Skip deleted entities
78                        }
79                    }
80                } else {
81                    // Entity not in resolved map — keep ours content
82                    output.push_str(&entity_region.content);
83                    if !entity_region.content.is_empty()
84                        && !entity_region.content.ends_with('\n')
85                    {
86                        output.push('\n');
87                    }
88                }
89
90                emitted_entities.insert(entity_region.entity_id.clone());
91
92                // Insert theirs-only entities that should come after this entity
93                if let Some(insertions) = theirs_insertions.get(&Some(entity_region.entity_id.clone())) {
94                    for theirs_entity in insertions {
95                        if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
96                            match resolved {
97                                ResolvedEntity::Clean(region) => {
98                                    output.push('\n');
99                                    output.push_str(&region.content);
100                                    if !region.content.is_empty()
101                                        && !region.content.ends_with('\n')
102                                    {
103                                        output.push('\n');
104                                    }
105                                }
106                                ResolvedEntity::Conflict(conflict) => {
107                                    output.push('\n');
108                                    output.push_str(&conflict.to_conflict_markers(marker_format));
109                                }
110                                ResolvedEntity::ScopedConflict { content, .. } => {
111                                    output.push('\n');
112                                    output.push_str(content);
113                                    if !content.is_empty() && !content.ends_with('\n') {
114                                        output.push('\n');
115                                    }
116                                }
117                                ResolvedEntity::Deleted => {}
118                            }
119                        }
120                        emitted_entities.insert(theirs_entity.id.clone());
121                    }
122                }
123            }
124        }
125    }
126
127    // Emit any theirs-only entities whose predecessor was None (should go at the start)
128    // or whose predecessor wasn't found — append at the end
129    if let Some(insertions) = theirs_insertions.get(&None) {
130        for theirs_entity in insertions {
131            if !emitted_entities.contains(&theirs_entity.id) {
132                if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
133                    emit_resolved(&mut output, resolved, marker_format);
134                }
135                emitted_entities.insert(theirs_entity.id.clone());
136            }
137        }
138    }
139
140    // Any remaining theirs-only entities not yet emitted (predecessor entity was deleted, etc.)
141    for (pred, insertions) in &theirs_insertions {
142        if pred.is_none() {
143            continue; // Already handled above
144        }
145        for theirs_entity in insertions {
146            if !emitted_entities.contains(&theirs_entity.id) {
147                if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
148                    emit_resolved(&mut output, resolved, marker_format);
149                }
150                emitted_entities.insert(theirs_entity.id.clone());
151            }
152        }
153    }
154
155    output
156}
157
158/// Emit a resolved entity into the output (for theirs-only insertions).
159fn emit_resolved(output: &mut String, resolved: &ResolvedEntity, marker_format: &MarkerFormat) {
160    match resolved {
161        ResolvedEntity::Clean(region) => {
162            if !output.is_empty() && !output.ends_with('\n') {
163                output.push('\n');
164            }
165            output.push('\n');
166            output.push_str(&region.content);
167            if !region.content.is_empty() && !region.content.ends_with('\n') {
168                output.push('\n');
169            }
170        }
171        ResolvedEntity::Conflict(conflict) => {
172            if !output.is_empty() && !output.ends_with('\n') {
173                output.push('\n');
174            }
175            output.push('\n');
176            output.push_str(&conflict.to_conflict_markers(marker_format));
177        }
178        ResolvedEntity::ScopedConflict { content, .. } => {
179            if !output.is_empty() && !output.ends_with('\n') {
180                output.push('\n');
181            }
182            output.push('\n');
183            output.push_str(content);
184            if !content.is_empty() && !content.ends_with('\n') {
185                output.push('\n');
186            }
187        }
188        ResolvedEntity::Deleted => {}
189    }
190}
191
192/// Find the entity ID that precedes the given entity in a region list.
193fn find_predecessor_in_regions(regions: &[FileRegion], entity_id: &str) -> Option<String> {
194    let mut last_entity_id: Option<String> = None;
195    for region in regions {
196        if let FileRegion::Entity(e) = region {
197            if e.entity_id == entity_id {
198                return last_entity_id;
199            }
200            last_entity_id = Some(e.entity_id.clone());
201        }
202    }
203    None
204}