1use std::{
2 collections::HashMap,
3 ops::Range,
4 sync::{Arc, Mutex, OnceLock},
5};
6
7use gpui::{
8 AnyElement, App, DefiniteLength, Div, ElementId, FontStyle, FontWeight, HighlightStyle, Hsla,
9 Image, ImageFormat, ImageSource, InteractiveElement as _, IntoElement, IsZero as _, Length,
10 ObjectFit, Overflow, ParentElement, Pixels, Rems, ScrollHandle, SharedString, SharedUri,
11 StatefulInteractiveElement, StyleRefinement, Styled, StyledImage as _, WhiteSpace, Window, div,
12 img, prelude::FluentBuilder as _, px, relative, rems,
13};
14use markdown::mdast;
15
16use crate::{
17 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 stream_fade::{StreamFadeFrame, TextLeafKey},
29 text_view::handle_link_click,
30 },
31 theme::ActiveTheme as _,
32};
33
34use super::{
35 SelectionFormat, TextViewStyle,
36 utils::{data_url_image, list_item_prefix},
37};
38
39const 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>"#;
40const 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>"#;
41
42#[derive(Debug, Clone, PartialEq)]
44pub(crate) enum BlockNode {
45 Root {
47 children: Vec<BlockNode>,
48 span: Option<Span>,
49 },
50 Paragraph(Paragraph),
51 Heading {
52 level: u8,
53 children: Paragraph,
54 span: Option<Span>,
55 },
56 Blockquote {
57 children: Vec<BlockNode>,
58 span: Option<Span>,
59 },
60 List {
61 children: Vec<BlockNode>,
63 ordered: bool,
64 span: Option<Span>,
65 },
66 ListItem {
67 children: Vec<BlockNode>,
68 spread: bool,
69 checked: Option<bool>,
71 span: Option<Span>,
72 },
73 CodeBlock(CodeBlock),
74 Custom(MarkdownNode),
76 Table(Table),
77 Break {
78 html: bool,
79 span: Option<Span>,
80 },
81 HorizontalRule {
82 span: Option<Span>,
83 },
84 Definition {
86 identifier: SharedString,
87 url: SharedString,
88 title: Option<SharedString>,
89 span: Option<Span>,
90 },
91 Unknown,
92}
93
94#[derive(Clone, Copy)]
95enum BlockTextKind {
96 All,
97 Selected,
98 SelectedSource,
101}
102
103impl BlockNode {
104 pub(super) fn is_list_item(&self) -> bool {
105 matches!(self, Self::ListItem { .. })
106 }
107
108 pub(super) fn compact(self) -> BlockNode {
110 match self {
111 Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
112 _ => self,
113 }
114 }
115
116 pub(crate) fn span(&self) -> Option<Span> {
118 match self {
119 BlockNode::Root { span, .. } => *span,
120 BlockNode::Paragraph(paragraph) => paragraph.span,
121 BlockNode::Heading { span, .. } => *span,
122 BlockNode::Blockquote { span, .. } => *span,
123 BlockNode::List { span, .. } => *span,
124 BlockNode::ListItem { span, .. } => *span,
125 BlockNode::CodeBlock(code_block) => code_block.span,
126 BlockNode::Custom(el) => el.span,
127 BlockNode::Table(table) => table.span,
128 BlockNode::Break { span, .. } => *span,
129 BlockNode::HorizontalRule { span, .. } => *span,
130 BlockNode::Definition { span, .. } => *span,
131 BlockNode::Unknown { .. } => None,
132 }
133 }
134
135 pub(super) fn text(&self) -> String {
136 self.text_by_kind(BlockTextKind::All)
137 }
138
139 pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
144 self.text_by_kind(match format {
145 SelectionFormat::Plain => BlockTextKind::Selected,
146 SelectionFormat::Source => BlockTextKind::SelectedSource,
147 })
148 }
149
150 fn text_by_kind(&self, kind: BlockTextKind) -> String {
151 let mut text = String::new();
152 match self {
153 BlockNode::Root { children, .. } => {
154 let block_text = Self::children_text(children, kind);
155 if !block_text.is_empty() {
156 text.push_str(&block_text);
157 text.push('\n');
158 }
159 }
160 BlockNode::Paragraph(paragraph) => {
161 let block_text = match kind {
162 BlockTextKind::All => paragraph.text(),
163 BlockTextKind::Selected => paragraph.selected_text(),
164 BlockTextKind::SelectedSource => paragraph.selected_source(),
165 };
166 if !block_text.is_empty() {
167 text.push_str(&block_text);
168 text.push('\n');
169 }
170 }
171 BlockNode::Heading {
172 level, children, ..
173 } => {
174 let block_text = match kind {
175 BlockTextKind::All => children.text(),
176 BlockTextKind::Selected => children.selected_text(),
177 BlockTextKind::SelectedSource => children.selected_source(),
178 };
179 if !block_text.is_empty() {
180 if matches!(kind, BlockTextKind::SelectedSource) {
183 text.push_str(&"#".repeat(*level as usize));
184 text.push(' ');
185 }
186 text.push_str(&block_text);
187 text.push('\n');
188 }
189 }
190 BlockNode::List {
191 children, ordered, ..
192 } => {
193 if matches!(kind, BlockTextKind::SelectedSource) {
194 text.push_str(&list_selected_source(children, *ordered, ""));
197 } else {
198 text.push_str(&Self::children_text(children, kind));
199 }
200 }
201 BlockNode::ListItem { children, .. } => {
202 text.push_str(&Self::children_text(children, kind));
203 }
204 BlockNode::Blockquote { children, .. } => {
205 let block_text = Self::children_text(children, kind);
206
207 if !block_text.is_empty() {
208 if matches!(kind, BlockTextKind::SelectedSource) {
209 let quoted = block_text
212 .trim_end_matches('\n')
213 .lines()
214 .map(|line| {
215 if line.is_empty() {
216 ">".to_string()
217 } else {
218 format!("> {}", line)
219 }
220 })
221 .collect::<Vec<_>>()
222 .join("\n");
223 text.push_str("ed);
224 } else {
225 text.push_str(&block_text);
226 }
227 text.push('\n');
228 }
229 }
230 BlockNode::Table(table) => {
231 if matches!(kind, BlockTextKind::SelectedSource) {
232 let block_text = table_selected_source(table);
233 if !block_text.is_empty() {
234 text.push_str(&block_text);
235 text.push('\n');
236 }
237 } else {
238 let mut block_text = String::new();
239 for row in table.children.iter() {
240 let mut row_texts = vec![];
241 for cell in row.children.iter() {
242 row_texts.push(match kind {
243 BlockTextKind::All => cell.children.text(),
244 _ => cell.children.selected_text(),
246 });
247 }
248 if !row_texts.is_empty() {
249 block_text.push_str(&row_texts.join(" "));
250 block_text.push('\n');
251 }
252 }
253
254 if !block_text.is_empty() {
255 text.push_str(&block_text);
256 text.push('\n');
257 }
258 }
259 }
260 BlockNode::CodeBlock(code_block) => {
261 let block_text = match kind {
262 BlockTextKind::All => code_block.text(),
263 BlockTextKind::Selected => code_block.selected_text(),
264 BlockTextKind::SelectedSource => code_block.selected_source(),
265 };
266 if !block_text.is_empty() {
267 text.push_str(&block_text);
268 text.push('\n');
269 }
270 }
271 BlockNode::Custom(node) => {
272 if let BlockTextKind::All = kind {
273 let content = node.as_text();
274 if !content.is_empty() {
275 text.push_str(content);
276 text.push('\n');
277 }
278 }
279 }
280 BlockNode::Definition { .. }
281 | BlockNode::Break { .. }
282 | BlockNode::HorizontalRule { .. }
283 | BlockNode::Unknown { .. } => {}
284 }
285
286 text
287 }
288
289 fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
290 let mut text = String::new();
291 for child in children.iter() {
292 text.push_str(&child.text_by_kind(kind));
293 }
294
295 text
296 }
297
298 pub(super) fn has_selection(&self) -> bool {
309 match self {
310 BlockNode::Root { children, .. }
311 | BlockNode::Blockquote { children, .. }
312 | BlockNode::List { children, .. }
313 | BlockNode::ListItem { children, .. } => {
314 children.iter().any(|child| child.has_selection())
315 }
316 BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
317 BlockNode::Heading { children, .. } => children.has_selection(),
318 BlockNode::Table(table) => table.children.iter().any(|row| {
319 row.children
320 .iter()
321 .any(|cell| cell.children.has_selection())
322 }),
323 BlockNode::CodeBlock(code_block) => code_block.has_selection(),
324 BlockNode::Custom { .. }
325 | BlockNode::Definition { .. }
326 | BlockNode::Break { .. }
327 | BlockNode::HorizontalRule { .. }
328 | BlockNode::Unknown { .. } => false,
329 }
330 }
331
332 pub(super) fn clear_selection(&self) {
333 match self {
334 BlockNode::Root { children, .. }
335 | BlockNode::Blockquote { children, .. }
336 | BlockNode::List { children, .. }
337 | BlockNode::ListItem { children, .. } => {
338 for child in children.iter() {
339 child.clear_selection();
340 }
341 }
342 BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
343 BlockNode::Heading { children, .. } => children.clear_selection(),
344 BlockNode::Table(table) => {
345 for row in table.children.iter() {
346 for cell in row.children.iter() {
347 cell.children.clear_selection();
348 }
349 }
350 }
351 BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
352 BlockNode::Custom { .. }
353 | BlockNode::Definition { .. }
354 | BlockNode::Break { .. }
355 | BlockNode::HorizontalRule { .. }
356 | BlockNode::Unknown { .. } => {}
357 }
358 }
359}
360
361#[allow(unused)]
362#[derive(Debug, Default, Clone, PartialEq)]
363pub struct LinkMark {
364 pub url: SharedString,
365 pub identifier: Option<SharedString>,
367 pub title: Option<SharedString>,
368}
369
370#[derive(Debug, Default, Clone, PartialEq)]
371pub struct TextMark {
372 pub bold: bool,
373 pub italic: bool,
374 pub strikethrough: bool,
375 pub underline: bool,
376 pub code: bool,
377 pub highlight: Option<Hsla>,
381 pub link: Option<LinkMark>,
382}
383
384impl TextMark {
385 pub fn bold(mut self) -> Self {
386 self.bold = true;
387 self
388 }
389
390 pub fn italic(mut self) -> Self {
391 self.italic = true;
392 self
393 }
394
395 pub fn strikethrough(mut self) -> Self {
396 self.strikethrough = true;
397 self
398 }
399
400 pub fn underline(mut self) -> Self {
401 self.underline = true;
402 self
403 }
404
405 pub fn code(mut self) -> Self {
406 self.code = true;
407 self
408 }
409
410 pub fn highlight(mut self, color: Hsla) -> Self {
412 self.highlight = Some(color);
413 self
414 }
415
416 pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
417 self.link = Some(link.into());
418 self
419 }
420
421 pub fn merge(&mut self, other: TextMark) {
422 self.bold |= other.bold;
423 self.italic |= other.italic;
424 self.strikethrough |= other.strikethrough;
425 self.underline |= other.underline;
426 self.code |= other.code;
427 if other.highlight.is_some() {
428 self.highlight = other.highlight;
429 }
430 if let Some(link) = other.link {
431 self.link = Some(link);
432 }
433 }
434}
435
436#[derive(Debug, Default, Copy, Clone, PartialEq)]
438pub struct Span {
439 pub start: usize,
440 pub end: usize,
441}
442
443#[allow(unused)]
444#[derive(Default, Clone)]
445pub struct ImageNode {
446 pub url: SharedUri,
447 pub link: Option<LinkMark>,
448 pub title: Option<SharedString>,
449 pub alt: Option<SharedString>,
450 pub width: Option<DefiniteLength>,
451 pub height: Option<DefiniteLength>,
452 pub(super) embedded: OnceLock<Option<Arc<Image>>>,
455}
456
457impl ImageNode {
458 pub fn title(&self) -> String {
459 self.title
460 .clone()
461 .unwrap_or_else(|| self.alt.clone().unwrap_or_default())
462 .to_string()
463 }
464
465 pub(super) fn source(&self) -> ImageSource {
473 match self.embedded.get_or_init(|| data_url_image(&self.url)) {
474 Some(image) => ImageSource::Image(image.clone()),
475 None => self.url.clone().into(),
476 }
477 }
478}
479
480impl std::fmt::Debug for ImageNode {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 f.debug_struct("ImageNode")
483 .field("url", &self.url)
484 .field("link", &self.link)
485 .field("title", &self.title)
486 .field("alt", &self.alt)
487 .field("width", &self.width)
488 .field("height", &self.height)
489 .finish()
490 }
491}
492
493impl PartialEq for ImageNode {
494 fn eq(&self, other: &Self) -> bool {
495 self.url == other.url
496 && self.link == other.link
497 && self.title == other.title
498 && self.alt == other.alt
499 && self.width == other.width
500 && self.height == other.height
501 }
502}
503
504#[derive(Default, Clone, Debug)]
505pub(crate) struct InlineNode {
506 pub(crate) text: SharedString,
508 pub(crate) image: Option<ImageNode>,
509 pub(crate) custom: Option<MarkdownNode>,
510 custom_selection: Arc<Mutex<bool>>,
511 pub(crate) marks: Vec<(Range<usize>, TextMark)>,
513
514 state: Arc<Mutex<InlineState>>,
515}
516
517impl PartialEq for InlineNode {
518 fn eq(&self, other: &Self) -> bool {
519 self.text == other.text
520 && self.image == other.image
521 && self.custom == other.custom
522 && self.marks == other.marks
523 }
524}
525
526pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
533 if text.is_empty() {
534 return String::new();
535 }
536
537 let mut out = text.to_string();
538 if mark.code {
539 out = format!("`{}`", out);
540 }
541 if mark.italic {
542 out = format!("*{}*", out);
543 }
544 if mark.bold {
545 out = format!("**{}**", out);
546 }
547 if mark.strikethrough {
548 out = format!("~~{}~~", out);
549 }
550 if mark.underline {
551 out = format!("<u>{}</u>", out);
554 }
555 if mark.highlight.is_some() {
556 out = format!("=={}==", out);
557 }
558 if let Some(link) = &mark.link {
559 out = match &link.title {
560 Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
561 None => format!("[{}]({})", out, link.url),
562 };
563 }
564 out
565}
566
567#[derive(Default)]
571struct MarkdownSource {
572 pieces: Vec<(String, Vec<TextMark>)>,
573}
574
575impl MarkdownSource {
576 fn push_str(&mut self, source: &str) {
577 self.push_marked(source, &TextMark::default());
578 }
579
580 fn push_marked(&mut self, source: &str, mark: &TextMark) {
581 if source.is_empty() {
582 return;
583 }
584 let mut layers = Vec::new();
587 if let Some(link) = &mark.link {
588 layers.push(TextMark {
589 link: Some(link.clone()),
590 ..Default::default()
591 });
592 }
593 if let Some(highlight) = mark.highlight {
594 layers.push(TextMark {
595 highlight: Some(highlight),
596 ..Default::default()
597 });
598 }
599 if mark.underline {
600 layers.push(TextMark::default().underline());
601 }
602 if mark.strikethrough {
603 layers.push(TextMark::default().strikethrough());
604 }
605 if mark.bold {
606 layers.push(TextMark::default().bold());
607 }
608 if mark.italic {
609 layers.push(TextMark::default().italic());
610 }
611 if mark.code {
612 layers.push(TextMark::default().code());
613 }
614 self.pieces.push((source.to_string(), layers));
615 }
616
617 fn push_text(
618 &mut self,
619 text: &str,
620 marks: &[(Range<usize>, TextMark)],
621 selection: Range<usize>,
622 ) {
623 let start = selection.start.min(text.len());
624 let end = selection.end.min(text.len());
625 if start >= end {
626 return;
627 }
628 let mut cursor = start;
629 for (range, mark) in marks {
630 let lo = range.start.max(start);
631 let hi = range.end.min(end);
632 if lo >= hi {
633 continue;
634 }
635 if cursor < lo {
636 self.push_str(&text[cursor..lo]);
637 }
638 self.push_marked(&text[lo..hi], mark);
639 cursor = hi;
640 }
641 if cursor < end {
642 self.push_str(&text[cursor..end]);
643 }
644 }
645
646 fn push_object(&mut self, node: &InlineNode) {
647 let mut mark = TextMark::default();
648 for (_, layer) in &node.marks {
649 mark.merge(layer.clone());
650 }
651 self.push_marked(&node.custom.as_ref().unwrap().to_markdown(), &mark);
652 }
653
654 fn is_empty(&self) -> bool {
655 self.pieces.is_empty()
656 }
657
658 fn finish(mut self) -> String {
659 fn write(pieces: &mut [(String, Vec<TextMark>)]) -> String {
660 let mut out = String::new();
661 let mut offset = 0;
662 while offset < pieces.len() {
663 let Some((mark, count)) = pieces[offset]
664 .1
665 .iter()
666 .map(|mark| {
667 let count = pieces[offset..]
668 .iter()
669 .take_while(|(_, layers)| layers.contains(mark))
670 .count();
671 (mark.clone(), count)
672 })
673 .reduce(|best, candidate| {
674 if candidate.1 > best.1 {
675 candidate
676 } else {
677 best
678 }
679 })
680 else {
681 out.push_str(&pieces[offset].0);
682 offset += 1;
683 continue;
684 };
685 let group = &mut pieces[offset..offset + count];
686 for (_, layers) in group.iter_mut() {
687 layers.retain(|layer| layer != &mark);
688 }
689 let content = write(group);
690 out.push_str(&wrap_with_mark(&content, &mark));
691 offset += count;
692 }
693 out
694 }
695 write(&mut self.pieces)
696 }
697}
698
699#[derive(Default)]
702struct RunSelection {
703 emitted: bool,
704 at_start: bool,
705 at_end: bool,
706}
707
708fn emit_run(
712 state: &Arc<Mutex<InlineState>>,
713 run: &[(usize, &InlineNode)],
714 pending_images: &mut Vec<String>,
715 out: &mut MarkdownSource,
716) -> RunSelection {
717 let mut selected = RunSelection::default();
718 let Ok(state) = state.lock() else {
719 return selected;
720 };
721 let Some(selection) = &state.selection else {
722 return selected;
723 };
724 if selection.start >= selection.end {
725 return selected;
726 }
727
728 selected.at_start = selection.start == 0;
729 selected.at_end = selection.end >= state.text.len();
730
731 for (start, child) in run {
732 let end = start + child.text.len();
733 let lo = selection.start.max(*start);
734 let hi = selection.end.min(end);
735 if lo >= hi {
736 continue;
737 }
738
739 if !selected.emitted {
740 if selected.at_start {
741 out.push_str(&pending_images.join(""));
742 }
743 pending_images.clear();
744 }
745 selected.emitted = true;
746
747 out.push_text(&child.text, &child.marks, (lo - start)..(hi - start));
748 }
749
750 selected
751}
752
753fn image_markdown(image: &ImageNode) -> String {
755 let alt = image.alt.clone().unwrap_or_default();
756 let title = image
757 .title
758 .clone()
759 .map_or(String::new(), |title| format!(" \"{}\"", title));
760 format!("", alt, image.url, title)
761}
762
763#[cfg(test)]
772pub(crate) fn reconstruct_markdown(
773 text: &str,
774 marks: &[(Range<usize>, TextMark)],
775 selection: Range<usize>,
776) -> String {
777 let mut source = MarkdownSource::default();
778 source.push_text(text, marks, selection);
779 source.finish()
780}
781
782fn table_selected_source(table: &Table) -> String {
789 let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
790
791 let any_selected = table.children.iter().any(|row| {
792 row.children
793 .iter()
794 .any(|cell| !cell_source(cell).trim().is_empty())
795 });
796 if !any_selected {
797 return String::new();
798 }
799
800 let mut lines: Vec<String> = Vec::new();
801 for (row_ix, row) in table.children.iter().enumerate() {
802 let cells: Vec<String> = row
803 .children
804 .iter()
805 .map(|cell| cell_source(cell).trim().to_string())
806 .collect();
807 lines.push(format!("| {} |", cells.join(" | ")));
808
809 if row_ix == 0 {
812 let aligns: Vec<String> = (0..row.children.len())
813 .map(|ix| {
814 match table.column_align(ix) {
815 ColumnumnAlign::Left => ":--",
816 ColumnumnAlign::Center => ":-:",
817 ColumnumnAlign::Right => "--:",
818 }
819 .to_string()
820 })
821 .collect();
822 lines.push(format!("| {} |", aligns.join(" | ")));
823 }
824 }
825
826 lines.join("\n")
827}
828
829fn list_selected_source(children: &[BlockNode], ordered: bool, indent: &str) -> String {
839 let mut out = String::new();
840 let mut item_ix = 0usize;
841
842 for child in children {
843 let BlockNode::ListItem {
844 children: item_children,
845 checked,
846 ..
847 } = child
848 else {
849 continue;
850 };
851
852 let marker = if ordered {
853 format!("{}. ", item_ix + 1)
854 } else {
855 "- ".to_string()
856 };
857 let checkbox = match checked {
858 Some(true) => "[x] ",
859 Some(false) => "[ ] ",
860 None => "",
861 };
862 let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
863
864 let mut content = String::new();
867 let mut nested = String::new();
868 for sub in item_children {
869 if let BlockNode::List {
870 children: sub_children,
871 ordered: sub_ordered,
872 ..
873 } = sub
874 {
875 nested.push_str(&list_selected_source(
876 sub_children,
877 *sub_ordered,
878 &child_indent,
879 ));
880 } else {
881 content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
882 }
883 }
884 let content = content.trim_end_matches('\n');
885
886 if content.is_empty() && nested.is_empty() {
887 item_ix += 1;
888 continue;
889 }
890
891 if content.is_empty() {
892 out.push_str(indent);
894 out.push_str(&marker);
895 out.push_str(checkbox.trim_end());
896 out.push('\n');
897 } else {
898 let mut lines = content.lines();
901 if let Some(first) = lines.next() {
902 out.push_str(indent);
903 out.push_str(&marker);
904 out.push_str(checkbox);
905 out.push_str(first);
906 out.push('\n');
907 }
908 for line in lines {
909 out.push_str(&child_indent);
910 out.push_str(line);
911 out.push('\n');
912 }
913 }
914 out.push_str(&nested);
915 item_ix += 1;
916 }
917
918 out
919}
920
921impl InlineNode {
922 pub(crate) fn new(text: impl Into<SharedString>) -> Self {
923 Self {
924 text: text.into(),
925 image: None,
926 custom: None,
927 custom_selection: Arc::default(),
928 marks: vec![],
929 state: Arc::new(Mutex::new(InlineState::default())),
930 }
931 }
932
933 pub(crate) fn custom(node: MarkdownNode) -> Self {
934 let mut this = Self::new(node.as_text().to_string());
935 this.custom = Some(node);
936 this
937 }
938
939 pub(crate) fn image(image: ImageNode) -> Self {
940 let mut this = Self::new("");
941 this.image = Some(image);
942 this
943 }
944
945 pub(crate) fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
946 self.marks = marks;
947 self
948 }
949}
950
951#[derive(Debug, Clone, Default)]
956pub(crate) struct Paragraph {
957 pub(super) span: Option<Span>,
958 pub(super) children: Vec<InlineNode>,
959 pub(super) link_refs: HashMap<SharedString, SharedString>,
963
964 pub(crate) state: Arc<Mutex<InlineState>>,
965 pub(super) render_cache: ParagraphRenderCache,
968}
969
970#[derive(Default)]
973pub(super) struct ParagraphRenderCache(Mutex<Option<ParagraphRender>>);
974
975impl Clone for ParagraphRenderCache {
976 fn clone(&self) -> Self {
977 Self::default()
978 }
979}
980
981impl std::fmt::Debug for ParagraphRenderCache {
982 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
983 f.write_str("ParagraphRenderCache")
984 }
985}
986
987struct ParagraphRender {
997 style: Arc<TextViewStyle>,
998 mono_font: SharedString,
999 text: SharedString,
1000 highlights: Vec<(Range<usize>, InlineHighlight)>,
1001 links: Vec<(Range<usize>, LinkMark)>,
1003}
1004
1005impl PartialEq for Paragraph {
1006 fn eq(&self, other: &Self) -> bool {
1007 self.span == other.span
1008 && self.children == other.children
1009 && self.link_refs == other.link_refs
1010 }
1011}
1012
1013impl Paragraph {
1014 pub(crate) fn new(text: String) -> Self {
1015 Self {
1016 span: None,
1017 children: vec![InlineNode::new(&text)],
1018 link_refs: HashMap::new(),
1019 state: Arc::new(Mutex::new(InlineState::default())),
1020 render_cache: ParagraphRenderCache::default(),
1021 }
1022 }
1023
1024 fn plain_render(
1027 &self,
1028 node_cx: &NodeContext,
1029 cx: &App,
1030 ) -> (
1031 SharedString,
1032 Vec<(Range<usize>, InlineHighlight)>,
1033 Vec<(Range<usize>, LinkMark)>,
1034 ) {
1035 let mono_font = cx.theme().tokens.typography.mono.clone();
1036 if let Ok(cache) = self.render_cache.0.lock()
1037 && let Some(cached) = cache.as_ref()
1038 && (Arc::ptr_eq(&cached.style, &node_cx.style) || *cached.style == *node_cx.style)
1039 && cached.mono_font == mono_font
1040 {
1041 return (
1042 cached.text.clone(),
1043 cached.highlights.clone(),
1044 cached.links.clone(),
1045 );
1046 }
1047
1048 let mut text = String::new();
1049 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1050 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1051 let mut offset = 0;
1052 for inline_node in &self.children {
1053 let text_len = inline_node.text.len();
1054 text.push_str(&inline_node.text);
1055 let mut node_highlights = vec![];
1056 for (range, style) in &inline_node.marks {
1057 let inner_range = (offset + range.start)..(offset + range.end);
1058 let mut highlight = mark_highlight(style, node_cx, cx);
1059 if let Some(link_mark) = style.link.clone() {
1060 highlight.style.color = Some(node_cx.style.link());
1061 highlight.style.underline = Some(gpui::UnderlineStyle {
1062 thickness: gpui::px(1.),
1063 ..Default::default()
1064 });
1065 links.push((inner_range.clone(), link_mark));
1066 }
1067 node_highlights.push((inner_range, highlight));
1068 }
1069 highlights = combine_highlights(highlights, node_highlights);
1070 offset += text_len;
1071 }
1072 let text = SharedString::from(text);
1073 if let Ok(mut cache) = self.render_cache.0.lock() {
1074 *cache = Some(ParagraphRender {
1075 style: node_cx.style.clone(),
1076 mono_font,
1077 text: text.clone(),
1078 highlights: highlights.clone(),
1079 links: links.clone(),
1080 });
1081 }
1082 (text, highlights, links)
1083 }
1084
1085 pub(super) fn selected_text(&self) -> String {
1086 let mut text = String::new();
1087
1088 for c in self.children.iter() {
1089 let Ok(state) = c.state.lock() else {
1090 continue;
1091 };
1092 if let Some(selection) = &state.selection {
1093 text.push_str(&state.text[selection.start..selection.end]);
1094 }
1095 if let Some(custom) = &c.custom
1096 && c.custom_selection.lock().is_ok_and(|selected| *selected)
1097 {
1098 text.push_str(custom.as_text());
1099 }
1100 }
1101
1102 if let Ok(state) = self.state.lock()
1103 && let Some(selection) = &state.selection
1104 {
1105 text.push_str(&state.text[selection.start..selection.end]);
1106 }
1107
1108 text
1109 }
1110
1111 pub(super) fn selected_source(&self) -> String {
1131 let mut source = MarkdownSource::default();
1132 let mut pending_images: Vec<String> = Vec::new();
1133 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1134 let mut offset = 0;
1135 let mut enters_image = true;
1136
1137 for child in self.children.iter() {
1138 if child.custom.is_some() {
1139 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1140 let object_selected = child
1141 .custom_selection
1142 .lock()
1143 .is_ok_and(|selected| *selected);
1144 if object_selected {
1145 if run.is_empty() || (selected.emitted && selected.at_end) {
1146 source.push_str(&pending_images.join(""));
1147 }
1148 source.push_object(child);
1149 }
1150 pending_images.clear();
1151 enters_image = object_selected;
1152 run.clear();
1153 offset = 0;
1154 continue;
1155 }
1156 let Some(image) = &child.image else {
1157 run.push((offset, child));
1158 offset += child.text.len();
1159 continue;
1160 };
1161
1162 let run_before = !run.is_empty();
1164 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1165 if run_before {
1166 enters_image = selected.emitted && selected.at_end;
1167 }
1168 if enters_image {
1169 pending_images.push(image_markdown(image));
1170 } else {
1171 pending_images.clear();
1172 }
1173
1174 run.clear();
1175 offset = 0;
1176 }
1177
1178 let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
1179 if !trailing.emitted && enters_image && !source.is_empty() {
1181 source.push_str(&pending_images.join(""));
1182 }
1183
1184 source.finish()
1185 }
1186
1187 pub(super) fn text(&self) -> String {
1188 let mut text = String::new();
1189 for node in self.children.iter() {
1190 text.push_str(&node.text);
1191 }
1192 text
1193 }
1194
1195 pub(super) fn has_selection(&self) -> bool {
1199 self.children.iter().any(|c| {
1200 c.state.lock().is_ok_and(|state| state.selection.is_some())
1201 || c.custom_selection.lock().is_ok_and(|selected| *selected)
1202 }) || self
1203 .state
1204 .lock()
1205 .is_ok_and(|state| state.selection.is_some())
1206 }
1207
1208 pub(super) fn clear_selection(&self) {
1209 for c in self.children.iter() {
1210 if let Ok(mut selected) = c.custom_selection.lock() {
1211 *selected = false;
1212 }
1213 if let Ok(mut state) = c.state.lock() {
1214 state.selection = None;
1215 }
1216 }
1217
1218 if let Ok(mut state) = self.state.lock() {
1219 state.selection = None;
1220 }
1221 }
1222}
1223
1224#[derive(Debug, Clone, Default, PartialEq)]
1225pub(crate) struct Table {
1226 pub(crate) children: Vec<TableRow>,
1227 pub(crate) column_aligns: Vec<ColumnumnAlign>,
1228 pub(crate) span: Option<Span>,
1229}
1230
1231#[derive(Debug, Clone, Default, PartialEq)]
1234pub struct TableData {
1235 pub headers: Vec<String>,
1237 pub rows: Vec<Vec<String>>,
1240 pub markdown: String,
1242 pub span: Option<Range<usize>>,
1249}
1250
1251impl Table {
1252 pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
1253 self.column_aligns.get(index).copied().unwrap_or_default()
1254 }
1255
1256 pub(crate) fn to_markdown(&self) -> String {
1263 let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
1264
1265 for (row_ix, row) in self.children.iter().enumerate() {
1266 let cells: Vec<String> = row
1267 .children
1268 .iter()
1269 .map(|cell| {
1270 cell.children
1271 .to_markdown()
1272 .trim()
1273 .replace('\n', " ")
1274 .replace('|', "\\|")
1275 })
1276 .collect();
1277 lines.push(format!("| {} |", cells.join(" | ")));
1278
1279 if row_ix == 0 {
1282 let aligns: Vec<String> = (0..row.children.len())
1283 .map(|ix| {
1284 match self.column_align(ix) {
1285 ColumnumnAlign::Left => ":--",
1286 ColumnumnAlign::Center => ":-:",
1287 ColumnumnAlign::Right => "--:",
1288 }
1289 .to_string()
1290 })
1291 .collect();
1292 lines.push(format!("| {} |", aligns.join(" | ")));
1293 }
1294 }
1295
1296 lines.join("\n")
1297 }
1298
1299 pub(crate) fn table_data(&self) -> TableData {
1302 let row_text = |row: &TableRow| {
1303 row.children
1304 .iter()
1305 .map(|cell| cell.children.text().trim().to_string())
1306 .collect::<Vec<_>>()
1307 };
1308
1309 TableData {
1310 headers: self.children.first().map(row_text).unwrap_or_default(),
1311 rows: self.children.iter().skip(1).map(row_text).collect(),
1312 markdown: self.to_markdown(),
1313 span: self.span.map(|span| span.start..span.end),
1314 }
1315 }
1316}
1317
1318#[derive(Debug, Default, Copy, Clone, PartialEq)]
1319pub(crate) enum ColumnumnAlign {
1320 #[default]
1321 Left,
1322 Center,
1323 Right,
1324}
1325
1326impl From<mdast::AlignKind> for ColumnumnAlign {
1327 fn from(value: mdast::AlignKind) -> Self {
1328 match value {
1329 mdast::AlignKind::None => ColumnumnAlign::Left,
1330 mdast::AlignKind::Left => ColumnumnAlign::Left,
1331 mdast::AlignKind::Center => ColumnumnAlign::Center,
1332 mdast::AlignKind::Right => ColumnumnAlign::Right,
1333 }
1334 }
1335}
1336
1337#[derive(Debug, Clone, Default, PartialEq)]
1338pub(crate) struct TableRow {
1339 pub children: Vec<TableCell>,
1340}
1341
1342#[derive(Debug, Clone, Default, PartialEq)]
1343pub(crate) struct TableCell {
1344 pub children: Paragraph,
1345 pub width: Option<DefiniteLength>,
1346}
1347
1348impl Paragraph {
1349 pub(crate) fn take(&mut self) -> Paragraph {
1350 std::mem::replace(
1351 self,
1352 Paragraph {
1353 span: None,
1354 children: vec![],
1355 link_refs: Default::default(),
1356 state: Arc::new(Mutex::new(InlineState::default())),
1357 render_cache: ParagraphRenderCache::default(),
1358 },
1359 )
1360 }
1361
1362 pub(crate) fn is_image(&self) -> bool {
1363 false
1364 }
1365
1366 pub(crate) fn set_span(&mut self, span: Span) {
1367 self.span = Some(span);
1368 }
1369
1370 pub(crate) fn push_str(&mut self, text: &str) {
1371 self.children.push(
1372 InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
1373 );
1374 self.invalidate_render_cache();
1375 }
1376
1377 pub(crate) fn push(&mut self, text: InlineNode) {
1378 self.children.push(text);
1379 self.invalidate_render_cache();
1380 }
1381
1382 pub(crate) fn push_image(&mut self, image: ImageNode) {
1383 self.children.push(InlineNode::image(image));
1384 self.invalidate_render_cache();
1385 }
1386
1387 fn invalidate_render_cache(&mut self) {
1389 self.render_cache = ParagraphRenderCache::default();
1390 }
1391
1392 pub(crate) fn is_empty(&self) -> bool {
1393 self.children.is_empty()
1394 || self
1395 .children
1396 .iter()
1397 .all(|node| node.text.is_empty() && node.image.is_none())
1398 }
1399
1400 pub(crate) fn text_len(&self) -> usize {
1402 self.children
1403 .iter()
1404 .map(|node| node.text.len())
1405 .sum::<usize>()
1406 }
1407
1408 pub(crate) fn merge(&mut self, other: Self) {
1409 self.children.extend(other.children);
1410 self.invalidate_render_cache();
1411 }
1412}
1413
1414#[derive(Debug, Clone)]
1415pub struct CodeBlock {
1416 lang: Option<SharedString>,
1417 state: Arc<Mutex<InlineState>>,
1418 highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
1419 pub span: Option<Span>,
1420}
1421
1422struct CachedCodeBlockHighlights {
1423 highlighter: Arc<CodeBlockHighlighterFn>,
1424 styles: Vec<(Range<usize>, HighlightStyle)>,
1425}
1426
1427impl std::fmt::Debug for CachedCodeBlockHighlights {
1428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1429 f.debug_struct("CachedCodeBlockHighlights")
1430 .field("styles", &self.styles)
1431 .finish_non_exhaustive()
1432 }
1433}
1434
1435impl PartialEq for CodeBlock {
1436 fn eq(&self, other: &Self) -> bool {
1437 self.lang == other.lang && self.code() == other.code() && self.span == other.span
1438 }
1439}
1440
1441impl CodeBlock {
1442 pub fn lang(&self) -> Option<SharedString> {
1444 self.lang.clone()
1445 }
1446
1447 pub fn code(&self) -> SharedString {
1449 self.state
1450 .lock()
1451 .map(|state| state.text.clone())
1452 .unwrap_or_default()
1453 }
1454
1455 pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
1461 Self::new(code.into(), lang.map(Into::into), None::<Span>)
1462 }
1463
1464 pub(crate) fn new(
1465 code: SharedString,
1466 lang: Option<SharedString>,
1467 span: Option<impl Into<Span>>,
1468 ) -> Self {
1469 let state = Arc::new(Mutex::new(InlineState::default()));
1470 if let Ok(mut state) = state.lock() {
1471 state.set_text(code);
1472 }
1473
1474 Self {
1475 lang,
1476 state,
1477 highlight_cache: Arc::new(Mutex::new(None)),
1478 span: span.map(|s| s.into()),
1479 }
1480 }
1481
1482 fn highlighted_styles(
1483 &self,
1484 highlighter: &Arc<CodeBlockHighlighterFn>,
1485 ) -> Vec<(Range<usize>, HighlightStyle)> {
1486 if let Ok(cache) = self.highlight_cache.lock()
1487 && let Some(cache) = cache.as_ref()
1488 && Arc::ptr_eq(&cache.highlighter, highlighter)
1489 {
1490 return cache.styles.clone();
1491 }
1492
1493 let code_len = self.code().len();
1494 let styles = highlighter(self)
1495 .into_iter()
1496 .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
1497 .collect::<Vec<_>>();
1498 if let Ok(mut cache) = self.highlight_cache.lock() {
1499 *cache = Some(CachedCodeBlockHighlights {
1500 highlighter: highlighter.clone(),
1501 styles: styles.clone(),
1502 });
1503 }
1504 styles
1505 }
1506
1507 pub(super) fn selected_text(&self) -> String {
1508 let mut text = String::new();
1509 if let Ok(state) = self.state.lock()
1510 && let Some(selection) = &state.selection
1511 {
1512 text.push_str(&state.text[selection.start..selection.end]);
1513 }
1514 text
1515 }
1516
1517 pub(super) fn selected_source(&self) -> String {
1524 let code = self.selected_text();
1525 if code.is_empty() {
1526 return String::new();
1527 }
1528 let lang = self.lang.clone().unwrap_or_default();
1529 let code = code.trim_end_matches('\n');
1532 format!("```{}\n{}\n```", lang, code)
1533 }
1534
1535 pub(super) fn text(&self) -> String {
1536 self.state
1537 .lock()
1538 .map(|state| state.text.to_string())
1539 .unwrap_or_default()
1540 }
1541
1542 pub(super) fn has_selection(&self) -> bool {
1546 self.state
1547 .lock()
1548 .is_ok_and(|state| state.selection.is_some())
1549 }
1550
1551 pub(super) fn clear_selection(&self) {
1552 if let Ok(mut state) = self.state.lock() {
1553 state.selection = None;
1554 }
1555 }
1556
1557 fn render(
1558 &self,
1559 options: &NodeRenderOptions,
1560 node_cx: &NodeContext,
1561 window: &mut Window,
1562 cx: &mut App,
1563 ) -> AnyElement {
1564 let style = &node_cx.style;
1565
1566 let block = div()
1567 .w_full()
1568 .min_w_0()
1569 .p_3()
1570 .bg(style.code_background())
1571 .font_family(cx.theme().tokens.typography.mono.clone())
1572 .text_size(cx.theme().tokens.typography.mono_md.size)
1573 .relative()
1574 .refine_style(&style.code_block())
1575 .child(Inline::new(
1576 self.state.clone(),
1577 vec![],
1578 fade_highlights(
1579 node_cx
1580 .code_block_highlighter
1581 .as_ref()
1582 .map(|highlighter| self.highlighted_styles(highlighter))
1583 .unwrap_or_default()
1584 .into_iter()
1585 .map(|(range, style)| (range, InlineHighlight::from(style)))
1586 .collect(),
1587 node_cx.stream_fades(self.span.map(|span| TextLeafKey::block(span.start))),
1588 ),
1589 node_cx.link_click_handler.clone(),
1590 ));
1591 let block = match node_cx.code_block_actions.clone() {
1595 Some(actions) => block
1596 .id(block_element_id("codeblock", self.span, options.ix))
1597 .child(
1598 div()
1599 .id("actions")
1600 .absolute()
1601 .top_2()
1602 .right_2()
1603 .bg(style.code_background())
1604 .rounded(cx.theme().tokens.radius.md)
1605 .child(actions(&self, window, cx)),
1606 )
1607 .into_any_element(),
1608 None => block.into_any_element(),
1609 };
1610
1611 gapped(
1612 block,
1613 if options.is_last {
1614 rems(0.)
1615 } else {
1616 style.paragraph_gap()
1617 },
1618 )
1619 }
1620}
1621
1622#[derive(Default, Clone)]
1624pub(crate) struct NodeContext {
1625 pub(crate) offset: usize,
1628 pub(crate) link_refs: HashMap<SharedString, LinkMark>,
1629 pub(crate) style: Arc<TextViewStyle>,
1630 pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
1631 pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
1632 pub(crate) table_actions: Option<Arc<TableActionsFn>>,
1633 pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
1634 pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
1635 pub(crate) stream_fade: Option<Arc<StreamFadeFrame>>,
1637}
1638
1639impl NodeContext {
1640 pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
1641 self.link_refs.insert(identifier, link);
1642 }
1643
1644 fn stream_fades(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, f32)] {
1646 match (&self.stream_fade, key) {
1647 (Some(frame), Some(key)) => frame.fades(key).unwrap_or_default(),
1648 _ => &[],
1649 }
1650 }
1651}
1652
1653impl PartialEq for NodeContext {
1654 fn eq(&self, other: &Self) -> bool {
1655 self.link_refs == other.link_refs && self.style == other.style
1656 }
1659}
1660
1661fn mark_highlight(mark: &TextMark, node_cx: &NodeContext, cx: &App) -> InlineHighlight {
1664 let mut highlight = HighlightStyle::default();
1665 if mark.bold {
1666 highlight.font_weight = Some(FontWeight::BOLD);
1667 }
1668 if mark.italic {
1669 highlight.font_style = Some(FontStyle::Italic);
1670 }
1671 if mark.strikethrough {
1672 highlight.strikethrough = Some(gpui::StrikethroughStyle {
1673 thickness: gpui::px(1.),
1674 ..Default::default()
1675 });
1676 }
1677 if mark.underline {
1678 highlight.underline = Some(gpui::UnderlineStyle {
1679 thickness: gpui::px(1.),
1680 ..Default::default()
1681 });
1682 }
1683 let mut font_family = None;
1684 if mark.code {
1685 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
1686 font_family = Some(cx.theme().tokens.typography.mono.clone());
1687 }
1688 if let Some(color) = mark.highlight {
1689 highlight.background_color = Some(color);
1690 }
1691 InlineHighlight {
1692 style: highlight,
1693 font_family,
1694 font_size_scale: mark.code.then_some(0.875),
1695 }
1696}
1697
1698impl Paragraph {
1699 fn inline_highlights(
1703 &self,
1704 node_cx: &NodeContext,
1705 cx: &App,
1706 ) -> Vec<(Range<usize>, InlineHighlight)> {
1707 let mut highlights = vec![];
1708 let mut offset = 0;
1709 for inline_node in &self.children {
1710 let node_highlights = inline_node
1711 .marks
1712 .iter()
1713 .map(|(range, mark)| {
1714 (
1715 (offset + range.start)..(offset + range.end),
1716 mark_highlight(mark, node_cx, cx),
1717 )
1718 })
1719 .collect::<Vec<_>>();
1720 highlights = combine_highlights(highlights, node_highlights);
1721 offset += inline_node.text.len();
1722 }
1723 highlights
1724 }
1725
1726 fn render(
1730 &self,
1731 fade_key: Option<TextLeafKey>,
1732 node_cx: &NodeContext,
1733 _window: &mut Window,
1734 cx: &mut App,
1735 ) -> AnyElement {
1736 let children = &self.children;
1737 let fades = node_cx.stream_fades(fade_key);
1738
1739 if self.should_render_inline_flow() {
1740 return InlineFlow::new(
1741 leaf_element_id(fade_key),
1742 self.inline_flow_items(fades, node_cx, cx),
1743 node_cx.link_click_handler.clone(),
1744 )
1745 .into_any_element();
1746 }
1747
1748 let has_image = children.iter().any(|child| child.image.is_some());
1749 if !has_image {
1752 let (text, highlights, mut links) = self.plain_render(node_cx, cx);
1753 if text.is_empty() {
1754 return div().into_any_element();
1755 }
1756 for (_, link_mark) in &mut links {
1757 if let Some(identifier) = link_mark.identifier.as_ref()
1758 && let Some(mark) = node_cx.link_refs.get(identifier)
1759 {
1760 *link_mark = mark.clone();
1761 }
1762 }
1763 let highlights = fade_highlights(highlights, &slice_fades(fades, 0, text.len()));
1764 if let Ok(mut state) = self.state.lock() {
1765 state.set_text(text);
1766 }
1767 return Inline::new(
1768 self.state.clone(),
1769 links,
1770 highlights,
1771 node_cx.link_click_handler.clone(),
1772 )
1773 .into_any_element();
1774 }
1775
1776 let mut child_nodes: Vec<AnyElement> = vec![];
1777
1778 let mut text = String::new();
1779 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1780 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1781 let mut offset = 0;
1782 let mut consumed = 0;
1785
1786 for (ix, inline_node) in children.iter().enumerate() {
1787 let text_len = inline_node.text.len();
1788 text.push_str(&inline_node.text);
1789
1790 if let Some(image) = &inline_node.image {
1791 if text.len() > 0 {
1792 if let Ok(mut state) = inline_node.state.lock() {
1793 state.set_text(text.clone().into());
1794 }
1795 child_nodes.push(
1796 Inline::new(
1797 inline_node.state.clone(),
1798 links.clone(),
1799 fade_highlights(
1800 highlights.clone(),
1801 &slice_fades(fades, consumed, consumed + text.len()),
1802 ),
1803 node_cx.link_click_handler.clone(),
1804 )
1805 .into_any_element(),
1806 );
1807 }
1808 let link_click_handler = node_cx.link_click_handler.clone();
1809 child_nodes.push(
1810 img(image.source())
1811 .id(ix)
1812 .object_fit(ObjectFit::Contain)
1813 .max_w(relative(1.))
1814 .when_some(image.width, |this, width| this.w(width))
1815 .when_some(image.link.clone(), |this, link| {
1816 let link_click_handler = link_click_handler.clone();
1817 let aux_link = link.clone();
1818 let aux_link_click_handler = link_click_handler.clone();
1819 this.cursor_pointer()
1820 .on_click(move |event, window, cx| {
1821 crate::TextSelection::end(window, cx);
1822 cx.stop_propagation();
1823 handle_link_click(
1824 &link_click_handler,
1825 link.url.clone(),
1826 event.clone(),
1827 window,
1828 cx,
1829 );
1830 })
1831 .on_aux_click(move |event, window, cx| {
1832 crate::TextSelection::end(window, cx);
1833 cx.stop_propagation();
1834 handle_link_click(
1835 &aux_link_click_handler,
1836 aux_link.url.clone(),
1837 event.clone(),
1838 window,
1839 cx,
1840 );
1841 })
1842 })
1843 .into_any_element(),
1844 );
1845
1846 consumed += text.len();
1847 text.clear();
1848 links.clear();
1849 highlights.clear();
1850 offset = 0;
1851 } else {
1852 let mut node_highlights = vec![];
1853 for (range, style) in &inline_node.marks {
1854 let inner_range = (offset + range.start)..(offset + range.end);
1855 let mut highlight = mark_highlight(style, node_cx, cx);
1856
1857 if let Some(mut link_mark) = style.link.clone() {
1858 highlight.style.color = Some(node_cx.style.link());
1859 highlight.style.underline = Some(gpui::UnderlineStyle {
1860 thickness: gpui::px(1.),
1861 ..Default::default()
1862 });
1863
1864 if let Some(identifier) = link_mark.identifier.as_ref() {
1866 if let Some(mark) = node_cx.link_refs.get(identifier) {
1867 link_mark = mark.clone();
1868 }
1869 }
1870
1871 links.push((inner_range.clone(), link_mark));
1872 }
1873
1874 node_highlights.push((inner_range, highlight));
1875 }
1876
1877 highlights = combine_highlights(highlights, node_highlights);
1878 offset += text_len;
1879 }
1880 }
1881
1882 if text.len() > 0 {
1884 let highlights = fade_highlights(
1885 highlights,
1886 &slice_fades(fades, consumed, consumed + text.len()),
1887 );
1888 if let Ok(mut state) = self.state.lock() {
1889 state.set_text(text.into());
1890 }
1891 child_nodes.push(
1892 Inline::new(
1893 self.state.clone(),
1894 links,
1895 highlights,
1896 node_cx.link_click_handler.clone(),
1897 )
1898 .into_any_element(),
1899 );
1900 }
1901
1902 if !has_image {
1906 return child_nodes
1907 .pop()
1908 .unwrap_or_else(|| div().into_any_element());
1909 }
1910
1911 div()
1912 .id(leaf_element_id(fade_key))
1913 .children(child_nodes)
1914 .into_any_element()
1915 }
1916
1917 fn should_render_inline_flow(&self) -> bool {
1918 let has_image = self.children.iter().any(|child| child.image.is_some());
1919 let has_text = self.children.iter().any(|child| !child.text.is_empty());
1920 self.children.iter().any(|child| child.custom.is_some())
1921 || (has_image && has_text)
1922 || self
1923 .children
1924 .iter()
1925 .any(|child| child.marks.iter().any(|(_, mark)| mark.code))
1926 }
1927
1928 fn inline_flow_items(
1929 &self,
1930 fades: &[(Range<usize>, f32)],
1931 node_cx: &NodeContext,
1932 cx: &mut App,
1933 ) -> Vec<InlineFlowItem> {
1934 let mut items = Vec::new();
1935 let mut text = String::new();
1936 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1937 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1938 let mut offset = 0;
1939 let mut consumed = 0;
1942
1943 for inline_node in &self.children {
1944 if let Some(node) = &inline_node.custom {
1945 if let Ok(mut state) = inline_node.state.lock() {
1946 state.set_text(text.clone().into());
1947 }
1948 if !text.is_empty() {
1949 let item_fades = slice_fades(fades, consumed, consumed + text.len());
1950 consumed += text.len();
1951 items.push(InlineFlowItem::Text {
1952 state: inline_node.state.clone(),
1953 text: std::mem::take(&mut text).into(),
1954 links: std::mem::take(&mut links),
1955 highlights: fade_highlights(std::mem::take(&mut highlights), &item_fades),
1956 });
1957 }
1958 let mut object_style = HighlightStyle::default();
1959 let mut object_link = None;
1960 for (_, mark) in &inline_node.marks {
1961 object_style = object_style.highlight(mark_highlight(mark, node_cx, cx).style);
1962 if let Some(link) = &mark.link {
1963 object_link = Some(
1964 link.identifier
1965 .as_ref()
1966 .and_then(|id| node_cx.link_refs.get(id))
1967 .unwrap_or(link)
1968 .clone(),
1969 );
1970 object_style.color = Some(node_cx.style.link());
1971 object_style.underline = Some(gpui::UnderlineStyle {
1972 thickness: px(1.),
1973 ..Default::default()
1974 });
1975 }
1976 }
1977 let rendered_node = node.clone();
1978 let extensions = node_cx.markdown_extensions.clone();
1979 items.push(InlineFlowItem::Object {
1980 text: node.shared_text(),
1981 accessibility_label: node.shared_accessibility_name(),
1982 id: node.source_range().map_or(items.len(), |range| range.start),
1983 renderer: Arc::new(move |context, window, cx| {
1984 extensions.render_inline(&rendered_node, context, window, cx)
1985 }),
1986 selected: inline_node.custom_selection.clone(),
1987 style: object_style,
1988 link: object_link,
1989 });
1990 consumed += inline_node.text.len();
1991 offset = 0;
1992 continue;
1993 }
1994 let text_len = inline_node.text.len();
1995 text.push_str(&inline_node.text);
1996
1997 if let Some(image) = &inline_node.image {
1998 if !text.is_empty() {
1999 if let Ok(mut state) = inline_node.state.lock() {
2000 state.set_text(text.clone().into());
2001 }
2002 items.push(InlineFlowItem::Text {
2003 state: inline_node.state.clone(),
2004 text: text.clone().into(),
2005 links: links.clone(),
2006 highlights: fade_highlights(
2007 highlights.clone(),
2008 &slice_fades(fades, consumed, consumed + text.len()),
2009 ),
2010 });
2011 }
2012
2013 items.push(InlineFlowItem::Image {
2014 source: image.source(),
2015 link: image.link.clone(),
2016 title: image.title(),
2017 width: image.width,
2018 height: image.height,
2019 });
2020
2021 consumed += text.len();
2022 text.clear();
2023 links.clear();
2024 highlights.clear();
2025 offset = 0;
2026 } else {
2027 let mut node_highlights = vec![];
2028 for (range, style) in &inline_node.marks {
2029 let inner_range = (offset + range.start)..(offset + range.end);
2030 let mut highlight = mark_highlight(style, node_cx, cx);
2031
2032 if let Some(mut link_mark) = style.link.clone() {
2033 highlight.style.color = Some(node_cx.style.link());
2034 highlight.style.underline = Some(gpui::UnderlineStyle {
2035 thickness: gpui::px(1.),
2036 ..Default::default()
2037 });
2038
2039 if let Some(identifier) = link_mark.identifier.as_ref()
2040 && let Some(mark) = node_cx.link_refs.get(identifier)
2041 {
2042 link_mark = mark.clone();
2043 }
2044
2045 links.push((inner_range.clone(), link_mark));
2046 }
2047
2048 node_highlights.push((inner_range, highlight));
2049 }
2050
2051 highlights = combine_highlights(highlights, node_highlights);
2052 offset += text_len;
2053 }
2054 }
2055
2056 if !text.is_empty() {
2057 if let Ok(mut state) = self.state.lock() {
2058 state.set_text(text.clone().into());
2059 }
2060 let highlights = fade_highlights(
2061 highlights,
2062 &slice_fades(fades, consumed, consumed + text.len()),
2063 );
2064 items.push(InlineFlowItem::Text {
2065 state: self.state.clone(),
2066 text: text.into(),
2067 links,
2068 highlights,
2069 });
2070 }
2071
2072 items
2073 }
2074}
2075
2076fn block_element_id(kind: &'static str, span: Option<Span>, ix: usize) -> ElementId {
2081 (kind, span.map_or(ix, |span| span.start)).into()
2082}
2083
2084fn leaf_element_id(fade_key: Option<TextLeafKey>) -> ElementId {
2089 fade_key.map_or_else(|| ElementId::from("p"), ElementId::from)
2090}
2091
2092fn gapped(block: AnyElement, gap: Rems) -> AnyElement {
2095 if gap.is_zero() {
2096 block
2097 } else {
2098 div().pb(gap).child(block).into_any_element()
2099 }
2100}
2101
2102fn slice_fades(
2104 fades: &[(Range<usize>, f32)],
2105 start: usize,
2106 end: usize,
2107) -> Vec<(Range<usize>, f32)> {
2108 slice_ranges(fades, start, end, |range, fade_out| (range, *fade_out))
2109}
2110
2111const CELL_PAD_PX: f32 = 16.0; const CELL_MIN_PX: f32 = 48.0;
2113const CELL_BORDER_PX: f32 = 1.0; fn measure_table_columns(
2120 table: &Table,
2121 col_count: usize,
2122 node_cx: &NodeContext,
2123 window: &mut Window,
2124 cx: &mut App,
2125) -> Vec<f32> {
2126 let text_style = window.text_style();
2127 let font_size = text_style.font_size.to_pixels(window.rem_size());
2128 let mut col_w = vec![CELL_MIN_PX; col_count];
2129 for row in table.children.iter() {
2130 for (ix, cell) in row.children.iter().enumerate() {
2131 let Some(slot) = col_w.get_mut(ix) else {
2132 continue;
2133 };
2134 if cell
2135 .children
2136 .children
2137 .iter()
2138 .any(|node| node.custom.is_some())
2139 {
2140 let items = cell.children.inline_flow_items(&[], node_cx, cx);
2141 let width = super::inline_flow::intrinsic_width(&items, window, cx);
2142 let border = if ix + 1 < col_count {
2143 CELL_BORDER_PX
2144 } else {
2145 0.
2146 };
2147 *slot = slot.max(f32::from(width) + CELL_PAD_PX + border);
2148 continue;
2149 }
2150 let text = cell.children.text();
2151 let highlights = cell.children.inline_highlights(node_cx, cx);
2152 let mut w = 0.0_f32;
2153 let mut line_start = 0;
2154 for line in text.split('\n') {
2155 let start = line_start + (line.len() - line.trim_start().len());
2156 let line_end = line_start + line.len();
2157 line_start = line_end + 1;
2158 let line = line.trim();
2159 if line.is_empty() {
2160 continue;
2161 }
2162 let end = start + line.len();
2163 let line_highlights = highlights
2164 .iter()
2165 .filter_map(|(range, highlight)| {
2166 let clipped = range.start.max(start)..range.end.min(end);
2167 (clipped.start < clipped.end).then(|| {
2168 (
2169 clipped.start - start..clipped.end - start,
2170 highlight.clone(),
2171 )
2172 })
2173 })
2174 .collect::<Vec<_>>();
2175 let mut line_w = gpui::Pixels::ZERO;
2176 for (range, scale) in text_size_ranges(line.len(), &line_highlights) {
2177 let highlights = slice_ranges(
2178 &line_highlights,
2179 range.start,
2180 range.end,
2181 |range, highlight| (range, highlight.clone()),
2182 );
2183 if highlights.iter().any(|(_, h)| h.font_size_scale.is_some()) {
2184 line_w += px(crate::text::inline_flow::INLINE_CODE_PADDING * 2.);
2185 }
2186 let runs = text_runs(range.len(), &text_style, &highlights);
2187 line_w += window
2188 .text_system()
2189 .layout_line(&line[range], font_size * scale, &runs, None)
2190 .width;
2191 }
2192 w = w.max(f32::from(line_w));
2193 }
2194 let border = if ix + 1 < col_count {
2197 CELL_BORDER_PX
2198 } else {
2199 0.
2200 };
2201 *slot = slot.max(w + CELL_PAD_PX + border);
2202 }
2203 }
2204 col_w
2205}
2206
2207impl Paragraph {
2208 fn to_markdown(&self) -> String {
2209 if self.children.iter().any(|node| node.custom.is_some()) {
2210 let mut source = MarkdownSource::default();
2211 for node in &self.children {
2212 if node.custom.is_some() {
2213 source.push_object(node);
2214 } else {
2215 source.push_text(&node.text, &node.marks, 0..node.text.len());
2216 if let Some(image) = &node.image {
2217 source.push_str(&image_markdown(image));
2218 }
2219 }
2220 }
2221 let mut text = source.finish();
2222 text.push_str("\n\n");
2223 return text;
2224 }
2225 let mut text = self
2226 .children
2227 .iter()
2228 .map(|text_node| {
2229 let mut text = text_node.text.to_string();
2230 for (range, style) in &text_node.marks {
2231 if style.bold {
2232 text = format!("**{}**", &text_node.text[range.clone()]);
2233 }
2234 if style.italic {
2235 text = format!("*{}*", &text_node.text[range.clone()]);
2236 }
2237 if style.strikethrough {
2238 text = format!("~~{}~~", &text_node.text[range.clone()]);
2239 }
2240 if style.code {
2241 text = format!("`{}`", &text_node.text[range.clone()]);
2242 }
2243 if style.highlight.is_some() {
2244 text = format!("=={}==", &text_node.text[range.clone()]);
2245 }
2246 if let Some(link) = &style.link {
2247 text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
2248 }
2249 }
2250
2251 if let Some(image) = &text_node.image {
2252 let alt = image.alt.clone().unwrap_or_default();
2253 let title = image
2254 .title
2255 .clone()
2256 .map_or(String::new(), |t| format!(" \"{}\"", t));
2257 text.push_str(&format!("", alt, image.url, title))
2258 }
2259
2260 text
2261 })
2262 .collect::<Vec<_>>()
2263 .join("");
2264
2265 text.push_str("\n\n");
2266 text
2267 }
2268}
2269
2270impl BlockNode {
2271 #[allow(dead_code)]
2275 pub(crate) fn to_markdown(&self) -> String {
2276 match self {
2277 BlockNode::Root { children, .. } => children
2278 .iter()
2279 .map(|child| child.to_markdown())
2280 .collect::<Vec<_>>()
2281 .join("\n\n"),
2282 BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
2283 BlockNode::Heading {
2284 level, children, ..
2285 } => {
2286 let hashes = "#".repeat(*level as usize);
2287 format!("{} {}", hashes, children.to_markdown())
2288 }
2289 BlockNode::Blockquote { children, .. } => {
2290 let content = children
2291 .iter()
2292 .map(|child| child.to_markdown())
2293 .collect::<Vec<_>>()
2294 .join("\n\n");
2295
2296 content
2297 .lines()
2298 .map(|line| format!("> {}", line))
2299 .collect::<Vec<_>>()
2300 .join("\n")
2301 }
2302 BlockNode::List {
2303 children, ordered, ..
2304 } => children
2305 .iter()
2306 .enumerate()
2307 .map(|(i, child)| {
2308 let prefix = if *ordered {
2309 format!("{}. ", i + 1)
2310 } else {
2311 "- ".to_string()
2312 };
2313 format!("{}{}", prefix, child.to_markdown())
2314 })
2315 .collect::<Vec<_>>()
2316 .join("\n"),
2317 BlockNode::ListItem {
2318 children, checked, ..
2319 } => {
2320 let checkbox = if let Some(checked) = checked {
2321 if *checked { "[x] " } else { "[ ] " }
2322 } else {
2323 ""
2324 };
2325 format!(
2326 "{}{}",
2327 checkbox,
2328 children
2329 .iter()
2330 .map(|child| child.to_markdown())
2331 .collect::<Vec<_>>()
2332 .join("\n")
2333 )
2334 }
2335 BlockNode::CodeBlock(code_block) => {
2336 format!(
2337 "```{}\n{}\n```",
2338 code_block.lang.clone().unwrap_or_default(),
2339 code_block.code()
2340 )
2341 }
2342 BlockNode::Table(table) => table.to_markdown(),
2343 BlockNode::Break { html, .. } => {
2344 if *html {
2345 "<br>".to_string()
2346 } else {
2347 "\n".to_string()
2348 }
2349 }
2350 BlockNode::HorizontalRule { .. } => "---".to_string(),
2351 BlockNode::Custom(node) => node.to_markdown(),
2352 BlockNode::Definition {
2353 identifier,
2354 url,
2355 title,
2356 ..
2357 } => {
2358 if let Some(title) = title {
2359 format!("[{}]: {} \"{}\"", identifier, url, title)
2360 } else {
2361 format!("[{}]: {}", identifier, url)
2362 }
2363 }
2364 BlockNode::Unknown { .. } => "".to_string(),
2365 }
2366 .trim()
2367 .to_string()
2368 }
2369}
2370
2371impl BlockNode {
2372 fn render_list_item_row(
2373 content: AnyElement,
2374 ix: usize,
2375 options: NodeRenderOptions,
2376 checked: Option<bool>,
2377 style: &TextViewStyle,
2378 line_height: Pixels,
2379 ) -> Div {
2380 h_flex()
2381 .w_full()
2382 .min_w_0()
2383 .relative()
2384 .items_start()
2385 .content_start()
2386 .when(!options.todo && checked.is_none(), |this| {
2387 this.child(list_item_prefix(ix, options.ordered, options.depth))
2388 })
2389 .when_some(checked, |this, checked| {
2390 let check_svg = if style.is_dark() {
2392 CHECK_SVG_DARK
2393 } else {
2394 CHECK_SVG_LIGHT
2395 };
2396 this.child(
2397 div()
2398 .flex()
2399 .mr_1p5()
2400 .h(line_height)
2401 .flex_none()
2402 .items_center()
2403 .justify_center()
2404 .child(
2405 div()
2406 .flex()
2407 .size(rems(0.875))
2408 .items_center()
2409 .justify_center()
2410 .border_1()
2411 .border_color(style.foreground())
2412 .when(checked, |this| {
2413 this.bg(style.foreground()).child(
2414 img(Arc::new(Image::from_bytes(
2415 ImageFormat::Svg,
2416 check_svg.to_vec(),
2417 )))
2418 .size(rems(0.625)),
2419 )
2420 }),
2421 ),
2422 )
2423 })
2424 .child(div().flex_1().min_w_0().overflow_hidden().child(content))
2425 }
2426
2427 fn render_list_item(
2428 item: &BlockNode,
2429 ix: usize,
2430 options: NodeRenderOptions,
2431 node_cx: &NodeContext,
2432 window: &mut Window,
2433 cx: &mut App,
2434 ) -> AnyElement {
2435 match item {
2436 BlockNode::ListItem {
2437 children,
2438 spread,
2439 checked,
2440 ..
2441 } => div()
2442 .w_full()
2443 .min_w_0()
2444 .when(*spread, |this| this.child(div()))
2445 .children({
2446 let mut items: Vec<Div> = Vec::with_capacity(children.len());
2447
2448 for (child_ix, child) in children.iter().enumerate() {
2449 match child {
2450 BlockNode::Paragraph { .. } => {
2451 let last_not_list = child_ix > 0
2452 && !matches!(children[child_ix - 1], BlockNode::List { .. });
2453
2454 let text = child.render_block(
2455 NodeRenderOptions {
2456 depth: options.depth + 1,
2457 todo: checked.is_some(),
2458 is_last: true,
2459 ..options
2460 },
2461 node_cx,
2462 window,
2463 cx,
2464 );
2465
2466 if last_not_list {
2470 if let Some(preceding_row) = items.pop() {
2471 items.push(
2472 div().child(preceding_row).child(
2473 div()
2474 .w_full()
2475 .pl(rems(1.))
2476 .overflow_hidden()
2477 .child(text),
2478 ),
2479 );
2480 continue;
2481 }
2482 }
2483
2484 items.push(Self::render_list_item_row(
2485 text,
2486 ix,
2487 options,
2488 *checked,
2489 &node_cx.style,
2490 window.line_height(),
2491 ));
2492 }
2493 BlockNode::List { .. } => {
2494 items.push(div().ml(rems(1.)).child(child.render_block(
2495 NodeRenderOptions {
2496 depth: options.depth + 1,
2497 todo: checked.is_some(),
2498 is_last: true,
2499 ..options
2500 },
2501 node_cx,
2502 window,
2503 cx,
2504 )));
2505 }
2506 BlockNode::Root { .. }
2507 | BlockNode::Heading { .. }
2508 | BlockNode::Blockquote { .. }
2509 | BlockNode::CodeBlock(_)
2510 | BlockNode::Custom(_)
2511 | BlockNode::Table(_)
2512 | BlockNode::HorizontalRule { .. } => {
2513 let block = child.render_block(
2514 NodeRenderOptions {
2515 depth: options.depth + 1,
2516 todo: checked.is_some(),
2517 is_last: true,
2518 ..options
2519 },
2520 node_cx,
2521 window,
2522 cx,
2523 );
2524
2525 if child_ix == 0 {
2526 items.push(Self::render_list_item_row(
2527 block,
2528 ix,
2529 options,
2530 *checked,
2531 &node_cx.style,
2532 window.line_height(),
2533 ));
2534 } else {
2535 items.push(
2539 div()
2540 .w_full()
2541 .min_w_0()
2542 .pl(rems(1.))
2543 .overflow_hidden()
2544 .child(block),
2545 );
2546 }
2547 }
2548 BlockNode::ListItem { .. }
2549 | BlockNode::Break { .. }
2550 | BlockNode::Definition { .. }
2551 | BlockNode::Unknown => {}
2552 }
2553 }
2554 items
2555 })
2556 .into_any_element(),
2557 _ => div().into_any_element(),
2558 }
2559 }
2560
2561 fn render_table(
2565 item: &BlockNode,
2566 options: &NodeRenderOptions,
2567 node_cx: &NodeContext,
2568 window: &mut Window,
2569 cx: &mut App,
2570 ) -> impl IntoElement {
2571 const DEFAULT_LENGTH: usize = 5;
2572
2573 let table = match item {
2574 BlockNode::Table(table) => table,
2575 _ => return div().into_any_element(),
2576 };
2577
2578 let mut col_lens: Vec<usize> = vec![];
2581 for row in table.children.iter() {
2582 for (ix, cell) in row.children.iter().enumerate() {
2583 if col_lens.len() <= ix {
2584 col_lens.push(DEFAULT_LENGTH);
2585 }
2586 col_lens[ix] = col_lens[ix].max(cell.children.text_len());
2587 }
2588 }
2589
2590 if matches!(node_cx.style.table().overflow.x, Some(Overflow::Scroll)) {
2592 Self::render_scroll_table(table, col_lens.len(), options, node_cx, window, cx)
2593 } else {
2594 Self::render_wrap_table(table, &col_lens, options, node_cx, window, cx)
2595 }
2596 }
2597
2598 fn render_scroll_table(
2617 table: &Table,
2618 col_count: usize,
2619 options: &NodeRenderOptions,
2620 node_cx: &NodeContext,
2621 window: &mut Window,
2622 cx: &mut App,
2623 ) -> AnyElement {
2624 const CELL_WRAP_MAX_LINES: f32 = 2.0;
2630 const CELL_WRAP_MIN_PX: f32 = 160.0;
2631 const CELL_WRAP_MAX_PX: f32 = 480.0;
2632 const TABLE_BORDER_PX: f32 = 2.0; let col_w = measure_table_columns(table, col_count, node_cx, window, cx);
2635 let style = &node_cx.style;
2636 let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
2640 let col_min_w: Vec<f32> = if nowrap {
2641 col_w.clone()
2642 } else {
2643 col_w
2644 .iter()
2645 .map(|w| {
2646 (w / CELL_WRAP_MAX_LINES)
2647 .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
2648 .min(*w)
2649 })
2650 .collect()
2651 };
2652 let min_total_w: f32 = col_min_w.iter().sum::<f32>() + TABLE_BORDER_PX;
2653
2654 let scroll_handle = window
2655 .use_keyed_state(
2656 block_element_id("table-scroll", table.span, options.ix),
2657 cx,
2658 |_, _| ScrollHandle::default(),
2659 )
2660 .read(cx)
2661 .clone();
2662 let row_count = table.children.len();
2663 let mut rows = Vec::with_capacity(row_count);
2664 let mut cell_ordinal = 0;
2665 for (row_ix, row) in table.children.iter().enumerate() {
2666 let mut cells = Vec::with_capacity(row.children.len());
2667 for (ix, cell) in row.children.iter().enumerate() {
2668 let fade_key = table
2669 .span
2670 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
2671 cell_ordinal += 1;
2672 let align = table.column_align(ix);
2673 let is_last_col = ix == row.children.len() - 1;
2674 let width = col_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
2675 let min_width = col_min_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
2676 cells.push(
2677 div()
2678 .flex_basis(px(width))
2685 .flex_grow(width)
2686 .flex_shrink(1.)
2687 .min_w(px(min_width))
2688 .overflow_hidden()
2689 .when(align == ColumnumnAlign::Center, |this| this.text_center())
2690 .when(align == ColumnumnAlign::Right, |this| this.text_right())
2691 .px_2()
2692 .py_1()
2693 .when(!is_last_col, |this| {
2694 this.border_r_1().border_color(style.border())
2695 })
2696 .refine_style(&style.table_cell())
2697 .child(cell.children.render(fade_key, node_cx, window, cx)),
2698 );
2699 }
2700 rows.push(
2701 div()
2702 .w_full()
2703 .when(row_ix < row_count - 1, |this| this.border_b_1())
2704 .border_color(style.border())
2705 .flex()
2706 .flex_row()
2707 .when(row_ix == 0, |this| {
2711 this.bg(style.code_background())
2712 .text_color(style.foreground())
2713 .refine_style(&style.table_head())
2714 })
2715 .children(cells),
2716 );
2717 }
2718
2719 div()
2720 .pb(rems(1.))
2721 .w_full()
2722 .child(
2723 horizontal_scroll_area(
2733 block_element_id("table", table.span, options.ix),
2734 &scroll_handle,
2735 &StyleRefinement::default()
2736 .bg(cx.theme().tokens.colors.surface)
2737 .border_1()
2738 .border_color(style.border())
2739 .refine_style(style.table()),
2740 div().min_w_full().w(px(min_total_w)).children(rows),
2746 ),
2747 )
2748 .children(node_cx.table_actions.clone().map(|f| {
2755 div()
2756 .id(block_element_id("table-actions", table.span, options.ix))
2757 .mt_1()
2758 .child(f(&table.table_data(), window, cx))
2759 }))
2760 .into_any_element()
2761 }
2762
2763 fn render_wrap_table(
2766 table: &Table,
2767 col_lens: &[usize],
2768 options: &NodeRenderOptions,
2769 node_cx: &NodeContext,
2770 window: &mut Window,
2771 cx: &mut App,
2772 ) -> AnyElement {
2773 const MAX_LENGTH: usize = 150;
2774
2775 let style = &node_cx.style;
2776 let row_count = table.children.len();
2777 let mut rows = Vec::with_capacity(row_count);
2778 let mut cell_ordinal = 0;
2779 for (row_ix, row) in table.children.iter().enumerate() {
2780 let mut cells = Vec::with_capacity(row.children.len());
2781 for (ix, cell) in row.children.iter().enumerate() {
2782 let fade_key = table
2783 .span
2784 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
2785 cell_ordinal += 1;
2786 let align = table.column_align(ix);
2787 let is_last_col = ix == row.children.len() - 1;
2788 let len = col_lens
2789 .get(ix)
2790 .copied()
2791 .unwrap_or(MAX_LENGTH)
2792 .min(MAX_LENGTH);
2793
2794 cells.push(
2795 div()
2796 .overflow_hidden()
2797 .when(align == ColumnumnAlign::Center, |this| this.text_center())
2798 .when(align == ColumnumnAlign::Right, |this| this.text_right())
2799 .min_w_16()
2800 .w(Length::Definite(relative(len as f32)))
2801 .px_2()
2802 .py_1()
2803 .when(!is_last_col, |this| {
2804 this.border_r_1().border_color(style.border())
2805 })
2806 .refine_style(&style.table_cell())
2807 .child(cell.children.render(fade_key, node_cx, window, cx)),
2808 );
2809 }
2810
2811 rows.push(
2812 div()
2813 .w_full()
2814 .when(row_ix < row_count - 1, |this| this.border_b_1())
2815 .border_color(style.border())
2816 .flex()
2817 .flex_row()
2818 .when(row_ix == 0, |this| {
2822 this.bg(style.code_background())
2823 .text_color(style.foreground())
2824 .refine_style(&style.table_head())
2825 })
2826 .children(cells),
2827 );
2828 }
2829
2830 div()
2831 .pb(rems(1.))
2832 .w_full()
2833 .child(
2834 div()
2835 .w_full()
2836 .bg(cx.theme().tokens.colors.surface)
2837 .border_1()
2838 .border_color(style.border())
2839 .overflow_hidden()
2840 .children(rows)
2841 .refine_style(&style.table()),
2842 )
2843 .children(node_cx.table_actions.clone().map(|f| {
2850 div()
2851 .id(block_element_id("table-actions", table.span, options.ix))
2852 .mt_1()
2853 .child(f(&table.table_data(), window, cx))
2854 }))
2855 .into_any_element()
2856 }
2857
2858 pub(crate) fn render_block(
2859 &self,
2860 options: NodeRenderOptions,
2861 node_cx: &NodeContext,
2862 window: &mut Window,
2863 cx: &mut App,
2864 ) -> AnyElement {
2865 let mb = if options.in_list || options.is_last {
2866 rems(0.)
2867 } else {
2868 node_cx.style.paragraph_gap()
2869 };
2870
2871 match self {
2872 BlockNode::Root { children, .. } => div()
2873 .children(children.into_iter().enumerate().map(move |(ix, node)| {
2874 node.render_block(NodeRenderOptions { ix, ..options }, node_cx, window, cx)
2875 }))
2876 .into_any_element(),
2877 BlockNode::Paragraph(paragraph) => gapped(
2878 paragraph.render(
2879 paragraph.span.map(|span| TextLeafKey::block(span.start)),
2880 node_cx,
2881 window,
2882 cx,
2883 ),
2884 mb,
2885 ),
2886 BlockNode::Heading {
2887 level,
2888 children,
2889 span,
2890 } => {
2891 let (text_size, font_weight) = match level {
2892 1 => (rems(2.), FontWeight::BOLD),
2893 2 => (rems(1.5), FontWeight::SEMIBOLD),
2894 3 => (rems(1.25), FontWeight::SEMIBOLD),
2895 4 => (rems(1.125), FontWeight::SEMIBOLD),
2896 5 => (rems(1.), FontWeight::SEMIBOLD),
2897 6 => (rems(1.), FontWeight::MEDIUM),
2898 _ => (rems(1.), FontWeight::NORMAL),
2899 };
2900
2901 let mut text_size = text_size.to_pixels(node_cx.style.heading_base_font_size());
2902 if let Some(size) = node_cx.style.heading_font_size(*level) {
2903 text_size = size;
2904 }
2905
2906 div()
2907 .pb(rems(0.3))
2908 .whitespace_normal()
2909 .text_size(text_size)
2910 .font_weight(font_weight)
2911 .child(children.render(
2912 span.map(|span| TextLeafKey::block(span.start)),
2913 node_cx,
2914 window,
2915 cx,
2916 ))
2917 .into_any_element()
2918 }
2919 BlockNode::Blockquote { children, .. } => gapped(
2920 div()
2921 .w_full()
2922 .text_color(node_cx.style.muted_foreground())
2923 .border_l_3()
2924 .border_color(node_cx.style.border())
2925 .px_4()
2926 .children({
2927 let children_len = children.len();
2928 children.into_iter().enumerate().map(move |(index, c)| {
2929 let is_last = index == children_len - 1;
2930 c.render_block(options.is_last(is_last), node_cx, window, cx)
2931 })
2932 })
2933 .into_any_element(),
2934 mb,
2935 ),
2936 BlockNode::List {
2937 children, ordered, ..
2938 } => div()
2939 .w_full()
2940 .min_w_0()
2941 .pb(mb)
2942 .children({
2943 let mut items = Vec::with_capacity(children.len());
2944 let mut item_index = 0;
2945 for (ix, item) in children.into_iter().enumerate() {
2946 let is_item = item.is_list_item();
2947
2948 items.push(Self::render_list_item(
2949 item,
2950 item_index,
2951 NodeRenderOptions {
2952 ix,
2953 ordered: *ordered,
2954 ..options
2955 },
2956 node_cx,
2957 window,
2958 cx,
2959 ));
2960
2961 if is_item {
2962 item_index += 1;
2963 }
2964 }
2965 items
2966 })
2967 .into_any_element(),
2968 BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
2969 BlockNode::Custom(node) => {
2970 let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
2971 Some(rendered) => rendered,
2972 None => div().child(node.as_text().to_string()).into_any_element(),
2973 };
2974
2975 div().pb(mb).child(inner).into_any_element()
2976 }
2977 BlockNode::Table { .. } => {
2978 Self::render_table(self, &options, node_cx, window, cx).into_any_element()
2979 }
2980 BlockNode::HorizontalRule { .. } => gapped(
2981 div()
2982 .bg(node_cx.style.border())
2983 .h(px(2.))
2984 .into_any_element(),
2985 mb,
2986 ),
2987 BlockNode::Break { .. } => div().into_any_element(),
2988 BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
2989 _ => {
2990 if cfg!(debug_assertions) {
2991 tracing::warn!("unknown implementation: {:?}", self);
2992 }
2993
2994 div().into_any_element()
2995 }
2996 }
2997 }
2998}
2999
3000#[cfg(test)]
3001mod tests {
3002 use super::*;
3003
3004 #[test]
3005 fn selected_inline_objects_coalesce_surrounding_emphasis() {
3006 for (object_mark, expected) in [
3007 (TextMark::default().italic(), "*fore $x$ aft*"),
3008 (TextMark::default().italic().bold(), "*fore **$x$** aft*"),
3009 ] {
3010 let italic = TextMark::default().italic();
3011 let before = InlineNode::new("before ").marks(vec![(0..7, italic.clone())]);
3012 let formula =
3013 InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"))
3014 .marks(vec![(0..1, object_mark)]);
3015 let after = InlineNode::new(" after").marks(vec![(0..6, italic)]);
3016 {
3017 let mut state = formula.state.lock().unwrap();
3018 state.text = "before ".into();
3019 state.selection = Some((2..7).into());
3020 }
3021 *formula.custom_selection.lock().unwrap() = true;
3022 let paragraph = Paragraph {
3023 children: vec![before, formula, after],
3024 ..Default::default()
3025 };
3026 {
3027 let mut state = paragraph.state.lock().unwrap();
3028 state.text = " after".into();
3029 state.selection = Some((0..4).into());
3030 }
3031 assert_eq!(paragraph.selected_source(), expected);
3032 }
3033 }
3034
3035 #[test]
3036 fn consecutive_inline_objects_copy_atomically_without_neighboring_text() {
3037 let first =
3038 InlineNode::custom(MarkdownNode::new("math", ()).text("甲²").markdown("$甲^2$"));
3039 let second = InlineNode::custom(MarkdownNode::new("math", ()).text("b").markdown("$b$"));
3040 *first.custom_selection.lock().unwrap() = true;
3041 *second.custom_selection.lock().unwrap() = true;
3042 let paragraph = Paragraph {
3043 children: vec![first, second],
3044 ..Default::default()
3045 };
3046 assert_eq!(paragraph.selected_text(), "甲²b");
3047 assert_eq!(paragraph.selected_source(), "$甲^2$$b$");
3048 assert!(paragraph.has_selection());
3049 paragraph.clear_selection();
3050 assert_eq!(paragraph.selected_text(), "");
3051 assert_eq!(paragraph.selected_source(), "");
3052 assert!(!paragraph.has_selection());
3053 }
3054
3055 #[test]
3056 fn selected_inline_object_interleaves_runs_and_preserves_enclosing_mark() {
3057 let before = InlineNode::new("中文 ");
3058 let formula =
3059 InlineNode::custom(MarkdownNode::new("math", ()).text("x²").markdown("$x^2$"))
3060 .marks(vec![(0..3, TextMark::default().bold())]);
3061 let after = InlineNode::new(" English");
3062 {
3063 let mut preceding = formula.state.lock().unwrap();
3064 preceding.text = "中文 ".into();
3065 preceding.selection = Some((3..7).into());
3066 }
3067 *formula.custom_selection.lock().unwrap() = true;
3068 let paragraph = Paragraph {
3069 children: vec![before, formula, after],
3070 ..Default::default()
3071 };
3072 {
3073 let mut trailing = paragraph.state.lock().unwrap();
3074 trailing.text = " English".into();
3075 trailing.selection = Some((0..4).into());
3076 }
3077 assert_eq!(paragraph.selected_text(), "文 x² Eng");
3078 assert_eq!(paragraph.selected_source(), "文 **$x^2$** Eng");
3079 }
3080
3081 #[test]
3082 fn custom_inline_inherits_marks_and_resolves_link_references() {
3083 use gpui::{Empty, TestApp};
3084 let mut app = TestApp::new();
3085 let mut window = app.open_window(|_, _| Empty);
3086 window.update(|_, _, cx| {
3087 cx.set_global(crate::Theme::default());
3088 let mut node_cx = NodeContext::default();
3089 let mut mark = TextMark::default().bold();
3090 mark.italic = true;
3091 mark.strikethrough = true;
3092 mark.link = Some(LinkMark {
3093 identifier: Some("ref".into()),
3094 ..Default::default()
3095 });
3096 node_cx.link_refs.insert(
3097 "ref".into(),
3098 LinkMark {
3099 url: "https://example.com".into(),
3100 ..Default::default()
3101 },
3102 );
3103 let paragraph = Paragraph {
3104 children: vec![
3105 InlineNode::custom(MarkdownNode::new("test", ()).text("x"))
3106 .marks(vec![(0..1, mark)]),
3107 ],
3108 ..Default::default()
3109 };
3110 let items = paragraph.inline_flow_items(&[], &node_cx, cx);
3111 let InlineFlowItem::Object { style, link, .. } = &items[0] else {
3112 panic!()
3113 };
3114 assert_eq!(style.font_weight, Some(FontWeight::BOLD));
3115 assert_eq!(style.font_style, Some(FontStyle::Italic));
3116 assert!(style.strikethrough.is_some());
3117 assert_eq!(link.as_ref().unwrap().url.as_ref(), "https://example.com");
3118 });
3119 }
3120
3121 #[test]
3122 fn table_column_uses_prepared_inline_metrics_after_resource_update() {
3123 use crate::text::InlineElement;
3124 use crate::text::inline::test_draw::in_prepaint;
3125 use gpui::{Image, ImageFormat, TestApp};
3126 let mut app = TestApp::new();
3127 let width = Arc::new(std::sync::atomic::AtomicUsize::new(400));
3128 let render_width = width.clone();
3129 let mut node_cx = NodeContext::default();
3130 node_cx.markdown_extensions = Arc::new(MarkdownExtensions::default().plugin(
3131 crate::text::markdown_ext::TestInlinePlugin::new("test").render_with(
3132 move |_, _, _, _| {
3133 Some(
3134 InlineElement::new(
3135 gpui::img(Arc::new(Image::from_bytes(
3136 ImageFormat::Svg,
3137 b"<svg/>".to_vec(),
3138 )))
3139 .w(px(
3140 render_width.load(std::sync::atomic::Ordering::Relaxed) as f32
3141 ))
3142 .h(px(20.)),
3143 )
3144 .with_baseline(px(15.)),
3145 )
3146 },
3147 ),
3148 ));
3149 let table = table_of(
3150 vec![vec![TableCell {
3151 children: Paragraph {
3152 children: vec![InlineNode::custom(MarkdownNode::new("test", ()).text("x"))],
3153 ..Default::default()
3154 },
3155 width: None,
3156 }]],
3157 vec![],
3158 );
3159 in_prepaint(&mut app, move |window, cx| {
3160 for expected in [400, 600] {
3161 width.store(expected, std::sync::atomic::Ordering::Relaxed);
3162 assert_eq!(
3163 measure_table_columns(&table, 1, &node_cx, window, cx)[0],
3164 expected as f32 + CELL_PAD_PX
3165 );
3166 }
3167 });
3168 }
3169
3170 #[test]
3174 fn table_column_of_inline_code_cells_fits_the_mono_width() {
3175 use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
3176 use gpui::{Empty, TestApp};
3177
3178 let code = "method_name()";
3179 let mut paragraph = Paragraph::default();
3180 paragraph
3181 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
3182 let table = Table {
3183 children: vec![TableRow {
3184 children: vec![TableCell {
3185 children: paragraph,
3186 width: None,
3187 }],
3188 }],
3189 column_aligns: vec![],
3190 span: None,
3191 };
3192 let node_cx = NodeContext::default();
3193
3194 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3195 let mut window = app.open_window(|_, _| Empty);
3196 let (col_w, font_size) = window.update(|_, window, cx| {
3197 let mut theme = crate::Theme::default();
3198 theme.tokens.typography.mono = MONO.into();
3199 cx.set_global(theme);
3200 let font_size = window.text_style().font_size.to_pixels(window.rem_size());
3201 (
3202 measure_table_columns(&table, 1, &node_cx, window, cx),
3203 font_size,
3204 )
3205 });
3206
3207 let mono_w = f32::from(WideMonoTextSystem::width_of(code, MONO, font_size * 0.875));
3208 assert!(
3209 (col_w[0]
3210 - (mono_w + CELL_PAD_PX + crate::text::inline_flow::INLINE_CODE_PADDING * 2.))
3211 .abs()
3212 < 0.01,
3213 "col_w {} must fit the mono width {} plus padding {}",
3214 col_w[0],
3215 mono_w,
3216 CELL_PAD_PX
3217 );
3218 }
3219
3220 #[test]
3221 fn code_block_highlights_are_cached_by_highlighter_identity() {
3222 use std::sync::atomic::{AtomicUsize, Ordering};
3223
3224 let calls = Arc::new(AtomicUsize::new(0));
3225 let calls_for_highlighter = calls.clone();
3226 let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
3227 calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
3228 Vec::new()
3229 });
3230 let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
3231
3232 block.highlighted_styles(&highlighter);
3233 block.highlighted_styles(&highlighter);
3234 assert_eq!(calls.load(Ordering::Relaxed), 1);
3235
3236 let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
3237 block.highlighted_styles(&replacement);
3238 assert!(Arc::ptr_eq(
3239 &block
3240 .highlight_cache
3241 .lock()
3242 .unwrap()
3243 .as_ref()
3244 .unwrap()
3245 .highlighter,
3246 &replacement
3247 ));
3248 }
3249
3250 #[test]
3251 fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
3252 let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
3256 vec![(
3257 0..2,
3258 HighlightStyle {
3259 color: Some(gpui::rgb(0x0000ff).into()),
3260 ..Default::default()
3261 },
3262 )]
3263 });
3264 let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
3265 vec![(
3266 0..2,
3267 HighlightStyle {
3268 color: Some(gpui::rgb(0xffff00).into()),
3269 ..Default::default()
3270 },
3271 )]
3272 });
3273 let block = CodeBlock::from_code("42", Some("json"));
3274
3275 let light_styles = block.highlighted_styles(&light);
3276 let dark_styles = block.highlighted_styles(&dark);
3277
3278 assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
3279 assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
3280 assert_eq!(block.code(), "42", "the document must survive the swap");
3281 }
3282
3283 #[test]
3284 fn reconstruct_markdown_wraps_marked_runs() {
3285 let marks = vec![(0..4, TextMark::default().bold())];
3287 assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
3288 assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
3290 }
3291
3292 #[test]
3293 fn reconstruct_markdown_emits_unmarked_text_verbatim() {
3294 let text = "a b c";
3296 let marks = vec![(2..3, TextMark::default().code())];
3297 assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
3298 assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
3300 }
3301
3302 #[test]
3303 fn reconstruct_markdown_handles_code_italic_strike_link() {
3304 assert_eq!(
3305 reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
3306 "`x`"
3307 );
3308 assert_eq!(
3309 reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
3310 "*x*"
3311 );
3312 assert_eq!(
3313 reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
3314 "~~x~~"
3315 );
3316 let link = TextMark::default().link(LinkMark {
3317 url: "https://example.com".into(),
3318 ..Default::default()
3319 });
3320 assert_eq!(
3321 reconstruct_markdown("x", &[(0..1, link)], 0..1),
3322 "[x](https://example.com)"
3323 );
3324 }
3325
3326 #[test]
3327 fn reconstruct_markdown_nested_bold_italic() {
3328 let mark = TextMark::default().bold().italic();
3330 assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
3332 }
3333
3334 fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
3338 let combined: String = children.iter().map(|c| c.text.to_string()).collect();
3339 let paragraph = Paragraph {
3340 span: None,
3341 children,
3342 link_refs: HashMap::new(),
3343 state: Arc::new(Mutex::new(InlineState::default())),
3344 render_cache: ParagraphRenderCache::default(),
3345 };
3346 if let Ok(mut state) = paragraph.state.lock() {
3347 state.set_text(combined.into());
3348 }
3349 paragraph
3350 }
3351
3352 fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
3353 if let Ok(mut state) = paragraph.state.lock() {
3354 state.selection = Some(range.into());
3355 }
3356 }
3357
3358 #[test]
3359 fn paragraph_selected_source_maps_partial_selection_across_runs() {
3360 let children = vec![
3362 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
3363 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
3364 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
3365 ];
3366 let paragraph = paragraph_with_children(children);
3367
3368 set_paragraph_selection(¶graph, 0..(9 + 4 + 6));
3370 assert_eq!(paragraph.selected_source(), "This has **bold** text.");
3371
3372 set_paragraph_selection(¶graph, 5..16);
3375 assert_eq!(paragraph.selected_source(), "has **bold** te");
3376
3377 set_paragraph_selection(¶graph, 10..12);
3379 assert_eq!(paragraph.selected_source(), "**ol**");
3380 }
3381
3382 #[test]
3383 fn paragraph_selected_source_matches_text_when_no_marks() {
3384 let children =
3385 vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
3386 let paragraph = paragraph_with_children(children);
3387 set_paragraph_selection(¶graph, 0..11);
3388 assert_eq!(paragraph.selected_source(), "plain words");
3389 assert_eq!(paragraph.selected_text(), "plain words");
3390 }
3391
3392 fn selected_paragraph(text: &str) -> Paragraph {
3393 let len = text.len();
3394 let paragraph = paragraph_with_children(vec![
3395 InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
3396 ]);
3397 set_paragraph_selection(¶graph, 0..len);
3398 paragraph
3399 }
3400
3401 #[test]
3402 fn heading_selected_source_prefixes_hashes() {
3403 let heading = BlockNode::Heading {
3404 level: 2,
3405 children: selected_paragraph("Title"),
3406 span: None,
3407 };
3408 assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
3409 assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
3411 }
3412
3413 #[test]
3414 fn unordered_list_selected_source_prefixes_dash() {
3415 let list = BlockNode::List {
3416 ordered: false,
3417 span: None,
3418 children: vec![
3419 BlockNode::ListItem {
3420 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
3421 spread: false,
3422 checked: None,
3423 span: None,
3424 },
3425 BlockNode::ListItem {
3426 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
3427 spread: false,
3428 checked: None,
3429 span: None,
3430 },
3431 ],
3432 };
3433 assert_eq!(
3434 list.selected_text(SelectionFormat::Source),
3435 "- one\n- two\n"
3436 );
3437 }
3438
3439 #[test]
3440 fn ordered_list_selected_source_prefixes_numbers() {
3441 let list = BlockNode::List {
3442 ordered: true,
3443 span: None,
3444 children: vec![
3445 BlockNode::ListItem {
3446 children: vec![BlockNode::Paragraph(selected_paragraph("first"))],
3447 spread: false,
3448 checked: None,
3449 span: None,
3450 },
3451 BlockNode::ListItem {
3452 children: vec![BlockNode::Paragraph(selected_paragraph("second"))],
3453 spread: false,
3454 checked: None,
3455 span: None,
3456 },
3457 ],
3458 };
3459 assert_eq!(
3460 list.selected_text(SelectionFormat::Source),
3461 "1. first\n2. second\n"
3462 );
3463 }
3464
3465 #[test]
3466 fn nested_list_selected_source_indents_sublists() {
3467 let nested = BlockNode::List {
3471 ordered: false,
3472 span: None,
3473 children: vec![BlockNode::ListItem {
3474 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
3475 spread: false,
3476 checked: None,
3477 span: None,
3478 }],
3479 };
3480 let list = BlockNode::List {
3481 ordered: false,
3482 span: None,
3483 children: vec![
3484 BlockNode::ListItem {
3485 children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
3486 spread: false,
3487 checked: None,
3488 span: None,
3489 },
3490 BlockNode::ListItem {
3491 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
3492 spread: false,
3493 checked: None,
3494 span: None,
3495 },
3496 ],
3497 };
3498 assert_eq!(
3499 list.selected_text(SelectionFormat::Source),
3500 "- one\n - nested\n- two\n"
3501 );
3502 }
3503
3504 #[test]
3505 fn task_list_selected_source_restores_checkboxes() {
3506 let list = BlockNode::List {
3507 ordered: false,
3508 span: None,
3509 children: vec![
3510 BlockNode::ListItem {
3511 children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
3512 spread: false,
3513 checked: Some(true),
3514 span: None,
3515 },
3516 BlockNode::ListItem {
3517 children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
3518 spread: false,
3519 checked: Some(false),
3520 span: None,
3521 },
3522 ],
3523 };
3524 assert_eq!(
3525 list.selected_text(SelectionFormat::Source),
3526 "- [x] done\n- [ ] todo\n"
3527 );
3528 }
3529
3530 #[test]
3531 fn blockquote_selected_source_prefixes_gt() {
3532 let quote = BlockNode::Blockquote {
3533 span: None,
3534 children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
3535 };
3536 assert_eq!(
3537 quote.selected_text(SelectionFormat::Source),
3538 "> quoted text\n"
3539 );
3540 }
3541
3542 #[test]
3543 fn table_selected_source_pipes_cells_with_alignment_row() {
3544 let cell = |text: &str| TableCell {
3545 children: selected_paragraph(text),
3546 width: None,
3547 };
3548 let table = Table {
3549 children: vec![
3550 TableRow {
3551 children: vec![cell("Name"), cell("Age")],
3552 },
3553 TableRow {
3554 children: vec![cell("Alice"), cell("30")],
3555 },
3556 ],
3557 column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
3558 span: None,
3559 };
3560 let block = BlockNode::Table(table);
3561 assert_eq!(
3562 block.selected_text(SelectionFormat::Source),
3563 "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
3564 );
3565 }
3566
3567 fn plain_cell(text: &str) -> TableCell {
3570 TableCell {
3571 children: Paragraph::new(text.to_string()),
3572 width: None,
3573 }
3574 }
3575
3576 fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
3577 Table {
3578 children: rows
3579 .into_iter()
3580 .map(|children| TableRow { children })
3581 .collect(),
3582 column_aligns,
3583 span: None,
3584 }
3585 }
3586
3587 #[test]
3588 fn table_to_markdown_pipes_cells_with_alignment_row() {
3589 let table = table_of(
3590 vec![
3591 vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
3592 vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
3593 ],
3594 vec![
3595 ColumnumnAlign::Left,
3596 ColumnumnAlign::Center,
3597 ColumnumnAlign::Right,
3598 ],
3599 );
3600
3601 assert_eq!(
3602 table.to_markdown(),
3603 "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
3604 );
3605 assert_eq!(
3607 BlockNode::Table(table.clone()).to_markdown(),
3608 table.to_markdown()
3609 );
3610 }
3611
3612 #[test]
3613 fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
3614 let table = table_of(
3615 vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
3616 vec![ColumnumnAlign::Left],
3617 );
3618
3619 assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
3620 }
3621
3622 #[test]
3623 fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
3624 let bold = TableCell {
3625 children: paragraph_with_children(vec![
3626 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
3627 ]),
3628 width: None,
3629 };
3630 let table = table_of(
3631 vec![
3632 vec![plain_cell("a | b"), plain_cell("plain")],
3633 vec![plain_cell("c"), bold],
3634 ],
3635 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
3636 );
3637
3638 assert_eq!(
3639 table.to_markdown(),
3640 "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
3641 );
3642 }
3643
3644 #[test]
3645 fn table_data_snapshots_plain_cells_and_markdown() {
3646 let mut table = table_of(
3647 vec![
3648 vec![plain_cell(" Name "), plain_cell("Age")],
3649 vec![plain_cell("Alice"), plain_cell("30")],
3650 ],
3651 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
3652 );
3653 table.span = Some(Span { start: 4, end: 42 });
3654
3655 let data = table.table_data();
3656 assert_eq!(data.headers, vec!["Name", "Age"]);
3657 assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
3658 assert_eq!(data.markdown, table.to_markdown());
3659 assert_eq!(data.span, Some(4..42));
3660 }
3661
3662 #[test]
3663 fn table_data_handles_tables_without_rows() {
3664 let header_only = table_of(
3666 vec![vec![plain_cell("Name"), plain_cell("Age")]],
3667 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
3668 );
3669 let data = header_only.table_data();
3670 assert_eq!(data.headers, vec!["Name", "Age"]);
3671 assert!(data.rows.is_empty());
3672 assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
3673
3674 assert_eq!(Table::default().table_data(), TableData::default());
3676 }
3677
3678 #[test]
3679 fn test_image_node_source() {
3680 use gpui::{ImageFormat, ImageSource, Resource};
3681
3682 fn image_node(url: &str) -> ImageNode {
3683 ImageNode {
3684 url: url.into(),
3685 ..Default::default()
3686 }
3687 }
3688
3689 fn assert_uri(url: &str) {
3693 match image_node(url).source() {
3694 ImageSource::Resource(Resource::Uri(uri)) => assert_eq!(uri.as_ref(), url),
3695 _ => panic!("expected Uri for {url:?}"),
3696 }
3697 }
3698 assert_uri("https://example.com/logo.png");
3699 assert_uri("http://example.com/logo.png");
3700 assert_uri("website/public/logo.svg");
3701 assert_uri("./images/a.png");
3702 assert_uri("../images/a.png");
3703 assert_uri("/absolute/path/logo.svg");
3704 assert_uri("file:///absolute/path/logo.svg");
3705 assert_uri(r"C:\images\logo.png");
3706 assert_uri("docs/a:b.png");
3707 assert_uri("data:text/plain;base64,aGVsbG8=");
3708
3709 let node = image_node("data:image/png;base64,iVBORw0KGgo=");
3712 let ImageSource::Image(first) = node.source() else {
3713 panic!("expected an embedded image");
3714 };
3715 assert_eq!(first.format(), ImageFormat::Png);
3716 assert_eq!(first.bytes(), b"\x89PNG\r\n\x1a\n");
3717 let ImageSource::Image(second) = node.source() else {
3718 panic!("expected an embedded image");
3719 };
3720 assert!(Arc::ptr_eq(&first, &second));
3721 }
3722
3723 fn image_paragraph(alt: &str, url: &str) -> Paragraph {
3724 let image = ImageNode {
3725 url: url.into(),
3726 alt: Some(alt.into()),
3727 ..Default::default()
3728 };
3729 Paragraph {
3730 span: None,
3731 children: vec![InlineNode::image(image)],
3732 link_refs: HashMap::new(),
3733 state: Arc::new(Mutex::new(InlineState::default())),
3734 render_cache: ParagraphRenderCache::default(),
3735 }
3736 }
3737
3738 #[test]
3741 fn marks_round_trip_through_reconstruction() {
3742 let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
3743
3744 assert_eq!(wrap(TextMark::default().bold()), "**x**");
3745 assert_eq!(wrap(TextMark::default().italic()), "*x*");
3746 assert_eq!(wrap(TextMark::default().code()), "`x`");
3747 assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
3748 assert_eq!(
3749 wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
3750 "==x=="
3751 );
3752 assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
3754
3755 assert_eq!(
3757 wrap(TextMark::default().link(LinkMark {
3758 url: "https://example.com".into(),
3759 title: Some("Tip".into()),
3760 ..Default::default()
3761 })),
3762 "[x](https://example.com \"Tip\")"
3763 );
3764 }
3765
3766 #[test]
3769 fn document_selected_source_slices_covered_blocks_from_the_source() {
3770 use crate::text::document::ParsedDocument;
3771
3772 let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
3775 let list = "3. _one_\n4. two";
3776 let list_start = source.find(list).unwrap();
3777 let rule_start = source.find("---").unwrap();
3778
3779 let document = ParsedDocument {
3780 source: source.into(),
3781 blocks: vec![
3782 BlockNode::Paragraph(selected_paragraph("start")),
3783 BlockNode::List {
3784 ordered: true,
3785 children: vec![],
3786 span: Some(Span {
3787 start: list_start,
3788 end: list_start + list.len(),
3789 }),
3790 },
3791 BlockNode::HorizontalRule {
3792 span: Some(Span {
3793 start: rule_start,
3794 end: rule_start + 3,
3795 }),
3796 },
3797 BlockNode::Paragraph(selected_paragraph("end")),
3798 ]
3799 .into(),
3800 };
3801
3802 assert_eq!(
3803 document.selected_text(SelectionFormat::Source, None),
3804 "start\n\n3. _one_\n4. two\n\n---\n\nend"
3805 );
3806 }
3807
3808 #[test]
3809 fn document_selected_source_includes_enclosed_image() {
3810 use crate::text::document::ParsedDocument;
3811
3812 let source = "before\n\n\n\nafter";
3816 let image_markdown = "";
3817 let start = source.find(image_markdown).unwrap();
3818 let mut image = image_paragraph("alt", "https://example.com/i.png");
3819 image.span = Some(Span {
3820 start,
3821 end: start + image_markdown.len(),
3822 });
3823
3824 let document = ParsedDocument {
3825 source: source.into(),
3826 blocks: vec![
3827 BlockNode::Paragraph(selected_paragraph("before")),
3828 BlockNode::Paragraph(image),
3829 BlockNode::Paragraph(selected_paragraph("after")),
3830 ]
3831 .into(),
3832 };
3833 assert_eq!(
3834 document.selected_text(SelectionFormat::Source, None),
3835 "before\n\n\n\nafter"
3836 );
3837 }
3838
3839 #[test]
3840 fn document_selected_source_drops_unenclosed_image() {
3841 use crate::text::document::ParsedDocument;
3842
3843 let document = ParsedDocument {
3846 source: String::new().into(),
3847 blocks: vec![
3848 BlockNode::Paragraph(selected_paragraph("before")),
3849 BlockNode::Paragraph(image_paragraph("alt", "u")),
3850 ]
3851 .into(),
3852 };
3853 assert_eq!(
3854 document.selected_text(SelectionFormat::Source, None),
3855 "before"
3856 );
3857 }
3858
3859 fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
3860 let block = CodeBlock::new(
3861 code.to_string().into(),
3862 lang.map(|l| l.to_string().into()),
3863 None::<Span>,
3864 );
3865 if let Ok(mut state) = block.state.lock() {
3866 let len = state.text.len();
3867 state.selection = Some((0..len).into());
3868 }
3869 BlockNode::CodeBlock(block)
3870 }
3871
3872 #[test]
3873 fn code_block_selected_source_wraps_in_fence_with_lang() {
3874 let block = selected_code_block("let x = 1;\n", Some("rust"));
3875 let code = block.selected_text(SelectionFormat::Plain);
3876 let code_trimmed = code.trim_end_matches('\n');
3877 assert_eq!(
3881 block.selected_text(SelectionFormat::Source),
3882 format!("```rust\n{}\n```\n", code_trimmed)
3883 );
3884 assert!(
3885 block
3886 .selected_text(SelectionFormat::Source)
3887 .starts_with("```rust\n")
3888 );
3889 assert!(
3890 block
3891 .selected_text(SelectionFormat::Source)
3892 .trim_end()
3893 .ends_with("\n```")
3894 );
3895 }
3896
3897 #[test]
3898 fn code_block_selected_source_without_lang() {
3899 let block = selected_code_block("plain\n", None);
3900 let code_trimmed = block.selected_text(SelectionFormat::Plain);
3901 let code_trimmed = code_trimmed.trim_end_matches('\n');
3902 assert_eq!(
3903 block.selected_text(SelectionFormat::Source),
3904 format!("```\n{}\n```\n", code_trimmed)
3905 );
3906 }
3907
3908 #[test]
3909 fn document_selected_source_joins_blocks_with_blank_line() {
3910 use crate::text::document::ParsedDocument;
3911
3912 let document = ParsedDocument {
3916 source: String::new().into(),
3917 blocks: vec![
3918 BlockNode::Heading {
3919 level: 1,
3920 children: selected_paragraph("Title"),
3921 span: None,
3922 },
3923 BlockNode::Paragraph(selected_paragraph("A paragraph.")),
3924 selected_code_block("let x = 1;\n", Some("rust")),
3925 BlockNode::List {
3926 ordered: true,
3927 span: None,
3928 children: vec![
3929 BlockNode::ListItem {
3930 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
3931 spread: false,
3932 checked: None,
3933 span: None,
3934 },
3935 BlockNode::ListItem {
3936 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
3937 spread: false,
3938 checked: None,
3939 span: None,
3940 },
3941 ],
3942 },
3943 ]
3944 .into(),
3945 };
3946
3947 assert_eq!(
3948 document.selected_text(SelectionFormat::Source, None),
3949 "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
3950 );
3951 }
3952
3953 #[test]
3954 fn code_block_equality_includes_code_content() {
3955 let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
3956 let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
3957
3958 assert_ne!(first, second);
3959 }
3960}