Skip to content

Commit 1f18b2d

Browse files
committed
newline cursor jumping fixed
1 parent 5b8bf5d commit 1f18b2d

File tree

8 files changed

+219
-36
lines changed

8 files changed

+219
-36
lines changed

crates/weaver-editor-browser/src/dom_sync.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,10 +611,15 @@ pub fn update_paragraph_dom(
611611
let _ = div.set_attribute("data-hash", &new_hash);
612612
let div_node: &web_sys::Node = div.as_ref();
613613
let _ = editor.insert_before(div_node, cursor_node.as_ref());
614-
}
615614

616-
if is_cursor_para {
617-
cursor_para_updated = true;
615+
if is_cursor_para {
616+
if let Err(e) =
617+
restore_cursor_position(cursor_offset, &new_para.offset_map, None)
618+
{
619+
tracing::warn!("Cursor restore for new paragraph failed: {:?}", e);
620+
}
621+
cursor_para_updated = true;
622+
}
618623
}
619624
}
620625
}

crates/weaver-editor-core/src/writer/events.rs

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use core::fmt;
44
use std::fmt::Write as _;
55
use std::ops::Range;
66

7-
use markdown_weaver::{Event, TagEnd};
7+
use markdown_weaver::{Event, Tag, TagEnd};
88
use markdown_weaver_escape::{escape_html, escape_html_body_text_with_char_count};
99

1010
use crate::offset_map::OffsetMapping;
@@ -58,6 +58,24 @@ where
5858
// Emit gap from last_byte_offset to range.end
5959
self.emit_gap_before(range.end)?;
6060
} else if !matches!(&event, Event::End(_)) {
61+
// For paragraph-level start events, capture pre-gap position so the
62+
// paragraph's char_range includes leading whitespace/gap content.
63+
let is_para_start = matches!(
64+
&event,
65+
Event::Start(
66+
Tag::Paragraph(_)
67+
| Tag::Heading { .. }
68+
| Tag::CodeBlock(_)
69+
| Tag::List(_)
70+
| Tag::BlockQuote(_)
71+
| Tag::HtmlBlock
72+
)
73+
);
74+
if is_para_start && self.paragraphs.should_track_boundaries() {
75+
self.paragraphs.pre_gap_start =
76+
Some((self.last_byte_offset, self.last_char_offset));
77+
}
78+
6179
// For other events, emit any gap before range.start
6280
// (emit_syntax handles char offset tracking)
6381
self.emit_gap_before(range.start)?;
@@ -79,16 +97,31 @@ where
7997
// else: Event updated offset (e.g. start_tag emitted opening syntax), keep that value
8098
}
8199

82-
// Emit any trailing syntax
83-
self.emit_gap_before(self.source.len())?;
100+
// Check if document ends with a paragraph break (double newline) BEFORE emitting trailing.
101+
// If so, we'll reserve the final newline for a synthetic trailing paragraph.
102+
let ends_with_para_break = self.source.ends_with("\n\n")
103+
|| self.source.ends_with("\n\u{200C}\n");
104+
105+
// Determine where to stop emitting trailing syntax
106+
let trailing_emit_end = if ends_with_para_break {
107+
// Don't emit the final newline - save it for synthetic paragraph
108+
self.source.len().saturating_sub(1)
109+
} else {
110+
self.source.len()
111+
};
112+
113+
// Emit trailing syntax up to the determined point
114+
self.emit_gap_before(trailing_emit_end)?;
84115

85116
// Handle unmapped trailing content (stripped by parser)
86117
// This includes trailing spaces that markdown ignores
87118
let doc_byte_len = self.source.len();
88119
let doc_char_len = self.text_buffer.len_chars();
89120

90-
if self.last_byte_offset < doc_byte_len || self.last_char_offset < doc_char_len {
91-
// Emit the trailing content as visible syntax
121+
if !ends_with_para_break
122+
&& (self.last_byte_offset < doc_byte_len || self.last_char_offset < doc_char_len)
123+
{
124+
// Emit the trailing content as visible syntax (only if not creating synthetic para)
92125
if self.last_byte_offset < doc_byte_len {
93126
let trailing = &self.source[self.last_byte_offset..];
94127
if !trailing.is_empty() {
@@ -125,7 +158,7 @@ where
125158
}
126159
}
127160

128-
// Add any remaining accumulated data for the last paragraph
161+
// Add any remaining accumulated data for the last paragraph FIRST
129162
// (content that wasn't followed by a paragraph boundary)
130163
if !self.current_para.offset_maps.is_empty()
131164
|| !self.current_para.syntax_spans.is_empty()
@@ -139,6 +172,48 @@ where
139172
.push(std::mem::take(&mut self.ref_collector.refs));
140173
}
141174

175+
// Now create a synthetic trailing paragraph if needed
176+
if ends_with_para_break {
177+
// Get the trailing content we reserved (the final newline)
178+
let trailing_content = &self.source[trailing_emit_end..];
179+
let trailing_char_len = trailing_content.chars().count();
180+
181+
let trailing_start_char = self.last_char_offset;
182+
let trailing_start_byte = self.last_byte_offset;
183+
let trailing_end_char = trailing_start_char + trailing_char_len;
184+
let trailing_end_byte = self.source.len();
185+
186+
// Create paragraph range that includes the trailing content
187+
self.paragraphs.ranges.push((
188+
trailing_start_byte..trailing_end_byte,
189+
trailing_start_char..trailing_end_char,
190+
));
191+
192+
// Start a new HTML segment for this trailing paragraph
193+
self.writer.new_segment();
194+
let node_id = self.gen_node_id();
195+
196+
// Write the actual trailing content plus ZWSP for cursor positioning
197+
write!(&mut self.writer, "<span id=\"{}\">", node_id)?;
198+
escape_html(&mut self.writer, trailing_content)?;
199+
self.write("\u{200B}</span>")?;
200+
201+
// Record offset mapping for the trailing content
202+
let mapping = OffsetMapping {
203+
byte_range: trailing_start_byte..trailing_end_byte,
204+
char_range: trailing_start_char..trailing_end_char,
205+
node_id,
206+
char_offset_in_node: 0,
207+
child_index: None,
208+
utf16_len: trailing_char_len + 1, // Content + ZWSP
209+
};
210+
211+
// Create offset_maps/syntax_spans/refs for this trailing paragraph
212+
self.offset_maps_by_para.push(vec![mapping]);
213+
self.syntax_spans_by_para.push(vec![]);
214+
self.refs_by_para.push(vec![]);
215+
}
216+
142217
// Get HTML segments from writer
143218
let html_segments = self.writer.into_segments();
144219

crates/weaver-editor-core/src/writer/state.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ pub struct ParagraphTracker {
163163
pub ranges: Vec<(Range<usize>, Range<usize>)>,
164164
/// Start of current paragraph: (byte_offset, char_offset)
165165
pub current_start: Option<(usize, usize)>,
166+
/// Pre-gap position for paragraph start (captured before gap emission).
167+
/// This ensures the paragraph's char_range includes leading whitespace.
168+
pub pre_gap_start: Option<(usize, usize)>,
166169
/// List nesting depth (suppress paragraph boundaries inside lists)
167170
pub list_depth: usize,
168171
/// In footnote definition (suppress inner paragraph boundaries)

crates/weaver-editor-core/src/writer/tags.rs

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,15 @@ where
147147
match tag {
148148
// HTML blocks get their own paragraph to try and corral them better
149149
Tag::HtmlBlock => {
150-
// Record paragraph start for boundary tracking
151-
// Skip if inside a list or footnote def - they own their paragraph boundary
150+
// Record paragraph start for boundary tracking.
151+
// Use pre_gap_start if available to include leading whitespace in char_range.
152+
// Skip if inside a list or footnote def - they own their paragraph boundary.
152153
if self.paragraphs.list_depth == 0 && !self.paragraphs.in_footnote_def {
153-
self.paragraphs.current_start =
154-
Some((self.last_byte_offset, self.last_char_offset));
154+
self.paragraphs.current_start = self
155+
.paragraphs
156+
.pre_gap_start
157+
.take()
158+
.or(Some((self.last_byte_offset, self.last_char_offset)));
155159
}
156160
let node_id = self.gen_node_id();
157161

@@ -189,11 +193,15 @@ where
189193
// Handle wrapper before block
190194
self.emit_wrapper_start()?;
191195

192-
// Record paragraph start for boundary tracking
193-
// Skip if inside a list or footnote def - they own their paragraph boundary
196+
// Record paragraph start for boundary tracking.
197+
// Use pre_gap_start if available to include leading whitespace in char_range.
198+
// Skip if inside a list or footnote def - they own their paragraph boundary.
194199
if self.paragraphs.list_depth == 0 && !self.paragraphs.in_footnote_def {
195-
self.paragraphs.current_start =
196-
Some((self.last_byte_offset, self.last_char_offset));
200+
self.paragraphs.current_start = self
201+
.paragraphs
202+
.pre_gap_start
203+
.take()
204+
.or(Some((self.last_byte_offset, self.last_char_offset)));
197205
}
198206

199207
let node_id = self.gen_node_id();
@@ -273,10 +281,14 @@ where
273281
// Emit wrapper if pending (but don't close on heading end - wraps following block too)
274282
self.emit_wrapper_start()?;
275283

276-
// Record paragraph start for boundary tracking
277-
// Treat headings as paragraph-level blocks
278-
self.paragraphs.current_start =
279-
Some((self.last_byte_offset, self.last_char_offset));
284+
// Record paragraph start for boundary tracking.
285+
// Use pre_gap_start if available to include leading whitespace in char_range.
286+
// Treat headings as paragraph-level blocks.
287+
self.paragraphs.current_start = self
288+
.paragraphs
289+
.pre_gap_start
290+
.take()
291+
.or(Some((self.last_byte_offset, self.last_char_offset)));
280292

281293
if !self.end_newline {
282294
self.write_newline()?;
@@ -435,9 +447,13 @@ where
435447
Tag::CodeBlock(info) => {
436448
self.emit_wrapper_start()?;
437449

438-
// Track code block as paragraph-level block
439-
self.paragraphs.current_start =
440-
Some((self.last_byte_offset, self.last_char_offset));
450+
// Track code block as paragraph-level block.
451+
// Use pre_gap_start if available to include leading whitespace in char_range.
452+
self.paragraphs.current_start = self
453+
.paragraphs
454+
.pre_gap_start
455+
.take()
456+
.or(Some((self.last_byte_offset, self.last_char_offset)));
441457

442458
if !self.end_newline {
443459
self.write_newline()?;
@@ -511,9 +527,13 @@ where
511527
}
512528
Tag::List(Some(1)) => {
513529
self.emit_wrapper_start()?;
514-
// Track list as paragraph-level block
515-
self.paragraphs.current_start =
516-
Some((self.last_byte_offset, self.last_char_offset));
530+
// Track list as paragraph-level block.
531+
// Use pre_gap_start if available to include leading whitespace in char_range.
532+
self.paragraphs.current_start = self
533+
.paragraphs
534+
.pre_gap_start
535+
.take()
536+
.or(Some((self.last_byte_offset, self.last_char_offset)));
517537
self.paragraphs.list_depth += 1;
518538
if self.end_newline {
519539
self.write("<ol>")
@@ -523,9 +543,13 @@ where
523543
}
524544
Tag::List(Some(start)) => {
525545
self.emit_wrapper_start()?;
526-
// Track list as paragraph-level block
527-
self.paragraphs.current_start =
528-
Some((self.last_byte_offset, self.last_char_offset));
546+
// Track list as paragraph-level block.
547+
// Use pre_gap_start if available to include leading whitespace in char_range.
548+
self.paragraphs.current_start = self
549+
.paragraphs
550+
.pre_gap_start
551+
.take()
552+
.or(Some((self.last_byte_offset, self.last_char_offset)));
529553
self.paragraphs.list_depth += 1;
530554
if self.end_newline {
531555
self.write("<ol start=\"")?;
@@ -537,9 +561,13 @@ where
537561
}
538562
Tag::List(None) => {
539563
self.emit_wrapper_start()?;
540-
// Track list as paragraph-level block
541-
self.paragraphs.current_start =
542-
Some((self.last_byte_offset, self.last_char_offset));
564+
// Track list as paragraph-level block.
565+
// Use pre_gap_start if available to include leading whitespace in char_range.
566+
self.paragraphs.current_start = self
567+
.paragraphs
568+
.pre_gap_start
569+
.take()
570+
.or(Some((self.last_byte_offset, self.last_char_offset)));
543571
self.paragraphs.list_depth += 1;
544572
if self.end_newline {
545573
self.write("<ul>")

crates/weaver-renderer/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub mod leaflet;
3535
pub mod math;
3636
#[cfg(feature = "pckt")]
3737
pub mod pckt;
38-
#[cfg(not(target_family = "wasm"))]
38+
#[cfg(all(not(target_family = "wasm"), feature = "syntax-highlighting"))]
3939
pub mod static_site;
4040
pub mod theme;
4141
pub mod types;

crates/weaver-renderer/src/static_site.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ where
231231
Ok(())
232232
}
233233

234+
#[cfg(feature = "syntax-css")]
234235
async fn generate_css_files(&self) -> Result<(), miette::Report> {
235236
use crate::css::{generate_base_css, generate_syntax_css};
236237

@@ -257,6 +258,13 @@ where
257258
Ok(())
258259
}
259260

261+
#[cfg(not(feature = "syntax-css"))]
262+
async fn generate_css_files(&self) -> Result<(), miette::Report> {
263+
Err(miette::miette!(
264+
"CSS generation requires the 'syntax-css' feature"
265+
))
266+
}
267+
260268
async fn generate_default_index(&self) -> Result<(), miette::Report> {
261269
let index_path = self.context.destination.join("index.html");
262270
let mut index_file = crate::utils::create_file(&index_path).await?;

crates/weaver-renderer/src/static_site/document.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#[cfg(feature = "syntax-css")]
12
use crate::css::{generate_base_css, generate_syntax_css};
23
use crate::static_site::context::{KaTeXSource, StaticSiteContext};
34
use crate::theme::default_resolved_theme;
@@ -97,6 +98,7 @@ pub async fn write_document_head<A: AgentSession>(
9798
.await
9899
.into_diagnostic()?;
99100
}
101+
#[cfg(feature = "syntax-css")]
100102
CssMode::Inline => {
101103
let default_theme = default_resolved_theme();
102104
let theme = context.theme.as_deref().unwrap_or(&default_theme);
@@ -116,6 +118,13 @@ pub async fn write_document_head<A: AgentSession>(
116118
.into_diagnostic()?;
117119
writer.write_all(b" </style>\n").await.into_diagnostic()?;
118120
}
121+
#[cfg(not(feature = "syntax-css"))]
122+
CssMode::Inline => {
123+
// CSS generation not available without syntax-css feature
124+
return Err(miette::miette!(
125+
"Inline CSS mode requires the 'syntax-css' feature"
126+
));
127+
}
119128
}
120129

121130
// KaTeX if enabled

0 commit comments

Comments
 (0)