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                // Chase the chain: if we insert X after this entity, also insert
94                // anything whose predecessor is X, then anything after that, etc.
95                // This handles multiple sequential additions (e.g. adding 3 keys
96                // at the end of a JSON file).
97                let mut current_pred = Some(entity_region.entity_id.clone());
98                while let Some(ref pred) = current_pred {
99                    if let Some(insertions) = theirs_insertions.get(&Some(pred.clone())) {
100                        let mut next_pred: Option<String> = None;
101                        for theirs_entity in insertions {
102                            if emitted_entities.contains(&theirs_entity.id) {
103                                continue;
104                            }
105                            if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
106                                match resolved {
107                                    ResolvedEntity::Clean(region) => {
108                                        // Only add blank-line separator for multi-line entities
109                                        // (functions, methods). Single-line entities (JSON props,
110                                        // struct fields) don't need one.
111                                        if region.content.trim_end().contains('\n') {
112                                            output.push('\n');
113                                        }
114                                        output.push_str(&region.content);
115                                        if !region.content.is_empty()
116                                            && !region.content.ends_with('\n')
117                                        {
118                                            output.push('\n');
119                                        }
120                                    }
121                                    ResolvedEntity::Conflict(conflict) => {
122                                        output.push('\n');
123                                        output.push_str(&conflict.to_conflict_markers(marker_format));
124                                    }
125                                    ResolvedEntity::ScopedConflict { content, .. } => {
126                                        output.push('\n');
127                                        output.push_str(content);
128                                        if !content.is_empty() && !content.ends_with('\n') {
129                                            output.push('\n');
130                                        }
131                                    }
132                                    ResolvedEntity::Deleted => {}
133                                }
134                            }
135                            emitted_entities.insert(theirs_entity.id.clone());
136                            next_pred = Some(theirs_entity.id.clone());
137                        }
138                        current_pred = next_pred;
139                    } else {
140                        break;
141                    }
142                }
143            }
144        }
145    }
146
147    // Emit any theirs-only entities whose predecessor was None (should go at the start)
148    // or whose predecessor wasn't found — append at the end
149    if let Some(insertions) = theirs_insertions.get(&None) {
150        for theirs_entity in insertions {
151            if !emitted_entities.contains(&theirs_entity.id) {
152                if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
153                    emit_resolved(&mut output, resolved, marker_format);
154                }
155                emitted_entities.insert(theirs_entity.id.clone());
156            }
157        }
158    }
159
160    // Any remaining theirs-only entities not yet emitted (predecessor entity was deleted, etc.)
161    for (pred, insertions) in &theirs_insertions {
162        if pred.is_none() {
163            continue; // Already handled above
164        }
165        for theirs_entity in insertions {
166            if !emitted_entities.contains(&theirs_entity.id) {
167                if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
168                    emit_resolved(&mut output, resolved, marker_format);
169                }
170                emitted_entities.insert(theirs_entity.id.clone());
171            }
172        }
173    }
174
175    output
176}
177
178/// Emit a resolved entity into the output (for theirs-only insertions).
179fn emit_resolved(output: &mut String, resolved: &ResolvedEntity, marker_format: &MarkerFormat) {
180    match resolved {
181        ResolvedEntity::Clean(region) => {
182            if !output.is_empty() && !output.ends_with('\n') {
183                output.push('\n');
184            }
185            output.push('\n');
186            output.push_str(&region.content);
187            if !region.content.is_empty() && !region.content.ends_with('\n') {
188                output.push('\n');
189            }
190        }
191        ResolvedEntity::Conflict(conflict) => {
192            if !output.is_empty() && !output.ends_with('\n') {
193                output.push('\n');
194            }
195            output.push('\n');
196            output.push_str(&conflict.to_conflict_markers(marker_format));
197        }
198        ResolvedEntity::ScopedConflict { content, .. } => {
199            if !output.is_empty() && !output.ends_with('\n') {
200                output.push('\n');
201            }
202            output.push('\n');
203            output.push_str(content);
204            if !content.is_empty() && !content.ends_with('\n') {
205                output.push('\n');
206            }
207        }
208        ResolvedEntity::Deleted => {}
209    }
210}
211
212/// Find the entity ID that precedes the given entity in a region list.
213fn find_predecessor_in_regions(regions: &[FileRegion], entity_id: &str) -> Option<String> {
214    let mut last_entity_id: Option<String> = None;
215    for region in regions {
216        if let FileRegion::Entity(e) = region {
217            if e.entity_id == entity_id {
218                return last_entity_id;
219            }
220            last_entity_id = Some(e.entity_id.clone());
221        }
222    }
223    None
224}