1use std::{
2 collections::HashMap,
3 ops::Range,
4 sync::{Arc, Mutex, OnceLock, Weak},
5};
6
7use gpui::{
8 AnyElement, App, Axis, DefiniteLength, Div, ElementId, FontStyle, FontWeight, HighlightStyle,
9 Hsla, Image, ImageFormat, ImageSource, InteractiveElement as _, IntoElement, IsZero as _,
10 Length, ObjectFit, Overflow, ParentElement, Pixels, Rems, ScrollHandle, SharedString,
11 SharedUri, StatefulInteractiveElement, StyleRefinement, Styled, StyledImage as _, WhiteSpace,
12 Window, div, img, prelude::FluentBuilder as _, px, relative, rems,
13};
14use markdown::mdast;
15
16use crate::{
17 ScrollableMask, Scrollbar, StyledExt, h_flex,
18 scrollable_mask::horizontal_scroll_area,
19 text::{
20 CodeBlockActionsFn, CodeBlockHighlighterFn, LinkClickHandlerFn, MarkdownExtensions,
21 MarkdownNode, TableActionsFn,
22 document::NodeRenderOptions,
23 inline::{
24 Inline, InlineHighlight, InlineState, combine_highlights, fade_highlights, text_runs,
25 text_size_ranges,
26 },
27 inline_flow::{InlineFlow, InlineFlowItem, slice_ranges},
28 range_highlight::{RangeHighlightFrame, RevealAt, RevealRequest},
29 stream_fade::{StreamFadeFrame, TextLeafKey},
30 text_view::handle_link_click,
31 },
32 theme::ActiveTheme as _,
33};
34
35use super::{
36 SelectionFormat, TextViewStyle,
37 utils::{data_url_image, list_item_prefix, ordered_list_ordinal},
38};
39
40const CHECK_SVG_LIGHT: &[u8] = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><path d="m3.25 8.25 3 3 6.5-7" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>"#;
41const CHECK_SVG_DARK: &[u8] = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><path d="m3.25 8.25 3 3 6.5-7" stroke="black" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>"#;
42
43#[derive(Debug, Clone, PartialEq)]
45pub(crate) enum BlockNode {
46 Root {
48 children: Vec<BlockNode>,
49 span: Option<Span>,
50 },
51 Paragraph(Paragraph),
52 Heading {
53 level: u8,
54 children: Paragraph,
55 span: Option<Span>,
56 },
57 Blockquote {
58 children: Vec<BlockNode>,
59 span: Option<Span>,
60 },
61 List {
62 children: Vec<BlockNode>,
64 ordered: bool,
65 start: Option<u32>,
67 span: Option<Span>,
68 },
69 ListItem {
70 children: Vec<BlockNode>,
71 spread: bool,
72 checked: Option<bool>,
74 span: Option<Span>,
75 },
76 CodeBlock(CodeBlock),
77 Custom(MarkdownNode),
79 Table(Table),
80 Break {
81 html: bool,
82 span: Option<Span>,
83 },
84 HorizontalRule {
85 span: Option<Span>,
86 },
87 Definition {
89 identifier: SharedString,
90 url: SharedString,
91 title: Option<SharedString>,
92 span: Option<Span>,
93 },
94 Unknown,
95}
96
97#[derive(Clone, Copy)]
98enum BlockTextKind {
99 All,
100 Selected,
101 SelectedSource,
104}
105
106impl BlockNode {
107 pub(super) fn is_list_item(&self) -> bool {
108 matches!(self, Self::ListItem { .. })
109 }
110
111 pub(super) fn compact(self) -> BlockNode {
113 match self {
114 Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
115 _ => self,
116 }
117 }
118
119 pub(crate) fn span(&self) -> Option<Span> {
121 match self {
122 BlockNode::Root { span, .. } => *span,
123 BlockNode::Paragraph(paragraph) => paragraph.span,
124 BlockNode::Heading { span, .. } => *span,
125 BlockNode::Blockquote { span, .. } => *span,
126 BlockNode::List { span, .. } => *span,
127 BlockNode::ListItem { span, .. } => *span,
128 BlockNode::CodeBlock(code_block) => code_block.span,
129 BlockNode::Custom(el) => el.span,
130 BlockNode::Table(table) => table.span,
131 BlockNode::Break { span, .. } => *span,
132 BlockNode::HorizontalRule { span, .. } => *span,
133 BlockNode::Definition { span, .. } => *span,
134 BlockNode::Unknown { .. } => None,
135 }
136 }
137
138 pub(super) fn text(&self) -> String {
139 self.text_by_kind(BlockTextKind::All)
140 }
141
142 pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
147 self.text_by_kind(match format {
148 SelectionFormat::Plain => BlockTextKind::Selected,
149 SelectionFormat::Source => BlockTextKind::SelectedSource,
150 })
151 }
152
153 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
154 let mut selected = SourceRangeSelection::Unselected;
155 match self {
156 BlockNode::Root { children, .. }
157 | BlockNode::Blockquote { children, .. }
158 | BlockNode::List { children, .. }
159 | BlockNode::ListItem { children, .. } => {
160 for child in children {
161 selected.merge(child.selected_source_range());
162 }
163 }
164 BlockNode::Paragraph(paragraph) => selected = paragraph.selected_source_range(),
165 BlockNode::Heading { children, .. } => selected = children.selected_source_range(),
166 BlockNode::Table(table) => {
167 for row in &table.children {
168 for cell in &row.children {
169 selected.merge(cell.children.selected_source_range());
170 }
171 }
172 }
173 BlockNode::CodeBlock(code_block) => selected = code_block.selected_source_range(),
174 BlockNode::Custom(_)
175 | BlockNode::Definition { .. }
176 | BlockNode::Break { .. }
177 | BlockNode::HorizontalRule { .. }
178 | BlockNode::Unknown => {}
179 }
180 selected
181 }
182
183 fn text_by_kind(&self, kind: BlockTextKind) -> String {
184 let mut text = String::new();
185 match self {
186 BlockNode::Root { children, .. } => {
187 let block_text = Self::children_text(children, kind);
188 if !block_text.is_empty() {
189 text.push_str(&block_text);
190 text.push('\n');
191 }
192 }
193 BlockNode::Paragraph(paragraph) => {
194 let block_text = match kind {
195 BlockTextKind::All => paragraph.text(),
196 BlockTextKind::Selected => paragraph.selected_text(),
197 BlockTextKind::SelectedSource => paragraph.selected_source(),
198 };
199 if !block_text.is_empty() {
200 text.push_str(&block_text);
201 text.push('\n');
202 }
203 }
204 BlockNode::Heading {
205 level, children, ..
206 } => {
207 let block_text = match kind {
208 BlockTextKind::All => children.text(),
209 BlockTextKind::Selected => children.selected_text(),
210 BlockTextKind::SelectedSource => children.selected_source(),
211 };
212 if !block_text.is_empty() {
213 if matches!(kind, BlockTextKind::SelectedSource) {
216 text.push_str(&"#".repeat(*level as usize));
217 text.push(' ');
218 }
219 text.push_str(&block_text);
220 text.push('\n');
221 }
222 }
223 BlockNode::List {
224 children,
225 ordered,
226 start,
227 ..
228 } => {
229 if matches!(kind, BlockTextKind::SelectedSource) {
230 text.push_str(&list_selected_source(children, *ordered, *start, ""));
233 } else {
234 text.push_str(&Self::children_text(children, kind));
235 }
236 }
237 BlockNode::ListItem { children, .. } => {
238 text.push_str(&Self::children_text(children, kind));
239 }
240 BlockNode::Blockquote { children, .. } => {
241 let block_text = Self::children_text(children, kind);
242
243 if !block_text.is_empty() {
244 if matches!(kind, BlockTextKind::SelectedSource) {
245 let quoted = block_text
248 .trim_end_matches('\n')
249 .lines()
250 .map(|line| {
251 if line.is_empty() {
252 ">".to_string()
253 } else {
254 format!("> {}", line)
255 }
256 })
257 .collect::<Vec<_>>()
258 .join("\n");
259 text.push_str("ed);
260 } else {
261 text.push_str(&block_text);
262 }
263 text.push('\n');
264 }
265 }
266 BlockNode::Table(table) => {
267 if matches!(kind, BlockTextKind::SelectedSource) {
268 let block_text = table_selected_source(table);
269 if !block_text.is_empty() {
270 text.push_str(&block_text);
271 text.push('\n');
272 }
273 } else {
274 let mut block_text = String::new();
275 for row in table.children.iter() {
276 let mut row_texts = vec![];
277 for cell in row.children.iter() {
278 row_texts.push(match kind {
279 BlockTextKind::All => cell.children.text(),
280 _ => cell.children.selected_text(),
282 });
283 }
284 if !row_texts.is_empty() {
285 block_text.push_str(&row_texts.join(" "));
286 block_text.push('\n');
287 }
288 }
289
290 if !block_text.is_empty() {
291 text.push_str(&block_text);
292 text.push('\n');
293 }
294 }
295 }
296 BlockNode::CodeBlock(code_block) => {
297 let block_text = match kind {
298 BlockTextKind::All => code_block.text(),
299 BlockTextKind::Selected => code_block.selected_text(),
300 BlockTextKind::SelectedSource => code_block.selected_source(),
301 };
302 if !block_text.is_empty() {
303 text.push_str(&block_text);
304 text.push('\n');
305 }
306 }
307 BlockNode::Custom(node) => {
308 if let BlockTextKind::All = kind {
309 let content = node.as_text();
310 if !content.is_empty() {
311 text.push_str(content);
312 text.push('\n');
313 }
314 }
315 }
316 BlockNode::Definition { .. }
317 | BlockNode::Break { .. }
318 | BlockNode::HorizontalRule { .. }
319 | BlockNode::Unknown { .. } => {}
320 }
321
322 text
323 }
324
325 fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
326 let mut text = String::new();
327 for child in children.iter() {
328 text.push_str(&child.text_by_kind(kind));
329 }
330
331 text
332 }
333
334 pub(super) fn has_selection(&self) -> bool {
345 match self {
346 BlockNode::Root { children, .. }
347 | BlockNode::Blockquote { children, .. }
348 | BlockNode::List { children, .. }
349 | BlockNode::ListItem { children, .. } => {
350 children.iter().any(|child| child.has_selection())
351 }
352 BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
353 BlockNode::Heading { children, .. } => children.has_selection(),
354 BlockNode::Table(table) => table.children.iter().any(|row| {
355 row.children
356 .iter()
357 .any(|cell| cell.children.has_selection())
358 }),
359 BlockNode::CodeBlock(code_block) => code_block.has_selection(),
360 BlockNode::Custom { .. }
361 | BlockNode::Definition { .. }
362 | BlockNode::Break { .. }
363 | BlockNode::HorizontalRule { .. }
364 | BlockNode::Unknown { .. } => false,
365 }
366 }
367
368 pub(super) fn clear_selection(&self) {
369 match self {
370 BlockNode::Root { children, .. }
371 | BlockNode::Blockquote { children, .. }
372 | BlockNode::List { children, .. }
373 | BlockNode::ListItem { children, .. } => {
374 for child in children.iter() {
375 child.clear_selection();
376 }
377 }
378 BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
379 BlockNode::Heading { children, .. } => children.clear_selection(),
380 BlockNode::Table(table) => {
381 for row in table.children.iter() {
382 for cell in row.children.iter() {
383 cell.children.clear_selection();
384 }
385 }
386 }
387 BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
388 BlockNode::Custom { .. }
389 | BlockNode::Definition { .. }
390 | BlockNode::Break { .. }
391 | BlockNode::HorizontalRule { .. }
392 | BlockNode::Unknown { .. } => {}
393 }
394 }
395}
396
397#[allow(unused)]
398#[derive(Debug, Default, Clone, PartialEq)]
399pub struct LinkMark {
400 pub url: SharedString,
401 pub identifier: Option<SharedString>,
403 pub title: Option<SharedString>,
404}
405
406#[derive(Debug, Default, Clone, PartialEq)]
407pub struct TextMark {
408 pub bold: bool,
409 pub italic: bool,
410 pub strikethrough: bool,
411 pub underline: bool,
412 pub code: bool,
413 pub highlight: Option<Hsla>,
417 pub link: Option<LinkMark>,
418}
419
420impl TextMark {
421 pub fn bold(mut self) -> Self {
422 self.bold = true;
423 self
424 }
425
426 pub fn italic(mut self) -> Self {
427 self.italic = true;
428 self
429 }
430
431 pub fn strikethrough(mut self) -> Self {
432 self.strikethrough = true;
433 self
434 }
435
436 pub fn underline(mut self) -> Self {
437 self.underline = true;
438 self
439 }
440
441 pub fn code(mut self) -> Self {
442 self.code = true;
443 self
444 }
445
446 pub fn highlight(mut self, color: Hsla) -> Self {
448 self.highlight = Some(color);
449 self
450 }
451
452 pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
453 self.link = Some(link.into());
454 self
455 }
456
457 pub fn merge(&mut self, other: TextMark) {
458 self.bold |= other.bold;
459 self.italic |= other.italic;
460 self.strikethrough |= other.strikethrough;
461 self.underline |= other.underline;
462 self.code |= other.code;
463 if other.highlight.is_some() {
464 self.highlight = other.highlight;
465 }
466 if let Some(link) = other.link {
467 self.link = Some(link);
468 }
469 }
470}
471
472#[derive(Debug, Default, Copy, Clone, PartialEq)]
474pub struct Span {
475 pub start: usize,
476 pub end: usize,
477}
478
479#[allow(unused)]
480#[derive(Default, Clone)]
481pub struct ImageNode {
482 pub url: SharedUri,
483 pub link: Option<LinkMark>,
484 pub title: Option<SharedString>,
485 pub alt: Option<SharedString>,
486 pub width: Option<DefiniteLength>,
487 pub height: Option<DefiniteLength>,
488 pub(crate) span: Option<Span>,
489 pub(super) embedded: OnceLock<Option<Arc<Image>>>,
492}
493
494impl ImageNode {
495 pub fn title(&self) -> String {
496 self.title
497 .clone()
498 .unwrap_or_else(|| self.alt.clone().unwrap_or_default())
499 .to_string()
500 }
501
502 pub(super) fn source(&self) -> ImageSource {
510 match self.embedded.get_or_init(|| data_url_image(&self.url)) {
511 Some(image) => ImageSource::Image(image.clone()),
512 None => self.url.clone().into(),
513 }
514 }
515}
516
517impl std::fmt::Debug for ImageNode {
518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519 f.debug_struct("ImageNode")
520 .field("url", &self.url)
521 .field("link", &self.link)
522 .field("title", &self.title)
523 .field("alt", &self.alt)
524 .field("width", &self.width)
525 .field("height", &self.height)
526 .field("span", &self.span)
527 .finish()
528 }
529}
530
531impl PartialEq for ImageNode {
532 fn eq(&self, other: &Self) -> bool {
533 self.url == other.url
534 && self.link == other.link
535 && self.title == other.title
536 && self.alt == other.alt
537 && self.width == other.width
538 && self.height == other.height
539 && self.span == other.span
540 }
541}
542
543#[derive(Debug, Clone, PartialEq)]
544pub(crate) struct SourceSegment {
545 pub(crate) rendered: Range<usize>,
546 pub(crate) source: Range<usize>,
547 pub(crate) linear: bool,
552}
553
554pub(crate) enum SourceRangeSelection {
555 Unselected,
556 Mapped(Range<usize>),
557 Unmapped,
558}
559
560impl SourceRangeSelection {
561 pub(crate) fn merge(&mut self, other: Self) {
562 match (&mut *self, other) {
563 (_, Self::Unselected) => {}
564 (_, Self::Unmapped) => *self = Self::Unmapped,
565 (Self::Unselected, mapped @ Self::Mapped(_)) => *self = mapped,
566 (Self::Mapped(selected), Self::Mapped(range)) => {
567 selected.start = selected.start.min(range.start);
568 selected.end = selected.end.max(range.end);
569 }
570 (Self::Unmapped, Self::Mapped(_)) => {}
571 }
572 }
573
574 pub(crate) fn into_range(self) -> Option<Range<usize>> {
575 match self {
576 Self::Mapped(range) => Some(range),
577 Self::Unselected | Self::Unmapped => None,
578 }
579 }
580}
581
582fn source_range_for_segments(
583 segments: &[SourceSegment],
584 selection: Range<usize>,
585) -> Option<Range<usize>> {
586 fn mapped_source_start(segment: &SourceSegment, rendered_start: usize) -> usize {
587 if segment.linear {
588 segment.source.start + rendered_start.saturating_sub(segment.rendered.start)
589 } else {
590 segment.source.start
591 }
592 }
593
594 fn mapped_source_end(segment: &SourceSegment, rendered_end: usize) -> usize {
595 if segment.linear {
596 segment.source.start
597 + rendered_end
598 .min(segment.rendered.end)
599 .saturating_sub(segment.rendered.start)
600 } else {
601 segment.source.end
602 }
603 }
604
605 if selection.start >= selection.end {
606 return None;
607 }
608
609 let mut overlapping = segments.iter().filter(|segment| {
610 segment.rendered.start < selection.end && segment.rendered.end > selection.start
611 });
612 let first = overlapping.next()?;
613 if first.rendered.start > selection.start {
614 return None;
615 }
616 let mut rendered_end = first.rendered.end;
617 let source_start = mapped_source_start(first, selection.start);
618 let mut source_end = mapped_source_end(first, selection.end);
619 for segment in overlapping {
620 if segment.rendered.start > rendered_end {
621 return None;
622 }
623 rendered_end = rendered_end.max(segment.rendered.end);
624 source_end = mapped_source_end(segment, selection.end);
625 }
626 (rendered_end >= selection.end).then_some(source_start..source_end)
627}
628
629#[derive(Default, Clone, Debug)]
630pub(crate) struct InlineNode {
631 pub(crate) text: SharedString,
633 pub(crate) image: Option<ImageNode>,
634 pub(crate) custom: Option<MarkdownNode>,
635 custom_selection: Arc<Mutex<bool>>,
636 pub(crate) marks: Vec<(Range<usize>, TextMark)>,
638 pub(crate) source_segments: Vec<SourceSegment>,
640
641 pub(super) state: Arc<Mutex<InlineState>>,
642}
643
644impl PartialEq for InlineNode {
645 fn eq(&self, other: &Self) -> bool {
646 self.text == other.text
647 && self.image == other.image
648 && self.custom == other.custom
649 && self.marks == other.marks
650 && self.source_segments == other.source_segments
651 }
652}
653
654pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
661 if text.is_empty() {
662 return String::new();
663 }
664
665 let mut out = text.to_string();
666 if mark.code {
667 out = format!("`{}`", out);
668 }
669 if mark.italic {
670 out = format!("*{}*", out);
671 }
672 if mark.bold {
673 out = format!("**{}**", out);
674 }
675 if mark.strikethrough {
676 out = format!("~~{}~~", out);
677 }
678 if mark.underline {
679 out = format!("<u>{}</u>", out);
682 }
683 if mark.highlight.is_some() {
684 out = format!("=={}==", out);
685 }
686 if let Some(link) = &mark.link {
687 out = match &link.title {
688 Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
689 None => format!("[{}]({})", out, link.url),
690 };
691 }
692 out
693}
694
695#[derive(Default)]
699struct MarkdownSource {
700 pieces: Vec<(String, Vec<TextMark>)>,
701}
702
703impl MarkdownSource {
704 fn push_str(&mut self, source: &str) {
705 self.push_marked(source, &TextMark::default());
706 }
707
708 fn push_marked(&mut self, source: &str, mark: &TextMark) {
709 if source.is_empty() {
710 return;
711 }
712 let mut layers = Vec::new();
715 if let Some(link) = &mark.link {
716 layers.push(TextMark {
717 link: Some(link.clone()),
718 ..Default::default()
719 });
720 }
721 if let Some(highlight) = mark.highlight {
722 layers.push(TextMark {
723 highlight: Some(highlight),
724 ..Default::default()
725 });
726 }
727 if mark.underline {
728 layers.push(TextMark::default().underline());
729 }
730 if mark.strikethrough {
731 layers.push(TextMark::default().strikethrough());
732 }
733 if mark.bold {
734 layers.push(TextMark::default().bold());
735 }
736 if mark.italic {
737 layers.push(TextMark::default().italic());
738 }
739 if mark.code {
740 layers.push(TextMark::default().code());
741 }
742 self.pieces.push((source.to_string(), layers));
743 }
744
745 fn push_text(
746 &mut self,
747 text: &str,
748 marks: &[(Range<usize>, TextMark)],
749 selection: Range<usize>,
750 ) {
751 let start = selection.start.min(text.len());
752 let end = selection.end.min(text.len());
753 if start >= end {
754 return;
755 }
756 let mut cursor = start;
757 for (range, mark) in marks {
758 let lo = range.start.max(start);
759 let hi = range.end.min(end);
760 if lo >= hi {
761 continue;
762 }
763 if cursor < lo {
764 self.push_str(&text[cursor..lo]);
765 }
766 self.push_marked(&text[lo..hi], mark);
767 cursor = hi;
768 }
769 if cursor < end {
770 self.push_str(&text[cursor..end]);
771 }
772 }
773
774 fn push_object(&mut self, node: &InlineNode) {
775 let mut mark = TextMark::default();
776 for (_, layer) in &node.marks {
777 mark.merge(layer.clone());
778 }
779 self.push_marked(&node.custom.as_ref().unwrap().to_markdown(), &mark);
780 }
781
782 fn is_empty(&self) -> bool {
783 self.pieces.is_empty()
784 }
785
786 fn finish(mut self) -> String {
787 fn write(pieces: &mut [(String, Vec<TextMark>)]) -> String {
788 let mut out = String::new();
789 let mut offset = 0;
790 while offset < pieces.len() {
791 let Some((mark, count)) = pieces[offset]
792 .1
793 .iter()
794 .map(|mark| {
795 let count = pieces[offset..]
796 .iter()
797 .take_while(|(_, layers)| layers.contains(mark))
798 .count();
799 (mark.clone(), count)
800 })
801 .reduce(|best, candidate| {
802 if candidate.1 > best.1 {
803 candidate
804 } else {
805 best
806 }
807 })
808 else {
809 out.push_str(&pieces[offset].0);
810 offset += 1;
811 continue;
812 };
813 let group = &mut pieces[offset..offset + count];
814 for (_, layers) in group.iter_mut() {
815 layers.retain(|layer| layer != &mark);
816 }
817 let content = write(group);
818 out.push_str(&wrap_with_mark(&content, &mark));
819 offset += count;
820 }
821 out
822 }
823 write(&mut self.pieces)
824 }
825}
826
827#[derive(Default)]
830struct RunSelection {
831 emitted: bool,
832 at_start: bool,
833 at_end: bool,
834}
835
836fn emit_run(
840 state: &Arc<Mutex<InlineState>>,
841 run: &[(usize, &InlineNode)],
842 pending_images: &mut Vec<String>,
843 out: &mut MarkdownSource,
844) -> RunSelection {
845 let mut selected = RunSelection::default();
846 let Ok(state) = state.lock() else {
847 return selected;
848 };
849 let Some(selection) = &state.selection else {
850 return selected;
851 };
852 if selection.start >= selection.end {
853 return selected;
854 }
855
856 selected.at_start = selection.start == 0;
857 selected.at_end = selection.end >= state.text.len();
858
859 for (start, child) in run {
860 let end = start + child.text.len();
861 let lo = selection.start.max(*start);
862 let hi = selection.end.min(end);
863 if lo >= hi {
864 continue;
865 }
866
867 if !selected.emitted {
868 if selected.at_start {
869 out.push_str(&pending_images.join(""));
870 }
871 pending_images.clear();
872 }
873 selected.emitted = true;
874
875 out.push_text(&child.text, &child.marks, (lo - start)..(hi - start));
876 }
877
878 selected
879}
880
881fn image_markdown(image: &ImageNode) -> String {
883 let alt = image.alt.clone().unwrap_or_default();
884 let title = image
885 .title
886 .clone()
887 .map_or(String::new(), |title| format!(" \"{}\"", title));
888 format!("", alt, image.url, title)
889}
890
891#[cfg(test)]
900pub(crate) fn reconstruct_markdown(
901 text: &str,
902 marks: &[(Range<usize>, TextMark)],
903 selection: Range<usize>,
904) -> String {
905 let mut source = MarkdownSource::default();
906 source.push_text(text, marks, selection);
907 source.finish()
908}
909
910fn table_selected_source(table: &Table) -> String {
917 let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
918
919 let any_selected = table.children.iter().any(|row| {
920 row.children
921 .iter()
922 .any(|cell| !cell_source(cell).trim().is_empty())
923 });
924 if !any_selected {
925 return String::new();
926 }
927
928 let mut lines: Vec<String> = Vec::new();
929 for (row_ix, row) in table.children.iter().enumerate() {
930 let cells: Vec<String> = row
931 .children
932 .iter()
933 .map(|cell| cell_source(cell).trim().to_string())
934 .collect();
935 lines.push(format!("| {} |", cells.join(" | ")));
936
937 if row_ix == 0 {
940 let aligns: Vec<String> = (0..row.children.len())
941 .map(|ix| {
942 match table.column_align(ix) {
943 ColumnumnAlign::Left => ":--",
944 ColumnumnAlign::Center => ":-:",
945 ColumnumnAlign::Right => "--:",
946 }
947 .to_string()
948 })
949 .collect();
950 lines.push(format!("| {} |", aligns.join(" | ")));
951 }
952 }
953
954 lines.join("\n")
955}
956
957fn list_selected_source(
967 children: &[BlockNode],
968 ordered: bool,
969 start: Option<u32>,
970 indent: &str,
971) -> String {
972 let mut out = String::new();
973 let mut item_ix = 0usize;
974
975 for child in children {
976 let BlockNode::ListItem {
977 children: item_children,
978 checked,
979 ..
980 } = child
981 else {
982 continue;
983 };
984
985 let marker = if ordered {
986 format!("{}. ", ordered_list_ordinal(start, item_ix))
987 } else {
988 "- ".to_string()
989 };
990 let checkbox = match checked {
991 Some(true) => "[x] ",
992 Some(false) => "[ ] ",
993 None => "",
994 };
995 let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
996
997 let mut content = String::new();
1000 let mut nested = String::new();
1001 for sub in item_children {
1002 if let BlockNode::List {
1003 children: sub_children,
1004 ordered: sub_ordered,
1005 start: sub_start,
1006 ..
1007 } = sub
1008 {
1009 nested.push_str(&list_selected_source(
1010 sub_children,
1011 *sub_ordered,
1012 *sub_start,
1013 &child_indent,
1014 ));
1015 } else {
1016 content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
1017 }
1018 }
1019 let content = content.trim_end_matches('\n');
1020
1021 if content.is_empty() && nested.is_empty() {
1022 item_ix += 1;
1023 continue;
1024 }
1025
1026 if content.is_empty() {
1027 out.push_str(indent);
1029 out.push_str(&marker);
1030 out.push_str(checkbox.trim_end());
1031 out.push('\n');
1032 } else {
1033 let mut lines = content.lines();
1036 if let Some(first) = lines.next() {
1037 out.push_str(indent);
1038 out.push_str(&marker);
1039 out.push_str(checkbox);
1040 out.push_str(first);
1041 out.push('\n');
1042 }
1043 for line in lines {
1044 out.push_str(&child_indent);
1045 out.push_str(line);
1046 out.push('\n');
1047 }
1048 }
1049 out.push_str(&nested);
1050 item_ix += 1;
1051 }
1052
1053 out
1054}
1055
1056impl InlineNode {
1057 pub(crate) fn new(text: impl Into<SharedString>) -> Self {
1058 Self {
1059 text: text.into(),
1060 image: None,
1061 custom: None,
1062 custom_selection: Arc::default(),
1063 marks: vec![],
1064 source_segments: vec![],
1065 state: Arc::new(Mutex::new(InlineState::default())),
1066 }
1067 }
1068
1069 pub(crate) fn custom(node: MarkdownNode) -> Self {
1070 let mut this = Self::new(node.as_text().to_string());
1071 this.custom = Some(node);
1072 this
1073 }
1074
1075 pub(crate) fn image(image: ImageNode) -> Self {
1076 let mut this = Self::new("");
1077 this.image = Some(image);
1078 this
1079 }
1080
1081 pub(crate) fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
1082 self.marks = marks;
1083 self
1084 }
1085
1086 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
1087 self.source_segments = source_segments;
1088 self
1089 }
1090
1091 fn selected_source_range(&self, selection: Range<usize>) -> Option<Range<usize>> {
1092 source_range_for_segments(&self.source_segments, selection)
1093 }
1094}
1095
1096#[derive(Debug, Clone, Default)]
1101pub(crate) struct Paragraph {
1102 pub(super) span: Option<Span>,
1103 pub(super) children: Vec<InlineNode>,
1104 pub(super) link_refs: HashMap<SharedString, SharedString>,
1108
1109 pub(crate) state: Arc<Mutex<InlineState>>,
1110 pub(super) render_cache: ParagraphRenderCache,
1113}
1114
1115#[derive(Default)]
1123pub(super) struct ParagraphRenderCache(Mutex<Option<Arc<ParagraphRender>>>);
1124
1125impl Clone for ParagraphRenderCache {
1126 fn clone(&self) -> Self {
1127 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1128 Self(Mutex::new(cached))
1129 }
1130}
1131
1132impl std::fmt::Debug for ParagraphRenderCache {
1133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1134 f.write_str("ParagraphRenderCache")
1135 }
1136}
1137
1138struct ParagraphRender {
1148 style: Arc<TextViewStyle>,
1149 mono_font: SharedString,
1150 text: SharedString,
1151 highlights: Vec<(Range<usize>, InlineHighlight)>,
1152 links: Vec<(Range<usize>, LinkMark)>,
1154}
1155
1156impl PartialEq for Paragraph {
1157 fn eq(&self, other: &Self) -> bool {
1158 self.span == other.span
1159 && self.children == other.children
1160 && self.link_refs == other.link_refs
1161 }
1162}
1163
1164impl Paragraph {
1165 pub(crate) fn new(text: String) -> Self {
1166 Self {
1167 span: None,
1168 children: vec![InlineNode::new(&text)],
1169 link_refs: HashMap::new(),
1170 state: Arc::new(Mutex::new(InlineState::default())),
1171 render_cache: ParagraphRenderCache::default(),
1172 }
1173 }
1174
1175 fn plain_render(
1178 &self,
1179 node_cx: &NodeContext,
1180 cx: &App,
1181 ) -> (
1182 SharedString,
1183 Vec<(Range<usize>, InlineHighlight)>,
1184 Vec<(Range<usize>, LinkMark)>,
1185 ) {
1186 let mono_font = cx.theme().tokens.typography.mono.clone();
1187 if let Ok(cache) = self.render_cache.0.lock()
1188 && let Some(cached) = cache.as_ref()
1189 && (Arc::ptr_eq(&cached.style, &node_cx.style) || *cached.style == *node_cx.style)
1190 && cached.mono_font == mono_font
1191 {
1192 return (
1193 cached.text.clone(),
1194 cached.highlights.clone(),
1195 cached.links.clone(),
1196 );
1197 }
1198
1199 let mut text = String::new();
1200 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1201 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1202 let mut offset = 0;
1203 for inline_node in &self.children {
1204 let text_len = inline_node.text.len();
1205 text.push_str(&inline_node.text);
1206 let mut node_highlights = vec![];
1207 for (range, style) in &inline_node.marks {
1208 let inner_range = (offset + range.start)..(offset + range.end);
1209 let mut highlight = mark_highlight(style, node_cx, cx);
1210 if let Some(link_mark) = style.link.clone() {
1211 highlight.style.color = Some(node_cx.style.link());
1212 highlight.style.underline = Some(gpui::UnderlineStyle {
1213 thickness: gpui::px(1.),
1214 ..Default::default()
1215 });
1216 links.push((inner_range.clone(), link_mark));
1217 }
1218 node_highlights.push((inner_range, highlight));
1219 }
1220 highlights = combine_highlights(highlights, node_highlights);
1221 offset += text_len;
1222 }
1223 let text = SharedString::from(text);
1224 if let Ok(mut cache) = self.render_cache.0.lock() {
1225 *cache = Some(Arc::new(ParagraphRender {
1226 style: node_cx.style.clone(),
1227 mono_font,
1228 text: text.clone(),
1229 highlights: highlights.clone(),
1230 links: links.clone(),
1231 }));
1232 }
1233 (text, highlights, links)
1234 }
1235
1236 pub(super) fn selected_text(&self) -> String {
1237 let mut text = String::new();
1238
1239 for c in self.children.iter() {
1240 let Ok(state) = c.state.lock() else {
1241 continue;
1242 };
1243 if let Some(selection) = &state.selection {
1244 text.push_str(&state.text[selection.start..selection.end]);
1245 }
1246 if let Some(custom) = &c.custom
1247 && c.custom_selection.lock().is_ok_and(|selected| *selected)
1248 {
1249 text.push_str(custom.as_text());
1250 }
1251 }
1252
1253 if let Ok(state) = self.state.lock()
1254 && let Some(selection) = &state.selection
1255 {
1256 text.push_str(&state.text[selection.start..selection.end]);
1257 }
1258
1259 text
1260 }
1261
1262 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1264 let mut selected = SourceRangeSelection::Unselected;
1265 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1266 let mut offset = 0;
1267 let mut pending_images: Vec<Option<Span>> = Vec::new();
1268 let mut enters_image = true;
1269
1270 let include_run = |state: &Arc<Mutex<InlineState>>,
1271 run: &[(usize, &InlineNode)]|
1272 -> (SourceRangeSelection, RunSelection) {
1273 let Ok(state) = state.lock() else {
1274 return (SourceRangeSelection::Unmapped, RunSelection::default());
1275 };
1276 let Some(selection) = state.selection else {
1277 return (SourceRangeSelection::Unselected, RunSelection::default());
1278 };
1279 if selection.start >= selection.end {
1280 return (SourceRangeSelection::Unselected, RunSelection::default());
1281 }
1282
1283 let mut run_selection = RunSelection {
1284 at_start: selection.start == 0,
1285 at_end: selection.end >= state.text.len(),
1286 ..Default::default()
1287 };
1288 let mut mapped = SourceRangeSelection::Unselected;
1289 let mut rendered_end = selection.start;
1290 for (start, child) in run {
1291 let end = start + child.text.len();
1292 let lo = selection.start.max(*start);
1293 let hi = selection.end.min(end);
1294 if lo >= hi {
1295 continue;
1296 }
1297 run_selection.emitted = true;
1298 if lo > rendered_end {
1299 return (SourceRangeSelection::Unmapped, run_selection);
1300 }
1301 let Some(range) = child.selected_source_range((lo - start)..(hi - start)) else {
1302 return (SourceRangeSelection::Unmapped, run_selection);
1303 };
1304 mapped.merge(SourceRangeSelection::Mapped(range));
1305 rendered_end = rendered_end.max(hi);
1306 }
1307 if rendered_end < selection.end {
1308 (SourceRangeSelection::Unmapped, run_selection)
1309 } else {
1310 (mapped, run_selection)
1311 }
1312 };
1313
1314 let merge_images = |selected: &mut SourceRangeSelection, images: &mut Vec<Option<Span>>| {
1315 for span in images.drain(..) {
1316 selected.merge(
1317 span.map(|span| SourceRangeSelection::Mapped(span.start..span.end))
1318 .unwrap_or(SourceRangeSelection::Unmapped),
1319 );
1320 }
1321 };
1322
1323 for child in &self.children {
1324 if child.custom.is_some() {
1325 let (run_range, run_selection) = include_run(&child.state, &run);
1326 if run_selection.emitted && run_selection.at_start {
1327 merge_images(&mut selected, &mut pending_images);
1328 }
1329 selected.merge(run_range);
1330
1331 match child.custom_selection.lock() {
1332 Ok(value) if *value => {
1333 if run.is_empty() || (run_selection.emitted && run_selection.at_end) {
1334 merge_images(&mut selected, &mut pending_images);
1335 }
1336 selected.merge(
1337 child
1338 .custom
1339 .as_ref()
1340 .and_then(MarkdownNode::source_range)
1341 .map(SourceRangeSelection::Mapped)
1342 .unwrap_or(SourceRangeSelection::Unmapped),
1343 );
1344 enters_image = true;
1345 }
1346 Ok(_) => enters_image = false,
1347 Err(_) => {
1348 selected.merge(SourceRangeSelection::Unmapped);
1349 enters_image = false;
1350 }
1351 }
1352 pending_images.clear();
1353 run.clear();
1354 offset = 0;
1355 continue;
1356 }
1357 if let Some(image) = &child.image {
1358 let run_before = !run.is_empty();
1359 let (run_range, run_selection) = include_run(&child.state, &run);
1360 if run_selection.emitted && run_selection.at_start {
1361 merge_images(&mut selected, &mut pending_images);
1362 }
1363 selected.merge(run_range);
1364 if run_before {
1365 enters_image = run_selection.emitted && run_selection.at_end;
1366 }
1367 if enters_image {
1368 pending_images.push(image.span);
1369 } else {
1370 pending_images.clear();
1371 }
1372 run.clear();
1373 offset = 0;
1374 continue;
1375 }
1376 run.push((offset, child));
1377 offset += child.text.len();
1378 }
1379
1380 let (trailing_range, trailing) = include_run(&self.state, &run);
1381 if trailing.emitted && trailing.at_start {
1382 merge_images(&mut selected, &mut pending_images);
1383 }
1384 selected.merge(trailing_range);
1385 if !trailing.emitted
1386 && enters_image
1387 && !matches!(selected, SourceRangeSelection::Unselected)
1388 {
1389 merge_images(&mut selected, &mut pending_images);
1390 }
1391 selected
1392 }
1393
1394 pub(super) fn selected_source(&self) -> String {
1414 let mut source = MarkdownSource::default();
1415 let mut pending_images: Vec<String> = Vec::new();
1416 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1417 let mut offset = 0;
1418 let mut enters_image = true;
1419
1420 for child in self.children.iter() {
1421 if child.custom.is_some() {
1422 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1423 let object_selected = child
1424 .custom_selection
1425 .lock()
1426 .is_ok_and(|selected| *selected);
1427 if object_selected {
1428 if run.is_empty() || (selected.emitted && selected.at_end) {
1429 source.push_str(&pending_images.join(""));
1430 }
1431 source.push_object(child);
1432 }
1433 pending_images.clear();
1434 enters_image = object_selected;
1435 run.clear();
1436 offset = 0;
1437 continue;
1438 }
1439 let Some(image) = &child.image else {
1440 run.push((offset, child));
1441 offset += child.text.len();
1442 continue;
1443 };
1444
1445 let run_before = !run.is_empty();
1447 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1448 if run_before {
1449 enters_image = selected.emitted && selected.at_end;
1450 }
1451 if enters_image {
1452 pending_images.push(image_markdown(image));
1453 } else {
1454 pending_images.clear();
1455 }
1456
1457 run.clear();
1458 offset = 0;
1459 }
1460
1461 let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
1462 if !trailing.emitted && enters_image && !source.is_empty() {
1464 source.push_str(&pending_images.join(""));
1465 }
1466
1467 source.finish()
1468 }
1469
1470 pub(super) fn text(&self) -> String {
1471 let mut text = String::new();
1472 for node in self.children.iter() {
1473 text.push_str(&node.text);
1474 }
1475 text
1476 }
1477
1478 pub(super) fn has_selection(&self) -> bool {
1482 self.children.iter().any(|c| {
1483 c.state.lock().is_ok_and(|state| state.selection.is_some())
1484 || c.custom_selection.lock().is_ok_and(|selected| *selected)
1485 }) || self
1486 .state
1487 .lock()
1488 .is_ok_and(|state| state.selection.is_some())
1489 }
1490
1491 pub(super) fn clear_selection(&self) {
1492 for c in self.children.iter() {
1493 if let Ok(mut selected) = c.custom_selection.lock() {
1494 *selected = false;
1495 }
1496 if let Ok(mut state) = c.state.lock() {
1497 state.selection = None;
1498 }
1499 }
1500
1501 if let Ok(mut state) = self.state.lock() {
1502 state.selection = None;
1503 }
1504 }
1505}
1506
1507#[derive(Debug, Clone, Default, PartialEq)]
1508pub(crate) struct Table {
1509 pub(crate) children: Vec<TableRow>,
1510 pub(crate) column_aligns: Vec<ColumnumnAlign>,
1511 pub(crate) span: Option<Span>,
1512 pub(crate) table_data_cache: TableDataCache,
1515 pub(crate) column_widths_cache: TableColumnWidthsCache,
1516}
1517
1518#[derive(Default)]
1526pub(crate) struct TableDataCache(Mutex<Option<Arc<TableData>>>);
1527
1528impl Clone for TableDataCache {
1529 fn clone(&self) -> Self {
1530 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1531 Self(Mutex::new(cached))
1532 }
1533}
1534
1535impl PartialEq for TableDataCache {
1536 fn eq(&self, _: &Self) -> bool {
1537 true
1538 }
1539}
1540
1541impl std::fmt::Debug for TableDataCache {
1542 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1543 f.write_str("TableDataCache")
1544 }
1545}
1546
1547#[derive(Default)]
1550pub(crate) struct TableColumnWidthsCache(Mutex<Option<Arc<TableColumnWidths>>>);
1551
1552impl Clone for TableColumnWidthsCache {
1553 fn clone(&self) -> Self {
1554 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1555 Self(Mutex::new(cached))
1556 }
1557}
1558
1559impl PartialEq for TableColumnWidthsCache {
1560 fn eq(&self, _: &Self) -> bool {
1561 true
1562 }
1563}
1564
1565impl std::fmt::Debug for TableColumnWidthsCache {
1566 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1567 f.write_str("TableColumnWidthsCache")
1568 }
1569}
1570
1571struct TableColumnWidths {
1572 text_system: Weak<gpui::WindowTextSystem>,
1573 text_style: gpui::TextStyle,
1574 rem_size: Pixels,
1575 mono_family: SharedString,
1576 inline_code: HighlightStyle,
1577 widths: Vec<f32>,
1579 custom_cells: Vec<(usize, usize)>,
1580}
1581
1582#[derive(Debug, Clone, Default, PartialEq)]
1585pub struct TableData {
1586 pub headers: Vec<String>,
1588 pub rows: Vec<Vec<String>>,
1591 pub markdown: String,
1593 pub span: Option<Range<usize>>,
1600}
1601
1602impl Table {
1603 pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
1604 self.column_aligns.get(index).copied().unwrap_or_default()
1605 }
1606
1607 pub(crate) fn to_markdown(&self) -> String {
1614 let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
1615
1616 for (row_ix, row) in self.children.iter().enumerate() {
1617 let cells: Vec<String> = row
1618 .children
1619 .iter()
1620 .map(|cell| {
1621 cell.children
1622 .to_markdown()
1623 .trim()
1624 .replace('\n', " ")
1625 .replace('|', "\\|")
1626 })
1627 .collect();
1628 lines.push(format!("| {} |", cells.join(" | ")));
1629
1630 if row_ix == 0 {
1633 let aligns: Vec<String> = (0..row.children.len())
1634 .map(|ix| {
1635 match self.column_align(ix) {
1636 ColumnumnAlign::Left => ":--",
1637 ColumnumnAlign::Center => ":-:",
1638 ColumnumnAlign::Right => "--:",
1639 }
1640 .to_string()
1641 })
1642 .collect();
1643 lines.push(format!("| {} |", aligns.join(" | ")));
1644 }
1645 }
1646
1647 lines.join("\n")
1648 }
1649
1650 pub(crate) fn table_data(&self) -> TableData {
1653 let row_text = |row: &TableRow| {
1654 row.children
1655 .iter()
1656 .map(|cell| cell.children.text().trim().to_string())
1657 .collect::<Vec<_>>()
1658 };
1659
1660 TableData {
1661 headers: self.children.first().map(row_text).unwrap_or_default(),
1662 rows: self.children.iter().skip(1).map(row_text).collect(),
1663 markdown: self.to_markdown(),
1664 span: self.span.map(|span| span.start..span.end),
1665 }
1666 }
1667
1668 fn cached_table_data(&self) -> Arc<TableData> {
1673 if let Ok(cache) = self.table_data_cache.0.lock()
1674 && let Some(cached) = cache.as_ref()
1675 {
1676 return cached.clone();
1677 }
1678
1679 let data = Arc::new(self.table_data());
1680 if let Ok(mut cache) = self.table_data_cache.0.lock() {
1681 *cache = Some(data.clone());
1682 }
1683 data
1684 }
1685}
1686
1687#[derive(Debug, Default, Copy, Clone, PartialEq)]
1688pub(crate) enum ColumnumnAlign {
1689 #[default]
1690 Left,
1691 Center,
1692 Right,
1693}
1694
1695impl From<mdast::AlignKind> for ColumnumnAlign {
1696 fn from(value: mdast::AlignKind) -> Self {
1697 match value {
1698 mdast::AlignKind::None => ColumnumnAlign::Left,
1699 mdast::AlignKind::Left => ColumnumnAlign::Left,
1700 mdast::AlignKind::Center => ColumnumnAlign::Center,
1701 mdast::AlignKind::Right => ColumnumnAlign::Right,
1702 }
1703 }
1704}
1705
1706#[derive(Debug, Clone, Default, PartialEq)]
1707pub(crate) struct TableRow {
1708 pub children: Vec<TableCell>,
1709}
1710
1711#[derive(Debug, Clone, Default, PartialEq)]
1712pub(crate) struct TableCell {
1713 pub children: Paragraph,
1714 pub width: Option<DefiniteLength>,
1715}
1716
1717impl Paragraph {
1718 pub(crate) fn take(&mut self) -> Paragraph {
1719 std::mem::replace(
1720 self,
1721 Paragraph {
1722 span: None,
1723 children: vec![],
1724 link_refs: Default::default(),
1725 state: Arc::new(Mutex::new(InlineState::default())),
1726 render_cache: ParagraphRenderCache::default(),
1727 },
1728 )
1729 }
1730
1731 pub(crate) fn is_image(&self) -> bool {
1732 false
1733 }
1734
1735 pub(crate) fn set_span(&mut self, span: Span) {
1736 self.span = Some(span);
1737 }
1738
1739 pub(crate) fn push_str(&mut self, text: &str) {
1740 self.children.push(
1741 InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
1742 );
1743 self.invalidate_render_cache();
1744 }
1745
1746 pub(crate) fn push(&mut self, text: InlineNode) {
1747 self.children.push(text);
1748 self.invalidate_render_cache();
1749 }
1750
1751 pub(crate) fn push_image(&mut self, image: ImageNode) {
1752 self.children.push(InlineNode::image(image));
1753 self.invalidate_render_cache();
1754 }
1755
1756 fn invalidate_render_cache(&mut self) {
1758 self.render_cache = ParagraphRenderCache::default();
1759 }
1760
1761 pub(crate) fn is_empty(&self) -> bool {
1762 self.children.is_empty()
1763 || self
1764 .children
1765 .iter()
1766 .all(|node| node.text.is_empty() && node.image.is_none())
1767 }
1768
1769 pub(crate) fn text_len(&self) -> usize {
1771 self.children
1772 .iter()
1773 .map(|node| node.text.len())
1774 .sum::<usize>()
1775 }
1776
1777 pub(crate) fn merge(&mut self, other: Self) {
1778 self.children.extend(other.children);
1779 self.invalidate_render_cache();
1780 }
1781}
1782
1783#[derive(Debug, Clone)]
1784pub struct CodeBlock {
1785 lang: Option<SharedString>,
1786 state: Arc<Mutex<InlineState>>,
1787 highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
1788 pub(crate) source_segments: Vec<SourceSegment>,
1791 pub span: Option<Span>,
1792}
1793
1794struct CachedCodeBlockHighlights {
1795 highlighter: Arc<CodeBlockHighlighterFn>,
1796 styles: Vec<(Range<usize>, HighlightStyle)>,
1797}
1798
1799impl std::fmt::Debug for CachedCodeBlockHighlights {
1800 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1801 f.debug_struct("CachedCodeBlockHighlights")
1802 .field("styles", &self.styles)
1803 .finish_non_exhaustive()
1804 }
1805}
1806
1807impl PartialEq for CodeBlock {
1808 fn eq(&self, other: &Self) -> bool {
1809 self.lang == other.lang && self.code() == other.code() && self.span == other.span
1810 }
1811}
1812
1813impl CodeBlock {
1814 pub fn lang(&self) -> Option<SharedString> {
1816 self.lang.clone()
1817 }
1818
1819 pub fn code(&self) -> SharedString {
1821 self.state
1822 .lock()
1823 .map(|state| state.text.clone())
1824 .unwrap_or_default()
1825 }
1826
1827 pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
1833 Self::new(code.into(), lang.map(Into::into), None::<Span>)
1834 }
1835
1836 pub(crate) fn new(
1837 code: SharedString,
1838 lang: Option<SharedString>,
1839 span: Option<impl Into<Span>>,
1840 ) -> Self {
1841 let state = Arc::new(Mutex::new(InlineState::default()));
1842 if let Ok(mut state) = state.lock() {
1843 state.set_text(code);
1844 }
1845
1846 Self {
1847 lang,
1848 state,
1849 highlight_cache: Arc::new(Mutex::new(None)),
1850 source_segments: vec![],
1851 span: span.map(|s| s.into()),
1852 }
1853 }
1854
1855 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
1856 self.source_segments = source_segments;
1857 self
1858 }
1859
1860 #[cfg(test)]
1861 pub(crate) fn set_selection(&self, selection: Range<usize>) {
1862 if let Ok(mut state) = self.state.lock() {
1863 state.selection = Some(selection.into());
1864 }
1865 }
1866
1867 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1868 let Ok(state) = self.state.lock() else {
1869 return SourceRangeSelection::Unmapped;
1870 };
1871 let Some(selection) = state.selection else {
1872 return SourceRangeSelection::Unselected;
1873 };
1874 if selection.start >= selection.end {
1875 return SourceRangeSelection::Unselected;
1876 }
1877 source_range_for_segments(&self.source_segments, selection.start..selection.end)
1878 .map(SourceRangeSelection::Mapped)
1879 .unwrap_or(SourceRangeSelection::Unmapped)
1880 }
1881
1882 fn highlighted_styles(
1883 &self,
1884 highlighter: &Arc<CodeBlockHighlighterFn>,
1885 ) -> Vec<(Range<usize>, HighlightStyle)> {
1886 if let Ok(cache) = self.highlight_cache.lock()
1887 && let Some(cache) = cache.as_ref()
1888 && Arc::ptr_eq(&cache.highlighter, highlighter)
1889 {
1890 return cache.styles.clone();
1891 }
1892
1893 let code_len = self.code().len();
1894 let styles = highlighter(self)
1895 .into_iter()
1896 .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
1897 .collect::<Vec<_>>();
1898 if let Ok(mut cache) = self.highlight_cache.lock() {
1899 *cache = Some(CachedCodeBlockHighlights {
1900 highlighter: highlighter.clone(),
1901 styles: styles.clone(),
1902 });
1903 }
1904 styles
1905 }
1906
1907 pub(super) fn selected_text(&self) -> String {
1908 let mut text = String::new();
1909 if let Ok(state) = self.state.lock()
1910 && let Some(selection) = &state.selection
1911 {
1912 text.push_str(&state.text[selection.start..selection.end]);
1913 }
1914 text
1915 }
1916
1917 pub(super) fn selected_source(&self) -> String {
1924 let code = self.selected_text();
1925 if code.is_empty() {
1926 return String::new();
1927 }
1928 let lang = self.lang.clone().unwrap_or_default();
1929 let code = code.trim_end_matches('\n');
1932 format!("```{}\n{}\n```", lang, code)
1933 }
1934
1935 pub(super) fn text(&self) -> String {
1936 self.state
1937 .lock()
1938 .map(|state| state.text.to_string())
1939 .unwrap_or_default()
1940 }
1941
1942 pub(super) fn has_selection(&self) -> bool {
1946 self.state
1947 .lock()
1948 .is_ok_and(|state| state.selection.is_some())
1949 }
1950
1951 pub(super) fn clear_selection(&self) {
1952 if let Ok(mut state) = self.state.lock() {
1953 state.selection = None;
1954 }
1955 }
1956
1957 fn render(
1958 &self,
1959 options: &NodeRenderOptions,
1960 node_cx: &NodeContext,
1961 window: &mut Window,
1962 cx: &mut App,
1963 ) -> AnyElement {
1964 let style = &node_cx.style;
1965 let leaf_key = self.span.map(|span| TextLeafKey::block(span.start));
1966
1967 let block = div()
1968 .w_full()
1969 .min_w_0()
1970 .p_3()
1971 .bg(style.code_background())
1972 .font_family(cx.theme().tokens.typography.mono.clone())
1973 .text_size(cx.theme().tokens.typography.mono_md.size)
1974 .relative()
1975 .refine_style(&style.code_block())
1976 .child(
1977 Inline::new(
1978 self.state.clone(),
1979 vec![],
1980 fade_highlights(
1981 node_cx
1982 .code_block_highlighter
1983 .as_ref()
1984 .map(|highlighter| self.highlighted_styles(highlighter))
1985 .unwrap_or_default()
1986 .into_iter()
1987 .map(|(range, style)| (range, InlineHighlight::from(style)))
1988 .collect(),
1989 node_cx.stream_fades(leaf_key),
1990 ),
1991 node_cx.link_click_handler.clone(),
1992 )
1993 .range_backgrounds(node_cx.range_backgrounds(leaf_key).to_vec())
1994 .reveal(node_cx.reveal_at(leaf_key, 0, self.code().len())),
1995 );
1996 let actions = node_cx.code_block_actions.clone().map(|actions| {
1997 div()
1998 .id("actions")
1999 .absolute()
2000 .top_2()
2001 .right_2()
2002 .bg(style.code_background())
2003 .rounded(cx.theme().tokens.radius.md)
2004 .child(actions(&self, window, cx))
2005 });
2006 let id = block_element_id("codeblock", self.span, options.ix);
2009 let block = if matches!(style.code_block().overflow.y, Some(Overflow::Scroll)) {
2010 let scroll_handle = window
2011 .use_keyed_state(
2012 block_element_id("codeblock-scroll", self.span, options.ix),
2013 cx,
2014 |_, _| ScrollHandle::default(),
2015 )
2016 .read(cx)
2017 .clone();
2018 div()
2025 .id(id.clone())
2026 .w_full()
2027 .min_w_0()
2028 .relative()
2029 .child(block.id("scroll").track_scroll(&scroll_handle))
2030 .child(ScrollableMask::new(Axis::Vertical, &scroll_handle).id(id))
2031 .child(
2032 div()
2033 .absolute()
2034 .inset_0()
2035 .child(Scrollbar::vertical(&scroll_handle)),
2036 )
2037 .children(actions)
2038 .into_any_element()
2039 } else {
2040 match actions {
2042 Some(actions) => block.id(id).child(actions).into_any_element(),
2043 None => block.into_any_element(),
2044 }
2045 };
2046
2047 gapped(
2048 block,
2049 if options.is_last {
2050 rems(0.)
2051 } else {
2052 style.paragraph_gap()
2053 },
2054 )
2055 }
2056}
2057
2058#[derive(Default, Clone)]
2060pub(crate) struct NodeContext {
2061 pub(crate) offset: usize,
2064 pub(crate) link_refs: HashMap<SharedString, LinkMark>,
2065 pub(crate) style: Arc<TextViewStyle>,
2066 pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
2067 pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
2068 pub(crate) table_actions: Option<Arc<TableActionsFn>>,
2069 pub(crate) image_source: Option<Arc<super::text_view::ImageSourceFn>>,
2070 pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
2071 pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
2072 pub(crate) stream_fade: Option<Arc<StreamFadeFrame>>,
2074 pub(crate) range_highlights: Option<Arc<RangeHighlightFrame>>,
2076 pub(crate) reveal: Option<RevealRequest>,
2078}
2079
2080impl NodeContext {
2081 fn image_source(&self, image: &ImageNode) -> ImageSource {
2082 match &self.image_source {
2083 Some(resolve) => resolve(&image.url),
2084 None => image.source(),
2085 }
2086 }
2087
2088 pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
2089 self.link_refs.insert(identifier, link);
2090 }
2091
2092 fn stream_fades(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, f32)] {
2094 match (&self.stream_fade, key) {
2095 (Some(frame), Some(key)) => frame.fades(key).unwrap_or_default(),
2096 _ => &[],
2097 }
2098 }
2099
2100 fn range_backgrounds(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, Hsla)] {
2103 match (&self.range_highlights, key) {
2104 (Some(frame), Some(key)) => frame.backgrounds(key),
2105 _ => &[],
2106 }
2107 }
2108
2109 fn reveal_at(&self, key: Option<TextLeafKey>, start: usize, end: usize) -> Option<RevealAt> {
2112 self.reveal.as_ref()?.at(key, start, end)
2113 }
2114}
2115
2116impl PartialEq for NodeContext {
2117 fn eq(&self, other: &Self) -> bool {
2118 self.link_refs == other.link_refs && self.style == other.style
2119 }
2122}
2123
2124fn mark_highlight(mark: &TextMark, node_cx: &NodeContext, cx: &App) -> InlineHighlight {
2127 let mut highlight = HighlightStyle::default();
2128 if mark.bold {
2129 highlight.font_weight = Some(FontWeight::BOLD);
2130 }
2131 if mark.italic {
2132 highlight.font_style = Some(FontStyle::Italic);
2133 }
2134 if mark.strikethrough {
2135 highlight.strikethrough = Some(gpui::StrikethroughStyle {
2136 thickness: gpui::px(1.),
2137 ..Default::default()
2138 });
2139 }
2140 if mark.underline {
2141 highlight.underline = Some(gpui::UnderlineStyle {
2142 thickness: gpui::px(1.),
2143 ..Default::default()
2144 });
2145 }
2146 let mut font_family = None;
2147 if mark.code {
2148 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
2149 font_family = Some(cx.theme().tokens.typography.mono.clone());
2150 }
2151 if let Some(color) = mark.highlight {
2152 highlight.background_color = Some(color);
2153 }
2154 InlineHighlight {
2155 style: highlight,
2156 font_family,
2157 font_size_scale: mark.code.then_some(0.875),
2158 }
2159}
2160
2161impl Paragraph {
2162 fn inline_highlights(
2166 &self,
2167 node_cx: &NodeContext,
2168 cx: &App,
2169 ) -> Vec<(Range<usize>, InlineHighlight)> {
2170 let mut highlights = vec![];
2171 let mut offset = 0;
2172 for inline_node in &self.children {
2173 let node_highlights = inline_node
2174 .marks
2175 .iter()
2176 .map(|(range, mark)| {
2177 (
2178 (offset + range.start)..(offset + range.end),
2179 mark_highlight(mark, node_cx, cx),
2180 )
2181 })
2182 .collect::<Vec<_>>();
2183 highlights = combine_highlights(highlights, node_highlights);
2184 offset += inline_node.text.len();
2185 }
2186 highlights
2187 }
2188
2189 fn render(
2193 &self,
2194 fade_key: Option<TextLeafKey>,
2195 node_cx: &NodeContext,
2196 _window: &mut Window,
2197 cx: &mut App,
2198 ) -> AnyElement {
2199 let children = &self.children;
2200 let fades = node_cx.stream_fades(fade_key);
2201 let backgrounds = node_cx.range_backgrounds(fade_key);
2202
2203 if self.should_render_inline_flow() {
2204 return InlineFlow::new(
2205 leaf_element_id(fade_key),
2206 self.inline_flow_items(fade_key, fades, backgrounds, node_cx, cx),
2207 node_cx.link_click_handler.clone(),
2208 )
2209 .into_any_element();
2210 }
2211
2212 let has_image = children.iter().any(|child| child.image.is_some());
2213 if !has_image {
2216 let (text, highlights, mut links) = self.plain_render(node_cx, cx);
2217 if text.is_empty() {
2218 return div().into_any_element();
2219 }
2220 for (_, link_mark) in &mut links {
2221 if let Some(identifier) = link_mark.identifier.as_ref()
2222 && let Some(mark) = node_cx.link_refs.get(identifier)
2223 {
2224 *link_mark = mark.clone();
2225 }
2226 }
2227 let highlights = fade_highlights(highlights, &slice_fades(fades, 0, text.len()));
2228 let backgrounds = slice_backgrounds(backgrounds, 0, text.len());
2229 let reveal = node_cx.reveal_at(fade_key, 0, text.len());
2230 if let Ok(mut state) = self.state.lock() {
2231 state.set_text(text);
2232 }
2233 return Inline::new(
2234 self.state.clone(),
2235 links,
2236 highlights,
2237 node_cx.link_click_handler.clone(),
2238 )
2239 .range_backgrounds(backgrounds)
2240 .reveal(reveal)
2241 .into_any_element();
2242 }
2243
2244 let mut child_nodes: Vec<AnyElement> = vec![];
2245
2246 let mut text = String::new();
2247 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2248 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2249 let mut offset = 0;
2250 let mut consumed = 0;
2253
2254 for (ix, inline_node) in children.iter().enumerate() {
2255 let text_len = inline_node.text.len();
2256 text.push_str(&inline_node.text);
2257
2258 if let Some(image) = &inline_node.image {
2259 if text.len() > 0 {
2260 if let Ok(mut state) = inline_node.state.lock() {
2261 state.set_text(text.clone().into());
2262 }
2263 child_nodes.push(
2264 Inline::new(
2265 inline_node.state.clone(),
2266 links.clone(),
2267 fade_highlights(
2268 highlights.clone(),
2269 &slice_fades(fades, consumed, consumed + text.len()),
2270 ),
2271 node_cx.link_click_handler.clone(),
2272 )
2273 .range_backgrounds(slice_backgrounds(
2274 backgrounds,
2275 consumed,
2276 consumed + text.len(),
2277 ))
2278 .reveal(node_cx.reveal_at(fade_key, consumed, consumed + text.len()))
2279 .into_any_element(),
2280 );
2281 }
2282 let link_click_handler = node_cx.link_click_handler.clone();
2283 child_nodes.push(
2284 img(node_cx.image_source(image))
2285 .id(ix)
2286 .object_fit(ObjectFit::Contain)
2287 .max_w(relative(1.))
2288 .when_some(image.width, |this, width| this.w(width))
2289 .when_some(image.link.clone(), |this, link| {
2290 let link_click_handler = link_click_handler.clone();
2291 let aux_link = link.clone();
2292 let aux_link_click_handler = link_click_handler.clone();
2293 this.cursor_pointer()
2294 .on_click(move |event, window, cx| {
2295 crate::TextSelection::end(window, cx);
2296 cx.stop_propagation();
2297 handle_link_click(
2298 &link_click_handler,
2299 link.url.clone(),
2300 event.clone(),
2301 window,
2302 cx,
2303 );
2304 })
2305 .on_aux_click(move |event, window, cx| {
2306 crate::TextSelection::end(window, cx);
2307 cx.stop_propagation();
2308 handle_link_click(
2309 &aux_link_click_handler,
2310 aux_link.url.clone(),
2311 event.clone(),
2312 window,
2313 cx,
2314 );
2315 })
2316 })
2317 .into_any_element(),
2318 );
2319
2320 consumed += text.len();
2321 text.clear();
2322 links.clear();
2323 highlights.clear();
2324 offset = 0;
2325 } else {
2326 let mut node_highlights = vec![];
2327 for (range, style) in &inline_node.marks {
2328 let inner_range = (offset + range.start)..(offset + range.end);
2329 let mut highlight = mark_highlight(style, node_cx, cx);
2330
2331 if let Some(mut link_mark) = style.link.clone() {
2332 highlight.style.color = Some(node_cx.style.link());
2333 highlight.style.underline = Some(gpui::UnderlineStyle {
2334 thickness: gpui::px(1.),
2335 ..Default::default()
2336 });
2337
2338 if let Some(identifier) = link_mark.identifier.as_ref() {
2340 if let Some(mark) = node_cx.link_refs.get(identifier) {
2341 link_mark = mark.clone();
2342 }
2343 }
2344
2345 links.push((inner_range.clone(), link_mark));
2346 }
2347
2348 node_highlights.push((inner_range, highlight));
2349 }
2350
2351 highlights = combine_highlights(highlights, node_highlights);
2352 offset += text_len;
2353 }
2354 }
2355
2356 if text.len() > 0 {
2358 let text_end = consumed + text.len();
2359 let highlights = fade_highlights(highlights, &slice_fades(fades, consumed, text_end));
2360 if let Ok(mut state) = self.state.lock() {
2361 state.set_text(text.into());
2362 }
2363 child_nodes.push(
2364 Inline::new(
2365 self.state.clone(),
2366 links,
2367 highlights,
2368 node_cx.link_click_handler.clone(),
2369 )
2370 .range_backgrounds(slice_backgrounds(backgrounds, consumed, text_end))
2371 .reveal(node_cx.reveal_at(fade_key, consumed, text_end))
2372 .into_any_element(),
2373 );
2374 }
2375
2376 if !has_image {
2380 return child_nodes
2381 .pop()
2382 .unwrap_or_else(|| div().into_any_element());
2383 }
2384
2385 div()
2386 .id(leaf_element_id(fade_key))
2387 .children(child_nodes)
2388 .into_any_element()
2389 }
2390
2391 fn should_render_inline_flow(&self) -> bool {
2392 let has_image = self.children.iter().any(|child| child.image.is_some());
2393 let has_text = self.children.iter().any(|child| !child.text.is_empty());
2394 self.children.iter().any(|child| child.custom.is_some())
2395 || (has_image && has_text)
2396 || self
2397 .children
2398 .iter()
2399 .any(|child| child.marks.iter().any(|(_, mark)| mark.code))
2400 }
2401
2402 fn inline_flow_items(
2403 &self,
2404 leaf_key: Option<TextLeafKey>,
2405 fades: &[(Range<usize>, f32)],
2406 backgrounds: &[(Range<usize>, Hsla)],
2407 node_cx: &NodeContext,
2408 cx: &mut App,
2409 ) -> Vec<InlineFlowItem> {
2410 let mut items = Vec::new();
2411 let mut text = String::new();
2412 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2413 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2414 let mut offset = 0;
2415 let mut consumed = 0;
2418
2419 for inline_node in &self.children {
2420 if let Some(node) = &inline_node.custom {
2421 if let Ok(mut state) = inline_node.state.lock() {
2422 state.set_text(text.clone().into());
2423 }
2424 if !text.is_empty() {
2425 let item_fades = slice_fades(fades, consumed, consumed + text.len());
2426 let item_backgrounds =
2427 slice_backgrounds(backgrounds, consumed, consumed + text.len());
2428 let item_reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
2429 consumed += text.len();
2430 items.push(InlineFlowItem::Text {
2431 state: inline_node.state.clone(),
2432 text: std::mem::take(&mut text).into(),
2433 links: std::mem::take(&mut links),
2434 highlights: fade_highlights(std::mem::take(&mut highlights), &item_fades),
2435 backgrounds: item_backgrounds,
2436 reveal: item_reveal,
2437 });
2438 }
2439 let mut object_style = HighlightStyle::default();
2440 let mut object_link = None;
2441 for (_, mark) in &inline_node.marks {
2442 object_style = object_style.highlight(mark_highlight(mark, node_cx, cx).style);
2443 if let Some(link) = &mark.link {
2444 object_link = Some(
2445 link.identifier
2446 .as_ref()
2447 .and_then(|id| node_cx.link_refs.get(id))
2448 .unwrap_or(link)
2449 .clone(),
2450 );
2451 object_style.color = Some(node_cx.style.link());
2452 object_style.underline = Some(gpui::UnderlineStyle {
2453 thickness: px(1.),
2454 ..Default::default()
2455 });
2456 }
2457 }
2458 let rendered_node = node.clone();
2459 let extensions = node_cx.markdown_extensions.clone();
2460 items.push(InlineFlowItem::Object {
2461 text: node.shared_text(),
2462 accessibility_label: node.shared_accessibility_name(),
2463 id: node.source_range().map_or(items.len(), |range| range.start),
2464 renderer: Arc::new(move |context, window, cx| {
2465 extensions.render_inline(&rendered_node, context, window, cx)
2466 }),
2467 selected: inline_node.custom_selection.clone(),
2468 style: object_style,
2469 link: object_link,
2470 });
2471 consumed += inline_node.text.len();
2472 offset = 0;
2473 continue;
2474 }
2475 let text_len = inline_node.text.len();
2476 text.push_str(&inline_node.text);
2477
2478 if let Some(image) = &inline_node.image {
2479 if !text.is_empty() {
2480 if let Ok(mut state) = inline_node.state.lock() {
2481 state.set_text(text.clone().into());
2482 }
2483 items.push(InlineFlowItem::Text {
2484 state: inline_node.state.clone(),
2485 text: text.clone().into(),
2486 links: links.clone(),
2487 highlights: fade_highlights(
2488 highlights.clone(),
2489 &slice_fades(fades, consumed, consumed + text.len()),
2490 ),
2491 backgrounds: slice_backgrounds(
2492 backgrounds,
2493 consumed,
2494 consumed + text.len(),
2495 ),
2496 reveal: node_cx.reveal_at(leaf_key, consumed, consumed + text.len()),
2497 });
2498 }
2499
2500 items.push(InlineFlowItem::Image {
2501 source: node_cx.image_source(image),
2502 link: image.link.clone(),
2503 title: image.title(),
2504 width: image.width,
2505 height: image.height,
2506 });
2507
2508 consumed += text.len();
2509 text.clear();
2510 links.clear();
2511 highlights.clear();
2512 offset = 0;
2513 } else {
2514 let mut node_highlights = vec![];
2515 for (range, style) in &inline_node.marks {
2516 let inner_range = (offset + range.start)..(offset + range.end);
2517 let mut highlight = mark_highlight(style, node_cx, cx);
2518
2519 if let Some(mut link_mark) = style.link.clone() {
2520 highlight.style.color = Some(node_cx.style.link());
2521 highlight.style.underline = Some(gpui::UnderlineStyle {
2522 thickness: gpui::px(1.),
2523 ..Default::default()
2524 });
2525
2526 if let Some(identifier) = link_mark.identifier.as_ref()
2527 && let Some(mark) = node_cx.link_refs.get(identifier)
2528 {
2529 link_mark = mark.clone();
2530 }
2531
2532 links.push((inner_range.clone(), link_mark));
2533 }
2534
2535 node_highlights.push((inner_range, highlight));
2536 }
2537
2538 highlights = combine_highlights(highlights, node_highlights);
2539 offset += text_len;
2540 }
2541 }
2542
2543 if !text.is_empty() {
2544 if let Ok(mut state) = self.state.lock() {
2545 state.set_text(text.clone().into());
2546 }
2547 let highlights = fade_highlights(
2548 highlights,
2549 &slice_fades(fades, consumed, consumed + text.len()),
2550 );
2551 let backgrounds = slice_backgrounds(backgrounds, consumed, consumed + text.len());
2552 let reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
2553 items.push(InlineFlowItem::Text {
2554 state: self.state.clone(),
2555 text: text.into(),
2556 links,
2557 highlights,
2558 backgrounds,
2559 reveal,
2560 });
2561 }
2562
2563 items
2564 }
2565}
2566
2567fn block_element_id(kind: &'static str, span: Option<Span>, ix: usize) -> ElementId {
2572 (kind, span.map_or(ix, |span| span.start)).into()
2573}
2574
2575fn leaf_element_id(fade_key: Option<TextLeafKey>) -> ElementId {
2580 fade_key.map_or_else(|| ElementId::from("p"), ElementId::from)
2581}
2582
2583fn gapped(block: AnyElement, gap: Rems) -> AnyElement {
2586 if gap.is_zero() {
2587 block
2588 } else {
2589 div().pb(gap).child(block).into_any_element()
2590 }
2591}
2592
2593fn slice_fades(
2595 fades: &[(Range<usize>, f32)],
2596 start: usize,
2597 end: usize,
2598) -> Vec<(Range<usize>, f32)> {
2599 slice_ranges(fades, start, end, |range, fade_out| (range, *fade_out))
2600}
2601
2602fn slice_backgrounds(
2605 backgrounds: &[(Range<usize>, Hsla)],
2606 start: usize,
2607 end: usize,
2608) -> Vec<(Range<usize>, Hsla)> {
2609 slice_ranges(backgrounds, start, end, |range, color| (range, *color))
2610}
2611
2612const CELL_PAD_PX: f32 = 16.0; const CELL_MIN_PX: f32 = 48.0;
2614const CELL_BORDER_PX: f32 = 1.0; fn measure_table_columns(
2621 table: &Table,
2622 col_count: usize,
2623 node_cx: &NodeContext,
2624 window: &mut Window,
2625 cx: &mut App,
2626) -> Vec<f32> {
2627 let text_style = window.text_style();
2628 let rem_size = window.rem_size();
2629 let mono_family = cx.theme().tokens.typography.mono.clone();
2630 let inline_code = node_cx.style.inline_code_highlight();
2631 let cached = table.column_widths_cache.0.lock().ok().and_then(|cache| {
2632 cache
2633 .as_ref()
2634 .filter(|cached| {
2635 cached.widths.len() == col_count
2636 && cached.text_style == text_style
2637 && cached.rem_size == rem_size
2638 && cached.mono_family == mono_family
2639 && cached.inline_code == inline_code
2640 && cached
2641 .text_system
2642 .upgrade()
2643 .is_some_and(|system| Arc::ptr_eq(&system, window.text_system()))
2644 })
2645 .cloned()
2646 });
2647 let measured = cached.unwrap_or_else(|| {
2648 let (widths, custom_cells) =
2649 measure_table_text_columns(table, col_count, node_cx, window, cx);
2650 let measured = Arc::new(TableColumnWidths {
2651 text_system: Arc::downgrade(window.text_system()),
2652 text_style,
2653 rem_size,
2654 mono_family,
2655 inline_code,
2656 widths,
2657 custom_cells,
2658 });
2659 if let Ok(mut cache) = table.column_widths_cache.0.lock() {
2660 *cache = Some(measured.clone());
2661 }
2662 measured
2663 });
2664
2665 let mut col_w = measured.widths.clone();
2669 for &(row_ix, ix) in &measured.custom_cells {
2670 let cell = &table.children[row_ix].children[ix];
2671 let items = cell.children.inline_flow_items(None, &[], &[], node_cx, cx);
2672 let width = super::inline_flow::intrinsic_width(&items, window, cx);
2673 let border = if ix + 1 < col_count {
2674 CELL_BORDER_PX
2675 } else {
2676 0.
2677 };
2678 col_w[ix] = col_w[ix].max(f32::from(width) + CELL_PAD_PX + border);
2679 }
2680 col_w
2681}
2682
2683fn measure_table_text_columns(
2686 table: &Table,
2687 col_count: usize,
2688 node_cx: &NodeContext,
2689 window: &mut Window,
2690 cx: &mut App,
2691) -> (Vec<f32>, Vec<(usize, usize)>) {
2692 let text_style = window.text_style();
2693 let font_size = text_style.font_size.to_pixels(window.rem_size());
2694 let mut col_w = vec![CELL_MIN_PX; col_count];
2695 let mut custom_cells = Vec::new();
2696 for (row_ix, row) in table.children.iter().enumerate() {
2697 for (ix, cell) in row.children.iter().enumerate() {
2698 let Some(slot) = col_w.get_mut(ix) else {
2699 continue;
2700 };
2701 if cell
2702 .children
2703 .children
2704 .iter()
2705 .any(|node| node.custom.is_some())
2706 {
2707 custom_cells.push((row_ix, ix));
2708 continue;
2709 }
2710 let text = cell.children.text();
2711 let highlights = cell.children.inline_highlights(node_cx, cx);
2712 let mut w = 0.0_f32;
2713 let mut line_start = 0;
2714 for line in text.split('\n') {
2715 let start = line_start + (line.len() - line.trim_start().len());
2716 let line_end = line_start + line.len();
2717 line_start = line_end + 1;
2718 let line = line.trim();
2719 if line.is_empty() {
2720 continue;
2721 }
2722 let end = start + line.len();
2723 let line_highlights = highlights
2724 .iter()
2725 .filter_map(|(range, highlight)| {
2726 let clipped = range.start.max(start)..range.end.min(end);
2727 (clipped.start < clipped.end).then(|| {
2728 (
2729 clipped.start - start..clipped.end - start,
2730 highlight.clone(),
2731 )
2732 })
2733 })
2734 .collect::<Vec<_>>();
2735 let mut line_w = gpui::Pixels::ZERO;
2736 for (range, scale) in text_size_ranges(line.len(), &line_highlights) {
2737 let highlights = slice_ranges(
2738 &line_highlights,
2739 range.start,
2740 range.end,
2741 |range, highlight| (range, highlight.clone()),
2742 );
2743 if highlights.iter().any(|(_, h)| h.font_size_scale.is_some()) {
2744 line_w += px(crate::text::inline_flow::INLINE_CODE_PADDING * 2.);
2745 }
2746 let runs = text_runs(range.len(), &text_style, &highlights);
2747 line_w += window
2748 .text_system()
2749 .layout_line(&line[range], font_size * scale, &runs, None)
2750 .width;
2751 }
2752 w = w.max(f32::from(line_w));
2753 }
2754 let border = if ix + 1 < col_count {
2757 CELL_BORDER_PX
2758 } else {
2759 0.
2760 };
2761 *slot = slot.max(w + CELL_PAD_PX + border);
2762 }
2763 }
2764 (col_w, custom_cells)
2765}
2766
2767impl Paragraph {
2768 fn to_markdown(&self) -> String {
2769 if self.children.iter().any(|node| node.custom.is_some()) {
2770 let mut source = MarkdownSource::default();
2771 for node in &self.children {
2772 if node.custom.is_some() {
2773 source.push_object(node);
2774 } else {
2775 source.push_text(&node.text, &node.marks, 0..node.text.len());
2776 if let Some(image) = &node.image {
2777 source.push_str(&image_markdown(image));
2778 }
2779 }
2780 }
2781 let mut text = source.finish();
2782 text.push_str("\n\n");
2783 return text;
2784 }
2785 let mut text = self
2786 .children
2787 .iter()
2788 .map(|text_node| {
2789 let mut text = text_node.text.to_string();
2790 for (range, style) in &text_node.marks {
2791 if style.bold {
2792 text = format!("**{}**", &text_node.text[range.clone()]);
2793 }
2794 if style.italic {
2795 text = format!("*{}*", &text_node.text[range.clone()]);
2796 }
2797 if style.strikethrough {
2798 text = format!("~~{}~~", &text_node.text[range.clone()]);
2799 }
2800 if style.code {
2801 text = format!("`{}`", &text_node.text[range.clone()]);
2802 }
2803 if style.highlight.is_some() {
2804 text = format!("=={}==", &text_node.text[range.clone()]);
2805 }
2806 if let Some(link) = &style.link {
2807 text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
2808 }
2809 }
2810
2811 if let Some(image) = &text_node.image {
2812 let alt = image.alt.clone().unwrap_or_default();
2813 let title = image
2814 .title
2815 .clone()
2816 .map_or(String::new(), |t| format!(" \"{}\"", t));
2817 text.push_str(&format!("", alt, image.url, title))
2818 }
2819
2820 text
2821 })
2822 .collect::<Vec<_>>()
2823 .join("");
2824
2825 text.push_str("\n\n");
2826 text
2827 }
2828}
2829
2830impl BlockNode {
2831 #[allow(dead_code)]
2835 pub(crate) fn to_markdown(&self) -> String {
2836 match self {
2837 BlockNode::Root { children, .. } => children
2838 .iter()
2839 .map(|child| child.to_markdown())
2840 .collect::<Vec<_>>()
2841 .join("\n\n"),
2842 BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
2843 BlockNode::Heading {
2844 level, children, ..
2845 } => {
2846 let hashes = "#".repeat(*level as usize);
2847 format!("{} {}", hashes, children.to_markdown())
2848 }
2849 BlockNode::Blockquote { children, .. } => {
2850 let content = children
2851 .iter()
2852 .map(|child| child.to_markdown())
2853 .collect::<Vec<_>>()
2854 .join("\n\n");
2855
2856 content
2857 .lines()
2858 .map(|line| format!("> {}", line))
2859 .collect::<Vec<_>>()
2860 .join("\n")
2861 }
2862 BlockNode::List {
2863 children,
2864 ordered,
2865 start,
2866 ..
2867 } => children
2868 .iter()
2869 .enumerate()
2870 .map(|(i, child)| {
2871 let prefix = if *ordered {
2872 format!("{}. ", ordered_list_ordinal(*start, i))
2873 } else {
2874 "- ".to_string()
2875 };
2876 format!("{}{}", prefix, child.to_markdown())
2877 })
2878 .collect::<Vec<_>>()
2879 .join("\n"),
2880 BlockNode::ListItem {
2881 children, checked, ..
2882 } => {
2883 let checkbox = if let Some(checked) = checked {
2884 if *checked { "[x] " } else { "[ ] " }
2885 } else {
2886 ""
2887 };
2888 format!(
2889 "{}{}",
2890 checkbox,
2891 children
2892 .iter()
2893 .map(|child| child.to_markdown())
2894 .collect::<Vec<_>>()
2895 .join("\n")
2896 )
2897 }
2898 BlockNode::CodeBlock(code_block) => {
2899 format!(
2900 "```{}\n{}\n```",
2901 code_block.lang.clone().unwrap_or_default(),
2902 code_block.code()
2903 )
2904 }
2905 BlockNode::Table(table) => table.to_markdown(),
2906 BlockNode::Break { html, .. } => {
2907 if *html {
2908 "<br>".to_string()
2909 } else {
2910 "\n".to_string()
2911 }
2912 }
2913 BlockNode::HorizontalRule { .. } => "---".to_string(),
2914 BlockNode::Custom(node) => node.to_markdown(),
2915 BlockNode::Definition {
2916 identifier,
2917 url,
2918 title,
2919 ..
2920 } => {
2921 if let Some(title) = title {
2922 format!("[{}]: {} \"{}\"", identifier, url, title)
2923 } else {
2924 format!("[{}]: {}", identifier, url)
2925 }
2926 }
2927 BlockNode::Unknown { .. } => "".to_string(),
2928 }
2929 .trim()
2930 .to_string()
2931 }
2932}
2933
2934impl BlockNode {
2935 fn render_list_item_row(
2936 content: AnyElement,
2937 ix: usize,
2938 options: NodeRenderOptions,
2939 checked: Option<bool>,
2940 style: &TextViewStyle,
2941 line_height: Pixels,
2942 ) -> Div {
2943 h_flex()
2944 .w_full()
2945 .min_w_0()
2946 .relative()
2947 .items_start()
2948 .content_start()
2949 .when(!options.todo && checked.is_none(), |this| {
2950 this.child(list_item_prefix(
2951 ix,
2952 options.list_start,
2953 options.ordered,
2954 options.depth,
2955 ))
2956 })
2957 .when_some(checked, |this, checked| {
2958 let check_svg = if style.is_dark() {
2960 CHECK_SVG_DARK
2961 } else {
2962 CHECK_SVG_LIGHT
2963 };
2964 this.child(
2965 div()
2966 .flex()
2967 .mr_1p5()
2968 .h(line_height)
2969 .flex_none()
2970 .items_center()
2971 .justify_center()
2972 .child(
2973 div()
2974 .flex()
2975 .size(rems(0.875))
2976 .items_center()
2977 .justify_center()
2978 .border_1()
2979 .border_color(style.foreground())
2980 .when(checked, |this| {
2981 this.bg(style.foreground()).child(
2982 img(Arc::new(Image::from_bytes(
2983 ImageFormat::Svg,
2984 check_svg.to_vec(),
2985 )))
2986 .size(rems(0.625)),
2987 )
2988 }),
2989 ),
2990 )
2991 })
2992 .child(div().flex_1().min_w_0().overflow_hidden().child(content))
2993 }
2994
2995 fn render_list_item(
2996 item: &BlockNode,
2997 ix: usize,
2998 options: NodeRenderOptions,
2999 node_cx: &NodeContext,
3000 window: &mut Window,
3001 cx: &mut App,
3002 ) -> AnyElement {
3003 match item {
3004 BlockNode::ListItem {
3005 children,
3006 spread,
3007 checked,
3008 ..
3009 } => div()
3010 .w_full()
3011 .min_w_0()
3012 .when(*spread, |this| this.child(div()))
3013 .children({
3014 let mut items: Vec<Div> = Vec::with_capacity(children.len());
3015
3016 for (child_ix, child) in children.iter().enumerate() {
3017 match child {
3018 BlockNode::Paragraph { .. } => {
3019 let last_not_list = child_ix > 0
3020 && !matches!(children[child_ix - 1], BlockNode::List { .. });
3021
3022 let text = child.render_block(
3023 NodeRenderOptions {
3024 depth: options.depth + 1,
3025 todo: checked.is_some(),
3026 is_last: true,
3027 ..options
3028 },
3029 node_cx,
3030 window,
3031 cx,
3032 );
3033
3034 if last_not_list {
3038 if let Some(preceding_row) = items.pop() {
3039 items.push(
3040 div().child(preceding_row).child(
3041 div()
3042 .w_full()
3043 .pl(rems(1.))
3044 .overflow_hidden()
3045 .child(text),
3046 ),
3047 );
3048 continue;
3049 }
3050 }
3051
3052 items.push(Self::render_list_item_row(
3053 text,
3054 ix,
3055 options,
3056 *checked,
3057 &node_cx.style,
3058 window.line_height(),
3059 ));
3060 }
3061 BlockNode::List { .. } => {
3062 items.push(div().ml(rems(1.)).child(child.render_block(
3063 NodeRenderOptions {
3064 depth: options.depth + 1,
3065 todo: checked.is_some(),
3066 is_last: true,
3067 ..options
3068 },
3069 node_cx,
3070 window,
3071 cx,
3072 )));
3073 }
3074 BlockNode::Root { .. }
3075 | BlockNode::Heading { .. }
3076 | BlockNode::Blockquote { .. }
3077 | BlockNode::CodeBlock(_)
3078 | BlockNode::Custom(_)
3079 | BlockNode::Table(_)
3080 | BlockNode::HorizontalRule { .. } => {
3081 let block = child.render_block(
3082 NodeRenderOptions {
3083 depth: options.depth + 1,
3084 todo: checked.is_some(),
3085 is_last: true,
3086 ..options
3087 },
3088 node_cx,
3089 window,
3090 cx,
3091 );
3092
3093 if child_ix == 0 {
3094 items.push(Self::render_list_item_row(
3095 block,
3096 ix,
3097 options,
3098 *checked,
3099 &node_cx.style,
3100 window.line_height(),
3101 ));
3102 } else {
3103 items.push(
3107 div()
3108 .w_full()
3109 .min_w_0()
3110 .pl(rems(1.))
3111 .overflow_hidden()
3112 .child(block),
3113 );
3114 }
3115 }
3116 BlockNode::ListItem { .. }
3117 | BlockNode::Break { .. }
3118 | BlockNode::Definition { .. }
3119 | BlockNode::Unknown => {}
3120 }
3121 }
3122 items
3123 })
3124 .into_any_element(),
3125 _ => div().into_any_element(),
3126 }
3127 }
3128
3129 fn render_table(
3133 item: &BlockNode,
3134 options: &NodeRenderOptions,
3135 node_cx: &NodeContext,
3136 window: &mut Window,
3137 cx: &mut App,
3138 ) -> impl IntoElement {
3139 const DEFAULT_LENGTH: usize = 5;
3140
3141 let table = match item {
3142 BlockNode::Table(table) => table,
3143 _ => return div().into_any_element(),
3144 };
3145
3146 if matches!(node_cx.style.table().overflow.x, Some(Overflow::Scroll)) {
3148 let col_count = table
3149 .children
3150 .iter()
3151 .map(|row| row.children.len())
3152 .max()
3153 .unwrap_or(0);
3154 return Self::render_scroll_table(table, col_count, options, node_cx, window, cx);
3155 }
3156
3157 let mut col_lens: Vec<usize> = vec![];
3160 for row in table.children.iter() {
3161 for (ix, cell) in row.children.iter().enumerate() {
3162 if col_lens.len() <= ix {
3163 col_lens.push(DEFAULT_LENGTH);
3164 }
3165 col_lens[ix] = col_lens[ix].max(cell.children.text_len());
3166 }
3167 }
3168
3169 Self::render_wrap_table(table, &col_lens, options, node_cx, window, cx)
3170 }
3171
3172 fn render_scroll_table(
3191 table: &Table,
3192 col_count: usize,
3193 options: &NodeRenderOptions,
3194 node_cx: &NodeContext,
3195 window: &mut Window,
3196 cx: &mut App,
3197 ) -> AnyElement {
3198 const CELL_WRAP_MAX_LINES: f32 = 2.0;
3204 const CELL_WRAP_MIN_PX: f32 = 160.0;
3205 const CELL_WRAP_MAX_PX: f32 = 480.0;
3206 const TABLE_BORDER_PX: f32 = 2.0; let col_w = measure_table_columns(table, col_count, node_cx, window, cx);
3209 let style = &node_cx.style;
3210 let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
3214 let col_min_w: Vec<f32> = if nowrap {
3215 col_w.clone()
3216 } else {
3217 col_w
3218 .iter()
3219 .map(|w| {
3220 (w / CELL_WRAP_MAX_LINES)
3221 .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
3222 .min(*w)
3223 })
3224 .collect()
3225 };
3226 let min_total_w: f32 = col_min_w.iter().sum::<f32>() + TABLE_BORDER_PX;
3227
3228 let scroll_handle = window
3229 .use_keyed_state(
3230 block_element_id("table-scroll", table.span, options.ix),
3231 cx,
3232 |_, _| ScrollHandle::default(),
3233 )
3234 .read(cx)
3235 .clone();
3236 let row_count = table.children.len();
3237 let mut rows = Vec::with_capacity(row_count);
3238 let mut cell_ordinal = 0;
3239 for (row_ix, row) in table.children.iter().enumerate() {
3240 let mut cells = Vec::with_capacity(row.children.len());
3241 for (ix, cell) in row.children.iter().enumerate() {
3242 let fade_key = table
3243 .span
3244 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
3245 cell_ordinal += 1;
3246 let align = table.column_align(ix);
3247 let is_last_col = ix == row.children.len() - 1;
3248 let width = col_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
3249 let min_width = col_min_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
3250 cells.push(
3251 div()
3252 .flex_basis(px(width))
3259 .flex_grow(width)
3260 .flex_shrink(1.)
3261 .min_w(px(min_width))
3262 .overflow_hidden()
3263 .when(align == ColumnumnAlign::Center, |this| this.text_center())
3264 .when(align == ColumnumnAlign::Right, |this| this.text_right())
3265 .px_2()
3266 .py_1()
3267 .when(!is_last_col, |this| {
3268 this.border_r_1().border_color(style.border())
3269 })
3270 .refine_style(&style.table_cell())
3271 .child(cell.children.render(fade_key, node_cx, window, cx)),
3272 );
3273 }
3274 rows.push(
3275 div()
3276 .w_full()
3277 .when(row_ix < row_count - 1, |this| this.border_b_1())
3278 .border_color(style.border())
3279 .flex()
3280 .flex_row()
3281 .when(row_ix == 0, |this| {
3285 this.bg(style.code_background())
3286 .text_color(style.foreground())
3287 .refine_style(&style.table_head())
3288 })
3289 .children(cells),
3290 );
3291 }
3292
3293 div()
3294 .pb(rems(1.))
3295 .w_full()
3296 .child(
3297 horizontal_scroll_area(
3307 block_element_id("table", table.span, options.ix),
3308 &scroll_handle,
3309 &StyleRefinement::default()
3310 .bg(style
3311 .table_background()
3312 .unwrap_or(cx.theme().tokens.colors.surface))
3313 .border_1()
3314 .border_color(style.border())
3315 .refine_style(style.table()),
3316 div().min_w_full().w(px(min_total_w)).children(rows),
3322 ),
3323 )
3324 .children(node_cx.table_actions.clone().map(|f| {
3331 div()
3332 .id(block_element_id("table-actions", table.span, options.ix))
3333 .mt_1()
3334 .child(f(&table.cached_table_data(), window, cx))
3335 }))
3336 .into_any_element()
3337 }
3338
3339 fn render_wrap_table(
3342 table: &Table,
3343 col_lens: &[usize],
3344 options: &NodeRenderOptions,
3345 node_cx: &NodeContext,
3346 window: &mut Window,
3347 cx: &mut App,
3348 ) -> AnyElement {
3349 const MAX_LENGTH: usize = 150;
3350
3351 let style = &node_cx.style;
3352 let row_count = table.children.len();
3353 let mut rows = Vec::with_capacity(row_count);
3354 let mut cell_ordinal = 0;
3355 for (row_ix, row) in table.children.iter().enumerate() {
3356 let mut cells = Vec::with_capacity(row.children.len());
3357 for (ix, cell) in row.children.iter().enumerate() {
3358 let fade_key = table
3359 .span
3360 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
3361 cell_ordinal += 1;
3362 let align = table.column_align(ix);
3363 let is_last_col = ix == row.children.len() - 1;
3364 let len = col_lens
3365 .get(ix)
3366 .copied()
3367 .unwrap_or(MAX_LENGTH)
3368 .min(MAX_LENGTH);
3369
3370 cells.push(
3371 div()
3372 .overflow_hidden()
3373 .when(align == ColumnumnAlign::Center, |this| this.text_center())
3374 .when(align == ColumnumnAlign::Right, |this| this.text_right())
3375 .min_w_16()
3376 .w(Length::Definite(relative(len as f32)))
3377 .px_2()
3378 .py_1()
3379 .when(!is_last_col, |this| {
3380 this.border_r_1().border_color(style.border())
3381 })
3382 .refine_style(&style.table_cell())
3383 .child(cell.children.render(fade_key, node_cx, window, cx)),
3384 );
3385 }
3386
3387 rows.push(
3388 div()
3389 .w_full()
3390 .when(row_ix < row_count - 1, |this| this.border_b_1())
3391 .border_color(style.border())
3392 .flex()
3393 .flex_row()
3394 .when(row_ix == 0, |this| {
3398 this.bg(style.code_background())
3399 .text_color(style.foreground())
3400 .refine_style(&style.table_head())
3401 })
3402 .children(cells),
3403 );
3404 }
3405
3406 div()
3407 .pb(rems(1.))
3408 .w_full()
3409 .child(
3410 div()
3411 .w_full()
3412 .bg(style
3413 .table_background()
3414 .unwrap_or(cx.theme().tokens.colors.surface))
3415 .border_1()
3416 .border_color(style.border())
3417 .overflow_hidden()
3418 .children(rows)
3419 .refine_style(&style.table()),
3420 )
3421 .children(node_cx.table_actions.clone().map(|f| {
3428 div()
3429 .id(block_element_id("table-actions", table.span, options.ix))
3430 .mt_1()
3431 .child(f(&table.cached_table_data(), window, cx))
3432 }))
3433 .into_any_element()
3434 }
3435
3436 pub(crate) fn render_block(
3437 &self,
3438 options: NodeRenderOptions,
3439 node_cx: &NodeContext,
3440 window: &mut Window,
3441 cx: &mut App,
3442 ) -> AnyElement {
3443 let mb = if options.in_list || options.is_last {
3444 rems(0.)
3445 } else {
3446 node_cx.style.paragraph_gap()
3447 };
3448
3449 match self {
3450 BlockNode::Root { children, .. } => div()
3451 .children(children.into_iter().enumerate().map(move |(ix, node)| {
3452 node.render_block(NodeRenderOptions { ix, ..options }, node_cx, window, cx)
3453 }))
3454 .into_any_element(),
3455 BlockNode::Paragraph(paragraph) => gapped(
3456 paragraph.render(
3457 paragraph.span.map(|span| TextLeafKey::block(span.start)),
3458 node_cx,
3459 window,
3460 cx,
3461 ),
3462 mb,
3463 ),
3464 BlockNode::Heading {
3465 level,
3466 children,
3467 span,
3468 } => {
3469 let (text_size, font_weight) = match level {
3470 1 => (rems(2.), FontWeight::BOLD),
3471 2 => (rems(1.5), FontWeight::SEMIBOLD),
3472 3 => (rems(1.25), FontWeight::SEMIBOLD),
3473 4 => (rems(1.125), FontWeight::SEMIBOLD),
3474 5 => (rems(1.), FontWeight::SEMIBOLD),
3475 6 => (rems(1.), FontWeight::MEDIUM),
3476 _ => (rems(1.), FontWeight::NORMAL),
3477 };
3478
3479 let text_size = text_size.to_pixels(px(14.));
3480
3481 div()
3482 .pb(rems(0.3))
3483 .whitespace_normal()
3484 .text_size(text_size)
3485 .font_weight(font_weight)
3486 .refine_style(&node_cx.style.heading(*level))
3487 .child(children.render(
3488 span.map(|span| TextLeafKey::block(span.start)),
3489 node_cx,
3490 window,
3491 cx,
3492 ))
3493 .into_any_element()
3494 }
3495 BlockNode::Blockquote { children, .. } => gapped(
3496 div()
3497 .w_full()
3498 .text_color(node_cx.style.muted_foreground())
3499 .border_l_3()
3500 .border_color(node_cx.style.border())
3501 .px_4()
3502 .children({
3503 let children_len = children.len();
3504 children.into_iter().enumerate().map(move |(index, c)| {
3505 let is_last = index == children_len - 1;
3506 c.render_block(options.is_last(is_last), node_cx, window, cx)
3507 })
3508 })
3509 .into_any_element(),
3510 mb,
3511 ),
3512 BlockNode::List {
3513 children,
3514 ordered,
3515 start,
3516 ..
3517 } => div()
3518 .w_full()
3519 .min_w_0()
3520 .pb(mb)
3521 .children({
3522 let mut items = Vec::with_capacity(children.len());
3523 let mut item_index = 0;
3524 for (ix, item) in children.into_iter().enumerate() {
3525 let is_item = item.is_list_item();
3526
3527 items.push(Self::render_list_item(
3528 item,
3529 item_index,
3530 NodeRenderOptions {
3531 ix,
3532 ordered: *ordered,
3533 list_start: *start,
3534 ..options
3535 },
3536 node_cx,
3537 window,
3538 cx,
3539 ));
3540
3541 if is_item {
3542 item_index += 1;
3543 }
3544 }
3545 items
3546 })
3547 .into_any_element(),
3548 BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
3549 BlockNode::Custom(node) => {
3550 let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
3551 Some(rendered) => rendered,
3552 None => div().child(node.as_text().to_string()).into_any_element(),
3553 };
3554
3555 div().pb(mb).child(inner).into_any_element()
3556 }
3557 BlockNode::Table { .. } => {
3558 Self::render_table(self, &options, node_cx, window, cx).into_any_element()
3559 }
3560 BlockNode::HorizontalRule { .. } => gapped(
3561 div()
3562 .bg(node_cx.style.border())
3563 .h(px(2.))
3564 .into_any_element(),
3565 mb,
3566 ),
3567 BlockNode::Break { .. } => div().into_any_element(),
3568 BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
3569 _ => {
3570 if cfg!(debug_assertions) {
3571 tracing::warn!("unknown implementation: {:?}", self);
3572 }
3573
3574 div().into_any_element()
3575 }
3576 }
3577 }
3578}
3579
3580#[cfg(test)]
3581mod tests {
3582 use super::*;
3583
3584 #[test]
3585 fn selected_inline_objects_coalesce_surrounding_emphasis() {
3586 for (object_mark, expected) in [
3587 (TextMark::default().italic(), "*fore $x$ aft*"),
3588 (TextMark::default().italic().bold(), "*fore **$x$** aft*"),
3589 ] {
3590 let italic = TextMark::default().italic();
3591 let before = InlineNode::new("before ").marks(vec![(0..7, italic.clone())]);
3592 let formula =
3593 InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"))
3594 .marks(vec![(0..1, object_mark)]);
3595 let after = InlineNode::new(" after").marks(vec![(0..6, italic)]);
3596 {
3597 let mut state = formula.state.lock().unwrap();
3598 state.text = "before ".into();
3599 state.selection = Some((2..7).into());
3600 }
3601 *formula.custom_selection.lock().unwrap() = true;
3602 let paragraph = Paragraph {
3603 children: vec![before, formula, after],
3604 ..Default::default()
3605 };
3606 {
3607 let mut state = paragraph.state.lock().unwrap();
3608 state.text = " after".into();
3609 state.selection = Some((0..4).into());
3610 }
3611 assert_eq!(paragraph.selected_source(), expected);
3612 }
3613 }
3614
3615 #[test]
3616 fn consecutive_inline_objects_copy_atomically_without_neighboring_text() {
3617 let first =
3618 InlineNode::custom(MarkdownNode::new("math", ()).text("甲²").markdown("$甲^2$"));
3619 let second = InlineNode::custom(MarkdownNode::new("math", ()).text("b").markdown("$b$"));
3620 *first.custom_selection.lock().unwrap() = true;
3621 *second.custom_selection.lock().unwrap() = true;
3622 let paragraph = Paragraph {
3623 children: vec![first, second],
3624 ..Default::default()
3625 };
3626 assert_eq!(paragraph.selected_text(), "甲²b");
3627 assert_eq!(paragraph.selected_source(), "$甲^2$$b$");
3628 assert!(paragraph.has_selection());
3629 paragraph.clear_selection();
3630 assert_eq!(paragraph.selected_text(), "");
3631 assert_eq!(paragraph.selected_source(), "");
3632 assert!(!paragraph.has_selection());
3633 }
3634
3635 #[test]
3636 fn selected_inline_object_interleaves_runs_and_preserves_enclosing_mark() {
3637 let before = InlineNode::new("中文 ");
3638 let formula =
3639 InlineNode::custom(MarkdownNode::new("math", ()).text("x²").markdown("$x^2$"))
3640 .marks(vec![(0..3, TextMark::default().bold())]);
3641 let after = InlineNode::new(" English");
3642 {
3643 let mut preceding = formula.state.lock().unwrap();
3644 preceding.text = "中文 ".into();
3645 preceding.selection = Some((3..7).into());
3646 }
3647 *formula.custom_selection.lock().unwrap() = true;
3648 let paragraph = Paragraph {
3649 children: vec![before, formula, after],
3650 ..Default::default()
3651 };
3652 {
3653 let mut trailing = paragraph.state.lock().unwrap();
3654 trailing.text = " English".into();
3655 trailing.selection = Some((0..4).into());
3656 }
3657 assert_eq!(paragraph.selected_text(), "文 x² Eng");
3658 assert_eq!(paragraph.selected_source(), "文 **$x^2$** Eng");
3659 }
3660
3661 #[test]
3662 fn custom_inline_inherits_marks_and_resolves_link_references() {
3663 use gpui::{Empty, TestApp};
3664 let mut app = TestApp::new();
3665 let mut window = app.open_window(|_, _| Empty);
3666 window.update(|_, _, cx| {
3667 cx.set_global(crate::Theme::default());
3668 let mut node_cx = NodeContext::default();
3669 let mut mark = TextMark::default().bold();
3670 mark.italic = true;
3671 mark.strikethrough = true;
3672 mark.link = Some(LinkMark {
3673 identifier: Some("ref".into()),
3674 ..Default::default()
3675 });
3676 node_cx.link_refs.insert(
3677 "ref".into(),
3678 LinkMark {
3679 url: "https://example.com".into(),
3680 ..Default::default()
3681 },
3682 );
3683 let paragraph = Paragraph {
3684 children: vec![
3685 InlineNode::custom(MarkdownNode::new("test", ()).text("x"))
3686 .marks(vec![(0..1, mark)]),
3687 ],
3688 ..Default::default()
3689 };
3690 let items = paragraph.inline_flow_items(None, &[], &[], &node_cx, cx);
3691 let InlineFlowItem::Object { style, link, .. } = &items[0] else {
3692 panic!()
3693 };
3694 assert_eq!(style.font_weight, Some(FontWeight::BOLD));
3695 assert_eq!(style.font_style, Some(FontStyle::Italic));
3696 assert!(style.strikethrough.is_some());
3697 assert_eq!(link.as_ref().unwrap().url.as_ref(), "https://example.com");
3698 });
3699 }
3700
3701 #[test]
3702 fn table_column_uses_prepared_inline_metrics_after_resource_update() {
3703 use crate::text::InlineElement;
3704 use crate::text::inline::test_draw::in_prepaint;
3705 use gpui::{Image, ImageFormat, TestApp};
3706 let mut app = TestApp::new();
3707 let width = Arc::new(std::sync::atomic::AtomicUsize::new(400));
3708 let render_width = width.clone();
3709 let mut node_cx = NodeContext::default();
3710 node_cx.markdown_extensions = Arc::new(MarkdownExtensions::default().plugin(
3711 crate::text::markdown_ext::TestInlinePlugin::new("test").render_with(
3712 move |_, _, _, _| {
3713 Some(
3714 InlineElement::new(
3715 gpui::img(Arc::new(Image::from_bytes(
3716 ImageFormat::Svg,
3717 b"<svg/>".to_vec(),
3718 )))
3719 .w(px(
3720 render_width.load(std::sync::atomic::Ordering::Relaxed) as f32
3721 ))
3722 .h(px(20.)),
3723 )
3724 .with_baseline(px(15.)),
3725 )
3726 },
3727 ),
3728 ));
3729 let table = table_of(
3730 vec![
3731 vec![plain_cell("ordinary text")],
3732 vec![TableCell {
3733 children: Paragraph {
3734 children: vec![InlineNode::custom(MarkdownNode::new("test", ()).text("x"))],
3735 ..Default::default()
3736 },
3737 width: None,
3738 }],
3739 ],
3740 vec![],
3741 );
3742 in_prepaint(&mut app, move |window, cx| {
3743 let mut cached = None;
3744 for expected in [400, 600, 10, 400] {
3745 width.store(expected, std::sync::atomic::Ordering::Relaxed);
3746 let measured = measure_table_columns(&table, 1, &node_cx, window, cx)[0];
3747 let current = cached_column_widths(&table);
3748 assert_eq!(
3749 measured,
3750 current.widths[0].max(expected as f32 + CELL_PAD_PX)
3751 );
3752 assert_eq!(current.custom_cells, vec![(1, 0)]);
3753 if let Some(cached) = cached.replace(current.clone()) {
3754 assert!(Arc::ptr_eq(&cached, ¤t));
3755 }
3756 }
3757 });
3758 }
3759
3760 fn cached_column_widths(table: &Table) -> Arc<TableColumnWidths> {
3761 table.column_widths_cache.0.lock().unwrap().clone().unwrap()
3762 }
3763
3764 #[test]
3765 fn table_columns_cache_preserves_plain_widths_and_clones() {
3766 use crate::text::inline::test_draw::in_prepaint;
3767 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem, record_shaped_lines};
3768 use gpui::{Refineable as _, TestApp, TextStyle};
3769
3770 let long = "é界".repeat(2048);
3771 let table = table_of(
3772 vec![
3773 vec![plain_cell(&format!(" \t{long} \nshort\n ")), plain_cell("")],
3774 vec![plain_cell("short")],
3775 vec![],
3776 vec![plain_cell(""), plain_cell("a\nabcdefghijklmnop")],
3777 ],
3778 vec![],
3779 );
3780 let uncached = table.clone();
3781 let node_cx = NodeContext::default();
3782 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3783 let (table, node_cx, cached) = in_prepaint(&mut app, move |window, cx| {
3784 window.set_rem_size(px(20.));
3785 let style = TextStyle {
3786 font_family: BODY.into(),
3787 font_size: rems(1.).into(),
3788 ..Default::default()
3789 };
3790 let cached =
3791 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
3792 let (widths, shaped) = record_shaped_lines(|| {
3793 measure_table_columns(&table, 2, &node_cx, window, cx)
3794 });
3795 assert_eq!(widths, vec![40960. + CELL_PAD_PX + CELL_BORDER_PX, 176.]);
3796 assert!(shaped.iter().any(|line| line == &long));
3797 assert_eq!(
3798 table, uncached,
3799 "derived widths do not change table equality"
3800 );
3801 assert!(uncached.column_widths_cache.0.lock().unwrap().is_none());
3802
3803 let cached = cached_column_widths(&table);
3804 let cloned = table.clone();
3805 for same in [&table, &cloned] {
3806 let (again, shaped) = record_shaped_lines(|| {
3807 measure_table_columns(same, 2, &node_cx, window, cx)
3808 });
3809 assert_eq!(again, widths);
3810 assert!(shaped.is_empty());
3811 assert!(Arc::ptr_eq(&cached, &cached_column_widths(same)));
3812 }
3813
3814 assert_eq!(
3816 measure_table_columns(&cloned, 3, &node_cx, window, cx),
3817 vec![widths[0], widths[1] + CELL_BORDER_PX, CELL_MIN_PX]
3818 );
3819 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&cloned)));
3820 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
3821 assert!(
3822 measure_table_columns(&Table::default(), 0, &node_cx, window, cx)
3823 .is_empty()
3824 );
3825 cached
3826 });
3827 (table, node_cx, cached)
3828 });
3829 in_prepaint(&mut app, move |window, cx| {
3832 window.set_rem_size(cached.rem_size);
3833 window.with_text_style(
3834 Some(cached.text_style.subtract(&Default::default())),
3835 |window| {
3836 measure_table_columns(&table, 2, &node_cx, window, cx);
3837 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&table)));
3838 },
3839 );
3840 });
3841 }
3842
3843 #[test]
3844 fn table_columns_cache_tracks_typography() {
3845 use crate::text::inline::test_draw::in_prepaint;
3846 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
3847 use gpui::{Refineable as _, TestApp, TextStyle};
3848
3849 let code = "0123456789";
3850 let mut paragraph = Paragraph::default();
3851 paragraph
3852 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
3853 let table = table_of(
3854 vec![vec![
3855 plain_cell(code),
3856 TableCell {
3857 children: paragraph,
3858 width: None,
3859 },
3860 ]],
3861 vec![],
3862 );
3863 let base = TextStyle {
3864 font_family: BODY.into(),
3865 font_size: rems(1.).into(),
3866 ..Default::default()
3867 };
3868 let mut larger = base.clone();
3869 larger.font_size = rems(2.).into();
3870 let mut bold = base.clone();
3871 bold.font_weight = FontWeight::BOLD;
3872 let mut mono = base.clone();
3873 mono.font_family = MONO.into();
3874 let normal_code = HighlightStyle::default();
3875 let bold_code = HighlightStyle {
3876 font_weight: Some(FontWeight::BOLD),
3877 ..Default::default()
3878 };
3879 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3880 in_prepaint(&mut app, move |window, cx| {
3881 let mut previous = None;
3882 for (style, rem_size, family, inline_code, expected) in [
3883 (base.clone(), 20., MONO, normal_code, [117., 195.]),
3884 (base.clone(), 30., MONO, normal_code, [167., 282.5]),
3885 (larger, 20., MONO, normal_code, [217., 370.]),
3886 (bold, 20., MONO, normal_code, [167., 238.75]),
3887 (mono, 20., MONO, normal_code, [217., 195.]),
3888 (base.clone(), 20., MONO, normal_code, [117., 195.]),
3889 (base.clone(), 20., BODY, normal_code, [117., 107.5]),
3890 (base.clone(), 20., MONO, normal_code, [117., 195.]),
3891 (base, 20., MONO, bold_code, [117., 238.75]),
3892 ] {
3893 let mut theme = crate::Theme::default();
3894 theme.tokens.typography.mono = family.into();
3895 cx.set_global(theme);
3896 window.set_rem_size(px(rem_size));
3897 let node_cx = NodeContext {
3898 style: Arc::new(TextViewStyle::default().with_inline_code(inline_code)),
3899 ..Default::default()
3900 };
3901 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
3902 let widths = measure_table_columns(&table, 2, &node_cx, window, cx);
3903 for (width, expected) in widths.iter().zip(expected) {
3904 assert!((width - expected).abs() < 0.01, "{width} != {expected}");
3905 }
3906 let cached = cached_column_widths(&table);
3907 if let Some(previous) = previous.replace(cached.clone()) {
3908 assert!(!Arc::ptr_eq(&previous, &cached));
3909 }
3910 assert_eq!(
3911 measure_table_columns(&table, 2, &node_cx, window, cx),
3912 widths
3913 );
3914 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
3915 });
3916 }
3917 });
3918 }
3919
3920 #[test]
3921 fn table_columns_cache_is_rebuilt_for_reparsed_tables() {
3922 use crate::text::format::markdown;
3923 use crate::text::inline::test_draw::in_prepaint;
3924 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem};
3925 use gpui::{Refineable as _, TestApp, TextStyle};
3926
3927 let mut node_cx = NodeContext::default();
3928 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3929 in_prepaint(&mut app, move |window, cx| {
3930 let style = TextStyle {
3931 font_family: BODY.into(),
3932 font_size: px(20.).into(),
3933 ..Default::default()
3934 };
3935 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
3936 let mut previous = None;
3937 for (text, expected) in [("longer cell", 126.), ("x", CELL_MIN_PX)] {
3938 let source = format!("| h |\n| - |\n| {text} |\n");
3939 let document = markdown::parse(&source, &mut node_cx).unwrap();
3940 let BlockNode::Table(table) = &document.blocks[0] else {
3941 panic!("expected a parsed table");
3942 };
3943 assert!(table.column_widths_cache.0.lock().unwrap().is_none());
3944 assert_eq!(
3945 measure_table_columns(table, 1, &node_cx, window, cx),
3946 vec![expected]
3947 );
3948 let cached = cached_column_widths(table);
3949 if let Some(previous) = previous.replace(cached.clone()) {
3950 assert!(!Arc::ptr_eq(&previous, &cached));
3951 }
3952 assert_eq!(document.source.as_ref(), source);
3953 }
3954 });
3955 });
3956 }
3957
3958 #[test]
3962 fn table_column_of_inline_code_cells_fits_the_mono_width() {
3963 use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
3964 use gpui::{Empty, TestApp};
3965
3966 let code = "method_name()";
3967 let mut paragraph = Paragraph::default();
3968 paragraph
3969 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
3970 let table = Table {
3971 children: vec![TableRow {
3972 children: vec![TableCell {
3973 children: paragraph,
3974 width: None,
3975 }],
3976 }],
3977 column_aligns: vec![],
3978 span: None,
3979 table_data_cache: TableDataCache::default(),
3980 column_widths_cache: TableColumnWidthsCache::default(),
3981 };
3982 let node_cx = NodeContext::default();
3983
3984 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3985 let mut window = app.open_window(|_, _| Empty);
3986 let (col_w, font_size) = window.update(|_, window, cx| {
3987 let mut theme = crate::Theme::default();
3988 theme.tokens.typography.mono = MONO.into();
3989 cx.set_global(theme);
3990 let font_size = window.text_style().font_size.to_pixels(window.rem_size());
3991 (
3992 measure_table_columns(&table, 1, &node_cx, window, cx),
3993 font_size,
3994 )
3995 });
3996
3997 let mono_w = f32::from(WideMonoTextSystem::width_of(code, MONO, font_size * 0.875));
3998 assert!(
3999 (col_w[0]
4000 - (mono_w + CELL_PAD_PX + crate::text::inline_flow::INLINE_CODE_PADDING * 2.))
4001 .abs()
4002 < 0.01,
4003 "col_w {} must fit the mono width {} plus padding {}",
4004 col_w[0],
4005 mono_w,
4006 CELL_PAD_PX
4007 );
4008 }
4009
4010 #[test]
4011 fn code_block_highlights_are_cached_by_highlighter_identity() {
4012 use std::sync::atomic::{AtomicUsize, Ordering};
4013
4014 let calls = Arc::new(AtomicUsize::new(0));
4015 let calls_for_highlighter = calls.clone();
4016 let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
4017 calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
4018 Vec::new()
4019 });
4020 let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
4021
4022 block.highlighted_styles(&highlighter);
4023 block.highlighted_styles(&highlighter);
4024 assert_eq!(calls.load(Ordering::Relaxed), 1);
4025
4026 let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
4027 block.highlighted_styles(&replacement);
4028 assert!(Arc::ptr_eq(
4029 &block
4030 .highlight_cache
4031 .lock()
4032 .unwrap()
4033 .as_ref()
4034 .unwrap()
4035 .highlighter,
4036 &replacement
4037 ));
4038 }
4039
4040 #[test]
4041 fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
4042 let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
4046 vec![(
4047 0..2,
4048 HighlightStyle {
4049 color: Some(gpui::rgb(0x0000ff).into()),
4050 ..Default::default()
4051 },
4052 )]
4053 });
4054 let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
4055 vec![(
4056 0..2,
4057 HighlightStyle {
4058 color: Some(gpui::rgb(0xffff00).into()),
4059 ..Default::default()
4060 },
4061 )]
4062 });
4063 let block = CodeBlock::from_code("42", Some("json"));
4064
4065 let light_styles = block.highlighted_styles(&light);
4066 let dark_styles = block.highlighted_styles(&dark);
4067
4068 assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
4069 assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
4070 assert_eq!(block.code(), "42", "the document must survive the swap");
4071 }
4072
4073 #[test]
4074 fn reconstruct_markdown_wraps_marked_runs() {
4075 let marks = vec![(0..4, TextMark::default().bold())];
4077 assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
4078 assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
4080 }
4081
4082 #[test]
4083 fn reconstruct_markdown_emits_unmarked_text_verbatim() {
4084 let text = "a b c";
4086 let marks = vec![(2..3, TextMark::default().code())];
4087 assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
4088 assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
4090 }
4091
4092 #[test]
4093 fn reconstruct_markdown_handles_code_italic_strike_link() {
4094 assert_eq!(
4095 reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
4096 "`x`"
4097 );
4098 assert_eq!(
4099 reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
4100 "*x*"
4101 );
4102 assert_eq!(
4103 reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
4104 "~~x~~"
4105 );
4106 let link = TextMark::default().link(LinkMark {
4107 url: "https://example.com".into(),
4108 ..Default::default()
4109 });
4110 assert_eq!(
4111 reconstruct_markdown("x", &[(0..1, link)], 0..1),
4112 "[x](https://example.com)"
4113 );
4114 }
4115
4116 #[test]
4117 fn reconstruct_markdown_nested_bold_italic() {
4118 let mark = TextMark::default().bold().italic();
4120 assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
4122 }
4123
4124 fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
4128 let combined: String = children.iter().map(|c| c.text.to_string()).collect();
4129 let paragraph = Paragraph {
4130 span: None,
4131 children,
4132 link_refs: HashMap::new(),
4133 state: Arc::new(Mutex::new(InlineState::default())),
4134 render_cache: ParagraphRenderCache::default(),
4135 };
4136 if let Ok(mut state) = paragraph.state.lock() {
4137 state.set_text(combined.into());
4138 }
4139 paragraph
4140 }
4141
4142 fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
4143 if let Ok(mut state) = paragraph.state.lock() {
4144 state.selection = Some(range.into());
4145 }
4146 }
4147
4148 #[test]
4149 fn paragraph_selected_source_maps_partial_selection_across_runs() {
4150 let children = vec![
4152 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
4153 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
4154 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
4155 ];
4156 let paragraph = paragraph_with_children(children);
4157
4158 set_paragraph_selection(¶graph, 0..(9 + 4 + 6));
4160 assert_eq!(paragraph.selected_source(), "This has **bold** text.");
4161
4162 set_paragraph_selection(¶graph, 5..16);
4165 assert_eq!(paragraph.selected_source(), "has **bold** te");
4166
4167 set_paragraph_selection(¶graph, 10..12);
4169 assert_eq!(paragraph.selected_source(), "**ol**");
4170 }
4171
4172 #[test]
4173 fn paragraph_selected_source_matches_text_when_no_marks() {
4174 let children =
4175 vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
4176 let paragraph = paragraph_with_children(children);
4177 set_paragraph_selection(¶graph, 0..11);
4178 assert_eq!(paragraph.selected_source(), "plain words");
4179 assert_eq!(paragraph.selected_text(), "plain words");
4180 }
4181
4182 fn selected_paragraph(text: &str) -> Paragraph {
4183 let len = text.len();
4184 let paragraph = paragraph_with_children(vec![
4185 InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
4186 ]);
4187 set_paragraph_selection(¶graph, 0..len);
4188 paragraph
4189 }
4190
4191 #[test]
4192 fn heading_selected_source_prefixes_hashes() {
4193 let heading = BlockNode::Heading {
4194 level: 2,
4195 children: selected_paragraph("Title"),
4196 span: None,
4197 };
4198 assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
4199 assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
4201 }
4202
4203 #[test]
4204 fn unordered_list_selected_source_prefixes_dash() {
4205 let list = BlockNode::List {
4206 ordered: false,
4207 start: None,
4208 span: None,
4209 children: vec![
4210 BlockNode::ListItem {
4211 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
4212 spread: false,
4213 checked: None,
4214 span: None,
4215 },
4216 BlockNode::ListItem {
4217 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4218 spread: false,
4219 checked: None,
4220 span: None,
4221 },
4222 ],
4223 };
4224 assert_eq!(
4225 list.selected_text(SelectionFormat::Source),
4226 "- one\n- two\n"
4227 );
4228 }
4229
4230 #[test]
4231 fn ordered_list_selected_source_preserves_start() {
4232 let list = BlockNode::List {
4233 ordered: true,
4234 start: Some(3),
4235 span: None,
4236 children: vec![
4237 BlockNode::ListItem {
4238 children: vec![BlockNode::Paragraph(selected_paragraph("three"))],
4239 spread: false,
4240 checked: None,
4241 span: None,
4242 },
4243 BlockNode::ListItem {
4244 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4245 spread: false,
4246 checked: None,
4247 span: None,
4248 },
4249 ],
4250 };
4251 assert_eq!(list.to_markdown(), "3. three\n4. four");
4252 assert_eq!(
4253 list.selected_text(SelectionFormat::Source),
4254 "3. three\n4. four\n"
4255 );
4256 }
4257
4258 #[test]
4259 fn ordered_list_selected_source_preserves_nested_starts_and_continuations() {
4260 let nested = BlockNode::List {
4261 ordered: true,
4262 start: Some(4),
4263 span: None,
4264 children: vec![
4265 BlockNode::ListItem {
4266 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
4267 spread: false,
4268 checked: None,
4269 span: None,
4270 },
4271 BlockNode::ListItem {
4272 children: vec![BlockNode::Paragraph(selected_paragraph("again"))],
4273 spread: false,
4274 checked: None,
4275 span: None,
4276 },
4277 ],
4278 };
4279 let nested_list = BlockNode::List {
4280 ordered: true,
4281 start: Some(3),
4282 span: None,
4283 children: vec![
4284 BlockNode::ListItem {
4285 children: vec![BlockNode::Paragraph(selected_paragraph("three")), nested],
4286 spread: false,
4287 checked: None,
4288 span: None,
4289 },
4290 BlockNode::ListItem {
4291 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4292 spread: false,
4293 checked: None,
4294 span: None,
4295 },
4296 ],
4297 };
4298 assert_eq!(
4299 nested_list.selected_text(SelectionFormat::Source),
4300 "3. three\n 4. nested\n 5. again\n4. four\n"
4301 );
4302
4303 let loose_list = BlockNode::List {
4304 ordered: true,
4305 start: Some(3),
4306 span: None,
4307 children: vec![
4308 BlockNode::ListItem {
4309 children: vec![
4310 BlockNode::Paragraph(selected_paragraph("loose")),
4311 BlockNode::Paragraph(selected_paragraph("continuation")),
4312 ],
4313 spread: true,
4314 checked: None,
4315 span: None,
4316 },
4317 BlockNode::ListItem {
4318 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4319 spread: false,
4320 checked: None,
4321 span: None,
4322 },
4323 ],
4324 };
4325 assert_eq!(
4326 loose_list.selected_text(SelectionFormat::Source),
4327 "3. loose\n continuation\n4. four\n"
4328 );
4329 }
4330
4331 #[test]
4332 fn nested_list_selected_source_indents_sublists() {
4333 let nested = BlockNode::List {
4337 ordered: false,
4338 start: None,
4339 span: None,
4340 children: vec![BlockNode::ListItem {
4341 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
4342 spread: false,
4343 checked: None,
4344 span: None,
4345 }],
4346 };
4347 let list = BlockNode::List {
4348 ordered: false,
4349 start: None,
4350 span: None,
4351 children: vec![
4352 BlockNode::ListItem {
4353 children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
4354 spread: false,
4355 checked: None,
4356 span: None,
4357 },
4358 BlockNode::ListItem {
4359 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4360 spread: false,
4361 checked: None,
4362 span: None,
4363 },
4364 ],
4365 };
4366 assert_eq!(
4367 list.selected_text(SelectionFormat::Source),
4368 "- one\n - nested\n- two\n"
4369 );
4370 }
4371
4372 #[test]
4373 fn task_list_selected_source_restores_checkboxes() {
4374 let list = BlockNode::List {
4375 ordered: false,
4376 start: None,
4377 span: None,
4378 children: vec![
4379 BlockNode::ListItem {
4380 children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
4381 spread: false,
4382 checked: Some(true),
4383 span: None,
4384 },
4385 BlockNode::ListItem {
4386 children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
4387 spread: false,
4388 checked: Some(false),
4389 span: None,
4390 },
4391 ],
4392 };
4393 assert_eq!(
4394 list.selected_text(SelectionFormat::Source),
4395 "- [x] done\n- [ ] todo\n"
4396 );
4397 }
4398
4399 #[test]
4400 fn blockquote_selected_source_prefixes_gt() {
4401 let quote = BlockNode::Blockquote {
4402 span: None,
4403 children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
4404 };
4405 assert_eq!(
4406 quote.selected_text(SelectionFormat::Source),
4407 "> quoted text\n"
4408 );
4409 }
4410
4411 #[test]
4412 fn table_selected_source_pipes_cells_with_alignment_row() {
4413 let cell = |text: &str| TableCell {
4414 children: selected_paragraph(text),
4415 width: None,
4416 };
4417 let table = Table {
4418 children: vec![
4419 TableRow {
4420 children: vec![cell("Name"), cell("Age")],
4421 },
4422 TableRow {
4423 children: vec![cell("Alice"), cell("30")],
4424 },
4425 ],
4426 column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
4427 span: None,
4428 table_data_cache: TableDataCache::default(),
4429 column_widths_cache: TableColumnWidthsCache::default(),
4430 };
4431 let block = BlockNode::Table(table);
4432 assert_eq!(
4433 block.selected_text(SelectionFormat::Source),
4434 "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
4435 );
4436 }
4437
4438 fn plain_cell(text: &str) -> TableCell {
4441 TableCell {
4442 children: Paragraph::new(text.to_string()),
4443 width: None,
4444 }
4445 }
4446
4447 fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
4448 Table {
4449 children: rows
4450 .into_iter()
4451 .map(|children| TableRow { children })
4452 .collect(),
4453 column_aligns,
4454 span: None,
4455 table_data_cache: TableDataCache::default(),
4456 column_widths_cache: TableColumnWidthsCache::default(),
4457 }
4458 }
4459
4460 #[test]
4461 fn table_to_markdown_pipes_cells_with_alignment_row() {
4462 let table = table_of(
4463 vec![
4464 vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
4465 vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
4466 ],
4467 vec![
4468 ColumnumnAlign::Left,
4469 ColumnumnAlign::Center,
4470 ColumnumnAlign::Right,
4471 ],
4472 );
4473
4474 assert_eq!(
4475 table.to_markdown(),
4476 "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
4477 );
4478 assert_eq!(
4480 BlockNode::Table(table.clone()).to_markdown(),
4481 table.to_markdown()
4482 );
4483 }
4484
4485 #[test]
4486 fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
4487 let table = table_of(
4488 vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
4489 vec![ColumnumnAlign::Left],
4490 );
4491
4492 assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
4493 }
4494
4495 #[test]
4496 fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
4497 let bold = TableCell {
4498 children: paragraph_with_children(vec![
4499 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
4500 ]),
4501 width: None,
4502 };
4503 let table = table_of(
4504 vec![
4505 vec![plain_cell("a | b"), plain_cell("plain")],
4506 vec![plain_cell("c"), bold],
4507 ],
4508 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
4509 );
4510
4511 assert_eq!(
4512 table.to_markdown(),
4513 "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
4514 );
4515 }
4516
4517 #[test]
4518 fn table_data_snapshots_plain_cells_and_markdown() {
4519 let mut table = table_of(
4520 vec![
4521 vec![plain_cell(" Name "), plain_cell("Age")],
4522 vec![plain_cell("Alice"), plain_cell("30")],
4523 ],
4524 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
4525 );
4526 table.span = Some(Span { start: 4, end: 42 });
4527
4528 let data = table.table_data();
4529 assert_eq!(data.headers, vec!["Name", "Age"]);
4530 assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
4531 assert_eq!(data.markdown, table.to_markdown());
4532 assert_eq!(data.span, Some(4..42));
4533 }
4534
4535 #[test]
4536 fn cloned_table_keeps_cached_table_data() {
4537 let table = table_of(
4540 vec![vec![plain_cell("Name")], vec![plain_cell("Alice")]],
4541 vec![ColumnumnAlign::Left],
4542 );
4543 let data = table.cached_table_data();
4544
4545 let cloned = table.clone();
4546 assert!(Arc::ptr_eq(&data, &cloned.cached_table_data()));
4547 }
4548
4549 #[test]
4550 fn table_data_handles_tables_without_rows() {
4551 let header_only = table_of(
4553 vec![vec![plain_cell("Name"), plain_cell("Age")]],
4554 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
4555 );
4556 let data = header_only.table_data();
4557 assert_eq!(data.headers, vec!["Name", "Age"]);
4558 assert!(data.rows.is_empty());
4559 assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
4560
4561 assert_eq!(Table::default().table_data(), TableData::default());
4563 }
4564
4565 #[test]
4566 fn test_image_node_source() {
4567 use gpui::{ImageFormat, ImageSource, Resource};
4568
4569 fn image_node(url: &str) -> ImageNode {
4570 ImageNode {
4571 url: url.into(),
4572 ..Default::default()
4573 }
4574 }
4575
4576 fn assert_uri(url: &str) {
4580 match image_node(url).source() {
4581 ImageSource::Resource(Resource::Uri(uri)) => assert_eq!(uri.as_ref(), url),
4582 _ => panic!("expected Uri for {url:?}"),
4583 }
4584 }
4585 assert_uri("https://example.com/logo.png");
4586 assert_uri("http://example.com/logo.png");
4587 assert_uri("website/public/logo.svg");
4588 assert_uri("./images/a.png");
4589 assert_uri("../images/a.png");
4590 assert_uri("/absolute/path/logo.svg");
4591 assert_uri("file:///absolute/path/logo.svg");
4592 assert_uri(r"C:\images\logo.png");
4593 assert_uri("docs/a:b.png");
4594 assert_uri("data:text/plain;base64,aGVsbG8=");
4595
4596 let node = image_node("data:image/png;base64,iVBORw0KGgo=");
4599 let ImageSource::Image(first) = node.source() else {
4600 panic!("expected an embedded image");
4601 };
4602 assert_eq!(first.format(), ImageFormat::Png);
4603 assert_eq!(first.bytes(), b"\x89PNG\r\n\x1a\n");
4604 let ImageSource::Image(second) = node.source() else {
4605 panic!("expected an embedded image");
4606 };
4607 assert!(Arc::ptr_eq(&first, &second));
4608 }
4609
4610 fn image_paragraph(alt: &str, url: &str) -> Paragraph {
4611 let image = ImageNode {
4612 url: url.into(),
4613 alt: Some(alt.into()),
4614 ..Default::default()
4615 };
4616 Paragraph {
4617 span: None,
4618 children: vec![InlineNode::image(image)],
4619 link_refs: HashMap::new(),
4620 state: Arc::new(Mutex::new(InlineState::default())),
4621 render_cache: ParagraphRenderCache::default(),
4622 }
4623 }
4624
4625 #[test]
4628 fn marks_round_trip_through_reconstruction() {
4629 let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
4630
4631 assert_eq!(wrap(TextMark::default().bold()), "**x**");
4632 assert_eq!(wrap(TextMark::default().italic()), "*x*");
4633 assert_eq!(wrap(TextMark::default().code()), "`x`");
4634 assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
4635 assert_eq!(
4636 wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
4637 "==x=="
4638 );
4639 assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
4641
4642 assert_eq!(
4644 wrap(TextMark::default().link(LinkMark {
4645 url: "https://example.com".into(),
4646 title: Some("Tip".into()),
4647 ..Default::default()
4648 })),
4649 "[x](https://example.com \"Tip\")"
4650 );
4651 }
4652
4653 #[test]
4656 fn document_selected_source_slices_covered_blocks_from_the_source() {
4657 use crate::text::document::ParsedDocument;
4658
4659 let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
4662 let list = "3. _one_\n4. two";
4663 let list_start = source.find(list).unwrap();
4664 let rule_start = source.find("---").unwrap();
4665
4666 let document = ParsedDocument {
4667 source: source.into(),
4668 blocks: vec![
4669 BlockNode::Paragraph(selected_paragraph("start")),
4670 BlockNode::List {
4671 ordered: true,
4672 start: Some(3),
4673 children: vec![],
4674 span: Some(Span {
4675 start: list_start,
4676 end: list_start + list.len(),
4677 }),
4678 },
4679 BlockNode::HorizontalRule {
4680 span: Some(Span {
4681 start: rule_start,
4682 end: rule_start + 3,
4683 }),
4684 },
4685 BlockNode::Paragraph(selected_paragraph("end")),
4686 ]
4687 .into(),
4688 };
4689
4690 assert_eq!(
4691 document.selected_text(SelectionFormat::Source, None),
4692 "start\n\n3. _one_\n4. two\n\n---\n\nend"
4693 );
4694 }
4695
4696 #[test]
4697 fn document_selected_source_includes_enclosed_image() {
4698 use crate::text::document::ParsedDocument;
4699
4700 let source = "before\n\n\n\nafter";
4704 let image_markdown = "";
4705 let start = source.find(image_markdown).unwrap();
4706 let mut image = image_paragraph("alt", "https://example.com/i.png");
4707 image.span = Some(Span {
4708 start,
4709 end: start + image_markdown.len(),
4710 });
4711
4712 let document = ParsedDocument {
4713 source: source.into(),
4714 blocks: vec![
4715 BlockNode::Paragraph(selected_paragraph("before")),
4716 BlockNode::Paragraph(image),
4717 BlockNode::Paragraph(selected_paragraph("after")),
4718 ]
4719 .into(),
4720 };
4721 assert_eq!(
4722 document.selected_text(SelectionFormat::Source, None),
4723 "before\n\n\n\nafter"
4724 );
4725 }
4726
4727 #[test]
4728 fn document_selected_source_drops_unenclosed_image() {
4729 use crate::text::document::ParsedDocument;
4730
4731 let document = ParsedDocument {
4734 source: String::new().into(),
4735 blocks: vec![
4736 BlockNode::Paragraph(selected_paragraph("before")),
4737 BlockNode::Paragraph(image_paragraph("alt", "u")),
4738 ]
4739 .into(),
4740 };
4741 assert_eq!(
4742 document.selected_text(SelectionFormat::Source, None),
4743 "before"
4744 );
4745 }
4746
4747 fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
4748 let block = CodeBlock::new(
4749 code.to_string().into(),
4750 lang.map(|l| l.to_string().into()),
4751 None::<Span>,
4752 );
4753 if let Ok(mut state) = block.state.lock() {
4754 let len = state.text.len();
4755 state.selection = Some((0..len).into());
4756 }
4757 BlockNode::CodeBlock(block)
4758 }
4759
4760 #[test]
4761 fn code_block_selected_source_wraps_in_fence_with_lang() {
4762 let block = selected_code_block("let x = 1;\n", Some("rust"));
4763 let code = block.selected_text(SelectionFormat::Plain);
4764 let code_trimmed = code.trim_end_matches('\n');
4765 assert_eq!(
4769 block.selected_text(SelectionFormat::Source),
4770 format!("```rust\n{}\n```\n", code_trimmed)
4771 );
4772 assert!(
4773 block
4774 .selected_text(SelectionFormat::Source)
4775 .starts_with("```rust\n")
4776 );
4777 assert!(
4778 block
4779 .selected_text(SelectionFormat::Source)
4780 .trim_end()
4781 .ends_with("\n```")
4782 );
4783 }
4784
4785 #[test]
4786 fn code_block_selected_source_without_lang() {
4787 let block = selected_code_block("plain\n", None);
4788 let code_trimmed = block.selected_text(SelectionFormat::Plain);
4789 let code_trimmed = code_trimmed.trim_end_matches('\n');
4790 assert_eq!(
4791 block.selected_text(SelectionFormat::Source),
4792 format!("```\n{}\n```\n", code_trimmed)
4793 );
4794 }
4795
4796 #[test]
4797 fn document_selected_source_joins_blocks_with_blank_line() {
4798 use crate::text::document::ParsedDocument;
4799
4800 let document = ParsedDocument {
4804 source: String::new().into(),
4805 blocks: vec![
4806 BlockNode::Heading {
4807 level: 1,
4808 children: selected_paragraph("Title"),
4809 span: None,
4810 },
4811 BlockNode::Paragraph(selected_paragraph("A paragraph.")),
4812 selected_code_block("let x = 1;\n", Some("rust")),
4813 BlockNode::List {
4814 ordered: true,
4815 start: Some(1),
4816 span: None,
4817 children: vec![
4818 BlockNode::ListItem {
4819 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
4820 spread: false,
4821 checked: None,
4822 span: None,
4823 },
4824 BlockNode::ListItem {
4825 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4826 spread: false,
4827 checked: None,
4828 span: None,
4829 },
4830 ],
4831 },
4832 ]
4833 .into(),
4834 };
4835
4836 assert_eq!(
4837 document.selected_text(SelectionFormat::Source, None),
4838 "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
4839 );
4840 }
4841
4842 #[test]
4843 fn code_block_equality_includes_code_content() {
4844 let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
4845 let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
4846
4847 assert_ne!(first, second);
4848 }
4849}