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    theirs_rename_base_ids: &std::collections::HashSet<String>,
22) -> String {
23    let mut output = String::new();
24
25    // Track which entity IDs we've emitted (from ours skeleton)
26    let mut emitted_entities: std::collections::HashSet<String> = std::collections::HashSet::new();
27
28    // Identify theirs-only entities (not in ours, and not renamed versions of ours entities)
29    let theirs_only: Vec<&SemanticEntity> = theirs_entities
30        .iter()
31        .filter(|e| {
32            !ours_entity_map.contains_key(e.id.as_str())
33                && !theirs_rename_base_ids.contains(&e.id)
34        })
35        .collect();
36
37    // Build a map of theirs-only entities by what precedes them in theirs ordering
38    let mut theirs_insertions: HashMap<Option<String>, Vec<&SemanticEntity>> = HashMap::new();
39    for entity in &theirs_only {
40        let predecessor = find_predecessor_in_regions(theirs_regions, &entity.id);
41        theirs_insertions
42            .entry(predecessor)
43            .or_default()
44            .push(entity);
45    }
46
47    // Walk ours regions as skeleton
48    for region in ours_regions {
49        match region {
50            FileRegion::Interstitial(interstitial) => {
51                // Use merged interstitial if available, otherwise ours
52                if let Some(merged) = merged_interstitials.get(&interstitial.position_key) {
53                    output.push_str(merged);
54                } else {
55                    output.push_str(&interstitial.content);
56                }
57            }
58            FileRegion::Entity(entity_region) => {
59                // Before emitting ours entity, check if there are theirs-only insertions
60                // that should go before this entity (predecessor is the entity before this one)
61
62                // Emit the resolved entity
63                if let Some(resolved) = resolved_entities.get(&entity_region.entity_id) {
64                    match resolved {
65                        ResolvedEntity::Clean(region) => {
66                            output.push_str(&region.content);
67                            if !region.content.is_empty() && !region.content.ends_with('\n') {
68                                output.push('\n');
69                            }
70                        }
71                        ResolvedEntity::Conflict(conflict) => {
72                            output.push_str(&conflict.to_conflict_markers(marker_format));
73                        }
74                        ResolvedEntity::ScopedConflict { content, .. } => {
75                            output.push_str(content);
76                            if !content.is_empty() && !content.ends_with('\n') {
77                                output.push('\n');
78                            }
79                        }
80                        ResolvedEntity::Deleted => {
81                            // Skip deleted entities
82                        }
83                    }
84                } else {
85                    // Entity not in resolved map — keep ours content
86                    output.push_str(&entity_region.content);
87                    if !entity_region.content.is_empty()
88                        && !entity_region.content.ends_with('\n')
89                    {
90                        output.push('\n');
91                    }
92                }
93
94                emitted_entities.insert(entity_region.entity_id.clone());
95
96                // Insert theirs-only entities that should come after this entity.
97                // Chase the chain: if we insert X after this entity, also insert
98                // anything whose predecessor is X, then anything after that, etc.
99                // This handles multiple sequential additions (e.g. adding 3 keys
100                // at the end of a JSON file).
101                let mut current_pred = Some(entity_region.entity_id.clone());
102                while let Some(ref pred) = current_pred {
103                    if let Some(insertions) = theirs_insertions.get(&Some(pred.clone())) {
104                        let mut next_pred: Option<String> = None;
105                        for theirs_entity in insertions {
106                            if emitted_entities.contains(&theirs_entity.id) {
107                                continue;
108                            }
109                            if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
110                                match resolved {
111                                    ResolvedEntity::Clean(region) => {
112                                        // Only add blank-line separator for multi-line entities
113                                        // (functions, methods). Single-line entities (JSON props,
114                                        // struct fields) don't need one.
115                                        if region.content.trim_end().contains('\n') {
116                                            output.push('\n');
117                                        }
118                                        output.push_str(&region.content);
119                                        if !region.content.is_empty()
120                                            && !region.content.ends_with('\n')
121                                        {
122                                            output.push('\n');
123                                        }
124                                    }
125                                    ResolvedEntity::Conflict(conflict) => {
126                                        output.push('\n');
127                                        output.push_str(&conflict.to_conflict_markers(marker_format));
128                                    }
129                                    ResolvedEntity::ScopedConflict { content, .. } => {
130                                        output.push('\n');
131                                        output.push_str(content);
132                                        if !content.is_empty() && !content.ends_with('\n') {
133                                            output.push('\n');
134                                        }
135                                    }
136                                    ResolvedEntity::Deleted => {}
137                                }
138                            }
139                            emitted_entities.insert(theirs_entity.id.clone());
140                            next_pred = Some(theirs_entity.id.clone());
141                        }
142                        current_pred = next_pred;
143                    } else {
144                        break;
145                    }
146                }
147            }
148        }
149    }
150
151    // Emit any theirs-only entities whose predecessor was None (should go at the start)
152    // or whose predecessor wasn't found — append at the end
153    if let Some(insertions) = theirs_insertions.get(&None) {
154        for theirs_entity in insertions {
155            if !emitted_entities.contains(&theirs_entity.id) {
156                if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
157                    emit_resolved(&mut output, resolved, marker_format);
158                }
159                emitted_entities.insert(theirs_entity.id.clone());
160            }
161        }
162    }
163
164    // Any remaining theirs-only entities not yet emitted (predecessor entity was deleted, etc.)
165    for (pred, insertions) in &theirs_insertions {
166        if pred.is_none() {
167            continue; // Already handled above
168        }
169        for theirs_entity in insertions {
170            if !emitted_entities.contains(&theirs_entity.id) {
171                if let Some(resolved) = resolved_entities.get(&theirs_entity.id) {
172                    emit_resolved(&mut output, resolved, marker_format);
173                }
174                emitted_entities.insert(theirs_entity.id.clone());
175            }
176        }
177    }
178
179    output
180}
181
182/// Emit a resolved entity into the output (for theirs-only insertions).
183fn emit_resolved(output: &mut String, resolved: &ResolvedEntity, marker_format: &MarkerFormat) {
184    match resolved {
185        ResolvedEntity::Clean(region) => {
186            if !output.is_empty() && !output.ends_with('\n') {
187                output.push('\n');
188            }
189            output.push('\n');
190            output.push_str(&region.content);
191            if !region.content.is_empty() && !region.content.ends_with('\n') {
192                output.push('\n');
193            }
194        }
195        ResolvedEntity::Conflict(conflict) => {
196            if !output.is_empty() && !output.ends_with('\n') {
197                output.push('\n');
198            }
199            output.push('\n');
200            output.push_str(&conflict.to_conflict_markers(marker_format));
201        }
202        ResolvedEntity::ScopedConflict { content, .. } => {
203            if !output.is_empty() && !output.ends_with('\n') {
204                output.push('\n');
205            }
206            output.push('\n');
207            output.push_str(content);
208            if !content.is_empty() && !content.ends_with('\n') {
209                output.push('\n');
210            }
211        }
212        ResolvedEntity::Deleted => {}
213    }
214}
215
216/// Find the entity ID that precedes the given entity in a region list.
217fn find_predecessor_in_regions(regions: &[FileRegion], entity_id: &str) -> Option<String> {
218    let mut last_entity_id: Option<String> = None;
219    for region in regions {
220        if let FileRegion::Entity(e) = region {
221            if e.entity_id == entity_id {
222                return last_entity_id;
223            }
224            last_entity_id = Some(e.entity_id.clone());
225        }
226    }
227    None
228}