1use std::{
2 collections::HashMap,
3 ops::Range,
4 sync::{Arc, Mutex},
5};
6
7use gpui::{
8 AnyElement, App, DefiniteLength, Div, ElementId, FontStyle, FontWeight, HighlightStyle, Hsla,
9 Image, ImageFormat, InteractiveElement as _, IntoElement, Length, ObjectFit, Overflow,
10 ParentElement, Pixels, ScrollHandle, SharedString, SharedUri, StatefulInteractiveElement,
11 StyleRefinement, Styled, StyledImage as _, WhiteSpace, Window, div, img,
12 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::{Inline, InlineState},
24 inline_flow::{InlineFlow, InlineFlowItem},
25 text_view::handle_link_click,
26 },
27 theme::ActiveTheme as _,
28 v_flex,
29};
30
31use super::{
32 SelectionFormat, TextViewStyle,
33 utils::{image_source, list_item_prefix},
34};
35
36const 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>"#;
37const 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>"#;
38
39#[derive(Debug, Clone, PartialEq)]
41pub(crate) enum BlockNode {
42 Root {
44 children: Vec<BlockNode>,
45 span: Option<Span>,
46 },
47 Paragraph(Paragraph),
48 Heading {
49 level: u8,
50 children: Paragraph,
51 span: Option<Span>,
52 },
53 Blockquote {
54 children: Vec<BlockNode>,
55 span: Option<Span>,
56 },
57 List {
58 children: Vec<BlockNode>,
60 ordered: bool,
61 span: Option<Span>,
62 },
63 ListItem {
64 children: Vec<BlockNode>,
65 spread: bool,
66 checked: Option<bool>,
68 span: Option<Span>,
69 },
70 CodeBlock(CodeBlock),
71 Custom(MarkdownNode),
73 Table(Table),
74 Break {
75 html: bool,
76 span: Option<Span>,
77 },
78 HorizontalRule {
79 span: Option<Span>,
80 },
81 Definition {
83 identifier: SharedString,
84 url: SharedString,
85 title: Option<SharedString>,
86 span: Option<Span>,
87 },
88 Unknown,
89}
90
91#[derive(Clone, Copy)]
92enum BlockTextKind {
93 All,
94 Selected,
95 SelectedSource,
98}
99
100impl BlockNode {
101 pub(super) fn is_list_item(&self) -> bool {
102 matches!(self, Self::ListItem { .. })
103 }
104
105 pub(super) fn compact(self) -> BlockNode {
107 match self {
108 Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
109 _ => self,
110 }
111 }
112
113 pub(crate) fn span(&self) -> Option<Span> {
115 match self {
116 BlockNode::Root { span, .. } => *span,
117 BlockNode::Paragraph(paragraph) => paragraph.span,
118 BlockNode::Heading { span, .. } => *span,
119 BlockNode::Blockquote { span, .. } => *span,
120 BlockNode::List { span, .. } => *span,
121 BlockNode::ListItem { span, .. } => *span,
122 BlockNode::CodeBlock(code_block) => code_block.span,
123 BlockNode::Custom(el) => el.span,
124 BlockNode::Table(table) => table.span,
125 BlockNode::Break { span, .. } => *span,
126 BlockNode::HorizontalRule { span, .. } => *span,
127 BlockNode::Definition { span, .. } => *span,
128 BlockNode::Unknown { .. } => None,
129 }
130 }
131
132 pub(super) fn text(&self) -> String {
133 self.text_by_kind(BlockTextKind::All)
134 }
135
136 pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
141 self.text_by_kind(match format {
142 SelectionFormat::Plain => BlockTextKind::Selected,
143 SelectionFormat::Source => BlockTextKind::SelectedSource,
144 })
145 }
146
147 fn text_by_kind(&self, kind: BlockTextKind) -> String {
148 let mut text = String::new();
149 match self {
150 BlockNode::Root { children, .. } => {
151 let block_text = Self::children_text(children, kind);
152 if !block_text.is_empty() {
153 text.push_str(&block_text);
154 text.push('\n');
155 }
156 }
157 BlockNode::Paragraph(paragraph) => {
158 let block_text = match kind {
159 BlockTextKind::All => paragraph.text(),
160 BlockTextKind::Selected => paragraph.selected_text(),
161 BlockTextKind::SelectedSource => paragraph.selected_source(),
162 };
163 if !block_text.is_empty() {
164 text.push_str(&block_text);
165 text.push('\n');
166 }
167 }
168 BlockNode::Heading {
169 level, children, ..
170 } => {
171 let block_text = match kind {
172 BlockTextKind::All => children.text(),
173 BlockTextKind::Selected => children.selected_text(),
174 BlockTextKind::SelectedSource => children.selected_source(),
175 };
176 if !block_text.is_empty() {
177 if matches!(kind, BlockTextKind::SelectedSource) {
180 text.push_str(&"#".repeat(*level as usize));
181 text.push(' ');
182 }
183 text.push_str(&block_text);
184 text.push('\n');
185 }
186 }
187 BlockNode::List {
188 children, ordered, ..
189 } => {
190 if matches!(kind, BlockTextKind::SelectedSource) {
191 text.push_str(&list_selected_source(children, *ordered, ""));
194 } else {
195 text.push_str(&Self::children_text(children, kind));
196 }
197 }
198 BlockNode::ListItem { children, .. } => {
199 text.push_str(&Self::children_text(children, kind));
200 }
201 BlockNode::Blockquote { children, .. } => {
202 let block_text = Self::children_text(children, kind);
203
204 if !block_text.is_empty() {
205 if matches!(kind, BlockTextKind::SelectedSource) {
206 let quoted = block_text
209 .trim_end_matches('\n')
210 .lines()
211 .map(|line| {
212 if line.is_empty() {
213 ">".to_string()
214 } else {
215 format!("> {}", line)
216 }
217 })
218 .collect::<Vec<_>>()
219 .join("\n");
220 text.push_str("ed);
221 } else {
222 text.push_str(&block_text);
223 }
224 text.push('\n');
225 }
226 }
227 BlockNode::Table(table) => {
228 if matches!(kind, BlockTextKind::SelectedSource) {
229 let block_text = table_selected_source(table);
230 if !block_text.is_empty() {
231 text.push_str(&block_text);
232 text.push('\n');
233 }
234 } else {
235 let mut block_text = String::new();
236 for row in table.children.iter() {
237 let mut row_texts = vec![];
238 for cell in row.children.iter() {
239 row_texts.push(match kind {
240 BlockTextKind::All => cell.children.text(),
241 _ => cell.children.selected_text(),
243 });
244 }
245 if !row_texts.is_empty() {
246 block_text.push_str(&row_texts.join(" "));
247 block_text.push('\n');
248 }
249 }
250
251 if !block_text.is_empty() {
252 text.push_str(&block_text);
253 text.push('\n');
254 }
255 }
256 }
257 BlockNode::CodeBlock(code_block) => {
258 let block_text = match kind {
259 BlockTextKind::All => code_block.text(),
260 BlockTextKind::Selected => code_block.selected_text(),
261 BlockTextKind::SelectedSource => code_block.selected_source(),
262 };
263 if !block_text.is_empty() {
264 text.push_str(&block_text);
265 text.push('\n');
266 }
267 }
268 BlockNode::Custom(node) => {
269 if let BlockTextKind::All = kind {
270 let content = node.as_text();
271 if !content.is_empty() {
272 text.push_str(content);
273 text.push('\n');
274 }
275 }
276 }
277 BlockNode::Definition { .. }
278 | BlockNode::Break { .. }
279 | BlockNode::HorizontalRule { .. }
280 | BlockNode::Unknown { .. } => {}
281 }
282
283 text
284 }
285
286 fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
287 let mut text = String::new();
288 for child in children.iter() {
289 text.push_str(&child.text_by_kind(kind));
290 }
291
292 text
293 }
294
295 pub(super) fn has_selection(&self) -> bool {
306 match self {
307 BlockNode::Root { children, .. }
308 | BlockNode::Blockquote { children, .. }
309 | BlockNode::List { children, .. }
310 | BlockNode::ListItem { children, .. } => {
311 children.iter().any(|child| child.has_selection())
312 }
313 BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
314 BlockNode::Heading { children, .. } => children.has_selection(),
315 BlockNode::Table(table) => table.children.iter().any(|row| {
316 row.children
317 .iter()
318 .any(|cell| cell.children.has_selection())
319 }),
320 BlockNode::CodeBlock(code_block) => code_block.has_selection(),
321 BlockNode::Custom { .. }
322 | BlockNode::Definition { .. }
323 | BlockNode::Break { .. }
324 | BlockNode::HorizontalRule { .. }
325 | BlockNode::Unknown { .. } => false,
326 }
327 }
328
329 pub(super) fn clear_selection(&self) {
330 match self {
331 BlockNode::Root { children, .. }
332 | BlockNode::Blockquote { children, .. }
333 | BlockNode::List { children, .. }
334 | BlockNode::ListItem { children, .. } => {
335 for child in children.iter() {
336 child.clear_selection();
337 }
338 }
339 BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
340 BlockNode::Heading { children, .. } => children.clear_selection(),
341 BlockNode::Table(table) => {
342 for row in table.children.iter() {
343 for cell in row.children.iter() {
344 cell.children.clear_selection();
345 }
346 }
347 }
348 BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
349 BlockNode::Custom { .. }
350 | BlockNode::Definition { .. }
351 | BlockNode::Break { .. }
352 | BlockNode::HorizontalRule { .. }
353 | BlockNode::Unknown { .. } => {}
354 }
355 }
356}
357
358#[allow(unused)]
359#[derive(Debug, Default, Clone, PartialEq)]
360pub struct LinkMark {
361 pub url: SharedString,
362 pub identifier: Option<SharedString>,
364 pub title: Option<SharedString>,
365}
366
367#[derive(Debug, Default, Clone, PartialEq)]
368pub struct TextMark {
369 pub bold: bool,
370 pub italic: bool,
371 pub strikethrough: bool,
372 pub underline: bool,
373 pub code: bool,
374 pub highlight: Option<Hsla>,
378 pub link: Option<LinkMark>,
379}
380
381impl TextMark {
382 pub fn bold(mut self) -> Self {
383 self.bold = true;
384 self
385 }
386
387 pub fn italic(mut self) -> Self {
388 self.italic = true;
389 self
390 }
391
392 pub fn strikethrough(mut self) -> Self {
393 self.strikethrough = true;
394 self
395 }
396
397 pub fn underline(mut self) -> Self {
398 self.underline = true;
399 self
400 }
401
402 pub fn code(mut self) -> Self {
403 self.code = true;
404 self
405 }
406
407 pub fn highlight(mut self, color: Hsla) -> Self {
409 self.highlight = Some(color);
410 self
411 }
412
413 pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
414 self.link = Some(link.into());
415 self
416 }
417
418 pub fn merge(&mut self, other: TextMark) {
419 self.bold |= other.bold;
420 self.italic |= other.italic;
421 self.strikethrough |= other.strikethrough;
422 self.underline |= other.underline;
423 self.code |= other.code;
424 if other.highlight.is_some() {
425 self.highlight = other.highlight;
426 }
427 if let Some(link) = other.link {
428 self.link = Some(link);
429 }
430 }
431}
432
433#[derive(Debug, Default, Copy, Clone, PartialEq)]
435pub struct Span {
436 pub start: usize,
437 pub end: usize,
438}
439
440impl From<Span> for ElementId {
441 fn from(value: Span) -> Self {
442 ElementId::Name(format!("md-{}:{}", value.start, value.end).into())
443 }
444}
445
446#[allow(unused)]
447#[derive(Debug, Default, Clone)]
448pub struct ImageNode {
449 pub url: SharedUri,
450 pub link: Option<LinkMark>,
451 pub title: Option<SharedString>,
452 pub alt: Option<SharedString>,
453 pub width: Option<DefiniteLength>,
454 pub height: Option<DefiniteLength>,
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
466impl PartialEq for ImageNode {
467 fn eq(&self, other: &Self) -> bool {
468 self.url == other.url
469 && self.link == other.link
470 && self.title == other.title
471 && self.alt == other.alt
472 && self.width == other.width
473 && self.height == other.height
474 }
475}
476
477#[derive(Default, Clone, Debug)]
478pub(crate) struct InlineNode {
479 pub(crate) text: SharedString,
481 pub(crate) image: Option<ImageNode>,
482 pub(crate) marks: Vec<(Range<usize>, TextMark)>,
484
485 state: Arc<Mutex<InlineState>>,
486}
487
488impl PartialEq for InlineNode {
489 fn eq(&self, other: &Self) -> bool {
490 self.text == other.text && self.image == other.image && self.marks == other.marks
491 }
492}
493
494pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
501 if text.is_empty() {
502 return String::new();
503 }
504
505 let mut out = text.to_string();
506 if mark.code {
507 out = format!("`{}`", out);
508 }
509 if mark.italic {
510 out = format!("*{}*", out);
511 }
512 if mark.bold {
513 out = format!("**{}**", out);
514 }
515 if mark.strikethrough {
516 out = format!("~~{}~~", out);
517 }
518 if mark.underline {
519 out = format!("<u>{}</u>", out);
522 }
523 if mark.highlight.is_some() {
524 out = format!("=={}==", out);
525 }
526 if let Some(link) = &mark.link {
527 out = match &link.title {
528 Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
529 None => format!("[{}]({})", out, link.url),
530 };
531 }
532 out
533}
534
535#[derive(Default)]
538struct RunSelection {
539 emitted: bool,
540 at_start: bool,
541 at_end: bool,
542}
543
544fn emit_run(
548 state: &Arc<Mutex<InlineState>>,
549 run: &[(usize, &InlineNode)],
550 pending_images: &mut Vec<String>,
551 out: &mut String,
552) -> RunSelection {
553 let mut selected = RunSelection::default();
554 let Ok(state) = state.lock() else {
555 return selected;
556 };
557 let Some(selection) = &state.selection else {
558 return selected;
559 };
560 if selection.start >= selection.end {
561 return selected;
562 }
563
564 selected.at_start = selection.start == 0;
565 selected.at_end = selection.end >= state.text.len();
566
567 for (start, child) in run {
568 let end = start + child.text.len();
569 let lo = selection.start.max(*start);
570 let hi = selection.end.min(end);
571 if lo >= hi {
572 continue;
573 }
574
575 if !selected.emitted {
576 if selected.at_start {
577 out.push_str(&pending_images.join(""));
578 }
579 pending_images.clear();
580 }
581 selected.emitted = true;
582
583 out.push_str(&reconstruct_markdown(
584 &child.text,
585 &child.marks,
586 (lo - start)..(hi - start),
587 ));
588 }
589
590 selected
591}
592
593fn image_markdown(image: &ImageNode) -> String {
595 let alt = image.alt.clone().unwrap_or_default();
596 let title = image
597 .title
598 .clone()
599 .map_or(String::new(), |title| format!(" \"{}\"", title));
600 format!("", alt, image.url, title)
601}
602
603pub(crate) fn reconstruct_markdown(
612 text: &str,
613 marks: &[(Range<usize>, TextMark)],
614 selection: Range<usize>,
615) -> String {
616 let start = selection.start.min(text.len());
617 let end = selection.end.min(text.len());
618 if start >= end {
619 return String::new();
620 }
621
622 let mut out = String::new();
623 let mut cursor = start;
624 for (range, mark) in marks.iter() {
626 let seg_start = range.start.max(start);
627 let seg_end = range.end.min(end);
628 if seg_start >= seg_end {
629 continue;
630 }
631 if cursor < seg_start {
633 out.push_str(&text[cursor..seg_start]);
634 }
635 out.push_str(&wrap_with_mark(&text[seg_start..seg_end], mark));
636 cursor = seg_end;
637 }
638 if cursor < end {
640 out.push_str(&text[cursor..end]);
641 }
642 out
643}
644
645fn table_selected_source(table: &Table) -> String {
652 let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
653
654 let any_selected = table.children.iter().any(|row| {
655 row.children
656 .iter()
657 .any(|cell| !cell_source(cell).trim().is_empty())
658 });
659 if !any_selected {
660 return String::new();
661 }
662
663 let mut lines: Vec<String> = Vec::new();
664 for (row_ix, row) in table.children.iter().enumerate() {
665 let cells: Vec<String> = row
666 .children
667 .iter()
668 .map(|cell| cell_source(cell).trim().to_string())
669 .collect();
670 lines.push(format!("| {} |", cells.join(" | ")));
671
672 if row_ix == 0 {
675 let aligns: Vec<String> = (0..row.children.len())
676 .map(|ix| {
677 match table.column_align(ix) {
678 ColumnumnAlign::Left => ":--",
679 ColumnumnAlign::Center => ":-:",
680 ColumnumnAlign::Right => "--:",
681 }
682 .to_string()
683 })
684 .collect();
685 lines.push(format!("| {} |", aligns.join(" | ")));
686 }
687 }
688
689 lines.join("\n")
690}
691
692fn list_selected_source(children: &[BlockNode], ordered: bool, indent: &str) -> String {
702 let mut out = String::new();
703 let mut item_ix = 0usize;
704
705 for child in children {
706 let BlockNode::ListItem {
707 children: item_children,
708 checked,
709 ..
710 } = child
711 else {
712 continue;
713 };
714
715 let marker = if ordered {
716 format!("{}. ", item_ix + 1)
717 } else {
718 "- ".to_string()
719 };
720 let checkbox = match checked {
721 Some(true) => "[x] ",
722 Some(false) => "[ ] ",
723 None => "",
724 };
725 let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
726
727 let mut content = String::new();
730 let mut nested = String::new();
731 for sub in item_children {
732 if let BlockNode::List {
733 children: sub_children,
734 ordered: sub_ordered,
735 ..
736 } = sub
737 {
738 nested.push_str(&list_selected_source(
739 sub_children,
740 *sub_ordered,
741 &child_indent,
742 ));
743 } else {
744 content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
745 }
746 }
747 let content = content.trim_end_matches('\n');
748
749 if content.is_empty() && nested.is_empty() {
750 item_ix += 1;
751 continue;
752 }
753
754 if content.is_empty() {
755 out.push_str(indent);
757 out.push_str(&marker);
758 out.push_str(checkbox.trim_end());
759 out.push('\n');
760 } else {
761 let mut lines = content.lines();
764 if let Some(first) = lines.next() {
765 out.push_str(indent);
766 out.push_str(&marker);
767 out.push_str(checkbox);
768 out.push_str(first);
769 out.push('\n');
770 }
771 for line in lines {
772 out.push_str(&child_indent);
773 out.push_str(line);
774 out.push('\n');
775 }
776 }
777 out.push_str(&nested);
778 item_ix += 1;
779 }
780
781 out
782}
783
784impl InlineNode {
785 pub(crate) fn new(text: impl Into<SharedString>) -> Self {
786 Self {
787 text: text.into(),
788 image: None,
789 marks: vec![],
790 state: Arc::new(Mutex::new(InlineState::default())),
791 }
792 }
793
794 pub(crate) fn image(image: ImageNode) -> Self {
795 let mut this = Self::new("");
796 this.image = Some(image);
797 this
798 }
799
800 pub(crate) fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
801 self.marks = marks;
802 self
803 }
804}
805
806#[derive(Debug, Clone, Default)]
811pub(crate) struct Paragraph {
812 pub(super) span: Option<Span>,
813 pub(super) children: Vec<InlineNode>,
814 pub(super) link_refs: HashMap<SharedString, SharedString>,
818
819 pub(crate) state: Arc<Mutex<InlineState>>,
820}
821
822impl PartialEq for Paragraph {
823 fn eq(&self, other: &Self) -> bool {
824 self.span == other.span
825 && self.children == other.children
826 && self.link_refs == other.link_refs
827 }
828}
829
830impl Paragraph {
831 pub(crate) fn new(text: String) -> Self {
832 Self {
833 span: None,
834 children: vec![InlineNode::new(&text)],
835 link_refs: HashMap::new(),
836 state: Arc::new(Mutex::new(InlineState::default())),
837 }
838 }
839
840 pub(super) fn selected_text(&self) -> String {
841 let mut text = String::new();
842
843 for c in self.children.iter() {
844 let Ok(state) = c.state.lock() else {
845 continue;
846 };
847 if let Some(selection) = &state.selection {
848 text.push_str(&state.text[selection.start..selection.end]);
849 }
850 }
851
852 if let Ok(state) = self.state.lock()
853 && let Some(selection) = &state.selection
854 {
855 text.push_str(&state.text[selection.start..selection.end]);
856 }
857
858 text
859 }
860
861 pub(super) fn selected_source(&self) -> String {
881 let mut source = String::new();
882 let mut pending_images: Vec<String> = Vec::new();
883 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
884 let mut offset = 0;
885 let mut enters_image = true;
886
887 for child in self.children.iter() {
888 let Some(image) = &child.image else {
889 run.push((offset, child));
890 offset += child.text.len();
891 continue;
892 };
893
894 let run_before = !run.is_empty();
896 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
897 if run_before {
898 enters_image = selected.emitted && selected.at_end;
899 }
900 if enters_image {
901 pending_images.push(image_markdown(image));
902 } else {
903 pending_images.clear();
904 }
905
906 run.clear();
907 offset = 0;
908 }
909
910 let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
911 if !trailing.emitted && enters_image && !source.is_empty() {
913 source.push_str(&pending_images.join(""));
914 }
915
916 source
917 }
918
919 pub(super) fn text(&self) -> String {
920 let mut text = String::new();
921 for node in self.children.iter() {
922 text.push_str(&node.text);
923 }
924 text
925 }
926
927 pub(super) fn has_selection(&self) -> bool {
931 self.children
932 .iter()
933 .any(|c| c.state.lock().is_ok_and(|state| state.selection.is_some()))
934 || self
935 .state
936 .lock()
937 .is_ok_and(|state| state.selection.is_some())
938 }
939
940 pub(super) fn clear_selection(&self) {
941 for c in self.children.iter() {
942 if let Ok(mut state) = c.state.lock() {
943 state.selection = None;
944 }
945 }
946
947 if let Ok(mut state) = self.state.lock() {
948 state.selection = None;
949 }
950 }
951}
952
953#[derive(Debug, Clone, Default, PartialEq)]
954pub(crate) struct Table {
955 pub(crate) children: Vec<TableRow>,
956 pub(crate) column_aligns: Vec<ColumnumnAlign>,
957 pub(crate) span: Option<Span>,
958}
959
960#[derive(Debug, Clone, Default, PartialEq)]
963pub struct TableData {
964 pub headers: Vec<String>,
966 pub rows: Vec<Vec<String>>,
969 pub markdown: String,
971 pub span: Option<Range<usize>>,
978}
979
980impl Table {
981 pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
982 self.column_aligns.get(index).copied().unwrap_or_default()
983 }
984
985 pub(crate) fn to_markdown(&self) -> String {
992 let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
993
994 for (row_ix, row) in self.children.iter().enumerate() {
995 let cells: Vec<String> = row
996 .children
997 .iter()
998 .map(|cell| {
999 cell.children
1000 .to_markdown()
1001 .trim()
1002 .replace('\n', " ")
1003 .replace('|', "\\|")
1004 })
1005 .collect();
1006 lines.push(format!("| {} |", cells.join(" | ")));
1007
1008 if row_ix == 0 {
1011 let aligns: Vec<String> = (0..row.children.len())
1012 .map(|ix| {
1013 match self.column_align(ix) {
1014 ColumnumnAlign::Left => ":--",
1015 ColumnumnAlign::Center => ":-:",
1016 ColumnumnAlign::Right => "--:",
1017 }
1018 .to_string()
1019 })
1020 .collect();
1021 lines.push(format!("| {} |", aligns.join(" | ")));
1022 }
1023 }
1024
1025 lines.join("\n")
1026 }
1027
1028 pub(crate) fn table_data(&self) -> TableData {
1031 let row_text = |row: &TableRow| {
1032 row.children
1033 .iter()
1034 .map(|cell| cell.children.text().trim().to_string())
1035 .collect::<Vec<_>>()
1036 };
1037
1038 TableData {
1039 headers: self.children.first().map(row_text).unwrap_or_default(),
1040 rows: self.children.iter().skip(1).map(row_text).collect(),
1041 markdown: self.to_markdown(),
1042 span: self.span.map(|span| span.start..span.end),
1043 }
1044 }
1045}
1046
1047#[derive(Debug, Default, Copy, Clone, PartialEq)]
1048pub(crate) enum ColumnumnAlign {
1049 #[default]
1050 Left,
1051 Center,
1052 Right,
1053}
1054
1055impl From<mdast::AlignKind> for ColumnumnAlign {
1056 fn from(value: mdast::AlignKind) -> Self {
1057 match value {
1058 mdast::AlignKind::None => ColumnumnAlign::Left,
1059 mdast::AlignKind::Left => ColumnumnAlign::Left,
1060 mdast::AlignKind::Center => ColumnumnAlign::Center,
1061 mdast::AlignKind::Right => ColumnumnAlign::Right,
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Clone, Default, PartialEq)]
1067pub(crate) struct TableRow {
1068 pub children: Vec<TableCell>,
1069}
1070
1071#[derive(Debug, Clone, Default, PartialEq)]
1072pub(crate) struct TableCell {
1073 pub children: Paragraph,
1074 pub width: Option<DefiniteLength>,
1075}
1076
1077impl Paragraph {
1078 pub(crate) fn take(&mut self) -> Paragraph {
1079 std::mem::replace(
1080 self,
1081 Paragraph {
1082 span: None,
1083 children: vec![],
1084 link_refs: Default::default(),
1085 state: Arc::new(Mutex::new(InlineState::default())),
1086 },
1087 )
1088 }
1089
1090 pub(crate) fn is_image(&self) -> bool {
1091 false
1092 }
1093
1094 pub(crate) fn set_span(&mut self, span: Span) {
1095 self.span = Some(span);
1096 }
1097
1098 pub(crate) fn push_str(&mut self, text: &str) {
1099 self.children.push(
1100 InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
1101 );
1102 }
1103
1104 pub(crate) fn push(&mut self, text: InlineNode) {
1105 self.children.push(text);
1106 }
1107
1108 pub(crate) fn push_image(&mut self, image: ImageNode) {
1109 self.children.push(InlineNode::image(image));
1110 }
1111
1112 pub(crate) fn is_empty(&self) -> bool {
1113 self.children.is_empty()
1114 || self
1115 .children
1116 .iter()
1117 .all(|node| node.text.is_empty() && node.image.is_none())
1118 }
1119
1120 pub(crate) fn text_len(&self) -> usize {
1122 self.children
1123 .iter()
1124 .map(|node| node.text.len())
1125 .sum::<usize>()
1126 }
1127
1128 pub(crate) fn merge(&mut self, other: Self) {
1129 self.children.extend(other.children);
1130 }
1131}
1132
1133#[derive(Debug, Clone)]
1134pub struct CodeBlock {
1135 lang: Option<SharedString>,
1136 state: Arc<Mutex<InlineState>>,
1137 highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
1138 pub span: Option<Span>,
1139}
1140
1141struct CachedCodeBlockHighlights {
1142 highlighter: Arc<CodeBlockHighlighterFn>,
1143 styles: Vec<(Range<usize>, HighlightStyle)>,
1144}
1145
1146impl std::fmt::Debug for CachedCodeBlockHighlights {
1147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1148 f.debug_struct("CachedCodeBlockHighlights")
1149 .field("styles", &self.styles)
1150 .finish_non_exhaustive()
1151 }
1152}
1153
1154impl PartialEq for CodeBlock {
1155 fn eq(&self, other: &Self) -> bool {
1156 self.lang == other.lang && self.code() == other.code() && self.span == other.span
1157 }
1158}
1159
1160impl CodeBlock {
1161 pub fn lang(&self) -> Option<SharedString> {
1163 self.lang.clone()
1164 }
1165
1166 pub fn code(&self) -> SharedString {
1168 self.state
1169 .lock()
1170 .map(|state| state.text.clone())
1171 .unwrap_or_default()
1172 }
1173
1174 pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
1180 Self::new(code.into(), lang.map(Into::into), None::<Span>)
1181 }
1182
1183 pub(crate) fn new(
1184 code: SharedString,
1185 lang: Option<SharedString>,
1186 span: Option<impl Into<Span>>,
1187 ) -> Self {
1188 let state = Arc::new(Mutex::new(InlineState::default()));
1189 if let Ok(mut state) = state.lock() {
1190 state.set_text(code);
1191 }
1192
1193 Self {
1194 lang,
1195 state,
1196 highlight_cache: Arc::new(Mutex::new(None)),
1197 span: span.map(|s| s.into()),
1198 }
1199 }
1200
1201 fn highlighted_styles(
1202 &self,
1203 highlighter: &Arc<CodeBlockHighlighterFn>,
1204 ) -> Vec<(Range<usize>, HighlightStyle)> {
1205 if let Ok(cache) = self.highlight_cache.lock()
1206 && let Some(cache) = cache.as_ref()
1207 && Arc::ptr_eq(&cache.highlighter, highlighter)
1208 {
1209 return cache.styles.clone();
1210 }
1211
1212 let code_len = self.code().len();
1213 let styles = highlighter(self)
1214 .into_iter()
1215 .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
1216 .collect::<Vec<_>>();
1217 if let Ok(mut cache) = self.highlight_cache.lock() {
1218 *cache = Some(CachedCodeBlockHighlights {
1219 highlighter: highlighter.clone(),
1220 styles: styles.clone(),
1221 });
1222 }
1223 styles
1224 }
1225
1226 pub(super) fn selected_text(&self) -> String {
1227 let mut text = String::new();
1228 if let Ok(state) = self.state.lock()
1229 && let Some(selection) = &state.selection
1230 {
1231 text.push_str(&state.text[selection.start..selection.end]);
1232 }
1233 text
1234 }
1235
1236 pub(super) fn selected_source(&self) -> String {
1243 let code = self.selected_text();
1244 if code.is_empty() {
1245 return String::new();
1246 }
1247 let lang = self.lang.clone().unwrap_or_default();
1248 let code = code.trim_end_matches('\n');
1251 format!("```{}\n{}\n```", lang, code)
1252 }
1253
1254 pub(super) fn text(&self) -> String {
1255 self.state
1256 .lock()
1257 .map(|state| state.text.to_string())
1258 .unwrap_or_default()
1259 }
1260
1261 pub(super) fn has_selection(&self) -> bool {
1265 self.state
1266 .lock()
1267 .is_ok_and(|state| state.selection.is_some())
1268 }
1269
1270 pub(super) fn clear_selection(&self) {
1271 if let Ok(mut state) = self.state.lock() {
1272 state.selection = None;
1273 }
1274 }
1275
1276 fn render(
1277 &self,
1278 options: &NodeRenderOptions,
1279 node_cx: &NodeContext,
1280 window: &mut Window,
1281 cx: &mut App,
1282 ) -> AnyElement {
1283 let style = &node_cx.style;
1284
1285 div()
1286 .w_full()
1287 .min_w_0()
1288 .when(!options.is_last, |this| this.pb(style.paragraph_gap()))
1289 .child(
1290 div()
1291 .id(("codeblock", options.ix))
1292 .w_full()
1293 .min_w_0()
1294 .p_3()
1295 .bg(style.code_background())
1296 .font_family(cx.theme().tokens.typography.mono.clone())
1297 .text_size(cx.theme().tokens.typography.mono_md.size)
1298 .relative()
1299 .refine_style(&style.code_block())
1300 .child(Inline::new(
1301 "code",
1302 self.state.clone(),
1303 vec![],
1304 node_cx
1305 .code_block_highlighter
1306 .as_ref()
1307 .map(|highlighter| self.highlighted_styles(highlighter))
1308 .unwrap_or_default(),
1309 node_cx.link_click_handler.clone(),
1310 ))
1311 .when_some(node_cx.code_block_actions.clone(), |this, actions| {
1312 this.child(
1313 div()
1314 .id("actions")
1315 .absolute()
1316 .top_2()
1317 .right_2()
1318 .bg(style.code_background())
1319 .rounded(cx.theme().tokens.radius.md)
1320 .child(actions(&self, window, cx)),
1321 )
1322 }),
1323 )
1324 .into_any_element()
1325 }
1326}
1327
1328#[derive(Default, Clone)]
1330pub(crate) struct NodeContext {
1331 pub(crate) offset: usize,
1334 pub(crate) link_refs: HashMap<SharedString, LinkMark>,
1335 pub(crate) style: TextViewStyle,
1336 pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
1337 pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
1338 pub(crate) table_actions: Option<Arc<TableActionsFn>>,
1339 pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
1340 pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
1341}
1342
1343impl NodeContext {
1344 pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
1345 self.link_refs.insert(identifier, link);
1346 }
1347}
1348
1349impl PartialEq for NodeContext {
1350 fn eq(&self, other: &Self) -> bool {
1351 self.link_refs == other.link_refs && self.style == other.style
1352 }
1355}
1356
1357impl Paragraph {
1358 fn render(&self, node_cx: &NodeContext, _window: &mut Window, cx: &mut App) -> AnyElement {
1359 let span = self.span;
1360 let children = &self.children;
1361
1362 if self.should_render_inline_flow() {
1363 return InlineFlow::new(
1364 span.unwrap_or_default(),
1365 self.inline_flow_items(node_cx, cx),
1366 node_cx.link_click_handler.clone(),
1367 )
1368 .into_any_element();
1369 }
1370
1371 let mut child_nodes: Vec<AnyElement> = vec![];
1372
1373 let mut text = String::new();
1374 let mut highlights: Vec<(Range<usize>, HighlightStyle)> = vec![];
1375 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1376 let mut offset = 0;
1377
1378 let mut ix = 0;
1379 for inline_node in children {
1380 let text_len = inline_node.text.len();
1381 text.push_str(&inline_node.text);
1382
1383 if let Some(image) = &inline_node.image {
1384 if text.len() > 0 {
1385 if let Ok(mut state) = inline_node.state.lock() {
1386 state.set_text(text.clone().into());
1387 }
1388 child_nodes.push(
1389 Inline::new(
1390 ix,
1391 inline_node.state.clone(),
1392 links.clone(),
1393 highlights.clone(),
1394 node_cx.link_click_handler.clone(),
1395 )
1396 .into_any_element(),
1397 );
1398 }
1399 let link_click_handler = node_cx.link_click_handler.clone();
1400 child_nodes.push(
1401 img(image_source(&image.url))
1402 .id(ix)
1403 .object_fit(ObjectFit::Contain)
1404 .max_w(relative(1.))
1405 .when_some(image.width, |this, width| this.w(width))
1406 .when_some(image.link.clone(), |this, link| {
1407 let link_click_handler = link_click_handler.clone();
1408 let aux_link = link.clone();
1409 let aux_link_click_handler = link_click_handler.clone();
1410 this.cursor_pointer()
1411 .on_click(move |event, window, cx| {
1412 crate::TextSelection::end(window, cx);
1413 cx.stop_propagation();
1414 handle_link_click(
1415 &link_click_handler,
1416 link.url.clone(),
1417 event.clone(),
1418 window,
1419 cx,
1420 );
1421 })
1422 .on_aux_click(move |event, window, cx| {
1423 crate::TextSelection::end(window, cx);
1424 cx.stop_propagation();
1425 handle_link_click(
1426 &aux_link_click_handler,
1427 aux_link.url.clone(),
1428 event.clone(),
1429 window,
1430 cx,
1431 );
1432 })
1433 })
1434 .into_any_element(),
1435 );
1436
1437 text.clear();
1438 links.clear();
1439 highlights.clear();
1440 offset = 0;
1441 } else {
1442 let mut node_highlights = vec![];
1443 for (range, style) in &inline_node.marks {
1444 let inner_range = (offset + range.start)..(offset + range.end);
1445
1446 let mut highlight = HighlightStyle::default();
1447 if style.bold {
1448 highlight.font_weight = Some(FontWeight::BOLD);
1449 }
1450 if style.italic {
1451 highlight.font_style = Some(FontStyle::Italic);
1452 }
1453 if style.strikethrough {
1454 highlight.strikethrough = Some(gpui::StrikethroughStyle {
1455 thickness: gpui::px(1.),
1456 ..Default::default()
1457 });
1458 }
1459 if style.underline {
1460 highlight.underline = Some(gpui::UnderlineStyle {
1461 thickness: gpui::px(1.),
1462 ..Default::default()
1463 });
1464 }
1465 if style.code {
1466 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
1467 }
1468 if let Some(color) = style.highlight {
1469 highlight.background_color = Some(color);
1470 }
1471
1472 if let Some(mut link_mark) = style.link.clone() {
1473 highlight.color = Some(node_cx.style.link());
1474 highlight.underline = Some(gpui::UnderlineStyle {
1475 thickness: gpui::px(1.),
1476 ..Default::default()
1477 });
1478
1479 if let Some(identifier) = link_mark.identifier.as_ref() {
1481 if let Some(mark) = node_cx.link_refs.get(identifier) {
1482 link_mark = mark.clone();
1483 }
1484 }
1485
1486 links.push((inner_range.clone(), link_mark));
1487 }
1488
1489 node_highlights.push((inner_range, highlight));
1490 }
1491
1492 highlights = gpui::combine_highlights(highlights, node_highlights).collect();
1493 offset += text_len;
1494 }
1495 ix += 1;
1496 }
1497
1498 if text.len() > 0 {
1500 if let Ok(mut state) = self.state.lock() {
1501 state.set_text(text.into());
1502 }
1503 child_nodes.push(
1504 Inline::new(
1505 ix,
1506 self.state.clone(),
1507 links,
1508 highlights,
1509 node_cx.link_click_handler.clone(),
1510 )
1511 .into_any_element(),
1512 );
1513 }
1514
1515 div()
1516 .id(span.unwrap_or_default())
1517 .children(child_nodes)
1518 .into_any_element()
1519 }
1520
1521 fn should_render_inline_flow(&self) -> bool {
1522 let has_image = self.children.iter().any(|child| child.image.is_some());
1523 let has_text = self.children.iter().any(|child| !child.text.is_empty());
1524 has_image && has_text
1525 }
1526
1527 fn inline_flow_items(&self, node_cx: &NodeContext, _cx: &mut App) -> Vec<InlineFlowItem> {
1528 let mut items = Vec::new();
1529 let mut text = String::new();
1530 let mut highlights: Vec<(Range<usize>, HighlightStyle)> = vec![];
1531 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1532 let mut offset = 0;
1533
1534 for inline_node in &self.children {
1535 let text_len = inline_node.text.len();
1536 text.push_str(&inline_node.text);
1537
1538 if let Some(image) = &inline_node.image {
1539 if !text.is_empty() {
1540 if let Ok(mut state) = inline_node.state.lock() {
1541 state.set_text(text.clone().into());
1542 }
1543 items.push(InlineFlowItem::Text {
1544 state: inline_node.state.clone(),
1545 text: text.clone().into(),
1546 links: links.clone(),
1547 highlights: highlights.clone(),
1548 });
1549 }
1550
1551 items.push(InlineFlowItem::Image {
1552 url: image.url.clone(),
1553 link: image.link.clone(),
1554 title: image.title(),
1555 width: image.width,
1556 height: image.height,
1557 });
1558
1559 text.clear();
1560 links.clear();
1561 highlights.clear();
1562 offset = 0;
1563 } else {
1564 let mut node_highlights = vec![];
1565 for (range, style) in &inline_node.marks {
1566 let inner_range = (offset + range.start)..(offset + range.end);
1567
1568 let mut highlight = HighlightStyle::default();
1569 if style.bold {
1570 highlight.font_weight = Some(FontWeight::BOLD);
1571 }
1572 if style.italic {
1573 highlight.font_style = Some(FontStyle::Italic);
1574 }
1575 if style.strikethrough {
1576 highlight.strikethrough = Some(gpui::StrikethroughStyle {
1577 thickness: gpui::px(1.),
1578 ..Default::default()
1579 });
1580 }
1581 if style.underline {
1582 highlight.underline = Some(gpui::UnderlineStyle {
1583 thickness: gpui::px(1.),
1584 ..Default::default()
1585 });
1586 }
1587 if style.code {
1588 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
1589 }
1590 if let Some(color) = style.highlight {
1591 highlight.background_color = Some(color);
1592 }
1593
1594 if let Some(mut link_mark) = style.link.clone() {
1595 highlight.color = Some(node_cx.style.link());
1596 highlight.underline = Some(gpui::UnderlineStyle {
1597 thickness: gpui::px(1.),
1598 ..Default::default()
1599 });
1600
1601 if let Some(identifier) = link_mark.identifier.as_ref()
1602 && let Some(mark) = node_cx.link_refs.get(identifier)
1603 {
1604 link_mark = mark.clone();
1605 }
1606
1607 links.push((inner_range.clone(), link_mark));
1608 }
1609
1610 node_highlights.push((inner_range, highlight));
1611 }
1612
1613 highlights = gpui::combine_highlights(highlights, node_highlights).collect();
1614 offset += text_len;
1615 }
1616 }
1617
1618 if !text.is_empty() {
1619 if let Ok(mut state) = self.state.lock() {
1620 state.set_text(text.clone().into());
1621 }
1622 items.push(InlineFlowItem::Text {
1623 state: self.state.clone(),
1624 text: text.into(),
1625 links,
1626 highlights,
1627 });
1628 }
1629
1630 items
1631 }
1632}
1633
1634impl Paragraph {
1635 fn to_markdown(&self) -> String {
1636 let mut text = self
1637 .children
1638 .iter()
1639 .map(|text_node| {
1640 let mut text = text_node.text.to_string();
1641 for (range, style) in &text_node.marks {
1642 if style.bold {
1643 text = format!("**{}**", &text_node.text[range.clone()]);
1644 }
1645 if style.italic {
1646 text = format!("*{}*", &text_node.text[range.clone()]);
1647 }
1648 if style.strikethrough {
1649 text = format!("~~{}~~", &text_node.text[range.clone()]);
1650 }
1651 if style.code {
1652 text = format!("`{}`", &text_node.text[range.clone()]);
1653 }
1654 if style.highlight.is_some() {
1655 text = format!("=={}==", &text_node.text[range.clone()]);
1656 }
1657 if let Some(link) = &style.link {
1658 text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
1659 }
1660 }
1661
1662 if let Some(image) = &text_node.image {
1663 let alt = image.alt.clone().unwrap_or_default();
1664 let title = image
1665 .title
1666 .clone()
1667 .map_or(String::new(), |t| format!(" \"{}\"", t));
1668 text.push_str(&format!("", alt, image.url, title))
1669 }
1670
1671 text
1672 })
1673 .collect::<Vec<_>>()
1674 .join("");
1675
1676 text.push_str("\n\n");
1677 text
1678 }
1679}
1680
1681impl BlockNode {
1682 #[allow(dead_code)]
1686 pub(crate) fn to_markdown(&self) -> String {
1687 match self {
1688 BlockNode::Root { children, .. } => children
1689 .iter()
1690 .map(|child| child.to_markdown())
1691 .collect::<Vec<_>>()
1692 .join("\n\n"),
1693 BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
1694 BlockNode::Heading {
1695 level, children, ..
1696 } => {
1697 let hashes = "#".repeat(*level as usize);
1698 format!("{} {}", hashes, children.to_markdown())
1699 }
1700 BlockNode::Blockquote { children, .. } => {
1701 let content = children
1702 .iter()
1703 .map(|child| child.to_markdown())
1704 .collect::<Vec<_>>()
1705 .join("\n\n");
1706
1707 content
1708 .lines()
1709 .map(|line| format!("> {}", line))
1710 .collect::<Vec<_>>()
1711 .join("\n")
1712 }
1713 BlockNode::List {
1714 children, ordered, ..
1715 } => children
1716 .iter()
1717 .enumerate()
1718 .map(|(i, child)| {
1719 let prefix = if *ordered {
1720 format!("{}. ", i + 1)
1721 } else {
1722 "- ".to_string()
1723 };
1724 format!("{}{}", prefix, child.to_markdown())
1725 })
1726 .collect::<Vec<_>>()
1727 .join("\n"),
1728 BlockNode::ListItem {
1729 children, checked, ..
1730 } => {
1731 let checkbox = if let Some(checked) = checked {
1732 if *checked { "[x] " } else { "[ ] " }
1733 } else {
1734 ""
1735 };
1736 format!(
1737 "{}{}",
1738 checkbox,
1739 children
1740 .iter()
1741 .map(|child| child.to_markdown())
1742 .collect::<Vec<_>>()
1743 .join("\n")
1744 )
1745 }
1746 BlockNode::CodeBlock(code_block) => {
1747 format!(
1748 "```{}\n{}\n```",
1749 code_block.lang.clone().unwrap_or_default(),
1750 code_block.code()
1751 )
1752 }
1753 BlockNode::Table(table) => table.to_markdown(),
1754 BlockNode::Break { html, .. } => {
1755 if *html {
1756 "<br>".to_string()
1757 } else {
1758 "\n".to_string()
1759 }
1760 }
1761 BlockNode::HorizontalRule { .. } => "---".to_string(),
1762 BlockNode::Custom(node) => node.to_markdown(),
1763 BlockNode::Definition {
1764 identifier,
1765 url,
1766 title,
1767 ..
1768 } => {
1769 if let Some(title) = title {
1770 format!("[{}]: {} \"{}\"", identifier, url, title)
1771 } else {
1772 format!("[{}]: {}", identifier, url)
1773 }
1774 }
1775 BlockNode::Unknown { .. } => "".to_string(),
1776 }
1777 .trim()
1778 .to_string()
1779 }
1780}
1781
1782impl BlockNode {
1783 fn render_list_item_row(
1784 content: AnyElement,
1785 ix: usize,
1786 options: NodeRenderOptions,
1787 checked: Option<bool>,
1788 style: &TextViewStyle,
1789 line_height: Pixels,
1790 ) -> Div {
1791 h_flex()
1792 .w_full()
1793 .flex_1()
1794 .min_w_0()
1795 .relative()
1796 .items_start()
1797 .content_start()
1798 .when(!options.todo && checked.is_none(), |this| {
1799 this.child(list_item_prefix(ix, options.ordered, options.depth))
1800 })
1801 .when_some(checked, |this, checked| {
1802 let check_svg = if style.is_dark() {
1804 CHECK_SVG_DARK
1805 } else {
1806 CHECK_SVG_LIGHT
1807 };
1808 this.child(
1809 div()
1810 .flex()
1811 .mr_1p5()
1812 .h(line_height)
1813 .flex_none()
1814 .items_center()
1815 .justify_center()
1816 .child(
1817 div()
1818 .flex()
1819 .size(rems(0.875))
1820 .items_center()
1821 .justify_center()
1822 .border_1()
1823 .border_color(style.foreground())
1824 .when(checked, |this| {
1825 this.bg(style.foreground()).child(
1826 img(Arc::new(Image::from_bytes(
1827 ImageFormat::Svg,
1828 check_svg.to_vec(),
1829 )))
1830 .size(rems(0.625)),
1831 )
1832 }),
1833 ),
1834 )
1835 })
1836 .child(div().flex_1().min_w_0().overflow_hidden().child(content))
1837 }
1838
1839 fn render_list_item(
1840 item: &BlockNode,
1841 ix: usize,
1842 options: NodeRenderOptions,
1843 node_cx: &NodeContext,
1844 window: &mut Window,
1845 cx: &mut App,
1846 ) -> AnyElement {
1847 match item {
1848 BlockNode::ListItem {
1849 children,
1850 spread,
1851 checked,
1852 ..
1853 } => v_flex()
1854 .id(("li", options.ix))
1855 .w_full()
1856 .min_w_0()
1857 .when(*spread, |this| this.child(div()))
1858 .children({
1859 let mut items: Vec<Div> = Vec::with_capacity(children.len());
1860
1861 for (child_ix, child) in children.iter().enumerate() {
1862 match child {
1863 BlockNode::Paragraph { .. } => {
1864 let last_not_list = child_ix > 0
1865 && !matches!(children[child_ix - 1], BlockNode::List { .. });
1866
1867 let text = child.render_block(
1868 NodeRenderOptions {
1869 depth: options.depth + 1,
1870 todo: checked.is_some(),
1871 is_last: true,
1872 ..options
1873 },
1874 node_cx,
1875 window,
1876 cx,
1877 );
1878
1879 if last_not_list {
1883 if let Some(preceding_row) = items.pop() {
1884 items.push(
1885 v_flex().child(preceding_row).child(
1886 div()
1887 .w_full()
1888 .pl(rems(1.))
1889 .overflow_hidden()
1890 .child(text),
1891 ),
1892 );
1893 continue;
1894 }
1895 }
1896
1897 items.push(Self::render_list_item_row(
1898 text,
1899 ix,
1900 options,
1901 *checked,
1902 &node_cx.style,
1903 window.line_height(),
1904 ));
1905 }
1906 BlockNode::List { .. } => {
1907 items.push(div().ml(rems(1.)).child(child.render_block(
1908 NodeRenderOptions {
1909 depth: options.depth + 1,
1910 todo: checked.is_some(),
1911 is_last: true,
1912 ..options
1913 },
1914 node_cx,
1915 window,
1916 cx,
1917 )));
1918 }
1919 BlockNode::Root { .. }
1920 | BlockNode::Heading { .. }
1921 | BlockNode::Blockquote { .. }
1922 | BlockNode::CodeBlock(_)
1923 | BlockNode::Custom(_)
1924 | BlockNode::Table(_)
1925 | BlockNode::HorizontalRule { .. } => {
1926 let block = child.render_block(
1927 NodeRenderOptions {
1928 depth: options.depth + 1,
1929 todo: checked.is_some(),
1930 is_last: true,
1931 ..options
1932 },
1933 node_cx,
1934 window,
1935 cx,
1936 );
1937
1938 if child_ix == 0 {
1939 items.push(Self::render_list_item_row(
1940 block,
1941 ix,
1942 options,
1943 *checked,
1944 &node_cx.style,
1945 window.line_height(),
1946 ));
1947 } else {
1948 items.push(
1952 div()
1953 .w_full()
1954 .min_w_0()
1955 .pl(rems(1.))
1956 .overflow_hidden()
1957 .child(block),
1958 );
1959 }
1960 }
1961 BlockNode::ListItem { .. }
1962 | BlockNode::Break { .. }
1963 | BlockNode::Definition { .. }
1964 | BlockNode::Unknown => {}
1965 }
1966 }
1967 items
1968 })
1969 .into_any_element(),
1970 _ => div().into_any_element(),
1971 }
1972 }
1973
1974 fn render_table(
1978 item: &BlockNode,
1979 options: &NodeRenderOptions,
1980 node_cx: &NodeContext,
1981 window: &mut Window,
1982 cx: &mut App,
1983 ) -> impl IntoElement {
1984 const DEFAULT_LENGTH: usize = 5;
1985
1986 let table = match item {
1987 BlockNode::Table(table) => table,
1988 _ => return div().into_any_element(),
1989 };
1990
1991 let mut col_lens: Vec<usize> = vec![];
1994 for row in table.children.iter() {
1995 for (ix, cell) in row.children.iter().enumerate() {
1996 if col_lens.len() <= ix {
1997 col_lens.push(DEFAULT_LENGTH);
1998 }
1999 col_lens[ix] = col_lens[ix].max(cell.children.text_len());
2000 }
2001 }
2002
2003 if matches!(node_cx.style.table().overflow.x, Some(Overflow::Scroll)) {
2005 Self::render_scroll_table(table, col_lens.len(), options, node_cx, window, cx)
2006 } else {
2007 Self::render_wrap_table(table, &col_lens, options, node_cx, window, cx)
2008 }
2009 }
2010
2011 fn render_scroll_table(
2030 table: &Table,
2031 col_count: usize,
2032 options: &NodeRenderOptions,
2033 node_cx: &NodeContext,
2034 window: &mut Window,
2035 cx: &mut App,
2036 ) -> AnyElement {
2037 const CELL_PAD_PX: f32 = 16.0; const CELL_MIN_PX: f32 = 48.0;
2039 const CELL_WRAP_MAX_LINES: f32 = 2.0;
2045 const CELL_WRAP_MIN_PX: f32 = 160.0;
2046 const CELL_WRAP_MAX_PX: f32 = 480.0;
2047 const CELL_BORDER_PX: f32 = 1.0; const TABLE_BORDER_PX: f32 = 2.0; let text_style = window.text_style();
2054 let font_size = text_style.font_size.to_pixels(window.rem_size());
2055 let mut col_w = vec![CELL_MIN_PX; col_count];
2056 for row in table.children.iter() {
2057 for (ix, cell) in row.children.iter().enumerate() {
2058 let Some(slot) = col_w.get_mut(ix) else {
2059 continue;
2060 };
2061 let mut w = 0.0_f32;
2062 for line in cell.children.text().split('\n') {
2063 let line = line.trim();
2064 if line.is_empty() {
2065 continue;
2066 }
2067 let run = text_style.to_run(line.len());
2068 let line_w = window
2069 .text_system()
2070 .layout_line(line, font_size, &[run], None)
2071 .width;
2072 w = w.max(f32::from(line_w));
2073 }
2074 let border = if ix + 1 < col_count {
2077 CELL_BORDER_PX
2078 } else {
2079 0.
2080 };
2081 *slot = slot.max(w + CELL_PAD_PX + border);
2082 }
2083 }
2084 let style = &node_cx.style;
2085 let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
2089 let col_min_w: Vec<f32> = if nowrap {
2090 col_w.clone()
2091 } else {
2092 col_w
2093 .iter()
2094 .map(|w| {
2095 (w / CELL_WRAP_MAX_LINES)
2096 .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
2097 .min(*w)
2098 })
2099 .collect()
2100 };
2101 let min_total_w: f32 = col_min_w.iter().sum::<f32>() + TABLE_BORDER_PX;
2102
2103 let table_scroll_key = if let Some(span) = table.span {
2104 SharedString::from(format!(
2105 "{}-table-scroll-{}:{}",
2106 window.current_view(),
2107 span.start,
2108 span.end
2109 ))
2110 } else {
2111 SharedString::from(format!(
2112 "{}-table-scroll-{}",
2113 window.current_view(),
2114 options.ix
2115 ))
2116 };
2117 let scroll_handle = window
2118 .use_keyed_state(table_scroll_key, cx, |_, _| ScrollHandle::default())
2119 .read(cx)
2120 .clone();
2121 let row_count = table.children.len();
2122 let mut rows = Vec::with_capacity(row_count);
2123 for (row_ix, row) in table.children.iter().enumerate() {
2124 let mut cells = Vec::with_capacity(row.children.len());
2125 for (ix, cell) in row.children.iter().enumerate() {
2126 let align = table.column_align(ix);
2127 let is_last_col = ix == row.children.len() - 1;
2128 let width = col_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
2129 let min_width = col_min_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
2130 cells.push(
2131 div()
2132 .id(("cell", ix))
2133 .flex_basis(px(width))
2140 .flex_grow(width)
2141 .flex_shrink(1.)
2142 .min_w(px(min_width))
2143 .overflow_hidden()
2144 .when(align == ColumnumnAlign::Center, |this| this.text_center())
2145 .when(align == ColumnumnAlign::Right, |this| this.text_right())
2146 .px_2()
2147 .py_1()
2148 .when(!is_last_col, |this| {
2149 this.border_r_1().border_color(style.border())
2150 })
2151 .refine_style(&style.table_cell())
2152 .child(cell.children.render(node_cx, window, cx)),
2153 );
2154 }
2155 rows.push(
2156 div()
2157 .id("row")
2158 .w_full()
2159 .when(row_ix < row_count - 1, |this| this.border_b_1())
2160 .border_color(style.border())
2161 .flex()
2162 .flex_row()
2163 .when(row_ix == 0, |this| {
2167 this.bg(style.code_background())
2168 .text_color(style.foreground())
2169 .refine_style(&style.table_head())
2170 })
2171 .children(cells),
2172 );
2173 }
2174
2175 div()
2176 .pb(rems(1.))
2177 .w_full()
2178 .child(
2179 horizontal_scroll_area(
2189 ("table", options.ix),
2190 &scroll_handle,
2191 &StyleRefinement::default()
2192 .bg(cx.theme().tokens.colors.surface)
2193 .border_1()
2194 .border_color(style.border())
2195 .refine_style(style.table()),
2196 div().min_w_full().w(px(min_total_w)).children(rows),
2202 ),
2203 )
2204 .children(node_cx.table_actions.clone().map(|f| {
2211 div().id(("table-actions", options.ix)).mt_1().child(f(
2212 &table.table_data(),
2213 window,
2214 cx,
2215 ))
2216 }))
2217 .into_any_element()
2218 }
2219
2220 fn render_wrap_table(
2223 table: &Table,
2224 col_lens: &[usize],
2225 options: &NodeRenderOptions,
2226 node_cx: &NodeContext,
2227 window: &mut Window,
2228 cx: &mut App,
2229 ) -> AnyElement {
2230 const MAX_LENGTH: usize = 150;
2231
2232 let style = &node_cx.style;
2233 let row_count = table.children.len();
2234 let mut rows = Vec::with_capacity(row_count);
2235 for (row_ix, row) in table.children.iter().enumerate() {
2236 let mut cells = Vec::with_capacity(row.children.len());
2237 for (ix, cell) in row.children.iter().enumerate() {
2238 let align = table.column_align(ix);
2239 let is_last_col = ix == row.children.len() - 1;
2240 let len = col_lens
2241 .get(ix)
2242 .copied()
2243 .unwrap_or(MAX_LENGTH)
2244 .min(MAX_LENGTH);
2245
2246 cells.push(
2247 div()
2248 .id(("cell", ix))
2249 .overflow_hidden()
2250 .when(align == ColumnumnAlign::Center, |this| this.text_center())
2251 .when(align == ColumnumnAlign::Right, |this| this.text_right())
2252 .min_w_16()
2253 .w(Length::Definite(relative(len as f32)))
2254 .px_2()
2255 .py_1()
2256 .when(!is_last_col, |this| {
2257 this.border_r_1().border_color(style.border())
2258 })
2259 .refine_style(&style.table_cell())
2260 .child(cell.children.render(node_cx, window, cx)),
2261 );
2262 }
2263
2264 rows.push(
2265 div()
2266 .id("row")
2267 .w_full()
2268 .when(row_ix < row_count - 1, |this| this.border_b_1())
2269 .border_color(style.border())
2270 .flex()
2271 .flex_row()
2272 .when(row_ix == 0, |this| {
2276 this.bg(style.code_background())
2277 .text_color(style.foreground())
2278 .refine_style(&style.table_head())
2279 })
2280 .children(cells),
2281 );
2282 }
2283
2284 div()
2285 .pb(rems(1.))
2286 .w_full()
2287 .child(
2288 div()
2289 .id(("table", options.ix))
2290 .w_full()
2291 .bg(cx.theme().tokens.colors.surface)
2292 .border_1()
2293 .border_color(style.border())
2294 .overflow_hidden()
2295 .children(rows)
2296 .refine_style(&style.table()),
2297 )
2298 .children(node_cx.table_actions.clone().map(|f| {
2305 div().id(("table-actions", options.ix)).mt_1().child(f(
2306 &table.table_data(),
2307 window,
2308 cx,
2309 ))
2310 }))
2311 .into_any_element()
2312 }
2313
2314 pub(crate) fn render_block(
2315 &self,
2316 options: NodeRenderOptions,
2317 node_cx: &NodeContext,
2318 window: &mut Window,
2319 cx: &mut App,
2320 ) -> AnyElement {
2321 let ix = options.ix;
2322 let mb = if options.in_list || options.is_last {
2323 rems(0.)
2324 } else {
2325 node_cx.style.paragraph_gap()
2326 };
2327
2328 match self {
2329 BlockNode::Root { children, .. } => div()
2330 .id(("div", ix))
2331 .children(children.into_iter().enumerate().map(move |(ix, node)| {
2332 node.render_block(NodeRenderOptions { ix, ..options }, node_cx, window, cx)
2333 }))
2334 .into_any_element(),
2335 BlockNode::Paragraph(paragraph) => div()
2336 .id(("p", ix))
2337 .pb(mb)
2338 .child(paragraph.render(node_cx, window, cx))
2339 .into_any_element(),
2340 BlockNode::Heading {
2341 level, children, ..
2342 } => {
2343 let (text_size, font_weight) = match level {
2344 1 => (rems(2.), FontWeight::BOLD),
2345 2 => (rems(1.5), FontWeight::SEMIBOLD),
2346 3 => (rems(1.25), FontWeight::SEMIBOLD),
2347 4 => (rems(1.125), FontWeight::SEMIBOLD),
2348 5 => (rems(1.), FontWeight::SEMIBOLD),
2349 6 => (rems(1.), FontWeight::MEDIUM),
2350 _ => (rems(1.), FontWeight::NORMAL),
2351 };
2352
2353 let mut text_size = text_size.to_pixels(node_cx.style.heading_base_font_size());
2354 if let Some(size) = node_cx.style.heading_font_size(*level) {
2355 text_size = size;
2356 }
2357
2358 div()
2359 .id(SharedString::from(format!("h{}-{}", level, ix)))
2360 .pb(rems(0.3))
2361 .whitespace_normal()
2362 .text_size(text_size)
2363 .font_weight(font_weight)
2364 .child(children.render(node_cx, window, cx))
2365 .into_any_element()
2366 }
2367 BlockNode::Blockquote { children, .. } => div()
2368 .w_full()
2369 .pb(mb)
2370 .child(
2371 div()
2372 .id(("blockquote", ix))
2373 .w_full()
2374 .text_color(node_cx.style.muted_foreground())
2375 .border_l_3()
2376 .border_color(node_cx.style.border())
2377 .px_4()
2378 .children({
2379 let children_len = children.len();
2380 children.into_iter().enumerate().map(move |(index, c)| {
2381 let is_last = index == children_len - 1;
2382 c.render_block(options.is_last(is_last), node_cx, window, cx)
2383 })
2384 }),
2385 )
2386 .into_any_element(),
2387 BlockNode::List {
2388 children, ordered, ..
2389 } => v_flex()
2390 .id((if *ordered { "ol" } else { "ul" }, ix))
2391 .w_full()
2392 .min_w_0()
2393 .pb(mb)
2394 .children({
2395 let mut items = Vec::with_capacity(children.len());
2396 let mut item_index = 0;
2397 for (ix, item) in children.into_iter().enumerate() {
2398 let is_item = item.is_list_item();
2399
2400 items.push(Self::render_list_item(
2401 item,
2402 item_index,
2403 NodeRenderOptions {
2404 ix,
2405 ordered: *ordered,
2406 ..options
2407 },
2408 node_cx,
2409 window,
2410 cx,
2411 ));
2412
2413 if is_item {
2414 item_index += 1;
2415 }
2416 }
2417 items
2418 })
2419 .into_any_element(),
2420 BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
2421 BlockNode::Custom(node) => {
2422 let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
2423 Some(rendered) => rendered,
2424 None => div().child(node.as_text().to_string()).into_any_element(),
2425 };
2426
2427 div().pb(mb).child(inner).into_any_element()
2428 }
2429 BlockNode::Table { .. } => {
2430 Self::render_table(self, &options, node_cx, window, cx).into_any_element()
2431 }
2432 BlockNode::HorizontalRule { .. } => div()
2433 .pb(mb)
2434 .child(
2435 div()
2436 .id("horizontal-rule")
2437 .bg(node_cx.style.border())
2438 .h(px(2.)),
2439 )
2440 .into_any_element(),
2441 BlockNode::Break { .. } => div().id("break").into_any_element(),
2442 BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
2443 _ => {
2444 if cfg!(debug_assertions) {
2445 tracing::warn!("unknown implementation: {:?}", self);
2446 }
2447
2448 div().into_any_element()
2449 }
2450 }
2451 }
2452}
2453
2454#[cfg(test)]
2455mod tests {
2456 use super::*;
2457
2458 #[test]
2459 fn code_block_highlights_are_cached_by_highlighter_identity() {
2460 use std::sync::atomic::{AtomicUsize, Ordering};
2461
2462 let calls = Arc::new(AtomicUsize::new(0));
2463 let calls_for_highlighter = calls.clone();
2464 let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
2465 calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
2466 Vec::new()
2467 });
2468 let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
2469
2470 block.highlighted_styles(&highlighter);
2471 block.highlighted_styles(&highlighter);
2472 assert_eq!(calls.load(Ordering::Relaxed), 1);
2473
2474 let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
2475 block.highlighted_styles(&replacement);
2476 assert!(Arc::ptr_eq(
2477 &block
2478 .highlight_cache
2479 .lock()
2480 .unwrap()
2481 .as_ref()
2482 .unwrap()
2483 .highlighter,
2484 &replacement
2485 ));
2486 }
2487
2488 #[test]
2489 fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
2490 let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
2494 vec![(
2495 0..2,
2496 HighlightStyle {
2497 color: Some(gpui::rgb(0x0000ff).into()),
2498 ..Default::default()
2499 },
2500 )]
2501 });
2502 let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
2503 vec![(
2504 0..2,
2505 HighlightStyle {
2506 color: Some(gpui::rgb(0xffff00).into()),
2507 ..Default::default()
2508 },
2509 )]
2510 });
2511 let block = CodeBlock::from_code("42", Some("json"));
2512
2513 let light_styles = block.highlighted_styles(&light);
2514 let dark_styles = block.highlighted_styles(&dark);
2515
2516 assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
2517 assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
2518 assert_eq!(block.code(), "42", "the document must survive the swap");
2519 }
2520
2521 #[test]
2522 fn reconstruct_markdown_wraps_marked_runs() {
2523 let marks = vec![(0..4, TextMark::default().bold())];
2525 assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
2526 assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
2528 }
2529
2530 #[test]
2531 fn reconstruct_markdown_emits_unmarked_text_verbatim() {
2532 let text = "a b c";
2534 let marks = vec![(2..3, TextMark::default().code())];
2535 assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
2536 assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
2538 }
2539
2540 #[test]
2541 fn reconstruct_markdown_handles_code_italic_strike_link() {
2542 assert_eq!(
2543 reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
2544 "`x`"
2545 );
2546 assert_eq!(
2547 reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
2548 "*x*"
2549 );
2550 assert_eq!(
2551 reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
2552 "~~x~~"
2553 );
2554 let link = TextMark::default().link(LinkMark {
2555 url: "https://example.com".into(),
2556 ..Default::default()
2557 });
2558 assert_eq!(
2559 reconstruct_markdown("x", &[(0..1, link)], 0..1),
2560 "[x](https://example.com)"
2561 );
2562 }
2563
2564 #[test]
2565 fn reconstruct_markdown_nested_bold_italic() {
2566 let mark = TextMark::default().bold().italic();
2568 assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
2570 }
2571
2572 fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
2576 let combined: String = children.iter().map(|c| c.text.to_string()).collect();
2577 let paragraph = Paragraph {
2578 span: None,
2579 children,
2580 link_refs: HashMap::new(),
2581 state: Arc::new(Mutex::new(InlineState::default())),
2582 };
2583 if let Ok(mut state) = paragraph.state.lock() {
2584 state.set_text(combined.into());
2585 }
2586 paragraph
2587 }
2588
2589 fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
2590 if let Ok(mut state) = paragraph.state.lock() {
2591 state.selection = Some(range.into());
2592 }
2593 }
2594
2595 #[test]
2596 fn paragraph_selected_source_maps_partial_selection_across_runs() {
2597 let children = vec![
2599 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
2600 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
2601 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
2602 ];
2603 let paragraph = paragraph_with_children(children);
2604
2605 set_paragraph_selection(¶graph, 0..(9 + 4 + 6));
2607 assert_eq!(paragraph.selected_source(), "This has **bold** text.");
2608
2609 set_paragraph_selection(¶graph, 5..16);
2612 assert_eq!(paragraph.selected_source(), "has **bold** te");
2613
2614 set_paragraph_selection(¶graph, 10..12);
2616 assert_eq!(paragraph.selected_source(), "**ol**");
2617 }
2618
2619 #[test]
2620 fn paragraph_selected_source_matches_text_when_no_marks() {
2621 let children =
2622 vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
2623 let paragraph = paragraph_with_children(children);
2624 set_paragraph_selection(¶graph, 0..11);
2625 assert_eq!(paragraph.selected_source(), "plain words");
2626 assert_eq!(paragraph.selected_text(), "plain words");
2627 }
2628
2629 fn selected_paragraph(text: &str) -> Paragraph {
2630 let len = text.len();
2631 let paragraph = paragraph_with_children(vec![
2632 InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
2633 ]);
2634 set_paragraph_selection(¶graph, 0..len);
2635 paragraph
2636 }
2637
2638 #[test]
2639 fn heading_selected_source_prefixes_hashes() {
2640 let heading = BlockNode::Heading {
2641 level: 2,
2642 children: selected_paragraph("Title"),
2643 span: None,
2644 };
2645 assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
2646 assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
2648 }
2649
2650 #[test]
2651 fn unordered_list_selected_source_prefixes_dash() {
2652 let list = BlockNode::List {
2653 ordered: false,
2654 span: None,
2655 children: vec![
2656 BlockNode::ListItem {
2657 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
2658 spread: false,
2659 checked: None,
2660 span: None,
2661 },
2662 BlockNode::ListItem {
2663 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
2664 spread: false,
2665 checked: None,
2666 span: None,
2667 },
2668 ],
2669 };
2670 assert_eq!(
2671 list.selected_text(SelectionFormat::Source),
2672 "- one\n- two\n"
2673 );
2674 }
2675
2676 #[test]
2677 fn ordered_list_selected_source_prefixes_numbers() {
2678 let list = BlockNode::List {
2679 ordered: true,
2680 span: None,
2681 children: vec![
2682 BlockNode::ListItem {
2683 children: vec![BlockNode::Paragraph(selected_paragraph("first"))],
2684 spread: false,
2685 checked: None,
2686 span: None,
2687 },
2688 BlockNode::ListItem {
2689 children: vec![BlockNode::Paragraph(selected_paragraph("second"))],
2690 spread: false,
2691 checked: None,
2692 span: None,
2693 },
2694 ],
2695 };
2696 assert_eq!(
2697 list.selected_text(SelectionFormat::Source),
2698 "1. first\n2. second\n"
2699 );
2700 }
2701
2702 #[test]
2703 fn nested_list_selected_source_indents_sublists() {
2704 let nested = BlockNode::List {
2708 ordered: false,
2709 span: None,
2710 children: vec![BlockNode::ListItem {
2711 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
2712 spread: false,
2713 checked: None,
2714 span: None,
2715 }],
2716 };
2717 let list = BlockNode::List {
2718 ordered: false,
2719 span: None,
2720 children: vec![
2721 BlockNode::ListItem {
2722 children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
2723 spread: false,
2724 checked: None,
2725 span: None,
2726 },
2727 BlockNode::ListItem {
2728 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
2729 spread: false,
2730 checked: None,
2731 span: None,
2732 },
2733 ],
2734 };
2735 assert_eq!(
2736 list.selected_text(SelectionFormat::Source),
2737 "- one\n - nested\n- two\n"
2738 );
2739 }
2740
2741 #[test]
2742 fn task_list_selected_source_restores_checkboxes() {
2743 let list = BlockNode::List {
2744 ordered: false,
2745 span: None,
2746 children: vec![
2747 BlockNode::ListItem {
2748 children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
2749 spread: false,
2750 checked: Some(true),
2751 span: None,
2752 },
2753 BlockNode::ListItem {
2754 children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
2755 spread: false,
2756 checked: Some(false),
2757 span: None,
2758 },
2759 ],
2760 };
2761 assert_eq!(
2762 list.selected_text(SelectionFormat::Source),
2763 "- [x] done\n- [ ] todo\n"
2764 );
2765 }
2766
2767 #[test]
2768 fn blockquote_selected_source_prefixes_gt() {
2769 let quote = BlockNode::Blockquote {
2770 span: None,
2771 children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
2772 };
2773 assert_eq!(
2774 quote.selected_text(SelectionFormat::Source),
2775 "> quoted text\n"
2776 );
2777 }
2778
2779 #[test]
2780 fn table_selected_source_pipes_cells_with_alignment_row() {
2781 let cell = |text: &str| TableCell {
2782 children: selected_paragraph(text),
2783 width: None,
2784 };
2785 let table = Table {
2786 children: vec![
2787 TableRow {
2788 children: vec![cell("Name"), cell("Age")],
2789 },
2790 TableRow {
2791 children: vec![cell("Alice"), cell("30")],
2792 },
2793 ],
2794 column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
2795 span: None,
2796 };
2797 let block = BlockNode::Table(table);
2798 assert_eq!(
2799 block.selected_text(SelectionFormat::Source),
2800 "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
2801 );
2802 }
2803
2804 fn plain_cell(text: &str) -> TableCell {
2807 TableCell {
2808 children: Paragraph::new(text.to_string()),
2809 width: None,
2810 }
2811 }
2812
2813 fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
2814 Table {
2815 children: rows
2816 .into_iter()
2817 .map(|children| TableRow { children })
2818 .collect(),
2819 column_aligns,
2820 span: None,
2821 }
2822 }
2823
2824 #[test]
2825 fn table_to_markdown_pipes_cells_with_alignment_row() {
2826 let table = table_of(
2827 vec![
2828 vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
2829 vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
2830 ],
2831 vec![
2832 ColumnumnAlign::Left,
2833 ColumnumnAlign::Center,
2834 ColumnumnAlign::Right,
2835 ],
2836 );
2837
2838 assert_eq!(
2839 table.to_markdown(),
2840 "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
2841 );
2842 assert_eq!(
2844 BlockNode::Table(table.clone()).to_markdown(),
2845 table.to_markdown()
2846 );
2847 }
2848
2849 #[test]
2850 fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
2851 let table = table_of(
2852 vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
2853 vec![ColumnumnAlign::Left],
2854 );
2855
2856 assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
2857 }
2858
2859 #[test]
2860 fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
2861 let bold = TableCell {
2862 children: paragraph_with_children(vec![
2863 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
2864 ]),
2865 width: None,
2866 };
2867 let table = table_of(
2868 vec![
2869 vec![plain_cell("a | b"), plain_cell("plain")],
2870 vec![plain_cell("c"), bold],
2871 ],
2872 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
2873 );
2874
2875 assert_eq!(
2876 table.to_markdown(),
2877 "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
2878 );
2879 }
2880
2881 #[test]
2882 fn table_data_snapshots_plain_cells_and_markdown() {
2883 let mut table = table_of(
2884 vec![
2885 vec![plain_cell(" Name "), plain_cell("Age")],
2886 vec![plain_cell("Alice"), plain_cell("30")],
2887 ],
2888 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
2889 );
2890 table.span = Some(Span { start: 4, end: 42 });
2891
2892 let data = table.table_data();
2893 assert_eq!(data.headers, vec!["Name", "Age"]);
2894 assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
2895 assert_eq!(data.markdown, table.to_markdown());
2896 assert_eq!(data.span, Some(4..42));
2897 }
2898
2899 #[test]
2900 fn table_data_handles_tables_without_rows() {
2901 let header_only = table_of(
2903 vec![vec![plain_cell("Name"), plain_cell("Age")]],
2904 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
2905 );
2906 let data = header_only.table_data();
2907 assert_eq!(data.headers, vec!["Name", "Age"]);
2908 assert!(data.rows.is_empty());
2909 assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
2910
2911 assert_eq!(Table::default().table_data(), TableData::default());
2913 }
2914
2915 fn image_paragraph(alt: &str, url: &str) -> Paragraph {
2916 let image = ImageNode {
2917 url: url.into(),
2918 alt: Some(alt.into()),
2919 ..Default::default()
2920 };
2921 Paragraph {
2922 span: None,
2923 children: vec![InlineNode::image(image)],
2924 link_refs: HashMap::new(),
2925 state: Arc::new(Mutex::new(InlineState::default())),
2926 }
2927 }
2928
2929 #[test]
2932 fn marks_round_trip_through_reconstruction() {
2933 let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
2934
2935 assert_eq!(wrap(TextMark::default().bold()), "**x**");
2936 assert_eq!(wrap(TextMark::default().italic()), "*x*");
2937 assert_eq!(wrap(TextMark::default().code()), "`x`");
2938 assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
2939 assert_eq!(
2940 wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
2941 "==x=="
2942 );
2943 assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
2945
2946 assert_eq!(
2948 wrap(TextMark::default().link(LinkMark {
2949 url: "https://example.com".into(),
2950 title: Some("Tip".into()),
2951 ..Default::default()
2952 })),
2953 "[x](https://example.com \"Tip\")"
2954 );
2955 }
2956
2957 #[test]
2960 fn document_selected_source_slices_covered_blocks_from_the_source() {
2961 use crate::text::document::ParsedDocument;
2962
2963 let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
2966 let list = "3. _one_\n4. two";
2967 let list_start = source.find(list).unwrap();
2968 let rule_start = source.find("---").unwrap();
2969
2970 let document = ParsedDocument {
2971 source: source.into(),
2972 blocks: vec![
2973 BlockNode::Paragraph(selected_paragraph("start")),
2974 BlockNode::List {
2975 ordered: true,
2976 children: vec![],
2977 span: Some(Span {
2978 start: list_start,
2979 end: list_start + list.len(),
2980 }),
2981 },
2982 BlockNode::HorizontalRule {
2983 span: Some(Span {
2984 start: rule_start,
2985 end: rule_start + 3,
2986 }),
2987 },
2988 BlockNode::Paragraph(selected_paragraph("end")),
2989 ]
2990 .into(),
2991 };
2992
2993 assert_eq!(
2994 document.selected_text(SelectionFormat::Source, None),
2995 "start\n\n3. _one_\n4. two\n\n---\n\nend"
2996 );
2997 }
2998
2999 #[test]
3000 fn document_selected_source_includes_enclosed_image() {
3001 use crate::text::document::ParsedDocument;
3002
3003 let source = "before\n\n\n\nafter";
3007 let image_markdown = "";
3008 let start = source.find(image_markdown).unwrap();
3009 let mut image = image_paragraph("alt", "https://example.com/i.png");
3010 image.span = Some(Span {
3011 start,
3012 end: start + image_markdown.len(),
3013 });
3014
3015 let document = ParsedDocument {
3016 source: source.into(),
3017 blocks: vec![
3018 BlockNode::Paragraph(selected_paragraph("before")),
3019 BlockNode::Paragraph(image),
3020 BlockNode::Paragraph(selected_paragraph("after")),
3021 ]
3022 .into(),
3023 };
3024 assert_eq!(
3025 document.selected_text(SelectionFormat::Source, None),
3026 "before\n\n\n\nafter"
3027 );
3028 }
3029
3030 #[test]
3031 fn document_selected_source_drops_unenclosed_image() {
3032 use crate::text::document::ParsedDocument;
3033
3034 let document = ParsedDocument {
3037 source: String::new().into(),
3038 blocks: vec![
3039 BlockNode::Paragraph(selected_paragraph("before")),
3040 BlockNode::Paragraph(image_paragraph("alt", "u")),
3041 ]
3042 .into(),
3043 };
3044 assert_eq!(
3045 document.selected_text(SelectionFormat::Source, None),
3046 "before"
3047 );
3048 }
3049
3050 fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
3051 let block = CodeBlock::new(
3052 code.to_string().into(),
3053 lang.map(|l| l.to_string().into()),
3054 None::<Span>,
3055 );
3056 if let Ok(mut state) = block.state.lock() {
3057 let len = state.text.len();
3058 state.selection = Some((0..len).into());
3059 }
3060 BlockNode::CodeBlock(block)
3061 }
3062
3063 #[test]
3064 fn code_block_selected_source_wraps_in_fence_with_lang() {
3065 let block = selected_code_block("let x = 1;\n", Some("rust"));
3066 let code = block.selected_text(SelectionFormat::Plain);
3067 let code_trimmed = code.trim_end_matches('\n');
3068 assert_eq!(
3072 block.selected_text(SelectionFormat::Source),
3073 format!("```rust\n{}\n```\n", code_trimmed)
3074 );
3075 assert!(
3076 block
3077 .selected_text(SelectionFormat::Source)
3078 .starts_with("```rust\n")
3079 );
3080 assert!(
3081 block
3082 .selected_text(SelectionFormat::Source)
3083 .trim_end()
3084 .ends_with("\n```")
3085 );
3086 }
3087
3088 #[test]
3089 fn code_block_selected_source_without_lang() {
3090 let block = selected_code_block("plain\n", None);
3091 let code_trimmed = block.selected_text(SelectionFormat::Plain);
3092 let code_trimmed = code_trimmed.trim_end_matches('\n');
3093 assert_eq!(
3094 block.selected_text(SelectionFormat::Source),
3095 format!("```\n{}\n```\n", code_trimmed)
3096 );
3097 }
3098
3099 #[test]
3100 fn document_selected_source_joins_blocks_with_blank_line() {
3101 use crate::text::document::ParsedDocument;
3102
3103 let document = ParsedDocument {
3107 source: String::new().into(),
3108 blocks: vec![
3109 BlockNode::Heading {
3110 level: 1,
3111 children: selected_paragraph("Title"),
3112 span: None,
3113 },
3114 BlockNode::Paragraph(selected_paragraph("A paragraph.")),
3115 selected_code_block("let x = 1;\n", Some("rust")),
3116 BlockNode::List {
3117 ordered: true,
3118 span: None,
3119 children: vec![
3120 BlockNode::ListItem {
3121 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
3122 spread: false,
3123 checked: None,
3124 span: None,
3125 },
3126 BlockNode::ListItem {
3127 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
3128 spread: false,
3129 checked: None,
3130 span: None,
3131 },
3132 ],
3133 },
3134 ]
3135 .into(),
3136 };
3137
3138 assert_eq!(
3139 document.selected_text(SelectionFormat::Source, None),
3140 "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
3141 );
3142 }
3143
3144 #[test]
3145 fn code_block_equality_includes_code_content() {
3146 let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
3147 let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
3148
3149 assert_ne!(first, second);
3150 }
3151}