fixed the stupid code block bug

Orual 0d35d8db 6ecfcf49

+89 -10
+29 -2
crates/weaver-app/src/components/editor/render.rs
··· 159 159 // Determine if we can use fast path (skip boundary discovery) 160 160 // Need cache and non-boundary-affecting edit info (for edit position) 161 161 let current_len = text.len_unicode(); 162 - let use_fast_path = cache.is_some() && edit.is_some() && !is_boundary_affecting(edit.unwrap()); 162 + 163 + let use_fast_path = cache.is_some() 164 + && edit.is_some() 165 + && !is_boundary_affecting(edit.unwrap()); 163 166 164 167 tracing::debug!( 165 168 target: "weaver::render", ··· 218 221 }) 219 222 .collect::<Vec<_>>() 220 223 } else { 221 - // Slow path: run boundary-only pass to discover paragraph boundaries 224 + vec![] // Will be populated by slow path below 225 + }; 226 + 227 + // Validate fast path results - if any ranges are invalid, use slow path 228 + let paragraph_ranges = if !paragraph_ranges.is_empty() { 229 + let all_valid = paragraph_ranges 230 + .iter() 231 + .all(|(_, char_range)| char_range.start <= char_range.end); 232 + if all_valid { 233 + paragraph_ranges 234 + } else { 235 + tracing::debug!( 236 + target: "weaver::render", 237 + "fast path produced invalid ranges, falling back to slow path" 238 + ); 239 + vec![] // Trigger slow path 240 + } 241 + } else { 242 + paragraph_ranges 243 + }; 244 + 245 + // Slow path: run boundary-only pass to discover paragraph boundaries 246 + let paragraph_ranges = if paragraph_ranges.is_empty() { 222 247 let parser = 223 248 Parser::new_ext(&source, weaver_renderer::default_md_options()).into_offset_iter(); 224 249 let mut scratch_output = String::new(); ··· 234 259 Ok(result) => result.paragraph_ranges, 235 260 Err(_) => return (Vec::new(), RenderCache::default(), vec![]), 236 261 } 262 + } else { 263 + paragraph_ranges 237 264 }; 238 265 239 266 // Log discovered paragraphs
+60 -8
crates/weaver-app/src/components/editor/writer.rs
··· 338 338 code_buffer: Option<(Option<String>, String)>, // (lang, content) 339 339 code_buffer_byte_range: Option<Range<usize>>, // byte range of buffered code content 340 340 code_buffer_char_range: Option<Range<usize>>, // char range of buffered code content 341 + code_block_char_start: Option<usize>, // char offset where code block started 342 + code_block_opening_span_idx: Option<usize>, // index of opening fence syntax span 341 343 pending_blockquote_range: Option<Range<usize>>, // range for emitting > inside next paragraph 342 344 343 345 // Table rendering mode ··· 455 457 code_buffer: None, 456 458 code_buffer_byte_range: None, 457 459 code_buffer_char_range: None, 460 + code_block_char_start: None, 461 + code_block_opening_span_idx: None, 458 462 pending_blockquote_range: None, 459 463 render_tables_as_markdown: true, // Default to markdown rendering 460 464 table_start_offset: None, ··· 503 507 code_buffer: None, 504 508 code_buffer_byte_range: None, 505 509 code_buffer_char_range: None, 510 + code_block_char_start: None, 511 + code_block_opening_span_idx: None, 506 512 pending_blockquote_range: None, 507 513 render_tables_as_markdown: true, 508 514 table_start_offset: None, ··· 545 551 code_buffer: self.code_buffer, 546 552 code_buffer_byte_range: self.code_buffer_byte_range, 547 553 code_buffer_char_range: self.code_buffer_char_range, 554 + code_block_char_start: self.code_block_char_start, 555 + code_block_opening_span_idx: self.code_block_opening_span_idx, 548 556 pending_blockquote_range: self.pending_blockquote_range, 549 557 render_tables_as_markdown: self.render_tables_as_markdown, 550 558 table_start_offset: self.table_start_offset, ··· 590 598 code_buffer: self.code_buffer, 591 599 code_buffer_byte_range: self.code_buffer_byte_range, 592 600 code_buffer_char_range: self.code_buffer_char_range, 601 + code_block_char_start: self.code_block_char_start, 602 + code_block_opening_span_idx: self.code_block_opening_span_idx, 593 603 pending_blockquote_range: self.pending_blockquote_range, 594 604 render_tables_as_markdown: self.render_tables_as_markdown, 595 605 table_start_offset: self.table_start_offset, ··· 1973 1983 escape_html(&mut self.writer, syntax)?; 1974 1984 self.write("</span>\n")?; 1975 1985 1986 + // Track opening span index for formatted_range update later 1987 + self.code_block_opening_span_idx = Some(self.syntax_spans.len()); 1988 + self.code_block_char_start = Some(char_start); 1989 + 1976 1990 self.syntax_spans.push(SyntaxSpanInfo { 1977 1991 syn_id, 1978 1992 char_range: char_start..char_end, 1979 1993 syntax_type: SyntaxType::Block, 1980 - formatted_range: None, 1994 + formatted_range: None, // Will be set in TagEnd::CodeBlock 1981 1995 }); 1982 1996 1983 1997 self.last_char_offset += syntax_char_len; ··· 2570 2584 self.record_mapping(code_byte_range, code_char_range); 2571 2585 } 2572 2586 2587 + // Get node_id for data-node-id attribute (needed for cursor positioning) 2588 + let node_id = self.current_node_id.clone(); 2589 + 2573 2590 if let Some(ref lang_str) = lang { 2574 2591 // Use a temporary String buffer for syntect 2575 2592 let mut temp_output = String::new(); ··· 2580 2597 &mut temp_output, 2581 2598 ) { 2582 2599 Ok(_) => { 2583 - self.write(&temp_output)?; 2600 + // Inject data-node-id into the <pre> tag for cursor positioning 2601 + if let Some(ref nid) = node_id { 2602 + let injected = temp_output.replacen( 2603 + "<pre>", 2604 + &format!("<pre data-node-id=\"{}\">", nid), 2605 + 1, 2606 + ); 2607 + self.write(&injected)?; 2608 + } else { 2609 + self.write(&temp_output)?; 2610 + } 2584 2611 } 2585 2612 Err(_) => { 2586 2613 // Fallback to plain code block 2587 - self.write("<pre><code class=\"language-")?; 2614 + if let Some(ref nid) = node_id { 2615 + write!(&mut self.writer, "<pre data-node-id=\"{}\"><code class=\"language-", nid)?; 2616 + } else { 2617 + self.write("<pre><code class=\"language-")?; 2618 + } 2588 2619 escape_html(&mut self.writer, lang_str)?; 2589 2620 self.write("\">")?; 2590 2621 escape_html_body_text(&mut self.writer, &buffer)?; ··· 2592 2623 } 2593 2624 } 2594 2625 } else { 2595 - self.write("<pre><code>")?; 2626 + if let Some(ref nid) = node_id { 2627 + write!(&mut self.writer, "<pre data-node-id=\"{}\"><code>", nid)?; 2628 + } else { 2629 + self.write("<pre><code>")?; 2630 + } 2596 2631 escape_html_body_text(&mut self.writer, &buffer)?; 2597 2632 self.write("</code></pre>\n")?; 2598 2633 } ··· 2604 2639 } 2605 2640 2606 2641 // Emit closing ``` (emit_gap_before is skipped while buffering) 2642 + // Track the opening span index and char start before we potentially clear them 2643 + let opening_span_idx = self.code_block_opening_span_idx.take(); 2644 + let code_block_start = self.code_block_char_start.take(); 2645 + 2607 2646 if range.start < range.end { 2608 2647 let raw_text = &self.source[range.clone()]; 2609 2648 if let Some(fence_line) = raw_text.lines().last() { ··· 2623 2662 escape_html(&mut self.writer, fence)?; 2624 2663 self.write("</span>")?; 2625 2664 2665 + self.last_char_offset += fence_char_len; 2666 + self.last_byte_offset += fence.len(); 2667 + 2668 + // Compute formatted_range for entire code block (opening fence to closing fence) 2669 + let formatted_range = code_block_start 2670 + .map(|start| start..self.last_char_offset); 2671 + 2672 + // Update opening fence span with formatted_range 2673 + if let (Some(idx), Some(fr)) = (opening_span_idx, formatted_range.as_ref()) 2674 + { 2675 + if let Some(span) = self.syntax_spans.get_mut(idx) { 2676 + span.formatted_range = Some(fr.clone()); 2677 + } 2678 + } 2679 + 2680 + // Push closing fence span with formatted_range 2626 2681 self.syntax_spans.push(SyntaxSpanInfo { 2627 2682 syn_id, 2628 2683 char_range: char_start..char_end, 2629 2684 syntax_type: SyntaxType::Block, 2630 - formatted_range: None, 2685 + formatted_range, 2631 2686 }); 2632 - 2633 - self.last_char_offset += fence_char_len; 2634 - self.last_byte_offset += fence.len(); 2635 2687 } 2636 2688 } 2637 2689 }