···6use dioxus::prelude::*;
78use super::document::{EditorDocument, Selection};
9-use super::offset_map::{find_nearest_valid_position, is_valid_cursor_position, SnapDirection};
10use super::paragraph::ParagraphRender;
1112/// Sync internal cursor and selection state from browser DOM selection.
···72 let focus_offset = selection.focus_offset() as usize;
7374 // Convert both DOM positions to rope offsets using cached paragraphs
75- let anchor_rope = dom_position_to_rope_offset(
76 &dom_document,
77 &editor_element,
78 &anchor_node,
···80 paragraphs,
81 direction_hint,
82 );
83- let focus_rope = dom_position_to_rope_offset(
84 &dom_document,
85 &editor_element,
86 &focus_node,
···114/// The `direction_hint` is used when snapping from invisible content to determine
115/// which direction to prefer.
116#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
117-fn dom_position_to_rope_offset(
118 dom_document: &web_sys::Document,
119 editor_element: &web_sys::Element,
120 node: &web_sys::Node,
···215 // No mapping found - try to find any valid position in paragraphs
216 // This handles clicks on non-text elements like images
217 for para in paragraphs {
218- if let Some(snapped) = find_nearest_valid_position(¶.offset_map, para.char_range.start, direction_hint) {
00219 return Some(snapped.char_offset());
220 }
221 }
···6use dioxus::prelude::*;
78use super::document::{EditorDocument, Selection};
9+use super::offset_map::{SnapDirection, find_nearest_valid_position, is_valid_cursor_position};
10use super::paragraph::ParagraphRender;
1112/// Sync internal cursor and selection state from browser DOM selection.
···72 let focus_offset = selection.focus_offset() as usize;
7374 // Convert both DOM positions to rope offsets using cached paragraphs
75+ let anchor_rope = dom_position_to_text_offset(
76 &dom_document,
77 &editor_element,
78 &anchor_node,
···80 paragraphs,
81 direction_hint,
82 );
83+ let focus_rope = dom_position_to_text_offset(
84 &dom_document,
85 &editor_element,
86 &focus_node,
···114/// The `direction_hint` is used when snapping from invisible content to determine
115/// which direction to prefer.
116#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
117+pub fn dom_position_to_text_offset(
118 dom_document: &web_sys::Document,
119 editor_element: &web_sys::Element,
120 node: &web_sys::Node,
···215 // No mapping found - try to find any valid position in paragraphs
216 // This handles clicks on non-text elements like images
217 for para in paragraphs {
218+ if let Some(snapped) =
219+ find_nearest_valid_position(¶.offset_map, para.char_range.start, direction_hint)
220+ {
221 return Some(snapped.char_offset());
222 }
223 }
+2
crates/weaver-app/src/components/editor/mod.rs
···4//! characters are hidden contextually based on cursor position, while still
5//! editing plain markdown text under the hood.
6007mod component;
8mod cursor;
9mod document;
···4//! characters are hidden contextually based on cursor position, while still
5//! editing plain markdown text under the hood.
67+mod actions;
8+mod beforeinput;
9mod component;
10mod cursor;
11mod document;
+31-1
crates/weaver-app/src/components/editor/writer.rs
···1322 self.record_mapping(range.clone(), char_start..char_end);
1323 self.last_char_offset = char_end;
1324 }
1325- SoftBreak => self.write_newline()?,
0000000000000000000000000000001326 HardBreak => {
1327 // Emit the two spaces as visible (dimmed) text, then <br>
1328 let gap = &self.source[range.clone()];
···1322 self.record_mapping(range.clone(), char_start..char_end);
1323 self.last_char_offset = char_end;
1324 }
1325+ SoftBreak => {
1326+ // Emit <br> for visual line break, plus a space for cursor positioning.
1327+ // This space maps to the \n so the cursor can land here when navigating.
1328+ let char_start = self.last_char_offset;
1329+1330+ // Emit <br>
1331+ self.write("<br />")?;
1332+ self.current_node_child_count += 1;
1333+1334+ // Emit space for cursor positioning - this gives the browser somewhere
1335+ // to place the cursor when navigating to this line
1336+ self.write(" ")?;
1337+ self.current_node_child_count += 1;
1338+1339+ // Map the space to the newline position - cursor landing here means
1340+ // we're at the end of the line (after the \n)
1341+ if let Some(ref node_id) = self.current_node_id {
1342+ let mapping = OffsetMapping {
1343+ byte_range: range.clone(),
1344+ char_range: char_start..char_start + 1,
1345+ node_id: node_id.clone(),
1346+ char_offset_in_node: self.current_node_char_offset,
1347+ child_index: None,
1348+ utf16_len: 1, // the space we emitted
1349+ };
1350+ self.offset_maps.push(mapping);
1351+ self.current_node_char_offset += 1;
1352+ }
1353+1354+ self.last_char_offset = char_start + 1; // +1 for the \n
1355+ }
1356 HardBreak => {
1357 // Emit the two spaces as visible (dimmed) text, then <br>
1358 let gap = &self.source[range.clone()];