1use crate::amalgamator::{AmalgamResult, amalgam_to_merge_result, amalgamate};
15use crate::diff3;
16use crate::parser::{self, ParseError};
17use crate::patterns::PatternRegistry;
18use crate::search::{self, SearchConfig};
19use crate::types::*;
20use crate::vsa;
21
22pub struct ResolverConfig {
24 pub auto_accept_threshold: Confidence,
26 pub max_vsa_candidates: usize,
28 pub search_config: SearchConfig,
30 pub language: Option<Language>,
32}
33
34impl Default for ResolverConfig {
35 fn default() -> Self {
36 Self {
37 auto_accept_threshold: Confidence::Medium,
38 max_vsa_candidates: 100,
39 search_config: SearchConfig::default(),
40 language: None,
41 }
42 }
43}
44
45pub struct Resolver {
47 config: ResolverConfig,
48 patterns: PatternRegistry,
49}
50
51#[derive(Debug)]
53pub struct ResolverOutput {
54 pub resolution: Option<ResolutionCandidate>,
56 pub candidates: Vec<ResolutionCandidate>,
58 pub strategies_tried: Vec<ResolutionStrategy>,
60 pub diff3_result: MergeResult,
62}
63
64impl Resolver {
65 pub fn new(config: ResolverConfig) -> Self {
66 Self {
67 config,
68 patterns: PatternRegistry::new(),
69 }
70 }
71
72 pub fn resolve_file(&self, base: &str, left: &str, right: &str) -> FileResolverOutput {
77 let scenario = MergeScenario::new(base, left, right);
78 let diff3_result = diff3::diff3_merge(&scenario);
79
80 match &diff3_result {
81 MergeResult::Resolved(content) => {
82 FileResolverOutput {
84 merged_content: content.clone(),
85 conflicts: vec![],
86 all_resolved: true,
87 }
88 }
89 MergeResult::Conflict { .. } => {
90 let _conflicts = diff3::extract_conflicts(&scenario);
92 let mut merged_parts = Vec::new();
93 let mut unresolved = Vec::new();
94 let mut all_resolved = true;
95
96 let hunks = diff3::diff3_hunks(&scenario);
98
99 for hunk in &hunks {
100 match hunk {
101 Diff3Hunk::Stable(lines)
102 | Diff3Hunk::LeftChanged(lines)
103 | Diff3Hunk::RightChanged(lines) => {
104 for line in lines {
105 merged_parts.push(line.clone());
106 }
107 }
108 Diff3Hunk::Conflict { base, left, right } => {
109 let conflict_scenario = MergeScenario::new(
110 base.join("\n").as_str().to_string(),
111 left.join("\n").as_str().to_string(),
112 right.join("\n").as_str().to_string(),
113 );
114
115 let output = self.resolve_conflict(
116 &conflict_scenario.base,
117 &conflict_scenario.left,
118 &conflict_scenario.right,
119 );
120
121 if let Some(ref resolution) = output.resolution {
122 for line in resolution.content.lines() {
123 merged_parts.push(line.to_string());
124 }
125 } else {
126 all_resolved = false;
127 merged_parts.push("<<<<<<< LEFT".to_string());
129 merged_parts.extend(left.iter().cloned());
130 merged_parts.push("||||||| BASE".to_string());
131 merged_parts.extend(base.iter().cloned());
132 merged_parts.push("=======".to_string());
133 merged_parts.extend(right.iter().cloned());
134 merged_parts.push(">>>>>>> RIGHT".to_string());
135 }
136 unresolved.push(output);
137 }
138 }
139 }
140
141 FileResolverOutput {
142 merged_content: merged_parts.join("\n"),
143 conflicts: unresolved,
144 all_resolved,
145 }
146 }
147 }
148 }
149
150 pub fn resolve_conflict(&self, base: &str, left: &str, right: &str) -> ResolverOutput {
152 let mut candidates: Vec<ResolutionCandidate> = Vec::new();
153 let mut strategies_tried = Vec::new();
154
155 let text_scenario = MergeScenario::new(base, left, right);
156 let diff3_result = diff3::diff3_merge(&text_scenario);
157
158 strategies_tried.push(ResolutionStrategy::PatternRule);
160 if let Some(resolution) = self.patterns.try_resolve(&text_scenario) {
161 if resolution.confidence >= self.config.auto_accept_threshold {
162 return ResolverOutput {
163 resolution: Some(resolution.clone()),
164 candidates: vec![resolution],
165 strategies_tried,
166 diff3_result,
167 };
168 }
169 candidates.push(resolution);
170 }
171
172 if let Some(lang) = self.config.language {
174 strategies_tried.push(ResolutionStrategy::StructuredMerge);
175 match self.try_structured_merge(base, left, right, lang) {
176 Ok(Some(result)) => {
177 if let MergeResult::Resolved(content) = result {
178 let resolution = ResolutionCandidate {
179 content,
180 confidence: Confidence::High,
181 strategy: ResolutionStrategy::StructuredMerge,
182 };
183 if resolution.confidence >= self.config.auto_accept_threshold {
184 return ResolverOutput {
185 resolution: Some(resolution.clone()),
186 candidates: vec![resolution],
187 strategies_tried,
188 diff3_result,
189 };
190 }
191 candidates.push(resolution);
192 }
193 }
194 Ok(None) => {} Err(_) => {} }
197 }
198
199 if let Some(lang) = self.config.language {
201 strategies_tried.push(ResolutionStrategy::VersionSpaceAlgebra);
202 if let Ok(vsa_candidates) = self.try_vsa_resolve(base, left, right, lang) {
203 candidates.extend(vsa_candidates);
204 }
205 }
206
207 strategies_tried.push(ResolutionStrategy::SearchBased);
209 let search_candidates = search::search_resolve(&text_scenario, &self.config.search_config);
210 candidates.extend(search_candidates);
211
212 candidates.sort_by(|a, b| b.confidence.cmp(&a.confidence));
214
215 let mut seen = std::collections::HashSet::new();
217 candidates.retain(|c| seen.insert(c.content.clone()));
218
219 let resolution = candidates
220 .first()
221 .filter(|c| c.confidence >= self.config.auto_accept_threshold)
222 .cloned();
223
224 ResolverOutput {
225 resolution,
226 candidates,
227 strategies_tried,
228 diff3_result,
229 }
230 }
231
232 fn try_structured_merge(
234 &self,
235 base: &str,
236 left: &str,
237 right: &str,
238 lang: Language,
239 ) -> Result<Option<MergeResult>, ParseError> {
240 let base_tree = parser::parse_to_cst(base, lang)?;
241 let left_tree = parser::parse_to_cst(left, lang)?;
242 let right_tree = parser::parse_to_cst(right, lang)?;
243
244 let scenario = MergeScenario::new(&base_tree, &left_tree, &right_tree);
245 let result = amalgamate(&scenario);
246
247 match result {
248 AmalgamResult::Merged(_) => Ok(Some(amalgam_to_merge_result(&result))),
249 AmalgamResult::Conflict { .. } => Ok(None),
250 }
251 }
252
253 fn try_vsa_resolve(
255 &self,
256 base: &str,
257 left: &str,
258 right: &str,
259 lang: Language,
260 ) -> Result<Vec<ResolutionCandidate>, ParseError> {
261 let base_tree = parser::parse_to_cst(base, lang)?;
262 let left_tree = parser::parse_to_cst(left, lang)?;
263 let right_tree = parser::parse_to_cst(right, lang)?;
264
265 let scenario = MergeScenario::new(&base_tree, &left_tree, &right_tree);
266 Ok(vsa::resolve_via_vsa(
267 &scenario,
268 self.config.max_vsa_candidates,
269 ))
270 }
271}
272
273#[derive(Debug)]
275pub struct FileResolverOutput {
276 pub merged_content: String,
278 pub conflicts: Vec<ResolverOutput>,
280 pub all_resolved: bool,
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_clean_merge() {
290 let resolver = Resolver::new(ResolverConfig::default());
291 let result = resolver.resolve_file(
292 "line1\nline2\nline3\n",
293 "lineA\nline2\nline3\n",
294 "line1\nline2\nlineC\n",
295 );
296 assert!(result.all_resolved);
297 }
298
299 #[test]
300 fn test_pattern_resolves_whitespace() {
301 let resolver = Resolver::new(ResolverConfig::default());
302 let output = resolver.resolve_conflict("int x = 1;", "int x = 1;", "int x = 1;");
303 assert!(output.resolution.is_some());
304 assert_eq!(
305 output.resolution.unwrap().strategy,
306 ResolutionStrategy::PatternRule
307 );
308 }
309
310 #[test]
311 fn test_search_fallback() {
312 let resolver = Resolver::new(ResolverConfig::default());
313 let output = resolver.resolve_conflict(
314 "fn foo() { return 1; }",
315 "fn foo() { return 2; }",
316 "fn bar() { return 1; }",
317 );
318 assert!(!output.candidates.is_empty());
320 }
321
322 #[test]
323 fn test_structured_merge_rust() {
324 let config = ResolverConfig {
325 language: Some(Language::Rust),
326 ..Default::default()
327 };
328 let resolver = Resolver::new(config);
329 let output = resolver.resolve_conflict(
330 "fn main() { let x = 1; }",
331 "fn main() { let x = 2; }",
332 "fn main() { let x = 1; let y = 3; }",
333 );
334 assert!(
336 output
337 .strategies_tried
338 .contains(&ResolutionStrategy::StructuredMerge)
339 );
340 }
341}