1use std::{
2 borrow::Cow,
3 collections::HashMap,
4 ops::Range,
5 sync::{Arc, Mutex, OnceLock, Weak},
6};
7
8use gpui::{
9 AnyElement, App, Axis, Bounds, DefiniteLength, Div, Element, ElementId, FontStyle, FontWeight,
10 GlobalElementId, HighlightStyle, Hsla, Image, ImageFormat, ImageSource, InspectorElementId,
11 InteractiveElement as _, IntoElement, IsZero as _, LayoutId, ObjectFit, Overflow,
12 ParentElement, Pixels, Point, Refineable as _, Rems, ScrollHandle, SharedString, SharedUri,
13 StatefulInteractiveElement, StyleRefinement, Styled, StyledImage as _, TextStyleRefinement,
14 WhiteSpace, Window, div, img, prelude::FluentBuilder as _, px, relative, rems,
15};
16use markdown::mdast;
17
18use crate::{
19 GlobalState, ScrollableMask, Scrollbar, StyledExt, h_flex,
20 scrollable_mask::horizontal_scroll_area,
21 text::{
22 CodeBlockActionsFn, CodeBlockHighlighterFn, LinkClickHandlerFn, MarkdownExtensions,
23 MarkdownNode, TableActionsFn,
24 document::{NodeRenderOptions, PrevBlock, flow_position},
25 inline::{
26 Inline, InlineHighlight, InlineState, combine_highlights, fade_highlights,
27 point_in_text_selection, text_runs, text_size_ranges,
28 },
29 inline_flow::{InlineFlow, InlineFlowItem, slice_ranges},
30 range_highlight::{RangeHighlightFrame, RevealAt, RevealRequest},
31 stream_fade::{StreamFadeFrame, TextLeafKey},
32 text_view::handle_link_click,
33 },
34 theme::ActiveTheme as _,
35};
36
37use super::{
38 SelectionFormat, TextViewStyle,
39 utils::{data_url_image, list_item_prefix, ordered_list_ordinal},
40};
41
42const 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>"#;
43const 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>"#;
44
45#[derive(Debug, Clone, PartialEq)]
47pub(crate) enum BlockNode {
48 Root {
50 children: Vec<BlockNode>,
51 span: Option<Span>,
52 },
53 Paragraph(Paragraph),
54 Heading {
55 level: u8,
56 children: Paragraph,
57 span: Option<Span>,
58 },
59 Blockquote {
60 children: Vec<BlockNode>,
61 span: Option<Span>,
62 },
63 List {
64 children: Vec<BlockNode>,
66 ordered: bool,
67 start: Option<u32>,
69 span: Option<Span>,
70 },
71 ListItem {
72 children: Vec<BlockNode>,
73 spread: bool,
74 checked: Option<bool>,
76 span: Option<Span>,
77 },
78 CodeBlock(CodeBlock),
79 Custom {
81 node: MarkdownNode,
82 state: BlockState,
83 },
84 Table(Table),
85 Break {
86 html: bool,
87 span: Option<Span>,
88 },
89 HorizontalRule {
90 span: Option<Span>,
91 },
92 Definition {
94 identifier: SharedString,
95 url: SharedString,
96 title: Option<SharedString>,
97 span: Option<Span>,
98 },
99 Unknown,
100}
101
102#[derive(Debug, Clone, Default)]
104pub(crate) struct BlockState {
105 selection: Arc<Mutex<Option<BlockSelection>>>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110enum BlockSelection {
111 Empty,
112 Full,
113}
114
115impl BlockState {
116 fn selection(&self) -> Option<BlockSelection> {
117 self.selection.lock().ok().and_then(|selection| *selection)
118 }
119
120 fn set_selection(&self, value: Option<BlockSelection>) {
121 if let Ok(mut selection) = self.selection.lock() {
122 *selection = value;
123 }
124 }
125
126 fn is_selected(&self) -> bool {
127 self.selection() == Some(BlockSelection::Full)
128 }
129
130 fn has_selection_observation(&self) -> bool {
131 self.selection().is_some()
132 }
133
134 fn clear_selection(&self) {
135 self.set_selection(None);
136 }
137}
138
139impl PartialEq for BlockState {
140 fn eq(&self, _: &Self) -> bool {
141 true
143 }
144}
145
146struct CustomBlockElement {
148 content: AnyElement,
149 state: BlockState,
150}
151
152impl IntoElement for CustomBlockElement {
153 type Element = Self;
154
155 fn into_element(self) -> Self {
156 self
157 }
158}
159
160impl Element for CustomBlockElement {
161 type RequestLayoutState = ();
162 type PrepaintState = ();
163
164 fn id(&self) -> Option<ElementId> {
165 None
166 }
167
168 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
169 None
170 }
171
172 fn request_layout(
173 &mut self,
174 _: Option<&GlobalElementId>,
175 _: Option<&InspectorElementId>,
176 window: &mut Window,
177 cx: &mut App,
178 ) -> (LayoutId, ()) {
179 (self.content.request_layout(window, cx), ())
180 }
181
182 fn prepaint(
183 &mut self,
184 _: Option<&GlobalElementId>,
185 _: Option<&InspectorElementId>,
186 _: Bounds<Pixels>,
187 _: &mut (),
188 window: &mut Window,
189 cx: &mut App,
190 ) {
191 self.content.prepaint(window, cx);
192 }
193
194 fn paint(
195 &mut self,
196 _: Option<&GlobalElementId>,
197 _: Option<&InspectorElementId>,
198 bounds: Bounds<Pixels>,
199 _: &mut (),
200 _: &mut (),
201 window: &mut Window,
202 cx: &mut App,
203 ) {
204 let view = GlobalState::global(cx).text_view_state();
205 let selection = view.as_ref().and_then(|view| {
206 let state = view.read(cx);
207 if !state.is_selectable() {
208 return None;
209 }
210 if state.is_all_selected() {
211 return Some(BlockSelection::Full);
212 }
213 if state.preserve_inline_selection {
214 return self.state.selection();
215 }
216 let (start, end) = state.selection_points(cx)?;
217 if bounds.size.width <= Pixels::ZERO
220 || bounds.size.height <= Pixels::ZERO
221 || bounds.bottom() <= start.y.min(end.y)
222 || bounds.top() > start.y.max(end.y)
223 {
224 return None;
225 }
226 Some(if custom_block_is_selected(bounds, start, end) {
227 BlockSelection::Full
228 } else {
229 BlockSelection::Empty
230 })
231 });
232 self.state.set_selection(selection);
233 if let Some(view) = &view {
234 let visible = bounds.intersect(&window.content_mask().bounds);
235 if view.read(cx).is_selectable()
236 && visible.size.width > Pixels::ZERO
237 && visible.size.height > Pixels::ZERO
238 {
239 view.read(cx)
240 .selection_adapter
241 .register_inline(vec![visible]);
242 }
243 }
244 self.content.paint(window, cx);
245 if selection == Some(BlockSelection::Full) {
246 let color = view.as_ref().unwrap().read(cx).text_view_style.selection();
247 window.paint_quad(gpui::fill(bounds, color));
248 }
249 }
250}
251
252fn custom_block_is_selected(
258 bounds: Bounds<Pixels>,
259 start: Point<Pixels>,
260 end: Point<Pixels>,
261) -> bool {
262 let (start_inside, end_inside) = (bounds.contains(&start), bounds.contains(&end));
263 start != end
264 && !(start_inside && end_inside)
265 && (start_inside
266 || end_inside
267 || point_in_text_selection(
268 bounds.origin,
269 bounds.size.width,
270 start,
271 end,
272 bounds.size.height,
273 ))
274}
275
276#[derive(Clone, Copy)]
277enum BlockTextKind {
278 All,
279 Selected,
280 SelectedSource,
283}
284
285impl BlockNode {
286 pub(super) fn is_list_item(&self) -> bool {
287 matches!(self, Self::ListItem { .. })
288 }
289
290 pub(super) fn compact(self) -> BlockNode {
292 match self {
293 Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
294 _ => self,
295 }
296 }
297
298 pub(crate) fn span(&self) -> Option<Span> {
300 match self {
301 BlockNode::Root { span, .. } => *span,
302 BlockNode::Paragraph(paragraph) => paragraph.span,
303 BlockNode::Heading { span, .. } => *span,
304 BlockNode::Blockquote { span, .. } => *span,
305 BlockNode::List { span, .. } => *span,
306 BlockNode::ListItem { span, .. } => *span,
307 BlockNode::CodeBlock(code_block) => code_block.span,
308 BlockNode::Custom { node, .. } => node.span,
309 BlockNode::Table(table) => table.span,
310 BlockNode::Break { span, .. } => *span,
311 BlockNode::HorizontalRule { span, .. } => *span,
312 BlockNode::Definition { span, .. } => *span,
313 BlockNode::Unknown { .. } => None,
314 }
315 }
316
317 pub(super) fn text(&self) -> String {
318 self.text_by_kind(BlockTextKind::All)
319 }
320
321 pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
326 self.text_by_kind(match format {
327 SelectionFormat::Plain => BlockTextKind::Selected,
328 SelectionFormat::Source => BlockTextKind::SelectedSource,
329 })
330 }
331
332 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
333 let mut selected = SourceRangeSelection::Unselected;
334 match self {
335 BlockNode::Root { children, .. }
336 | BlockNode::Blockquote { children, .. }
337 | BlockNode::List { children, .. }
338 | BlockNode::ListItem { children, .. } => {
339 for child in children {
340 selected.merge(child.selected_source_range());
341 }
342 }
343 BlockNode::Paragraph(paragraph) => selected = paragraph.selected_source_range(),
344 BlockNode::Heading { children, .. } => selected = children.selected_source_range(),
345 BlockNode::Table(table) => {
346 for row in &table.children {
347 for cell in &row.children {
348 selected.merge(cell.children.selected_source_range());
349 }
350 }
351 }
352 BlockNode::CodeBlock(code_block) => selected = code_block.selected_source_range(),
353 BlockNode::Custom { node, state } => {
354 if state.is_selected() {
355 selected = node
356 .source_range()
357 .map_or(SourceRangeSelection::Unmapped, SourceRangeSelection::Mapped);
358 }
359 }
360 BlockNode::Definition { .. }
361 | BlockNode::Break { .. }
362 | BlockNode::HorizontalRule { .. }
363 | BlockNode::Unknown => {}
364 }
365 selected
366 }
367
368 fn text_by_kind(&self, kind: BlockTextKind) -> String {
369 let mut text = String::new();
370 match self {
371 BlockNode::Root { children, .. } => {
372 let block_text = Self::children_text(children, kind);
373 if !block_text.is_empty() {
374 text.push_str(&block_text);
375 text.push('\n');
376 }
377 }
378 BlockNode::Paragraph(paragraph) => {
379 let block_text = match kind {
380 BlockTextKind::All => paragraph.text(),
381 BlockTextKind::Selected => paragraph.selected_text(),
382 BlockTextKind::SelectedSource => paragraph.selected_source(),
383 };
384 if !block_text.is_empty() {
385 text.push_str(&block_text);
386 text.push('\n');
387 }
388 }
389 BlockNode::Heading {
390 level, children, ..
391 } => {
392 let block_text = match kind {
393 BlockTextKind::All => children.text(),
394 BlockTextKind::Selected => children.selected_text(),
395 BlockTextKind::SelectedSource => children.selected_source(),
396 };
397 if !block_text.is_empty() {
398 if matches!(kind, BlockTextKind::SelectedSource) {
401 text.push_str(&"#".repeat(*level as usize));
402 text.push(' ');
403 }
404 text.push_str(&block_text);
405 text.push('\n');
406 }
407 }
408 BlockNode::List {
409 children,
410 ordered,
411 start,
412 ..
413 } => {
414 if matches!(kind, BlockTextKind::SelectedSource) {
415 text.push_str(&list_selected_source(children, *ordered, *start, ""));
418 } else {
419 text.push_str(&Self::children_text(children, kind));
420 }
421 }
422 BlockNode::ListItem { children, .. } => {
423 text.push_str(&Self::children_text(children, kind));
424 }
425 BlockNode::Blockquote { children, .. } => {
426 let block_text = Self::children_text(children, kind);
427
428 if !block_text.is_empty() {
429 if matches!(kind, BlockTextKind::SelectedSource) {
430 let quoted = block_text
433 .trim_end_matches('\n')
434 .lines()
435 .map(|line| {
436 if line.is_empty() {
437 ">".to_string()
438 } else {
439 format!("> {}", line)
440 }
441 })
442 .collect::<Vec<_>>()
443 .join("\n");
444 text.push_str("ed);
445 } else {
446 text.push_str(&block_text);
447 }
448 text.push('\n');
449 }
450 }
451 BlockNode::Table(table) => {
452 if matches!(kind, BlockTextKind::SelectedSource) {
453 let block_text = table_selected_source(table);
454 if !block_text.is_empty() {
455 text.push_str(&block_text);
456 text.push('\n');
457 }
458 } else {
459 let mut block_text = String::new();
460 for row in table.children.iter() {
461 let mut row_texts = vec![];
462 for cell in row.children.iter() {
463 row_texts.push(match kind {
464 BlockTextKind::All => cell.children.text(),
465 _ => cell.children.selected_text(),
467 });
468 }
469 if !row_texts.is_empty() {
470 block_text.push_str(&row_texts.join(" "));
471 block_text.push('\n');
472 }
473 }
474
475 if !block_text.is_empty() {
476 text.push_str(&block_text);
477 text.push('\n');
478 }
479 }
480 }
481 BlockNode::CodeBlock(code_block) => {
482 let block_text = match kind {
483 BlockTextKind::All => code_block.text(),
484 BlockTextKind::Selected => code_block.selected_text(),
485 BlockTextKind::SelectedSource => code_block.selected_source(),
486 };
487 if !block_text.is_empty() {
488 text.push_str(&block_text);
489 text.push('\n');
490 }
491 }
492 BlockNode::Custom { node, state } => {
493 if matches!(kind, BlockTextKind::All) || state.is_selected() {
494 let content = match kind {
495 BlockTextKind::SelectedSource => Cow::Owned(node.to_markdown()),
496 _ => Cow::Borrowed(node.as_text()),
497 };
498 if !content.is_empty() {
499 text.push_str(&content);
500 text.push('\n');
501 }
502 }
503 }
504 BlockNode::Definition { .. }
505 | BlockNode::Break { .. }
506 | BlockNode::HorizontalRule { .. }
507 | BlockNode::Unknown { .. } => {}
508 }
509
510 text
511 }
512
513 fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
514 let mut text = String::new();
515 for child in children.iter() {
516 text.push_str(&child.text_by_kind(kind));
517 }
518
519 text
520 }
521
522 pub(super) fn has_selection(&self) -> bool {
533 match self {
534 BlockNode::Root { children, .. }
535 | BlockNode::Blockquote { children, .. }
536 | BlockNode::List { children, .. }
537 | BlockNode::ListItem { children, .. } => {
538 children.iter().any(|child| child.has_selection())
539 }
540 BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
541 BlockNode::Heading { children, .. } => children.has_selection(),
542 BlockNode::Table(table) => table.children.iter().any(|row| {
543 row.children
544 .iter()
545 .any(|cell| cell.children.has_selection())
546 }),
547 BlockNode::CodeBlock(code_block) => code_block.has_selection(),
548 BlockNode::Custom { state, .. } => {
549 state.has_selection_observation()
551 }
552 BlockNode::Definition { .. }
553 | BlockNode::Break { .. }
554 | BlockNode::HorizontalRule { .. }
555 | BlockNode::Unknown { .. } => false,
556 }
557 }
558
559 pub(super) fn clear_selection(&self) {
560 match self {
561 BlockNode::Root { children, .. }
562 | BlockNode::Blockquote { children, .. }
563 | BlockNode::List { children, .. }
564 | BlockNode::ListItem { children, .. } => {
565 for child in children.iter() {
566 child.clear_selection();
567 }
568 }
569 BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
570 BlockNode::Heading { children, .. } => children.clear_selection(),
571 BlockNode::Table(table) => {
572 for row in table.children.iter() {
573 for cell in row.children.iter() {
574 cell.children.clear_selection();
575 }
576 }
577 }
578 BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
579 BlockNode::Custom { state, .. } => {
580 state.clear_selection();
581 }
582 BlockNode::Definition { .. }
583 | BlockNode::Break { .. }
584 | BlockNode::HorizontalRule { .. }
585 | BlockNode::Unknown { .. } => {}
586 }
587 }
588}
589
590#[allow(unused)]
591#[derive(Debug, Default, Clone, PartialEq)]
592pub struct LinkMark {
593 pub url: SharedString,
594 pub identifier: Option<SharedString>,
596 pub title: Option<SharedString>,
597}
598
599#[derive(Debug, Default, Clone, PartialEq)]
600pub struct TextMark {
601 pub bold: bool,
602 pub italic: bool,
603 pub strikethrough: bool,
604 pub underline: bool,
605 pub code: bool,
606 pub highlight: Option<Hsla>,
610 pub link: Option<LinkMark>,
611}
612
613impl TextMark {
614 pub fn bold(mut self) -> Self {
615 self.bold = true;
616 self
617 }
618
619 pub fn italic(mut self) -> Self {
620 self.italic = true;
621 self
622 }
623
624 pub fn strikethrough(mut self) -> Self {
625 self.strikethrough = true;
626 self
627 }
628
629 pub fn underline(mut self) -> Self {
630 self.underline = true;
631 self
632 }
633
634 pub fn code(mut self) -> Self {
635 self.code = true;
636 self
637 }
638
639 pub fn highlight(mut self, color: Hsla) -> Self {
641 self.highlight = Some(color);
642 self
643 }
644
645 pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
646 self.link = Some(link.into());
647 self
648 }
649
650 pub fn merge(&mut self, other: TextMark) {
651 self.bold |= other.bold;
652 self.italic |= other.italic;
653 self.strikethrough |= other.strikethrough;
654 self.underline |= other.underline;
655 self.code |= other.code;
656 if other.highlight.is_some() {
657 self.highlight = other.highlight;
658 }
659 if let Some(link) = other.link {
660 self.link = Some(link);
661 }
662 }
663}
664
665#[derive(Debug, Default, Copy, Clone, PartialEq)]
667pub struct Span {
668 pub start: usize,
669 pub end: usize,
670}
671
672#[allow(unused)]
673#[derive(Default, Clone)]
674pub struct ImageNode {
675 pub url: SharedUri,
676 pub link: Option<LinkMark>,
677 pub title: Option<SharedString>,
678 pub alt: Option<SharedString>,
679 pub width: Option<DefiniteLength>,
680 pub height: Option<DefiniteLength>,
681 pub(crate) span: Option<Span>,
682 pub(super) embedded: OnceLock<Option<Arc<Image>>>,
685}
686
687impl ImageNode {
688 pub fn title(&self) -> String {
689 self.title
690 .clone()
691 .unwrap_or_else(|| self.alt.clone().unwrap_or_default())
692 .to_string()
693 }
694
695 pub(super) fn source(&self) -> ImageSource {
703 match self.embedded.get_or_init(|| data_url_image(&self.url)) {
704 Some(image) => ImageSource::Image(image.clone()),
705 None => self.url.clone().into(),
706 }
707 }
708}
709
710impl std::fmt::Debug for ImageNode {
711 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
712 f.debug_struct("ImageNode")
713 .field("url", &self.url)
714 .field("link", &self.link)
715 .field("title", &self.title)
716 .field("alt", &self.alt)
717 .field("width", &self.width)
718 .field("height", &self.height)
719 .field("span", &self.span)
720 .finish()
721 }
722}
723
724impl PartialEq for ImageNode {
725 fn eq(&self, other: &Self) -> bool {
726 self.url == other.url
727 && self.link == other.link
728 && self.title == other.title
729 && self.alt == other.alt
730 && self.width == other.width
731 && self.height == other.height
732 && self.span == other.span
733 }
734}
735
736#[derive(Debug, Clone, PartialEq)]
737pub(crate) struct SourceSegment {
738 pub(crate) rendered: Range<usize>,
739 pub(crate) source: Range<usize>,
740 pub(crate) linear: bool,
745}
746
747pub(crate) enum SourceRangeSelection {
748 Unselected,
749 Mapped(Range<usize>),
750 Unmapped,
751}
752
753impl SourceRangeSelection {
754 pub(crate) fn merge(&mut self, other: Self) {
755 match (&mut *self, other) {
756 (_, Self::Unselected) => {}
757 (_, Self::Unmapped) => *self = Self::Unmapped,
758 (Self::Unselected, mapped @ Self::Mapped(_)) => *self = mapped,
759 (Self::Mapped(selected), Self::Mapped(range)) => {
760 selected.start = selected.start.min(range.start);
761 selected.end = selected.end.max(range.end);
762 }
763 (Self::Unmapped, Self::Mapped(_)) => {}
764 }
765 }
766
767 pub(crate) fn into_range(self) -> Option<Range<usize>> {
768 match self {
769 Self::Mapped(range) => Some(range),
770 Self::Unselected | Self::Unmapped => None,
771 }
772 }
773}
774
775fn source_range_for_segments(
776 segments: &[SourceSegment],
777 selection: Range<usize>,
778) -> Option<Range<usize>> {
779 fn mapped_source_start(segment: &SourceSegment, rendered_start: usize) -> usize {
780 if segment.linear {
781 segment.source.start + rendered_start.saturating_sub(segment.rendered.start)
782 } else {
783 segment.source.start
784 }
785 }
786
787 fn mapped_source_end(segment: &SourceSegment, rendered_end: usize) -> usize {
788 if segment.linear {
789 segment.source.start
790 + rendered_end
791 .min(segment.rendered.end)
792 .saturating_sub(segment.rendered.start)
793 } else {
794 segment.source.end
795 }
796 }
797
798 if selection.start >= selection.end {
799 return None;
800 }
801
802 let mut overlapping = segments.iter().filter(|segment| {
803 segment.rendered.start < selection.end && segment.rendered.end > selection.start
804 });
805 let first = overlapping.next()?;
806 if first.rendered.start > selection.start {
807 return None;
808 }
809 let mut rendered_end = first.rendered.end;
810 let source_start = mapped_source_start(first, selection.start);
811 let mut source_end = mapped_source_end(first, selection.end);
812 for segment in overlapping {
813 if segment.rendered.start > rendered_end {
814 return None;
815 }
816 rendered_end = rendered_end.max(segment.rendered.end);
817 source_end = mapped_source_end(segment, selection.end);
818 }
819 (rendered_end >= selection.end).then_some(source_start..source_end)
820}
821
822#[derive(Default, Clone, Debug)]
823pub(crate) struct InlineNode {
824 pub(crate) text: SharedString,
826 pub(crate) image: Option<ImageNode>,
827 pub(crate) custom: Option<MarkdownNode>,
828 custom_selection: Arc<Mutex<bool>>,
829 pub(crate) marks: Vec<(Range<usize>, TextMark)>,
831 pub(crate) source_segments: Vec<SourceSegment>,
833
834 pub(super) state: Arc<Mutex<InlineState>>,
835}
836
837impl PartialEq for InlineNode {
838 fn eq(&self, other: &Self) -> bool {
839 self.text == other.text
840 && self.image == other.image
841 && self.custom == other.custom
842 && self.marks == other.marks
843 && self.source_segments == other.source_segments
844 }
845}
846
847pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
854 if text.is_empty() {
855 return String::new();
856 }
857
858 let mut out = text.to_string();
859 if mark.code {
860 out = format!("`{}`", out);
861 }
862 if mark.italic {
863 out = format!("*{}*", out);
864 }
865 if mark.bold {
866 out = format!("**{}**", out);
867 }
868 if mark.strikethrough {
869 out = format!("~~{}~~", out);
870 }
871 if mark.underline {
872 out = format!("<u>{}</u>", out);
875 }
876 if mark.highlight.is_some() {
877 out = format!("=={}==", out);
878 }
879 if let Some(link) = &mark.link {
880 out = match &link.title {
881 Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
882 None => format!("[{}]({})", out, link.url),
883 };
884 }
885 out
886}
887
888#[derive(Default)]
892struct MarkdownSource {
893 pieces: Vec<(String, Vec<TextMark>)>,
894}
895
896impl MarkdownSource {
897 fn push_str(&mut self, source: &str) {
898 self.push_marked(source, &TextMark::default());
899 }
900
901 fn push_marked(&mut self, source: &str, mark: &TextMark) {
902 if source.is_empty() {
903 return;
904 }
905 let mut layers = Vec::new();
908 if let Some(link) = &mark.link {
909 layers.push(TextMark {
910 link: Some(link.clone()),
911 ..Default::default()
912 });
913 }
914 if let Some(highlight) = mark.highlight {
915 layers.push(TextMark {
916 highlight: Some(highlight),
917 ..Default::default()
918 });
919 }
920 if mark.underline {
921 layers.push(TextMark::default().underline());
922 }
923 if mark.strikethrough {
924 layers.push(TextMark::default().strikethrough());
925 }
926 if mark.bold {
927 layers.push(TextMark::default().bold());
928 }
929 if mark.italic {
930 layers.push(TextMark::default().italic());
931 }
932 if mark.code {
933 layers.push(TextMark::default().code());
934 }
935 self.pieces.push((source.to_string(), layers));
936 }
937
938 fn push_text(
939 &mut self,
940 text: &str,
941 marks: &[(Range<usize>, TextMark)],
942 selection: Range<usize>,
943 ) {
944 let start = selection.start.min(text.len());
945 let end = selection.end.min(text.len());
946 if start >= end {
947 return;
948 }
949 let mut cursor = start;
950 for (range, mark) in marks {
951 let lo = range.start.max(start);
952 let hi = range.end.min(end);
953 if lo >= hi {
954 continue;
955 }
956 if cursor < lo {
957 self.push_str(&text[cursor..lo]);
958 }
959 self.push_marked(&text[lo..hi], mark);
960 cursor = hi;
961 }
962 if cursor < end {
963 self.push_str(&text[cursor..end]);
964 }
965 }
966
967 fn push_object(&mut self, node: &InlineNode) {
968 let mut mark = TextMark::default();
969 for (_, layer) in &node.marks {
970 mark.merge(layer.clone());
971 }
972 self.push_marked(&node.custom.as_ref().unwrap().to_markdown(), &mark);
973 }
974
975 fn is_empty(&self) -> bool {
976 self.pieces.is_empty()
977 }
978
979 fn finish(mut self) -> String {
980 fn write(pieces: &mut [(String, Vec<TextMark>)]) -> String {
981 let mut out = String::new();
982 let mut offset = 0;
983 while offset < pieces.len() {
984 let Some((mark, count)) = pieces[offset]
985 .1
986 .iter()
987 .map(|mark| {
988 let count = pieces[offset..]
989 .iter()
990 .take_while(|(_, layers)| layers.contains(mark))
991 .count();
992 (mark.clone(), count)
993 })
994 .reduce(|best, candidate| {
995 if candidate.1 > best.1 {
996 candidate
997 } else {
998 best
999 }
1000 })
1001 else {
1002 out.push_str(&pieces[offset].0);
1003 offset += 1;
1004 continue;
1005 };
1006 let group = &mut pieces[offset..offset + count];
1007 for (_, layers) in group.iter_mut() {
1008 layers.retain(|layer| layer != &mark);
1009 }
1010 let content = write(group);
1011 out.push_str(&wrap_with_mark(&content, &mark));
1012 offset += count;
1013 }
1014 out
1015 }
1016 write(&mut self.pieces)
1017 }
1018}
1019
1020#[derive(Default)]
1023struct RunSelection {
1024 emitted: bool,
1025 at_start: bool,
1026 at_end: bool,
1027}
1028
1029fn emit_run(
1033 state: &Arc<Mutex<InlineState>>,
1034 run: &[(usize, &InlineNode)],
1035 pending_images: &mut Vec<String>,
1036 out: &mut MarkdownSource,
1037) -> RunSelection {
1038 let mut selected = RunSelection::default();
1039 let Ok(state) = state.lock() else {
1040 return selected;
1041 };
1042 let Some(selection) = &state.selection else {
1043 return selected;
1044 };
1045 if selection.start >= selection.end {
1046 return selected;
1047 }
1048
1049 selected.at_start = selection.start == 0;
1050 selected.at_end = selection.end >= state.text.len();
1051
1052 for (start, child) in run {
1053 let end = start + child.text.len();
1054 let lo = selection.start.max(*start);
1055 let hi = selection.end.min(end);
1056 if lo >= hi {
1057 continue;
1058 }
1059
1060 if !selected.emitted {
1061 if selected.at_start {
1062 out.push_str(&pending_images.join(""));
1063 }
1064 pending_images.clear();
1065 }
1066 selected.emitted = true;
1067
1068 out.push_text(&child.text, &child.marks, (lo - start)..(hi - start));
1069 }
1070
1071 selected
1072}
1073
1074fn image_markdown(image: &ImageNode) -> String {
1076 let alt = image.alt.clone().unwrap_or_default();
1077 let title = image
1078 .title
1079 .clone()
1080 .map_or(String::new(), |title| format!(" \"{}\"", title));
1081 format!("", alt, image.url, title)
1082}
1083
1084#[cfg(test)]
1093pub(crate) fn reconstruct_markdown(
1094 text: &str,
1095 marks: &[(Range<usize>, TextMark)],
1096 selection: Range<usize>,
1097) -> String {
1098 let mut source = MarkdownSource::default();
1099 source.push_text(text, marks, selection);
1100 source.finish()
1101}
1102
1103fn table_selected_source(table: &Table) -> String {
1110 let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
1111
1112 let any_selected = table.children.iter().any(|row| {
1113 row.children
1114 .iter()
1115 .any(|cell| !cell_source(cell).trim().is_empty())
1116 });
1117 if !any_selected {
1118 return String::new();
1119 }
1120
1121 let mut lines: Vec<String> = Vec::new();
1122 for (row_ix, row) in table.children.iter().enumerate() {
1123 let cells: Vec<String> = row
1124 .children
1125 .iter()
1126 .map(|cell| cell_source(cell).trim().to_string())
1127 .collect();
1128 lines.push(format!("| {} |", cells.join(" | ")));
1129
1130 if row_ix == 0 {
1133 let aligns: Vec<String> = (0..row.children.len())
1134 .map(|ix| {
1135 match table.column_align(ix) {
1136 ColumnumnAlign::Left => ":--",
1137 ColumnumnAlign::Center => ":-:",
1138 ColumnumnAlign::Right => "--:",
1139 }
1140 .to_string()
1141 })
1142 .collect();
1143 lines.push(format!("| {} |", aligns.join(" | ")));
1144 }
1145 }
1146
1147 lines.join("\n")
1148}
1149
1150fn list_selected_source(
1160 children: &[BlockNode],
1161 ordered: bool,
1162 start: Option<u32>,
1163 indent: &str,
1164) -> String {
1165 let mut out = String::new();
1166 let mut item_ix = 0usize;
1167
1168 for child in children {
1169 let BlockNode::ListItem {
1170 children: item_children,
1171 checked,
1172 ..
1173 } = child
1174 else {
1175 continue;
1176 };
1177
1178 let marker = if ordered {
1179 format!("{}. ", ordered_list_ordinal(start, item_ix))
1180 } else {
1181 "- ".to_string()
1182 };
1183 let checkbox = match checked {
1184 Some(true) => "[x] ",
1185 Some(false) => "[ ] ",
1186 None => "",
1187 };
1188 let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
1189
1190 let mut content = String::new();
1193 let mut nested = String::new();
1194 for sub in item_children {
1195 if let BlockNode::List {
1196 children: sub_children,
1197 ordered: sub_ordered,
1198 start: sub_start,
1199 ..
1200 } = sub
1201 {
1202 nested.push_str(&list_selected_source(
1203 sub_children,
1204 *sub_ordered,
1205 *sub_start,
1206 &child_indent,
1207 ));
1208 } else {
1209 content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
1210 }
1211 }
1212 let content = content.trim_end_matches('\n');
1213
1214 if content.is_empty() && nested.is_empty() {
1215 item_ix += 1;
1216 continue;
1217 }
1218
1219 if content.is_empty() {
1220 out.push_str(indent);
1222 out.push_str(&marker);
1223 out.push_str(checkbox.trim_end());
1224 out.push('\n');
1225 } else {
1226 let mut lines = content.lines();
1229 if let Some(first) = lines.next() {
1230 out.push_str(indent);
1231 out.push_str(&marker);
1232 out.push_str(checkbox);
1233 out.push_str(first);
1234 out.push('\n');
1235 }
1236 for line in lines {
1237 out.push_str(&child_indent);
1238 out.push_str(line);
1239 out.push('\n');
1240 }
1241 }
1242 out.push_str(&nested);
1243 item_ix += 1;
1244 }
1245
1246 out
1247}
1248
1249impl InlineNode {
1250 pub(crate) fn new(text: impl Into<SharedString>) -> Self {
1251 Self {
1252 text: text.into(),
1253 image: None,
1254 custom: None,
1255 custom_selection: Arc::default(),
1256 marks: vec![],
1257 source_segments: vec![],
1258 state: Arc::new(Mutex::new(InlineState::default())),
1259 }
1260 }
1261
1262 pub(crate) fn custom(node: MarkdownNode) -> Self {
1263 let mut this = Self::new(node.as_text().to_string());
1264 this.custom = Some(node);
1265 this
1266 }
1267
1268 pub(crate) fn image(image: ImageNode) -> Self {
1269 let mut this = Self::new("");
1270 this.image = Some(image);
1271 this
1272 }
1273
1274 pub(crate) fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
1275 self.marks = marks;
1276 self
1277 }
1278
1279 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
1280 self.source_segments = source_segments;
1281 self
1282 }
1283
1284 fn selected_source_range(&self, selection: Range<usize>) -> Option<Range<usize>> {
1285 source_range_for_segments(&self.source_segments, selection)
1286 }
1287}
1288
1289#[derive(Debug, Clone, Default)]
1294pub(crate) struct Paragraph {
1295 pub(super) span: Option<Span>,
1296 pub(super) children: Vec<InlineNode>,
1297 pub(super) link_refs: HashMap<SharedString, SharedString>,
1301
1302 pub(crate) state: Arc<Mutex<InlineState>>,
1303 pub(super) render_cache: ParagraphRenderCache,
1306}
1307
1308#[derive(Default)]
1316pub(super) struct ParagraphRenderCache(Mutex<Option<Arc<ParagraphRender>>>);
1317
1318impl Clone for ParagraphRenderCache {
1319 fn clone(&self) -> Self {
1320 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1321 Self(Mutex::new(cached))
1322 }
1323}
1324
1325impl std::fmt::Debug for ParagraphRenderCache {
1326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1327 f.write_str("ParagraphRenderCache")
1328 }
1329}
1330
1331struct ParagraphRender {
1341 style: Arc<TextViewStyle>,
1342 mono_font: SharedString,
1343 text: SharedString,
1344 highlights: Vec<(Range<usize>, InlineHighlight)>,
1345 links: Vec<(Range<usize>, LinkMark)>,
1347}
1348
1349impl PartialEq for Paragraph {
1350 fn eq(&self, other: &Self) -> bool {
1351 self.span == other.span
1352 && self.children == other.children
1353 && self.link_refs == other.link_refs
1354 }
1355}
1356
1357impl Paragraph {
1358 pub(crate) fn new(text: String) -> Self {
1359 Self {
1360 span: None,
1361 children: vec![InlineNode::new(&text)],
1362 link_refs: HashMap::new(),
1363 state: Arc::new(Mutex::new(InlineState::default())),
1364 render_cache: ParagraphRenderCache::default(),
1365 }
1366 }
1367
1368 fn plain_render(
1371 &self,
1372 node_cx: &NodeContext,
1373 cx: &App,
1374 ) -> (
1375 SharedString,
1376 Vec<(Range<usize>, InlineHighlight)>,
1377 Vec<(Range<usize>, LinkMark)>,
1378 ) {
1379 let mono_font = cx.theme().tokens.typography.mono.clone();
1380 if let Ok(cache) = self.render_cache.0.lock()
1381 && let Some(cached) = cache.as_ref()
1382 && (Arc::ptr_eq(&cached.style, &node_cx.style) || *cached.style == *node_cx.style)
1383 && cached.mono_font == mono_font
1384 {
1385 return (
1386 cached.text.clone(),
1387 cached.highlights.clone(),
1388 cached.links.clone(),
1389 );
1390 }
1391
1392 let mut text = String::new();
1393 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1394 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1395 let mut offset = 0;
1396 for inline_node in &self.children {
1397 let text_len = inline_node.text.len();
1398 text.push_str(&inline_node.text);
1399 let mut node_highlights = vec![];
1400 for (range, style) in &inline_node.marks {
1401 let inner_range = (offset + range.start)..(offset + range.end);
1402 let mut highlight = mark_highlight(style, node_cx, cx);
1403 if let Some(link_mark) = style.link.clone() {
1404 highlight.style.color = Some(node_cx.style.link());
1405 highlight.style.underline = Some(link_underline(&node_cx.style));
1406 links.push((inner_range.clone(), link_mark));
1407 }
1408 node_highlights.push((inner_range, highlight));
1409 }
1410 highlights = combine_highlights(highlights, node_highlights);
1411 offset += text_len;
1412 }
1413 let text = SharedString::from(text);
1414 if let Ok(mut cache) = self.render_cache.0.lock() {
1415 *cache = Some(Arc::new(ParagraphRender {
1416 style: node_cx.style.clone(),
1417 mono_font,
1418 text: text.clone(),
1419 highlights: highlights.clone(),
1420 links: links.clone(),
1421 }));
1422 }
1423 (text, highlights, links)
1424 }
1425
1426 pub(super) fn selected_text(&self) -> String {
1427 let mut text = String::new();
1428
1429 for c in self.children.iter() {
1430 let Ok(state) = c.state.lock() else {
1431 continue;
1432 };
1433 if let Some(selection) = &state.selection {
1434 text.push_str(&state.text[selection.start..selection.end]);
1435 }
1436 if let Some(custom) = &c.custom
1437 && c.custom_selection.lock().is_ok_and(|selected| *selected)
1438 {
1439 text.push_str(custom.as_text());
1440 }
1441 }
1442
1443 if let Ok(state) = self.state.lock()
1444 && let Some(selection) = &state.selection
1445 {
1446 text.push_str(&state.text[selection.start..selection.end]);
1447 }
1448
1449 text
1450 }
1451
1452 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1454 let mut selected = SourceRangeSelection::Unselected;
1455 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1456 let mut offset = 0;
1457 let mut pending_images: Vec<Option<Span>> = Vec::new();
1458 let mut enters_image = true;
1459
1460 let include_run = |state: &Arc<Mutex<InlineState>>,
1461 run: &[(usize, &InlineNode)]|
1462 -> (SourceRangeSelection, RunSelection) {
1463 let Ok(state) = state.lock() else {
1464 return (SourceRangeSelection::Unmapped, RunSelection::default());
1465 };
1466 let Some(selection) = state.selection else {
1467 return (SourceRangeSelection::Unselected, RunSelection::default());
1468 };
1469 if selection.start >= selection.end {
1470 return (SourceRangeSelection::Unselected, RunSelection::default());
1471 }
1472
1473 let mut run_selection = RunSelection {
1474 at_start: selection.start == 0,
1475 at_end: selection.end >= state.text.len(),
1476 ..Default::default()
1477 };
1478 let mut mapped = SourceRangeSelection::Unselected;
1479 let mut rendered_end = selection.start;
1480 for (start, child) in run {
1481 let end = start + child.text.len();
1482 let lo = selection.start.max(*start);
1483 let hi = selection.end.min(end);
1484 if lo >= hi {
1485 continue;
1486 }
1487 run_selection.emitted = true;
1488 if lo > rendered_end {
1489 return (SourceRangeSelection::Unmapped, run_selection);
1490 }
1491 let Some(range) = child.selected_source_range((lo - start)..(hi - start)) else {
1492 return (SourceRangeSelection::Unmapped, run_selection);
1493 };
1494 mapped.merge(SourceRangeSelection::Mapped(range));
1495 rendered_end = rendered_end.max(hi);
1496 }
1497 if rendered_end < selection.end {
1498 (SourceRangeSelection::Unmapped, run_selection)
1499 } else {
1500 (mapped, run_selection)
1501 }
1502 };
1503
1504 let merge_images = |selected: &mut SourceRangeSelection, images: &mut Vec<Option<Span>>| {
1505 for span in images.drain(..) {
1506 selected.merge(
1507 span.map(|span| SourceRangeSelection::Mapped(span.start..span.end))
1508 .unwrap_or(SourceRangeSelection::Unmapped),
1509 );
1510 }
1511 };
1512
1513 for child in &self.children {
1514 if child.custom.is_some() {
1515 let (run_range, run_selection) = include_run(&child.state, &run);
1516 if run_selection.emitted && run_selection.at_start {
1517 merge_images(&mut selected, &mut pending_images);
1518 }
1519 selected.merge(run_range);
1520
1521 match child.custom_selection.lock() {
1522 Ok(value) if *value => {
1523 if run.is_empty() || (run_selection.emitted && run_selection.at_end) {
1524 merge_images(&mut selected, &mut pending_images);
1525 }
1526 selected.merge(
1527 child
1528 .custom
1529 .as_ref()
1530 .and_then(MarkdownNode::source_range)
1531 .map(SourceRangeSelection::Mapped)
1532 .unwrap_or(SourceRangeSelection::Unmapped),
1533 );
1534 enters_image = true;
1535 }
1536 Ok(_) => enters_image = false,
1537 Err(_) => {
1538 selected.merge(SourceRangeSelection::Unmapped);
1539 enters_image = false;
1540 }
1541 }
1542 pending_images.clear();
1543 run.clear();
1544 offset = 0;
1545 continue;
1546 }
1547 if let Some(image) = &child.image {
1548 let run_before = !run.is_empty();
1549 let (run_range, run_selection) = include_run(&child.state, &run);
1550 if run_selection.emitted && run_selection.at_start {
1551 merge_images(&mut selected, &mut pending_images);
1552 }
1553 selected.merge(run_range);
1554 if run_before {
1555 enters_image = run_selection.emitted && run_selection.at_end;
1556 }
1557 if enters_image {
1558 pending_images.push(image.span);
1559 } else {
1560 pending_images.clear();
1561 }
1562 run.clear();
1563 offset = 0;
1564 continue;
1565 }
1566 run.push((offset, child));
1567 offset += child.text.len();
1568 }
1569
1570 let (trailing_range, trailing) = include_run(&self.state, &run);
1571 if trailing.emitted && trailing.at_start {
1572 merge_images(&mut selected, &mut pending_images);
1573 }
1574 selected.merge(trailing_range);
1575 if !trailing.emitted
1576 && enters_image
1577 && !matches!(selected, SourceRangeSelection::Unselected)
1578 {
1579 merge_images(&mut selected, &mut pending_images);
1580 }
1581 selected
1582 }
1583
1584 pub(super) fn selected_source(&self) -> String {
1604 let mut source = MarkdownSource::default();
1605 let mut pending_images: Vec<String> = Vec::new();
1606 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1607 let mut offset = 0;
1608 let mut enters_image = true;
1609
1610 for child in self.children.iter() {
1611 if child.custom.is_some() {
1612 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1613 let object_selected = child
1614 .custom_selection
1615 .lock()
1616 .is_ok_and(|selected| *selected);
1617 if object_selected {
1618 if run.is_empty() || (selected.emitted && selected.at_end) {
1619 source.push_str(&pending_images.join(""));
1620 }
1621 source.push_object(child);
1622 }
1623 pending_images.clear();
1624 enters_image = object_selected;
1625 run.clear();
1626 offset = 0;
1627 continue;
1628 }
1629 let Some(image) = &child.image else {
1630 run.push((offset, child));
1631 offset += child.text.len();
1632 continue;
1633 };
1634
1635 let run_before = !run.is_empty();
1637 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1638 if run_before {
1639 enters_image = selected.emitted && selected.at_end;
1640 }
1641 if enters_image {
1642 pending_images.push(image_markdown(image));
1643 } else {
1644 pending_images.clear();
1645 }
1646
1647 run.clear();
1648 offset = 0;
1649 }
1650
1651 let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
1652 if !trailing.emitted && enters_image && !source.is_empty() {
1654 source.push_str(&pending_images.join(""));
1655 }
1656
1657 source.finish()
1658 }
1659
1660 pub(super) fn text(&self) -> String {
1661 let mut text = String::new();
1662 for node in self.children.iter() {
1663 text.push_str(&node.text);
1664 }
1665 text
1666 }
1667
1668 pub(super) fn has_selection(&self) -> bool {
1672 self.children.iter().any(|c| {
1673 c.state.lock().is_ok_and(|state| state.selection.is_some())
1674 || c.custom_selection.lock().is_ok_and(|selected| *selected)
1675 }) || self
1676 .state
1677 .lock()
1678 .is_ok_and(|state| state.selection.is_some())
1679 }
1680
1681 pub(super) fn clear_selection(&self) {
1682 for c in self.children.iter() {
1683 if let Ok(mut selected) = c.custom_selection.lock() {
1684 *selected = false;
1685 }
1686 if let Ok(mut state) = c.state.lock() {
1687 state.selection = None;
1688 }
1689 }
1690
1691 if let Ok(mut state) = self.state.lock() {
1692 state.selection = None;
1693 }
1694 }
1695}
1696
1697#[derive(Debug, Clone, Default, PartialEq)]
1698pub(crate) struct Table {
1699 pub(crate) children: Vec<TableRow>,
1700 pub(crate) column_aligns: Vec<ColumnumnAlign>,
1701 pub(crate) span: Option<Span>,
1702 pub(crate) table_data_cache: TableDataCache,
1705 pub(crate) column_widths_cache: TableColumnWidthsCache,
1706}
1707
1708#[derive(Default)]
1716pub(crate) struct TableDataCache(Mutex<Option<Arc<TableData>>>);
1717
1718impl Clone for TableDataCache {
1719 fn clone(&self) -> Self {
1720 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1721 Self(Mutex::new(cached))
1722 }
1723}
1724
1725impl PartialEq for TableDataCache {
1726 fn eq(&self, _: &Self) -> bool {
1727 true
1728 }
1729}
1730
1731impl std::fmt::Debug for TableDataCache {
1732 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1733 f.write_str("TableDataCache")
1734 }
1735}
1736
1737#[derive(Default)]
1740pub(crate) struct TableColumnWidthsCache(Mutex<Option<Arc<TableColumnWidths>>>);
1741
1742impl Clone for TableColumnWidthsCache {
1743 fn clone(&self) -> Self {
1744 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1745 Self(Mutex::new(cached))
1746 }
1747}
1748
1749impl PartialEq for TableColumnWidthsCache {
1750 fn eq(&self, _: &Self) -> bool {
1751 true
1752 }
1753}
1754
1755impl std::fmt::Debug for TableColumnWidthsCache {
1756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1757 f.write_str("TableColumnWidthsCache")
1758 }
1759}
1760
1761struct TableColumnWidths {
1762 text_system: Weak<gpui::WindowTextSystem>,
1763 text_styles: TableTextStyles,
1765 rem_size: Pixels,
1766 mono_family: SharedString,
1767 inline_code: HighlightStyle,
1768 widths: Vec<f32>,
1770 custom_cells: Vec<(usize, usize)>,
1771}
1772
1773#[derive(Clone, PartialEq)]
1777struct TableTextStyles {
1778 head: gpui::TextStyle,
1779 body: gpui::TextStyle,
1780 head_refinement: TextStyleRefinement,
1781 body_refinement: TextStyleRefinement,
1782}
1783
1784impl TableTextStyles {
1785 fn new(style: &TextViewStyle, window: &Window) -> Self {
1786 let mut body_refinement = style.table().text.clone();
1787 let mut head_refinement = body_refinement.clone();
1788 head_refinement.refine(&style.table_head().text);
1789 head_refinement.refine(&style.table_cell().text);
1790 body_refinement.refine(&style.table_cell().text);
1791 let mut head = window.text_style();
1792 let mut body = head.clone();
1793 head.refine(&head_refinement);
1794 body.refine(&body_refinement);
1795 Self {
1796 head,
1797 body,
1798 head_refinement,
1799 body_refinement,
1800 }
1801 }
1802
1803 fn row(&self, row_ix: usize) -> &gpui::TextStyle {
1804 if row_ix == 0 { &self.head } else { &self.body }
1805 }
1806
1807 fn row_refinement(&self, row_ix: usize) -> &TextStyleRefinement {
1808 if row_ix == 0 {
1809 &self.head_refinement
1810 } else {
1811 &self.body_refinement
1812 }
1813 }
1814}
1815
1816#[derive(Debug, Clone, Default, PartialEq)]
1819pub struct TableData {
1820 pub headers: Vec<String>,
1822 pub rows: Vec<Vec<String>>,
1825 pub markdown: String,
1827 pub span: Option<Range<usize>>,
1834}
1835
1836impl Table {
1837 pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
1838 self.column_aligns.get(index).copied().unwrap_or_default()
1839 }
1840
1841 pub(crate) fn to_markdown(&self) -> String {
1848 let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
1849
1850 for (row_ix, row) in self.children.iter().enumerate() {
1851 let cells: Vec<String> = row
1852 .children
1853 .iter()
1854 .map(|cell| {
1855 cell.children
1856 .to_markdown()
1857 .trim()
1858 .replace('\n', " ")
1859 .replace('|', "\\|")
1860 })
1861 .collect();
1862 lines.push(format!("| {} |", cells.join(" | ")));
1863
1864 if row_ix == 0 {
1867 let aligns: Vec<String> = (0..row.children.len())
1868 .map(|ix| {
1869 match self.column_align(ix) {
1870 ColumnumnAlign::Left => ":--",
1871 ColumnumnAlign::Center => ":-:",
1872 ColumnumnAlign::Right => "--:",
1873 }
1874 .to_string()
1875 })
1876 .collect();
1877 lines.push(format!("| {} |", aligns.join(" | ")));
1878 }
1879 }
1880
1881 lines.join("\n")
1882 }
1883
1884 pub(crate) fn table_data(&self) -> TableData {
1887 let row_text = |row: &TableRow| {
1888 row.children
1889 .iter()
1890 .map(|cell| cell.children.text().trim().to_string())
1891 .collect::<Vec<_>>()
1892 };
1893
1894 TableData {
1895 headers: self.children.first().map(row_text).unwrap_or_default(),
1896 rows: self.children.iter().skip(1).map(row_text).collect(),
1897 markdown: self.to_markdown(),
1898 span: self.span.map(|span| span.start..span.end),
1899 }
1900 }
1901
1902 fn cached_table_data(&self) -> Arc<TableData> {
1907 if let Ok(cache) = self.table_data_cache.0.lock()
1908 && let Some(cached) = cache.as_ref()
1909 {
1910 return cached.clone();
1911 }
1912
1913 let data = Arc::new(self.table_data());
1914 if let Ok(mut cache) = self.table_data_cache.0.lock() {
1915 *cache = Some(data.clone());
1916 }
1917 data
1918 }
1919}
1920
1921#[derive(Debug, Default, Copy, Clone, PartialEq)]
1922pub(crate) enum ColumnumnAlign {
1923 #[default]
1924 Left,
1925 Center,
1926 Right,
1927}
1928
1929impl From<mdast::AlignKind> for ColumnumnAlign {
1930 fn from(value: mdast::AlignKind) -> Self {
1931 match value {
1932 mdast::AlignKind::None => ColumnumnAlign::Left,
1933 mdast::AlignKind::Left => ColumnumnAlign::Left,
1934 mdast::AlignKind::Center => ColumnumnAlign::Center,
1935 mdast::AlignKind::Right => ColumnumnAlign::Right,
1936 }
1937 }
1938}
1939
1940#[derive(Debug, Clone, Default, PartialEq)]
1941pub(crate) struct TableRow {
1942 pub children: Vec<TableCell>,
1943}
1944
1945#[derive(Debug, Clone, Default, PartialEq)]
1946pub(crate) struct TableCell {
1947 pub children: Paragraph,
1948 pub width: Option<DefiniteLength>,
1949}
1950
1951impl Paragraph {
1952 pub(crate) fn take(&mut self) -> Paragraph {
1953 std::mem::replace(
1954 self,
1955 Paragraph {
1956 span: None,
1957 children: vec![],
1958 link_refs: Default::default(),
1959 state: Arc::new(Mutex::new(InlineState::default())),
1960 render_cache: ParagraphRenderCache::default(),
1961 },
1962 )
1963 }
1964
1965 pub(crate) fn is_image(&self) -> bool {
1966 false
1967 }
1968
1969 pub(crate) fn set_span(&mut self, span: Span) {
1970 self.span = Some(span);
1971 }
1972
1973 pub(crate) fn push_str(&mut self, text: &str) {
1974 self.children.push(
1975 InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
1976 );
1977 self.invalidate_render_cache();
1978 }
1979
1980 pub(crate) fn push(&mut self, text: InlineNode) {
1981 self.children.push(text);
1982 self.invalidate_render_cache();
1983 }
1984
1985 pub(crate) fn push_image(&mut self, image: ImageNode) {
1986 self.children.push(InlineNode::image(image));
1987 self.invalidate_render_cache();
1988 }
1989
1990 fn invalidate_render_cache(&mut self) {
1992 self.render_cache = ParagraphRenderCache::default();
1993 }
1994
1995 pub(crate) fn is_empty(&self) -> bool {
1996 self.children.is_empty()
1997 || self
1998 .children
1999 .iter()
2000 .all(|node| node.text.is_empty() && node.image.is_none())
2001 }
2002
2003 pub(crate) fn text_len(&self) -> usize {
2005 self.children
2006 .iter()
2007 .map(|node| node.text.len())
2008 .sum::<usize>()
2009 }
2010
2011 pub(crate) fn merge(&mut self, other: Self) {
2012 self.children.extend(other.children);
2013 self.invalidate_render_cache();
2014 }
2015}
2016
2017#[derive(Debug, Clone)]
2018pub struct CodeBlock {
2019 lang: Option<SharedString>,
2020 state: Arc<Mutex<InlineState>>,
2021 highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
2022 pub(crate) source_segments: Vec<SourceSegment>,
2025 pub span: Option<Span>,
2026}
2027
2028struct CachedCodeBlockHighlights {
2029 highlighter: Arc<CodeBlockHighlighterFn>,
2030 styles: Vec<(Range<usize>, HighlightStyle)>,
2031}
2032
2033impl std::fmt::Debug for CachedCodeBlockHighlights {
2034 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2035 f.debug_struct("CachedCodeBlockHighlights")
2036 .field("styles", &self.styles)
2037 .finish_non_exhaustive()
2038 }
2039}
2040
2041impl PartialEq for CodeBlock {
2042 fn eq(&self, other: &Self) -> bool {
2043 self.lang == other.lang && self.code() == other.code() && self.span == other.span
2044 }
2045}
2046
2047impl CodeBlock {
2048 pub fn lang(&self) -> Option<SharedString> {
2050 self.lang.clone()
2051 }
2052
2053 pub fn code(&self) -> SharedString {
2055 self.state
2056 .lock()
2057 .map(|state| state.text.clone())
2058 .unwrap_or_default()
2059 }
2060
2061 pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
2067 Self::new(code.into(), lang.map(Into::into), None::<Span>)
2068 }
2069
2070 pub(crate) fn new(
2071 code: SharedString,
2072 lang: Option<SharedString>,
2073 span: Option<impl Into<Span>>,
2074 ) -> Self {
2075 let state = Arc::new(Mutex::new(InlineState::default()));
2076 if let Ok(mut state) = state.lock() {
2077 state.set_text(code);
2078 }
2079
2080 Self {
2081 lang,
2082 state,
2083 highlight_cache: Arc::new(Mutex::new(None)),
2084 source_segments: vec![],
2085 span: span.map(|s| s.into()),
2086 }
2087 }
2088
2089 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
2090 self.source_segments = source_segments;
2091 self
2092 }
2093
2094 #[cfg(test)]
2095 pub(crate) fn set_selection(&self, selection: Range<usize>) {
2096 if let Ok(mut state) = self.state.lock() {
2097 state.selection = Some(selection.into());
2098 }
2099 }
2100
2101 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
2102 let Ok(state) = self.state.lock() else {
2103 return SourceRangeSelection::Unmapped;
2104 };
2105 let Some(selection) = state.selection else {
2106 return SourceRangeSelection::Unselected;
2107 };
2108 if selection.start >= selection.end {
2109 return SourceRangeSelection::Unselected;
2110 }
2111 source_range_for_segments(&self.source_segments, selection.start..selection.end)
2112 .map(SourceRangeSelection::Mapped)
2113 .unwrap_or(SourceRangeSelection::Unmapped)
2114 }
2115
2116 fn highlighted_styles(
2117 &self,
2118 highlighter: &Arc<CodeBlockHighlighterFn>,
2119 ) -> Vec<(Range<usize>, HighlightStyle)> {
2120 if let Ok(cache) = self.highlight_cache.lock()
2121 && let Some(cache) = cache.as_ref()
2122 && Arc::ptr_eq(&cache.highlighter, highlighter)
2123 {
2124 return cache.styles.clone();
2125 }
2126
2127 let code_len = self.code().len();
2128 let styles = highlighter(self)
2129 .into_iter()
2130 .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
2131 .collect::<Vec<_>>();
2132 if let Ok(mut cache) = self.highlight_cache.lock() {
2133 *cache = Some(CachedCodeBlockHighlights {
2134 highlighter: highlighter.clone(),
2135 styles: styles.clone(),
2136 });
2137 }
2138 styles
2139 }
2140
2141 pub(super) fn selected_text(&self) -> String {
2142 let mut text = String::new();
2143 if let Ok(state) = self.state.lock()
2144 && let Some(selection) = &state.selection
2145 {
2146 text.push_str(&state.text[selection.start..selection.end]);
2147 }
2148 text
2149 }
2150
2151 pub(super) fn selected_source(&self) -> String {
2158 let code = self.selected_text();
2159 if code.is_empty() {
2160 return String::new();
2161 }
2162 let lang = self.lang.clone().unwrap_or_default();
2163 let code = code.trim_end_matches('\n');
2166 format!("```{}\n{}\n```", lang, code)
2167 }
2168
2169 pub(super) fn text(&self) -> String {
2170 self.state
2171 .lock()
2172 .map(|state| state.text.to_string())
2173 .unwrap_or_default()
2174 }
2175
2176 pub(super) fn has_selection(&self) -> bool {
2180 self.state
2181 .lock()
2182 .is_ok_and(|state| state.selection.is_some())
2183 }
2184
2185 pub(super) fn clear_selection(&self) {
2186 if let Ok(mut state) = self.state.lock() {
2187 state.selection = None;
2188 }
2189 }
2190
2191 fn render(
2192 &self,
2193 options: &NodeRenderOptions,
2194 node_cx: &NodeContext,
2195 window: &mut Window,
2196 cx: &mut App,
2197 ) -> AnyElement {
2198 let style = &node_cx.style;
2199 let leaf_key = self.span.map(|span| TextLeafKey::block(span.start));
2200 let lang_label = self
2205 .lang()
2206 .filter(|lang| !lang.is_empty() && node_cx.code_block_actions.is_none())
2207 .map(|lang| {
2208 div()
2209 .absolute()
2210 .top(px(7.))
2211 .right_3()
2212 .font_family(cx.theme().tokens.typography.sans.clone())
2213 .text_size(cx.theme().tokens.typography.xs.size)
2214 .line_height(px(16.))
2215 .text_color(style.muted_foreground())
2216 .child(lang)
2217 });
2218
2219 let block = div()
2220 .w_full()
2221 .min_w_0()
2222 .px_4()
2223 .pt(if lang_label.is_some() {
2224 px(28.)
2225 } else {
2226 px(14.)
2227 })
2228 .pb(px(14.))
2229 .bg(style.code_background())
2230 .border_1()
2231 .border_color(style.border())
2232 .font_family(cx.theme().tokens.typography.mono.clone())
2233 .text_size(cx.theme().tokens.typography.mono_md.size)
2234 .line_height(relative(1.6))
2235 .relative()
2236 .refine_style(&style.code_block())
2237 .child(
2238 Inline::new(
2239 self.state.clone(),
2240 vec![],
2241 fade_highlights(
2242 node_cx
2243 .code_block_highlighter
2244 .as_ref()
2245 .map(|highlighter| self.highlighted_styles(highlighter))
2246 .unwrap_or_default()
2247 .into_iter()
2248 .map(|(range, style)| (range, InlineHighlight::from(style)))
2249 .collect(),
2250 node_cx.stream_fades(leaf_key),
2251 ),
2252 node_cx.link_click_handler.clone(),
2253 )
2254 .range_backgrounds(node_cx.range_backgrounds(leaf_key).to_vec())
2255 .reveal(node_cx.reveal_at(leaf_key, 0, self.code().len())),
2256 );
2257 let actions = node_cx.code_block_actions.clone().map(|actions| {
2258 div()
2259 .id("actions")
2260 .absolute()
2261 .top_2()
2262 .right_2()
2263 .bg(style.code_background())
2264 .rounded(cx.theme().tokens.radius.md)
2265 .child(actions(&self, window, cx))
2266 });
2267 let id = block_element_id("codeblock", self.span, options.ix);
2270 let block = if matches!(style.code_block().overflow.y, Some(Overflow::Scroll)) {
2271 let scroll_handle = window
2272 .use_keyed_state(
2273 block_element_id("codeblock-scroll", self.span, options.ix),
2274 cx,
2275 |_, _| ScrollHandle::default(),
2276 )
2277 .read(cx)
2278 .clone();
2279 div()
2286 .id(id.clone())
2287 .w_full()
2288 .min_w_0()
2289 .relative()
2290 .child(block.id("scroll").track_scroll(&scroll_handle))
2291 .child(ScrollableMask::new(Axis::Vertical, &scroll_handle).id(id))
2292 .child(
2293 div()
2294 .absolute()
2295 .inset_0()
2296 .child(Scrollbar::vertical(&scroll_handle)),
2297 )
2298 .children(actions)
2299 .children(lang_label)
2300 .into_any_element()
2301 } else {
2302 match actions {
2304 Some(actions) => block.id(id).child(actions).into_any_element(),
2305 None => block.children(lang_label).into_any_element(),
2306 }
2307 };
2308
2309 gapped(
2310 block,
2311 if options.is_last {
2312 rems(0.)
2313 } else {
2314 style.paragraph_gap()
2315 },
2316 )
2317 }
2318}
2319
2320#[derive(Default, Clone)]
2322pub(crate) struct NodeContext {
2323 pub(crate) offset: usize,
2326 pub(crate) link_refs: HashMap<SharedString, LinkMark>,
2327 pub(crate) style: Arc<TextViewStyle>,
2328 pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
2329 pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
2330 pub(crate) table_actions: Option<Arc<TableActionsFn>>,
2331 pub(crate) image_source: Option<Arc<super::text_view::ImageSourceFn>>,
2332 pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
2333 pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
2334 pub(crate) stream_fade: Option<Arc<StreamFadeFrame>>,
2336 pub(crate) range_highlights: Option<Arc<RangeHighlightFrame>>,
2338 pub(crate) reveal: Option<RevealRequest>,
2340 pub(crate) body_font_size: Pixels,
2343}
2344
2345impl NodeContext {
2346 fn image_source(&self, image: &ImageNode) -> ImageSource {
2347 match &self.image_source {
2348 Some(resolve) => resolve(&image.url),
2349 None => image.source(),
2350 }
2351 }
2352
2353 pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
2354 self.link_refs.insert(identifier, link);
2355 }
2356
2357 fn stream_fades(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, f32)] {
2359 match (&self.stream_fade, key) {
2360 (Some(frame), Some(key)) => frame.fades(key).unwrap_or_default(),
2361 _ => &[],
2362 }
2363 }
2364
2365 fn range_backgrounds(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, Hsla)] {
2368 match (&self.range_highlights, key) {
2369 (Some(frame), Some(key)) => frame.backgrounds(key),
2370 _ => &[],
2371 }
2372 }
2373
2374 fn reveal_at(&self, key: Option<TextLeafKey>, start: usize, end: usize) -> Option<RevealAt> {
2377 self.reveal.as_ref()?.at(key, start, end)
2378 }
2379}
2380
2381impl PartialEq for NodeContext {
2382 fn eq(&self, other: &Self) -> bool {
2383 self.link_refs == other.link_refs && self.style == other.style
2384 }
2387}
2388
2389fn mark_highlight(mark: &TextMark, node_cx: &NodeContext, cx: &App) -> InlineHighlight {
2392 let mut highlight = HighlightStyle::default();
2393 if mark.bold {
2394 highlight.font_weight = Some(FontWeight::BOLD);
2395 }
2396 if mark.italic {
2397 highlight.font_style = Some(FontStyle::Italic);
2398 }
2399 if mark.strikethrough {
2400 highlight.strikethrough = Some(gpui::StrikethroughStyle {
2401 thickness: gpui::px(1.),
2402 ..Default::default()
2403 });
2404 }
2405 if mark.underline {
2406 highlight.underline = Some(gpui::UnderlineStyle {
2407 thickness: gpui::px(1.),
2408 ..Default::default()
2409 });
2410 }
2411 let mut font_family = None;
2412 if mark.code {
2413 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
2414 font_family = Some(cx.theme().tokens.typography.mono.clone());
2415 }
2416 if let Some(color) = mark.highlight {
2417 highlight.background_color = Some(color);
2418 }
2419 InlineHighlight {
2420 style: highlight,
2421 font_family,
2422 font_size_scale: mark.code.then_some(0.875),
2423 }
2424}
2425
2426impl Paragraph {
2427 fn inline_highlights(
2431 &self,
2432 node_cx: &NodeContext,
2433 cx: &App,
2434 ) -> Vec<(Range<usize>, InlineHighlight)> {
2435 let mut highlights = vec![];
2436 let mut offset = 0;
2437 for inline_node in &self.children {
2438 let node_highlights = inline_node
2439 .marks
2440 .iter()
2441 .map(|(range, mark)| {
2442 (
2443 (offset + range.start)..(offset + range.end),
2444 mark_highlight(mark, node_cx, cx),
2445 )
2446 })
2447 .collect::<Vec<_>>();
2448 highlights = combine_highlights(highlights, node_highlights);
2449 offset += inline_node.text.len();
2450 }
2451 highlights
2452 }
2453
2454 fn render(
2458 &self,
2459 fade_key: Option<TextLeafKey>,
2460 node_cx: &NodeContext,
2461 _window: &mut Window,
2462 cx: &mut App,
2463 ) -> AnyElement {
2464 let children = &self.children;
2465 let fades = node_cx.stream_fades(fade_key);
2466 let backgrounds = node_cx.range_backgrounds(fade_key);
2467
2468 if self.should_render_inline_flow() {
2469 return InlineFlow::new(
2470 leaf_element_id(fade_key),
2471 self.inline_flow_items(fade_key, fades, backgrounds, node_cx, cx),
2472 node_cx.link_click_handler.clone(),
2473 )
2474 .into_any_element();
2475 }
2476
2477 let has_image = children.iter().any(|child| child.image.is_some());
2478 if !has_image {
2481 let (text, highlights, mut links) = self.plain_render(node_cx, cx);
2482 if text.is_empty() {
2483 return div().into_any_element();
2484 }
2485 for (_, link_mark) in &mut links {
2486 if let Some(identifier) = link_mark.identifier.as_ref()
2487 && let Some(mark) = node_cx.link_refs.get(identifier)
2488 {
2489 *link_mark = mark.clone();
2490 }
2491 }
2492 let highlights = fade_highlights(highlights, &slice_fades(fades, 0, text.len()));
2493 let backgrounds = slice_backgrounds(backgrounds, 0, text.len());
2494 let reveal = node_cx.reveal_at(fade_key, 0, text.len());
2495 if let Ok(mut state) = self.state.lock() {
2496 state.set_text(text);
2497 }
2498 return Inline::new(
2499 self.state.clone(),
2500 links,
2501 highlights,
2502 node_cx.link_click_handler.clone(),
2503 )
2504 .range_backgrounds(backgrounds)
2505 .reveal(reveal)
2506 .into_any_element();
2507 }
2508
2509 let mut child_nodes: Vec<AnyElement> = vec![];
2510
2511 let mut text = String::new();
2512 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2513 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2514 let mut offset = 0;
2515 let mut consumed = 0;
2518
2519 for (ix, inline_node) in children.iter().enumerate() {
2520 let text_len = inline_node.text.len();
2521 text.push_str(&inline_node.text);
2522
2523 if let Some(image) = &inline_node.image {
2524 if text.len() > 0 {
2525 if let Ok(mut state) = inline_node.state.lock() {
2526 state.set_text(text.clone().into());
2527 }
2528 child_nodes.push(
2529 Inline::new(
2530 inline_node.state.clone(),
2531 links.clone(),
2532 fade_highlights(
2533 highlights.clone(),
2534 &slice_fades(fades, consumed, consumed + text.len()),
2535 ),
2536 node_cx.link_click_handler.clone(),
2537 )
2538 .range_backgrounds(slice_backgrounds(
2539 backgrounds,
2540 consumed,
2541 consumed + text.len(),
2542 ))
2543 .reveal(node_cx.reveal_at(fade_key, consumed, consumed + text.len()))
2544 .into_any_element(),
2545 );
2546 }
2547 let link_click_handler = node_cx.link_click_handler.clone();
2548 child_nodes.push(
2549 img(node_cx.image_source(image))
2550 .id(ix)
2551 .object_fit(ObjectFit::Contain)
2552 .max_w(relative(1.))
2553 .when_some(image.width, |this, width| this.w(width))
2554 .when_some(image.link.clone(), |this, link| {
2555 let link_click_handler = link_click_handler.clone();
2556 let aux_link = link.clone();
2557 let aux_link_click_handler = link_click_handler.clone();
2558 this.cursor_pointer()
2559 .on_click(move |event, window, cx| {
2560 crate::TextSelection::end(window, cx);
2561 cx.stop_propagation();
2562 handle_link_click(
2563 &link_click_handler,
2564 link.url.clone(),
2565 event.clone(),
2566 window,
2567 cx,
2568 );
2569 })
2570 .on_aux_click(move |event, window, cx| {
2571 crate::TextSelection::end(window, cx);
2572 cx.stop_propagation();
2573 handle_link_click(
2574 &aux_link_click_handler,
2575 aux_link.url.clone(),
2576 event.clone(),
2577 window,
2578 cx,
2579 );
2580 })
2581 })
2582 .into_any_element(),
2583 );
2584
2585 consumed += text.len();
2586 text.clear();
2587 links.clear();
2588 highlights.clear();
2589 offset = 0;
2590 } else {
2591 let mut node_highlights = vec![];
2592 for (range, style) in &inline_node.marks {
2593 let inner_range = (offset + range.start)..(offset + range.end);
2594 let mut highlight = mark_highlight(style, node_cx, cx);
2595
2596 if let Some(mut link_mark) = style.link.clone() {
2597 highlight.style.color = Some(node_cx.style.link());
2598 highlight.style.underline = Some(link_underline(&node_cx.style));
2599
2600 if let Some(identifier) = link_mark.identifier.as_ref() {
2602 if let Some(mark) = node_cx.link_refs.get(identifier) {
2603 link_mark = mark.clone();
2604 }
2605 }
2606
2607 links.push((inner_range.clone(), link_mark));
2608 }
2609
2610 node_highlights.push((inner_range, highlight));
2611 }
2612
2613 highlights = combine_highlights(highlights, node_highlights);
2614 offset += text_len;
2615 }
2616 }
2617
2618 if text.len() > 0 {
2620 let text_end = consumed + text.len();
2621 let highlights = fade_highlights(highlights, &slice_fades(fades, consumed, text_end));
2622 if let Ok(mut state) = self.state.lock() {
2623 state.set_text(text.into());
2624 }
2625 child_nodes.push(
2626 Inline::new(
2627 self.state.clone(),
2628 links,
2629 highlights,
2630 node_cx.link_click_handler.clone(),
2631 )
2632 .range_backgrounds(slice_backgrounds(backgrounds, consumed, text_end))
2633 .reveal(node_cx.reveal_at(fade_key, consumed, text_end))
2634 .into_any_element(),
2635 );
2636 }
2637
2638 if !has_image {
2642 return child_nodes
2643 .pop()
2644 .unwrap_or_else(|| div().into_any_element());
2645 }
2646
2647 div()
2648 .id(leaf_element_id(fade_key))
2649 .children(child_nodes)
2650 .into_any_element()
2651 }
2652
2653 fn should_render_inline_flow(&self) -> bool {
2654 let has_image = self.children.iter().any(|child| child.image.is_some());
2655 let has_text = self.children.iter().any(|child| !child.text.is_empty());
2656 self.children.iter().any(|child| child.custom.is_some())
2657 || (has_image && has_text)
2658 || self
2659 .children
2660 .iter()
2661 .any(|child| child.marks.iter().any(|(_, mark)| mark.code))
2662 }
2663
2664 fn inline_flow_items(
2665 &self,
2666 leaf_key: Option<TextLeafKey>,
2667 fades: &[(Range<usize>, f32)],
2668 backgrounds: &[(Range<usize>, Hsla)],
2669 node_cx: &NodeContext,
2670 cx: &mut App,
2671 ) -> Vec<InlineFlowItem> {
2672 let mut items = Vec::new();
2673 let mut text = String::new();
2674 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2675 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2676 let mut offset = 0;
2677 let mut consumed = 0;
2680
2681 for inline_node in &self.children {
2682 if let Some(node) = &inline_node.custom {
2683 if let Ok(mut state) = inline_node.state.lock() {
2684 state.set_text(text.clone().into());
2685 }
2686 if !text.is_empty() {
2687 let item_fades = slice_fades(fades, consumed, consumed + text.len());
2688 let item_backgrounds =
2689 slice_backgrounds(backgrounds, consumed, consumed + text.len());
2690 let item_reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
2691 consumed += text.len();
2692 items.push(InlineFlowItem::Text {
2693 state: inline_node.state.clone(),
2694 text: std::mem::take(&mut text).into(),
2695 links: std::mem::take(&mut links),
2696 highlights: fade_highlights(std::mem::take(&mut highlights), &item_fades),
2697 backgrounds: item_backgrounds,
2698 reveal: item_reveal,
2699 });
2700 }
2701 let mut object_style = HighlightStyle::default();
2702 let mut object_link = None;
2703 for (_, mark) in &inline_node.marks {
2704 object_style = object_style.highlight(mark_highlight(mark, node_cx, cx).style);
2705 if let Some(link) = &mark.link {
2706 object_link = Some(
2707 link.identifier
2708 .as_ref()
2709 .and_then(|id| node_cx.link_refs.get(id))
2710 .unwrap_or(link)
2711 .clone(),
2712 );
2713 object_style.color = Some(node_cx.style.link());
2714 object_style.underline = Some(link_underline(&node_cx.style));
2715 }
2716 }
2717 let rendered_node = node.clone();
2718 let extensions = node_cx.markdown_extensions.clone();
2719 items.push(InlineFlowItem::Object {
2720 text: node.shared_text(),
2721 accessibility_label: node.shared_accessibility_name(),
2722 id: node.source_range().map_or(items.len(), |range| range.start),
2723 renderer: Arc::new(move |context, window, cx| {
2724 extensions.render_inline(&rendered_node, context, window, cx)
2725 }),
2726 selected: inline_node.custom_selection.clone(),
2727 style: object_style,
2728 link: object_link,
2729 });
2730 consumed += inline_node.text.len();
2731 offset = 0;
2732 continue;
2733 }
2734 let text_len = inline_node.text.len();
2735 text.push_str(&inline_node.text);
2736
2737 if let Some(image) = &inline_node.image {
2738 if !text.is_empty() {
2739 if let Ok(mut state) = inline_node.state.lock() {
2740 state.set_text(text.clone().into());
2741 }
2742 items.push(InlineFlowItem::Text {
2743 state: inline_node.state.clone(),
2744 text: text.clone().into(),
2745 links: links.clone(),
2746 highlights: fade_highlights(
2747 highlights.clone(),
2748 &slice_fades(fades, consumed, consumed + text.len()),
2749 ),
2750 backgrounds: slice_backgrounds(
2751 backgrounds,
2752 consumed,
2753 consumed + text.len(),
2754 ),
2755 reveal: node_cx.reveal_at(leaf_key, consumed, consumed + text.len()),
2756 });
2757 }
2758
2759 items.push(InlineFlowItem::Image {
2760 source: node_cx.image_source(image),
2761 link: image.link.clone(),
2762 title: image.title(),
2763 width: image.width,
2764 height: image.height,
2765 });
2766
2767 consumed += text.len();
2768 text.clear();
2769 links.clear();
2770 highlights.clear();
2771 offset = 0;
2772 } else {
2773 let mut node_highlights = vec![];
2774 for (range, style) in &inline_node.marks {
2775 let inner_range = (offset + range.start)..(offset + range.end);
2776 let mut highlight = mark_highlight(style, node_cx, cx);
2777
2778 if let Some(mut link_mark) = style.link.clone() {
2779 highlight.style.color = Some(node_cx.style.link());
2780 highlight.style.underline = Some(link_underline(&node_cx.style));
2781
2782 if let Some(identifier) = link_mark.identifier.as_ref()
2783 && let Some(mark) = node_cx.link_refs.get(identifier)
2784 {
2785 link_mark = mark.clone();
2786 }
2787
2788 links.push((inner_range.clone(), link_mark));
2789 }
2790
2791 node_highlights.push((inner_range, highlight));
2792 }
2793
2794 highlights = combine_highlights(highlights, node_highlights);
2795 offset += text_len;
2796 }
2797 }
2798
2799 if !text.is_empty() {
2800 if let Ok(mut state) = self.state.lock() {
2801 state.set_text(text.clone().into());
2802 }
2803 let highlights = fade_highlights(
2804 highlights,
2805 &slice_fades(fades, consumed, consumed + text.len()),
2806 );
2807 let backgrounds = slice_backgrounds(backgrounds, consumed, consumed + text.len());
2808 let reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
2809 items.push(InlineFlowItem::Text {
2810 state: self.state.clone(),
2811 text: text.into(),
2812 links,
2813 highlights,
2814 backgrounds,
2815 reveal,
2816 });
2817 }
2818
2819 items
2820 }
2821}
2822
2823fn block_element_id(kind: &'static str, span: Option<Span>, ix: usize) -> ElementId {
2828 (kind, span.map_or(ix, |span| span.start)).into()
2829}
2830
2831fn leaf_element_id(fade_key: Option<TextLeafKey>) -> ElementId {
2836 fade_key.map_or_else(|| ElementId::from("p"), ElementId::from)
2837}
2838
2839fn link_underline(style: &TextViewStyle) -> gpui::UnderlineStyle {
2842 gpui::UnderlineStyle {
2843 thickness: px(1.),
2844 color: Some(style.link().opacity(0.4)),
2845 ..Default::default()
2846 }
2847}
2848
2849fn body_font_size(node_cx: &NodeContext, window: &Window) -> Pixels {
2856 if node_cx.body_font_size > Pixels::ZERO {
2857 node_cx.body_font_size
2858 } else {
2859 window.text_style().font_size.to_pixels(window.rem_size())
2860 }
2861}
2862
2863fn heading_metrics(level: u8) -> (f32, f32, f32) {
2868 match level {
2869 1 => (1.8, 1.25, 1.1),
2870 2 => (4. / 3., 1.35, 1.6),
2871 3 => (17. / 15., 1.35, 1.4),
2872 4 => (1., 1.35, 1.4),
2873 5 => (14. / 15., 1.35, 1.4),
2874 _ => (13. / 15., 1.35, 1.4),
2875 }
2876}
2877
2878const HEADING_BOTTOM_GAP: f32 = 0.35;
2880
2881fn heading_size(level: u8, node_cx: &NodeContext, window: &Window) -> Pixels {
2884 node_cx.style.heading(level).text.font_size.map_or_else(
2885 || body_font_size(node_cx, window) * heading_metrics(level).0,
2886 |size| size.to_pixels(window.rem_size()),
2887 )
2888}
2889
2890fn rule_space(body_font_size: Pixels) -> Pixels {
2892 body_font_size * 1.6
2893}
2894
2895fn block_gap(options: &NodeRenderOptions, style: &TextViewStyle) -> Rems {
2897 if options.in_list || options.is_last {
2898 rems(0.)
2899 } else {
2900 style.paragraph_gap()
2901 }
2902}
2903
2904fn gapped(block: AnyElement, gap: Rems) -> AnyElement {
2907 if gap.is_zero() {
2908 block
2909 } else {
2910 div().pb(gap).child(block).into_any_element()
2911 }
2912}
2913
2914fn slice_fades(
2916 fades: &[(Range<usize>, f32)],
2917 start: usize,
2918 end: usize,
2919) -> Vec<(Range<usize>, f32)> {
2920 slice_ranges(fades, start, end, |range, fade_out| (range, *fade_out))
2921}
2922
2923fn slice_backgrounds(
2926 backgrounds: &[(Range<usize>, Hsla)],
2927 start: usize,
2928 end: usize,
2929) -> Vec<(Range<usize>, Hsla)> {
2930 slice_ranges(backgrounds, start, end, |range, color| (range, *color))
2931}
2932
2933struct TableColumn {
2935 max: f32,
2937 min: f32,
2939 divider: f32,
2940}
2941
2942fn table_cell_padding_x(style: &TextViewStyle, rem_size: Pixels) -> f32 {
2945 let padding = &style.table_cell().padding;
2946 let side = |length: Option<DefiniteLength>| match length {
2947 Some(DefiniteLength::Absolute(length)) => f32::from(length.to_pixels(rem_size)),
2948 _ => CELL_PAD_PX / 2.,
2949 };
2950 side(padding.left) + side(padding.right)
2951}
2952
2953const CELL_PAD_PX: f32 = 24.0; const CELL_MIN_PX: f32 = 48.0;
2955const CELL_BORDER_PX: f32 = 1.0; fn measure_table_columns(
2962 table: &Table,
2963 col_count: usize,
2964 node_cx: &NodeContext,
2965 window: &mut Window,
2966 cx: &mut App,
2967) -> Vec<f32> {
2968 let text_styles = TableTextStyles::new(&node_cx.style, window);
2969 let rem_size = window.rem_size();
2970 let mono_family = cx.theme().tokens.typography.mono.clone();
2971 let inline_code = node_cx.style.inline_code_highlight();
2972 let cached = table.column_widths_cache.0.lock().ok().and_then(|cache| {
2973 cache
2974 .as_ref()
2975 .filter(|cached| {
2976 cached.widths.len() == col_count
2977 && cached.text_styles == text_styles
2978 && cached.rem_size == rem_size
2979 && cached.mono_family == mono_family
2980 && cached.inline_code == inline_code
2981 && cached
2982 .text_system
2983 .upgrade()
2984 .is_some_and(|system| Arc::ptr_eq(&system, window.text_system()))
2985 })
2986 .cloned()
2987 });
2988 let measured = cached.unwrap_or_else(|| {
2989 let (widths, custom_cells) =
2990 measure_table_text_columns(table, col_count, &text_styles, node_cx, window, cx);
2991 let measured = Arc::new(TableColumnWidths {
2992 text_system: Arc::downgrade(window.text_system()),
2993 text_styles: text_styles.clone(),
2994 rem_size,
2995 mono_family,
2996 inline_code,
2997 widths,
2998 custom_cells,
2999 });
3000 if let Ok(mut cache) = table.column_widths_cache.0.lock() {
3001 *cache = Some(measured.clone());
3002 }
3003 measured
3004 });
3005
3006 let mut col_w = measured.widths.clone();
3010 for &(row_ix, ix) in &measured.custom_cells {
3011 let cell = &table.children[row_ix].children[ix];
3012 let items = cell.children.inline_flow_items(None, &[], &[], node_cx, cx);
3013 let style = text_styles.row_refinement(row_ix).clone();
3014 let width = window.with_text_style(Some(style), |window| {
3015 super::inline_flow::intrinsic_width(&items, window, cx)
3016 });
3017 let border = if ix + 1 < col_count {
3018 CELL_BORDER_PX
3019 } else {
3020 0.
3021 };
3022 col_w[ix] = col_w[ix].max(f32::from(width) + CELL_PAD_PX + border);
3023 }
3024 col_w
3025}
3026
3027fn measure_table_text_columns(
3030 table: &Table,
3031 col_count: usize,
3032 text_styles: &TableTextStyles,
3033 node_cx: &NodeContext,
3034 window: &mut Window,
3035 cx: &mut App,
3036) -> (Vec<f32>, Vec<(usize, usize)>) {
3037 let mut col_w = vec![CELL_MIN_PX; col_count];
3038 let mut custom_cells = Vec::new();
3039 for (row_ix, row) in table.children.iter().enumerate() {
3040 let text_style = text_styles.row(row_ix);
3041 let font_size = text_style.font_size.to_pixels(window.rem_size());
3042 for (ix, cell) in row.children.iter().enumerate() {
3043 let Some(slot) = col_w.get_mut(ix) else {
3044 continue;
3045 };
3046 if cell
3047 .children
3048 .children
3049 .iter()
3050 .any(|node| node.custom.is_some())
3051 {
3052 custom_cells.push((row_ix, ix));
3053 continue;
3054 }
3055 let text = cell.children.text();
3056 let highlights = cell.children.inline_highlights(node_cx, cx);
3057 let mut w = 0.0_f32;
3058 let mut line_start = 0;
3059 for line in text.split('\n') {
3060 let start = line_start + (line.len() - line.trim_start().len());
3061 let line_end = line_start + line.len();
3062 line_start = line_end + 1;
3063 let line = line.trim();
3064 if line.is_empty() {
3065 continue;
3066 }
3067 let end = start + line.len();
3068 let line_highlights = highlights
3069 .iter()
3070 .filter_map(|(range, highlight)| {
3071 let clipped = range.start.max(start)..range.end.min(end);
3072 (clipped.start < clipped.end).then(|| {
3073 (
3074 clipped.start - start..clipped.end - start,
3075 highlight.clone(),
3076 )
3077 })
3078 })
3079 .collect::<Vec<_>>();
3080 let mut line_w = gpui::Pixels::ZERO;
3081 for (range, scale) in text_size_ranges(line.len(), &line_highlights) {
3082 let highlights = slice_ranges(
3083 &line_highlights,
3084 range.start,
3085 range.end,
3086 |range, highlight| (range, highlight.clone()),
3087 );
3088 if highlights.iter().any(|(_, h)| h.font_size_scale.is_some()) {
3089 line_w += px(crate::text::inline_flow::INLINE_CODE_PADDING * 2.);
3090 }
3091 let runs = text_runs(range.len(), text_style, &highlights);
3092 line_w += window
3093 .text_system()
3094 .layout_line(&line[range], font_size * scale, &runs, None)
3095 .width;
3096 }
3097 w = w.max(f32::from(line_w));
3098 }
3099 let border = if ix + 1 < col_count {
3102 CELL_BORDER_PX
3103 } else {
3104 0.
3105 };
3106 *slot = slot.max(w + CELL_PAD_PX + border);
3107 }
3108 }
3109 (col_w, custom_cells)
3110}
3111
3112impl Paragraph {
3113 fn to_markdown(&self) -> String {
3114 if self.children.iter().any(|node| node.custom.is_some()) {
3115 let mut source = MarkdownSource::default();
3116 for node in &self.children {
3117 if node.custom.is_some() {
3118 source.push_object(node);
3119 } else {
3120 source.push_text(&node.text, &node.marks, 0..node.text.len());
3121 if let Some(image) = &node.image {
3122 source.push_str(&image_markdown(image));
3123 }
3124 }
3125 }
3126 let mut text = source.finish();
3127 text.push_str("\n\n");
3128 return text;
3129 }
3130 let mut text = self
3131 .children
3132 .iter()
3133 .map(|text_node| {
3134 let mut text = text_node.text.to_string();
3135 for (range, style) in &text_node.marks {
3136 if style.bold {
3137 text = format!("**{}**", &text_node.text[range.clone()]);
3138 }
3139 if style.italic {
3140 text = format!("*{}*", &text_node.text[range.clone()]);
3141 }
3142 if style.strikethrough {
3143 text = format!("~~{}~~", &text_node.text[range.clone()]);
3144 }
3145 if style.code {
3146 text = format!("`{}`", &text_node.text[range.clone()]);
3147 }
3148 if style.highlight.is_some() {
3149 text = format!("=={}==", &text_node.text[range.clone()]);
3150 }
3151 if let Some(link) = &style.link {
3152 text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
3153 }
3154 }
3155
3156 if let Some(image) = &text_node.image {
3157 let alt = image.alt.clone().unwrap_or_default();
3158 let title = image
3159 .title
3160 .clone()
3161 .map_or(String::new(), |t| format!(" \"{}\"", t));
3162 text.push_str(&format!("", alt, image.url, title))
3163 }
3164
3165 text
3166 })
3167 .collect::<Vec<_>>()
3168 .join("");
3169
3170 text.push_str("\n\n");
3171 text
3172 }
3173}
3174
3175impl BlockNode {
3176 #[allow(dead_code)]
3180 pub(crate) fn to_markdown(&self) -> String {
3181 match self {
3182 BlockNode::Root { children, .. } => children
3183 .iter()
3184 .map(|child| child.to_markdown())
3185 .collect::<Vec<_>>()
3186 .join("\n\n"),
3187 BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
3188 BlockNode::Heading {
3189 level, children, ..
3190 } => {
3191 let hashes = "#".repeat(*level as usize);
3192 format!("{} {}", hashes, children.to_markdown())
3193 }
3194 BlockNode::Blockquote { children, .. } => {
3195 let content = children
3196 .iter()
3197 .map(|child| child.to_markdown())
3198 .collect::<Vec<_>>()
3199 .join("\n\n");
3200
3201 content
3202 .lines()
3203 .map(|line| format!("> {}", line))
3204 .collect::<Vec<_>>()
3205 .join("\n")
3206 }
3207 BlockNode::List {
3208 children,
3209 ordered,
3210 start,
3211 ..
3212 } => children
3213 .iter()
3214 .enumerate()
3215 .map(|(i, child)| {
3216 let prefix = if *ordered {
3217 format!("{}. ", ordered_list_ordinal(*start, i))
3218 } else {
3219 "- ".to_string()
3220 };
3221 format!("{}{}", prefix, child.to_markdown())
3222 })
3223 .collect::<Vec<_>>()
3224 .join("\n"),
3225 BlockNode::ListItem {
3226 children, checked, ..
3227 } => {
3228 let checkbox = if let Some(checked) = checked {
3229 if *checked { "[x] " } else { "[ ] " }
3230 } else {
3231 ""
3232 };
3233 format!(
3234 "{}{}",
3235 checkbox,
3236 children
3237 .iter()
3238 .map(|child| child.to_markdown())
3239 .collect::<Vec<_>>()
3240 .join("\n")
3241 )
3242 }
3243 BlockNode::CodeBlock(code_block) => {
3244 format!(
3245 "```{}\n{}\n```",
3246 code_block.lang.clone().unwrap_or_default(),
3247 code_block.code()
3248 )
3249 }
3250 BlockNode::Table(table) => table.to_markdown(),
3251 BlockNode::Break { html, .. } => {
3252 if *html {
3253 "<br>".to_string()
3254 } else {
3255 "\n".to_string()
3256 }
3257 }
3258 BlockNode::HorizontalRule { .. } => "---".to_string(),
3259 BlockNode::Custom { node, .. } => node.to_markdown(),
3260 BlockNode::Definition {
3261 identifier,
3262 url,
3263 title,
3264 ..
3265 } => {
3266 if let Some(title) = title {
3267 format!("[{}]: {} \"{}\"", identifier, url, title)
3268 } else {
3269 format!("[{}]: {}", identifier, url)
3270 }
3271 }
3272 BlockNode::Unknown { .. } => "".to_string(),
3273 }
3274 .trim()
3275 .to_string()
3276 }
3277}
3278
3279impl BlockNode {
3280 fn render_list_item_row(
3281 content: AnyElement,
3282 ix: usize,
3283 options: NodeRenderOptions,
3284 checked: Option<bool>,
3285 style: &TextViewStyle,
3286 line_height: Pixels,
3287 indent: Pixels,
3288 ) -> Div {
3289 h_flex()
3290 .w_full()
3291 .min_w_0()
3292 .relative()
3293 .items_start()
3294 .content_start()
3295 .when(!options.todo && checked.is_none(), |this| {
3296 this.child(
3300 div()
3301 .flex_none()
3302 .min_w(indent)
3303 .pr(indent * 0.3)
3304 .text_right()
3305 .text_color(style.muted_foreground())
3306 .child(
3307 list_item_prefix(
3308 ix,
3309 options.list_start,
3310 options.ordered,
3311 options.depth,
3312 )
3313 .trim_end()
3314 .to_string(),
3315 ),
3316 )
3317 })
3318 .when_some(checked, |this, checked| {
3319 let check_svg = if style.is_dark() {
3321 CHECK_SVG_DARK
3322 } else {
3323 CHECK_SVG_LIGHT
3324 };
3325 this.child(
3326 div()
3327 .flex()
3328 .mr_2()
3329 .h(line_height)
3330 .flex_none()
3331 .items_center()
3332 .justify_center()
3333 .child(
3334 div()
3335 .flex()
3336 .size(rems(0.9375))
3337 .items_center()
3338 .justify_center()
3339 .rounded(px(4.))
3340 .border(px(1.5))
3341 .border_color(style.muted_foreground())
3342 .when(checked, |this| {
3343 this.bg(style.link()).border_color(style.link()).child(
3344 img(Arc::new(Image::from_bytes(
3345 ImageFormat::Svg,
3346 check_svg.to_vec(),
3347 )))
3348 .size(rems(0.75)),
3349 )
3350 }),
3351 ),
3352 )
3353 })
3354 .child(
3355 div()
3356 .flex_1()
3357 .min_w_0()
3358 .overflow_hidden()
3359 .when(checked == Some(true), |this| {
3361 this.text_color(style.muted_foreground())
3362 })
3363 .child(content),
3364 )
3365 }
3366
3367 fn render_list_item(
3368 item: &BlockNode,
3369 ix: usize,
3370 options: NodeRenderOptions,
3371 node_cx: &NodeContext,
3372 window: &mut Window,
3373 cx: &mut App,
3374 ) -> AnyElement {
3375 match item {
3376 BlockNode::ListItem {
3377 children,
3378 spread,
3379 checked,
3380 ..
3381 } => {
3382 let body = body_font_size(node_cx, window);
3383 let indent = body * 1.4;
3384 div()
3385 .w_full()
3386 .min_w_0()
3387 .when(ix > 0, |this| this.pt(body * 0.25))
3389 .when(*spread, |this| this.child(div()))
3390 .children({
3391 let mut items: Vec<Div> = Vec::with_capacity(children.len());
3392
3393 for (child_ix, child) in children.iter().enumerate() {
3394 match child {
3395 BlockNode::Paragraph { .. } => {
3396 let last_not_list = child_ix > 0
3397 && !matches!(
3398 children[child_ix - 1],
3399 BlockNode::List { .. }
3400 );
3401
3402 let text = child.render_block(
3403 NodeRenderOptions {
3404 depth: options.depth + 1,
3405 todo: checked.is_some(),
3406 is_last: true,
3407 ..options
3408 },
3409 node_cx,
3410 window,
3411 cx,
3412 );
3413
3414 if last_not_list {
3418 if let Some(preceding_row) = items.pop() {
3419 items.push(
3420 div().child(preceding_row).child(
3421 div()
3422 .w_full()
3423 .pl(indent)
3424 .overflow_hidden()
3425 .child(text),
3426 ),
3427 );
3428 continue;
3429 }
3430 }
3431
3432 items.push(Self::render_list_item_row(
3433 text,
3434 ix,
3435 options,
3436 *checked,
3437 &node_cx.style,
3438 window.line_height(),
3439 indent,
3440 ));
3441 }
3442 BlockNode::List { .. } => {
3443 items.push(div().ml(indent).child(child.render_block(
3444 NodeRenderOptions {
3445 depth: options.depth + 1,
3446 todo: checked.is_some(),
3447 is_last: true,
3448 ..options
3449 },
3450 node_cx,
3451 window,
3452 cx,
3453 )));
3454 }
3455 BlockNode::Root { .. }
3456 | BlockNode::Heading { .. }
3457 | BlockNode::Blockquote { .. }
3458 | BlockNode::CodeBlock(_)
3459 | BlockNode::Custom { .. }
3460 | BlockNode::Table(_)
3461 | BlockNode::HorizontalRule { .. } => {
3462 let block = child.render_block(
3463 NodeRenderOptions {
3464 depth: options.depth + 1,
3465 todo: checked.is_some(),
3466 is_last: true,
3467 ..options
3468 },
3469 node_cx,
3470 window,
3471 cx,
3472 );
3473
3474 if child_ix == 0 {
3475 items.push(Self::render_list_item_row(
3476 block,
3477 ix,
3478 options,
3479 *checked,
3480 &node_cx.style,
3481 window.line_height(),
3482 indent,
3483 ));
3484 } else {
3485 items.push(
3489 div()
3490 .w_full()
3491 .min_w_0()
3492 .pl(indent)
3493 .overflow_hidden()
3494 .child(block),
3495 );
3496 }
3497 }
3498 BlockNode::ListItem { .. }
3499 | BlockNode::Break { .. }
3500 | BlockNode::Definition { .. }
3501 | BlockNode::Unknown => {}
3502 }
3503 }
3504 items
3505 })
3506 .into_any_element()
3507 }
3508 _ => div().into_any_element(),
3509 }
3510 }
3511
3512 fn render_table(
3534 item: &BlockNode,
3535 options: &NodeRenderOptions,
3536 node_cx: &NodeContext,
3537 text_size: Pixels,
3538 window: &mut Window,
3539 cx: &mut App,
3540 ) -> impl IntoElement {
3541 const CELL_WRAP_MAX_LINES: f32 = 2.0;
3547 const CELL_WRAP_MIN_PX: f32 = 160.0;
3548 const CELL_WRAP_MAX_PX: f32 = 480.0;
3549 const TABLE_BORDER_PX: f32 = 2.0; let table = match item {
3552 BlockNode::Table(table) => table,
3553 _ => return div().into_any_element(),
3554 };
3555
3556 let style = &node_cx.style;
3557 let col_count = table
3558 .children
3559 .iter()
3560 .map(|row| row.children.len())
3561 .max()
3562 .unwrap_or(0);
3563 let col_w = measure_table_columns(table, col_count, node_cx, window, cx);
3564 let scroll = matches!(style.table().overflow.x, Some(Overflow::Scroll));
3565 let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
3566 let rem_size = window.rem_size();
3567 let wrap_min_px = f32::from(rems(4.).to_pixels(rem_size));
3568 let padding_delta = table_cell_padding_x(style, rem_size) - CELL_PAD_PX;
3570 let scale_factor = window.scale_factor();
3574 let device_ceil = |width: f32| (width * scale_factor).ceil() / scale_factor;
3575 let columns: Vec<TableColumn> = col_w
3576 .iter()
3577 .enumerate()
3578 .map(|(ix, &w)| {
3579 let w = w + padding_delta;
3580 let floor = if scroll && nowrap {
3581 w
3582 } else if scroll {
3583 (w / CELL_WRAP_MAX_LINES)
3584 .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
3585 .min(w)
3586 } else {
3587 w.min(wrap_min_px)
3588 };
3589 let divider = if ix + 1 < col_count {
3590 CELL_BORDER_PX
3591 } else {
3592 0.
3593 };
3594 let max = device_ceil(w - divider);
3595 TableColumn {
3596 min: device_ceil(floor - divider).min(max),
3597 max,
3598 divider,
3599 }
3600 })
3601 .collect();
3602
3603 let rows = Self::render_table_rows(table, &columns, node_cx, window, cx);
3604 let frame = StyleRefinement::default()
3605 .text_size(text_size)
3606 .bg(style
3607 .table_background()
3608 .unwrap_or(cx.theme().tokens.colors.surface))
3609 .border_1()
3610 .border_color(style.border())
3611 .refine_style(style.table());
3612 let body = if scroll {
3613 let scroll_handle = window
3614 .use_keyed_state(
3615 block_element_id("table-scroll", table.span, options.ix),
3616 cx,
3617 |_, _| ScrollHandle::default(),
3618 )
3619 .read(cx)
3620 .clone();
3621 let min_total_w = columns
3622 .iter()
3623 .map(|column| column.min + column.divider)
3624 .sum::<f32>()
3625 + TABLE_BORDER_PX;
3626 horizontal_scroll_area(
3636 block_element_id("table", table.span, options.ix),
3637 &scroll_handle,
3638 &frame,
3639 div().min_w_full().w(px(min_total_w)).children(rows),
3645 )
3646 .into_any_element()
3647 } else {
3648 div()
3649 .w_full()
3650 .overflow_hidden()
3651 .refine_style(&frame)
3652 .children(rows)
3653 .into_any_element()
3654 };
3655
3656 div()
3657 .pb(block_gap(options, style))
3658 .w_full()
3659 .child(body)
3660 .children(node_cx.table_actions.clone().map(|f| {
3667 div()
3668 .id(block_element_id("table-actions", table.span, options.ix))
3669 .mt_1()
3670 .child(f(&table.cached_table_data(), window, cx))
3671 }))
3672 .into_any_element()
3673 }
3674
3675 fn table_row_corner_radii(style: &TextViewStyle, window: &Window) -> [Option<Pixels>; 4] {
3687 let rem_size = window.rem_size();
3688 let inset = |radius: Option<gpui::AbsoluteLength>| {
3689 radius.map(|radius| (radius.to_pixels(rem_size) - px(1.)).max(px(0.)))
3690 };
3691 let radii = &style.table().corner_radii;
3692 [
3693 inset(radii.top_left),
3694 inset(radii.top_right),
3695 inset(radii.bottom_left),
3696 inset(radii.bottom_right),
3697 ]
3698 }
3699
3700 fn render_table_rows(
3714 table: &Table,
3715 columns: &[TableColumn],
3716 node_cx: &NodeContext,
3717 window: &mut Window,
3718 cx: &mut App,
3719 ) -> Vec<AnyElement> {
3720 const CELL_GROW: f32 = 1e6;
3726 const SLACK_GROW: f32 = 1.0;
3727
3728 let style = &node_cx.style;
3729 let col_count = columns.len();
3730 let total_w = columns.iter().map(|column| column.max).sum::<f32>().max(1.);
3731 let mut slack_style = StyleRefinement::default();
3734 slack_style.background = style.table_cell().background.clone();
3735 slack_style.border_color = style.table_cell().border_color;
3736 let slack = |grow: f32| {
3737 div()
3738 .flex_basis(px(0.))
3739 .flex_grow(grow)
3740 .flex_shrink(0.)
3741 .border_color(style.border())
3742 .refine_style(&slack_style)
3743 };
3744
3745 let row_count = table.children.len();
3746 let [top_left, top_right, bottom_left, bottom_right] =
3747 Self::table_row_corner_radii(style, window);
3748 let mut rows = Vec::with_capacity(row_count);
3749 let mut cell_ordinal = 0;
3750 for (row_ix, row) in table.children.iter().enumerate() {
3751 let mut cells = Vec::with_capacity(col_count * 2);
3752 for (ix, column) in columns.iter().enumerate() {
3753 let is_last_col = ix + 1 == col_count;
3754 let align = table.column_align(ix);
3755 let content = row.children.get(ix).map(|cell| {
3756 let fade_key = table
3757 .span
3758 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
3759 cell_ordinal += 1;
3760 cell.children.render(fade_key, node_cx, window, cx)
3761 });
3762
3763 let grow = SLACK_GROW * column.max / total_w;
3764 let (lead, trail) = match align {
3765 ColumnumnAlign::Left => (0., grow),
3766 ColumnumnAlign::Center => (grow / 2., grow / 2.),
3767 ColumnumnAlign::Right => (grow, 0.),
3768 };
3769 if lead > 0. {
3770 cells.push(slack(lead).into_any_element());
3771 }
3772 cells.push(
3773 div()
3774 .flex_basis(px(0.))
3775 .flex_grow(CELL_GROW)
3776 .min_w(px(column.min))
3777 .max_w(px(column.max))
3778 .overflow_hidden()
3779 .when(align == ColumnumnAlign::Center, |this| this.text_center())
3780 .when(align == ColumnumnAlign::Right, |this| this.text_right())
3781 .px_3()
3782 .py(px(7.))
3783 .refine_style(&style.table_cell())
3784 .children(content)
3785 .into_any_element(),
3786 );
3787 if trail > 0. || !is_last_col {
3788 cells.push(
3789 slack(trail)
3790 .when(!is_last_col, |this| this.border_r_1())
3791 .into_any_element(),
3792 );
3793 }
3794 }
3795 rows.push(
3796 div()
3797 .w_full()
3798 .when(row_ix < row_count - 1, |this| this.border_b_1())
3799 .border_color(style.border())
3800 .flex()
3801 .flex_row()
3802 .when(row_ix == 0, |this| {
3806 this.bg(style.code_background())
3807 .text_color(style.foreground())
3808 .when_some(top_left, |this, radius| this.rounded_tl(radius))
3809 .when_some(top_right, |this, radius| this.rounded_tr(radius))
3810 .refine_style(&style.table_head())
3811 })
3812 .when(row_ix + 1 == row_count, |this| {
3813 this.when_some(bottom_left, |this, radius| this.rounded_bl(radius))
3814 .when_some(bottom_right, |this, radius| this.rounded_br(radius))
3815 })
3816 .children(cells)
3817 .into_any_element(),
3818 );
3819 }
3820 rows
3821 }
3822
3823 pub(crate) fn render_block(
3824 &self,
3825 options: NodeRenderOptions,
3826 node_cx: &NodeContext,
3827 window: &mut Window,
3828 cx: &mut App,
3829 ) -> AnyElement {
3830 let mb = block_gap(&options, &node_cx.style);
3831
3832 match self {
3833 BlockNode::Root { children, .. } => div()
3834 .children(children.into_iter().enumerate().map(move |(ix, node)| {
3835 let (is_first, prev) = match flow_position(children, ix) {
3839 (true, _) => (options.is_first, options.prev),
3840 (false, prev) => (false, prev),
3841 };
3842 node.render_block(
3843 NodeRenderOptions {
3844 ix,
3845 is_first,
3846 prev,
3847 is_last: options.is_last && ix + 1 == children.len(),
3848 ..options
3849 },
3850 node_cx,
3851 window,
3852 cx,
3853 )
3854 }))
3855 .into_any_element(),
3856 BlockNode::Paragraph(paragraph) => gapped(
3857 paragraph.render(
3858 paragraph.span.map(|span| TextLeafKey::block(span.start)),
3859 node_cx,
3860 window,
3861 cx,
3862 ),
3863 mb,
3864 ),
3865 BlockNode::Heading {
3866 level,
3867 children,
3868 span,
3869 } => {
3870 let (_, line_height, section_gap) = heading_metrics(*level);
3871 let refinement = node_cx.style.heading(*level);
3872 let text_size = heading_size(*level, node_cx, window);
3873 let top = if options.is_first || options.depth > 0 {
3879 px(0.)
3880 } else {
3881 match options.prev {
3882 PrevBlock::Heading(_) => text_size * 0.25,
3883 PrevBlock::Rule => (text_size * section_gap
3884 - rule_space(body_font_size(node_cx, window)))
3885 .max(px(0.)),
3886 PrevBlock::Other => (text_size * section_gap
3887 - node_cx.style.paragraph_gap().to_pixels(window.rem_size()))
3888 .max(px(0.)),
3889 }
3890 };
3891 let bottom = if options.is_last {
3892 px(0.)
3893 } else {
3894 text_size * HEADING_BOTTOM_GAP
3895 };
3896
3897 div()
3898 .pt(top)
3899 .pb(bottom)
3900 .whitespace_normal()
3901 .text_size(text_size)
3902 .line_height(relative(line_height))
3903 .font_weight(FontWeight::SEMIBOLD)
3904 .when(*level >= 6, |this| {
3905 this.text_color(node_cx.style.muted_foreground())
3906 })
3907 .refine_style(&refinement)
3908 .child(children.render(
3909 span.map(|span| TextLeafKey::block(span.start)),
3910 node_cx,
3911 window,
3912 cx,
3913 ))
3914 .into_any_element()
3915 }
3916 BlockNode::Blockquote { children, .. } => gapped(
3917 div()
3918 .w_full()
3919 .text_color(node_cx.style.muted_foreground())
3920 .border_l_2()
3921 .border_color(node_cx.style.border())
3922 .pl(body_font_size(node_cx, window) * 0.9)
3923 .children({
3924 let children_len = children.len();
3925 children.into_iter().enumerate().map(move |(index, c)| {
3926 let is_last = index == children_len - 1;
3927 let (is_first, prev) = flow_position(children, index);
3928 c.render_block(
3929 NodeRenderOptions {
3930 is_first,
3931 prev,
3932 ..options.is_last(is_last)
3933 },
3934 node_cx,
3935 window,
3936 cx,
3937 )
3938 })
3939 })
3940 .into_any_element(),
3941 mb,
3942 ),
3943 BlockNode::List {
3944 children,
3945 ordered,
3946 start,
3947 ..
3948 } => div()
3949 .w_full()
3950 .min_w_0()
3951 .pb(mb)
3952 .children({
3953 let mut items = Vec::with_capacity(children.len());
3954 let mut item_index = 0;
3955 for (ix, item) in children.into_iter().enumerate() {
3956 let is_item = item.is_list_item();
3957
3958 items.push(Self::render_list_item(
3959 item,
3960 item_index,
3961 NodeRenderOptions {
3962 ix,
3963 ordered: *ordered,
3964 list_start: *start,
3965 ..options
3966 },
3967 node_cx,
3968 window,
3969 cx,
3970 ));
3971
3972 if is_item {
3973 item_index += 1;
3974 }
3975 }
3976 items
3977 })
3978 .into_any_element(),
3979 BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
3980 BlockNode::Custom { node, state } => {
3981 let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
3982 Some(rendered) => rendered,
3983 None => div().child(node.as_text().to_string()).into_any_element(),
3984 };
3985
3986 div()
3987 .pb(mb)
3988 .child(CustomBlockElement {
3989 content: inner,
3990 state: state.clone(),
3991 })
3992 .into_any_element()
3993 }
3994 BlockNode::Table { .. } => {
3995 let text_size = body_font_size(node_cx, window) * (14. / 15.);
4000 window.with_text_style(
4001 Some(TextStyleRefinement {
4002 font_size: Some(text_size.into()),
4003 ..Default::default()
4004 }),
4005 |window| {
4006 Self::render_table(self, &options, node_cx, text_size, window, cx)
4007 .into_any_element()
4008 },
4009 )
4010 }
4011 BlockNode::HorizontalRule { .. } => {
4012 let space = rule_space(body_font_size(node_cx, window));
4016 let top = if options.is_first || options.depth > 0 {
4017 px(0.)
4018 } else {
4019 let above = match options.prev {
4020 PrevBlock::Rule => space,
4021 PrevBlock::Heading(level) => {
4022 heading_size(level, node_cx, window) * HEADING_BOTTOM_GAP
4023 }
4024 PrevBlock::Other => {
4025 node_cx.style.paragraph_gap().to_pixels(window.rem_size())
4026 }
4027 };
4028 (space - above).max(px(0.))
4029 };
4030 div()
4031 .pt(top)
4032 .when(!options.is_last, |this| this.pb(space))
4033 .child(div().bg(node_cx.style.border()).h(px(1.)))
4034 .into_any_element()
4035 }
4036 BlockNode::Break { .. } => div().into_any_element(),
4037 BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
4038 _ => {
4039 if cfg!(debug_assertions) {
4040 tracing::warn!("unknown implementation: {:?}", self);
4041 }
4042
4043 div().into_any_element()
4044 }
4045 }
4046 }
4047}
4048
4049#[cfg(test)]
4050mod tests {
4051 use super::*;
4052
4053 #[test]
4054 fn block_plugin_selection_copies_both_formats_and_clears_synchronously() {
4055 let source = "$$\nx\n$$";
4056 let mut node = MarkdownNode::new("math", ()).text("x").markdown(source);
4057 node.set_span(Some(Span {
4058 start: 0,
4059 end: source.len(),
4060 }));
4061 let state = BlockState::default();
4062 let same_content = BlockNode::Custom {
4063 node: node.clone(),
4064 state: BlockState::default(),
4065 };
4066 let block = BlockNode::Custom {
4067 node,
4068 state: state.clone(),
4069 };
4070 let document = crate::text::document::ParsedDocument {
4071 source: source.into(),
4072 blocks: Arc::new(vec![block.clone()]),
4073 };
4074 assert!(!block.has_selection());
4075 state.set_selection(Some(BlockSelection::Full));
4076 assert_eq!(block, same_content);
4078 for blocks in [None, Some(0..=0)] {
4079 assert_eq!(
4080 document.selected_text(SelectionFormat::Plain, blocks.clone()),
4081 "x\n"
4082 );
4083 assert_eq!(
4084 document.selected_text(SelectionFormat::Source, blocks),
4085 source
4086 );
4087 }
4088 assert_eq!(document.selected_source_range(), Some(0..source.len()));
4089
4090 block.clear_selection();
4092 assert!(!document.blocks[0].has_selection());
4093 assert_eq!(
4094 document.selected_text(SelectionFormat::Plain, Some(0..=0)),
4095 ""
4096 );
4097 assert_eq!(document.selected_source_range(), None);
4098 }
4099
4100 #[test]
4101 fn block_plugin_empty_endpoint_suppresses_virtualized_copy_fallback() {
4102 let state = BlockState::default();
4103 let block = BlockNode::Custom {
4104 node: MarkdownNode::new("math", ())
4105 .text("formula")
4106 .markdown("$$formula$$"),
4107 state: state.clone(),
4108 };
4109 let after = CodeBlock::from_code("after", None::<SharedString>);
4110 after.set_selection(0..5);
4111 let document = crate::text::document::ParsedDocument {
4112 blocks: Arc::new(vec![block, BlockNode::CodeBlock(after)]),
4113 ..Default::default()
4114 };
4115 assert!(
4116 document
4117 .selected_text(SelectionFormat::Plain, Some(0..=1))
4118 .contains("formula")
4119 );
4120 state.set_selection(Some(BlockSelection::Empty));
4121 assert!(document.blocks[0].has_selection());
4122 for format in [SelectionFormat::Plain, SelectionFormat::Source] {
4123 let text = document.selected_text(format, Some(0..=1));
4124 assert!(text.contains("after"));
4125 assert!(!text.contains("formula"));
4126 }
4127 }
4128
4129 #[test]
4130 fn block_plugin_geometry_selects_whole_blocks_in_both_drag_directions() {
4131 let bounds = Bounds::new(gpui::point(px(10.), px(20.)), gpui::size(px(100.), px(40.)));
4132 let inside = bounds.center();
4133 let above = gpui::point(px(0.), px(0.));
4134 let below = gpui::point(px(0.), px(80.));
4135 for (start, end) in [(above, inside), (inside, below), (above, below)] {
4136 assert!(custom_block_is_selected(bounds, start, end));
4137 assert!(custom_block_is_selected(bounds, end, start));
4138 }
4139 assert!(!custom_block_is_selected(bounds, inside, inside));
4140 for (start, end) in [
4143 (inside, inside + gpui::point(px(1.), px(0.))),
4144 (
4145 bounds.origin,
4146 bounds.bottom_right() - gpui::point(px(1.), px(1.)),
4147 ),
4148 ] {
4149 assert!(!custom_block_is_selected(bounds, start, end));
4150 assert!(!custom_block_is_selected(bounds, end, start));
4151 }
4152 assert!(!custom_block_is_selected(
4153 bounds,
4154 gpui::point(px(150.), px(30.)),
4155 gpui::point(px(160.), px(40.))
4156 ));
4157 }
4158
4159 #[test]
4160 fn selected_inline_objects_coalesce_surrounding_emphasis() {
4161 for (object_mark, expected) in [
4162 (TextMark::default().italic(), "*fore $x$ aft*"),
4163 (TextMark::default().italic().bold(), "*fore **$x$** aft*"),
4164 ] {
4165 let italic = TextMark::default().italic();
4166 let before = InlineNode::new("before ").marks(vec![(0..7, italic.clone())]);
4167 let formula =
4168 InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"))
4169 .marks(vec![(0..1, object_mark)]);
4170 let after = InlineNode::new(" after").marks(vec![(0..6, italic)]);
4171 {
4172 let mut state = formula.state.lock().unwrap();
4173 state.text = "before ".into();
4174 state.selection = Some((2..7).into());
4175 }
4176 *formula.custom_selection.lock().unwrap() = true;
4177 let paragraph = Paragraph {
4178 children: vec![before, formula, after],
4179 ..Default::default()
4180 };
4181 {
4182 let mut state = paragraph.state.lock().unwrap();
4183 state.text = " after".into();
4184 state.selection = Some((0..4).into());
4185 }
4186 assert_eq!(paragraph.selected_source(), expected);
4187 }
4188 }
4189
4190 #[test]
4191 fn consecutive_inline_objects_copy_atomically_without_neighboring_text() {
4192 let first =
4193 InlineNode::custom(MarkdownNode::new("math", ()).text("甲²").markdown("$甲^2$"));
4194 let second = InlineNode::custom(MarkdownNode::new("math", ()).text("b").markdown("$b$"));
4195 *first.custom_selection.lock().unwrap() = true;
4196 *second.custom_selection.lock().unwrap() = true;
4197 let paragraph = Paragraph {
4198 children: vec![first, second],
4199 ..Default::default()
4200 };
4201 assert_eq!(paragraph.selected_text(), "甲²b");
4202 assert_eq!(paragraph.selected_source(), "$甲^2$$b$");
4203 assert!(paragraph.has_selection());
4204 paragraph.clear_selection();
4205 assert_eq!(paragraph.selected_text(), "");
4206 assert_eq!(paragraph.selected_source(), "");
4207 assert!(!paragraph.has_selection());
4208 }
4209
4210 #[test]
4211 fn selected_inline_object_interleaves_runs_and_preserves_enclosing_mark() {
4212 let before = InlineNode::new("中文 ");
4213 let formula =
4214 InlineNode::custom(MarkdownNode::new("math", ()).text("x²").markdown("$x^2$"))
4215 .marks(vec![(0..3, TextMark::default().bold())]);
4216 let after = InlineNode::new(" English");
4217 {
4218 let mut preceding = formula.state.lock().unwrap();
4219 preceding.text = "中文 ".into();
4220 preceding.selection = Some((3..7).into());
4221 }
4222 *formula.custom_selection.lock().unwrap() = true;
4223 let paragraph = Paragraph {
4224 children: vec![before, formula, after],
4225 ..Default::default()
4226 };
4227 {
4228 let mut trailing = paragraph.state.lock().unwrap();
4229 trailing.text = " English".into();
4230 trailing.selection = Some((0..4).into());
4231 }
4232 assert_eq!(paragraph.selected_text(), "文 x² Eng");
4233 assert_eq!(paragraph.selected_source(), "文 **$x^2$** Eng");
4234 }
4235
4236 #[test]
4237 fn custom_inline_inherits_marks_and_resolves_link_references() {
4238 use gpui::{Empty, TestApp};
4239 let mut app = TestApp::new();
4240 let mut window = app.open_window(|_, _| Empty);
4241 window.update(|_, _, cx| {
4242 cx.set_global(crate::Theme::default());
4243 let mut node_cx = NodeContext::default();
4244 let mut mark = TextMark::default().bold();
4245 mark.italic = true;
4246 mark.strikethrough = true;
4247 mark.link = Some(LinkMark {
4248 identifier: Some("ref".into()),
4249 ..Default::default()
4250 });
4251 node_cx.link_refs.insert(
4252 "ref".into(),
4253 LinkMark {
4254 url: "https://example.com".into(),
4255 ..Default::default()
4256 },
4257 );
4258 let paragraph = Paragraph {
4259 children: vec![
4260 InlineNode::custom(MarkdownNode::new("test", ()).text("x"))
4261 .marks(vec![(0..1, mark)]),
4262 ],
4263 ..Default::default()
4264 };
4265 let items = paragraph.inline_flow_items(None, &[], &[], &node_cx, cx);
4266 let InlineFlowItem::Object { style, link, .. } = &items[0] else {
4267 panic!()
4268 };
4269 assert_eq!(style.font_weight, Some(FontWeight::BOLD));
4270 assert_eq!(style.font_style, Some(FontStyle::Italic));
4271 assert!(style.strikethrough.is_some());
4272 assert_eq!(link.as_ref().unwrap().url.as_ref(), "https://example.com");
4273 });
4274 }
4275
4276 #[test]
4277 fn table_column_uses_prepared_inline_metrics_after_resource_update() {
4278 use crate::text::InlineElement;
4279 use crate::text::inline::test_draw::in_prepaint;
4280 use gpui::{Image, ImageFormat, TestApp};
4281 let mut app = TestApp::new();
4282 let width = Arc::new(std::sync::atomic::AtomicUsize::new(400));
4283 let render_width = width.clone();
4284 let mut node_cx = NodeContext::default();
4285 node_cx.markdown_extensions = Arc::new(MarkdownExtensions::default().plugin(
4286 crate::text::markdown_ext::TestInlinePlugin::new("test").render_with(
4287 move |_, _, _, _| {
4288 Some(
4289 InlineElement::new(
4290 gpui::img(Arc::new(Image::from_bytes(
4291 ImageFormat::Svg,
4292 b"<svg/>".to_vec(),
4293 )))
4294 .w(px(
4295 render_width.load(std::sync::atomic::Ordering::Relaxed) as f32
4296 ))
4297 .h(px(20.)),
4298 )
4299 .with_baseline(px(15.)),
4300 )
4301 },
4302 ),
4303 ));
4304 let table = table_of(
4305 vec![
4306 vec![plain_cell("ordinary text")],
4307 vec![TableCell {
4308 children: Paragraph {
4309 children: vec![InlineNode::custom(MarkdownNode::new("test", ()).text("x"))],
4310 ..Default::default()
4311 },
4312 width: None,
4313 }],
4314 ],
4315 vec![],
4316 );
4317 in_prepaint(&mut app, move |window, cx| {
4318 let mut cached = None;
4319 for expected in [400, 600, 10, 400] {
4320 width.store(expected, std::sync::atomic::Ordering::Relaxed);
4321 let measured = measure_table_columns(&table, 1, &node_cx, window, cx)[0];
4322 let current = cached_column_widths(&table);
4323 assert_eq!(
4324 measured,
4325 current.widths[0].max(expected as f32 + CELL_PAD_PX)
4326 );
4327 assert_eq!(current.custom_cells, vec![(1, 0)]);
4328 if let Some(cached) = cached.replace(current.clone()) {
4329 assert!(Arc::ptr_eq(&cached, ¤t));
4330 }
4331 }
4332 });
4333 }
4334
4335 fn cached_column_widths(table: &Table) -> Arc<TableColumnWidths> {
4336 table.column_widths_cache.0.lock().unwrap().clone().unwrap()
4337 }
4338
4339 #[test]
4340 fn table_columns_cache_preserves_plain_widths_and_clones() {
4341 use crate::text::inline::test_draw::in_prepaint;
4342 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem, record_shaped_lines};
4343 use gpui::{Refineable as _, TestApp, TextStyle};
4344
4345 let long = "é界".repeat(2048);
4346 let table = table_of(
4347 vec![
4348 vec![plain_cell(&format!(" \t{long} \nshort\n ")), plain_cell("")],
4349 vec![plain_cell("short")],
4350 vec![],
4351 vec![plain_cell(""), plain_cell("a\nabcdefghijklmnop")],
4352 ],
4353 vec![],
4354 );
4355 let uncached = table.clone();
4356 let node_cx = NodeContext::default();
4357 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4358 let (table, node_cx, cached) = in_prepaint(&mut app, move |window, cx| {
4359 window.set_rem_size(px(20.));
4360 let style = TextStyle {
4361 font_family: BODY.into(),
4362 font_size: rems(1.).into(),
4363 ..Default::default()
4364 };
4365 let cached =
4366 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
4367 let (widths, shaped) = record_shaped_lines(|| {
4368 measure_table_columns(&table, 2, &node_cx, window, cx)
4369 });
4370 assert_eq!(widths, vec![40960. + CELL_PAD_PX + CELL_BORDER_PX, 184.]);
4371 assert!(shaped.iter().any(|line| line == &long));
4372 assert_eq!(
4373 table, uncached,
4374 "derived widths do not change table equality"
4375 );
4376 assert!(uncached.column_widths_cache.0.lock().unwrap().is_none());
4377
4378 let cached = cached_column_widths(&table);
4379 let cloned = table.clone();
4380 for same in [&table, &cloned] {
4381 let (again, shaped) = record_shaped_lines(|| {
4382 measure_table_columns(same, 2, &node_cx, window, cx)
4383 });
4384 assert_eq!(again, widths);
4385 assert!(shaped.is_empty());
4386 assert!(Arc::ptr_eq(&cached, &cached_column_widths(same)));
4387 }
4388
4389 assert_eq!(
4391 measure_table_columns(&cloned, 3, &node_cx, window, cx),
4392 vec![widths[0], widths[1] + CELL_BORDER_PX, CELL_MIN_PX]
4393 );
4394 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&cloned)));
4395 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4396 assert!(
4397 measure_table_columns(&Table::default(), 0, &node_cx, window, cx)
4398 .is_empty()
4399 );
4400 cached
4401 });
4402 (table, node_cx, cached)
4403 });
4404 in_prepaint(&mut app, move |window, cx| {
4407 window.set_rem_size(cached.rem_size);
4408 window.with_text_style(
4409 Some(cached.text_styles.body.subtract(&Default::default())),
4410 |window| {
4411 measure_table_columns(&table, 2, &node_cx, window, cx);
4412 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4413 },
4414 );
4415 });
4416 }
4417
4418 #[test]
4419 fn table_columns_cache_tracks_typography() {
4420 use crate::text::inline::test_draw::in_prepaint;
4421 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
4422 use gpui::{Refineable as _, TestApp, TextStyle};
4423
4424 let code = "0123456789";
4425 let mut paragraph = Paragraph::default();
4426 paragraph
4427 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
4428 let table = table_of(
4429 vec![vec![
4430 plain_cell(code),
4431 TableCell {
4432 children: paragraph,
4433 width: None,
4434 },
4435 ]],
4436 vec![],
4437 );
4438 let base = TextStyle {
4439 font_family: BODY.into(),
4440 font_size: rems(1.).into(),
4441 ..Default::default()
4442 };
4443 let mut larger = base.clone();
4444 larger.font_size = rems(2.).into();
4445 let mut bold = base.clone();
4446 bold.font_weight = FontWeight::BOLD;
4447 let mut mono = base.clone();
4448 mono.font_family = MONO.into();
4449 let normal_code = HighlightStyle::default();
4450 let bold_code = HighlightStyle {
4451 font_weight: Some(FontWeight::BOLD),
4452 ..Default::default()
4453 };
4454 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4455 in_prepaint(&mut app, move |window, cx| {
4456 let mut previous = None;
4457 for (style, rem_size, family, inline_code, expected) in [
4458 (base.clone(), 20., MONO, normal_code, [125., 207.]),
4459 (base.clone(), 30., MONO, normal_code, [175., 294.5]),
4460 (larger, 20., MONO, normal_code, [225., 382.]),
4461 (bold, 20., MONO, normal_code, [175., 250.75]),
4462 (mono, 20., MONO, normal_code, [225., 207.]),
4463 (base.clone(), 20., MONO, normal_code, [125., 207.]),
4464 (base.clone(), 20., BODY, normal_code, [125., 119.5]),
4465 (base.clone(), 20., MONO, normal_code, [125., 207.]),
4466 (base, 20., MONO, bold_code, [125., 250.75]),
4467 ] {
4468 let mut theme = crate::Theme::default();
4469 theme.tokens.typography.mono = family.into();
4470 cx.set_global(theme);
4471 window.set_rem_size(px(rem_size));
4472 let node_cx = NodeContext {
4473 style: Arc::new(TextViewStyle::default().with_inline_code(inline_code)),
4474 ..Default::default()
4475 };
4476 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
4477 let widths = measure_table_columns(&table, 2, &node_cx, window, cx);
4478 for (width, expected) in widths.iter().zip(expected) {
4479 assert!((width - expected).abs() < 0.01, "{width} != {expected}");
4480 }
4481 let cached = cached_column_widths(&table);
4482 if let Some(previous) = previous.replace(cached.clone()) {
4483 assert!(!Arc::ptr_eq(&previous, &cached));
4484 }
4485 assert_eq!(
4486 measure_table_columns(&table, 2, &node_cx, window, cx),
4487 widths
4488 );
4489 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4490 });
4491 }
4492 });
4493 }
4494
4495 #[test]
4496 fn table_columns_cache_is_rebuilt_for_reparsed_tables() {
4497 use crate::text::format::markdown;
4498 use crate::text::inline::test_draw::in_prepaint;
4499 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem};
4500 use gpui::{Refineable as _, TestApp, TextStyle};
4501
4502 let mut node_cx = NodeContext::default();
4503 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4504 in_prepaint(&mut app, move |window, cx| {
4505 let style = TextStyle {
4506 font_family: BODY.into(),
4507 font_size: px(20.).into(),
4508 ..Default::default()
4509 };
4510 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
4511 let mut previous = None;
4512 for (text, expected) in [("longer cell", 134.), ("x", CELL_MIN_PX)] {
4513 let source = format!("| h |\n| - |\n| {text} |\n");
4514 let document = markdown::parse(&source, &mut node_cx).unwrap();
4515 let BlockNode::Table(table) = &document.blocks[0] else {
4516 panic!("expected a parsed table");
4517 };
4518 assert!(table.column_widths_cache.0.lock().unwrap().is_none());
4519 assert_eq!(
4520 measure_table_columns(table, 1, &node_cx, window, cx),
4521 vec![expected]
4522 );
4523 let cached = cached_column_widths(table);
4524 if let Some(previous) = previous.replace(cached.clone()) {
4525 assert!(!Arc::ptr_eq(&previous, &cached));
4526 }
4527 assert_eq!(document.source.as_ref(), source);
4528 }
4529 });
4530 });
4531 }
4532
4533 #[test]
4537 fn table_column_of_inline_code_cells_fits_the_mono_width() {
4538 use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
4539 use gpui::{Empty, TestApp};
4540
4541 let code = "method_name()";
4542 let mut paragraph = Paragraph::default();
4543 paragraph
4544 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
4545 let table = Table {
4546 children: vec![TableRow {
4547 children: vec![TableCell {
4548 children: paragraph,
4549 width: None,
4550 }],
4551 }],
4552 column_aligns: vec![],
4553 span: None,
4554 table_data_cache: TableDataCache::default(),
4555 column_widths_cache: TableColumnWidthsCache::default(),
4556 };
4557 let node_cx = NodeContext::default();
4558
4559 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4560 let mut window = app.open_window(|_, _| Empty);
4561 let (col_w, font_size) = window.update(|_, window, cx| {
4562 let mut theme = crate::Theme::default();
4563 theme.tokens.typography.mono = MONO.into();
4564 cx.set_global(theme);
4565 let font_size = window.text_style().font_size.to_pixels(window.rem_size());
4566 (
4567 measure_table_columns(&table, 1, &node_cx, window, cx),
4568 font_size,
4569 )
4570 });
4571
4572 let mono_w = f32::from(WideMonoTextSystem::width_of(code, MONO, font_size * 0.875));
4573 assert!(
4574 (col_w[0]
4575 - (mono_w + CELL_PAD_PX + crate::text::inline_flow::INLINE_CODE_PADDING * 2.))
4576 .abs()
4577 < 0.01,
4578 "col_w {} must fit the mono width {} plus padding {}",
4579 col_w[0],
4580 mono_w,
4581 CELL_PAD_PX
4582 );
4583 }
4584
4585 #[test]
4586 fn code_block_highlights_are_cached_by_highlighter_identity() {
4587 use std::sync::atomic::{AtomicUsize, Ordering};
4588
4589 let calls = Arc::new(AtomicUsize::new(0));
4590 let calls_for_highlighter = calls.clone();
4591 let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
4592 calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
4593 Vec::new()
4594 });
4595 let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
4596
4597 block.highlighted_styles(&highlighter);
4598 block.highlighted_styles(&highlighter);
4599 assert_eq!(calls.load(Ordering::Relaxed), 1);
4600
4601 let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
4602 block.highlighted_styles(&replacement);
4603 assert!(Arc::ptr_eq(
4604 &block
4605 .highlight_cache
4606 .lock()
4607 .unwrap()
4608 .as_ref()
4609 .unwrap()
4610 .highlighter,
4611 &replacement
4612 ));
4613 }
4614
4615 #[test]
4616 fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
4617 let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
4621 vec![(
4622 0..2,
4623 HighlightStyle {
4624 color: Some(gpui::rgb(0x0000ff).into()),
4625 ..Default::default()
4626 },
4627 )]
4628 });
4629 let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
4630 vec![(
4631 0..2,
4632 HighlightStyle {
4633 color: Some(gpui::rgb(0xffff00).into()),
4634 ..Default::default()
4635 },
4636 )]
4637 });
4638 let block = CodeBlock::from_code("42", Some("json"));
4639
4640 let light_styles = block.highlighted_styles(&light);
4641 let dark_styles = block.highlighted_styles(&dark);
4642
4643 assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
4644 assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
4645 assert_eq!(block.code(), "42", "the document must survive the swap");
4646 }
4647
4648 #[test]
4649 fn reconstruct_markdown_wraps_marked_runs() {
4650 let marks = vec![(0..4, TextMark::default().bold())];
4652 assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
4653 assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
4655 }
4656
4657 #[test]
4658 fn reconstruct_markdown_emits_unmarked_text_verbatim() {
4659 let text = "a b c";
4661 let marks = vec![(2..3, TextMark::default().code())];
4662 assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
4663 assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
4665 }
4666
4667 #[test]
4668 fn reconstruct_markdown_handles_code_italic_strike_link() {
4669 assert_eq!(
4670 reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
4671 "`x`"
4672 );
4673 assert_eq!(
4674 reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
4675 "*x*"
4676 );
4677 assert_eq!(
4678 reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
4679 "~~x~~"
4680 );
4681 let link = TextMark::default().link(LinkMark {
4682 url: "https://example.com".into(),
4683 ..Default::default()
4684 });
4685 assert_eq!(
4686 reconstruct_markdown("x", &[(0..1, link)], 0..1),
4687 "[x](https://example.com)"
4688 );
4689 }
4690
4691 #[test]
4692 fn reconstruct_markdown_nested_bold_italic() {
4693 let mark = TextMark::default().bold().italic();
4695 assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
4697 }
4698
4699 fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
4703 let combined: String = children.iter().map(|c| c.text.to_string()).collect();
4704 let paragraph = Paragraph {
4705 span: None,
4706 children,
4707 link_refs: HashMap::new(),
4708 state: Arc::new(Mutex::new(InlineState::default())),
4709 render_cache: ParagraphRenderCache::default(),
4710 };
4711 if let Ok(mut state) = paragraph.state.lock() {
4712 state.set_text(combined.into());
4713 }
4714 paragraph
4715 }
4716
4717 fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
4718 if let Ok(mut state) = paragraph.state.lock() {
4719 state.selection = Some(range.into());
4720 }
4721 }
4722
4723 #[test]
4724 fn paragraph_selected_source_maps_partial_selection_across_runs() {
4725 let children = vec![
4727 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
4728 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
4729 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
4730 ];
4731 let paragraph = paragraph_with_children(children);
4732
4733 set_paragraph_selection(¶graph, 0..(9 + 4 + 6));
4735 assert_eq!(paragraph.selected_source(), "This has **bold** text.");
4736
4737 set_paragraph_selection(¶graph, 5..16);
4740 assert_eq!(paragraph.selected_source(), "has **bold** te");
4741
4742 set_paragraph_selection(¶graph, 10..12);
4744 assert_eq!(paragraph.selected_source(), "**ol**");
4745 }
4746
4747 #[test]
4748 fn paragraph_selected_source_matches_text_when_no_marks() {
4749 let children =
4750 vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
4751 let paragraph = paragraph_with_children(children);
4752 set_paragraph_selection(¶graph, 0..11);
4753 assert_eq!(paragraph.selected_source(), "plain words");
4754 assert_eq!(paragraph.selected_text(), "plain words");
4755 }
4756
4757 fn selected_paragraph(text: &str) -> Paragraph {
4758 let len = text.len();
4759 let paragraph = paragraph_with_children(vec![
4760 InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
4761 ]);
4762 set_paragraph_selection(¶graph, 0..len);
4763 paragraph
4764 }
4765
4766 #[test]
4767 fn heading_selected_source_prefixes_hashes() {
4768 let heading = BlockNode::Heading {
4769 level: 2,
4770 children: selected_paragraph("Title"),
4771 span: None,
4772 };
4773 assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
4774 assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
4776 }
4777
4778 #[test]
4779 fn unordered_list_selected_source_prefixes_dash() {
4780 let list = BlockNode::List {
4781 ordered: false,
4782 start: None,
4783 span: None,
4784 children: vec![
4785 BlockNode::ListItem {
4786 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
4787 spread: false,
4788 checked: None,
4789 span: None,
4790 },
4791 BlockNode::ListItem {
4792 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4793 spread: false,
4794 checked: None,
4795 span: None,
4796 },
4797 ],
4798 };
4799 assert_eq!(
4800 list.selected_text(SelectionFormat::Source),
4801 "- one\n- two\n"
4802 );
4803 }
4804
4805 #[test]
4806 fn ordered_list_selected_source_preserves_start() {
4807 let list = BlockNode::List {
4808 ordered: true,
4809 start: Some(3),
4810 span: None,
4811 children: vec![
4812 BlockNode::ListItem {
4813 children: vec![BlockNode::Paragraph(selected_paragraph("three"))],
4814 spread: false,
4815 checked: None,
4816 span: None,
4817 },
4818 BlockNode::ListItem {
4819 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4820 spread: false,
4821 checked: None,
4822 span: None,
4823 },
4824 ],
4825 };
4826 assert_eq!(list.to_markdown(), "3. three\n4. four");
4827 assert_eq!(
4828 list.selected_text(SelectionFormat::Source),
4829 "3. three\n4. four\n"
4830 );
4831 }
4832
4833 #[test]
4834 fn ordered_list_selected_source_preserves_nested_starts_and_continuations() {
4835 let nested = BlockNode::List {
4836 ordered: true,
4837 start: Some(4),
4838 span: None,
4839 children: vec![
4840 BlockNode::ListItem {
4841 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
4842 spread: false,
4843 checked: None,
4844 span: None,
4845 },
4846 BlockNode::ListItem {
4847 children: vec![BlockNode::Paragraph(selected_paragraph("again"))],
4848 spread: false,
4849 checked: None,
4850 span: None,
4851 },
4852 ],
4853 };
4854 let nested_list = BlockNode::List {
4855 ordered: true,
4856 start: Some(3),
4857 span: None,
4858 children: vec![
4859 BlockNode::ListItem {
4860 children: vec![BlockNode::Paragraph(selected_paragraph("three")), nested],
4861 spread: false,
4862 checked: None,
4863 span: None,
4864 },
4865 BlockNode::ListItem {
4866 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4867 spread: false,
4868 checked: None,
4869 span: None,
4870 },
4871 ],
4872 };
4873 assert_eq!(
4874 nested_list.selected_text(SelectionFormat::Source),
4875 "3. three\n 4. nested\n 5. again\n4. four\n"
4876 );
4877
4878 let loose_list = BlockNode::List {
4879 ordered: true,
4880 start: Some(3),
4881 span: None,
4882 children: vec![
4883 BlockNode::ListItem {
4884 children: vec![
4885 BlockNode::Paragraph(selected_paragraph("loose")),
4886 BlockNode::Paragraph(selected_paragraph("continuation")),
4887 ],
4888 spread: true,
4889 checked: None,
4890 span: None,
4891 },
4892 BlockNode::ListItem {
4893 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4894 spread: false,
4895 checked: None,
4896 span: None,
4897 },
4898 ],
4899 };
4900 assert_eq!(
4901 loose_list.selected_text(SelectionFormat::Source),
4902 "3. loose\n continuation\n4. four\n"
4903 );
4904 }
4905
4906 #[test]
4907 fn nested_list_selected_source_indents_sublists() {
4908 let nested = BlockNode::List {
4912 ordered: false,
4913 start: None,
4914 span: None,
4915 children: vec![BlockNode::ListItem {
4916 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
4917 spread: false,
4918 checked: None,
4919 span: None,
4920 }],
4921 };
4922 let list = BlockNode::List {
4923 ordered: false,
4924 start: None,
4925 span: None,
4926 children: vec![
4927 BlockNode::ListItem {
4928 children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
4929 spread: false,
4930 checked: None,
4931 span: None,
4932 },
4933 BlockNode::ListItem {
4934 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4935 spread: false,
4936 checked: None,
4937 span: None,
4938 },
4939 ],
4940 };
4941 assert_eq!(
4942 list.selected_text(SelectionFormat::Source),
4943 "- one\n - nested\n- two\n"
4944 );
4945 }
4946
4947 #[test]
4948 fn task_list_selected_source_restores_checkboxes() {
4949 let list = BlockNode::List {
4950 ordered: false,
4951 start: None,
4952 span: None,
4953 children: vec![
4954 BlockNode::ListItem {
4955 children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
4956 spread: false,
4957 checked: Some(true),
4958 span: None,
4959 },
4960 BlockNode::ListItem {
4961 children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
4962 spread: false,
4963 checked: Some(false),
4964 span: None,
4965 },
4966 ],
4967 };
4968 assert_eq!(
4969 list.selected_text(SelectionFormat::Source),
4970 "- [x] done\n- [ ] todo\n"
4971 );
4972 }
4973
4974 #[test]
4975 fn blockquote_selected_source_prefixes_gt() {
4976 let quote = BlockNode::Blockquote {
4977 span: None,
4978 children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
4979 };
4980 assert_eq!(
4981 quote.selected_text(SelectionFormat::Source),
4982 "> quoted text\n"
4983 );
4984 }
4985
4986 #[test]
4987 fn table_selected_source_pipes_cells_with_alignment_row() {
4988 let cell = |text: &str| TableCell {
4989 children: selected_paragraph(text),
4990 width: None,
4991 };
4992 let table = Table {
4993 children: vec![
4994 TableRow {
4995 children: vec![cell("Name"), cell("Age")],
4996 },
4997 TableRow {
4998 children: vec![cell("Alice"), cell("30")],
4999 },
5000 ],
5001 column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5002 span: None,
5003 table_data_cache: TableDataCache::default(),
5004 column_widths_cache: TableColumnWidthsCache::default(),
5005 };
5006 let block = BlockNode::Table(table);
5007 assert_eq!(
5008 block.selected_text(SelectionFormat::Source),
5009 "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
5010 );
5011 }
5012
5013 fn plain_cell(text: &str) -> TableCell {
5016 TableCell {
5017 children: Paragraph::new(text.to_string()),
5018 width: None,
5019 }
5020 }
5021
5022 fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
5023 Table {
5024 children: rows
5025 .into_iter()
5026 .map(|children| TableRow { children })
5027 .collect(),
5028 column_aligns,
5029 span: None,
5030 table_data_cache: TableDataCache::default(),
5031 column_widths_cache: TableColumnWidthsCache::default(),
5032 }
5033 }
5034
5035 #[test]
5036 fn table_to_markdown_pipes_cells_with_alignment_row() {
5037 let table = table_of(
5038 vec![
5039 vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
5040 vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
5041 ],
5042 vec![
5043 ColumnumnAlign::Left,
5044 ColumnumnAlign::Center,
5045 ColumnumnAlign::Right,
5046 ],
5047 );
5048
5049 assert_eq!(
5050 table.to_markdown(),
5051 "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
5052 );
5053 assert_eq!(
5055 BlockNode::Table(table.clone()).to_markdown(),
5056 table.to_markdown()
5057 );
5058 }
5059
5060 #[test]
5061 fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
5062 let table = table_of(
5063 vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
5064 vec![ColumnumnAlign::Left],
5065 );
5066
5067 assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
5068 }
5069
5070 #[test]
5071 fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
5072 let bold = TableCell {
5073 children: paragraph_with_children(vec![
5074 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5075 ]),
5076 width: None,
5077 };
5078 let table = table_of(
5079 vec![
5080 vec![plain_cell("a | b"), plain_cell("plain")],
5081 vec![plain_cell("c"), bold],
5082 ],
5083 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
5084 );
5085
5086 assert_eq!(
5087 table.to_markdown(),
5088 "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
5089 );
5090 }
5091
5092 #[test]
5093 fn table_data_snapshots_plain_cells_and_markdown() {
5094 let mut table = table_of(
5095 vec![
5096 vec![plain_cell(" Name "), plain_cell("Age")],
5097 vec![plain_cell("Alice"), plain_cell("30")],
5098 ],
5099 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5100 );
5101 table.span = Some(Span { start: 4, end: 42 });
5102
5103 let data = table.table_data();
5104 assert_eq!(data.headers, vec!["Name", "Age"]);
5105 assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
5106 assert_eq!(data.markdown, table.to_markdown());
5107 assert_eq!(data.span, Some(4..42));
5108 }
5109
5110 #[test]
5111 fn cloned_table_keeps_cached_table_data() {
5112 let table = table_of(
5115 vec![vec![plain_cell("Name")], vec![plain_cell("Alice")]],
5116 vec![ColumnumnAlign::Left],
5117 );
5118 let data = table.cached_table_data();
5119
5120 let cloned = table.clone();
5121 assert!(Arc::ptr_eq(&data, &cloned.cached_table_data()));
5122 }
5123
5124 #[test]
5125 fn table_data_handles_tables_without_rows() {
5126 let header_only = table_of(
5128 vec![vec![plain_cell("Name"), plain_cell("Age")]],
5129 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
5130 );
5131 let data = header_only.table_data();
5132 assert_eq!(data.headers, vec!["Name", "Age"]);
5133 assert!(data.rows.is_empty());
5134 assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
5135
5136 assert_eq!(Table::default().table_data(), TableData::default());
5138 }
5139
5140 #[test]
5141 fn test_image_node_source() {
5142 use gpui::{ImageFormat, ImageSource, Resource};
5143
5144 fn image_node(url: &str) -> ImageNode {
5145 ImageNode {
5146 url: url.into(),
5147 ..Default::default()
5148 }
5149 }
5150
5151 fn assert_uri(url: &str) {
5155 match image_node(url).source() {
5156 ImageSource::Resource(Resource::Uri(uri)) => assert_eq!(uri.as_ref(), url),
5157 _ => panic!("expected Uri for {url:?}"),
5158 }
5159 }
5160 assert_uri("https://example.com/logo.png");
5161 assert_uri("http://example.com/logo.png");
5162 assert_uri("website/public/logo.svg");
5163 assert_uri("./images/a.png");
5164 assert_uri("../images/a.png");
5165 assert_uri("/absolute/path/logo.svg");
5166 assert_uri("file:///absolute/path/logo.svg");
5167 assert_uri(r"C:\images\logo.png");
5168 assert_uri("docs/a:b.png");
5169 assert_uri("data:text/plain;base64,aGVsbG8=");
5170
5171 let node = image_node("data:image/png;base64,iVBORw0KGgo=");
5174 let ImageSource::Image(first) = node.source() else {
5175 panic!("expected an embedded image");
5176 };
5177 assert_eq!(first.format(), ImageFormat::Png);
5178 assert_eq!(first.bytes(), b"\x89PNG\r\n\x1a\n");
5179 let ImageSource::Image(second) = node.source() else {
5180 panic!("expected an embedded image");
5181 };
5182 assert!(Arc::ptr_eq(&first, &second));
5183 }
5184
5185 fn image_paragraph(alt: &str, url: &str) -> Paragraph {
5186 let image = ImageNode {
5187 url: url.into(),
5188 alt: Some(alt.into()),
5189 ..Default::default()
5190 };
5191 Paragraph {
5192 span: None,
5193 children: vec![InlineNode::image(image)],
5194 link_refs: HashMap::new(),
5195 state: Arc::new(Mutex::new(InlineState::default())),
5196 render_cache: ParagraphRenderCache::default(),
5197 }
5198 }
5199
5200 #[test]
5203 fn marks_round_trip_through_reconstruction() {
5204 let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
5205
5206 assert_eq!(wrap(TextMark::default().bold()), "**x**");
5207 assert_eq!(wrap(TextMark::default().italic()), "*x*");
5208 assert_eq!(wrap(TextMark::default().code()), "`x`");
5209 assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
5210 assert_eq!(
5211 wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
5212 "==x=="
5213 );
5214 assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
5216
5217 assert_eq!(
5219 wrap(TextMark::default().link(LinkMark {
5220 url: "https://example.com".into(),
5221 title: Some("Tip".into()),
5222 ..Default::default()
5223 })),
5224 "[x](https://example.com \"Tip\")"
5225 );
5226 }
5227
5228 #[test]
5231 fn document_selected_source_slices_covered_blocks_from_the_source() {
5232 use crate::text::document::ParsedDocument;
5233
5234 let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
5237 let list = "3. _one_\n4. two";
5238 let list_start = source.find(list).unwrap();
5239 let rule_start = source.find("---").unwrap();
5240
5241 let document = ParsedDocument {
5242 source: source.into(),
5243 blocks: vec![
5244 BlockNode::Paragraph(selected_paragraph("start")),
5245 BlockNode::List {
5246 ordered: true,
5247 start: Some(3),
5248 children: vec![],
5249 span: Some(Span {
5250 start: list_start,
5251 end: list_start + list.len(),
5252 }),
5253 },
5254 BlockNode::HorizontalRule {
5255 span: Some(Span {
5256 start: rule_start,
5257 end: rule_start + 3,
5258 }),
5259 },
5260 BlockNode::Paragraph(selected_paragraph("end")),
5261 ]
5262 .into(),
5263 };
5264
5265 assert_eq!(
5266 document.selected_text(SelectionFormat::Source, None),
5267 "start\n\n3. _one_\n4. two\n\n---\n\nend"
5268 );
5269 }
5270
5271 #[test]
5272 fn document_selected_source_includes_enclosed_image() {
5273 use crate::text::document::ParsedDocument;
5274
5275 let source = "before\n\n\n\nafter";
5279 let image_markdown = "";
5280 let start = source.find(image_markdown).unwrap();
5281 let mut image = image_paragraph("alt", "https://example.com/i.png");
5282 image.span = Some(Span {
5283 start,
5284 end: start + image_markdown.len(),
5285 });
5286
5287 let document = ParsedDocument {
5288 source: source.into(),
5289 blocks: vec![
5290 BlockNode::Paragraph(selected_paragraph("before")),
5291 BlockNode::Paragraph(image),
5292 BlockNode::Paragraph(selected_paragraph("after")),
5293 ]
5294 .into(),
5295 };
5296 assert_eq!(
5297 document.selected_text(SelectionFormat::Source, None),
5298 "before\n\n\n\nafter"
5299 );
5300 }
5301
5302 #[test]
5303 fn document_selected_source_drops_unenclosed_image() {
5304 use crate::text::document::ParsedDocument;
5305
5306 let document = ParsedDocument {
5309 source: String::new().into(),
5310 blocks: vec![
5311 BlockNode::Paragraph(selected_paragraph("before")),
5312 BlockNode::Paragraph(image_paragraph("alt", "u")),
5313 ]
5314 .into(),
5315 };
5316 assert_eq!(
5317 document.selected_text(SelectionFormat::Source, None),
5318 "before"
5319 );
5320 }
5321
5322 fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
5323 let block = CodeBlock::new(
5324 code.to_string().into(),
5325 lang.map(|l| l.to_string().into()),
5326 None::<Span>,
5327 );
5328 if let Ok(mut state) = block.state.lock() {
5329 let len = state.text.len();
5330 state.selection = Some((0..len).into());
5331 }
5332 BlockNode::CodeBlock(block)
5333 }
5334
5335 #[test]
5336 fn code_block_selected_source_wraps_in_fence_with_lang() {
5337 let block = selected_code_block("let x = 1;\n", Some("rust"));
5338 let code = block.selected_text(SelectionFormat::Plain);
5339 let code_trimmed = code.trim_end_matches('\n');
5340 assert_eq!(
5344 block.selected_text(SelectionFormat::Source),
5345 format!("```rust\n{}\n```\n", code_trimmed)
5346 );
5347 assert!(
5348 block
5349 .selected_text(SelectionFormat::Source)
5350 .starts_with("```rust\n")
5351 );
5352 assert!(
5353 block
5354 .selected_text(SelectionFormat::Source)
5355 .trim_end()
5356 .ends_with("\n```")
5357 );
5358 }
5359
5360 #[test]
5361 fn code_block_selected_source_without_lang() {
5362 let block = selected_code_block("plain\n", None);
5363 let code_trimmed = block.selected_text(SelectionFormat::Plain);
5364 let code_trimmed = code_trimmed.trim_end_matches('\n');
5365 assert_eq!(
5366 block.selected_text(SelectionFormat::Source),
5367 format!("```\n{}\n```\n", code_trimmed)
5368 );
5369 }
5370
5371 #[test]
5372 fn document_selected_source_joins_blocks_with_blank_line() {
5373 use crate::text::document::ParsedDocument;
5374
5375 let document = ParsedDocument {
5379 source: String::new().into(),
5380 blocks: vec![
5381 BlockNode::Heading {
5382 level: 1,
5383 children: selected_paragraph("Title"),
5384 span: None,
5385 },
5386 BlockNode::Paragraph(selected_paragraph("A paragraph.")),
5387 selected_code_block("let x = 1;\n", Some("rust")),
5388 BlockNode::List {
5389 ordered: true,
5390 start: Some(1),
5391 span: None,
5392 children: vec![
5393 BlockNode::ListItem {
5394 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
5395 spread: false,
5396 checked: None,
5397 span: None,
5398 },
5399 BlockNode::ListItem {
5400 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
5401 spread: false,
5402 checked: None,
5403 span: None,
5404 },
5405 ],
5406 },
5407 ]
5408 .into(),
5409 };
5410
5411 assert_eq!(
5412 document.selected_text(SelectionFormat::Source, None),
5413 "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
5414 );
5415 }
5416
5417 #[test]
5418 fn code_block_equality_includes_code_content() {
5419 let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
5420 let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
5421
5422 assert_ne!(first, second);
5423 }
5424}