use std::collections::HashMap;
use std::path::Path;
use automerge::{ObjType, ReadDoc, Value, transaction::Transactable};
use sem_core::parser::registry::ParserRegistry;
use weave_core::region::{extract_regions, FileRegion};
use crate::content::get_entity_content;
use crate::error::Result;
use crate::merge::CrdtMergeResult;
use crate::ops::{get_str, upsert_entity};
use crate::state::EntityStateDoc;
pub fn sync_from_files(
state: &mut EntityStateDoc,
repo_root: &Path,
file_paths: &[String],
registry: &ParserRegistry,
) -> Result<usize> {
let mut count = 0;
for file_path in file_paths {
let full_path = repo_root.join(file_path);
let content = match std::fs::read_to_string(&full_path) {
Ok(c) => c,
Err(_) => continue, };
let plugin = match registry.get_plugin(file_path) {
Some(p) => p,
None => continue, };
let entities = plugin.extract_entities(&content, file_path);
for entity in &entities {
upsert_entity(
state,
&entity.id,
&entity.name,
&entity.entity_type,
file_path,
&entity.content_hash,
)?;
let entities_map = state.entities_id()?;
if let Ok(Some((_, entity_obj))) = state.doc.get(&entities_map, entity.id.as_str()) {
state.doc.put(&entity_obj, "content", entity.content.as_str())?;
let base = get_str(&state.doc, &entity_obj, "base_content").unwrap_or_default();
if base.is_empty() {
state.doc.put(&entity_obj, "base_content", entity.content.as_str())?;
}
}
count += 1;
}
let entity_order = state.file_entity_order_id()?;
let order_list = state.doc.put_object(&entity_order, file_path.as_str(), ObjType::List)?;
for (i, entity) in entities.iter().enumerate() {
state.doc.insert(&order_list, i, entity.id.as_str())?;
}
let regions = extract_regions(&content, &entities);
let interstitials_map = state.file_interstitials_id()?;
for region in ®ions {
if let FileRegion::Interstitial(inter) = region {
let key = format!("{}::{}", file_path, inter.position_key);
state.doc.put(&interstitials_map, key.as_str(), inter.content.as_str())?;
}
}
}
Ok(count)
}
pub fn extract_entity_ids(
content: &str,
file_path: &str,
registry: &ParserRegistry,
) -> Vec<(String, String, String)> {
let plugin = match registry.get_plugin(file_path) {
Some(p) => p,
None => return Vec::new(),
};
plugin
.extract_entities(content, file_path)
.into_iter()
.map(|e| (e.id, e.name, e.entity_type))
.collect()
}
pub fn resolve_entity_id(
content: &str,
file_path: &str,
entity_name: &str,
registry: &ParserRegistry,
) -> Option<String> {
let entities = extract_entity_ids(content, file_path, registry);
entities
.into_iter()
.find(|(_, name, _)| name == entity_name)
.map(|(id, _, _)| id)
}
pub fn merge_file_entities(
state: &mut EntityStateDoc,
file_path: &str,
_registry: &ParserRegistry,
) -> Result<CrdtMergeResult> {
let entity_order = read_file_entity_order(state, file_path);
if entity_order.is_empty() {
return Ok(CrdtMergeResult {
file_path: file_path.to_string(),
entities_auto_merged: 0,
entities_conflicted: 0,
merged_content: None,
});
}
let mut auto_merged = 0;
let mut conflicted = 0;
for entity_id in &entity_order {
let content_status = match get_entity_content(state, entity_id) {
Ok(s) => s,
Err(_) => continue,
};
if content_status.merge_state == "conflict" {
conflicted += 1;
continue;
}
let vv = &content_status.version_vector;
let active_agents: Vec<&String> = vv
.counters()
.iter()
.filter(|(_, &c)| c > 0)
.map(|(a, _)| a)
.collect();
if active_agents.len() <= 1 {
auto_merged += 1;
continue;
}
let base = content_status.base_content.clone();
let current = content_status.content.clone();
if !base.is_empty() && base != current {
auto_merged += 1;
} else {
auto_merged += 1;
}
}
let merged = reconstruct_file_from_crdt(state, file_path)?;
Ok(CrdtMergeResult {
file_path: file_path.to_string(),
entities_auto_merged: auto_merged,
entities_conflicted: conflicted,
merged_content: Some(merged),
})
}
pub fn reconstruct_file_from_crdt(
state: &EntityStateDoc,
file_path: &str,
) -> Result<String> {
let entity_order = read_file_entity_order(state, file_path);
let interstitials = read_file_interstitials(state, file_path);
let mut output = String::new();
let header_key = format!("{}::file_header", file_path);
if let Some(header) = interstitials.get(&header_key) {
output.push_str(header);
}
for (i, entity_id) in entity_order.iter().enumerate() {
if i > 0 {
let prev_id = &entity_order[i - 1];
let between_key = format!("{}::between:{}:{}", file_path, prev_id, entity_id);
if let Some(between) = interstitials.get(&between_key) {
output.push_str(between);
}
}
match get_entity_content(state, entity_id) {
Ok(status) => {
if status.merge_state == "conflict" {
output.push_str(&format!("<<<<<<< {}\n",
status.conflict_ours_agent.as_deref().unwrap_or("ours")));
if let Some(ref ours) = status.conflict_ours {
output.push_str(ours);
if !ours.ends_with('\n') {
output.push('\n');
}
}
output.push_str("=======\n");
if let Some(ref theirs) = status.conflict_theirs {
output.push_str(theirs);
if !theirs.ends_with('\n') {
output.push('\n');
}
}
output.push_str(&format!(">>>>>>> {}\n",
status.conflict_theirs_agent.as_deref().unwrap_or("theirs")));
} else {
output.push_str(&status.content);
if !status.content.is_empty() && !status.content.ends_with('\n') {
output.push('\n');
}
}
}
Err(_) => continue,
}
}
let footer_key = format!("{}::file_footer", file_path);
if let Some(footer) = interstitials.get(&footer_key) {
output.push_str(footer);
}
Ok(output)
}
fn read_file_entity_order(state: &EntityStateDoc, file_path: &str) -> Vec<String> {
let order_map = match state.file_entity_order_id() {
Ok(id) => id,
Err(_) => return Vec::new(),
};
let list_id = match state.doc.get(&order_map, file_path) {
Ok(Some((_, id))) => id,
_ => return Vec::new(),
};
let len = state.doc.length(&list_id);
let mut order = Vec::with_capacity(len);
for i in 0..len {
if let Ok(Some((Value::Scalar(v), _))) = state.doc.get(&list_id, i) {
if let automerge::ScalarValue::Str(s) = v.as_ref() {
order.push(s.to_string());
}
}
}
order
}
fn read_file_interstitials(state: &EntityStateDoc, file_path: &str) -> HashMap<String, String> {
let inter_map = match state.file_interstitials_id() {
Ok(id) => id,
Err(_) => return HashMap::new(),
};
let prefix = format!("{}::", file_path);
let mut result = HashMap::new();
for key in state.doc.keys(&inter_map) {
if key.starts_with(&prefix) {
if let Some(val) = get_str(&state.doc, &inter_map, &key) {
result.insert(key, val);
}
}
}
result
}