at main 738 lines 29 kB view raw
1//! DOM synchronization for the markdown editor. 2//! 3//! Handles syncing cursor/selection state between the browser DOM and the 4//! editor document model, and updating paragraph DOM elements. 5 6use wasm_bindgen::JsCast; 7use weaver_editor_core::{ 8 CursorSync, OffsetMapping, ParagraphRender, SnapDirection, find_nearest_valid_position, 9 is_valid_cursor_position, 10}; 11 12use weaver_editor_core::{EditorDocument, Selection, SyntaxSpanInfo}; 13 14use crate::cursor::restore_cursor_position; 15use crate::update_syntax_visibility; 16 17/// Result of syncing cursor from DOM. 18#[derive(Debug, Clone)] 19pub enum CursorSyncResult { 20 /// Cursor is collapsed at this offset. 21 Cursor(usize), 22 /// Selection from anchor to head. 23 Selection { anchor: usize, head: usize }, 24 /// Could not determine cursor position. 25 None, 26} 27 28/// Browser-based cursor sync implementation. 29/// 30/// Holds reference to editor element ID and provides methods to sync 31/// cursor state from DOM back to the editor model. 32pub struct BrowserCursorSync { 33 editor_id: String, 34} 35 36impl BrowserCursorSync { 37 /// Create a new browser cursor sync for the given editor element. 38 pub fn new(editor_id: impl Into<String>) -> Self { 39 Self { 40 editor_id: editor_id.into(), 41 } 42 } 43 44 /// Get the editor element ID. 45 pub fn editor_id(&self) -> &str { 46 &self.editor_id 47 } 48} 49 50impl CursorSync for BrowserCursorSync { 51 fn sync_cursor_from_platform<F, G>( 52 &self, 53 paragraphs: &[ParagraphRender], 54 direction_hint: Option<SnapDirection>, 55 on_cursor: F, 56 on_selection: G, 57 ) where 58 F: FnOnce(usize), 59 G: FnOnce(usize, usize), 60 { 61 if let Some(result) = sync_cursor_from_dom_impl(&self.editor_id, paragraphs, direction_hint) 62 { 63 match result { 64 CursorSyncResult::Cursor(offset) => on_cursor(offset), 65 CursorSyncResult::Selection { anchor, head } => { 66 if anchor == head { 67 on_cursor(anchor); 68 } else { 69 on_selection(anchor, head); 70 } 71 } 72 CursorSyncResult::None => {} 73 } 74 } 75 } 76} 77 78/// Sync cursor state from DOM selection, returning the result. 79/// 80/// This is the core implementation that reads the browser's selection state 81/// and converts it to character offsets using paragraph offset maps. 82pub fn sync_cursor_from_dom_impl( 83 editor_id: &str, 84 paragraphs: &[ParagraphRender], 85 direction_hint: Option<SnapDirection>, 86) -> Option<CursorSyncResult> { 87 if paragraphs.is_empty() { 88 return Some(CursorSyncResult::None); 89 } 90 91 let window = web_sys::window()?; 92 let dom_document = window.document()?; 93 let editor_element = dom_document.get_element_by_id(editor_id)?; 94 95 let selection = window.get_selection().ok()??; 96 97 let anchor_node = selection.anchor_node()?; 98 let focus_node = selection.focus_node()?; 99 let anchor_offset = selection.anchor_offset() as usize; 100 let focus_offset = selection.focus_offset() as usize; 101 102 tracing::trace!( 103 anchor_node_name = %anchor_node.node_name(), 104 anchor_offset, 105 focus_node_name = %focus_node.node_name(), 106 focus_offset, 107 "sync_cursor_from_dom_impl: browser selection state" 108 ); 109 110 let anchor_char = dom_position_to_text_offset( 111 &dom_document, 112 &editor_element, 113 &anchor_node, 114 anchor_offset, 115 paragraphs, 116 direction_hint, 117 ); 118 let focus_char = dom_position_to_text_offset( 119 &dom_document, 120 &editor_element, 121 &focus_node, 122 focus_offset, 123 paragraphs, 124 direction_hint, 125 ); 126 127 match (anchor_char, focus_char) { 128 (Some(anchor), Some(head)) => { 129 if anchor == head { 130 Some(CursorSyncResult::Cursor(head)) 131 } else { 132 Some(CursorSyncResult::Selection { anchor, head }) 133 } 134 } 135 _ => { 136 tracing::warn!("Could not map DOM selection to text offsets"); 137 Some(CursorSyncResult::None) 138 } 139 } 140} 141 142/// Convert a DOM position (node + offset) to a text char offset. 143/// 144/// Walks up from the node to find a container with a node ID, then uses 145/// the paragraph offset maps to convert the UTF-16 offset to a character offset. 146/// The `direction_hint` is used when snapping from invisible content to determine 147/// which direction to prefer. 148pub fn dom_position_to_text_offset( 149 dom_document: &web_sys::Document, 150 editor_element: &web_sys::Element, 151 node: &web_sys::Node, 152 offset_in_text_node: usize, 153 paragraphs: &[ParagraphRender], 154 direction_hint: Option<SnapDirection>, 155) -> Option<usize> { 156 // Find the containing element with a node ID (walk up from text node). 157 let mut current_node = node.clone(); 158 let mut walked_from: Option<web_sys::Node> = None; 159 160 let node_id = loop { 161 let node_name = current_node.node_name(); 162 let node_id_attr = current_node 163 .dyn_ref::<web_sys::Element>() 164 .and_then(|e| e.get_attribute("id")); 165 let text_content_preview = current_node 166 .text_content() 167 .map(|s| s.chars().take(20).collect::<String>()) 168 .unwrap_or_default(); 169 tracing::trace!( 170 node_name = %node_name, 171 node_id_attr = ?node_id_attr, 172 text_preview = %text_content_preview.escape_debug(), 173 "dom_position_to_text_offset: walk-up iteration" 174 ); 175 176 if let Some(element) = current_node.dyn_ref::<web_sys::Element>() { 177 if element == editor_element { 178 // Selection is on the editor container itself. 179 // IMPORTANT: If we WALKED UP to the editor from a descendant, 180 // offset_in_text_node is the offset within that descendant, NOT the 181 // child index in the editor. 182 if let Some(ref walked_node) = walked_from { 183 tracing::trace!( 184 walked_from_node_name = %walked_node.node_name(), 185 "dom_position_to_text_offset: walked up to editor from descendant" 186 ); 187 188 // Find paragraph containing this node by checking paragraph wrapper divs. 189 for (idx, para) in paragraphs.iter().enumerate() { 190 if let Some(para_elem) = dom_document.get_element_by_id(&para.id) { 191 let para_node: &web_sys::Node = para_elem.as_ref(); 192 if para_node.contains(Some(walked_node)) { 193 tracing::trace!( 194 para_id = %para.id, 195 para_idx = idx, 196 char_start = para.char_range.start, 197 "dom_position_to_text_offset: found containing paragraph" 198 ); 199 return Some(para.char_range.start); 200 } 201 } 202 } 203 tracing::warn!( 204 "dom_position_to_text_offset: walked up to editor but couldn't find containing paragraph" 205 ); 206 break None; 207 } 208 209 // Selection is directly on the editor container (e.g., Cmd+A). 210 let child_count = editor_element.child_element_count() as usize; 211 tracing::trace!( 212 offset_in_text_node, 213 child_count, 214 "dom_position_to_text_offset: selection directly on editor container" 215 ); 216 if offset_in_text_node == 0 { 217 tracing::trace!( 218 "dom_position_to_text_offset: returning 0 (editor container offset 0)" 219 ); 220 return Some(0); 221 } else if offset_in_text_node >= child_count { 222 let end = paragraphs.last().map(|p| p.char_range.end); 223 tracing::trace!(end = ?end, "dom_position_to_text_offset: returning end of last paragraph"); 224 return end; 225 } 226 break None; 227 } 228 229 let id = element 230 .get_attribute("id") 231 .or_else(|| element.get_attribute("data-node-id")); 232 233 if let Some(id) = id { 234 // Match both old-style "n0" and paragraph-prefixed "p-2-n0" node IDs. 235 let is_node_id = id.starts_with('n') || id.contains("-n"); 236 tracing::trace!( 237 id = %id, 238 is_node_id, 239 "dom_position_to_text_offset: checking ID pattern" 240 ); 241 if is_node_id { 242 break Some(id); 243 } 244 } 245 } 246 247 walked_from = Some(current_node.clone()); 248 current_node = current_node.parent_node()?; 249 }; 250 251 let node_id = match node_id { 252 Some(id) => id, 253 None => { 254 tracing::trace!("dom_position_to_text_offset: no node_id found in walk-up"); 255 return None; 256 } 257 }; 258 259 tracing::trace!(node_id = %node_id, "dom_position_to_text_offset: found node_id"); 260 261 let container = dom_document.get_element_by_id(&node_id).or_else(|| { 262 let selector = format!("[data-node-id='{}']", node_id); 263 dom_document.query_selector(&selector).ok().flatten() 264 })?; 265 266 // Calculate UTF-16 offset from start of container to the position. 267 let mut utf16_offset_in_container = 0; 268 269 let node_is_container = node 270 .dyn_ref::<web_sys::Element>() 271 .map(|e| e == &container) 272 .unwrap_or(false); 273 274 if node_is_container { 275 // offset_in_text_node is a child index - count text content up to that child. 276 let child_index = offset_in_text_node; 277 let children = container.child_nodes(); 278 let mut text_counted = 0usize; 279 280 for i in 0..child_index.min(children.length() as usize) { 281 if let Some(child) = children.get(i as u32) { 282 if let Some(text) = child.text_content() { 283 text_counted += text.encode_utf16().count(); 284 } 285 } 286 } 287 utf16_offset_in_container = text_counted; 288 289 tracing::trace!( 290 child_index, 291 utf16_offset = utf16_offset_in_container, 292 "dom_position_to_text_offset: node is container, using child index" 293 ); 294 } else { 295 // Normal case: node is a text node, walk to find it. 296 if let Ok(walker) = 297 dom_document.create_tree_walker_with_what_to_show(&container, 0xFFFFFFFF) 298 { 299 let mut skip_until_exit: Option<web_sys::Element> = None; 300 301 while let Ok(Some(dom_node)) = walker.next_node() { 302 if let Some(ref skip_elem) = skip_until_exit { 303 if !skip_elem.contains(Some(&dom_node)) { 304 skip_until_exit = None; 305 } 306 } 307 308 if skip_until_exit.is_none() { 309 if let Some(element) = dom_node.dyn_ref::<web_sys::Element>() { 310 if element.get_attribute("contenteditable").as_deref() == Some("false") { 311 skip_until_exit = Some(element.clone()); 312 continue; 313 } 314 } 315 } 316 317 if skip_until_exit.is_some() { 318 continue; 319 } 320 321 if dom_node.node_type() == web_sys::Node::TEXT_NODE { 322 if &dom_node == node { 323 utf16_offset_in_container += offset_in_text_node; 324 break; 325 } 326 327 if let Some(text) = dom_node.text_content() { 328 utf16_offset_in_container += text.encode_utf16().count(); 329 } 330 } 331 } 332 } 333 } 334 335 // Log what we're looking for. 336 tracing::trace!( 337 node_id = %node_id, 338 utf16_offset = utf16_offset_in_container, 339 num_paragraphs = paragraphs.len(), 340 "dom_position_to_text_offset: looking up mapping" 341 ); 342 343 // Look up the offset in paragraph offset maps. 344 // Track the best match for the node_id in case offset is past the end. 345 let mut best_match_for_node: Option<(usize, &OffsetMapping)> = None; 346 347 for para in paragraphs { 348 for mapping in &para.offset_map { 349 if mapping.node_id == node_id { 350 let mapping_start = mapping.char_offset_in_node; 351 let mapping_end = mapping.char_offset_in_node + mapping.utf16_len; 352 353 tracing::trace!( 354 mapping_node_id = %mapping.node_id, 355 mapping_start, 356 mapping_end, 357 utf16_offset = utf16_offset_in_container, 358 char_range_start = mapping.char_range.start, 359 char_range_end = mapping.char_range.end, 360 "dom_position_to_text_offset: found matching node_id" 361 ); 362 363 // Track the mapping with the highest end position for this node. 364 if best_match_for_node.is_none() || mapping_end > best_match_for_node.unwrap().0 { 365 best_match_for_node = Some((mapping_end, mapping)); 366 } 367 368 let in_range = utf16_offset_in_container >= mapping_start 369 && utf16_offset_in_container <= mapping_end; 370 371 if in_range { 372 let offset_in_mapping = utf16_offset_in_container - mapping_start; 373 let char_offset = mapping.char_range.start + offset_in_mapping; 374 375 tracing::trace!( 376 node_id = %node_id, 377 utf16_offset = utf16_offset_in_container, 378 mapping_start, 379 mapping_end, 380 offset_in_mapping, 381 char_range_start = mapping.char_range.start, 382 char_offset, 383 "dom_position_to_text_offset: MATCHED mapping" 384 ); 385 386 // Check if position is valid (not on invisible content). 387 if is_valid_cursor_position(&para.offset_map, char_offset) { 388 tracing::trace!( 389 char_offset, 390 "dom_position_to_text_offset: returning valid position from mapping" 391 ); 392 return Some(char_offset); 393 } 394 395 // Position is on invisible content, snap to nearest valid. 396 if let Some(snapped) = 397 find_nearest_valid_position(&para.offset_map, char_offset, direction_hint) 398 { 399 tracing::trace!( 400 original = char_offset, 401 snapped = snapped.char_offset(), 402 "dom_position_to_text_offset: snapped from invisible to valid" 403 ); 404 return Some(snapped.char_offset()); 405 } 406 407 // Fallback to original if no snap target. 408 tracing::trace!( 409 char_offset, 410 "dom_position_to_text_offset: returning original (no snap target)" 411 ); 412 return Some(char_offset); 413 } 414 } 415 } 416 } 417 418 // If we found the node_id but offset was past the end, snap to the last tracked position. 419 if let Some((max_end, mapping)) = best_match_for_node { 420 if utf16_offset_in_container > max_end { 421 // Cursor is past the end of tracked content - snap to end of last mapping. 422 let char_offset = mapping.char_range.end; 423 tracing::trace!( 424 node_id = %node_id, 425 utf16_offset = utf16_offset_in_container, 426 max_tracked_end = max_end, 427 snapped_to = char_offset, 428 "dom_position_to_text_offset: offset past tracked content, snapping to end" 429 ); 430 return Some(char_offset); 431 } 432 } 433 434 // No mapping found - try to find a valid position in the paragraph matching the node_id. 435 // Extract paragraph index from node_id format "p-{idx}-n{node}" to avoid jumping to wrong paragraph. 436 let para_idx_from_node = node_id 437 .strip_prefix("p-") 438 .and_then(|rest| rest.split('-').next()) 439 .and_then(|idx_str| idx_str.parse::<usize>().ok()); 440 441 tracing::trace!( 442 node_id = %node_id, 443 utf16_offset = utf16_offset_in_container, 444 para_idx_from_node = ?para_idx_from_node, 445 num_paragraphs = paragraphs.len(), 446 "dom_position_to_text_offset: NO MAPPING FOUND - falling back" 447 ); 448 449 // First try the paragraph that matches the node_id prefix. 450 if let Some(idx) = para_idx_from_node { 451 if let Some(para) = paragraphs.get(idx) { 452 if let Some(snapped) = 453 find_nearest_valid_position(&para.offset_map, para.char_range.start, direction_hint) 454 { 455 tracing::trace!( 456 para_id = %para.id, 457 snapped_offset = snapped.char_offset(), 458 "dom_position_to_text_offset: fallback to matching paragraph" 459 ); 460 return Some(snapped.char_offset()); 461 } 462 } 463 } 464 465 // Last resort: try any paragraph (starting from first). 466 for para in paragraphs { 467 if let Some(snapped) = 468 find_nearest_valid_position(&para.offset_map, para.char_range.start, direction_hint) 469 { 470 tracing::trace!( 471 para_id = %para.id, 472 snapped_offset = snapped.char_offset(), 473 "dom_position_to_text_offset: fallback to first available paragraph" 474 ); 475 return Some(snapped.char_offset()); 476 } 477 } 478 479 None 480} 481 482/// Sync cursor state from DOM to an EditorDocument. 483/// 484/// This is a generic version that works with any `EditorDocument` implementation. 485/// It reads the browser's selection state and updates the document's cursor and selection. 486pub fn sync_cursor_from_dom<D: EditorDocument>( 487 doc: &mut D, 488 editor_id: &str, 489 paragraphs: &[ParagraphRender], 490 direction_hint: Option<SnapDirection>, 491) { 492 if let Some(result) = sync_cursor_from_dom_impl(editor_id, paragraphs, direction_hint) { 493 match result { 494 CursorSyncResult::Cursor(offset) => { 495 doc.set_cursor_offset(offset); 496 doc.set_selection(None); 497 } 498 CursorSyncResult::Selection { anchor, head } => { 499 doc.set_cursor_offset(head); 500 if anchor != head { 501 doc.set_selection(Some(Selection { anchor, head })); 502 } else { 503 doc.set_selection(None); 504 } 505 } 506 CursorSyncResult::None => {} 507 } 508 } 509} 510 511/// Sync cursor from DOM and update syntax visibility in one call. 512/// 513/// This is the common pattern used by most event handlers: sync the cursor 514/// position from the browser's selection, then update which syntax elements 515/// are visible based on the new cursor position. 516/// 517/// Use this for: onclick, onselect, onselectstart, onselectionchange, onkeyup. 518pub fn sync_cursor_and_visibility<D: EditorDocument>( 519 doc: &mut D, 520 editor_id: &str, 521 paragraphs: &[ParagraphRender], 522 syntax_spans: &[SyntaxSpanInfo], 523 direction_hint: Option<SnapDirection>, 524) { 525 sync_cursor_from_dom(doc, editor_id, paragraphs, direction_hint); 526 let cursor_offset = doc.cursor_offset(); 527 let selection = doc.selection(); 528 update_syntax_visibility(cursor_offset, selection.as_ref(), syntax_spans, paragraphs); 529} 530 531/// Update paragraph DOM elements incrementally. 532/// 533/// Uses stable content-based paragraph IDs for efficient DOM reconciliation: 534/// - Unchanged paragraphs (same ID + hash) are not touched 535/// - Changed paragraphs (same ID, different hash) get innerHTML updated 536/// - New paragraphs get created and inserted at correct position 537/// - Removed paragraphs get deleted 538/// 539/// When `FORCE_INNERHTML_UPDATE` is false, cursor paragraph innerHTML updates 540/// are skipped if only text content changed (syntax spans unchanged) and the 541/// DOM content length matches expected. This allows browser-native editing 542/// to proceed without disrupting the selection. 543/// 544/// Returns true if the paragraph containing the cursor was updated. 545pub fn update_paragraph_dom( 546 editor_id: &str, 547 old_paragraphs: &[ParagraphRender], 548 new_paragraphs: &[ParagraphRender], 549 cursor_offset: usize, 550 force: bool, 551) -> bool { 552 use crate::FORCE_INNERHTML_UPDATE; 553 use std::collections::HashMap; 554 555 let window = match web_sys::window() { 556 Some(w) => w, 557 None => return false, 558 }; 559 560 let document = match window.document() { 561 Some(d) => d, 562 None => return false, 563 }; 564 565 let editor = match document.get_element_by_id(editor_id) { 566 Some(e) => e, 567 None => return false, 568 }; 569 570 let mut cursor_para_updated = false; 571 572 // Build lookup for old paragraphs by ID (for syntax span comparison). 573 let old_para_map: HashMap<&str, &ParagraphRender> = 574 old_paragraphs.iter().map(|p| (p.id.as_str(), p)).collect(); 575 576 // Build pool of existing DOM elements by ID. 577 let mut old_elements: HashMap<String, web_sys::Element> = HashMap::new(); 578 let mut child_opt = editor.first_element_child(); 579 while let Some(child) = child_opt { 580 if let Some(id) = child.get_attribute("id") { 581 let next = child.next_element_sibling(); 582 old_elements.insert(id, child); 583 child_opt = next; 584 } else { 585 child_opt = child.next_element_sibling(); 586 } 587 } 588 589 let mut cursor_node: Option<web_sys::Node> = editor.first_element_child().map(|e| e.into()); 590 591 for new_para in new_paragraphs.iter() { 592 let para_id = &new_para.id; 593 let new_hash = format!("{:x}", new_para.source_hash); 594 let is_cursor_para = 595 new_para.char_range.start <= cursor_offset && cursor_offset <= new_para.char_range.end; 596 597 if let Some(existing_elem) = old_elements.remove(para_id.as_str()) { 598 let old_hash = existing_elem.get_attribute("data-hash").unwrap_or_default(); 599 let needs_update = force || old_hash != new_hash; 600 601 let existing_as_node: &web_sys::Node = existing_elem.as_ref(); 602 let at_correct_position = cursor_node 603 .as_ref() 604 .map(|c| c == existing_as_node) 605 .unwrap_or(false); 606 607 if !at_correct_position { 608 tracing::warn!( 609 para_id = %para_id, 610 is_cursor_para, 611 "update_paragraph_dom: element not at correct position, moving" 612 ); 613 let _ = editor.insert_before(existing_as_node, cursor_node.as_ref()); 614 if is_cursor_para { 615 cursor_para_updated = true; 616 } 617 } else { 618 cursor_node = existing_elem.next_element_sibling().map(|e| e.into()); 619 } 620 621 if needs_update { 622 // For cursor paragraph: only update if syntax/formatting changed. 623 // This prevents destroying browser selection during fast typing. 624 // 625 // HOWEVER: we must verify browser actually updated the DOM. 626 // PassThrough assumes browser handles edit, but sometimes it doesn't. 627 let should_skip_cursor_update = 628 !FORCE_INNERHTML_UPDATE && is_cursor_para && !force && { 629 let old_para = old_para_map.get(para_id.as_str()); 630 let syntax_unchanged = old_para 631 .map(|old| old.syntax_spans == new_para.syntax_spans) 632 .unwrap_or(false); 633 634 // Verify DOM content length matches expected. 635 let dom_matches_expected = if syntax_unchanged { 636 let inner_elem = existing_elem.first_element_child(); 637 let dom_text = inner_elem 638 .as_ref() 639 .and_then(|e| e.text_content()) 640 .unwrap_or_default(); 641 let expected_len = new_para.byte_range.end - new_para.byte_range.start; 642 let dom_len = dom_text.len(); 643 let matches = dom_len == expected_len; 644 tracing::trace!( 645 para_id = %para_id, 646 dom_len, 647 expected_len, 648 matches, 649 "DOM sync check" 650 ); 651 matches 652 } else { 653 false 654 }; 655 656 syntax_unchanged && dom_matches_expected 657 }; 658 659 if should_skip_cursor_update { 660 tracing::trace!( 661 para_id = %para_id, 662 "update_paragraph_dom: skipping cursor para innerHTML (syntax unchanged, DOM verified)" 663 ); 664 let _ = existing_elem.set_attribute("data-hash", &new_hash); 665 } else { 666 if tracing::enabled!(tracing::Level::TRACE) { 667 let old_inner = existing_elem.inner_html(); 668 tracing::trace!( 669 para_id = %para_id, 670 old_inner = %old_inner.escape_debug(), 671 new_html = %new_para.html.escape_debug(), 672 "update_paragraph_dom: replacing innerHTML" 673 ); 674 } 675 676 // Timing instrumentation. 677 let start = web_sys::window() 678 .and_then(|w| w.performance()) 679 .map(|p| p.now()); 680 681 existing_elem.set_inner_html(&new_para.html); 682 let _ = existing_elem.set_attribute("data-hash", &new_hash); 683 684 if let Some(start_time) = start { 685 if let Some(end_time) = web_sys::window() 686 .and_then(|w| w.performance()) 687 .map(|p| p.now()) 688 { 689 let elapsed_ms = end_time - start_time; 690 tracing::trace!( 691 para_id = %para_id, 692 is_cursor_para, 693 elapsed_ms, 694 html_len = new_para.html.len(), 695 "update_paragraph_dom: innerHTML update timing" 696 ); 697 } 698 } 699 700 if is_cursor_para { 701 if let Err(e) = 702 restore_cursor_position(cursor_offset, &new_para.offset_map, None) 703 { 704 tracing::warn!("Synchronous cursor restore failed: {:?}", e); 705 } 706 cursor_para_updated = true; 707 } 708 } 709 } 710 } else { 711 // New element - create and insert. 712 if let Ok(div) = document.create_element("div") { 713 div.set_id(para_id); 714 div.set_inner_html(&new_para.html); 715 let _ = div.set_attribute("data-hash", &new_hash); 716 let div_node: &web_sys::Node = div.as_ref(); 717 let _ = editor.insert_before(div_node, cursor_node.as_ref()); 718 719 if is_cursor_para { 720 if let Err(e) = 721 restore_cursor_position(cursor_offset, &new_para.offset_map, None) 722 { 723 tracing::warn!("Cursor restore for new paragraph failed: {:?}", e); 724 } 725 cursor_para_updated = true; 726 } 727 } 728 } 729 } 730 731 // Remove stale elements. 732 for (_, elem) in old_elements { 733 let _ = elem.remove(); 734 cursor_para_updated = true; 735 } 736 737 cursor_para_updated 738}