//! Resolve optimistic native UTF-16 display positions against their own source //! generation, never against an authority which may already include peer edits. use crate::{operations, projection, structure}; use serde_json::{json, Value}; use yrs::{Doc, Transact}; struct Span { node: String, start: u32, length: u32, atomic: bool } fn spans(value: &Value, offset: &mut u32, output: &mut Vec) { let tag = value["type"].as_str().unwrap_or(""); if tag == "text" { let length = value["runs"].as_array().map(|runs| runs.iter().map(|r| r["insert"].as_str().unwrap_or("").encode_utf16().count() as u32).sum()).unwrap_or(0); output.push(Span { node: value["id"].as_str().unwrap_or("").to_owned(), start: *offset, length, atomic: false }); *offset += length; return; } if ["hard_break", "image", "horizontal_rule"].contains(&tag) { output.push(Span { node: value["id"].as_str().unwrap_or("").to_owned(), start: *offset, length: 1, atomic: true }); *offset += if tag == "horizontal_rule" { 2 } else { 1 }; return; } let start = *offset; if let Some(children) = value["children"].as_array() { for child in children { spans(child, offset, output); } } if ["paragraph", "heading", "code_block"].contains(&tag) { if *offset == start { output.push(Span { node: value["id"].as_str().unwrap_or("").to_owned(), start, length: 0, atomic: false }); } *offset += 1; } } pub(crate) fn locate(doc: &Doc, at: u32, length: u32) -> Result<(String,u32), &'static str> { let root = doc.get_or_insert_xml_fragment("prosemirror"); let mut offset = 0; let mut ranges = Vec::new(); for child in projection::children(&root, &doc.transact()) { spans(&child, &mut offset, &mut ranges); } let end = at.checked_add(length).ok_or("invalid_range")?; ranges.iter().find(|r| !r.atomic && at >= r.start && end <= r.start + r.length) .or_else(|| ranges.iter().find(|r| r.atomic && at >= r.start && end <= r.start + r.length)) .map(|r| (r.node.clone(), at-r.start)).ok_or("unsupported_range") } fn endpoint(doc: &Doc, at: u32) -> Result<(String,u32), &'static str> { use yrs::{XmlFragment, XmlTextPrelim}; let (id, local) = locate(doc,at,0)?; let root = doc.get_or_insert_xml_fragment("prosemirror"); let mut txn = doc.transact_mut_with("native-edit"); let inline_atom = projection::find(&root,&txn,&id).and_then(yrs::XmlOut::into_xml_element) .is_some_and(|e| ["image","hard_break"].contains(&e.tag().as_ref())); if inline_atom { let (parent,index) = structure::parent(&root,&txn,&id).ok_or("missing_parent")?; let text = parent.insert(&mut txn,index + if local == 1 { 1 } else { 0 },XmlTextPrelim::new("")); return Ok((projection::node_id(&yrs::XmlOut::Text(text)),0)); } Ok((id,local)) } pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { let at = request["at"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?; let mut length = request["length"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?; let root = doc.get_or_insert_xml_fragment("prosemirror"); let mut display_length = 0; let mut ranges = Vec::new(); for child in projection::children(&root, &doc.transact()) { spans(&child, &mut display_length, &mut ranges); } let end = at.checked_add(length).ok_or("invalid_range")?; if end > display_length { return Err("invalid_range"); } // The final native paragraph terminator is presentation, not user content. if length > 0 && end == display_length { length -= 1; } let text = request["text"].as_str().ok_or("invalid_command")?; let (node, local) = match locate(doc, at, length) { Ok(value) => value, Err(_) if length > 0 => { let (start, local) = endpoint(doc, at)?; let (end, end_at) = endpoint(doc, at.checked_add(length).ok_or("invalid_range")?)?; structure::execute(doc, &json!({"op":"join_range","node":start,"at":local,"end_node":end,"end_at":end_at}))?; return execute(doc, &json!({"op":"display_edit","at":at,"length":0,"text":text,"marks":request["marks"]})); }, Err(error) => return Err(error), }; let atom = projection::find(&root,&doc.transact(),&node).and_then(yrs::XmlOut::into_xml_element) .filter(|element| ["image","hard_break","horizontal_rule"].contains(&element.tag().as_ref())); if let Some(atom) = atom { use yrs::{XmlFragment, XmlElementPrelim, XmlTextPrelim}; if local > 1 || length > 1 || (length == 1 && local != 0) { return Err("invalid_range"); } let mut txn = doc.transact_mut_with("native-edit"); let (parent,index) = structure::parent(&root,&txn,&node).ok_or("missing_parent")?; if atom.tag().as_ref() == "horizontal_rule" { if length != 1 { return Err("unsupported_range"); } parent.remove(&mut txn,index); parent.insert(&mut txn,index,XmlElementPrelim::new("paragraph",[XmlTextPrelim::new("").into()])); } else { if length == 1 { parent.remove(&mut txn,index); } parent.insert(&mut txn,index + if local == 1 { 1 } else { 0 },XmlTextPrelim::new("")); } drop(txn); return execute(doc,&json!({"op":"display_edit","at":at,"length":0,"text":text,"marks":request["marks"]})); } let is_code = { let txn = doc.transact(); let block = match projection::find(&root,&txn,&node) { Some(yrs::XmlOut::Element(element)) => Some(element), Some(yrs::XmlOut::Text(_)) => structure::parent(&root,&txn,&node).and_then(|(parent,_)| projection::find(&root,&txn,&projection::node_id(&yrs::XmlOut::Fragment(parent))).and_then(yrs::XmlOut::into_xml_element)), _ => None, }; block.is_some_and(|block| block.tag().as_ref() == "code_block") }; if is_code { return operations::execute(doc, &json!({"op":"replace_text","node":node,"at":local,"length":length,"text":text.replace('\u{2028}',"\n"),"marks":request["marks"]})); } let mut parts = text.split(['\n', '\u{2028}']); let first = parts.next().unwrap_or(""); operations::execute(doc, &json!({"op":"replace_text","node":node,"at":local,"length":length,"text":first,"marks":request["marks"]}))?; let mut cursor = at + first.encode_utf16().count() as u32; for (separator, part) in text.chars().filter(|c| *c == '\n' || *c == '\u{2028}').zip(parts) { let (node, local) = locate(doc, cursor, 0)?; let op = if separator == '\n' { "split" } else { "hard_break" }; structure::execute(doc, &json!({"op":op,"node":node,"at":local}))?; cursor += 1; let (node, local) = locate(doc, cursor, 0)?; operations::execute(doc, &json!({"op":"replace_text","node":node,"at":local,"length":0,"text":part,"marks":request["marks"]}))?; cursor += part.encode_utf16().count() as u32; } Ok(()) } pub fn anchor(doc: &Doc, at: u32) -> Result { use yrs::{Assoc, IndexedSequence, Text, XmlOut}; let Ok((id, local)) = locate(doc, at, 0) else { return Ok(json!({"fallback":at})); }; let root = doc.get_or_insert_xml_fragment("prosemirror"); let mut txn = doc.transact_mut_with("selection"); match projection::find(&root, &txn, &id).ok_or("missing_node")? { XmlOut::Text(text) => { let assoc = if local == text.len(&txn) { Assoc::Before } else { Assoc::After }; let sticky = text.sticky_index(&mut txn, local, assoc).unwrap_or_else(|| yrs::StickyIndex::from_type(&txn,&text,Assoc::After)); Ok(json!({"node":id,"sticky":sticky,"fallback":at})) }, _ => Ok(json!({"node":id,"offset":local,"fallback":at})), } } pub fn resolve(doc: &Doc, anchor: &Value) -> Result { let root = doc.get_or_insert_xml_fragment("prosemirror"); let txn = doc.transact(); let mut offset = 0; let mut ranges = Vec::new(); for child in projection::children(&root, &txn) { spans(&child, &mut offset, &mut ranges); } let local = if anchor["sticky"].is_null() { anchor["offset"].as_u64().unwrap_or(0) as u32 } else { let sticky: yrs::StickyIndex = serde_json::from_value(anchor["sticky"].clone()).map_err(|_| "invalid_anchor")?; sticky.get_offset(&txn).map(|value| value.index).unwrap_or(0) }; if let Some(range) = ranges.iter().find(|r| Some(r.node.as_str()) == anchor["node"].as_str()) { return Ok(range.start + local.min(range.length)); } Ok((anchor["fallback"].as_u64().unwrap_or(0) as u32).min(offset.saturating_sub(1))) }