1use std::collections::HashMap;
19use std::ffi::{CStr, CString};
20use std::os::raw::c_char;
21use std::ptr;
22
23use crate::{component, crdt, frontmatter, syntax, template};
24
25#[unsafe(no_mangle)]
31pub unsafe extern "C" fn agent_doc_free_string(ptr: *mut c_char) {
32 if !ptr.is_null() {
33 drop(unsafe { CString::from_raw(ptr) });
34 }
35}
36
37#[unsafe(no_mangle)]
45pub unsafe extern "C" fn agent_doc_free_state(ptr: *mut u8, len: usize) {
46 if !ptr.is_null() {
47 drop(unsafe { Vec::from_raw_parts(ptr, len, len) });
48 }
49}
50
51#[repr(C)]
53pub struct FfiComponentList {
54 pub json: *mut c_char,
56 pub count: usize,
58}
59
60#[repr(C)]
64pub struct FfiPatchResult {
65 pub text: *mut c_char,
67 pub error: *mut c_char,
69}
70
71pub fn ffi_patch_ok(text: String) -> FfiPatchResult {
73 FfiPatchResult {
74 text: CString::new(text).unwrap_or_default().into_raw(),
75 error: ptr::null_mut(),
76 }
77}
78
79pub fn ffi_patch_err(msg: &str) -> FfiPatchResult {
81 FfiPatchResult {
82 text: ptr::null_mut(),
83 error: CString::new(msg).unwrap_or_default().into_raw(),
84 }
85}
86
87pub fn ffi_patch_from_result(result: anyhow::Result<String>) -> FfiPatchResult {
89 match result {
90 Ok(text) => ffi_patch_ok(text),
91 Err(e) => ffi_patch_err(&format!("{e:#}")),
92 }
93}
94
95pub fn normalize_editor_visible_result(text: String) -> anyhow::Result<String> {
97 template::normalize_editor_visible_template_structure(&text)
98}
99
100fn utf8_offsets_to_utf16_offsets(doc: &str, offsets: &[usize]) -> HashMap<usize, usize> {
106 let mut targets = offsets.to_vec();
107 targets.sort_unstable();
108 targets.dedup();
109
110 let mut mapped = HashMap::with_capacity(targets.len());
111 let mut target_idx = 0usize;
112 let mut utf8_offset = 0usize;
113 let mut utf16_offset = 0usize;
114
115 while target_idx < targets.len() && targets[target_idx] == 0 {
116 mapped.insert(0, 0);
117 target_idx += 1;
118 }
119
120 for ch in doc.chars() {
121 let next_utf8 = utf8_offset + ch.len_utf8();
122 while target_idx < targets.len() && targets[target_idx] < next_utf8 {
123 mapped.insert(targets[target_idx], utf16_offset);
124 target_idx += 1;
125 }
126
127 utf8_offset = next_utf8;
128 utf16_offset += ch.len_utf16();
129
130 while target_idx < targets.len() && targets[target_idx] == utf8_offset {
131 mapped.insert(targets[target_idx], utf16_offset);
132 target_idx += 1;
133 }
134 }
135
136 while target_idx < targets.len() {
137 mapped.insert(targets[target_idx], utf16_offset);
138 target_idx += 1;
139 }
140
141 mapped
142}
143
144#[unsafe(no_mangle)]
154pub unsafe extern "C" fn agent_doc_parse_components(doc: *const c_char) -> FfiComponentList {
155 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
156 Ok(s) => s,
157 Err(_) => {
158 return FfiComponentList {
159 json: ptr::null_mut(),
160 count: 0,
161 };
162 }
163 };
164
165 let components = match component::parse(doc_str) {
166 Ok(c) => c,
167 Err(_) => {
168 return FfiComponentList {
169 json: ptr::null_mut(),
170 count: 0,
171 };
172 }
173 };
174
175 let count = components.len();
176
177 let json_items: Vec<serde_json::Value> = components
179 .iter()
180 .map(|c| {
181 serde_json::json!({
182 "name": c.name,
183 "attrs": c.attrs,
184 "open_start": c.open_start,
185 "open_end": c.open_end,
186 "close_start": c.close_start,
187 "close_end": c.close_end,
188 "content": c.content(doc_str),
189 })
190 })
191 .collect();
192
193 let json_str = serde_json::to_string(&json_items).unwrap_or_default();
194 let c_json = CString::new(json_str).unwrap_or_default();
195
196 FfiComponentList {
197 json: c_json.into_raw(),
198 count,
199 }
200}
201
202#[unsafe(no_mangle)]
212pub unsafe extern "C" fn agent_doc_visual_tokens_json(doc: *const c_char) -> *mut c_char {
213 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
214 Ok(s) => s,
215 Err(_) => return ptr::null_mut(),
216 };
217 let tokens = syntax::collect_visual_tokens(doc_str);
218 let offsets: Vec<usize> = tokens
219 .iter()
220 .flat_map(|token| [token.start, token.end])
221 .collect();
222 let utf16_offsets = utf8_offsets_to_utf16_offsets(doc_str, &offsets);
223 let editor_tokens: Vec<_> = tokens
224 .iter()
225 .map(|token| {
226 serde_json::json!({
227 "kind": token.kind,
228 "start": utf16_offsets[&token.start],
229 "end": utf16_offsets[&token.end],
230 })
231 })
232 .collect();
233 let json = match serde_json::to_string(&editor_tokens) {
234 Ok(json) => json,
235 Err(_) => return ptr::null_mut(),
236 };
237 match CString::new(json) {
238 Ok(value) => value.into_raw(),
239 Err(_) => ptr::null_mut(),
240 }
241}
242
243#[unsafe(no_mangle)]
252pub unsafe extern "C" fn agent_doc_merge_frontmatter(
253 doc: *const c_char,
254 yaml_fields: *const c_char,
255) -> FfiPatchResult {
256 let make_err = |msg: &str| FfiPatchResult {
257 text: ptr::null_mut(),
258 error: CString::new(msg).unwrap_or_default().into_raw(),
259 };
260
261 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
262 Ok(s) => s,
263 Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
264 };
265 let fields_str = match unsafe { CStr::from_ptr(yaml_fields) }.to_str() {
266 Ok(s) => s,
267 Err(e) => return make_err(&format!("invalid yaml_fields UTF-8: {e}")),
268 };
269
270 match frontmatter::merge_fields(doc_str, fields_str) {
271 Ok(result) => FfiPatchResult {
272 text: CString::new(result).unwrap_or_default().into_raw(),
273 error: ptr::null_mut(),
274 },
275 Err(e) => make_err(&format!("{e}")),
276 }
277}
278
279#[unsafe(no_mangle)]
289pub unsafe extern "C" fn agent_doc_normalize_template_structure(
290 doc: *const c_char,
291) -> FfiPatchResult {
292 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
293 Ok(s) => s,
294 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
295 };
296
297 ffi_patch_from_result(template::normalize_editor_visible_template_structure(
298 doc_str,
299 ))
300}
301
302#[repr(C)]
304pub struct FfiMergeResult {
305 pub text: *mut c_char,
307 pub state: *mut u8,
309 pub state_len: usize,
311 pub error: *mut c_char,
313}
314
315#[unsafe(no_mangle)]
326pub unsafe extern "C" fn agent_doc_crdt_merge(
327 base_state: *const u8,
328 base_state_len: usize,
329 ours: *const c_char,
330 theirs: *const c_char,
331) -> FfiMergeResult {
332 let make_err = |msg: &str| FfiMergeResult {
333 text: ptr::null_mut(),
334 state: ptr::null_mut(),
335 state_len: 0,
336 error: CString::new(msg).unwrap_or_default().into_raw(),
337 };
338
339 let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
340 Ok(s) => s,
341 Err(e) => return make_err(&format!("invalid ours UTF-8: {e}")),
342 };
343 let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
344 Ok(s) => s,
345 Err(e) => return make_err(&format!("invalid theirs UTF-8: {e}")),
346 };
347
348 let base = if base_state.is_null() {
349 None
350 } else {
351 Some(unsafe { std::slice::from_raw_parts(base_state, base_state_len) })
352 };
353
354 match crdt::merge(base, ours_str, theirs_str) {
355 Ok(merged_text) => {
356 let doc = crdt::CrdtDoc::from_text(&merged_text);
358 let state_bytes = doc.encode_state();
359 let state_len = state_bytes.len();
360 let state_ptr = {
361 let mut boxed = state_bytes.into_boxed_slice();
362 let ptr = boxed.as_mut_ptr();
363 std::mem::forget(boxed);
364 ptr
365 };
366
367 FfiMergeResult {
368 text: CString::new(merged_text).unwrap_or_default().into_raw(),
369 state: state_ptr,
370 state_len,
371 error: ptr::null_mut(),
372 }
373 }
374 Err(e) => make_err(&format!("{e}")),
375 }
376}
377
378#[unsafe(no_mangle)]
390pub unsafe extern "C" fn agent_doc_merge_crdt(
391 base: *const c_char,
392 ours: *const c_char,
393 theirs: *const c_char,
394) -> *mut c_char {
395 let base_str = match unsafe { CStr::from_ptr(base) }.to_str() {
396 Ok(s) => s,
397 Err(_) => return CString::new("").unwrap_or_default().into_raw(),
398 };
399 let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
400 Ok(s) => s,
401 Err(_) => return CString::new("").unwrap_or_default().into_raw(),
402 };
403 let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
404 Ok(s) => s,
405 Err(_) => return CString::new("").unwrap_or_default().into_raw(),
406 };
407
408 let base_doc = crdt::CrdtDoc::from_text(base_str);
410 let base_state = base_doc.encode_state();
411
412 let merged = crdt::merge(Some(&base_state), ours_str, theirs_str)
413 .unwrap_or_else(|_| ours_str.to_string());
414 CString::new(merged).unwrap_or_default().into_raw()
415}
416
417#[unsafe(no_mangle)]
428pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end(
429 doc: *const c_char,
430) -> FfiPatchResult {
431 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
432 Ok(s) => s,
433 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
434 };
435
436 let result = template::reposition_boundary_to_end_clean(doc_str);
437 ffi_patch_from_result(normalize_editor_visible_result(result))
438}
439
440#[unsafe(no_mangle)]
450pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_with_id(
451 doc: *const c_char,
452 boundary_id: *const c_char,
453) -> FfiPatchResult {
454 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
455 Ok(s) => s,
456 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
457 };
458 let boundary_id_str = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
459 Ok(s) => s,
460 Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
461 };
462
463 let result = template::reposition_boundary_to_end_clean_with_id(doc_str, Some(boundary_id_str));
464 ffi_patch_from_result(normalize_editor_visible_result(result))
465}
466
467#[unsafe(no_mangle)]
477pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_preserve_head(
478 doc: *const c_char,
479) -> FfiPatchResult {
480 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
481 Ok(s) => s,
482 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
483 };
484
485 let result = template::reposition_boundary_to_end_preserve_head(doc_str);
486 ffi_patch_from_result(normalize_editor_visible_result(result))
487}
488
489#[unsafe(no_mangle)]
495pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_preserve_head_with_id(
496 doc: *const c_char,
497 boundary_id: *const c_char,
498) -> FfiPatchResult {
499 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
500 Ok(s) => s,
501 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
502 };
503 let boundary_id_str = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
504 Ok(s) => s,
505 Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
506 };
507
508 let result =
509 template::reposition_boundary_to_end_preserve_head_with_id(doc_str, Some(boundary_id_str));
510 ffi_patch_from_result(normalize_editor_visible_result(result))
511}
512
513#[unsafe(no_mangle)]
521pub unsafe extern "C" fn agent_doc_apply_patch(
522 doc: *const c_char,
523 component_name: *const c_char,
524 content: *const c_char,
525 mode: *const c_char,
526) -> FfiPatchResult {
527 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
528 Ok(s) => s,
529 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
530 };
531 let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
532 Ok(s) => s,
533 Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
534 };
535 let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
536 Ok(s) => s,
537 Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
538 };
539 let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
540 Ok(s) => s,
541 Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
542 };
543
544 let patch = template::PatchBlock::new(name, patch_content);
546
547 let mut overrides = HashMap::new();
549 overrides.insert(name.to_string(), mode_str.to_string());
550
551 ffi_patch_from_result(
554 template::apply_patches_with_overrides_pure(
555 doc_str,
556 &[patch],
557 "",
558 None,
559 &HashMap::new(),
560 &HashMap::new(),
561 &overrides,
562 )
563 .and_then(normalize_editor_visible_result),
564 )
565}
566
567#[unsafe(no_mangle)]
579pub unsafe extern "C" fn agent_doc_apply_patch_with_caret(
580 doc: *const c_char,
581 component_name: *const c_char,
582 content: *const c_char,
583 mode: *const c_char,
584 caret_offset: i32,
585) -> FfiPatchResult {
586 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
587 Ok(s) => s,
588 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
589 };
590 let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
591 Ok(s) => s,
592 Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
593 };
594 let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
595 Ok(s) => s,
596 Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
597 };
598 let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
599 Ok(s) => s,
600 Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
601 };
602
603 if mode_str == "append" && caret_offset >= 0 {
605 let components = match component::parse(doc_str) {
606 Ok(c) => c,
607 Err(e) => return ffi_patch_err(&format!("{e}")),
608 };
609 if let Some(comp) = components.iter().find(|c| c.name == name) {
610 let result =
611 comp.append_with_caret(doc_str, patch_content, Some(caret_offset as usize));
612 return ffi_patch_from_result(normalize_editor_visible_result(result));
613 }
614 }
615
616 let patch = template::PatchBlock::new(name, patch_content);
618 let mut overrides = HashMap::new();
619 overrides.insert(name.to_string(), mode_str.to_string());
620 ffi_patch_from_result(
621 template::apply_patches_with_overrides_pure(
622 doc_str,
623 &[patch],
624 "",
625 None,
626 &HashMap::new(),
627 &HashMap::new(),
628 &overrides,
629 )
630 .and_then(normalize_editor_visible_result),
631 )
632}
633
634#[unsafe(no_mangle)]
647pub unsafe extern "C" fn agent_doc_apply_patch_with_boundary(
648 doc: *const c_char,
649 component_name: *const c_char,
650 content: *const c_char,
651 mode: *const c_char,
652 boundary_id: *const c_char,
653) -> FfiPatchResult {
654 let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
655 Ok(s) => s,
656 Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
657 };
658 let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
659 Ok(s) => s,
660 Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
661 };
662 let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
663 Ok(s) => s,
664 Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
665 };
666 let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
667 Ok(s) => s,
668 Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
669 };
670 let bid = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
671 Ok(s) => s,
672 Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
673 };
674
675 if mode_str == "append" && !bid.is_empty() {
677 let components = match component::parse(doc_str) {
678 Ok(c) => c,
679 Err(e) => return ffi_patch_err(&format!("{e}")),
680 };
681 if let Some(comp) = components.iter().find(|c| c.name == name) {
682 let result = comp.append_with_boundary(doc_str, patch_content, bid);
683 let result = if name == "exchange" {
684 template::annotate_exchange_headings_against_baseline(&result, doc_str)
685 } else {
686 result
687 };
688 return ffi_patch_from_result(normalize_editor_visible_result(result));
689 }
690 }
691
692 let patch = template::PatchBlock::new(name, patch_content);
694 let mut overrides = HashMap::new();
695 overrides.insert(name.to_string(), mode_str.to_string());
696 ffi_patch_from_result(
697 template::apply_patches_with_overrides_pure(
698 doc_str,
699 &[patch],
700 "",
701 None,
702 &HashMap::new(),
703 &HashMap::new(),
704 &overrides,
705 )
706 .and_then(normalize_editor_visible_result),
707 )
708}