use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use crate::{component, crdt, frontmatter, syntax, template};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
drop(unsafe { CString::from_raw(ptr) });
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_free_state(ptr: *mut u8, len: usize) {
if !ptr.is_null() {
drop(unsafe { Vec::from_raw_parts(ptr, len, len) });
}
}
#[repr(C)]
pub struct FfiComponentList {
pub json: *mut c_char,
pub count: usize,
}
#[repr(C)]
pub struct FfiPatchResult {
pub text: *mut c_char,
pub error: *mut c_char,
}
pub fn ffi_patch_ok(text: String) -> FfiPatchResult {
FfiPatchResult {
text: CString::new(text).unwrap_or_default().into_raw(),
error: ptr::null_mut(),
}
}
pub fn ffi_patch_err(msg: &str) -> FfiPatchResult {
FfiPatchResult {
text: ptr::null_mut(),
error: CString::new(msg).unwrap_or_default().into_raw(),
}
}
pub fn ffi_patch_from_result(result: anyhow::Result<String>) -> FfiPatchResult {
match result {
Ok(text) => ffi_patch_ok(text),
Err(e) => ffi_patch_err(&format!("{e:#}")),
}
}
pub fn normalize_editor_visible_result(text: String) -> anyhow::Result<String> {
template::normalize_editor_visible_template_structure(&text)
}
fn utf8_offsets_to_utf16_offsets(doc: &str, offsets: &[usize]) -> HashMap<usize, usize> {
let mut targets = offsets.to_vec();
targets.sort_unstable();
targets.dedup();
let mut mapped = HashMap::with_capacity(targets.len());
let mut target_idx = 0usize;
let mut utf8_offset = 0usize;
let mut utf16_offset = 0usize;
while target_idx < targets.len() && targets[target_idx] == 0 {
mapped.insert(0, 0);
target_idx += 1;
}
for ch in doc.chars() {
let next_utf8 = utf8_offset + ch.len_utf8();
while target_idx < targets.len() && targets[target_idx] < next_utf8 {
mapped.insert(targets[target_idx], utf16_offset);
target_idx += 1;
}
utf8_offset = next_utf8;
utf16_offset += ch.len_utf16();
while target_idx < targets.len() && targets[target_idx] == utf8_offset {
mapped.insert(targets[target_idx], utf16_offset);
target_idx += 1;
}
}
while target_idx < targets.len() {
mapped.insert(targets[target_idx], utf16_offset);
target_idx += 1;
}
mapped
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_parse_components(doc: *const c_char) -> FfiComponentList {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(_) => {
return FfiComponentList {
json: ptr::null_mut(),
count: 0,
};
}
};
let components = match component::parse(doc_str) {
Ok(c) => c,
Err(_) => {
return FfiComponentList {
json: ptr::null_mut(),
count: 0,
};
}
};
let count = components.len();
let json_items: Vec<serde_json::Value> = components
.iter()
.map(|c| {
serde_json::json!({
"name": c.name,
"attrs": c.attrs,
"open_start": c.open_start,
"open_end": c.open_end,
"close_start": c.close_start,
"close_end": c.close_end,
"content": c.content(doc_str),
})
})
.collect();
let json_str = serde_json::to_string(&json_items).unwrap_or_default();
let c_json = CString::new(json_str).unwrap_or_default();
FfiComponentList {
json: c_json.into_raw(),
count,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_visual_tokens_json(doc: *const c_char) -> *mut c_char {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let tokens = syntax::collect_visual_tokens(doc_str);
let offsets: Vec<usize> = tokens
.iter()
.flat_map(|token| [token.start, token.end])
.collect();
let utf16_offsets = utf8_offsets_to_utf16_offsets(doc_str, &offsets);
let editor_tokens: Vec<_> = tokens
.iter()
.map(|token| {
serde_json::json!({
"kind": token.kind,
"start": utf16_offsets[&token.start],
"end": utf16_offsets[&token.end],
})
})
.collect();
let json = match serde_json::to_string(&editor_tokens) {
Ok(json) => json,
Err(_) => return ptr::null_mut(),
};
match CString::new(json) {
Ok(value) => value.into_raw(),
Err(_) => ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_merge_frontmatter(
doc: *const c_char,
yaml_fields: *const c_char,
) -> FfiPatchResult {
let make_err = |msg: &str| FfiPatchResult {
text: ptr::null_mut(),
error: CString::new(msg).unwrap_or_default().into_raw(),
};
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
};
let fields_str = match unsafe { CStr::from_ptr(yaml_fields) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid yaml_fields UTF-8: {e}")),
};
match frontmatter::merge_fields(doc_str, fields_str) {
Ok(result) => FfiPatchResult {
text: CString::new(result).unwrap_or_default().into_raw(),
error: ptr::null_mut(),
},
Err(e) => make_err(&format!("{e}")),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_normalize_template_structure(
doc: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
ffi_patch_from_result(template::normalize_editor_visible_template_structure(
doc_str,
))
}
#[repr(C)]
pub struct FfiMergeResult {
pub text: *mut c_char,
pub state: *mut u8,
pub state_len: usize,
pub error: *mut c_char,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_crdt_merge(
base_state: *const u8,
base_state_len: usize,
ours: *const c_char,
theirs: *const c_char,
) -> FfiMergeResult {
let make_err = |msg: &str| FfiMergeResult {
text: ptr::null_mut(),
state: ptr::null_mut(),
state_len: 0,
error: CString::new(msg).unwrap_or_default().into_raw(),
};
let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid ours UTF-8: {e}")),
};
let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid theirs UTF-8: {e}")),
};
let base = if base_state.is_null() {
None
} else {
Some(unsafe { std::slice::from_raw_parts(base_state, base_state_len) })
};
match crdt::merge(base, ours_str, theirs_str) {
Ok(merged_text) => {
let doc = crdt::CrdtDoc::from_text(&merged_text);
let state_bytes = doc.encode_state();
let state_len = state_bytes.len();
let state_ptr = {
let mut boxed = state_bytes.into_boxed_slice();
let ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
ptr
};
FfiMergeResult {
text: CString::new(merged_text).unwrap_or_default().into_raw(),
state: state_ptr,
state_len,
error: ptr::null_mut(),
}
}
Err(e) => make_err(&format!("{e}")),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_merge_crdt(
base: *const c_char,
ours: *const c_char,
theirs: *const c_char,
) -> *mut c_char {
let base_str = match unsafe { CStr::from_ptr(base) }.to_str() {
Ok(s) => s,
Err(_) => return CString::new("").unwrap_or_default().into_raw(),
};
let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
Ok(s) => s,
Err(_) => return CString::new("").unwrap_or_default().into_raw(),
};
let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
Ok(s) => s,
Err(_) => return CString::new("").unwrap_or_default().into_raw(),
};
let base_doc = crdt::CrdtDoc::from_text(base_str);
let base_state = base_doc.encode_state();
let merged = crdt::merge(Some(&base_state), ours_str, theirs_str)
.unwrap_or_else(|_| ours_str.to_string());
CString::new(merged).unwrap_or_default().into_raw()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end(
doc: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let result = template::reposition_boundary_to_end_clean(doc_str);
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_with_id(
doc: *const c_char,
boundary_id: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let boundary_id_str = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
};
let result = template::reposition_boundary_to_end_clean_with_id(doc_str, Some(boundary_id_str));
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_preserve_head(
doc: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let result = template::reposition_boundary_to_end_preserve_head(doc_str);
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_preserve_head_with_id(
doc: *const c_char,
boundary_id: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let boundary_id_str = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
};
let result =
template::reposition_boundary_to_end_preserve_head_with_id(doc_str, Some(boundary_id_str));
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_apply_patch(
doc: *const c_char,
component_name: *const c_char,
content: *const c_char,
mode: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
};
let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
};
let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
};
let patch = template::PatchBlock::new(name, patch_content);
let mut overrides = HashMap::new();
overrides.insert(name.to_string(), mode_str.to_string());
ffi_patch_from_result(
template::apply_patches_with_overrides_pure(
doc_str,
&[patch],
"",
None,
&HashMap::new(),
&HashMap::new(),
&overrides,
)
.and_then(normalize_editor_visible_result),
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_apply_patch_with_caret(
doc: *const c_char,
component_name: *const c_char,
content: *const c_char,
mode: *const c_char,
caret_offset: i32,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
};
let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
};
let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
};
if mode_str == "append" && caret_offset >= 0 {
let components = match component::parse(doc_str) {
Ok(c) => c,
Err(e) => return ffi_patch_err(&format!("{e}")),
};
if let Some(comp) = components.iter().find(|c| c.name == name) {
let result =
comp.append_with_caret(doc_str, patch_content, Some(caret_offset as usize));
return ffi_patch_from_result(normalize_editor_visible_result(result));
}
}
let patch = template::PatchBlock::new(name, patch_content);
let mut overrides = HashMap::new();
overrides.insert(name.to_string(), mode_str.to_string());
ffi_patch_from_result(
template::apply_patches_with_overrides_pure(
doc_str,
&[patch],
"",
None,
&HashMap::new(),
&HashMap::new(),
&overrides,
)
.and_then(normalize_editor_visible_result),
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_apply_patch_with_boundary(
doc: *const c_char,
component_name: *const c_char,
content: *const c_char,
mode: *const c_char,
boundary_id: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
};
let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
};
let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
};
let bid = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
};
if mode_str == "append" && !bid.is_empty() {
let components = match component::parse(doc_str) {
Ok(c) => c,
Err(e) => return ffi_patch_err(&format!("{e}")),
};
if let Some(comp) = components.iter().find(|c| c.name == name) {
let result = comp.append_with_boundary(doc_str, patch_content, bid);
let result = if name == "exchange" {
template::annotate_exchange_headings_against_baseline(&result, doc_str)
} else {
result
};
return ffi_patch_from_result(normalize_editor_visible_result(result));
}
}
let patch = template::PatchBlock::new(name, patch_content);
let mut overrides = HashMap::new();
overrides.insert(name.to_string(), mode_str.to_string());
ffi_patch_from_result(
template::apply_patches_with_overrides_pure(
doc_str,
&[patch],
"",
None,
&HashMap::new(),
&HashMap::new(),
&overrides,
)
.and_then(normalize_editor_visible_result),
)
}