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, code_insets, 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)]
52#[non_exhaustive]
53pub enum BlockNode {
54 Root {
56 children: Vec<BlockNode>,
57 span: Option<Span>,
58 },
59 Paragraph(Paragraph),
60 Heading {
61 level: u8,
62 children: Paragraph,
63 span: Option<Span>,
64 },
65 Blockquote {
66 children: Vec<BlockNode>,
67 span: Option<Span>,
68 },
69 List {
70 children: Vec<BlockNode>,
72 ordered: bool,
73 start: Option<u32>,
75 span: Option<Span>,
76 },
77 ListItem {
78 children: Vec<BlockNode>,
79 spread: bool,
80 checked: Option<bool>,
82 span: Option<Span>,
83 },
84 CodeBlock(CodeBlock),
85 Custom {
87 node: MarkdownNode,
88 state: BlockState,
89 },
90 Table(Table),
91 Break {
92 html: bool,
93 span: Option<Span>,
94 },
95 HorizontalRule {
96 span: Option<Span>,
97 },
98 Definition {
100 identifier: SharedString,
101 url: SharedString,
102 title: Option<SharedString>,
103 span: Option<Span>,
104 },
105 Unknown,
106}
107
108#[derive(Debug, Clone, Default)]
113pub struct BlockState {
114 selection: Arc<Mutex<Option<BlockSelection>>>,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119enum BlockSelection {
120 Empty,
121 Full,
122}
123
124impl BlockState {
125 fn selection(&self) -> Option<BlockSelection> {
126 self.selection.lock().ok().and_then(|selection| *selection)
127 }
128
129 fn set_selection(&self, value: Option<BlockSelection>) {
130 if let Ok(mut selection) = self.selection.lock() {
131 *selection = value;
132 }
133 }
134
135 fn is_selected(&self) -> bool {
136 self.selection() == Some(BlockSelection::Full)
137 }
138
139 fn has_selection_observation(&self) -> bool {
140 self.selection().is_some()
141 }
142
143 fn clear_selection(&self) {
144 self.set_selection(None);
145 }
146}
147
148impl PartialEq for BlockState {
149 fn eq(&self, _: &Self) -> bool {
150 true
152 }
153}
154
155pub(super) struct CustomBlockSelectionProjection {
156 state: BlockState,
157 bounds: Bounds<Pixels>,
158}
159
160impl CustomBlockSelectionProjection {
161 pub(super) fn project(&self, start: Point<Pixels>, end: Point<Pixels>) {
162 self.state
163 .set_selection(custom_block_selection(self.bounds, start, end));
164 }
165
166 pub(super) fn deselect(&self) {
167 self.state.set_selection(None);
168 }
169}
170
171fn custom_block_selection(
172 bounds: Bounds<Pixels>,
173 start: Point<Pixels>,
174 end: Point<Pixels>,
175) -> Option<BlockSelection> {
176 if bounds.size.width <= Pixels::ZERO
177 || bounds.size.height <= Pixels::ZERO
178 || bounds.bottom() <= start.y.min(end.y)
179 || bounds.top() > start.y.max(end.y)
180 {
181 return None;
182 }
183 Some(if custom_block_is_selected(bounds, start, end) {
184 BlockSelection::Full
185 } else {
186 BlockSelection::Empty
187 })
188}
189
190struct TableCellElement(AnyElement);
193
194impl IntoElement for TableCellElement {
195 type Element = Self;
196
197 fn into_element(self) -> Self {
198 self
199 }
200}
201
202impl Element for TableCellElement {
203 type RequestLayoutState = ();
204 type PrepaintState = ();
205
206 fn id(&self) -> Option<ElementId> {
207 None
208 }
209
210 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
211 None
212 }
213
214 fn request_layout(
215 &mut self,
216 _: Option<&GlobalElementId>,
217 _: Option<&InspectorElementId>,
218 window: &mut Window,
219 cx: &mut App,
220 ) -> (LayoutId, ()) {
221 (self.0.request_layout(window, cx), ())
222 }
223
224 fn prepaint(
225 &mut self,
226 _: Option<&GlobalElementId>,
227 _: Option<&InspectorElementId>,
228 bounds: Bounds<Pixels>,
229 _: &mut (),
230 window: &mut Window,
231 cx: &mut App,
232 ) {
233 if let Some(view) = GlobalState::global(cx).text_view_state() {
236 view.read(cx)
237 .selection_adapter
238 .record_table_cell(bounds, cx);
239 }
240 GlobalState::global(cx).push_table_cell(bounds);
241 self.0.prepaint(window, cx);
242 GlobalState::global(cx).pop_table_cell();
243 }
244
245 fn paint(
246 &mut self,
247 _: Option<&GlobalElementId>,
248 _: Option<&InspectorElementId>,
249 bounds: Bounds<Pixels>,
250 _: &mut (),
251 _: &mut (),
252 window: &mut Window,
253 cx: &mut App,
254 ) {
255 GlobalState::global(cx).push_table_cell(bounds);
256 self.0.paint(window, cx);
257 GlobalState::global(cx).pop_table_cell();
258 }
259}
260
261struct CustomBlockElement {
263 content: AnyElement,
264 state: BlockState,
265}
266
267impl IntoElement for CustomBlockElement {
268 type Element = Self;
269
270 fn into_element(self) -> Self {
271 self
272 }
273}
274
275impl Element for CustomBlockElement {
276 type RequestLayoutState = ();
277 type PrepaintState = ();
278
279 fn id(&self) -> Option<ElementId> {
280 None
281 }
282
283 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
284 None
285 }
286
287 fn request_layout(
288 &mut self,
289 _: Option<&GlobalElementId>,
290 _: Option<&InspectorElementId>,
291 window: &mut Window,
292 cx: &mut App,
293 ) -> (LayoutId, ()) {
294 (self.content.request_layout(window, cx), ())
295 }
296
297 fn prepaint(
298 &mut self,
299 _: Option<&GlobalElementId>,
300 _: Option<&InspectorElementId>,
301 _: Bounds<Pixels>,
302 _: &mut (),
303 window: &mut Window,
304 cx: &mut App,
305 ) {
306 self.content.prepaint(window, cx);
307 }
308
309 fn paint(
310 &mut self,
311 _: Option<&GlobalElementId>,
312 _: Option<&InspectorElementId>,
313 bounds: Bounds<Pixels>,
314 _: &mut (),
315 _: &mut (),
316 window: &mut Window,
317 cx: &mut App,
318 ) {
319 let view = GlobalState::global(cx).text_view_state();
320 let selection = view.as_ref().and_then(|view| {
321 let state = view.read(cx);
322 if !state.is_selectable() {
323 return None;
324 }
325 if state.is_all_selected() {
326 return Some(BlockSelection::Full);
327 }
328 if state.preserve_inline_selection {
329 return self.state.selection();
330 }
331 let (start, end) = state.painted_selection_points(cx)?;
332 custom_block_selection(bounds, start, end)
333 });
334 self.state.set_selection(selection);
335 if let Some(view) = &view {
336 if view.read(cx).is_selectable() {
337 view.read(cx).selection_adapter.register_copy_projection(
338 super::selection_adapter::CopyProjection::CustomBlock(
339 CustomBlockSelectionProjection {
340 state: self.state.clone(),
341 bounds,
342 },
343 ),
344 cx,
345 );
346 }
347 let visible = bounds.intersect(&window.content_mask().bounds);
348 if view.read(cx).is_selectable()
349 && visible.size.width > Pixels::ZERO
350 && visible.size.height > Pixels::ZERO
351 {
352 view.read(cx)
353 .selection_adapter
354 .register_inline(vec![visible]);
355 }
356 }
357 self.content.paint(window, cx);
358 if selection == Some(BlockSelection::Full) {
359 let color = view.as_ref().unwrap().read(cx).text_view_style.selection();
360 window.paint_quad(gpui::fill(bounds, color));
361 }
362 }
363}
364
365fn custom_block_is_selected(
371 bounds: Bounds<Pixels>,
372 start: Point<Pixels>,
373 end: Point<Pixels>,
374) -> bool {
375 let (start_inside, end_inside) = (bounds.contains(&start), bounds.contains(&end));
376 start != end
377 && !(start_inside && end_inside)
378 && (start_inside
379 || end_inside
380 || point_in_text_selection(
381 bounds.origin,
382 bounds.size.width,
383 start,
384 end,
385 bounds.size.height,
386 ))
387}
388
389#[derive(Clone, Copy, Debug, PartialEq, Eq)]
393pub(super) enum SelectionStyles {
394 None,
395 One,
396 Mixed,
397}
398
399impl SelectionStyles {
400 fn merge(&mut self, other: Self) {
403 *self = match (*self, other) {
404 (Self::None, other) | (other, Self::None) => other,
405 _ => Self::Mixed,
406 };
407 }
408}
409
410#[derive(Clone, Copy)]
411enum BlockTextKind {
412 All,
413 Selected,
414 SelectedSource,
417}
418
419impl BlockNode {
420 pub(super) fn is_list_item(&self) -> bool {
421 matches!(self, Self::ListItem { .. })
422 }
423
424 pub(super) fn compact(self) -> BlockNode {
426 match self {
427 Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
428 _ => self,
429 }
430 }
431
432 pub fn span(&self) -> Option<Span> {
434 match self {
435 BlockNode::Root { span, .. } => *span,
436 BlockNode::Paragraph(paragraph) => paragraph.span,
437 BlockNode::Heading { span, .. } => *span,
438 BlockNode::Blockquote { span, .. } => *span,
439 BlockNode::List { span, .. } => *span,
440 BlockNode::ListItem { span, .. } => *span,
441 BlockNode::CodeBlock(code_block) => code_block.span,
442 BlockNode::Custom { node, .. } => node.span,
443 BlockNode::Table(table) => table.span,
444 BlockNode::Break { span, .. } => *span,
445 BlockNode::HorizontalRule { span, .. } => *span,
446 BlockNode::Definition { span, .. } => *span,
447 BlockNode::Unknown { .. } => None,
448 }
449 }
450
451 pub(super) fn text(&self) -> String {
452 self.text_by_kind(BlockTextKind::All)
453 }
454
455 pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
460 self.text_by_kind(match format {
461 SelectionFormat::Plain => BlockTextKind::Selected,
462 SelectionFormat::Source | SelectionFormat::SourceAcrossStyles => {
463 BlockTextKind::SelectedSource
464 }
465 })
466 }
467
468 pub(super) fn selection_styles(&self) -> SelectionStyles {
470 let mut styles = SelectionStyles::None;
471 match self {
472 BlockNode::Root { children, .. }
473 | BlockNode::Blockquote { children, .. }
474 | BlockNode::List { children, .. }
475 | BlockNode::ListItem { children, .. } => {
476 for child in children {
477 styles.merge(child.selection_styles());
478 }
479 }
480 BlockNode::Paragraph(paragraph) => styles = paragraph.selection_styles(),
481 BlockNode::Heading { children, .. } => styles = children.selection_styles(),
482 BlockNode::Table(table) => {
483 for row in &table.children {
484 for cell in &row.children {
485 styles.merge(cell.children.selection_styles());
486 }
487 }
488 }
489 BlockNode::CodeBlock(code_block) => {
490 if code_block.has_selection() {
491 styles = SelectionStyles::One;
492 }
493 }
494 BlockNode::Custom { state, .. } => {
496 if state.is_selected() {
497 styles = SelectionStyles::Mixed;
498 }
499 }
500 BlockNode::Definition { .. }
501 | BlockNode::Break { .. }
502 | BlockNode::HorizontalRule { .. }
503 | BlockNode::Unknown => {}
504 }
505 styles
506 }
507
508 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
509 let mut selected = SourceRangeSelection::Unselected;
510 match self {
511 BlockNode::Root { children, .. }
512 | BlockNode::Blockquote { children, .. }
513 | BlockNode::List { children, .. }
514 | BlockNode::ListItem { children, .. } => {
515 for child in children {
516 selected.merge(child.selected_source_range());
517 }
518 }
519 BlockNode::Paragraph(paragraph) => selected = paragraph.selected_source_range(),
520 BlockNode::Heading { children, .. } => selected = children.selected_source_range(),
521 BlockNode::Table(table) => {
522 for row in &table.children {
523 for cell in &row.children {
524 selected.merge(cell.children.selected_source_range());
525 }
526 }
527 }
528 BlockNode::CodeBlock(code_block) => selected = code_block.selected_source_range(),
529 BlockNode::Custom { node, state } => {
530 if state.is_selected() {
531 selected = node
532 .source_range()
533 .map_or(SourceRangeSelection::Unmapped, SourceRangeSelection::Mapped);
534 }
535 }
536 BlockNode::Definition { .. }
537 | BlockNode::Break { .. }
538 | BlockNode::HorizontalRule { .. }
539 | BlockNode::Unknown => {}
540 }
541 selected
542 }
543
544 fn text_by_kind(&self, kind: BlockTextKind) -> String {
545 let mut text = String::new();
546 match self {
547 BlockNode::Root { children, .. } => {
548 let block_text = Self::children_text(children, kind);
549 if !block_text.is_empty() {
550 text.push_str(&block_text);
551 text.push('\n');
552 }
553 }
554 BlockNode::Paragraph(paragraph) => {
555 let block_text = match kind {
556 BlockTextKind::All => paragraph.text(),
557 BlockTextKind::Selected => paragraph.selected_text(),
558 BlockTextKind::SelectedSource => paragraph.selected_source(),
559 };
560 if !block_text.is_empty() {
561 text.push_str(&block_text);
562 text.push('\n');
563 }
564 }
565 BlockNode::Heading {
566 level, children, ..
567 } => {
568 let block_text = match kind {
569 BlockTextKind::All => children.text(),
570 BlockTextKind::Selected => children.selected_text(),
571 BlockTextKind::SelectedSource => children.selected_source(),
572 };
573 if !block_text.is_empty() {
574 if matches!(kind, BlockTextKind::SelectedSource) {
577 text.push_str(&"#".repeat(*level as usize));
578 text.push(' ');
579 }
580 text.push_str(&block_text);
581 text.push('\n');
582 }
583 }
584 BlockNode::List {
585 children,
586 ordered,
587 start,
588 ..
589 } => {
590 if matches!(kind, BlockTextKind::SelectedSource) {
591 text.push_str(&list_selected_source(children, *ordered, *start, ""));
594 } else {
595 text.push_str(&Self::children_text(children, kind));
596 }
597 }
598 BlockNode::ListItem { children, .. } => {
599 text.push_str(&Self::children_text(children, kind));
600 }
601 BlockNode::Blockquote { children, .. } => {
602 let block_text = Self::children_text(children, kind);
603
604 if !block_text.is_empty() {
605 if matches!(kind, BlockTextKind::SelectedSource) {
606 let quoted = block_text
609 .trim_end_matches('\n')
610 .lines()
611 .map(|line| {
612 if line.is_empty() {
613 ">".to_string()
614 } else {
615 format!("> {}", line)
616 }
617 })
618 .collect::<Vec<_>>()
619 .join("\n");
620 text.push_str("ed);
621 } else {
622 text.push_str(&block_text);
623 }
624 text.push('\n');
625 }
626 }
627 BlockNode::Table(table) => {
628 if matches!(kind, BlockTextKind::SelectedSource) {
629 let block_text = table_selected_source(table);
630 if !block_text.is_empty() {
631 text.push_str(&block_text);
632 text.push('\n');
633 }
634 } else {
635 let mut block_text = String::new();
636 for row in table.children.iter() {
637 let mut row_texts = vec![];
638 for cell in row.children.iter() {
639 match kind {
640 BlockTextKind::All => row_texts.push(cell.children.text()),
641 _ => {
645 let selected = cell.children.selected_text();
646 if !selected.is_empty() {
647 row_texts.push(selected);
648 }
649 }
650 }
651 }
652 if !row_texts.is_empty() {
653 block_text.push_str(&row_texts.join(" "));
654 block_text.push('\n');
655 }
656 }
657
658 if !block_text.is_empty() {
659 text.push_str(&block_text);
660 text.push('\n');
661 }
662 }
663 }
664 BlockNode::CodeBlock(code_block) => {
665 let block_text = match kind {
666 BlockTextKind::All => code_block.text(),
667 BlockTextKind::Selected => code_block.selected_text(),
668 BlockTextKind::SelectedSource => code_block.selected_source(),
669 };
670 if !block_text.is_empty() {
671 text.push_str(&block_text);
672 text.push('\n');
673 }
674 }
675 BlockNode::Custom { node, state } => {
676 if matches!(kind, BlockTextKind::All) || state.is_selected() {
677 let content = match kind {
678 BlockTextKind::SelectedSource => Cow::Owned(node.to_markdown()),
679 _ => Cow::Borrowed(node.as_text()),
680 };
681 if !content.is_empty() {
682 text.push_str(&content);
683 text.push('\n');
684 }
685 }
686 }
687 BlockNode::Definition { .. }
688 | BlockNode::Break { .. }
689 | BlockNode::HorizontalRule { .. }
690 | BlockNode::Unknown { .. } => {}
691 }
692
693 text
694 }
695
696 fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
697 let mut text = String::new();
698 for child in children.iter() {
699 text.push_str(&child.text_by_kind(kind));
700 }
701
702 text
703 }
704
705 pub(super) fn has_selection(&self) -> bool {
716 match self {
717 BlockNode::Root { children, .. }
718 | BlockNode::Blockquote { children, .. }
719 | BlockNode::List { children, .. }
720 | BlockNode::ListItem { children, .. } => {
721 children.iter().any(|child| child.has_selection())
722 }
723 BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
724 BlockNode::Heading { children, .. } => children.has_selection(),
725 BlockNode::Table(table) => table.children.iter().any(|row| {
726 row.children
727 .iter()
728 .any(|cell| cell.children.has_selection())
729 }),
730 BlockNode::CodeBlock(code_block) => code_block.has_selection(),
731 BlockNode::Custom { state, .. } => {
732 state.has_selection_observation()
734 }
735 BlockNode::Definition { .. }
736 | BlockNode::Break { .. }
737 | BlockNode::HorizontalRule { .. }
738 | BlockNode::Unknown { .. } => false,
739 }
740 }
741
742 pub(super) fn clear_selection(&self) {
743 match self {
744 BlockNode::Root { children, .. }
745 | BlockNode::Blockquote { children, .. }
746 | BlockNode::List { children, .. }
747 | BlockNode::ListItem { children, .. } => {
748 for child in children.iter() {
749 child.clear_selection();
750 }
751 }
752 BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
753 BlockNode::Heading { children, .. } => children.clear_selection(),
754 BlockNode::Table(table) => {
755 for row in table.children.iter() {
756 for cell in row.children.iter() {
757 cell.children.clear_selection();
758 }
759 }
760 }
761 BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
762 BlockNode::Custom { state, .. } => {
763 state.clear_selection();
764 }
765 BlockNode::Definition { .. }
766 | BlockNode::Break { .. }
767 | BlockNode::HorizontalRule { .. }
768 | BlockNode::Unknown { .. } => {}
769 }
770 }
771}
772
773#[allow(unused)]
774#[derive(Debug, Default, Clone, PartialEq)]
775pub struct LinkMark {
776 pub url: SharedString,
777 pub identifier: Option<SharedString>,
779 pub title: Option<SharedString>,
780}
781
782#[derive(Debug, Default, Clone, PartialEq)]
783pub struct TextMark {
784 pub bold: bool,
785 pub italic: bool,
786 pub strikethrough: bool,
787 pub underline: bool,
788 pub code: bool,
789 pub highlight: Option<Hsla>,
793 pub link: Option<LinkMark>,
794}
795
796impl TextMark {
797 pub fn bold(mut self) -> Self {
798 self.bold = true;
799 self
800 }
801
802 pub fn italic(mut self) -> Self {
803 self.italic = true;
804 self
805 }
806
807 pub fn strikethrough(mut self) -> Self {
808 self.strikethrough = true;
809 self
810 }
811
812 pub fn underline(mut self) -> Self {
813 self.underline = true;
814 self
815 }
816
817 pub fn code(mut self) -> Self {
818 self.code = true;
819 self
820 }
821
822 pub fn highlight(mut self, color: Hsla) -> Self {
824 self.highlight = Some(color);
825 self
826 }
827
828 pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
829 self.link = Some(link.into());
830 self
831 }
832
833 pub fn merge(&mut self, other: TextMark) {
834 self.bold |= other.bold;
835 self.italic |= other.italic;
836 self.strikethrough |= other.strikethrough;
837 self.underline |= other.underline;
838 self.code |= other.code;
839 if other.highlight.is_some() {
840 self.highlight = other.highlight;
841 }
842 if let Some(link) = other.link {
843 self.link = Some(link);
844 }
845 }
846}
847
848#[derive(Debug, Default, Copy, Clone, PartialEq)]
850pub struct Span {
851 pub start: usize,
852 pub end: usize,
853}
854
855#[allow(unused)]
856#[derive(Default, Clone)]
857pub struct ImageNode {
858 pub url: SharedUri,
859 pub link: Option<LinkMark>,
860 pub title: Option<SharedString>,
861 pub alt: Option<SharedString>,
862 pub width: Option<DefiniteLength>,
863 pub height: Option<DefiniteLength>,
864 pub(crate) span: Option<Span>,
865 pub(super) embedded: OnceLock<Option<Arc<Image>>>,
868}
869
870impl ImageNode {
871 pub fn title(&self) -> String {
872 self.title
873 .clone()
874 .unwrap_or_else(|| self.alt.clone().unwrap_or_default())
875 .to_string()
876 }
877
878 pub(super) fn source(&self) -> ImageSource {
886 match self.embedded.get_or_init(|| data_url_image(&self.url)) {
887 Some(image) => ImageSource::Image(image.clone()),
888 None => self.url.clone().into(),
889 }
890 }
891}
892
893impl std::fmt::Debug for ImageNode {
894 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
895 f.debug_struct("ImageNode")
896 .field("url", &self.url)
897 .field("link", &self.link)
898 .field("title", &self.title)
899 .field("alt", &self.alt)
900 .field("width", &self.width)
901 .field("height", &self.height)
902 .field("span", &self.span)
903 .finish()
904 }
905}
906
907impl PartialEq for ImageNode {
908 fn eq(&self, other: &Self) -> bool {
909 self.url == other.url
910 && self.link == other.link
911 && self.title == other.title
912 && self.alt == other.alt
913 && self.width == other.width
914 && self.height == other.height
915 && self.span == other.span
916 }
917}
918
919#[derive(Debug, Clone, PartialEq)]
920pub(crate) struct SourceSegment {
921 pub(crate) rendered: Range<usize>,
922 pub(crate) source: Range<usize>,
923 pub(crate) linear: bool,
928}
929
930pub(crate) enum SourceRangeSelection {
931 Unselected,
932 Mapped(Range<usize>),
933 Unmapped,
934}
935
936impl SourceRangeSelection {
937 pub(crate) fn merge(&mut self, other: Self) {
938 match (&mut *self, other) {
939 (_, Self::Unselected) => {}
940 (_, Self::Unmapped) => *self = Self::Unmapped,
941 (Self::Unselected, mapped @ Self::Mapped(_)) => *self = mapped,
942 (Self::Mapped(selected), Self::Mapped(range)) => {
943 selected.start = selected.start.min(range.start);
944 selected.end = selected.end.max(range.end);
945 }
946 (Self::Unmapped, Self::Mapped(_)) => {}
947 }
948 }
949
950 pub(crate) fn into_range(self) -> Option<Range<usize>> {
951 match self {
952 Self::Mapped(range) => Some(range),
953 Self::Unselected | Self::Unmapped => None,
954 }
955 }
956}
957
958fn source_range_for_segments(
959 segments: &[SourceSegment],
960 selection: Range<usize>,
961) -> Option<Range<usize>> {
962 fn mapped_source_start(segment: &SourceSegment, rendered_start: usize) -> usize {
963 if segment.linear {
964 segment.source.start + rendered_start.saturating_sub(segment.rendered.start)
965 } else {
966 segment.source.start
967 }
968 }
969
970 fn mapped_source_end(segment: &SourceSegment, rendered_end: usize) -> usize {
971 if segment.linear {
972 segment.source.start
973 + rendered_end
974 .min(segment.rendered.end)
975 .saturating_sub(segment.rendered.start)
976 } else {
977 segment.source.end
978 }
979 }
980
981 if selection.start >= selection.end {
982 return None;
983 }
984
985 let mut overlapping = segments.iter().filter(|segment| {
986 segment.rendered.start < selection.end && segment.rendered.end > selection.start
987 });
988 let first = overlapping.next()?;
989 if first.rendered.start > selection.start {
990 return None;
991 }
992 let mut rendered_end = first.rendered.end;
993 let source_start = mapped_source_start(first, selection.start);
994 let mut source_end = mapped_source_end(first, selection.end);
995 for segment in overlapping {
996 if segment.rendered.start > rendered_end {
997 return None;
998 }
999 rendered_end = rendered_end.max(segment.rendered.end);
1000 source_end = mapped_source_end(segment, selection.end);
1001 }
1002 (rendered_end >= selection.end).then_some(source_start..source_end)
1003}
1004
1005#[derive(Default, Clone, Debug)]
1008pub struct InlineNode {
1009 pub(crate) text: SharedString,
1011 pub(crate) image: Option<ImageNode>,
1012 pub(crate) custom: Option<MarkdownNode>,
1013 custom_selection: Arc<Mutex<bool>>,
1014 pub(crate) marks: Vec<(Range<usize>, TextMark)>,
1016 pub(crate) source_segments: Vec<SourceSegment>,
1018
1019 pub(super) state: Arc<Mutex<InlineState>>,
1020}
1021
1022impl PartialEq for InlineNode {
1023 fn eq(&self, other: &Self) -> bool {
1024 self.text == other.text
1025 && self.image == other.image
1026 && self.custom == other.custom
1027 && self.marks == other.marks
1028 && self.source_segments == other.source_segments
1029 }
1030}
1031
1032fn set_leaf_key(state: &mut Arc<Mutex<InlineState>>, key: u64) {
1033 let text = state
1034 .lock()
1035 .map(|state| state.text.clone())
1036 .unwrap_or_default();
1037 *state = Arc::new(Mutex::new(InlineState {
1040 text,
1041 editor_key: Some(key),
1042 cleanup_senders: Vec::new(),
1043 ..Default::default()
1044 }));
1045}
1046
1047pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
1054 if text.is_empty() {
1055 return String::new();
1056 }
1057
1058 let mut out = text.to_string();
1059 if mark.code {
1060 out = format!("`{}`", out);
1061 }
1062 if mark.italic {
1063 out = format!("*{}*", out);
1064 }
1065 if mark.bold {
1066 out = format!("**{}**", out);
1067 }
1068 if mark.strikethrough {
1069 out = format!("~~{}~~", out);
1070 }
1071 if mark.underline {
1072 out = format!("<u>{}</u>", out);
1075 }
1076 if mark.highlight.is_some() {
1077 out = format!("=={}==", out);
1078 }
1079 if let Some(link) = &mark.link {
1080 out = match &link.title {
1081 Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
1082 None => format!("[{}]({})", out, link.url),
1083 };
1084 }
1085 out
1086}
1087
1088#[derive(Default)]
1092struct MarkdownSource {
1093 pieces: Vec<(String, Vec<TextMark>)>,
1094 has_objects: bool,
1097}
1098
1099impl MarkdownSource {
1100 fn push_str(&mut self, source: &str) {
1101 self.push_marked(source, &TextMark::default());
1102 }
1103
1104 fn push_marked(&mut self, source: &str, mark: &TextMark) {
1105 if source.is_empty() {
1106 return;
1107 }
1108 let mut layers = Vec::new();
1111 if let Some(link) = &mark.link {
1112 layers.push(TextMark {
1113 link: Some(link.clone()),
1114 ..Default::default()
1115 });
1116 }
1117 if let Some(highlight) = mark.highlight {
1118 layers.push(TextMark {
1119 highlight: Some(highlight),
1120 ..Default::default()
1121 });
1122 }
1123 if mark.underline {
1124 layers.push(TextMark::default().underline());
1125 }
1126 if mark.strikethrough {
1127 layers.push(TextMark::default().strikethrough());
1128 }
1129 if mark.bold {
1130 layers.push(TextMark::default().bold());
1131 }
1132 if mark.italic {
1133 layers.push(TextMark::default().italic());
1134 }
1135 if mark.code {
1136 layers.push(TextMark::default().code());
1137 }
1138 self.pieces.push((source.to_string(), layers));
1139 }
1140
1141 fn push_text(
1142 &mut self,
1143 text: &str,
1144 marks: &[(Range<usize>, TextMark)],
1145 selection: Range<usize>,
1146 ) {
1147 let start = selection.start.min(text.len());
1148 let end = selection.end.min(text.len());
1149 if start >= end {
1150 return;
1151 }
1152 let mut cursor = start;
1153 for (range, mark) in marks {
1154 let lo = range.start.max(start);
1155 let hi = range.end.min(end);
1156 if lo >= hi {
1157 continue;
1158 }
1159 if cursor < lo {
1160 self.push_str(&text[cursor..lo]);
1161 }
1162 self.push_marked(&text[lo..hi], mark);
1163 cursor = hi;
1164 }
1165 if cursor < end {
1166 self.push_str(&text[cursor..end]);
1167 }
1168 }
1169
1170 fn push_images(&mut self, images: &[String]) {
1171 if images.is_empty() {
1172 return;
1173 }
1174 self.has_objects = true;
1175 self.push_str(&images.join(""));
1176 }
1177
1178 fn push_object(&mut self, node: &InlineNode) {
1179 self.has_objects = true;
1180 let mut mark = TextMark::default();
1181 for (_, layer) in &node.marks {
1182 mark.merge(layer.clone());
1183 }
1184 self.push_marked(&node.custom.as_ref().unwrap().to_markdown(), &mark);
1185 }
1186
1187 fn is_empty(&self) -> bool {
1188 self.pieces.is_empty()
1189 }
1190
1191 fn styles(&self) -> SelectionStyles {
1192 if self.is_empty() {
1193 SelectionStyles::None
1194 } else if !self.has_objects && self.pieces.windows(2).all(|pair| pair[0].1 == pair[1].1) {
1195 SelectionStyles::One
1196 } else {
1197 SelectionStyles::Mixed
1198 }
1199 }
1200
1201 fn finish(mut self) -> String {
1202 fn write(pieces: &mut [(String, Vec<TextMark>)]) -> String {
1203 let mut out = String::new();
1204 let mut offset = 0;
1205 while offset < pieces.len() {
1206 let Some((mark, count)) = pieces[offset]
1207 .1
1208 .iter()
1209 .map(|mark| {
1210 let count = pieces[offset..]
1211 .iter()
1212 .take_while(|(_, layers)| layers.contains(mark))
1213 .count();
1214 (mark.clone(), count)
1215 })
1216 .reduce(|best, candidate| {
1217 if candidate.1 > best.1 {
1218 candidate
1219 } else {
1220 best
1221 }
1222 })
1223 else {
1224 out.push_str(&pieces[offset].0);
1225 offset += 1;
1226 continue;
1227 };
1228 let group = &mut pieces[offset..offset + count];
1229 for (_, layers) in group.iter_mut() {
1230 layers.retain(|layer| layer != &mark);
1231 }
1232 let content = write(group);
1233 out.push_str(&wrap_with_mark(&content, &mark));
1234 offset += count;
1235 }
1236 out
1237 }
1238 write(&mut self.pieces)
1239 }
1240}
1241
1242#[derive(Default)]
1245struct RunSelection {
1246 emitted: bool,
1247 at_start: bool,
1248 at_end: bool,
1249}
1250
1251fn emit_run(
1255 state: &Arc<Mutex<InlineState>>,
1256 run: &[(usize, &InlineNode)],
1257 pending_images: &mut Vec<String>,
1258 out: &mut MarkdownSource,
1259) -> RunSelection {
1260 let mut selected = RunSelection::default();
1261 let Ok(state) = state.lock() else {
1262 return selected;
1263 };
1264 let Some(selection) = &state.selection else {
1265 return selected;
1266 };
1267 if selection.start >= selection.end {
1268 return selected;
1269 }
1270
1271 selected.at_start = selection.start == 0;
1272 selected.at_end = selection.end >= state.text.len();
1273
1274 for (start, child) in run {
1275 let end = start + child.text.len();
1276 let lo = selection.start.max(*start);
1277 let hi = selection.end.min(end);
1278 if lo >= hi {
1279 continue;
1280 }
1281
1282 if !selected.emitted {
1283 if selected.at_start {
1284 out.push_images(pending_images);
1285 }
1286 pending_images.clear();
1287 }
1288 selected.emitted = true;
1289
1290 out.push_text(&child.text, &child.marks, (lo - start)..(hi - start));
1291 }
1292
1293 selected
1294}
1295
1296fn image_markdown(image: &ImageNode) -> String {
1298 let alt = image.alt.clone().unwrap_or_default();
1299 let title = image
1300 .title
1301 .clone()
1302 .map_or(String::new(), |title| format!(" \"{}\"", title));
1303 format!("", alt, image.url, title)
1304}
1305
1306#[cfg(test)]
1315pub(crate) fn reconstruct_markdown(
1316 text: &str,
1317 marks: &[(Range<usize>, TextMark)],
1318 selection: Range<usize>,
1319) -> String {
1320 let mut source = MarkdownSource::default();
1321 source.push_text(text, marks, selection);
1322 source.finish()
1323}
1324
1325fn table_selected_source(table: &Table) -> String {
1333 let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
1334
1335 let mut selected = table
1336 .children
1337 .iter()
1338 .flat_map(|row| row.children.iter())
1339 .map(cell_source)
1340 .filter(|source| !source.trim().is_empty());
1341 match (selected.next(), selected.next()) {
1342 (None, _) => return String::new(),
1343 (Some(only), None) => return only.trim().to_string(),
1344 _ => {}
1345 }
1346
1347 let mut lines: Vec<String> = Vec::new();
1348 for (row_ix, row) in table.children.iter().enumerate() {
1349 let cells: Vec<String> = row
1350 .children
1351 .iter()
1352 .map(|cell| cell_source(cell).trim().to_string())
1353 .collect();
1354 lines.push(format!("| {} |", cells.join(" | ")));
1355
1356 if row_ix == 0 {
1359 let aligns: Vec<String> = (0..row.children.len())
1360 .map(|ix| {
1361 match table.column_align(ix) {
1362 ColumnumnAlign::None | ColumnumnAlign::Left => ":--",
1363 ColumnumnAlign::Center => ":-:",
1364 ColumnumnAlign::Right => "--:",
1365 }
1366 .to_string()
1367 })
1368 .collect();
1369 lines.push(format!("| {} |", aligns.join(" | ")));
1370 }
1371 }
1372
1373 lines.join("\n")
1374}
1375
1376fn list_selected_source(
1386 children: &[BlockNode],
1387 ordered: bool,
1388 start: Option<u32>,
1389 indent: &str,
1390) -> String {
1391 let mut out = String::new();
1392 let mut item_ix = 0usize;
1393
1394 for child in children {
1395 let BlockNode::ListItem {
1396 children: item_children,
1397 checked,
1398 ..
1399 } = child
1400 else {
1401 continue;
1402 };
1403
1404 let marker = if ordered {
1405 format!("{}. ", ordered_list_ordinal(start, item_ix))
1406 } else {
1407 "- ".to_string()
1408 };
1409 let checkbox = match checked {
1410 Some(true) => "[x] ",
1411 Some(false) => "[ ] ",
1412 None => "",
1413 };
1414 let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
1415
1416 let mut content = String::new();
1419 let mut nested = String::new();
1420 for sub in item_children {
1421 if let BlockNode::List {
1422 children: sub_children,
1423 ordered: sub_ordered,
1424 start: sub_start,
1425 ..
1426 } = sub
1427 {
1428 nested.push_str(&list_selected_source(
1429 sub_children,
1430 *sub_ordered,
1431 *sub_start,
1432 &child_indent,
1433 ));
1434 } else {
1435 content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
1436 }
1437 }
1438 let content = content.trim_end_matches('\n');
1439
1440 if content.is_empty() && nested.is_empty() {
1441 item_ix += 1;
1442 continue;
1443 }
1444
1445 if content.is_empty() {
1446 out.push_str(indent);
1448 out.push_str(&marker);
1449 out.push_str(checkbox.trim_end());
1450 out.push('\n');
1451 } else {
1452 let mut lines = content.lines();
1455 if let Some(first) = lines.next() {
1456 out.push_str(indent);
1457 out.push_str(&marker);
1458 out.push_str(checkbox);
1459 out.push_str(first);
1460 out.push('\n');
1461 }
1462 for line in lines {
1463 out.push_str(&child_indent);
1464 out.push_str(line);
1465 out.push('\n');
1466 }
1467 }
1468 out.push_str(&nested);
1469 item_ix += 1;
1470 }
1471
1472 out
1473}
1474
1475impl InlineNode {
1476 pub fn new(text: impl Into<SharedString>) -> Self {
1478 Self {
1479 text: text.into(),
1480 image: None,
1481 custom: None,
1482 custom_selection: Arc::default(),
1483 marks: vec![],
1484 source_segments: vec![],
1485 state: Arc::new(Mutex::new(InlineState::default())),
1486 }
1487 }
1488
1489 pub fn custom(node: MarkdownNode) -> Self {
1491 let mut this = Self::new(node.as_text().to_string());
1492 this.custom = Some(node);
1493 this
1494 }
1495
1496 pub fn image(image: ImageNode) -> Self {
1498 let mut this = Self::new("");
1499 this.image = Some(image);
1500 this
1501 }
1502
1503 pub fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
1505 self.marks = marks;
1506 self
1507 }
1508
1509 pub fn text(&self) -> &str {
1511 &self.text
1512 }
1513
1514 pub fn as_image(&self) -> Option<&ImageNode> {
1516 self.image.as_ref()
1517 }
1518
1519 pub fn as_custom(&self) -> Option<&MarkdownNode> {
1521 self.custom.as_ref()
1522 }
1523
1524 pub fn mark_runs(&self) -> &[(Range<usize>, TextMark)] {
1526 &self.marks
1527 }
1528
1529 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
1530 self.source_segments = source_segments;
1531 self
1532 }
1533
1534 fn selected_source_range(&self, selection: Range<usize>) -> Option<Range<usize>> {
1535 source_range_for_segments(&self.source_segments, selection)
1536 }
1537}
1538
1539#[derive(Debug, Clone, Default)]
1544pub struct Paragraph {
1545 pub(super) span: Option<Span>,
1546 pub(super) children: Vec<InlineNode>,
1547 pub(super) link_refs: HashMap<SharedString, SharedString>,
1551
1552 pub(crate) state: Arc<Mutex<InlineState>>,
1553 pub(super) render_cache: ParagraphRenderCache,
1556}
1557
1558#[derive(Default)]
1566pub(super) struct ParagraphRenderCache(Mutex<Option<Arc<ParagraphRender>>>);
1567
1568impl Clone for ParagraphRenderCache {
1569 fn clone(&self) -> Self {
1570 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1571 Self(Mutex::new(cached))
1572 }
1573}
1574
1575impl std::fmt::Debug for ParagraphRenderCache {
1576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1577 f.write_str("ParagraphRenderCache")
1578 }
1579}
1580
1581struct ParagraphRender {
1591 style: Arc<TextViewStyle>,
1592 mono_font: SharedString,
1593 text: SharedString,
1594 highlights: Vec<(Range<usize>, InlineHighlight)>,
1595 links: Vec<(Range<usize>, LinkMark)>,
1597}
1598
1599impl PartialEq for Paragraph {
1600 fn eq(&self, other: &Self) -> bool {
1601 self.span == other.span
1602 && self.children == other.children
1603 && self.link_refs == other.link_refs
1604 }
1605}
1606
1607impl Paragraph {
1608 pub(crate) fn new(text: String) -> Self {
1609 Self {
1610 span: None,
1611 children: vec![InlineNode::new(&text)],
1612 link_refs: HashMap::new(),
1613 state: Arc::new(Mutex::new(InlineState::default())),
1614 render_cache: ParagraphRenderCache::default(),
1615 }
1616 }
1617
1618 fn plain_render(
1621 &self,
1622 node_cx: &NodeContext,
1623 cx: &App,
1624 ) -> (
1625 SharedString,
1626 Vec<(Range<usize>, InlineHighlight)>,
1627 Vec<(Range<usize>, LinkMark)>,
1628 ) {
1629 let mono_font = cx.theme().tokens.typography.mono.clone();
1630 if let Ok(cache) = self.render_cache.0.lock()
1631 && let Some(cached) = cache.as_ref()
1632 && (Arc::ptr_eq(&cached.style, &node_cx.style) || *cached.style == *node_cx.style)
1633 && cached.mono_font == mono_font
1634 {
1635 return (
1636 cached.text.clone(),
1637 cached.highlights.clone(),
1638 cached.links.clone(),
1639 );
1640 }
1641
1642 let mut text = String::new();
1643 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1644 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1645 let mut offset = 0;
1646 for inline_node in &self.children {
1647 let text_len = inline_node.text.len();
1648 text.push_str(&inline_node.text);
1649 let mut node_highlights = vec![];
1650 for (range, style) in &inline_node.marks {
1651 let inner_range = (offset + range.start)..(offset + range.end);
1652 let mut highlight = mark_highlight(style, node_cx, cx);
1653 if let Some(link_mark) = style.link.clone() {
1654 highlight.style.color = Some(node_cx.style.link());
1655 highlight.style.underline = Some(link_underline(&node_cx.style));
1656 links.push((inner_range.clone(), link_mark));
1657 }
1658 node_highlights.push((inner_range, highlight));
1659 }
1660 highlights = combine_highlights(highlights, node_highlights);
1661 offset += text_len;
1662 }
1663 let text = SharedString::from(text);
1664 if let Ok(mut cache) = self.render_cache.0.lock() {
1665 *cache = Some(Arc::new(ParagraphRender {
1666 style: node_cx.style.clone(),
1667 mono_font,
1668 text: text.clone(),
1669 highlights: highlights.clone(),
1670 links: links.clone(),
1671 }));
1672 }
1673 (text, highlights, links)
1674 }
1675
1676 pub(super) fn selected_text(&self) -> String {
1677 let mut text = String::new();
1678
1679 for c in self.children.iter() {
1680 let Ok(state) = c.state.lock() else {
1681 continue;
1682 };
1683 if let Some(selection) = &state.selection {
1684 text.push_str(&state.text[selection.start..selection.end]);
1685 }
1686 if let Some(custom) = &c.custom
1687 && c.custom_selection.lock().is_ok_and(|selected| *selected)
1688 {
1689 text.push_str(custom.as_text());
1690 }
1691 }
1692
1693 if let Ok(state) = self.state.lock()
1694 && let Some(selection) = &state.selection
1695 {
1696 text.push_str(&state.text[selection.start..selection.end]);
1697 }
1698
1699 text
1700 }
1701
1702 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1704 let mut selected = SourceRangeSelection::Unselected;
1705 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1706 let mut offset = 0;
1707 let mut pending_images: Vec<Option<Span>> = Vec::new();
1708 let mut enters_image = true;
1709
1710 let include_run = |state: &Arc<Mutex<InlineState>>,
1711 run: &[(usize, &InlineNode)]|
1712 -> (SourceRangeSelection, RunSelection) {
1713 let Ok(state) = state.lock() else {
1714 return (SourceRangeSelection::Unmapped, RunSelection::default());
1715 };
1716 let Some(selection) = state.selection else {
1717 return (SourceRangeSelection::Unselected, RunSelection::default());
1718 };
1719 if selection.start >= selection.end {
1720 return (SourceRangeSelection::Unselected, RunSelection::default());
1721 }
1722
1723 let mut run_selection = RunSelection {
1724 at_start: selection.start == 0,
1725 at_end: selection.end >= state.text.len(),
1726 ..Default::default()
1727 };
1728 let mut mapped = SourceRangeSelection::Unselected;
1729 let mut rendered_end = selection.start;
1730 for (start, child) in run {
1731 let end = start + child.text.len();
1732 let lo = selection.start.max(*start);
1733 let hi = selection.end.min(end);
1734 if lo >= hi {
1735 continue;
1736 }
1737 run_selection.emitted = true;
1738 if lo > rendered_end {
1739 return (SourceRangeSelection::Unmapped, run_selection);
1740 }
1741 let Some(range) = child.selected_source_range((lo - start)..(hi - start)) else {
1742 return (SourceRangeSelection::Unmapped, run_selection);
1743 };
1744 mapped.merge(SourceRangeSelection::Mapped(range));
1745 rendered_end = rendered_end.max(hi);
1746 }
1747 if rendered_end < selection.end {
1748 (SourceRangeSelection::Unmapped, run_selection)
1749 } else {
1750 (mapped, run_selection)
1751 }
1752 };
1753
1754 let merge_images = |selected: &mut SourceRangeSelection, images: &mut Vec<Option<Span>>| {
1755 for span in images.drain(..) {
1756 selected.merge(
1757 span.map(|span| SourceRangeSelection::Mapped(span.start..span.end))
1758 .unwrap_or(SourceRangeSelection::Unmapped),
1759 );
1760 }
1761 };
1762
1763 for child in &self.children {
1764 if child.custom.is_some() {
1765 let (run_range, run_selection) = include_run(&child.state, &run);
1766 if run_selection.emitted && run_selection.at_start {
1767 merge_images(&mut selected, &mut pending_images);
1768 }
1769 selected.merge(run_range);
1770
1771 match child.custom_selection.lock() {
1772 Ok(value) if *value => {
1773 if run.is_empty() || (run_selection.emitted && run_selection.at_end) {
1774 merge_images(&mut selected, &mut pending_images);
1775 }
1776 selected.merge(
1777 child
1778 .custom
1779 .as_ref()
1780 .and_then(MarkdownNode::source_range)
1781 .map(SourceRangeSelection::Mapped)
1782 .unwrap_or(SourceRangeSelection::Unmapped),
1783 );
1784 enters_image = true;
1785 }
1786 Ok(_) => enters_image = false,
1787 Err(_) => {
1788 selected.merge(SourceRangeSelection::Unmapped);
1789 enters_image = false;
1790 }
1791 }
1792 pending_images.clear();
1793 run.clear();
1794 offset = 0;
1795 continue;
1796 }
1797 if let Some(image) = &child.image {
1798 let run_before = !run.is_empty();
1799 let (run_range, run_selection) = include_run(&child.state, &run);
1800 if run_selection.emitted && run_selection.at_start {
1801 merge_images(&mut selected, &mut pending_images);
1802 }
1803 selected.merge(run_range);
1804 if run_before {
1805 enters_image = run_selection.emitted && run_selection.at_end;
1806 }
1807 if enters_image {
1808 pending_images.push(image.span);
1809 } else {
1810 pending_images.clear();
1811 }
1812 run.clear();
1813 offset = 0;
1814 continue;
1815 }
1816 run.push((offset, child));
1817 offset += child.text.len();
1818 }
1819
1820 let (trailing_range, trailing) = include_run(&self.state, &run);
1821 if trailing.emitted && trailing.at_start {
1822 merge_images(&mut selected, &mut pending_images);
1823 }
1824 selected.merge(trailing_range);
1825 if !trailing.emitted
1826 && enters_image
1827 && !matches!(selected, SourceRangeSelection::Unselected)
1828 {
1829 merge_images(&mut selected, &mut pending_images);
1830 }
1831 selected
1832 }
1833
1834 pub(super) fn selected_source(&self) -> String {
1854 self.selected_markdown().finish()
1855 }
1856
1857 pub(super) fn selection_styles(&self) -> SelectionStyles {
1859 self.selected_markdown().styles()
1860 }
1861
1862 fn selected_markdown(&self) -> MarkdownSource {
1865 let mut source = MarkdownSource::default();
1866 let mut pending_images: Vec<String> = Vec::new();
1867 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1868 let mut offset = 0;
1869 let mut enters_image = true;
1870
1871 for child in self.children.iter() {
1872 if child.custom.is_some() {
1873 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1874 let object_selected = child
1875 .custom_selection
1876 .lock()
1877 .is_ok_and(|selected| *selected);
1878 if object_selected {
1879 if run.is_empty() || (selected.emitted && selected.at_end) {
1880 source.push_images(&pending_images);
1881 }
1882 source.push_object(child);
1883 }
1884 pending_images.clear();
1885 enters_image = object_selected;
1886 run.clear();
1887 offset = 0;
1888 continue;
1889 }
1890 let Some(image) = &child.image else {
1891 run.push((offset, child));
1892 offset += child.text.len();
1893 continue;
1894 };
1895
1896 let run_before = !run.is_empty();
1898 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1899 if run_before {
1900 enters_image = selected.emitted && selected.at_end;
1901 }
1902 if enters_image {
1903 pending_images.push(image_markdown(image));
1904 } else {
1905 pending_images.clear();
1906 }
1907
1908 run.clear();
1909 offset = 0;
1910 }
1911
1912 let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
1913 if !trailing.emitted && enters_image && !source.is_empty() {
1915 source.push_images(&pending_images);
1916 }
1917
1918 source
1919 }
1920
1921 pub(super) fn text(&self) -> String {
1922 let mut text = String::new();
1923 for node in self.children.iter() {
1924 text.push_str(&node.text);
1925 }
1926 text
1927 }
1928
1929 pub(super) fn has_selection(&self) -> bool {
1933 self.children.iter().any(|c| {
1934 c.state.lock().is_ok_and(|state| state.selection.is_some())
1935 || c.custom_selection.lock().is_ok_and(|selected| *selected)
1936 }) || self
1937 .state
1938 .lock()
1939 .is_ok_and(|state| state.selection.is_some())
1940 }
1941
1942 pub(super) fn clear_selection(&self) {
1943 for c in self.children.iter() {
1944 if let Ok(mut selected) = c.custom_selection.lock() {
1945 *selected = false;
1946 }
1947 if let Ok(mut state) = c.state.lock() {
1948 state.selection = None;
1949 }
1950 }
1951
1952 if let Ok(mut state) = self.state.lock() {
1953 state.selection = None;
1954 }
1955 }
1956}
1957
1958#[derive(Debug, Clone, Default, PartialEq)]
1960pub struct Table {
1961 pub(crate) children: Vec<TableRow>,
1962 pub(crate) column_aligns: Vec<ColumnumnAlign>,
1963 pub(crate) span: Option<Span>,
1964 pub(crate) table_data_cache: TableDataCache,
1967 pub(crate) column_widths_cache: TableColumnWidthsCache,
1968}
1969
1970#[derive(Default)]
1978pub(crate) struct TableDataCache(Mutex<Option<Arc<TableData>>>);
1979
1980impl Clone for TableDataCache {
1981 fn clone(&self) -> Self {
1982 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1983 Self(Mutex::new(cached))
1984 }
1985}
1986
1987impl PartialEq for TableDataCache {
1988 fn eq(&self, _: &Self) -> bool {
1989 true
1990 }
1991}
1992
1993impl std::fmt::Debug for TableDataCache {
1994 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1995 f.write_str("TableDataCache")
1996 }
1997}
1998
1999#[derive(Default)]
2002pub(crate) struct TableColumnWidthsCache(Mutex<Option<Arc<TableColumnWidths>>>);
2003
2004impl Clone for TableColumnWidthsCache {
2005 fn clone(&self) -> Self {
2006 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
2007 Self(Mutex::new(cached))
2008 }
2009}
2010
2011impl PartialEq for TableColumnWidthsCache {
2012 fn eq(&self, _: &Self) -> bool {
2013 true
2014 }
2015}
2016
2017impl std::fmt::Debug for TableColumnWidthsCache {
2018 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2019 f.write_str("TableColumnWidthsCache")
2020 }
2021}
2022
2023struct TableColumnWidths {
2024 text_system: Weak<gpui::WindowTextSystem>,
2025 text_styles: TableTextStyles,
2027 rem_size: Pixels,
2028 mono_family: SharedString,
2029 inline_code: HighlightStyle,
2030 widths: Vec<f32>,
2032 custom_cells: Vec<(usize, usize)>,
2033}
2034
2035#[derive(Clone, PartialEq)]
2039struct TableTextStyles {
2040 head: gpui::TextStyle,
2041 body: gpui::TextStyle,
2042 head_refinement: TextStyleRefinement,
2043 body_refinement: TextStyleRefinement,
2044}
2045
2046impl TableTextStyles {
2047 fn new(style: &TextViewStyle, window: &Window) -> Self {
2048 let mut body_refinement = style.table().text.clone();
2049 let mut head_refinement = body_refinement.clone();
2050 head_refinement.refine(&style.table_head().text);
2051 head_refinement.refine(&style.table_cell().text);
2052 body_refinement.refine(&style.table_cell().text);
2053 let mut head = window.text_style();
2054 let mut body = head.clone();
2055 head.refine(&head_refinement);
2056 body.refine(&body_refinement);
2057 Self {
2058 head,
2059 body,
2060 head_refinement,
2061 body_refinement,
2062 }
2063 }
2064
2065 fn row(&self, row_ix: usize) -> &gpui::TextStyle {
2066 if row_ix == 0 { &self.head } else { &self.body }
2067 }
2068
2069 fn row_refinement(&self, row_ix: usize) -> &TextStyleRefinement {
2070 if row_ix == 0 {
2071 &self.head_refinement
2072 } else {
2073 &self.body_refinement
2074 }
2075 }
2076}
2077
2078#[derive(Debug, Clone, Default, PartialEq)]
2081pub struct TableData {
2082 pub headers: Vec<String>,
2084 pub rows: Vec<Vec<String>>,
2087 pub markdown: String,
2089 pub span: Option<Range<usize>>,
2096}
2097
2098impl Table {
2099 pub fn new(rows: Vec<Vec<Paragraph>>, column_aligns: Vec<ColumnumnAlign>) -> Self {
2101 Self {
2102 children: rows
2103 .into_iter()
2104 .map(|row| TableRow {
2105 children: row
2106 .into_iter()
2107 .map(|children| TableCell {
2108 children,
2109 width: None,
2110 })
2111 .collect(),
2112 })
2113 .collect(),
2114 column_aligns,
2115 ..Default::default()
2116 }
2117 }
2118
2119 pub fn rows(&self) -> impl Iterator<Item = impl Iterator<Item = &Paragraph>> {
2121 self.children
2122 .iter()
2123 .map(|row| row.children.iter().map(|cell| &cell.children))
2124 }
2125
2126 pub fn column_aligns(&self) -> &[ColumnumnAlign] {
2128 &self.column_aligns
2129 }
2130
2131 pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
2132 self.column_aligns.get(index).copied().unwrap_or_default()
2133 }
2134
2135 pub(crate) fn to_markdown(&self) -> String {
2142 let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
2143
2144 for (row_ix, row) in self.children.iter().enumerate() {
2145 let cells: Vec<String> = row
2146 .children
2147 .iter()
2148 .map(|cell| {
2149 cell.children
2150 .to_markdown()
2151 .trim()
2152 .replace('\n', " ")
2153 .replace('|', "\\|")
2154 })
2155 .collect();
2156 lines.push(format!("| {} |", cells.join(" | ")));
2157
2158 if row_ix == 0 {
2161 let aligns: Vec<String> = (0..row.children.len())
2162 .map(|ix| {
2163 match self.column_align(ix) {
2164 ColumnumnAlign::None | ColumnumnAlign::Left => ":--",
2165 ColumnumnAlign::Center => ":-:",
2166 ColumnumnAlign::Right => "--:",
2167 }
2168 .to_string()
2169 })
2170 .collect();
2171 lines.push(format!("| {} |", aligns.join(" | ")));
2172 }
2173 }
2174
2175 lines.join("\n")
2176 }
2177
2178 pub(crate) fn table_data(&self) -> TableData {
2181 let row_text = |row: &TableRow| {
2182 row.children
2183 .iter()
2184 .map(|cell| cell.children.text().trim().to_string())
2185 .collect::<Vec<_>>()
2186 };
2187
2188 TableData {
2189 headers: self.children.first().map(row_text).unwrap_or_default(),
2190 rows: self.children.iter().skip(1).map(row_text).collect(),
2191 markdown: self.to_markdown(),
2192 span: self.span.map(|span| span.start..span.end),
2193 }
2194 }
2195
2196 fn cached_table_data(&self) -> Arc<TableData> {
2201 if let Ok(cache) = self.table_data_cache.0.lock()
2202 && let Some(cached) = cache.as_ref()
2203 {
2204 return cached.clone();
2205 }
2206
2207 let data = Arc::new(self.table_data());
2208 if let Ok(mut cache) = self.table_data_cache.0.lock() {
2209 *cache = Some(data.clone());
2210 }
2211 data
2212 }
2213}
2214
2215#[derive(Debug, Default, Copy, Clone, PartialEq)]
2217pub enum ColumnumnAlign {
2218 #[default]
2220 None,
2221 Left,
2222 Center,
2223 Right,
2224}
2225
2226impl From<mdast::AlignKind> for ColumnumnAlign {
2227 fn from(value: mdast::AlignKind) -> Self {
2228 match value {
2229 mdast::AlignKind::None => ColumnumnAlign::None,
2230 mdast::AlignKind::Left => ColumnumnAlign::Left,
2231 mdast::AlignKind::Center => ColumnumnAlign::Center,
2232 mdast::AlignKind::Right => ColumnumnAlign::Right,
2233 }
2234 }
2235}
2236
2237#[derive(Debug, Clone, Default, PartialEq)]
2238pub(crate) struct TableRow {
2239 pub children: Vec<TableCell>,
2240}
2241
2242#[derive(Debug, Clone, Default, PartialEq)]
2243pub(crate) struct TableCell {
2244 pub children: Paragraph,
2245 pub width: Option<DefiniteLength>,
2246}
2247
2248impl Paragraph {
2249 pub(crate) fn take(&mut self) -> Paragraph {
2250 std::mem::replace(
2251 self,
2252 Paragraph {
2253 span: None,
2254 children: vec![],
2255 link_refs: Default::default(),
2256 state: Arc::new(Mutex::new(InlineState::default())),
2257 render_cache: ParagraphRenderCache::default(),
2258 },
2259 )
2260 }
2261
2262 pub(crate) fn is_image(&self) -> bool {
2263 false
2264 }
2265
2266 pub(crate) fn set_span(&mut self, span: Span) {
2267 self.span = Some(span);
2268 }
2269
2270 pub(crate) fn push_str(&mut self, text: &str) {
2271 self.children.push(
2272 InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
2273 );
2274 self.invalidate_render_cache();
2275 }
2276
2277 pub fn push(&mut self, text: InlineNode) {
2279 self.children.push(text);
2280 self.invalidate_render_cache();
2281 }
2282
2283 pub fn children(&self) -> &[InlineNode] {
2285 &self.children
2286 }
2287
2288 pub fn with_leaf_key(mut self, key: u64) -> Self {
2293 set_leaf_key(&mut self.state, key);
2294 self
2295 }
2296
2297 pub(crate) fn push_image(&mut self, image: ImageNode) {
2298 self.children.push(InlineNode::image(image));
2299 self.invalidate_render_cache();
2300 }
2301
2302 fn invalidate_render_cache(&mut self) {
2304 self.render_cache = ParagraphRenderCache::default();
2305 }
2306
2307 pub(crate) fn is_empty(&self) -> bool {
2308 self.children.is_empty()
2309 || self
2310 .children
2311 .iter()
2312 .all(|node| node.text.is_empty() && node.image.is_none())
2313 }
2314
2315 pub(crate) fn text_len(&self) -> usize {
2317 self.children
2318 .iter()
2319 .map(|node| node.text.len())
2320 .sum::<usize>()
2321 }
2322
2323 pub(crate) fn merge(&mut self, other: Self) {
2324 self.children.extend(other.children);
2325 self.invalidate_render_cache();
2326 }
2327}
2328
2329#[derive(Debug, Clone)]
2330pub struct CodeBlock {
2331 lang: Option<SharedString>,
2332 meta: Option<SharedString>,
2333 state: Arc<Mutex<InlineState>>,
2334 highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
2335 pub(crate) source_segments: Vec<SourceSegment>,
2338 pub span: Option<Span>,
2339}
2340
2341struct CachedCodeBlockHighlights {
2342 highlighter: Arc<CodeBlockHighlighterFn>,
2343 styles: Vec<(Range<usize>, HighlightStyle)>,
2344}
2345
2346impl std::fmt::Debug for CachedCodeBlockHighlights {
2347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2348 f.debug_struct("CachedCodeBlockHighlights")
2349 .field("styles", &self.styles)
2350 .finish_non_exhaustive()
2351 }
2352}
2353
2354impl PartialEq for CodeBlock {
2355 fn eq(&self, other: &Self) -> bool {
2356 self.lang == other.lang
2357 && self.meta == other.meta
2358 && self.code() == other.code()
2359 && self.span == other.span
2360 }
2361}
2362
2363impl CodeBlock {
2364 pub fn lang(&self) -> Option<SharedString> {
2366 self.lang.clone()
2367 }
2368
2369 pub fn meta(&self) -> Option<SharedString> {
2371 self.meta.clone()
2372 }
2373
2374 pub fn with_meta(mut self, meta: Option<SharedString>) -> Self {
2377 self.meta = meta;
2378 self
2379 }
2380
2381 pub fn with_leaf_key(mut self, key: u64) -> Self {
2384 set_leaf_key(&mut self.state, key);
2385 self
2386 }
2387
2388 pub fn code(&self) -> SharedString {
2390 self.state
2391 .lock()
2392 .map(|state| state.text.clone())
2393 .unwrap_or_default()
2394 }
2395
2396 pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
2402 Self::new(code.into(), lang.map(Into::into), None::<Span>)
2403 }
2404
2405 pub fn new(
2407 code: SharedString,
2408 lang: Option<SharedString>,
2409 span: Option<impl Into<Span>>,
2410 ) -> Self {
2411 let state = Arc::new(Mutex::new(InlineState::default()));
2412 if let Ok(mut state) = state.lock() {
2413 state.set_text(code);
2414 }
2415
2416 Self {
2417 lang,
2418 meta: None,
2419 state,
2420 highlight_cache: Arc::new(Mutex::new(None)),
2421 source_segments: vec![],
2422 span: span.map(|s| s.into()),
2423 }
2424 }
2425
2426 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
2427 self.source_segments = source_segments;
2428 self
2429 }
2430
2431 #[cfg(test)]
2432 pub(crate) fn set_selection(&self, selection: Range<usize>) {
2433 if let Ok(mut state) = self.state.lock() {
2434 state.selection = Some(selection.into());
2435 }
2436 }
2437
2438 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
2439 let Ok(state) = self.state.lock() else {
2440 return SourceRangeSelection::Unmapped;
2441 };
2442 let Some(selection) = state.selection else {
2443 return SourceRangeSelection::Unselected;
2444 };
2445 if selection.start >= selection.end {
2446 return SourceRangeSelection::Unselected;
2447 }
2448 source_range_for_segments(&self.source_segments, selection.start..selection.end)
2449 .map(SourceRangeSelection::Mapped)
2450 .unwrap_or(SourceRangeSelection::Unmapped)
2451 }
2452
2453 fn highlighted_styles(
2454 &self,
2455 highlighter: &Arc<CodeBlockHighlighterFn>,
2456 ) -> Vec<(Range<usize>, HighlightStyle)> {
2457 if let Ok(cache) = self.highlight_cache.lock()
2458 && let Some(cache) = cache.as_ref()
2459 && Arc::ptr_eq(&cache.highlighter, highlighter)
2460 {
2461 return cache.styles.clone();
2462 }
2463
2464 let code_len = self.code().len();
2465 let styles = highlighter(self)
2466 .into_iter()
2467 .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
2468 .collect::<Vec<_>>();
2469 if let Ok(mut cache) = self.highlight_cache.lock() {
2470 *cache = Some(CachedCodeBlockHighlights {
2471 highlighter: highlighter.clone(),
2472 styles: styles.clone(),
2473 });
2474 }
2475 styles
2476 }
2477
2478 pub(super) fn selected_text(&self) -> String {
2479 let mut text = String::new();
2480 if let Ok(state) = self.state.lock()
2481 && let Some(selection) = &state.selection
2482 {
2483 text.push_str(&state.text[selection.start..selection.end]);
2484 }
2485 text
2486 }
2487
2488 pub(super) fn selected_source(&self) -> String {
2495 let code = self.selected_text();
2496 if code.is_empty() {
2497 return String::new();
2498 }
2499 let lang = self.lang.clone().unwrap_or_default();
2500 let code = code.trim_end_matches('\n');
2503 format!("```{}\n{}\n```", lang, code)
2504 }
2505
2506 pub(super) fn text(&self) -> String {
2507 self.state
2508 .lock()
2509 .map(|state| state.text.to_string())
2510 .unwrap_or_default()
2511 }
2512
2513 pub(super) fn has_selection(&self) -> bool {
2517 self.state
2518 .lock()
2519 .is_ok_and(|state| state.selection.is_some())
2520 }
2521
2522 pub(super) fn clear_selection(&self) {
2523 if let Ok(mut state) = self.state.lock() {
2524 state.selection = None;
2525 }
2526 }
2527
2528 fn render(
2529 &self,
2530 options: &NodeRenderOptions,
2531 node_cx: &NodeContext,
2532 window: &mut Window,
2533 cx: &mut App,
2534 ) -> AnyElement {
2535 let style = &node_cx.style;
2536 let leaf_key = self.span.map(|span| TextLeafKey::block(span.start));
2537 let lang_label = self
2542 .lang()
2543 .filter(|lang| !lang.is_empty() && node_cx.code_block_actions.is_none())
2544 .map(|lang| {
2545 div()
2546 .absolute()
2547 .top(px(7.))
2548 .right_3()
2549 .font_family(cx.theme().tokens.typography.sans.clone())
2550 .text_size(cx.theme().tokens.typography.xs.size)
2551 .line_height(px(16.))
2552 .text_color(style.muted_foreground())
2553 .child(lang)
2554 });
2555
2556 let block = div()
2557 .w_full()
2558 .min_w_0()
2559 .px_4()
2560 .pt(if lang_label.is_some() {
2561 px(28.)
2562 } else {
2563 px(14.)
2564 })
2565 .pb(px(14.))
2566 .bg(style.code_background())
2567 .border_1()
2568 .border_color(style.border())
2569 .font_family(cx.theme().tokens.typography.mono.clone())
2570 .text_size(cx.theme().tokens.typography.mono_md.size)
2571 .line_height(relative(1.6))
2572 .relative()
2573 .refine_style(&style.code_block())
2574 .child(
2575 Inline::new(
2576 self.state.clone(),
2577 vec![],
2578 fade_highlights(
2579 node_cx
2580 .code_block_highlighter
2581 .as_ref()
2582 .map(|highlighter| self.highlighted_styles(highlighter))
2583 .unwrap_or_default()
2584 .into_iter()
2585 .map(|(range, style)| (range, InlineHighlight::from(style)))
2586 .collect(),
2587 node_cx.stream_fades(leaf_key),
2588 ),
2589 node_cx.link_click_handler.clone(),
2590 )
2591 .range_backgrounds(node_cx.range_backgrounds(leaf_key).to_vec())
2592 .reveal(node_cx.reveal_at(leaf_key, 0, self.code().len())),
2593 );
2594 let actions = node_cx.code_block_actions.clone().map(|actions| {
2595 div()
2596 .id("actions")
2597 .absolute()
2598 .top_2()
2599 .right_2()
2600 .bg(style.code_background())
2601 .rounded(cx.theme().tokens.radius.md)
2602 .child(actions(&self, window, cx))
2603 });
2604 let id = block_element_id("codeblock", self.span, options.ix);
2607 let block = if matches!(style.code_block().overflow.y, Some(Overflow::Scroll)) {
2608 let scroll_handle = window
2609 .use_keyed_state(
2610 block_element_id("codeblock-scroll", self.span, options.ix),
2611 cx,
2612 |_, _| ScrollHandle::default(),
2613 )
2614 .read(cx)
2615 .clone();
2616 div()
2623 .id(id.clone())
2624 .w_full()
2625 .min_w_0()
2626 .relative()
2627 .child(block.id("scroll").track_scroll(&scroll_handle))
2628 .child(ScrollableMask::new(Axis::Vertical, &scroll_handle).id(id))
2629 .child(
2630 div()
2631 .absolute()
2632 .inset_0()
2633 .child(Scrollbar::vertical(&scroll_handle)),
2634 )
2635 .children(actions)
2636 .children(lang_label)
2637 .into_any_element()
2638 } else {
2639 match actions {
2641 Some(actions) => block.id(id).child(actions).into_any_element(),
2642 None => block.children(lang_label).into_any_element(),
2643 }
2644 };
2645
2646 gapped(
2647 block,
2648 if options.is_last {
2649 rems(0.)
2650 } else {
2651 style.paragraph_gap()
2652 },
2653 )
2654 }
2655}
2656
2657#[derive(Default, Clone)]
2659pub(crate) struct NodeContext {
2660 pub(crate) offset: usize,
2663 pub(crate) link_refs: HashMap<SharedString, LinkMark>,
2664 pub(crate) style: Arc<TextViewStyle>,
2665 pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
2666 pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
2667 pub(crate) table_actions: Option<Arc<TableActionsFn>>,
2668 pub(crate) image_source: Option<Arc<super::text_view::ImageSourceFn>>,
2669 pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
2670 pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
2671 pub(crate) stream_fade: Option<Arc<StreamFadeFrame>>,
2673 pub(crate) range_highlights: Option<Arc<RangeHighlightFrame>>,
2675 pub(crate) reveal: Option<RevealRequest>,
2677 pub(crate) body_font_size: Pixels,
2680}
2681
2682impl NodeContext {
2683 fn image_source(&self, image: &ImageNode) -> ImageSource {
2684 match &self.image_source {
2685 Some(resolve) => resolve(&image.url),
2686 None => image.source(),
2687 }
2688 }
2689
2690 pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
2691 self.link_refs.insert(identifier, link);
2692 }
2693
2694 fn stream_fades(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, f32)] {
2696 match (&self.stream_fade, key) {
2697 (Some(frame), Some(key)) => frame.fades(key).unwrap_or_default(),
2698 _ => &[],
2699 }
2700 }
2701
2702 fn range_backgrounds(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, Hsla)] {
2705 match (&self.range_highlights, key) {
2706 (Some(frame), Some(key)) => frame.backgrounds(key),
2707 _ => &[],
2708 }
2709 }
2710
2711 fn reveal_at(&self, key: Option<TextLeafKey>, start: usize, end: usize) -> Option<RevealAt> {
2714 self.reveal.as_ref()?.at(key, start, end)
2715 }
2716}
2717
2718impl PartialEq for NodeContext {
2719 fn eq(&self, other: &Self) -> bool {
2720 self.link_refs == other.link_refs && self.style == other.style
2721 }
2724}
2725
2726fn mark_highlight(mark: &TextMark, node_cx: &NodeContext, cx: &App) -> InlineHighlight {
2729 let mut highlight = HighlightStyle::default();
2730 if mark.bold {
2731 highlight.font_weight = Some(FontWeight::BOLD);
2732 }
2733 if mark.italic {
2734 highlight.font_style = Some(FontStyle::Italic);
2735 }
2736 if mark.strikethrough {
2737 highlight.strikethrough = Some(gpui::StrikethroughStyle {
2738 thickness: gpui::px(1.),
2739 ..Default::default()
2740 });
2741 }
2742 if mark.underline {
2743 highlight.underline = Some(gpui::UnderlineStyle {
2744 thickness: gpui::px(1.),
2745 ..Default::default()
2746 });
2747 }
2748 let mut font_family = None;
2749 if mark.code {
2750 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
2751 let style = node_cx.style.inline_code_style();
2752 highlight = highlight.highlight(HighlightStyle {
2753 color: style.text.color,
2754 font_weight: style.text.font_weight,
2755 font_style: style.text.font_style,
2756 underline: style.text.underline,
2757 strikethrough: style.text.strikethrough,
2758 ..Default::default()
2759 });
2760 font_family = Some(
2761 style
2762 .text
2763 .font_family
2764 .clone()
2765 .unwrap_or_else(|| cx.theme().tokens.typography.mono.clone()),
2766 );
2767 }
2768 if let Some(color) = mark.highlight {
2769 highlight.background_color = Some(color);
2770 }
2771 InlineHighlight {
2772 style: highlight,
2773 font_family,
2774 font_size_scale: mark.code.then_some(0.875),
2775 code_style: mark.code.then(|| node_cx.style.inline_code_style().clone()),
2776 }
2777}
2778
2779impl Paragraph {
2780 fn inline_highlights(
2784 &self,
2785 node_cx: &NodeContext,
2786 cx: &App,
2787 ) -> Vec<(Range<usize>, InlineHighlight)> {
2788 let mut highlights = vec![];
2789 let mut offset = 0;
2790 for inline_node in &self.children {
2791 let node_highlights = inline_node
2792 .marks
2793 .iter()
2794 .map(|(range, mark)| {
2795 (
2796 (offset + range.start)..(offset + range.end),
2797 mark_highlight(mark, node_cx, cx),
2798 )
2799 })
2800 .collect::<Vec<_>>();
2801 highlights = combine_highlights(highlights, node_highlights);
2802 offset += inline_node.text.len();
2803 }
2804 highlights
2805 }
2806
2807 fn render(
2811 &self,
2812 fade_key: Option<TextLeafKey>,
2813 node_cx: &NodeContext,
2814 _window: &mut Window,
2815 cx: &mut App,
2816 ) -> AnyElement {
2817 let children = &self.children;
2818 let fades = node_cx.stream_fades(fade_key);
2819 let backgrounds = node_cx.range_backgrounds(fade_key);
2820
2821 if self.should_render_inline_flow() {
2822 return InlineFlow::new(
2823 leaf_element_id(fade_key),
2824 self.inline_flow_items(fade_key, fades, backgrounds, node_cx, cx),
2825 node_cx.link_click_handler.clone(),
2826 )
2827 .editor_key(self.leaf_key())
2828 .into_any_element();
2829 }
2830
2831 let has_image = children.iter().any(|child| child.image.is_some());
2832 if !has_image {
2835 let (text, highlights, mut links) = self.plain_render(node_cx, cx);
2836 if text.is_empty() && self.leaf_key().is_none() {
2839 return div().into_any_element();
2840 }
2841 for (_, link_mark) in &mut links {
2842 if let Some(identifier) = link_mark.identifier.as_ref()
2843 && let Some(mark) = node_cx.link_refs.get(identifier)
2844 {
2845 *link_mark = mark.clone();
2846 }
2847 }
2848 let highlights = fade_highlights(highlights, &slice_fades(fades, 0, text.len()));
2849 let backgrounds = slice_backgrounds(backgrounds, 0, text.len());
2850 let reveal = node_cx.reveal_at(fade_key, 0, text.len());
2851 if let Ok(mut state) = self.state.lock() {
2852 state.set_text(text);
2853 }
2854 return Inline::new(
2855 self.state.clone(),
2856 links,
2857 highlights,
2858 node_cx.link_click_handler.clone(),
2859 )
2860 .range_backgrounds(backgrounds)
2861 .reveal(reveal)
2862 .into_any_element();
2863 }
2864
2865 let mut child_nodes: Vec<AnyElement> = vec![];
2866
2867 let mut text = String::new();
2868 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2869 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2870 let mut offset = 0;
2871 let mut consumed = 0;
2874
2875 for (ix, inline_node) in children.iter().enumerate() {
2876 let text_len = inline_node.text.len();
2877 text.push_str(&inline_node.text);
2878
2879 if let Some(image) = &inline_node.image {
2880 if text.len() > 0 {
2881 if let Ok(mut state) = inline_node.state.lock() {
2882 state.set_text(text.clone().into());
2883 }
2884 child_nodes.push(
2885 Inline::new(
2886 inline_node.state.clone(),
2887 links.clone(),
2888 fade_highlights(
2889 highlights.clone(),
2890 &slice_fades(fades, consumed, consumed + text.len()),
2891 ),
2892 node_cx.link_click_handler.clone(),
2893 )
2894 .range_backgrounds(slice_backgrounds(
2895 backgrounds,
2896 consumed,
2897 consumed + text.len(),
2898 ))
2899 .reveal(node_cx.reveal_at(fade_key, consumed, consumed + text.len()))
2900 .into_any_element(),
2901 );
2902 }
2903 let link_click_handler = node_cx.link_click_handler.clone();
2904 child_nodes.push(
2905 img(node_cx.image_source(image))
2906 .id(ix)
2907 .object_fit(ObjectFit::Contain)
2908 .max_w(relative(1.))
2909 .when_some(image.width, |this, width| this.w(width))
2910 .when_some(image.link.clone(), |this, link| {
2911 let link_click_handler = link_click_handler.clone();
2912 let aux_link = link.clone();
2913 let aux_link_click_handler = link_click_handler.clone();
2914 this.cursor_pointer()
2915 .on_click(move |event, window, cx| {
2916 crate::TextSelection::end(window, cx);
2917 cx.stop_propagation();
2918 handle_link_click(
2919 &link_click_handler,
2920 link.url.clone(),
2921 event.clone(),
2922 window,
2923 cx,
2924 );
2925 })
2926 .on_aux_click(move |event, window, cx| {
2927 crate::TextSelection::end(window, cx);
2928 cx.stop_propagation();
2929 handle_link_click(
2930 &aux_link_click_handler,
2931 aux_link.url.clone(),
2932 event.clone(),
2933 window,
2934 cx,
2935 );
2936 })
2937 })
2938 .into_any_element(),
2939 );
2940
2941 consumed += text.len();
2942 text.clear();
2943 links.clear();
2944 highlights.clear();
2945 offset = 0;
2946 } else {
2947 let mut node_highlights = vec![];
2948 for (range, style) in &inline_node.marks {
2949 let inner_range = (offset + range.start)..(offset + range.end);
2950 let mut highlight = mark_highlight(style, node_cx, cx);
2951
2952 if let Some(mut link_mark) = style.link.clone() {
2953 highlight.style.color = Some(node_cx.style.link());
2954 highlight.style.underline = Some(link_underline(&node_cx.style));
2955
2956 if let Some(identifier) = link_mark.identifier.as_ref() {
2958 if let Some(mark) = node_cx.link_refs.get(identifier) {
2959 link_mark = mark.clone();
2960 }
2961 }
2962
2963 links.push((inner_range.clone(), link_mark));
2964 }
2965
2966 node_highlights.push((inner_range, highlight));
2967 }
2968
2969 highlights = combine_highlights(highlights, node_highlights);
2970 offset += text_len;
2971 }
2972 }
2973
2974 if text.len() > 0 {
2976 let text_end = consumed + text.len();
2977 let highlights = fade_highlights(highlights, &slice_fades(fades, consumed, text_end));
2978 if let Ok(mut state) = self.state.lock() {
2979 state.set_text(text.into());
2980 }
2981 child_nodes.push(
2982 Inline::new(
2983 self.state.clone(),
2984 links,
2985 highlights,
2986 node_cx.link_click_handler.clone(),
2987 )
2988 .range_backgrounds(slice_backgrounds(backgrounds, consumed, text_end))
2989 .reveal(node_cx.reveal_at(fade_key, consumed, text_end))
2990 .into_any_element(),
2991 );
2992 }
2993
2994 if !has_image {
2998 return child_nodes
2999 .pop()
3000 .unwrap_or_else(|| div().into_any_element());
3001 }
3002
3003 div()
3004 .id(leaf_element_id(fade_key))
3005 .children(child_nodes)
3006 .into_any_element()
3007 }
3008
3009 fn leaf_key(&self) -> Option<u64> {
3011 self.state.lock().ok().and_then(|state| state.editor_key)
3012 }
3013
3014 fn should_render_inline_flow(&self) -> bool {
3015 let has_image = self.children.iter().any(|child| child.image.is_some());
3016 let has_text = self.children.iter().any(|child| !child.text.is_empty());
3017 if has_image && self.leaf_key().is_some() {
3021 return true;
3022 }
3023 self.children.iter().any(|child| child.custom.is_some())
3024 || (has_image && has_text)
3025 || self
3026 .children
3027 .iter()
3028 .any(|child| child.marks.iter().any(|(_, mark)| mark.code))
3029 }
3030
3031 fn inline_flow_items(
3032 &self,
3033 leaf_key: Option<TextLeafKey>,
3034 fades: &[(Range<usize>, f32)],
3035 backgrounds: &[(Range<usize>, Hsla)],
3036 node_cx: &NodeContext,
3037 cx: &mut App,
3038 ) -> Vec<InlineFlowItem> {
3039 let mut items = Vec::new();
3040 let mut text = String::new();
3041 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
3042 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
3043 let mut offset = 0;
3044 let mut consumed = 0;
3047
3048 for inline_node in &self.children {
3049 if let Some(node) = &inline_node.custom {
3050 if let Ok(mut state) = inline_node.state.lock() {
3051 state.set_text(text.clone().into());
3052 }
3053 if !text.is_empty() {
3054 let item_fades = slice_fades(fades, consumed, consumed + text.len());
3055 let item_backgrounds =
3056 slice_backgrounds(backgrounds, consumed, consumed + text.len());
3057 let item_reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
3058 consumed += text.len();
3059 items.push(InlineFlowItem::Text {
3060 state: inline_node.state.clone(),
3061 text: std::mem::take(&mut text).into(),
3062 links: std::mem::take(&mut links),
3063 highlights: fade_highlights(std::mem::take(&mut highlights), &item_fades),
3064 backgrounds: item_backgrounds,
3065 reveal: item_reveal,
3066 });
3067 }
3068 let mut object_style = HighlightStyle::default();
3069 let mut object_link = None;
3070 for (_, mark) in &inline_node.marks {
3071 object_style = object_style.highlight(mark_highlight(mark, node_cx, cx).style);
3072 if let Some(link) = &mark.link {
3073 object_link = Some(
3074 link.identifier
3075 .as_ref()
3076 .and_then(|id| node_cx.link_refs.get(id))
3077 .unwrap_or(link)
3078 .clone(),
3079 );
3080 object_style.color = Some(node_cx.style.link());
3081 object_style.underline = Some(link_underline(&node_cx.style));
3082 }
3083 }
3084 let rendered_node = node.clone();
3085 let extensions = node_cx.markdown_extensions.clone();
3086 items.push(InlineFlowItem::Object {
3087 text: node.shared_text(),
3088 accessibility_label: node.shared_accessibility_name(),
3089 id: node.source_range().map_or(items.len(), |range| range.start),
3090 renderer: Arc::new(move |context, window, cx| {
3091 extensions.render_inline(&rendered_node, context, window, cx)
3092 }),
3093 selected: inline_node.custom_selection.clone(),
3094 style: object_style,
3095 link: object_link,
3096 });
3097 consumed += inline_node.text.len();
3098 offset = 0;
3099 continue;
3100 }
3101 let text_len = inline_node.text.len();
3102 text.push_str(&inline_node.text);
3103
3104 if let Some(image) = &inline_node.image {
3105 if !text.is_empty() {
3106 if let Ok(mut state) = inline_node.state.lock() {
3107 state.set_text(text.clone().into());
3108 }
3109 items.push(InlineFlowItem::Text {
3110 state: inline_node.state.clone(),
3111 text: text.clone().into(),
3112 links: links.clone(),
3113 highlights: fade_highlights(
3114 highlights.clone(),
3115 &slice_fades(fades, consumed, consumed + text.len()),
3116 ),
3117 backgrounds: slice_backgrounds(
3118 backgrounds,
3119 consumed,
3120 consumed + text.len(),
3121 ),
3122 reveal: node_cx.reveal_at(leaf_key, consumed, consumed + text.len()),
3123 });
3124 }
3125
3126 items.push(InlineFlowItem::Image {
3127 source: node_cx.image_source(image),
3128 link: image.link.clone(),
3129 title: image.title(),
3130 width: image.width,
3131 height: image.height,
3132 });
3133
3134 consumed += text.len();
3135 text.clear();
3136 links.clear();
3137 highlights.clear();
3138 offset = 0;
3139 } else {
3140 let mut node_highlights = vec![];
3141 for (range, style) in &inline_node.marks {
3142 let inner_range = (offset + range.start)..(offset + range.end);
3143 let mut highlight = mark_highlight(style, node_cx, cx);
3144
3145 if let Some(mut link_mark) = style.link.clone() {
3146 highlight.style.color = Some(node_cx.style.link());
3147 highlight.style.underline = Some(link_underline(&node_cx.style));
3148
3149 if let Some(identifier) = link_mark.identifier.as_ref()
3150 && let Some(mark) = node_cx.link_refs.get(identifier)
3151 {
3152 link_mark = mark.clone();
3153 }
3154
3155 links.push((inner_range.clone(), link_mark));
3156 }
3157
3158 node_highlights.push((inner_range, highlight));
3159 }
3160
3161 highlights = combine_highlights(highlights, node_highlights);
3162 offset += text_len;
3163 }
3164 }
3165
3166 if !text.is_empty() {
3167 if let Ok(mut state) = self.state.lock() {
3168 state.set_text(text.clone().into());
3169 }
3170 let highlights = fade_highlights(
3171 highlights,
3172 &slice_fades(fades, consumed, consumed + text.len()),
3173 );
3174 let backgrounds = slice_backgrounds(backgrounds, consumed, consumed + text.len());
3175 let reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
3176 items.push(InlineFlowItem::Text {
3177 state: self.state.clone(),
3178 text: text.into(),
3179 links,
3180 highlights,
3181 backgrounds,
3182 reveal,
3183 });
3184 }
3185
3186 items
3187 }
3188}
3189
3190fn block_element_id(kind: &'static str, span: Option<Span>, ix: usize) -> ElementId {
3195 (kind, span.map_or(ix, |span| span.start)).into()
3196}
3197
3198fn leaf_element_id(fade_key: Option<TextLeafKey>) -> ElementId {
3203 fade_key.map_or_else(|| ElementId::from("p"), ElementId::from)
3204}
3205
3206fn link_underline(style: &TextViewStyle) -> gpui::UnderlineStyle {
3209 gpui::UnderlineStyle {
3210 thickness: px(1.),
3211 color: Some(style.link().opacity(0.4)),
3212 ..Default::default()
3213 }
3214}
3215
3216fn body_font_size(node_cx: &NodeContext, window: &Window) -> Pixels {
3223 if node_cx.body_font_size > Pixels::ZERO {
3224 node_cx.body_font_size
3225 } else {
3226 window.text_style().font_size.to_pixels(window.rem_size())
3227 }
3228}
3229
3230fn heading_metrics(level: u8) -> (f32, f32, f32) {
3235 match level {
3236 1 => (1.8, 1.25, 1.1),
3237 2 => (4. / 3., 1.35, 1.6),
3238 3 => (17. / 15., 1.35, 1.4),
3239 4 => (1., 1.35, 1.4),
3240 5 => (14. / 15., 1.35, 1.4),
3241 _ => (13. / 15., 1.35, 1.4),
3242 }
3243}
3244
3245const HEADING_BOTTOM_GAP: f32 = 0.35;
3247
3248fn heading_size(level: u8, node_cx: &NodeContext, window: &Window) -> Pixels {
3251 node_cx.style.heading(level).text.font_size.map_or_else(
3252 || body_font_size(node_cx, window) * heading_metrics(level).0,
3253 |size| size.to_pixels(window.rem_size()),
3254 )
3255}
3256
3257fn rule_space(body_font_size: Pixels) -> Pixels {
3259 body_font_size * 1.6
3260}
3261
3262fn block_gap(options: &NodeRenderOptions, style: &TextViewStyle) -> Rems {
3264 if options.in_list || options.is_last {
3265 rems(0.)
3266 } else {
3267 style.paragraph_gap()
3268 }
3269}
3270
3271fn gapped(block: AnyElement, gap: Rems) -> AnyElement {
3274 if gap.is_zero() {
3275 block
3276 } else {
3277 div().pb(gap).child(block).into_any_element()
3278 }
3279}
3280
3281fn slice_fades(
3283 fades: &[(Range<usize>, f32)],
3284 start: usize,
3285 end: usize,
3286) -> Vec<(Range<usize>, f32)> {
3287 slice_ranges(fades, start, end, |range, fade_out| (range, *fade_out))
3288}
3289
3290fn slice_backgrounds(
3293 backgrounds: &[(Range<usize>, Hsla)],
3294 start: usize,
3295 end: usize,
3296) -> Vec<(Range<usize>, Hsla)> {
3297 slice_ranges(backgrounds, start, end, |range, color| (range, *color))
3298}
3299
3300struct TableColumn {
3302 max: f32,
3304 min: f32,
3306 divider: f32,
3307}
3308
3309fn table_cell_padding_x(style: &TextViewStyle, rem_size: Pixels) -> f32 {
3312 let padding = &style.table_cell().padding;
3313 let side = |length: Option<DefiniteLength>| match length {
3314 Some(DefiniteLength::Absolute(length)) => f32::from(length.to_pixels(rem_size)),
3315 _ => CELL_PAD_PX / 2.,
3316 };
3317 side(padding.left) + side(padding.right)
3318}
3319
3320const CELL_PAD_PX: f32 = 24.0; const CELL_MIN_PX: f32 = 48.0;
3322const CELL_BORDER_PX: f32 = 1.0; fn measure_table_columns(
3329 table: &Table,
3330 col_count: usize,
3331 node_cx: &NodeContext,
3332 window: &mut Window,
3333 cx: &mut App,
3334) -> Vec<f32> {
3335 let text_styles = TableTextStyles::new(&node_cx.style, window);
3336 let rem_size = window.rem_size();
3337 let mono_family = cx.theme().tokens.typography.mono.clone();
3338 let inline_code = node_cx.style.inline_code_highlight();
3339 let cached = table.column_widths_cache.0.lock().ok().and_then(|cache| {
3340 cache
3341 .as_ref()
3342 .filter(|cached| {
3343 cached.widths.len() == col_count
3344 && cached.text_styles == text_styles
3345 && cached.rem_size == rem_size
3346 && cached.mono_family == mono_family
3347 && cached.inline_code == inline_code
3348 && cached
3349 .text_system
3350 .upgrade()
3351 .is_some_and(|system| Arc::ptr_eq(&system, window.text_system()))
3352 })
3353 .cloned()
3354 });
3355 let measured = cached.unwrap_or_else(|| {
3356 let (widths, custom_cells) =
3357 measure_table_text_columns(table, col_count, &text_styles, node_cx, window, cx);
3358 let measured = Arc::new(TableColumnWidths {
3359 text_system: Arc::downgrade(window.text_system()),
3360 text_styles: text_styles.clone(),
3361 rem_size,
3362 mono_family,
3363 inline_code,
3364 widths,
3365 custom_cells,
3366 });
3367 if let Ok(mut cache) = table.column_widths_cache.0.lock() {
3368 *cache = Some(measured.clone());
3369 }
3370 measured
3371 });
3372
3373 let mut col_w = measured.widths.clone();
3377 for &(row_ix, ix) in &measured.custom_cells {
3378 let cell = &table.children[row_ix].children[ix];
3379 let items = cell.children.inline_flow_items(None, &[], &[], node_cx, cx);
3380 let style = text_styles.row_refinement(row_ix).clone();
3381 let width = window.with_text_style(Some(style), |window| {
3382 super::inline_flow::intrinsic_width(&items, window, cx)
3383 });
3384 let border = if ix + 1 < col_count {
3385 CELL_BORDER_PX
3386 } else {
3387 0.
3388 };
3389 col_w[ix] = col_w[ix].max(f32::from(width) + CELL_PAD_PX + border);
3390 }
3391 col_w
3392}
3393
3394fn measure_table_text_columns(
3397 table: &Table,
3398 col_count: usize,
3399 text_styles: &TableTextStyles,
3400 node_cx: &NodeContext,
3401 window: &mut Window,
3402 cx: &mut App,
3403) -> (Vec<f32>, Vec<(usize, usize)>) {
3404 let mut col_w = vec![CELL_MIN_PX; col_count];
3405 let mut custom_cells = Vec::new();
3406 for (row_ix, row) in table.children.iter().enumerate() {
3407 let text_style = text_styles.row(row_ix);
3408 let font_size = text_style.font_size.to_pixels(window.rem_size());
3409 for (ix, cell) in row.children.iter().enumerate() {
3410 let Some(slot) = col_w.get_mut(ix) else {
3411 continue;
3412 };
3413 if cell
3414 .children
3415 .children
3416 .iter()
3417 .any(|node| node.custom.is_some())
3418 {
3419 custom_cells.push((row_ix, ix));
3420 continue;
3421 }
3422 let text = cell.children.text();
3423 let highlights = cell.children.inline_highlights(node_cx, cx);
3424 let mut w = 0.0_f32;
3425 let mut line_start = 0;
3426 for line in text.split('\n') {
3427 let start = line_start + (line.len() - line.trim_start().len());
3428 let line_end = line_start + line.len();
3429 line_start = line_end + 1;
3430 let line = line.trim();
3431 if line.is_empty() {
3432 continue;
3433 }
3434 let end = start + line.len();
3435 let line_highlights = highlights
3436 .iter()
3437 .filter_map(|(range, highlight)| {
3438 let clipped = range.start.max(start)..range.end.min(end);
3439 (clipped.start < clipped.end).then(|| {
3440 (
3441 clipped.start - start..clipped.end - start,
3442 highlight.clone(),
3443 )
3444 })
3445 })
3446 .collect::<Vec<_>>();
3447 let mut line_w = gpui::Pixels::ZERO;
3448 for (range, scale) in
3449 text_size_ranges(line.len(), &line_highlights, font_size, window.rem_size())
3450 {
3451 let highlights = slice_ranges(
3452 &line_highlights,
3453 range.start,
3454 range.end,
3455 |range, highlight| (range, highlight.clone()),
3456 );
3457 let insets = code_insets(&highlights, font_size * scale, window.rem_size());
3458 line_w += insets.left + insets.right;
3459 let runs = text_runs(range.len(), text_style, &highlights);
3460 line_w += window
3461 .text_system()
3462 .layout_line(&line[range], font_size * scale, &runs, None)
3463 .width;
3464 }
3465 w = w.max(f32::from(line_w));
3466 }
3467 let border = if ix + 1 < col_count {
3470 CELL_BORDER_PX
3471 } else {
3472 0.
3473 };
3474 *slot = slot.max(w + CELL_PAD_PX + border);
3475 }
3476 }
3477 (col_w, custom_cells)
3478}
3479
3480impl Paragraph {
3481 fn to_markdown(&self) -> String {
3482 if self.children.iter().any(|node| node.custom.is_some()) {
3483 let mut source = MarkdownSource::default();
3484 for node in &self.children {
3485 if node.custom.is_some() {
3486 source.push_object(node);
3487 } else {
3488 source.push_text(&node.text, &node.marks, 0..node.text.len());
3489 if let Some(image) = &node.image {
3490 source.push_str(&image_markdown(image));
3491 }
3492 }
3493 }
3494 let mut text = source.finish();
3495 text.push_str("\n\n");
3496 return text;
3497 }
3498 let mut text = self
3499 .children
3500 .iter()
3501 .map(|text_node| {
3502 let mut text = text_node.text.to_string();
3503 for (range, style) in &text_node.marks {
3504 if style.bold {
3505 text = format!("**{}**", &text_node.text[range.clone()]);
3506 }
3507 if style.italic {
3508 text = format!("*{}*", &text_node.text[range.clone()]);
3509 }
3510 if style.strikethrough {
3511 text = format!("~~{}~~", &text_node.text[range.clone()]);
3512 }
3513 if style.code {
3514 text = format!("`{}`", &text_node.text[range.clone()]);
3515 }
3516 if style.highlight.is_some() {
3517 text = format!("=={}==", &text_node.text[range.clone()]);
3518 }
3519 if let Some(link) = &style.link {
3520 text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
3521 }
3522 }
3523
3524 if let Some(image) = &text_node.image {
3525 let alt = image.alt.clone().unwrap_or_default();
3526 let title = image
3527 .title
3528 .clone()
3529 .map_or(String::new(), |t| format!(" \"{}\"", t));
3530 text.push_str(&format!("", alt, image.url, title))
3531 }
3532
3533 text
3534 })
3535 .collect::<Vec<_>>()
3536 .join("");
3537
3538 text.push_str("\n\n");
3539 text
3540 }
3541}
3542
3543impl BlockNode {
3544 #[allow(dead_code)]
3548 pub(crate) fn to_markdown(&self) -> String {
3549 match self {
3550 BlockNode::Root { children, .. } => children
3551 .iter()
3552 .map(|child| child.to_markdown())
3553 .collect::<Vec<_>>()
3554 .join("\n\n"),
3555 BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
3556 BlockNode::Heading {
3557 level, children, ..
3558 } => {
3559 let hashes = "#".repeat(*level as usize);
3560 format!("{} {}", hashes, children.to_markdown())
3561 }
3562 BlockNode::Blockquote { children, .. } => {
3563 let content = children
3564 .iter()
3565 .map(|child| child.to_markdown())
3566 .collect::<Vec<_>>()
3567 .join("\n\n");
3568
3569 content
3570 .lines()
3571 .map(|line| format!("> {}", line))
3572 .collect::<Vec<_>>()
3573 .join("\n")
3574 }
3575 BlockNode::List {
3576 children,
3577 ordered,
3578 start,
3579 ..
3580 } => children
3581 .iter()
3582 .enumerate()
3583 .map(|(i, child)| {
3584 let prefix = if *ordered {
3585 format!("{}. ", ordered_list_ordinal(*start, i))
3586 } else {
3587 "- ".to_string()
3588 };
3589 format!("{}{}", prefix, child.to_markdown())
3590 })
3591 .collect::<Vec<_>>()
3592 .join("\n"),
3593 BlockNode::ListItem {
3594 children, checked, ..
3595 } => {
3596 let checkbox = if let Some(checked) = checked {
3597 if *checked { "[x] " } else { "[ ] " }
3598 } else {
3599 ""
3600 };
3601 format!(
3602 "{}{}",
3603 checkbox,
3604 children
3605 .iter()
3606 .map(|child| child.to_markdown())
3607 .collect::<Vec<_>>()
3608 .join("\n")
3609 )
3610 }
3611 BlockNode::CodeBlock(code_block) => {
3612 format!(
3613 "```{}\n{}\n```",
3614 code_block.lang.clone().unwrap_or_default(),
3615 code_block.code()
3616 )
3617 }
3618 BlockNode::Table(table) => table.to_markdown(),
3619 BlockNode::Break { html, .. } => {
3620 if *html {
3621 "<br>".to_string()
3622 } else {
3623 "\n".to_string()
3624 }
3625 }
3626 BlockNode::HorizontalRule { .. } => "---".to_string(),
3627 BlockNode::Custom { node, .. } => node.to_markdown(),
3628 BlockNode::Definition {
3629 identifier,
3630 url,
3631 title,
3632 ..
3633 } => {
3634 if let Some(title) = title {
3635 format!("[{}]: {} \"{}\"", identifier, url, title)
3636 } else {
3637 format!("[{}]: {}", identifier, url)
3638 }
3639 }
3640 BlockNode::Unknown { .. } => "".to_string(),
3641 }
3642 .trim()
3643 .to_string()
3644 }
3645}
3646
3647impl BlockNode {
3648 fn render_list_item_row(
3649 content: AnyElement,
3650 ix: usize,
3651 options: NodeRenderOptions,
3652 checked: Option<bool>,
3653 style: &TextViewStyle,
3654 line_height: Pixels,
3655 indent: Pixels,
3656 ) -> Div {
3657 h_flex()
3658 .w_full()
3659 .min_w_0()
3660 .relative()
3661 .items_start()
3662 .content_start()
3663 .when(!options.todo && checked.is_none(), |this| {
3664 this.child(
3668 div()
3669 .flex_none()
3670 .min_w(indent)
3671 .pr(indent * 0.3)
3672 .text_right()
3673 .text_color(style.muted_foreground())
3674 .refine_style(style.list_style())
3675 .child(
3676 list_item_prefix(
3677 ix,
3678 options.list_start,
3679 options.ordered,
3680 options.depth,
3681 )
3682 .trim_end()
3683 .to_string(),
3684 ),
3685 )
3686 })
3687 .when_some(checked, |this, checked| {
3688 let check_svg = if style.is_dark() {
3690 CHECK_SVG_DARK
3691 } else {
3692 CHECK_SVG_LIGHT
3693 };
3694 this.child(
3695 div()
3696 .flex()
3697 .mr_2()
3698 .h(line_height)
3699 .flex_none()
3700 .items_center()
3701 .justify_center()
3702 .child(
3703 div()
3704 .flex()
3705 .size(rems(0.9375))
3706 .items_center()
3707 .justify_center()
3708 .rounded(px(4.))
3709 .border(px(1.5))
3710 .border_color(
3711 style
3712 .list_style()
3713 .text
3714 .color
3715 .unwrap_or(style.muted_foreground()),
3716 )
3717 .when(checked, |this| {
3718 this.bg(style.link()).border_color(style.link()).child(
3719 img(Arc::new(Image::from_bytes(
3720 ImageFormat::Svg,
3721 check_svg.to_vec(),
3722 )))
3723 .size(rems(0.75)),
3724 )
3725 }),
3726 ),
3727 )
3728 })
3729 .child(
3730 div()
3731 .flex_1()
3732 .min_w_0()
3733 .overflow_hidden()
3734 .when(checked == Some(true), |this| {
3736 this.text_color(style.muted_foreground())
3737 })
3738 .child(content),
3739 )
3740 }
3741
3742 fn render_list_item(
3743 item: &BlockNode,
3744 ix: usize,
3745 options: NodeRenderOptions,
3746 node_cx: &NodeContext,
3747 window: &mut Window,
3748 cx: &mut App,
3749 ) -> AnyElement {
3750 match item {
3751 BlockNode::ListItem {
3752 children,
3753 spread,
3754 checked,
3755 ..
3756 } => {
3757 let body = body_font_size(node_cx, window);
3758 let indent = body * 1.4;
3759 div()
3760 .w_full()
3761 .min_w_0()
3762 .when(ix > 0, |this| this.pt(body * 0.25))
3764 .when(*spread, |this| this.child(div()))
3765 .children({
3766 let mut items: Vec<Div> = Vec::with_capacity(children.len());
3767
3768 for (child_ix, child) in children.iter().enumerate() {
3769 match child {
3770 BlockNode::Paragraph { .. } => {
3771 let last_not_list = child_ix > 0
3772 && !matches!(
3773 children[child_ix - 1],
3774 BlockNode::List { .. }
3775 );
3776
3777 let text = child.render_block(
3778 NodeRenderOptions {
3779 depth: options.depth + 1,
3780 todo: checked.is_some(),
3781 is_last: true,
3782 ..options
3783 },
3784 node_cx,
3785 window,
3786 cx,
3787 );
3788
3789 if last_not_list {
3793 if let Some(preceding_row) = items.pop() {
3794 items.push(
3795 div().child(preceding_row).child(
3796 div()
3797 .w_full()
3798 .pl(indent)
3799 .overflow_hidden()
3800 .child(text),
3801 ),
3802 );
3803 continue;
3804 }
3805 }
3806
3807 items.push(Self::render_list_item_row(
3808 text,
3809 ix,
3810 options,
3811 *checked,
3812 &node_cx.style,
3813 window.line_height(),
3814 indent,
3815 ));
3816 }
3817 BlockNode::List { .. } => {
3818 items.push(div().ml(indent).child(child.render_block(
3819 NodeRenderOptions {
3820 depth: options.depth + 1,
3821 todo: checked.is_some(),
3822 is_last: true,
3823 ..options
3824 },
3825 node_cx,
3826 window,
3827 cx,
3828 )));
3829 }
3830 BlockNode::Root { .. }
3831 | BlockNode::Heading { .. }
3832 | BlockNode::Blockquote { .. }
3833 | BlockNode::CodeBlock(_)
3834 | BlockNode::Custom { .. }
3835 | BlockNode::Table(_)
3836 | BlockNode::HorizontalRule { .. } => {
3837 let block = child.render_block(
3838 NodeRenderOptions {
3839 depth: options.depth + 1,
3840 todo: checked.is_some(),
3841 is_last: true,
3842 ..options
3843 },
3844 node_cx,
3845 window,
3846 cx,
3847 );
3848
3849 if child_ix == 0 {
3850 items.push(Self::render_list_item_row(
3851 block,
3852 ix,
3853 options,
3854 *checked,
3855 &node_cx.style,
3856 window.line_height(),
3857 indent,
3858 ));
3859 } else {
3860 items.push(
3864 div()
3865 .w_full()
3866 .min_w_0()
3867 .pl(indent)
3868 .overflow_hidden()
3869 .child(block),
3870 );
3871 }
3872 }
3873 BlockNode::ListItem { .. }
3874 | BlockNode::Break { .. }
3875 | BlockNode::Definition { .. }
3876 | BlockNode::Unknown => {}
3877 }
3878 }
3879 items
3880 })
3881 .into_any_element()
3882 }
3883 _ => div().into_any_element(),
3884 }
3885 }
3886
3887 fn render_table(
3909 item: &BlockNode,
3910 options: &NodeRenderOptions,
3911 node_cx: &NodeContext,
3912 text_size: Pixels,
3913 window: &mut Window,
3914 cx: &mut App,
3915 ) -> impl IntoElement {
3916 const CELL_WRAP_MAX_LINES: f32 = 2.0;
3922 const CELL_WRAP_MIN_PX: f32 = 160.0;
3923 const CELL_WRAP_MAX_PX: f32 = 480.0;
3924 const TABLE_BORDER_PX: f32 = 2.0; let table = match item {
3927 BlockNode::Table(table) => table,
3928 _ => return div().into_any_element(),
3929 };
3930
3931 let style = &node_cx.style;
3932 let col_count = table
3933 .children
3934 .iter()
3935 .map(|row| row.children.len())
3936 .max()
3937 .unwrap_or(0);
3938 let col_w = measure_table_columns(table, col_count, node_cx, window, cx);
3939 let scroll = matches!(style.table().overflow.x, Some(Overflow::Scroll));
3940 let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
3941 let rem_size = window.rem_size();
3942 let wrap_min_px = f32::from(rems(4.).to_pixels(rem_size));
3943 let padding_delta = table_cell_padding_x(style, rem_size) - CELL_PAD_PX;
3945 let scale_factor = window.scale_factor();
3949 let device_ceil = |width: f32| (width * scale_factor).ceil() / scale_factor;
3950 let columns: Vec<TableColumn> = col_w
3951 .iter()
3952 .enumerate()
3953 .map(|(ix, &w)| {
3954 let w = w + padding_delta;
3955 let floor = if scroll && nowrap {
3956 w
3957 } else if scroll {
3958 (w / CELL_WRAP_MAX_LINES)
3959 .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
3960 .min(w)
3961 } else {
3962 w.min(wrap_min_px)
3963 };
3964 let divider = if ix + 1 < col_count {
3965 CELL_BORDER_PX
3966 } else {
3967 0.
3968 };
3969 let max = device_ceil(w - divider);
3970 TableColumn {
3971 min: device_ceil(floor - divider).min(max),
3972 max,
3973 divider,
3974 }
3975 })
3976 .collect();
3977
3978 let rows = Self::render_table_rows(table, &columns, node_cx, window, cx);
3979 let frame = StyleRefinement::default()
3980 .text_size(text_size)
3981 .bg(style
3982 .table_background()
3983 .unwrap_or(cx.theme().tokens.colors.surface))
3984 .border_1()
3985 .border_color(style.border())
3986 .refine_style(style.table());
3987 let body = if scroll {
3988 let scroll_handle = window
3989 .use_keyed_state(
3990 block_element_id("table-scroll", table.span, options.ix),
3991 cx,
3992 |_, _| ScrollHandle::default(),
3993 )
3994 .read(cx)
3995 .clone();
3996 let min_total_w = columns
3997 .iter()
3998 .map(|column| column.min + column.divider)
3999 .sum::<f32>()
4000 + TABLE_BORDER_PX;
4001 horizontal_scroll_area(
4011 block_element_id("table", table.span, options.ix),
4012 &scroll_handle,
4013 &frame,
4014 div().min_w_full().w(px(min_total_w)).children(rows),
4020 )
4021 .into_any_element()
4022 } else {
4023 div()
4024 .w_full()
4025 .overflow_hidden()
4026 .refine_style(&frame)
4027 .children(rows)
4028 .into_any_element()
4029 };
4030
4031 div()
4032 .pb(block_gap(options, style))
4033 .w_full()
4034 .child(body)
4035 .children(node_cx.table_actions.clone().map(|f| {
4042 div()
4043 .id(block_element_id("table-actions", table.span, options.ix))
4044 .mt_1()
4045 .child(f(&table.cached_table_data(), window, cx))
4046 }))
4047 .into_any_element()
4048 }
4049
4050 fn table_row_corner_radii(style: &TextViewStyle, window: &Window) -> [Option<Pixels>; 4] {
4062 let rem_size = window.rem_size();
4063 let inset = |radius: Option<gpui::AbsoluteLength>| {
4064 radius.map(|radius| (radius.to_pixels(rem_size) - px(1.)).max(px(0.)))
4065 };
4066 let radii = &style.table().corner_radii;
4067 [
4068 inset(radii.top_left),
4069 inset(radii.top_right),
4070 inset(radii.bottom_left),
4071 inset(radii.bottom_right),
4072 ]
4073 }
4074
4075 fn render_table_rows(
4089 table: &Table,
4090 columns: &[TableColumn],
4091 node_cx: &NodeContext,
4092 window: &mut Window,
4093 cx: &mut App,
4094 ) -> Vec<AnyElement> {
4095 const CELL_GROW: f32 = 1e6;
4101 const SLACK_GROW: f32 = 1.0;
4102
4103 let style = &node_cx.style;
4104 let col_count = columns.len();
4105 let total_w = columns.iter().map(|column| column.max).sum::<f32>().max(1.);
4106 let mut slack_style = StyleRefinement::default();
4109 slack_style.background = style.table_cell().background.clone();
4110 slack_style.border_color = style.table_cell().border_color;
4111 let slack = |grow: f32| {
4112 div()
4113 .flex_basis(px(0.))
4114 .flex_grow(grow)
4115 .flex_shrink(0.)
4116 .border_color(style.border())
4117 .refine_style(&slack_style)
4118 };
4119
4120 let row_count = table.children.len();
4121 let [top_left, top_right, bottom_left, bottom_right] =
4122 Self::table_row_corner_radii(style, window);
4123 let mut rows = Vec::with_capacity(row_count);
4124 let mut cell_ordinal = 0;
4125 for (row_ix, row) in table.children.iter().enumerate() {
4126 let mut cells = Vec::with_capacity(col_count * 2);
4127 for (ix, column) in columns.iter().enumerate() {
4128 let is_last_col = ix + 1 == col_count;
4129 let align = table.column_align(ix);
4130 let content = row.children.get(ix).map(|cell| {
4131 let fade_key = table
4132 .span
4133 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
4134 cell_ordinal += 1;
4135 cell.children.render(fade_key, node_cx, window, cx)
4136 });
4137
4138 let grow = SLACK_GROW * column.max / total_w;
4139 let (lead, trail) = match align {
4140 ColumnumnAlign::None | ColumnumnAlign::Left => (0., grow),
4141 ColumnumnAlign::Center => (grow / 2., grow / 2.),
4142 ColumnumnAlign::Right => (grow, 0.),
4143 };
4144 if lead > 0. {
4145 cells.push(slack(lead).into_any_element());
4146 }
4147 cells.push(
4148 TableCellElement(
4149 div()
4150 .flex_basis(px(0.))
4151 .flex_grow(CELL_GROW)
4152 .min_w(px(column.min))
4153 .max_w(px(column.max))
4154 .overflow_hidden()
4155 .when(align == ColumnumnAlign::Center, |this| this.text_center())
4156 .when(align == ColumnumnAlign::Right, |this| this.text_right())
4157 .px_3()
4158 .py(px(7.))
4159 .refine_style(&style.table_cell())
4160 .children(content)
4161 .into_any_element(),
4162 )
4163 .into_any_element(),
4164 );
4165 if trail > 0. || !is_last_col {
4166 cells.push(
4167 slack(trail)
4168 .when(!is_last_col, |this| this.border_r_1())
4169 .into_any_element(),
4170 );
4171 }
4172 }
4173 rows.push(
4174 div()
4175 .w_full()
4176 .when(row_ix < row_count - 1, |this| this.border_b_1())
4177 .border_color(style.border())
4178 .flex()
4179 .flex_row()
4180 .when(row_ix == 0, |this| {
4184 this.bg(style.code_background())
4185 .text_color(style.foreground())
4186 .when_some(top_left, |this, radius| this.rounded_tl(radius))
4187 .when_some(top_right, |this, radius| this.rounded_tr(radius))
4188 .refine_style(&style.table_head())
4189 })
4190 .when(row_ix + 1 == row_count, |this| {
4191 this.when_some(bottom_left, |this, radius| this.rounded_bl(radius))
4192 .when_some(bottom_right, |this, radius| this.rounded_br(radius))
4193 })
4194 .children(cells)
4195 .into_any_element(),
4196 );
4197 }
4198 rows
4199 }
4200
4201 pub(crate) fn render_block(
4202 &self,
4203 options: NodeRenderOptions,
4204 node_cx: &NodeContext,
4205 window: &mut Window,
4206 cx: &mut App,
4207 ) -> AnyElement {
4208 let mb = block_gap(&options, &node_cx.style);
4209
4210 match self {
4211 BlockNode::Root { children, .. } => div()
4212 .children(children.into_iter().enumerate().map(move |(ix, node)| {
4213 let (is_first, prev) = match flow_position(children, ix) {
4217 (true, _) => (options.is_first, options.prev),
4218 (false, prev) => (false, prev),
4219 };
4220 node.render_block(
4221 NodeRenderOptions {
4222 ix,
4223 is_first,
4224 prev,
4225 is_last: options.is_last && ix + 1 == children.len(),
4226 ..options
4227 },
4228 node_cx,
4229 window,
4230 cx,
4231 )
4232 }))
4233 .into_any_element(),
4234 BlockNode::Paragraph(paragraph) => gapped(
4235 paragraph.render(
4236 paragraph.span.map(|span| TextLeafKey::block(span.start)),
4237 node_cx,
4238 window,
4239 cx,
4240 ),
4241 mb,
4242 ),
4243 BlockNode::Heading {
4244 level,
4245 children,
4246 span,
4247 } => {
4248 let (_, line_height, section_gap) = heading_metrics(*level);
4249 let refinement = node_cx.style.heading(*level);
4250 let text_size = heading_size(*level, node_cx, window);
4251 let top = if options.is_first || options.depth > 0 {
4257 px(0.)
4258 } else {
4259 match options.prev {
4260 PrevBlock::Heading(_) => text_size * 0.25,
4261 PrevBlock::Rule => (text_size * section_gap
4262 - rule_space(body_font_size(node_cx, window)))
4263 .max(px(0.)),
4264 PrevBlock::Other => (text_size * section_gap
4265 - node_cx.style.paragraph_gap().to_pixels(window.rem_size()))
4266 .max(px(0.)),
4267 }
4268 };
4269 let bottom = if options.is_last {
4270 px(0.)
4271 } else {
4272 text_size * HEADING_BOTTOM_GAP
4273 };
4274
4275 div()
4276 .pt(top)
4277 .pb(bottom)
4278 .whitespace_normal()
4279 .text_size(text_size)
4280 .line_height(relative(line_height))
4281 .font_weight(FontWeight::SEMIBOLD)
4282 .when(*level >= 6, |this| {
4283 this.text_color(node_cx.style.muted_foreground())
4284 })
4285 .refine_style(&refinement)
4286 .child(children.render(
4287 span.map(|span| TextLeafKey::block(span.start)),
4288 node_cx,
4289 window,
4290 cx,
4291 ))
4292 .into_any_element()
4293 }
4294 BlockNode::Blockquote { children, .. } => gapped(
4295 div()
4296 .w_full()
4297 .text_color(node_cx.style.muted_foreground())
4298 .border_l_2()
4299 .border_color(node_cx.style.border())
4300 .pl(body_font_size(node_cx, window) * 0.9)
4301 .refine_style(node_cx.style.blockquote_style())
4302 .children({
4304 let children_len = children.len();
4305 children.into_iter().enumerate().map(move |(index, c)| {
4306 let is_last = index == children_len - 1;
4307 let (is_first, prev) = flow_position(children, index);
4308 window.with_text_style(
4309 Some(node_cx.style.blockquote_style().text.clone()),
4310 |window| {
4311 c.render_block(
4312 NodeRenderOptions {
4313 is_first,
4314 prev,
4315 ..options.is_last(is_last)
4316 },
4317 node_cx,
4318 window,
4319 cx,
4320 )
4321 },
4322 )
4323 })
4324 })
4325 .into_any_element(),
4326 mb,
4327 ),
4328 BlockNode::List {
4329 children,
4330 ordered,
4331 start,
4332 ..
4333 } => div()
4334 .w_full()
4335 .min_w_0()
4336 .pb(mb)
4337 .children({
4338 let mut items = Vec::with_capacity(children.len());
4339 let mut item_index = 0;
4340 for (ix, item) in children.into_iter().enumerate() {
4341 let is_item = item.is_list_item();
4342
4343 items.push(Self::render_list_item(
4344 item,
4345 item_index,
4346 NodeRenderOptions {
4347 ix,
4348 ordered: *ordered,
4349 list_start: *start,
4350 ..options
4351 },
4352 node_cx,
4353 window,
4354 cx,
4355 ));
4356
4357 if is_item {
4358 item_index += 1;
4359 }
4360 }
4361 items
4362 })
4363 .into_any_element(),
4364 BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
4365 BlockNode::Custom { node, state } => {
4366 let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
4367 Some(rendered) => rendered,
4368 None => div().child(node.as_text().to_string()).into_any_element(),
4369 };
4370
4371 div()
4372 .pb(mb)
4373 .child(CustomBlockElement {
4374 content: inner,
4375 state: state.clone(),
4376 })
4377 .into_any_element()
4378 }
4379 BlockNode::Table { .. } => {
4380 let text_size = body_font_size(node_cx, window) * (14. / 15.);
4385 window.with_text_style(
4386 Some(TextStyleRefinement {
4387 font_size: Some(text_size.into()),
4388 ..Default::default()
4389 }),
4390 |window| {
4391 Self::render_table(self, &options, node_cx, text_size, window, cx)
4392 .into_any_element()
4393 },
4394 )
4395 }
4396 BlockNode::HorizontalRule { .. } => {
4397 let space = rule_space(body_font_size(node_cx, window));
4401 let top = if options.is_first || options.depth > 0 {
4402 px(0.)
4403 } else {
4404 let above = match options.prev {
4405 PrevBlock::Rule => space,
4406 PrevBlock::Heading(level) => {
4407 heading_size(level, node_cx, window) * HEADING_BOTTOM_GAP
4408 }
4409 PrevBlock::Other => {
4410 node_cx.style.paragraph_gap().to_pixels(window.rem_size())
4411 }
4412 };
4413 (space - above).max(px(0.))
4414 };
4415 div()
4416 .pt(top)
4417 .when(!options.is_last, |this| this.pb(space))
4418 .child(div().bg(node_cx.style.border()).h(px(1.)))
4419 .into_any_element()
4420 }
4421 BlockNode::Break { .. } => div().into_any_element(),
4422 BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
4423 _ => {
4424 if cfg!(debug_assertions) {
4425 tracing::warn!("unknown implementation: {:?}", self);
4426 }
4427
4428 div().into_any_element()
4429 }
4430 }
4431 }
4432}
4433
4434#[cfg(test)]
4435mod tests {
4436 use super::*;
4437
4438 #[test]
4439 fn block_plugin_selection_copies_both_formats_and_clears_synchronously() {
4440 let source = "$$\nx\n$$";
4441 let mut node = MarkdownNode::new("math", ()).text("x").markdown(source);
4442 node.set_span(Some(Span {
4443 start: 0,
4444 end: source.len(),
4445 }));
4446 let state = BlockState::default();
4447 let same_content = BlockNode::Custom {
4448 node: node.clone(),
4449 state: BlockState::default(),
4450 };
4451 let block = BlockNode::Custom {
4452 node,
4453 state: state.clone(),
4454 };
4455 let document = crate::text::document::ParsedDocument {
4456 source: source.into(),
4457 blocks: Arc::new(vec![block.clone()]),
4458 };
4459 assert!(!block.has_selection());
4460 state.set_selection(Some(BlockSelection::Full));
4461 assert_eq!(block, same_content);
4463 for blocks in [None, Some(0..=0)] {
4464 assert_eq!(
4465 document.selected_text(SelectionFormat::Plain, blocks.clone()),
4466 "x\n"
4467 );
4468 assert_eq!(
4469 document.selected_text(SelectionFormat::Source, blocks),
4470 source
4471 );
4472 }
4473 assert_eq!(document.selected_source_range(), Some(0..source.len()));
4474
4475 block.clear_selection();
4477 assert!(!document.blocks[0].has_selection());
4478 assert_eq!(
4479 document.selected_text(SelectionFormat::Plain, Some(0..=0)),
4480 ""
4481 );
4482 assert_eq!(document.selected_source_range(), None);
4483 }
4484
4485 #[test]
4486 fn block_plugin_empty_endpoint_suppresses_virtualized_copy_fallback() {
4487 let state = BlockState::default();
4488 let block = BlockNode::Custom {
4489 node: MarkdownNode::new("math", ())
4490 .text("formula")
4491 .markdown("$$formula$$"),
4492 state: state.clone(),
4493 };
4494 let after = CodeBlock::from_code("after", None::<SharedString>);
4495 after.set_selection(0..5);
4496 let document = crate::text::document::ParsedDocument {
4497 blocks: Arc::new(vec![block, BlockNode::CodeBlock(after)]),
4498 ..Default::default()
4499 };
4500 assert!(
4501 document
4502 .selected_text(SelectionFormat::Plain, Some(0..=1))
4503 .contains("formula")
4504 );
4505 state.set_selection(Some(BlockSelection::Empty));
4506 assert!(document.blocks[0].has_selection());
4507 for format in [SelectionFormat::Plain, SelectionFormat::Source] {
4508 let text = document.selected_text(format, Some(0..=1));
4509 assert!(text.contains("after"));
4510 assert!(!text.contains("formula"));
4511 }
4512 }
4513
4514 #[test]
4515 fn block_plugin_geometry_selects_whole_blocks_in_both_drag_directions() {
4516 let bounds = Bounds::new(gpui::point(px(10.), px(20.)), gpui::size(px(100.), px(40.)));
4517 let inside = bounds.center();
4518 let above = gpui::point(px(0.), px(0.));
4519 let below = gpui::point(px(0.), px(80.));
4520 for (start, end) in [(above, inside), (inside, below), (above, below)] {
4521 assert!(custom_block_is_selected(bounds, start, end));
4522 assert!(custom_block_is_selected(bounds, end, start));
4523 }
4524 assert!(!custom_block_is_selected(bounds, inside, inside));
4525 for (start, end) in [
4528 (inside, inside + gpui::point(px(1.), px(0.))),
4529 (
4530 bounds.origin,
4531 bounds.bottom_right() - gpui::point(px(1.), px(1.)),
4532 ),
4533 ] {
4534 assert!(!custom_block_is_selected(bounds, start, end));
4535 assert!(!custom_block_is_selected(bounds, end, start));
4536 }
4537 assert!(!custom_block_is_selected(
4538 bounds,
4539 gpui::point(px(150.), px(30.)),
4540 gpui::point(px(160.), px(40.))
4541 ));
4542 }
4543
4544 #[test]
4545 fn selected_inline_objects_coalesce_surrounding_emphasis() {
4546 for (object_mark, expected) in [
4547 (TextMark::default().italic(), "*fore $x$ aft*"),
4548 (TextMark::default().italic().bold(), "*fore **$x$** aft*"),
4549 ] {
4550 let italic = TextMark::default().italic();
4551 let before = InlineNode::new("before ").marks(vec![(0..7, italic.clone())]);
4552 let formula =
4553 InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"))
4554 .marks(vec![(0..1, object_mark)]);
4555 let after = InlineNode::new(" after").marks(vec![(0..6, italic)]);
4556 {
4557 let mut state = formula.state.lock().unwrap();
4558 state.text = "before ".into();
4559 state.selection = Some((2..7).into());
4560 }
4561 *formula.custom_selection.lock().unwrap() = true;
4562 let paragraph = Paragraph {
4563 children: vec![before, formula, after],
4564 ..Default::default()
4565 };
4566 {
4567 let mut state = paragraph.state.lock().unwrap();
4568 state.text = " after".into();
4569 state.selection = Some((0..4).into());
4570 }
4571 assert_eq!(paragraph.selected_source(), expected);
4572 }
4573 }
4574
4575 #[test]
4576 fn consecutive_inline_objects_copy_atomically_without_neighboring_text() {
4577 let first =
4578 InlineNode::custom(MarkdownNode::new("math", ()).text("甲²").markdown("$甲^2$"));
4579 let second = InlineNode::custom(MarkdownNode::new("math", ()).text("b").markdown("$b$"));
4580 *first.custom_selection.lock().unwrap() = true;
4581 *second.custom_selection.lock().unwrap() = true;
4582 let paragraph = Paragraph {
4583 children: vec![first, second],
4584 ..Default::default()
4585 };
4586 assert_eq!(paragraph.selected_text(), "甲²b");
4587 assert_eq!(paragraph.selected_source(), "$甲^2$$b$");
4588 assert!(paragraph.has_selection());
4589 paragraph.clear_selection();
4590 assert_eq!(paragraph.selected_text(), "");
4591 assert_eq!(paragraph.selected_source(), "");
4592 assert!(!paragraph.has_selection());
4593 }
4594
4595 #[test]
4596 fn selected_inline_object_interleaves_runs_and_preserves_enclosing_mark() {
4597 let before = InlineNode::new("中文 ");
4598 let formula =
4599 InlineNode::custom(MarkdownNode::new("math", ()).text("x²").markdown("$x^2$"))
4600 .marks(vec![(0..3, TextMark::default().bold())]);
4601 let after = InlineNode::new(" English");
4602 {
4603 let mut preceding = formula.state.lock().unwrap();
4604 preceding.text = "中文 ".into();
4605 preceding.selection = Some((3..7).into());
4606 }
4607 *formula.custom_selection.lock().unwrap() = true;
4608 let paragraph = Paragraph {
4609 children: vec![before, formula, after],
4610 ..Default::default()
4611 };
4612 {
4613 let mut trailing = paragraph.state.lock().unwrap();
4614 trailing.text = " English".into();
4615 trailing.selection = Some((0..4).into());
4616 }
4617 assert_eq!(paragraph.selected_text(), "文 x² Eng");
4618 assert_eq!(paragraph.selected_source(), "文 **$x^2$** Eng");
4619 }
4620
4621 #[test]
4622 fn custom_inline_inherits_marks_and_resolves_link_references() {
4623 use gpui::{Empty, TestApp};
4624 let mut app = TestApp::new();
4625 let mut window = app.open_window(|_, _| Empty);
4626 window.update(|_, _, cx| {
4627 cx.set_global(crate::Theme::default());
4628 let mut node_cx = NodeContext::default();
4629 let mut mark = TextMark::default().bold();
4630 mark.italic = true;
4631 mark.strikethrough = true;
4632 mark.link = Some(LinkMark {
4633 identifier: Some("ref".into()),
4634 ..Default::default()
4635 });
4636 node_cx.link_refs.insert(
4637 "ref".into(),
4638 LinkMark {
4639 url: "https://example.com".into(),
4640 ..Default::default()
4641 },
4642 );
4643 let paragraph = Paragraph {
4644 children: vec![
4645 InlineNode::custom(MarkdownNode::new("test", ()).text("x"))
4646 .marks(vec![(0..1, mark)]),
4647 ],
4648 ..Default::default()
4649 };
4650 let items = paragraph.inline_flow_items(None, &[], &[], &node_cx, cx);
4651 let InlineFlowItem::Object { style, link, .. } = &items[0] else {
4652 panic!()
4653 };
4654 assert_eq!(style.font_weight, Some(FontWeight::BOLD));
4655 assert_eq!(style.font_style, Some(FontStyle::Italic));
4656 assert!(style.strikethrough.is_some());
4657 assert_eq!(link.as_ref().unwrap().url.as_ref(), "https://example.com");
4658 });
4659 }
4660
4661 #[test]
4662 fn table_column_uses_prepared_inline_metrics_after_resource_update() {
4663 use crate::text::InlineElement;
4664 use crate::text::inline::test_draw::in_prepaint;
4665 use gpui::{Image, ImageFormat, TestApp};
4666 let mut app = TestApp::new();
4667 let width = Arc::new(std::sync::atomic::AtomicUsize::new(400));
4668 let render_width = width.clone();
4669 let mut node_cx = NodeContext::default();
4670 node_cx.markdown_extensions = Arc::new(MarkdownExtensions::default().plugin(
4671 crate::text::markdown_ext::TestInlinePlugin::new("test").render_with(
4672 move |_, _, _, _| {
4673 Some(
4674 InlineElement::new(
4675 gpui::img(Arc::new(Image::from_bytes(
4676 ImageFormat::Svg,
4677 b"<svg/>".to_vec(),
4678 )))
4679 .w(px(
4680 render_width.load(std::sync::atomic::Ordering::Relaxed) as f32
4681 ))
4682 .h(px(20.)),
4683 )
4684 .with_baseline(px(15.)),
4685 )
4686 },
4687 ),
4688 ));
4689 let table = table_of(
4690 vec![
4691 vec![plain_cell("ordinary text")],
4692 vec![TableCell {
4693 children: Paragraph {
4694 children: vec![InlineNode::custom(MarkdownNode::new("test", ()).text("x"))],
4695 ..Default::default()
4696 },
4697 width: None,
4698 }],
4699 ],
4700 vec![],
4701 );
4702 in_prepaint(&mut app, move |window, cx| {
4703 let mut cached = None;
4704 for expected in [400, 600, 10, 400] {
4705 width.store(expected, std::sync::atomic::Ordering::Relaxed);
4706 let measured = measure_table_columns(&table, 1, &node_cx, window, cx)[0];
4707 let current = cached_column_widths(&table);
4708 assert_eq!(
4709 measured,
4710 current.widths[0].max(expected as f32 + CELL_PAD_PX)
4711 );
4712 assert_eq!(current.custom_cells, vec![(1, 0)]);
4713 if let Some(cached) = cached.replace(current.clone()) {
4714 assert!(Arc::ptr_eq(&cached, ¤t));
4715 }
4716 }
4717 });
4718 }
4719
4720 fn cached_column_widths(table: &Table) -> Arc<TableColumnWidths> {
4721 table.column_widths_cache.0.lock().unwrap().clone().unwrap()
4722 }
4723
4724 #[test]
4725 fn blockquote_typography_reaches_nested_table_measurement() {
4726 use crate::text::inline::test_draw::in_prepaint;
4727 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
4728 use gpui::TestApp;
4729
4730 let quote = BlockNode::Blockquote {
4731 children: vec![BlockNode::Table(table_of(
4732 vec![vec![plain_cell("0123456789")]],
4733 vec![],
4734 ))],
4735 span: None,
4736 };
4737 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4738 in_prepaint(&mut app, move |window, cx| {
4739 cx.set_global(crate::Theme::default());
4740 window.with_text_style(
4741 Some(
4742 StyleRefinement::default()
4743 .font_family(BODY)
4744 .text_size(px(15.))
4745 .text,
4746 ),
4747 |window| {
4748 for (refinement, expected_width) in [
4749 (StyleRefinement::default(), 94.),
4750 (
4751 StyleRefinement::default()
4752 .font_family(MONO)
4753 .text_size(px(30.))
4754 .font_weight(FontWeight::BOLD),
4755 199.,
4756 ),
4757 ] {
4758 let node_cx = NodeContext {
4759 style: Arc::new(
4760 TextViewStyle::default().with_blockquote_style(refinement),
4761 ),
4762 body_font_size: px(15.),
4763 ..Default::default()
4764 };
4765 quote.render_block(NodeRenderOptions::default(), &node_cx, window, cx);
4766 let BlockNode::Blockquote { children, .. } = "e else {
4767 unreachable!()
4768 };
4769 let BlockNode::Table(table) = &children[0] else {
4770 unreachable!()
4771 };
4772 assert_eq!(cached_column_widths(table).widths, vec![expected_width]);
4774 }
4775 },
4776 );
4777 });
4778 }
4779
4780 #[test]
4781 fn table_columns_cache_preserves_plain_widths_and_clones() {
4782 use crate::text::inline::test_draw::in_prepaint;
4783 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem, record_shaped_lines};
4784 use gpui::{Refineable as _, TestApp, TextStyle};
4785
4786 let long = "é界".repeat(2048);
4787 let table = table_of(
4788 vec![
4789 vec![plain_cell(&format!(" \t{long} \nshort\n ")), plain_cell("")],
4790 vec![plain_cell("short")],
4791 vec![],
4792 vec![plain_cell(""), plain_cell("a\nabcdefghijklmnop")],
4793 ],
4794 vec![],
4795 );
4796 let uncached = table.clone();
4797 let node_cx = NodeContext::default();
4798 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4799 let (table, node_cx, cached) = in_prepaint(&mut app, move |window, cx| {
4800 window.set_rem_size(px(20.));
4801 let style = TextStyle {
4802 font_family: BODY.into(),
4803 font_size: rems(1.).into(),
4804 ..Default::default()
4805 };
4806 let cached =
4807 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
4808 let (widths, shaped) = record_shaped_lines(|| {
4809 measure_table_columns(&table, 2, &node_cx, window, cx)
4810 });
4811 assert_eq!(widths, vec![40960. + CELL_PAD_PX + CELL_BORDER_PX, 184.]);
4812 assert!(shaped.iter().any(|line| line == &long));
4813 assert_eq!(
4814 table, uncached,
4815 "derived widths do not change table equality"
4816 );
4817 assert!(uncached.column_widths_cache.0.lock().unwrap().is_none());
4818
4819 let cached = cached_column_widths(&table);
4820 let cloned = table.clone();
4821 for same in [&table, &cloned] {
4822 let (again, shaped) = record_shaped_lines(|| {
4823 measure_table_columns(same, 2, &node_cx, window, cx)
4824 });
4825 assert_eq!(again, widths);
4826 assert!(shaped.is_empty());
4827 assert!(Arc::ptr_eq(&cached, &cached_column_widths(same)));
4828 }
4829
4830 assert_eq!(
4832 measure_table_columns(&cloned, 3, &node_cx, window, cx),
4833 vec![widths[0], widths[1] + CELL_BORDER_PX, CELL_MIN_PX]
4834 );
4835 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&cloned)));
4836 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4837 assert!(
4838 measure_table_columns(&Table::default(), 0, &node_cx, window, cx)
4839 .is_empty()
4840 );
4841 cached
4842 });
4843 (table, node_cx, cached)
4844 });
4845 in_prepaint(&mut app, move |window, cx| {
4848 window.set_rem_size(cached.rem_size);
4849 window.with_text_style(
4850 Some(cached.text_styles.body.subtract(&Default::default())),
4851 |window| {
4852 measure_table_columns(&table, 2, &node_cx, window, cx);
4853 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4854 },
4855 );
4856 });
4857 }
4858
4859 #[test]
4860 fn table_columns_cache_tracks_typography() {
4861 use crate::text::inline::test_draw::in_prepaint;
4862 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
4863 use gpui::{Refineable as _, TestApp, TextStyle};
4864
4865 let code = "0123456789";
4866 let mut paragraph = Paragraph::default();
4867 paragraph
4868 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
4869 let table = table_of(
4870 vec![vec![
4871 plain_cell(code),
4872 TableCell {
4873 children: paragraph,
4874 width: None,
4875 },
4876 ]],
4877 vec![],
4878 );
4879 let base = TextStyle {
4880 font_family: BODY.into(),
4881 font_size: rems(1.).into(),
4882 ..Default::default()
4883 };
4884 let mut larger = base.clone();
4885 larger.font_size = rems(2.).into();
4886 let mut bold = base.clone();
4887 bold.font_weight = FontWeight::BOLD;
4888 let mut mono = base.clone();
4889 mono.font_family = MONO.into();
4890 let normal_code = HighlightStyle::default();
4891 let bold_code = HighlightStyle {
4892 font_weight: Some(FontWeight::BOLD),
4893 ..Default::default()
4894 };
4895 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4896 in_prepaint(&mut app, move |window, cx| {
4897 let mut previous = None;
4898 for (style, rem_size, family, inline_code, expected) in [
4899 (base.clone(), 20., MONO, normal_code, [125., 207.]),
4900 (base.clone(), 30., MONO, normal_code, [175., 294.5]),
4901 (larger, 20., MONO, normal_code, [225., 382.]),
4902 (bold, 20., MONO, normal_code, [175., 250.75]),
4903 (mono, 20., MONO, normal_code, [225., 207.]),
4904 (base.clone(), 20., MONO, normal_code, [125., 207.]),
4905 (base.clone(), 20., BODY, normal_code, [125., 119.5]),
4906 (base.clone(), 20., MONO, normal_code, [125., 207.]),
4907 (base, 20., MONO, bold_code, [125., 250.75]),
4908 ] {
4909 let mut theme = crate::Theme::default();
4910 theme.tokens.typography.mono = family.into();
4911 cx.set_global(theme);
4912 window.set_rem_size(px(rem_size));
4913 let node_cx = NodeContext {
4914 style: Arc::new(TextViewStyle::default().with_inline_code(inline_code)),
4915 ..Default::default()
4916 };
4917 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
4918 let widths = measure_table_columns(&table, 2, &node_cx, window, cx);
4919 for (width, expected) in widths.iter().zip(expected) {
4920 assert!((width - expected).abs() < 0.01, "{width} != {expected}");
4921 }
4922 let cached = cached_column_widths(&table);
4923 if let Some(previous) = previous.replace(cached.clone()) {
4924 assert!(!Arc::ptr_eq(&previous, &cached));
4925 }
4926 assert_eq!(
4927 measure_table_columns(&table, 2, &node_cx, window, cx),
4928 widths
4929 );
4930 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4931 });
4932 }
4933 });
4934 }
4935
4936 #[test]
4937 fn table_columns_cache_is_rebuilt_for_reparsed_tables() {
4938 use crate::text::format::markdown;
4939 use crate::text::inline::test_draw::in_prepaint;
4940 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem};
4941 use gpui::{Refineable as _, TestApp, TextStyle};
4942
4943 let mut node_cx = NodeContext::default();
4944 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4945 in_prepaint(&mut app, move |window, cx| {
4946 let style = TextStyle {
4947 font_family: BODY.into(),
4948 font_size: px(20.).into(),
4949 ..Default::default()
4950 };
4951 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
4952 let mut previous = None;
4953 for (text, expected) in [("longer cell", 134.), ("x", CELL_MIN_PX)] {
4954 let source = format!("| h |\n| - |\n| {text} |\n");
4955 let document = markdown::parse(&source, &mut node_cx).unwrap();
4956 let BlockNode::Table(table) = &document.blocks[0] else {
4957 panic!("expected a parsed table");
4958 };
4959 assert!(table.column_widths_cache.0.lock().unwrap().is_none());
4960 assert_eq!(
4961 measure_table_columns(table, 1, &node_cx, window, cx),
4962 vec![expected]
4963 );
4964 let cached = cached_column_widths(table);
4965 if let Some(previous) = previous.replace(cached.clone()) {
4966 assert!(!Arc::ptr_eq(&previous, &cached));
4967 }
4968 assert_eq!(document.source.as_ref(), source);
4969 }
4970 });
4971 });
4972 }
4973
4974 #[test]
4978 fn table_column_of_inline_code_cells_fits_the_mono_width() {
4979 use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
4980 use gpui::{Empty, TestApp};
4981
4982 let code = "method_name()";
4983 let mut paragraph = Paragraph::default();
4984 paragraph
4985 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
4986 let table = Table {
4987 children: vec![TableRow {
4988 children: vec![TableCell {
4989 children: paragraph,
4990 width: None,
4991 }],
4992 }],
4993 column_aligns: vec![],
4994 span: None,
4995 table_data_cache: TableDataCache::default(),
4996 column_widths_cache: TableColumnWidthsCache::default(),
4997 };
4998 let node_cx = NodeContext::default();
4999
5000 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5001 let mut window = app.open_window(|_, _| Empty);
5002 let (col_w, font_size) = window.update(|_, window, cx| {
5003 let mut theme = crate::Theme::default();
5004 theme.tokens.typography.mono = MONO.into();
5005 cx.set_global(theme);
5006 let font_size = window.text_style().font_size.to_pixels(window.rem_size());
5007 (
5008 measure_table_columns(&table, 1, &node_cx, window, cx),
5009 font_size,
5010 )
5011 });
5012
5013 let mono_w = f32::from(WideMonoTextSystem::width_of(code, MONO, font_size * 0.875));
5014 assert!(
5015 (col_w[0]
5016 - (mono_w + CELL_PAD_PX + crate::text::inline_flow::INLINE_CODE_PADDING * 2.))
5017 .abs()
5018 < 0.01,
5019 "col_w {} must fit the mono width {} plus padding {}",
5020 col_w[0],
5021 mono_w,
5022 CELL_PAD_PX
5023 );
5024 }
5025
5026 #[test]
5027 fn code_block_highlights_are_cached_by_highlighter_identity() {
5028 use std::sync::atomic::{AtomicUsize, Ordering};
5029
5030 let calls = Arc::new(AtomicUsize::new(0));
5031 let calls_for_highlighter = calls.clone();
5032 let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
5033 calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
5034 Vec::new()
5035 });
5036 let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
5037
5038 block.highlighted_styles(&highlighter);
5039 block.highlighted_styles(&highlighter);
5040 assert_eq!(calls.load(Ordering::Relaxed), 1);
5041
5042 let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
5043 block.highlighted_styles(&replacement);
5044 assert!(Arc::ptr_eq(
5045 &block
5046 .highlight_cache
5047 .lock()
5048 .unwrap()
5049 .as_ref()
5050 .unwrap()
5051 .highlighter,
5052 &replacement
5053 ));
5054 }
5055
5056 #[test]
5057 fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
5058 let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
5062 vec![(
5063 0..2,
5064 HighlightStyle {
5065 color: Some(gpui::rgb(0x0000ff).into()),
5066 ..Default::default()
5067 },
5068 )]
5069 });
5070 let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
5071 vec![(
5072 0..2,
5073 HighlightStyle {
5074 color: Some(gpui::rgb(0xffff00).into()),
5075 ..Default::default()
5076 },
5077 )]
5078 });
5079 let block = CodeBlock::from_code("42", Some("json"));
5080
5081 let light_styles = block.highlighted_styles(&light);
5082 let dark_styles = block.highlighted_styles(&dark);
5083
5084 assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
5085 assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
5086 assert_eq!(block.code(), "42", "the document must survive the swap");
5087 }
5088
5089 #[test]
5090 fn reconstruct_markdown_wraps_marked_runs() {
5091 let marks = vec![(0..4, TextMark::default().bold())];
5093 assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
5094 assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
5096 }
5097
5098 #[test]
5099 fn reconstruct_markdown_emits_unmarked_text_verbatim() {
5100 let text = "a b c";
5102 let marks = vec![(2..3, TextMark::default().code())];
5103 assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
5104 assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
5106 }
5107
5108 #[test]
5109 fn reconstruct_markdown_handles_code_italic_strike_link() {
5110 assert_eq!(
5111 reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
5112 "`x`"
5113 );
5114 assert_eq!(
5115 reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
5116 "*x*"
5117 );
5118 assert_eq!(
5119 reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
5120 "~~x~~"
5121 );
5122 let link = TextMark::default().link(LinkMark {
5123 url: "https://example.com".into(),
5124 ..Default::default()
5125 });
5126 assert_eq!(
5127 reconstruct_markdown("x", &[(0..1, link)], 0..1),
5128 "[x](https://example.com)"
5129 );
5130 }
5131
5132 #[test]
5133 fn reconstruct_markdown_nested_bold_italic() {
5134 let mark = TextMark::default().bold().italic();
5136 assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
5138 }
5139
5140 fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
5144 let combined: String = children.iter().map(|c| c.text.to_string()).collect();
5145 let paragraph = Paragraph {
5146 span: None,
5147 children,
5148 link_refs: HashMap::new(),
5149 state: Arc::new(Mutex::new(InlineState::default())),
5150 render_cache: ParagraphRenderCache::default(),
5151 };
5152 if let Ok(mut state) = paragraph.state.lock() {
5153 state.set_text(combined.into());
5154 }
5155 paragraph
5156 }
5157
5158 fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
5159 if let Ok(mut state) = paragraph.state.lock() {
5160 state.selection = Some(range.into());
5161 }
5162 }
5163
5164 #[test]
5165 fn paragraph_selected_source_maps_partial_selection_across_runs() {
5166 let children = vec![
5168 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
5169 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5170 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
5171 ];
5172 let paragraph = paragraph_with_children(children);
5173
5174 set_paragraph_selection(¶graph, 0..(9 + 4 + 6));
5176 assert_eq!(paragraph.selected_source(), "This has **bold** text.");
5177
5178 set_paragraph_selection(¶graph, 5..16);
5181 assert_eq!(paragraph.selected_source(), "has **bold** te");
5182
5183 set_paragraph_selection(¶graph, 10..12);
5185 assert_eq!(paragraph.selected_source(), "**ol**");
5186 }
5187
5188 #[test]
5189 fn paragraph_selected_source_matches_text_when_no_marks() {
5190 let children =
5191 vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
5192 let paragraph = paragraph_with_children(children);
5193 set_paragraph_selection(¶graph, 0..11);
5194 assert_eq!(paragraph.selected_source(), "plain words");
5195 assert_eq!(paragraph.selected_text(), "plain words");
5196 }
5197
5198 fn copy_across_styles(blocks: Vec<BlockNode>) -> String {
5201 let document = crate::text::document::ParsedDocument {
5202 blocks: blocks.into(),
5203 ..Default::default()
5204 };
5205 document.selected_text(SelectionFormat::SourceAcrossStyles, None)
5206 }
5207
5208 #[test]
5209 fn source_across_styles_copies_plain_text_within_one_style() {
5210 let paragraph = || {
5212 paragraph_with_children(vec![
5213 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
5214 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5215 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
5216 ])
5217 };
5218 let copy = |range: Range<usize>| {
5219 let paragraph = paragraph();
5220 set_paragraph_selection(¶graph, range);
5221 copy_across_styles(vec![BlockNode::Paragraph(paragraph)])
5222 };
5223
5224 assert_eq!(copy(10..12), "ol\n");
5226 assert_eq!(copy(9..13), "bold\n");
5227 assert_eq!(copy(0..9), "This has \n");
5229 assert_eq!(copy(5..16), "has **bold** te");
5231 assert_eq!(copy(8..10), " **b**");
5232 }
5233
5234 #[test]
5235 fn source_across_styles_drops_the_url_of_a_link_selected_alone() {
5236 let link = TextMark::default().link(LinkMark {
5237 url: "https://example.com".into(),
5238 ..Default::default()
5239 });
5240 let paragraph = paragraph_with_children(vec![
5241 InlineNode::new("see ").marks(vec![(0..4, TextMark::default())]),
5242 InlineNode::new("the docs").marks(vec![(0..8, link)]),
5243 ]);
5244
5245 set_paragraph_selection(¶graph, 4..12);
5246 assert_eq!(
5247 copy_across_styles(vec![BlockNode::Paragraph(paragraph.clone())]),
5248 "the docs\n"
5249 );
5250
5251 set_paragraph_selection(¶graph, 0..12);
5252 assert_eq!(
5253 copy_across_styles(vec![BlockNode::Paragraph(paragraph)]),
5254 "see [the docs](https://example.com)"
5255 );
5256 }
5257
5258 #[test]
5259 fn source_across_styles_drops_the_markup_of_a_single_block() {
5260 let heading = BlockNode::Heading {
5261 level: 2,
5262 children: selected_paragraph("Title"),
5263 span: None,
5264 };
5265 assert_eq!(copy_across_styles(vec![heading]), "Title\n");
5266
5267 let code = CodeBlock::from_code("let a = 1;\nlet b = 2;", Some("rust"));
5268 code.set_selection(0..10);
5269 assert_eq!(
5270 copy_across_styles(vec![BlockNode::CodeBlock(code)]),
5271 "let a = 1;\n"
5272 );
5273
5274 let item = |text: &str| BlockNode::ListItem {
5275 children: vec![BlockNode::Paragraph(selected_paragraph(text))],
5276 spread: false,
5277 checked: None,
5278 span: None,
5279 };
5280 let unselected_item = BlockNode::ListItem {
5281 children: vec![BlockNode::Paragraph(paragraph_with_children(vec![
5282 InlineNode::new("two"),
5283 ]))],
5284 spread: false,
5285 checked: None,
5286 span: None,
5287 };
5288 let list = BlockNode::List {
5289 ordered: false,
5290 start: None,
5291 span: None,
5292 children: vec![item("one"), unselected_item],
5293 };
5294 assert_eq!(copy_across_styles(vec![list]), "one\n");
5295 }
5296
5297 #[test]
5298 fn source_across_styles_keeps_the_source_across_blocks() {
5299 let item = |text: &str| BlockNode::ListItem {
5300 children: vec![BlockNode::Paragraph(selected_paragraph(text))],
5301 spread: false,
5302 checked: None,
5303 span: None,
5304 };
5305 let list = BlockNode::List {
5306 ordered: false,
5307 start: None,
5308 span: None,
5309 children: vec![item("one"), item("two")],
5310 };
5311 assert_eq!(copy_across_styles(vec![list]), "- one\n- two");
5312
5313 assert_eq!(
5314 copy_across_styles(vec![
5315 BlockNode::Paragraph(selected_paragraph("start")),
5316 BlockNode::Paragraph(selected_paragraph("end")),
5317 ]),
5318 "start\n\nend"
5319 );
5320 }
5321
5322 #[test]
5323 fn source_across_styles_keeps_the_source_of_an_inline_object() {
5324 let formula = InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"));
5325 *formula.custom_selection.lock().unwrap() = true;
5326 let paragraph = Paragraph {
5327 children: vec![formula],
5328 ..Default::default()
5329 };
5330 assert_eq!(
5331 copy_across_styles(vec![BlockNode::Paragraph(paragraph)]),
5332 "$x$"
5333 );
5334 }
5335
5336 fn selected_paragraph(text: &str) -> Paragraph {
5337 let len = text.len();
5338 let paragraph = paragraph_with_children(vec![
5339 InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
5340 ]);
5341 set_paragraph_selection(¶graph, 0..len);
5342 paragraph
5343 }
5344
5345 #[test]
5346 fn heading_selected_source_prefixes_hashes() {
5347 let heading = BlockNode::Heading {
5348 level: 2,
5349 children: selected_paragraph("Title"),
5350 span: None,
5351 };
5352 assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
5353 assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
5355 }
5356
5357 #[test]
5358 fn unordered_list_selected_source_prefixes_dash() {
5359 let list = BlockNode::List {
5360 ordered: false,
5361 start: None,
5362 span: None,
5363 children: vec![
5364 BlockNode::ListItem {
5365 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
5366 spread: false,
5367 checked: None,
5368 span: None,
5369 },
5370 BlockNode::ListItem {
5371 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
5372 spread: false,
5373 checked: None,
5374 span: None,
5375 },
5376 ],
5377 };
5378 assert_eq!(
5379 list.selected_text(SelectionFormat::Source),
5380 "- one\n- two\n"
5381 );
5382 }
5383
5384 #[test]
5385 fn ordered_list_selected_source_preserves_start() {
5386 let list = BlockNode::List {
5387 ordered: true,
5388 start: Some(3),
5389 span: None,
5390 children: vec![
5391 BlockNode::ListItem {
5392 children: vec![BlockNode::Paragraph(selected_paragraph("three"))],
5393 spread: false,
5394 checked: None,
5395 span: None,
5396 },
5397 BlockNode::ListItem {
5398 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
5399 spread: false,
5400 checked: None,
5401 span: None,
5402 },
5403 ],
5404 };
5405 assert_eq!(list.to_markdown(), "3. three\n4. four");
5406 assert_eq!(
5407 list.selected_text(SelectionFormat::Source),
5408 "3. three\n4. four\n"
5409 );
5410 }
5411
5412 #[test]
5413 fn ordered_list_selected_source_preserves_nested_starts_and_continuations() {
5414 let nested = BlockNode::List {
5415 ordered: true,
5416 start: Some(4),
5417 span: None,
5418 children: vec![
5419 BlockNode::ListItem {
5420 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
5421 spread: false,
5422 checked: None,
5423 span: None,
5424 },
5425 BlockNode::ListItem {
5426 children: vec![BlockNode::Paragraph(selected_paragraph("again"))],
5427 spread: false,
5428 checked: None,
5429 span: None,
5430 },
5431 ],
5432 };
5433 let nested_list = BlockNode::List {
5434 ordered: true,
5435 start: Some(3),
5436 span: None,
5437 children: vec![
5438 BlockNode::ListItem {
5439 children: vec![BlockNode::Paragraph(selected_paragraph("three")), nested],
5440 spread: false,
5441 checked: None,
5442 span: None,
5443 },
5444 BlockNode::ListItem {
5445 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
5446 spread: false,
5447 checked: None,
5448 span: None,
5449 },
5450 ],
5451 };
5452 assert_eq!(
5453 nested_list.selected_text(SelectionFormat::Source),
5454 "3. three\n 4. nested\n 5. again\n4. four\n"
5455 );
5456
5457 let loose_list = BlockNode::List {
5458 ordered: true,
5459 start: Some(3),
5460 span: None,
5461 children: vec![
5462 BlockNode::ListItem {
5463 children: vec![
5464 BlockNode::Paragraph(selected_paragraph("loose")),
5465 BlockNode::Paragraph(selected_paragraph("continuation")),
5466 ],
5467 spread: true,
5468 checked: None,
5469 span: None,
5470 },
5471 BlockNode::ListItem {
5472 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
5473 spread: false,
5474 checked: None,
5475 span: None,
5476 },
5477 ],
5478 };
5479 assert_eq!(
5480 loose_list.selected_text(SelectionFormat::Source),
5481 "3. loose\n continuation\n4. four\n"
5482 );
5483 }
5484
5485 #[test]
5486 fn nested_list_selected_source_indents_sublists() {
5487 let nested = BlockNode::List {
5491 ordered: false,
5492 start: None,
5493 span: None,
5494 children: vec![BlockNode::ListItem {
5495 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
5496 spread: false,
5497 checked: None,
5498 span: None,
5499 }],
5500 };
5501 let list = BlockNode::List {
5502 ordered: false,
5503 start: None,
5504 span: None,
5505 children: vec![
5506 BlockNode::ListItem {
5507 children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
5508 spread: false,
5509 checked: None,
5510 span: None,
5511 },
5512 BlockNode::ListItem {
5513 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
5514 spread: false,
5515 checked: None,
5516 span: None,
5517 },
5518 ],
5519 };
5520 assert_eq!(
5521 list.selected_text(SelectionFormat::Source),
5522 "- one\n - nested\n- two\n"
5523 );
5524 }
5525
5526 #[test]
5527 fn task_list_selected_source_restores_checkboxes() {
5528 let list = BlockNode::List {
5529 ordered: false,
5530 start: None,
5531 span: None,
5532 children: vec![
5533 BlockNode::ListItem {
5534 children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
5535 spread: false,
5536 checked: Some(true),
5537 span: None,
5538 },
5539 BlockNode::ListItem {
5540 children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
5541 spread: false,
5542 checked: Some(false),
5543 span: None,
5544 },
5545 ],
5546 };
5547 assert_eq!(
5548 list.selected_text(SelectionFormat::Source),
5549 "- [x] done\n- [ ] todo\n"
5550 );
5551 }
5552
5553 #[test]
5554 fn blockquote_selected_source_prefixes_gt() {
5555 let quote = BlockNode::Blockquote {
5556 span: None,
5557 children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
5558 };
5559 assert_eq!(
5560 quote.selected_text(SelectionFormat::Source),
5561 "> quoted text\n"
5562 );
5563 }
5564
5565 #[test]
5566 fn table_selected_source_pipes_cells_with_alignment_row() {
5567 let cell = |text: &str| TableCell {
5568 children: selected_paragraph(text),
5569 width: None,
5570 };
5571 let table = Table {
5572 children: vec![
5573 TableRow {
5574 children: vec![cell("Name"), cell("Age")],
5575 },
5576 TableRow {
5577 children: vec![cell("Alice"), cell("30")],
5578 },
5579 ],
5580 column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5581 span: None,
5582 table_data_cache: TableDataCache::default(),
5583 column_widths_cache: TableColumnWidthsCache::default(),
5584 };
5585 let block = BlockNode::Table(table);
5586 assert_eq!(
5587 block.selected_text(SelectionFormat::Source),
5588 "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
5589 );
5590 }
5591
5592 #[test]
5595 fn a_selection_in_one_table_cell_copies_only_that_cell() {
5596 let table = table_of(
5597 vec![
5598 vec![plain_cell("Name"), plain_cell("Age")],
5599 vec![
5600 TableCell {
5601 children: selected_paragraph("Alice"),
5602 width: None,
5603 },
5604 plain_cell("30"),
5605 ],
5606 ],
5607 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5608 );
5609 let block = BlockNode::Table(table);
5610 assert_eq!(block.selected_text(SelectionFormat::Plain).trim(), "Alice");
5611 assert_eq!(block.selected_text(SelectionFormat::Source), "Alice\n");
5612 }
5613
5614 fn plain_cell(text: &str) -> TableCell {
5617 TableCell {
5618 children: Paragraph::new(text.to_string()),
5619 width: None,
5620 }
5621 }
5622
5623 fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
5624 Table {
5625 children: rows
5626 .into_iter()
5627 .map(|children| TableRow { children })
5628 .collect(),
5629 column_aligns,
5630 span: None,
5631 table_data_cache: TableDataCache::default(),
5632 column_widths_cache: TableColumnWidthsCache::default(),
5633 }
5634 }
5635
5636 #[test]
5637 fn table_to_markdown_pipes_cells_with_alignment_row() {
5638 let table = table_of(
5639 vec![
5640 vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
5641 vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
5642 ],
5643 vec![
5644 ColumnumnAlign::Left,
5645 ColumnumnAlign::Center,
5646 ColumnumnAlign::Right,
5647 ],
5648 );
5649
5650 assert_eq!(
5651 table.to_markdown(),
5652 "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
5653 );
5654 assert_eq!(
5656 BlockNode::Table(table.clone()).to_markdown(),
5657 table.to_markdown()
5658 );
5659 }
5660
5661 #[test]
5662 fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
5663 let table = table_of(
5664 vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
5665 vec![ColumnumnAlign::Left],
5666 );
5667
5668 assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
5669 }
5670
5671 #[test]
5672 fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
5673 let bold = TableCell {
5674 children: paragraph_with_children(vec![
5675 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5676 ]),
5677 width: None,
5678 };
5679 let table = table_of(
5680 vec![
5681 vec![plain_cell("a | b"), plain_cell("plain")],
5682 vec![plain_cell("c"), bold],
5683 ],
5684 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
5685 );
5686
5687 assert_eq!(
5688 table.to_markdown(),
5689 "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
5690 );
5691 }
5692
5693 #[test]
5694 fn table_data_snapshots_plain_cells_and_markdown() {
5695 let mut table = table_of(
5696 vec![
5697 vec![plain_cell(" Name "), plain_cell("Age")],
5698 vec![plain_cell("Alice"), plain_cell("30")],
5699 ],
5700 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5701 );
5702 table.span = Some(Span { start: 4, end: 42 });
5703
5704 let data = table.table_data();
5705 assert_eq!(data.headers, vec!["Name", "Age"]);
5706 assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
5707 assert_eq!(data.markdown, table.to_markdown());
5708 assert_eq!(data.span, Some(4..42));
5709 }
5710
5711 #[test]
5712 fn cloned_table_keeps_cached_table_data() {
5713 let table = table_of(
5716 vec![vec![plain_cell("Name")], vec![plain_cell("Alice")]],
5717 vec![ColumnumnAlign::Left],
5718 );
5719 let data = table.cached_table_data();
5720
5721 let cloned = table.clone();
5722 assert!(Arc::ptr_eq(&data, &cloned.cached_table_data()));
5723 }
5724
5725 #[test]
5726 fn table_data_handles_tables_without_rows() {
5727 let header_only = table_of(
5729 vec![vec![plain_cell("Name"), plain_cell("Age")]],
5730 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
5731 );
5732 let data = header_only.table_data();
5733 assert_eq!(data.headers, vec!["Name", "Age"]);
5734 assert!(data.rows.is_empty());
5735 assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
5736
5737 assert_eq!(Table::default().table_data(), TableData::default());
5739 }
5740
5741 #[test]
5742 fn test_image_node_source() {
5743 use gpui::{ImageFormat, ImageSource, Resource};
5744
5745 fn image_node(url: &str) -> ImageNode {
5746 ImageNode {
5747 url: url.into(),
5748 ..Default::default()
5749 }
5750 }
5751
5752 fn assert_uri(url: &str) {
5756 match image_node(url).source() {
5757 ImageSource::Resource(Resource::Uri(uri)) => assert_eq!(uri.as_ref(), url),
5758 _ => panic!("expected Uri for {url:?}"),
5759 }
5760 }
5761 assert_uri("https://example.com/logo.png");
5762 assert_uri("http://example.com/logo.png");
5763 assert_uri("website/public/logo.svg");
5764 assert_uri("./images/a.png");
5765 assert_uri("../images/a.png");
5766 assert_uri("/absolute/path/logo.svg");
5767 assert_uri("file:///absolute/path/logo.svg");
5768 assert_uri(r"C:\images\logo.png");
5769 assert_uri("docs/a:b.png");
5770 assert_uri("data:text/plain;base64,aGVsbG8=");
5771
5772 let node = image_node("data:image/png;base64,iVBORw0KGgo=");
5775 let ImageSource::Image(first) = node.source() else {
5776 panic!("expected an embedded image");
5777 };
5778 assert_eq!(first.format(), ImageFormat::Png);
5779 assert_eq!(first.bytes(), b"\x89PNG\r\n\x1a\n");
5780 let ImageSource::Image(second) = node.source() else {
5781 panic!("expected an embedded image");
5782 };
5783 assert!(Arc::ptr_eq(&first, &second));
5784 }
5785
5786 fn image_paragraph(alt: &str, url: &str) -> Paragraph {
5787 let image = ImageNode {
5788 url: url.into(),
5789 alt: Some(alt.into()),
5790 ..Default::default()
5791 };
5792 Paragraph {
5793 span: None,
5794 children: vec![InlineNode::image(image)],
5795 link_refs: HashMap::new(),
5796 state: Arc::new(Mutex::new(InlineState::default())),
5797 render_cache: ParagraphRenderCache::default(),
5798 }
5799 }
5800
5801 #[test]
5804 fn marks_round_trip_through_reconstruction() {
5805 let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
5806
5807 assert_eq!(wrap(TextMark::default().bold()), "**x**");
5808 assert_eq!(wrap(TextMark::default().italic()), "*x*");
5809 assert_eq!(wrap(TextMark::default().code()), "`x`");
5810 assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
5811 assert_eq!(
5812 wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
5813 "==x=="
5814 );
5815 assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
5817
5818 assert_eq!(
5820 wrap(TextMark::default().link(LinkMark {
5821 url: "https://example.com".into(),
5822 title: Some("Tip".into()),
5823 ..Default::default()
5824 })),
5825 "[x](https://example.com \"Tip\")"
5826 );
5827 }
5828
5829 #[test]
5832 fn document_selected_source_slices_covered_blocks_from_the_source() {
5833 use crate::text::document::ParsedDocument;
5834
5835 let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
5838 let list = "3. _one_\n4. two";
5839 let list_start = source.find(list).unwrap();
5840 let rule_start = source.find("---").unwrap();
5841
5842 let document = ParsedDocument {
5843 source: source.into(),
5844 blocks: vec![
5845 BlockNode::Paragraph(selected_paragraph("start")),
5846 BlockNode::List {
5847 ordered: true,
5848 start: Some(3),
5849 children: vec![],
5850 span: Some(Span {
5851 start: list_start,
5852 end: list_start + list.len(),
5853 }),
5854 },
5855 BlockNode::HorizontalRule {
5856 span: Some(Span {
5857 start: rule_start,
5858 end: rule_start + 3,
5859 }),
5860 },
5861 BlockNode::Paragraph(selected_paragraph("end")),
5862 ]
5863 .into(),
5864 };
5865
5866 assert_eq!(
5867 document.selected_text(SelectionFormat::Source, None),
5868 "start\n\n3. _one_\n4. two\n\n---\n\nend"
5869 );
5870 }
5871
5872 #[test]
5873 fn document_selected_source_includes_enclosed_image() {
5874 use crate::text::document::ParsedDocument;
5875
5876 let source = "before\n\n\n\nafter";
5880 let image_markdown = "";
5881 let start = source.find(image_markdown).unwrap();
5882 let mut image = image_paragraph("alt", "https://example.com/i.png");
5883 image.span = Some(Span {
5884 start,
5885 end: start + image_markdown.len(),
5886 });
5887
5888 let document = ParsedDocument {
5889 source: source.into(),
5890 blocks: vec![
5891 BlockNode::Paragraph(selected_paragraph("before")),
5892 BlockNode::Paragraph(image),
5893 BlockNode::Paragraph(selected_paragraph("after")),
5894 ]
5895 .into(),
5896 };
5897 assert_eq!(
5898 document.selected_text(SelectionFormat::Source, None),
5899 "before\n\n\n\nafter"
5900 );
5901 }
5902
5903 #[test]
5904 fn document_selected_source_drops_unenclosed_image() {
5905 use crate::text::document::ParsedDocument;
5906
5907 let document = ParsedDocument {
5910 source: String::new().into(),
5911 blocks: vec![
5912 BlockNode::Paragraph(selected_paragraph("before")),
5913 BlockNode::Paragraph(image_paragraph("alt", "u")),
5914 ]
5915 .into(),
5916 };
5917 assert_eq!(
5918 document.selected_text(SelectionFormat::Source, None),
5919 "before"
5920 );
5921 }
5922
5923 fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
5924 let block = CodeBlock::new(
5925 code.to_string().into(),
5926 lang.map(|l| l.to_string().into()),
5927 None::<Span>,
5928 );
5929 if let Ok(mut state) = block.state.lock() {
5930 let len = state.text.len();
5931 state.selection = Some((0..len).into());
5932 }
5933 BlockNode::CodeBlock(block)
5934 }
5935
5936 #[test]
5937 fn code_block_selected_source_wraps_in_fence_with_lang() {
5938 let block = selected_code_block("let x = 1;\n", Some("rust"));
5939 let code = block.selected_text(SelectionFormat::Plain);
5940 let code_trimmed = code.trim_end_matches('\n');
5941 assert_eq!(
5945 block.selected_text(SelectionFormat::Source),
5946 format!("```rust\n{}\n```\n", code_trimmed)
5947 );
5948 assert!(
5949 block
5950 .selected_text(SelectionFormat::Source)
5951 .starts_with("```rust\n")
5952 );
5953 assert!(
5954 block
5955 .selected_text(SelectionFormat::Source)
5956 .trim_end()
5957 .ends_with("\n```")
5958 );
5959 }
5960
5961 #[test]
5962 fn code_block_selected_source_without_lang() {
5963 let block = selected_code_block("plain\n", None);
5964 let code_trimmed = block.selected_text(SelectionFormat::Plain);
5965 let code_trimmed = code_trimmed.trim_end_matches('\n');
5966 assert_eq!(
5967 block.selected_text(SelectionFormat::Source),
5968 format!("```\n{}\n```\n", code_trimmed)
5969 );
5970 }
5971
5972 #[test]
5973 fn document_selected_source_joins_blocks_with_blank_line() {
5974 use crate::text::document::ParsedDocument;
5975
5976 let document = ParsedDocument {
5980 source: String::new().into(),
5981 blocks: vec![
5982 BlockNode::Heading {
5983 level: 1,
5984 children: selected_paragraph("Title"),
5985 span: None,
5986 },
5987 BlockNode::Paragraph(selected_paragraph("A paragraph.")),
5988 selected_code_block("let x = 1;\n", Some("rust")),
5989 BlockNode::List {
5990 ordered: true,
5991 start: Some(1),
5992 span: None,
5993 children: vec![
5994 BlockNode::ListItem {
5995 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
5996 spread: false,
5997 checked: None,
5998 span: None,
5999 },
6000 BlockNode::ListItem {
6001 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
6002 spread: false,
6003 checked: None,
6004 span: None,
6005 },
6006 ],
6007 },
6008 ]
6009 .into(),
6010 };
6011
6012 assert_eq!(
6013 document.selected_text(SelectionFormat::Source, None),
6014 "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
6015 );
6016 }
6017
6018 #[test]
6019 fn code_block_equality_includes_code_content() {
6020 let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
6021 let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
6022
6023 assert_ne!(first, second);
6024 }
6025}