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