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