1use std::{
2 borrow::Cow,
3 collections::HashMap,
4 ops::Range,
5 sync::{Arc, Mutex, OnceLock},
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 background: Hsla,
1594 text: SharedString,
1595 highlights: Vec<(Range<usize>, InlineHighlight)>,
1596 links: Vec<(Range<usize>, LinkMark)>,
1598}
1599
1600impl PartialEq for Paragraph {
1601 fn eq(&self, other: &Self) -> bool {
1602 self.span == other.span
1603 && self.children == other.children
1604 && self.link_refs == other.link_refs
1605 }
1606}
1607
1608impl Paragraph {
1609 pub(crate) fn new(text: String) -> Self {
1610 Self {
1611 span: None,
1612 children: vec![InlineNode::new(&text)],
1613 link_refs: HashMap::new(),
1614 state: Arc::new(Mutex::new(InlineState::default())),
1615 render_cache: ParagraphRenderCache::default(),
1616 }
1617 }
1618
1619 fn plain_render(
1622 &self,
1623 node_cx: &NodeContext,
1624 cx: &App,
1625 ) -> (
1626 SharedString,
1627 Vec<(Range<usize>, InlineHighlight)>,
1628 Vec<(Range<usize>, LinkMark)>,
1629 ) {
1630 let mono_font = cx.theme().tokens.typography.mono.clone();
1631 let background = cx.theme().tokens.colors.background;
1632 if let Ok(cache) = self.render_cache.0.lock()
1633 && let Some(cached) = cache.as_ref()
1634 && (Arc::ptr_eq(&cached.style, &node_cx.style) || *cached.style == *node_cx.style)
1635 && cached.mono_font == mono_font
1636 && cached.background == background
1637 {
1638 return (
1639 cached.text.clone(),
1640 cached.highlights.clone(),
1641 cached.links.clone(),
1642 );
1643 }
1644
1645 let mut text = String::new();
1646 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1647 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1648 let mut offset = 0;
1649 for inline_node in &self.children {
1650 let text_len = inline_node.text.len();
1651 text.push_str(&inline_node.text);
1652 let mut node_highlights = vec![];
1653 for (range, style) in &inline_node.marks {
1654 let inner_range = (offset + range.start)..(offset + range.end);
1655 let mut highlight = mark_highlight(style, node_cx, cx);
1656 if let Some(link_mark) = style.link.clone() {
1657 highlight.style.color = Some(node_cx.style.link());
1658 highlight.style.underline = Some(link_underline(&node_cx.style));
1659 links.push((inner_range.clone(), link_mark));
1660 }
1661 node_highlights.push((inner_range, highlight));
1662 }
1663 highlights = combine_highlights(highlights, node_highlights);
1664 offset += text_len;
1665 }
1666 let text = SharedString::from(text);
1667 if let Ok(mut cache) = self.render_cache.0.lock() {
1668 *cache = Some(Arc::new(ParagraphRender {
1669 style: node_cx.style.clone(),
1670 mono_font,
1671 background,
1672 text: text.clone(),
1673 highlights: highlights.clone(),
1674 links: links.clone(),
1675 }));
1676 }
1677 (text, highlights, links)
1678 }
1679
1680 pub(super) fn selected_text(&self) -> String {
1681 let mut text = String::new();
1682
1683 for c in self.children.iter() {
1684 let Ok(state) = c.state.lock() else {
1685 continue;
1686 };
1687 if let Some(selection) = &state.selection {
1688 text.push_str(&state.text[selection.start..selection.end]);
1689 }
1690 if let Some(custom) = &c.custom
1691 && c.custom_selection.lock().is_ok_and(|selected| *selected)
1692 {
1693 text.push_str(custom.as_text());
1694 }
1695 }
1696
1697 if let Ok(state) = self.state.lock()
1698 && let Some(selection) = &state.selection
1699 {
1700 text.push_str(&state.text[selection.start..selection.end]);
1701 }
1702
1703 text
1704 }
1705
1706 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1708 let mut selected = SourceRangeSelection::Unselected;
1709 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1710 let mut offset = 0;
1711 let mut pending_images: Vec<Option<Span>> = Vec::new();
1712 let mut enters_image = true;
1713
1714 let include_run = |state: &Arc<Mutex<InlineState>>,
1715 run: &[(usize, &InlineNode)]|
1716 -> (SourceRangeSelection, RunSelection) {
1717 let Ok(state) = state.lock() else {
1718 return (SourceRangeSelection::Unmapped, RunSelection::default());
1719 };
1720 let Some(selection) = state.selection else {
1721 return (SourceRangeSelection::Unselected, RunSelection::default());
1722 };
1723 if selection.start >= selection.end {
1724 return (SourceRangeSelection::Unselected, RunSelection::default());
1725 }
1726
1727 let mut run_selection = RunSelection {
1728 at_start: selection.start == 0,
1729 at_end: selection.end >= state.text.len(),
1730 ..Default::default()
1731 };
1732 let mut mapped = SourceRangeSelection::Unselected;
1733 let mut rendered_end = selection.start;
1734 for (start, child) in run {
1735 let end = start + child.text.len();
1736 let lo = selection.start.max(*start);
1737 let hi = selection.end.min(end);
1738 if lo >= hi {
1739 continue;
1740 }
1741 run_selection.emitted = true;
1742 if lo > rendered_end {
1743 return (SourceRangeSelection::Unmapped, run_selection);
1744 }
1745 let Some(range) = child.selected_source_range((lo - start)..(hi - start)) else {
1746 return (SourceRangeSelection::Unmapped, run_selection);
1747 };
1748 mapped.merge(SourceRangeSelection::Mapped(range));
1749 rendered_end = rendered_end.max(hi);
1750 }
1751 if rendered_end < selection.end {
1752 (SourceRangeSelection::Unmapped, run_selection)
1753 } else {
1754 (mapped, run_selection)
1755 }
1756 };
1757
1758 let merge_images = |selected: &mut SourceRangeSelection, images: &mut Vec<Option<Span>>| {
1759 for span in images.drain(..) {
1760 selected.merge(
1761 span.map(|span| SourceRangeSelection::Mapped(span.start..span.end))
1762 .unwrap_or(SourceRangeSelection::Unmapped),
1763 );
1764 }
1765 };
1766
1767 for child in &self.children {
1768 if child.custom.is_some() {
1769 let (run_range, run_selection) = include_run(&child.state, &run);
1770 if run_selection.emitted && run_selection.at_start {
1771 merge_images(&mut selected, &mut pending_images);
1772 }
1773 selected.merge(run_range);
1774
1775 match child.custom_selection.lock() {
1776 Ok(value) if *value => {
1777 if run.is_empty() || (run_selection.emitted && run_selection.at_end) {
1778 merge_images(&mut selected, &mut pending_images);
1779 }
1780 selected.merge(
1781 child
1782 .custom
1783 .as_ref()
1784 .and_then(MarkdownNode::source_range)
1785 .map(SourceRangeSelection::Mapped)
1786 .unwrap_or(SourceRangeSelection::Unmapped),
1787 );
1788 enters_image = true;
1789 }
1790 Ok(_) => enters_image = false,
1791 Err(_) => {
1792 selected.merge(SourceRangeSelection::Unmapped);
1793 enters_image = false;
1794 }
1795 }
1796 pending_images.clear();
1797 run.clear();
1798 offset = 0;
1799 continue;
1800 }
1801 if let Some(image) = &child.image {
1802 let run_before = !run.is_empty();
1803 let (run_range, run_selection) = include_run(&child.state, &run);
1804 if run_selection.emitted && run_selection.at_start {
1805 merge_images(&mut selected, &mut pending_images);
1806 }
1807 selected.merge(run_range);
1808 if run_before {
1809 enters_image = run_selection.emitted && run_selection.at_end;
1810 }
1811 if enters_image {
1812 pending_images.push(image.span);
1813 } else {
1814 pending_images.clear();
1815 }
1816 run.clear();
1817 offset = 0;
1818 continue;
1819 }
1820 run.push((offset, child));
1821 offset += child.text.len();
1822 }
1823
1824 let (trailing_range, trailing) = include_run(&self.state, &run);
1825 if trailing.emitted && trailing.at_start {
1826 merge_images(&mut selected, &mut pending_images);
1827 }
1828 selected.merge(trailing_range);
1829 if !trailing.emitted
1830 && enters_image
1831 && !matches!(selected, SourceRangeSelection::Unselected)
1832 {
1833 merge_images(&mut selected, &mut pending_images);
1834 }
1835 selected
1836 }
1837
1838 pub(super) fn selected_source(&self) -> String {
1858 self.selected_markdown().finish()
1859 }
1860
1861 pub(super) fn selection_styles(&self) -> SelectionStyles {
1863 self.selected_markdown().styles()
1864 }
1865
1866 fn selected_markdown(&self) -> MarkdownSource {
1869 let mut source = MarkdownSource::default();
1870 let mut pending_images: Vec<String> = Vec::new();
1871 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1872 let mut offset = 0;
1873 let mut enters_image = true;
1874
1875 for child in self.children.iter() {
1876 if child.custom.is_some() {
1877 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1878 let object_selected = child
1879 .custom_selection
1880 .lock()
1881 .is_ok_and(|selected| *selected);
1882 if object_selected {
1883 if run.is_empty() || (selected.emitted && selected.at_end) {
1884 source.push_images(&pending_images);
1885 }
1886 source.push_object(child);
1887 }
1888 pending_images.clear();
1889 enters_image = object_selected;
1890 run.clear();
1891 offset = 0;
1892 continue;
1893 }
1894 let Some(image) = &child.image else {
1895 run.push((offset, child));
1896 offset += child.text.len();
1897 continue;
1898 };
1899
1900 let run_before = !run.is_empty();
1902 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1903 if run_before {
1904 enters_image = selected.emitted && selected.at_end;
1905 }
1906 if enters_image {
1907 pending_images.push(image_markdown(image));
1908 } else {
1909 pending_images.clear();
1910 }
1911
1912 run.clear();
1913 offset = 0;
1914 }
1915
1916 let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
1917 if !trailing.emitted && enters_image && !source.is_empty() {
1919 source.push_images(&pending_images);
1920 }
1921
1922 source
1923 }
1924
1925 pub(super) fn text(&self) -> String {
1926 let mut text = String::new();
1927 for node in self.children.iter() {
1928 text.push_str(&node.text);
1929 }
1930 text
1931 }
1932
1933 pub(super) fn has_selection(&self) -> bool {
1937 self.children.iter().any(|c| {
1938 c.state.lock().is_ok_and(|state| state.selection.is_some())
1939 || c.custom_selection.lock().is_ok_and(|selected| *selected)
1940 }) || self
1941 .state
1942 .lock()
1943 .is_ok_and(|state| state.selection.is_some())
1944 }
1945
1946 pub(super) fn clear_selection(&self) {
1947 for c in self.children.iter() {
1948 if let Ok(mut selected) = c.custom_selection.lock() {
1949 *selected = false;
1950 }
1951 if let Ok(mut state) = c.state.lock() {
1952 state.selection = None;
1953 }
1954 }
1955
1956 if let Ok(mut state) = self.state.lock() {
1957 state.selection = None;
1958 }
1959 }
1960}
1961
1962#[derive(Debug, Clone, Default, PartialEq)]
1964pub struct Table {
1965 pub(crate) children: Vec<TableRow>,
1966 pub(crate) column_aligns: Vec<ColumnumnAlign>,
1967 pub(crate) span: Option<Span>,
1968 pub(crate) table_data_cache: TableDataCache,
1971 pub(crate) column_widths_cache: TableColumnWidthsCache,
1972}
1973
1974#[derive(Default)]
1982pub(crate) struct TableDataCache(Mutex<Option<Arc<TableData>>>);
1983
1984impl Clone for TableDataCache {
1985 fn clone(&self) -> Self {
1986 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1987 Self(Mutex::new(cached))
1988 }
1989}
1990
1991impl PartialEq for TableDataCache {
1992 fn eq(&self, _: &Self) -> bool {
1993 true
1994 }
1995}
1996
1997impl std::fmt::Debug for TableDataCache {
1998 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1999 f.write_str("TableDataCache")
2000 }
2001}
2002
2003#[derive(Default)]
2006pub(crate) struct TableColumnWidthsCache(Mutex<Option<Arc<TableColumnWidths>>>);
2007
2008impl Clone for TableColumnWidthsCache {
2009 fn clone(&self) -> Self {
2010 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
2011 Self(Mutex::new(cached))
2012 }
2013}
2014
2015impl PartialEq for TableColumnWidthsCache {
2016 fn eq(&self, _: &Self) -> bool {
2017 true
2018 }
2019}
2020
2021impl std::fmt::Debug for TableColumnWidthsCache {
2022 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2023 f.write_str("TableColumnWidthsCache")
2024 }
2025}
2026
2027fn text_system_id(window: &Window) -> usize {
2028 std::rc::Rc::as_ptr(window.text_system()) as usize
2029}
2030
2031struct TableColumnWidths {
2032 text_system: usize,
2036 text_styles: TableTextStyles,
2038 rem_size: Pixels,
2039 mono_family: SharedString,
2040 inline_code: HighlightStyle,
2041 widths: Vec<f32>,
2043 custom_cells: Vec<(usize, usize)>,
2044}
2045
2046#[derive(Clone, PartialEq)]
2050struct TableTextStyles {
2051 head: gpui::TextStyle,
2052 body: gpui::TextStyle,
2053 head_refinement: TextStyleRefinement,
2054 body_refinement: TextStyleRefinement,
2055}
2056
2057impl TableTextStyles {
2058 fn new(style: &TextViewStyle, window: &Window) -> Self {
2059 let mut body_refinement = style.table().text.clone();
2060 let mut head_refinement = body_refinement.clone();
2061 head_refinement.refine(&style.table_head().text);
2062 head_refinement.refine(&style.table_cell().text);
2063 body_refinement.refine(&style.table_cell().text);
2064 let mut head = window.text_style();
2065 let mut body = head.clone();
2066 head.refine(&head_refinement);
2067 body.refine(&body_refinement);
2068 Self {
2069 head,
2070 body,
2071 head_refinement,
2072 body_refinement,
2073 }
2074 }
2075
2076 fn row(&self, row_ix: usize) -> &gpui::TextStyle {
2077 if row_ix == 0 { &self.head } else { &self.body }
2078 }
2079
2080 fn row_refinement(&self, row_ix: usize) -> &TextStyleRefinement {
2081 if row_ix == 0 {
2082 &self.head_refinement
2083 } else {
2084 &self.body_refinement
2085 }
2086 }
2087}
2088
2089#[derive(Debug, Clone, Default, PartialEq)]
2092pub struct TableData {
2093 pub headers: Vec<String>,
2095 pub rows: Vec<Vec<String>>,
2098 pub markdown: String,
2100 pub span: Option<Range<usize>>,
2107}
2108
2109impl Table {
2110 pub fn new(rows: Vec<Vec<Paragraph>>, column_aligns: Vec<ColumnumnAlign>) -> Self {
2112 Self {
2113 children: rows
2114 .into_iter()
2115 .map(|row| TableRow {
2116 children: row
2117 .into_iter()
2118 .map(|children| TableCell {
2119 children,
2120 width: None,
2121 })
2122 .collect(),
2123 })
2124 .collect(),
2125 column_aligns,
2126 ..Default::default()
2127 }
2128 }
2129
2130 pub fn rows(&self) -> impl Iterator<Item = impl Iterator<Item = &Paragraph>> {
2132 self.children
2133 .iter()
2134 .map(|row| row.children.iter().map(|cell| &cell.children))
2135 }
2136
2137 pub fn column_aligns(&self) -> &[ColumnumnAlign] {
2139 &self.column_aligns
2140 }
2141
2142 pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
2143 self.column_aligns.get(index).copied().unwrap_or_default()
2144 }
2145
2146 pub(crate) fn to_markdown(&self) -> String {
2153 let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
2154
2155 for (row_ix, row) in self.children.iter().enumerate() {
2156 let cells: Vec<String> = row
2157 .children
2158 .iter()
2159 .map(|cell| {
2160 cell.children
2161 .to_markdown()
2162 .trim()
2163 .replace('\n', " ")
2164 .replace('|', "\\|")
2165 })
2166 .collect();
2167 lines.push(format!("| {} |", cells.join(" | ")));
2168
2169 if row_ix == 0 {
2172 let aligns: Vec<String> = (0..row.children.len())
2173 .map(|ix| {
2174 match self.column_align(ix) {
2175 ColumnumnAlign::None | ColumnumnAlign::Left => ":--",
2176 ColumnumnAlign::Center => ":-:",
2177 ColumnumnAlign::Right => "--:",
2178 }
2179 .to_string()
2180 })
2181 .collect();
2182 lines.push(format!("| {} |", aligns.join(" | ")));
2183 }
2184 }
2185
2186 lines.join("\n")
2187 }
2188
2189 pub(crate) fn table_data(&self) -> TableData {
2192 let row_text = |row: &TableRow| {
2193 row.children
2194 .iter()
2195 .map(|cell| cell.children.text().trim().to_string())
2196 .collect::<Vec<_>>()
2197 };
2198
2199 TableData {
2200 headers: self.children.first().map(row_text).unwrap_or_default(),
2201 rows: self.children.iter().skip(1).map(row_text).collect(),
2202 markdown: self.to_markdown(),
2203 span: self.span.map(|span| span.start..span.end),
2204 }
2205 }
2206
2207 fn cached_table_data(&self) -> Arc<TableData> {
2212 if let Ok(cache) = self.table_data_cache.0.lock()
2213 && let Some(cached) = cache.as_ref()
2214 {
2215 return cached.clone();
2216 }
2217
2218 let data = Arc::new(self.table_data());
2219 if let Ok(mut cache) = self.table_data_cache.0.lock() {
2220 *cache = Some(data.clone());
2221 }
2222 data
2223 }
2224}
2225
2226#[derive(Debug, Default, Copy, Clone, PartialEq)]
2228pub enum ColumnumnAlign {
2229 #[default]
2231 None,
2232 Left,
2233 Center,
2234 Right,
2235}
2236
2237impl From<mdast::AlignKind> for ColumnumnAlign {
2238 fn from(value: mdast::AlignKind) -> Self {
2239 match value {
2240 mdast::AlignKind::None => ColumnumnAlign::None,
2241 mdast::AlignKind::Left => ColumnumnAlign::Left,
2242 mdast::AlignKind::Center => ColumnumnAlign::Center,
2243 mdast::AlignKind::Right => ColumnumnAlign::Right,
2244 }
2245 }
2246}
2247
2248#[derive(Debug, Clone, Default, PartialEq)]
2249pub(crate) struct TableRow {
2250 pub children: Vec<TableCell>,
2251}
2252
2253#[derive(Debug, Clone, Default, PartialEq)]
2254pub(crate) struct TableCell {
2255 pub children: Paragraph,
2256 pub width: Option<DefiniteLength>,
2257}
2258
2259impl Paragraph {
2260 pub(crate) fn take(&mut self) -> Paragraph {
2261 std::mem::replace(
2262 self,
2263 Paragraph {
2264 span: None,
2265 children: vec![],
2266 link_refs: Default::default(),
2267 state: Arc::new(Mutex::new(InlineState::default())),
2268 render_cache: ParagraphRenderCache::default(),
2269 },
2270 )
2271 }
2272
2273 pub(crate) fn is_image(&self) -> bool {
2274 false
2275 }
2276
2277 pub(crate) fn set_span(&mut self, span: Span) {
2278 self.span = Some(span);
2279 }
2280
2281 pub(crate) fn push_str(&mut self, text: &str) {
2282 self.children.push(
2283 InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
2284 );
2285 self.invalidate_render_cache();
2286 }
2287
2288 pub fn push(&mut self, text: InlineNode) {
2290 self.children.push(text);
2291 self.invalidate_render_cache();
2292 }
2293
2294 pub fn children(&self) -> &[InlineNode] {
2296 &self.children
2297 }
2298
2299 pub fn with_leaf_key(mut self, key: u64) -> Self {
2304 set_leaf_key(&mut self.state, key);
2305 self
2306 }
2307
2308 pub(crate) fn push_image(&mut self, image: ImageNode) {
2309 self.children.push(InlineNode::image(image));
2310 self.invalidate_render_cache();
2311 }
2312
2313 fn invalidate_render_cache(&mut self) {
2315 self.render_cache = ParagraphRenderCache::default();
2316 }
2317
2318 pub(crate) fn is_empty(&self) -> bool {
2319 self.children.is_empty()
2320 || self
2321 .children
2322 .iter()
2323 .all(|node| node.text.is_empty() && node.image.is_none())
2324 }
2325
2326 pub(crate) fn text_len(&self) -> usize {
2328 self.children
2329 .iter()
2330 .map(|node| node.text.len())
2331 .sum::<usize>()
2332 }
2333
2334 pub(crate) fn merge(&mut self, other: Self) {
2335 self.children.extend(other.children);
2336 self.invalidate_render_cache();
2337 }
2338}
2339
2340#[derive(Debug, Clone)]
2341pub struct CodeBlock {
2342 lang: Option<SharedString>,
2343 meta: Option<SharedString>,
2344 state: Arc<Mutex<InlineState>>,
2345 highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
2346 pub(crate) source_segments: Vec<SourceSegment>,
2349 pub span: Option<Span>,
2350}
2351
2352struct CachedCodeBlockHighlights {
2353 highlighter: Arc<CodeBlockHighlighterFn>,
2354 styles: Vec<(Range<usize>, HighlightStyle)>,
2355}
2356
2357impl std::fmt::Debug for CachedCodeBlockHighlights {
2358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2359 f.debug_struct("CachedCodeBlockHighlights")
2360 .field("styles", &self.styles)
2361 .finish_non_exhaustive()
2362 }
2363}
2364
2365impl PartialEq for CodeBlock {
2366 fn eq(&self, other: &Self) -> bool {
2367 self.lang == other.lang
2368 && self.meta == other.meta
2369 && self.code() == other.code()
2370 && self.span == other.span
2371 }
2372}
2373
2374impl CodeBlock {
2375 pub fn lang(&self) -> Option<SharedString> {
2377 self.lang.clone()
2378 }
2379
2380 pub fn meta(&self) -> Option<SharedString> {
2382 self.meta.clone()
2383 }
2384
2385 pub fn with_meta(mut self, meta: Option<SharedString>) -> Self {
2388 self.meta = meta;
2389 self
2390 }
2391
2392 pub fn with_leaf_key(mut self, key: u64) -> Self {
2395 set_leaf_key(&mut self.state, key);
2396 self
2397 }
2398
2399 pub fn code(&self) -> SharedString {
2401 self.state
2402 .lock()
2403 .map(|state| state.text.clone())
2404 .unwrap_or_default()
2405 }
2406
2407 pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
2413 Self::new(code.into(), lang.map(Into::into), None::<Span>)
2414 }
2415
2416 pub fn new(
2418 code: SharedString,
2419 lang: Option<SharedString>,
2420 span: Option<impl Into<Span>>,
2421 ) -> Self {
2422 let state = Arc::new(Mutex::new(InlineState::default()));
2423 if let Ok(mut state) = state.lock() {
2424 state.set_text(code);
2425 }
2426
2427 Self {
2428 lang,
2429 meta: None,
2430 state,
2431 highlight_cache: Arc::new(Mutex::new(None)),
2432 source_segments: vec![],
2433 span: span.map(|s| s.into()),
2434 }
2435 }
2436
2437 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
2438 self.source_segments = source_segments;
2439 self
2440 }
2441
2442 #[cfg(test)]
2443 pub(crate) fn set_selection(&self, selection: Range<usize>) {
2444 if let Ok(mut state) = self.state.lock() {
2445 state.selection = Some(selection.into());
2446 }
2447 }
2448
2449 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
2450 let Ok(state) = self.state.lock() else {
2451 return SourceRangeSelection::Unmapped;
2452 };
2453 let Some(selection) = state.selection else {
2454 return SourceRangeSelection::Unselected;
2455 };
2456 if selection.start >= selection.end {
2457 return SourceRangeSelection::Unselected;
2458 }
2459 source_range_for_segments(&self.source_segments, selection.start..selection.end)
2460 .map(SourceRangeSelection::Mapped)
2461 .unwrap_or(SourceRangeSelection::Unmapped)
2462 }
2463
2464 fn highlighted_styles(
2465 &self,
2466 highlighter: &Arc<CodeBlockHighlighterFn>,
2467 ) -> Vec<(Range<usize>, HighlightStyle)> {
2468 if let Ok(cache) = self.highlight_cache.lock()
2469 && let Some(cache) = cache.as_ref()
2470 && Arc::ptr_eq(&cache.highlighter, highlighter)
2471 {
2472 return cache.styles.clone();
2473 }
2474
2475 let code_len = self.code().len();
2476 let styles = highlighter(self)
2477 .into_iter()
2478 .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
2479 .collect::<Vec<_>>();
2480 if let Ok(mut cache) = self.highlight_cache.lock() {
2481 *cache = Some(CachedCodeBlockHighlights {
2482 highlighter: highlighter.clone(),
2483 styles: styles.clone(),
2484 });
2485 }
2486 styles
2487 }
2488
2489 pub(super) fn selected_text(&self) -> String {
2490 let mut text = String::new();
2491 if let Ok(state) = self.state.lock()
2492 && let Some(selection) = &state.selection
2493 {
2494 text.push_str(&state.text[selection.start..selection.end]);
2495 }
2496 text
2497 }
2498
2499 pub(super) fn selected_source(&self) -> String {
2506 let code = self.selected_text();
2507 if code.is_empty() {
2508 return String::new();
2509 }
2510 let lang = self.lang.clone().unwrap_or_default();
2511 let code = code.trim_end_matches('\n');
2514 format!("```{}\n{}\n```", lang, code)
2515 }
2516
2517 pub(super) fn text(&self) -> String {
2518 self.state
2519 .lock()
2520 .map(|state| state.text.to_string())
2521 .unwrap_or_default()
2522 }
2523
2524 pub(super) fn has_selection(&self) -> bool {
2528 self.state
2529 .lock()
2530 .is_ok_and(|state| state.selection.is_some())
2531 }
2532
2533 pub(super) fn clear_selection(&self) {
2534 if let Ok(mut state) = self.state.lock() {
2535 state.selection = None;
2536 }
2537 }
2538
2539 fn render(
2540 &self,
2541 options: &NodeRenderOptions,
2542 node_cx: &NodeContext,
2543 window: &mut Window,
2544 cx: &mut App,
2545 ) -> AnyElement {
2546 let style = &node_cx.style;
2547 let leaf_key = self.span.map(|span| TextLeafKey::block(span.start));
2548 let lang_label = self
2553 .lang()
2554 .filter(|lang| !lang.is_empty() && node_cx.code_block_actions.is_none())
2555 .map(|lang| {
2556 div()
2557 .absolute()
2558 .top(px(7.))
2559 .right_3()
2560 .font_family(cx.theme().tokens.typography.sans.clone())
2561 .text_size(cx.theme().tokens.typography.xs.size)
2562 .line_height(px(16.))
2563 .text_color(style.muted_foreground())
2564 .child(lang)
2565 });
2566
2567 let block = div()
2568 .w_full()
2569 .min_w_0()
2570 .px_4()
2571 .pt(if lang_label.is_some() {
2572 px(28.)
2573 } else {
2574 px(14.)
2575 })
2576 .pb(px(14.))
2577 .bg(style.code_background())
2578 .border_1()
2579 .border_color(style.border())
2580 .font_family(cx.theme().tokens.typography.mono.clone())
2581 .text_size(cx.theme().tokens.typography.mono_md.size)
2582 .line_height(relative(1.6))
2583 .relative()
2584 .refine_style(&style.code_block())
2585 .child(
2586 Inline::new(
2587 self.state.clone(),
2588 vec![],
2589 fade_highlights(
2590 node_cx
2591 .code_block_highlighter
2592 .as_ref()
2593 .map(|highlighter| self.highlighted_styles(highlighter))
2594 .unwrap_or_default()
2595 .into_iter()
2596 .map(|(range, style)| (range, InlineHighlight::from(style)))
2597 .collect(),
2598 node_cx.stream_fades(leaf_key),
2599 ),
2600 node_cx.link_click_handler.clone(),
2601 )
2602 .range_backgrounds(node_cx.range_backgrounds(leaf_key).to_vec())
2603 .reveal(node_cx.reveal_at(leaf_key, 0, self.code().len())),
2604 );
2605 let actions = node_cx.code_block_actions.clone().map(|actions| {
2606 div()
2607 .id("actions")
2608 .absolute()
2609 .top_2()
2610 .right_2()
2611 .bg(style.code_background())
2612 .rounded(cx.theme().tokens.radius.md)
2613 .child(actions(&self, window, cx))
2614 });
2615 let id = block_element_id("codeblock", self.span, options.ix);
2618 let block = if matches!(style.code_block().overflow.y, Some(Overflow::Scroll)) {
2619 let scroll_handle = window
2620 .use_keyed_state(
2621 block_element_id("codeblock-scroll", self.span, options.ix),
2622 cx,
2623 |_, _| ScrollHandle::default(),
2624 )
2625 .read(cx)
2626 .clone();
2627 div()
2634 .id(id.clone())
2635 .w_full()
2636 .min_w_0()
2637 .relative()
2638 .child(block.id("scroll").track_scroll(&scroll_handle))
2639 .child(ScrollableMask::new(Axis::Vertical, &scroll_handle).id(id))
2640 .child(
2641 div()
2642 .absolute()
2643 .inset_0()
2644 .child(Scrollbar::vertical(&scroll_handle)),
2645 )
2646 .children(actions)
2647 .children(lang_label)
2648 .into_any_element()
2649 } else {
2650 match actions {
2652 Some(actions) => block.id(id).child(actions).into_any_element(),
2653 None => block.children(lang_label).into_any_element(),
2654 }
2655 };
2656
2657 gapped(
2658 block,
2659 if options.is_last {
2660 rems(0.)
2661 } else {
2662 style.paragraph_gap()
2663 },
2664 )
2665 }
2666}
2667
2668#[derive(Default, Clone)]
2670pub(crate) struct NodeContext {
2671 pub(crate) offset: usize,
2674 pub(crate) link_refs: HashMap<SharedString, LinkMark>,
2675 pub(crate) style: Arc<TextViewStyle>,
2676 pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
2677 pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
2678 pub(crate) table_actions: Option<Arc<TableActionsFn>>,
2679 pub(crate) image_source: Option<Arc<super::text_view::ImageSourceFn>>,
2680 pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
2681 pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
2682 pub(crate) stream_fade: Option<Arc<StreamFadeFrame>>,
2684 pub(crate) range_highlights: Option<Arc<RangeHighlightFrame>>,
2686 pub(crate) reveal: Option<RevealRequest>,
2688 pub(crate) body_font_size: Pixels,
2691}
2692
2693impl NodeContext {
2694 fn image_source(&self, image: &ImageNode) -> ImageSource {
2695 match &self.image_source {
2696 Some(resolve) => resolve(&image.url),
2697 None => image.source(),
2698 }
2699 }
2700
2701 pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
2702 self.link_refs.insert(identifier, link);
2703 }
2704
2705 fn stream_fades(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, f32)] {
2707 match (&self.stream_fade, key) {
2708 (Some(frame), Some(key)) => frame.fades(key).unwrap_or_default(),
2709 _ => &[],
2710 }
2711 }
2712
2713 fn range_backgrounds(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, Hsla)] {
2716 match (&self.range_highlights, key) {
2717 (Some(frame), Some(key)) => frame.backgrounds(key),
2718 _ => &[],
2719 }
2720 }
2721
2722 fn reveal_at(&self, key: Option<TextLeafKey>, start: usize, end: usize) -> Option<RevealAt> {
2725 self.reveal.as_ref()?.at(key, start, end)
2726 }
2727}
2728
2729impl PartialEq for NodeContext {
2730 fn eq(&self, other: &Self) -> bool {
2731 self.link_refs == other.link_refs && self.style == other.style
2732 }
2735}
2736
2737fn mark_text_color(highlight: Hsla, background: Hsla) -> Hsla {
2740 fn channel(value: f32) -> f32 {
2741 if value <= 0.04045 {
2742 value / 12.92
2743 } else {
2744 ((value + 0.055) / 1.055).powf(2.4)
2745 }
2746 }
2747 let rgb = background.to_rgb().blend(highlight.to_rgb());
2748 let luminance = 0.2126 * channel(rgb.r) + 0.7152 * channel(rgb.g) + 0.0722 * channel(rgb.b);
2749 if (luminance + 0.05) * (luminance + 0.05) > 0.0525 {
2752 gpui::black()
2753 } else {
2754 gpui::white()
2755 }
2756}
2757
2758fn mark_highlight(mark: &TextMark, node_cx: &NodeContext, cx: &App) -> InlineHighlight {
2761 let mut highlight = HighlightStyle::default();
2762 if mark.bold {
2763 highlight.font_weight = Some(FontWeight::BOLD);
2764 }
2765 if mark.italic {
2766 highlight.font_style = Some(FontStyle::Italic);
2767 }
2768 if mark.strikethrough {
2769 highlight.strikethrough = Some(gpui::StrikethroughStyle {
2770 thickness: gpui::px(1.),
2771 ..Default::default()
2772 });
2773 }
2774 if mark.underline {
2775 highlight.underline = Some(gpui::UnderlineStyle {
2776 thickness: gpui::px(1.),
2777 ..Default::default()
2778 });
2779 }
2780 let mut font_family = None;
2781 if mark.code {
2782 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
2783 let style = node_cx.style.inline_code_style();
2784 highlight = highlight.highlight(HighlightStyle {
2785 color: style.text.color,
2786 font_weight: style.text.font_weight,
2787 font_style: style.text.font_style,
2788 underline: style.text.underline,
2789 strikethrough: style.text.strikethrough,
2790 ..Default::default()
2791 });
2792 font_family = Some(
2793 style
2794 .text
2795 .font_family
2796 .clone()
2797 .unwrap_or_else(|| cx.theme().tokens.typography.mono.clone()),
2798 );
2799 }
2800 if let Some(color) = mark.highlight {
2801 highlight.background_color = Some(color);
2802 if color.a > 0.0 {
2808 highlight.color = Some(mark_text_color(color, cx.theme().tokens.colors.background));
2809 }
2810 }
2811 InlineHighlight {
2812 style: highlight,
2813 font_family,
2814 font_size_scale: mark.code.then_some(0.875),
2815 code_style: mark.code.then(|| node_cx.style.inline_code_style().clone()),
2816 }
2817}
2818
2819impl Paragraph {
2820 fn inline_highlights(
2824 &self,
2825 node_cx: &NodeContext,
2826 cx: &App,
2827 ) -> Vec<(Range<usize>, InlineHighlight)> {
2828 let mut highlights = vec![];
2829 let mut offset = 0;
2830 for inline_node in &self.children {
2831 let node_highlights = inline_node
2832 .marks
2833 .iter()
2834 .map(|(range, mark)| {
2835 (
2836 (offset + range.start)..(offset + range.end),
2837 mark_highlight(mark, node_cx, cx),
2838 )
2839 })
2840 .collect::<Vec<_>>();
2841 highlights = combine_highlights(highlights, node_highlights);
2842 offset += inline_node.text.len();
2843 }
2844 highlights
2845 }
2846
2847 fn render(
2851 &self,
2852 fade_key: Option<TextLeafKey>,
2853 node_cx: &NodeContext,
2854 _window: &mut Window,
2855 cx: &mut App,
2856 ) -> AnyElement {
2857 let children = &self.children;
2858 let fades = node_cx.stream_fades(fade_key);
2859 let backgrounds = node_cx.range_backgrounds(fade_key);
2860
2861 if self.should_render_inline_flow() {
2862 return InlineFlow::new(
2863 leaf_element_id(fade_key),
2864 self.inline_flow_items(fade_key, fades, backgrounds, node_cx, cx),
2865 node_cx.link_click_handler.clone(),
2866 )
2867 .editor_key(self.leaf_key())
2868 .into_any_element();
2869 }
2870
2871 let has_image = children.iter().any(|child| child.image.is_some());
2872 if !has_image {
2875 let (text, highlights, mut links) = self.plain_render(node_cx, cx);
2876 if text.is_empty() && self.leaf_key().is_none() {
2879 return div().into_any_element();
2880 }
2881 for (_, link_mark) in &mut links {
2882 if let Some(identifier) = link_mark.identifier.as_ref()
2883 && let Some(mark) = node_cx.link_refs.get(identifier)
2884 {
2885 *link_mark = mark.clone();
2886 }
2887 }
2888 let highlights = fade_highlights(highlights, &slice_fades(fades, 0, text.len()));
2889 let backgrounds = slice_backgrounds(backgrounds, 0, text.len());
2890 let reveal = node_cx.reveal_at(fade_key, 0, text.len());
2891 if let Ok(mut state) = self.state.lock() {
2892 state.set_text(text);
2893 }
2894 return Inline::new(
2895 self.state.clone(),
2896 links,
2897 highlights,
2898 node_cx.link_click_handler.clone(),
2899 )
2900 .range_backgrounds(backgrounds)
2901 .reveal(reveal)
2902 .into_any_element();
2903 }
2904
2905 let mut child_nodes: Vec<AnyElement> = vec![];
2906
2907 let mut text = String::new();
2908 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2909 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2910 let mut offset = 0;
2911 let mut consumed = 0;
2914
2915 for (ix, inline_node) in children.iter().enumerate() {
2916 let text_len = inline_node.text.len();
2917 text.push_str(&inline_node.text);
2918
2919 if let Some(image) = &inline_node.image {
2920 if text.len() > 0 {
2921 if let Ok(mut state) = inline_node.state.lock() {
2922 state.set_text(text.clone().into());
2923 }
2924 child_nodes.push(
2925 Inline::new(
2926 inline_node.state.clone(),
2927 links.clone(),
2928 fade_highlights(
2929 highlights.clone(),
2930 &slice_fades(fades, consumed, consumed + text.len()),
2931 ),
2932 node_cx.link_click_handler.clone(),
2933 )
2934 .range_backgrounds(slice_backgrounds(
2935 backgrounds,
2936 consumed,
2937 consumed + text.len(),
2938 ))
2939 .reveal(node_cx.reveal_at(fade_key, consumed, consumed + text.len()))
2940 .into_any_element(),
2941 );
2942 }
2943 let link_click_handler = node_cx.link_click_handler.clone();
2944 child_nodes.push(
2945 img(node_cx.image_source(image))
2946 .id(ix)
2947 .object_fit(ObjectFit::Contain)
2948 .max_w(relative(1.))
2949 .when_some(image.width, |this, width| this.w(width))
2950 .when_some(image.link.clone(), |this, link| {
2951 let link_click_handler = link_click_handler.clone();
2952 let aux_link = link.clone();
2953 let aux_link_click_handler = link_click_handler.clone();
2954 this.cursor_pointer()
2955 .on_click(move |event, window, cx| {
2956 crate::TextSelection::end(window, cx);
2957 cx.stop_propagation();
2958 handle_link_click(
2959 &link_click_handler,
2960 link.url.clone(),
2961 event.clone(),
2962 window,
2963 cx,
2964 );
2965 })
2966 .on_aux_click(move |event, window, cx| {
2967 crate::TextSelection::end(window, cx);
2968 cx.stop_propagation();
2969 handle_link_click(
2970 &aux_link_click_handler,
2971 aux_link.url.clone(),
2972 event.clone(),
2973 window,
2974 cx,
2975 );
2976 })
2977 })
2978 .into_any_element(),
2979 );
2980
2981 consumed += text.len();
2982 text.clear();
2983 links.clear();
2984 highlights.clear();
2985 offset = 0;
2986 } else {
2987 let mut node_highlights = vec![];
2988 for (range, style) in &inline_node.marks {
2989 let inner_range = (offset + range.start)..(offset + range.end);
2990 let mut highlight = mark_highlight(style, node_cx, cx);
2991
2992 if let Some(mut link_mark) = style.link.clone() {
2993 highlight.style.color = Some(node_cx.style.link());
2994 highlight.style.underline = Some(link_underline(&node_cx.style));
2995
2996 if let Some(identifier) = link_mark.identifier.as_ref() {
2998 if let Some(mark) = node_cx.link_refs.get(identifier) {
2999 link_mark = mark.clone();
3000 }
3001 }
3002
3003 links.push((inner_range.clone(), link_mark));
3004 }
3005
3006 node_highlights.push((inner_range, highlight));
3007 }
3008
3009 highlights = combine_highlights(highlights, node_highlights);
3010 offset += text_len;
3011 }
3012 }
3013
3014 if text.len() > 0 {
3016 let text_end = consumed + text.len();
3017 let highlights = fade_highlights(highlights, &slice_fades(fades, consumed, text_end));
3018 if let Ok(mut state) = self.state.lock() {
3019 state.set_text(text.into());
3020 }
3021 child_nodes.push(
3022 Inline::new(
3023 self.state.clone(),
3024 links,
3025 highlights,
3026 node_cx.link_click_handler.clone(),
3027 )
3028 .range_backgrounds(slice_backgrounds(backgrounds, consumed, text_end))
3029 .reveal(node_cx.reveal_at(fade_key, consumed, text_end))
3030 .into_any_element(),
3031 );
3032 }
3033
3034 if !has_image {
3038 return child_nodes
3039 .pop()
3040 .unwrap_or_else(|| div().into_any_element());
3041 }
3042
3043 div()
3044 .id(leaf_element_id(fade_key))
3045 .children(child_nodes)
3046 .into_any_element()
3047 }
3048
3049 fn leaf_key(&self) -> Option<u64> {
3051 self.state.lock().ok().and_then(|state| state.editor_key)
3052 }
3053
3054 fn should_render_inline_flow(&self) -> bool {
3055 let has_image = self.children.iter().any(|child| child.image.is_some());
3056 let has_text = self.children.iter().any(|child| !child.text.is_empty());
3057 if has_image && self.leaf_key().is_some() {
3061 return true;
3062 }
3063 self.children.iter().any(|child| child.custom.is_some())
3064 || (has_image && has_text)
3065 || self
3066 .children
3067 .iter()
3068 .any(|child| child.marks.iter().any(|(_, mark)| mark.code))
3069 }
3070
3071 fn inline_flow_items(
3072 &self,
3073 leaf_key: Option<TextLeafKey>,
3074 fades: &[(Range<usize>, f32)],
3075 backgrounds: &[(Range<usize>, Hsla)],
3076 node_cx: &NodeContext,
3077 cx: &mut App,
3078 ) -> Vec<InlineFlowItem> {
3079 let mut items = Vec::new();
3080 let mut text = String::new();
3081 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
3082 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
3083 let mut offset = 0;
3084 let mut consumed = 0;
3087
3088 for inline_node in &self.children {
3089 if let Some(node) = &inline_node.custom {
3090 if let Ok(mut state) = inline_node.state.lock() {
3091 state.set_text(text.clone().into());
3092 }
3093 if !text.is_empty() {
3094 let item_fades = slice_fades(fades, consumed, consumed + text.len());
3095 let item_backgrounds =
3096 slice_backgrounds(backgrounds, consumed, consumed + text.len());
3097 let item_reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
3098 consumed += text.len();
3099 items.push(InlineFlowItem::Text {
3100 state: inline_node.state.clone(),
3101 text: std::mem::take(&mut text).into(),
3102 links: std::mem::take(&mut links),
3103 highlights: fade_highlights(std::mem::take(&mut highlights), &item_fades),
3104 backgrounds: item_backgrounds,
3105 reveal: item_reveal,
3106 });
3107 }
3108 let mut object_style = HighlightStyle::default();
3109 let mut object_link = None;
3110 for (_, mark) in &inline_node.marks {
3111 object_style = object_style.highlight(mark_highlight(mark, node_cx, cx).style);
3112 if let Some(link) = &mark.link {
3113 object_link = Some(
3114 link.identifier
3115 .as_ref()
3116 .and_then(|id| node_cx.link_refs.get(id))
3117 .unwrap_or(link)
3118 .clone(),
3119 );
3120 object_style.color = Some(node_cx.style.link());
3121 object_style.underline = Some(link_underline(&node_cx.style));
3122 }
3123 }
3124 let rendered_node = node.clone();
3125 let extensions = node_cx.markdown_extensions.clone();
3126 items.push(InlineFlowItem::Object {
3127 text: node.shared_text(),
3128 accessibility_label: node.shared_accessibility_name(),
3129 id: node.source_range().map_or(items.len(), |range| range.start),
3130 renderer: Arc::new(move |context, window, cx| {
3131 extensions.render_inline(&rendered_node, context, window, cx)
3132 }),
3133 selected: inline_node.custom_selection.clone(),
3134 style: object_style,
3135 link: object_link,
3136 });
3137 consumed += inline_node.text.len();
3138 offset = 0;
3139 continue;
3140 }
3141 let text_len = inline_node.text.len();
3142 text.push_str(&inline_node.text);
3143
3144 if let Some(image) = &inline_node.image {
3145 if !text.is_empty() {
3146 if let Ok(mut state) = inline_node.state.lock() {
3147 state.set_text(text.clone().into());
3148 }
3149 items.push(InlineFlowItem::Text {
3150 state: inline_node.state.clone(),
3151 text: text.clone().into(),
3152 links: links.clone(),
3153 highlights: fade_highlights(
3154 highlights.clone(),
3155 &slice_fades(fades, consumed, consumed + text.len()),
3156 ),
3157 backgrounds: slice_backgrounds(
3158 backgrounds,
3159 consumed,
3160 consumed + text.len(),
3161 ),
3162 reveal: node_cx.reveal_at(leaf_key, consumed, consumed + text.len()),
3163 });
3164 }
3165
3166 items.push(InlineFlowItem::Image {
3167 source: node_cx.image_source(image),
3168 link: image.link.clone(),
3169 title: image.title(),
3170 width: image.width,
3171 height: image.height,
3172 });
3173
3174 consumed += text.len();
3175 text.clear();
3176 links.clear();
3177 highlights.clear();
3178 offset = 0;
3179 } else {
3180 let mut node_highlights = vec![];
3181 for (range, style) in &inline_node.marks {
3182 let inner_range = (offset + range.start)..(offset + range.end);
3183 let mut highlight = mark_highlight(style, node_cx, cx);
3184
3185 if let Some(mut link_mark) = style.link.clone() {
3186 highlight.style.color = Some(node_cx.style.link());
3187 highlight.style.underline = Some(link_underline(&node_cx.style));
3188
3189 if let Some(identifier) = link_mark.identifier.as_ref()
3190 && let Some(mark) = node_cx.link_refs.get(identifier)
3191 {
3192 link_mark = mark.clone();
3193 }
3194
3195 links.push((inner_range.clone(), link_mark));
3196 }
3197
3198 node_highlights.push((inner_range, highlight));
3199 }
3200
3201 highlights = combine_highlights(highlights, node_highlights);
3202 offset += text_len;
3203 }
3204 }
3205
3206 if !text.is_empty() {
3207 if let Ok(mut state) = self.state.lock() {
3208 state.set_text(text.clone().into());
3209 }
3210 let highlights = fade_highlights(
3211 highlights,
3212 &slice_fades(fades, consumed, consumed + text.len()),
3213 );
3214 let backgrounds = slice_backgrounds(backgrounds, consumed, consumed + text.len());
3215 let reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
3216 items.push(InlineFlowItem::Text {
3217 state: self.state.clone(),
3218 text: text.into(),
3219 links,
3220 highlights,
3221 backgrounds,
3222 reveal,
3223 });
3224 }
3225
3226 items
3227 }
3228}
3229
3230fn block_element_id(kind: &'static str, span: Option<Span>, ix: usize) -> ElementId {
3235 (kind, span.map_or(ix, |span| span.start)).into()
3236}
3237
3238fn leaf_element_id(fade_key: Option<TextLeafKey>) -> ElementId {
3243 fade_key.map_or_else(|| ElementId::from("p"), ElementId::from)
3244}
3245
3246fn link_underline(style: &TextViewStyle) -> gpui::UnderlineStyle {
3249 gpui::UnderlineStyle {
3250 thickness: px(1.),
3251 color: Some(style.link().opacity(0.4)),
3252 ..Default::default()
3253 }
3254}
3255
3256fn body_font_size(node_cx: &NodeContext, window: &Window) -> Pixels {
3263 if node_cx.body_font_size > Pixels::ZERO {
3264 node_cx.body_font_size
3265 } else {
3266 window.text_style().font_size.to_pixels(window.rem_size())
3267 }
3268}
3269
3270fn heading_metrics(level: u8) -> (f32, f32, f32) {
3275 match level {
3276 1 => (1.8, 1.25, 1.1),
3277 2 => (4. / 3., 1.35, 1.6),
3278 3 => (17. / 15., 1.35, 1.4),
3279 4 => (1., 1.35, 1.4),
3280 5 => (14. / 15., 1.35, 1.4),
3281 _ => (13. / 15., 1.35, 1.4),
3282 }
3283}
3284
3285const HEADING_BOTTOM_GAP: f32 = 0.35;
3287
3288fn heading_size(level: u8, node_cx: &NodeContext, window: &Window) -> Pixels {
3291 node_cx.style.heading(level).text.font_size.map_or_else(
3292 || body_font_size(node_cx, window) * heading_metrics(level).0,
3293 |size| size.to_pixels(window.rem_size()),
3294 )
3295}
3296
3297fn rule_space(body_font_size: Pixels) -> Pixels {
3299 body_font_size * 1.6
3300}
3301
3302fn block_gap(options: &NodeRenderOptions, style: &TextViewStyle) -> Rems {
3304 if options.in_list || options.is_last {
3305 rems(0.)
3306 } else {
3307 style.paragraph_gap()
3308 }
3309}
3310
3311fn gapped(block: AnyElement, gap: Rems) -> AnyElement {
3314 if gap.is_zero() {
3315 block
3316 } else {
3317 div().pb(gap).child(block).into_any_element()
3318 }
3319}
3320
3321fn slice_fades(
3323 fades: &[(Range<usize>, f32)],
3324 start: usize,
3325 end: usize,
3326) -> Vec<(Range<usize>, f32)> {
3327 slice_ranges(fades, start, end, |range, fade_out| (range, *fade_out))
3328}
3329
3330fn slice_backgrounds(
3333 backgrounds: &[(Range<usize>, Hsla)],
3334 start: usize,
3335 end: usize,
3336) -> Vec<(Range<usize>, Hsla)> {
3337 slice_ranges(backgrounds, start, end, |range, color| (range, *color))
3338}
3339
3340struct TableColumn {
3342 max: f32,
3344 min: f32,
3346 divider: f32,
3347}
3348
3349fn table_cell_padding_x(style: &TextViewStyle, rem_size: Pixels) -> f32 {
3352 let padding = &style.table_cell().padding;
3353 let side = |length: Option<DefiniteLength>| match length {
3354 Some(DefiniteLength::Absolute(length)) => f32::from(length.to_pixels(rem_size)),
3355 _ => CELL_PAD_PX / 2.,
3356 };
3357 side(padding.left) + side(padding.right)
3358}
3359
3360const CELL_PAD_PX: f32 = 24.0; const CELL_MIN_PX: f32 = 48.0;
3362const CELL_BORDER_PX: f32 = 1.0; fn measure_table_columns(
3369 table: &Table,
3370 col_count: usize,
3371 node_cx: &NodeContext,
3372 window: &mut Window,
3373 cx: &mut App,
3374) -> Vec<f32> {
3375 let text_styles = TableTextStyles::new(&node_cx.style, window);
3376 let rem_size = window.rem_size();
3377 let mono_family = cx.theme().tokens.typography.mono.clone();
3378 let inline_code = node_cx.style.inline_code_highlight();
3379 let cached = table.column_widths_cache.0.lock().ok().and_then(|cache| {
3380 cache
3381 .as_ref()
3382 .filter(|cached| {
3383 cached.widths.len() == col_count
3384 && cached.text_styles == text_styles
3385 && cached.rem_size == rem_size
3386 && cached.mono_family == mono_family
3387 && cached.inline_code == inline_code
3388 && cached.text_system == text_system_id(window)
3389 })
3390 .cloned()
3391 });
3392 let measured = cached.unwrap_or_else(|| {
3393 let (widths, custom_cells) =
3394 measure_table_text_columns(table, col_count, &text_styles, node_cx, window, cx);
3395 let measured = Arc::new(TableColumnWidths {
3396 text_system: text_system_id(window),
3397 text_styles: text_styles.clone(),
3398 rem_size,
3399 mono_family,
3400 inline_code,
3401 widths,
3402 custom_cells,
3403 });
3404 if let Ok(mut cache) = table.column_widths_cache.0.lock() {
3405 *cache = Some(measured.clone());
3406 }
3407 measured
3408 });
3409
3410 let mut col_w = measured.widths.clone();
3414 for &(row_ix, ix) in &measured.custom_cells {
3415 let cell = &table.children[row_ix].children[ix];
3416 let items = cell.children.inline_flow_items(None, &[], &[], node_cx, cx);
3417 let style = text_styles.row_refinement(row_ix).clone();
3418 let width = window.with_text_style(Some(style), |window| {
3419 super::inline_flow::intrinsic_width(&items, window, cx)
3420 });
3421 let border = if ix + 1 < col_count {
3422 CELL_BORDER_PX
3423 } else {
3424 0.
3425 };
3426 col_w[ix] = col_w[ix].max(f32::from(width) + CELL_PAD_PX + border);
3427 }
3428 col_w
3429}
3430
3431fn measure_table_text_columns(
3434 table: &Table,
3435 col_count: usize,
3436 text_styles: &TableTextStyles,
3437 node_cx: &NodeContext,
3438 window: &mut Window,
3439 cx: &mut App,
3440) -> (Vec<f32>, Vec<(usize, usize)>) {
3441 let mut col_w = vec![CELL_MIN_PX; col_count];
3442 let mut custom_cells = Vec::new();
3443 for (row_ix, row) in table.children.iter().enumerate() {
3444 let text_style = text_styles.row(row_ix);
3445 let font_size = text_style.font_size.to_pixels(window.rem_size());
3446 for (ix, cell) in row.children.iter().enumerate() {
3447 let Some(slot) = col_w.get_mut(ix) else {
3448 continue;
3449 };
3450 if cell
3451 .children
3452 .children
3453 .iter()
3454 .any(|node| node.custom.is_some())
3455 {
3456 custom_cells.push((row_ix, ix));
3457 continue;
3458 }
3459 let text = cell.children.text();
3460 let highlights = cell.children.inline_highlights(node_cx, cx);
3461 let mut w = 0.0_f32;
3462 let mut line_start = 0;
3463 for line in text.split('\n') {
3464 let start = line_start + (line.len() - line.trim_start().len());
3465 let line_end = line_start + line.len();
3466 line_start = line_end + 1;
3467 let line = line.trim();
3468 if line.is_empty() {
3469 continue;
3470 }
3471 let end = start + line.len();
3472 let line_highlights = highlights
3473 .iter()
3474 .filter_map(|(range, highlight)| {
3475 let clipped = range.start.max(start)..range.end.min(end);
3476 (clipped.start < clipped.end).then(|| {
3477 (
3478 clipped.start - start..clipped.end - start,
3479 highlight.clone(),
3480 )
3481 })
3482 })
3483 .collect::<Vec<_>>();
3484 let mut line_w = gpui::Pixels::ZERO;
3485 for (range, scale) in
3486 text_size_ranges(line.len(), &line_highlights, font_size, window.rem_size())
3487 {
3488 let highlights = slice_ranges(
3489 &line_highlights,
3490 range.start,
3491 range.end,
3492 |range, highlight| (range, highlight.clone()),
3493 );
3494 let insets = code_insets(&highlights, font_size * scale, window.rem_size());
3495 line_w += insets.left + insets.right;
3496 let runs = text_runs(range.len(), text_style, &highlights);
3497 line_w += window
3498 .text_system()
3499 .layout_line(&line[range], font_size * scale, &runs, None)
3500 .width;
3501 }
3502 w = w.max(f32::from(line_w));
3503 }
3504 let border = if ix + 1 < col_count {
3507 CELL_BORDER_PX
3508 } else {
3509 0.
3510 };
3511 *slot = slot.max(w + CELL_PAD_PX + border);
3512 }
3513 }
3514 (col_w, custom_cells)
3515}
3516
3517impl Paragraph {
3518 fn to_markdown(&self) -> String {
3519 if self.children.iter().any(|node| node.custom.is_some()) {
3520 let mut source = MarkdownSource::default();
3521 for node in &self.children {
3522 if node.custom.is_some() {
3523 source.push_object(node);
3524 } else {
3525 source.push_text(&node.text, &node.marks, 0..node.text.len());
3526 if let Some(image) = &node.image {
3527 source.push_str(&image_markdown(image));
3528 }
3529 }
3530 }
3531 let mut text = source.finish();
3532 text.push_str("\n\n");
3533 return text;
3534 }
3535 let mut text = self
3536 .children
3537 .iter()
3538 .map(|text_node| {
3539 let mut text = text_node.text.to_string();
3540 for (range, style) in &text_node.marks {
3541 if style.bold {
3542 text = format!("**{}**", &text_node.text[range.clone()]);
3543 }
3544 if style.italic {
3545 text = format!("*{}*", &text_node.text[range.clone()]);
3546 }
3547 if style.strikethrough {
3548 text = format!("~~{}~~", &text_node.text[range.clone()]);
3549 }
3550 if style.code {
3551 text = format!("`{}`", &text_node.text[range.clone()]);
3552 }
3553 if style.highlight.is_some() {
3554 text = format!("=={}==", &text_node.text[range.clone()]);
3555 }
3556 if let Some(link) = &style.link {
3557 text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
3558 }
3559 }
3560
3561 if let Some(image) = &text_node.image {
3562 let alt = image.alt.clone().unwrap_or_default();
3563 let title = image
3564 .title
3565 .clone()
3566 .map_or(String::new(), |t| format!(" \"{}\"", t));
3567 text.push_str(&format!("", alt, image.url, title))
3568 }
3569
3570 text
3571 })
3572 .collect::<Vec<_>>()
3573 .join("");
3574
3575 text.push_str("\n\n");
3576 text
3577 }
3578}
3579
3580impl BlockNode {
3581 #[allow(dead_code)]
3585 pub(crate) fn to_markdown(&self) -> String {
3586 match self {
3587 BlockNode::Root { children, .. } => children
3588 .iter()
3589 .map(|child| child.to_markdown())
3590 .collect::<Vec<_>>()
3591 .join("\n\n"),
3592 BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
3593 BlockNode::Heading {
3594 level, children, ..
3595 } => {
3596 let hashes = "#".repeat(*level as usize);
3597 format!("{} {}", hashes, children.to_markdown())
3598 }
3599 BlockNode::Blockquote { children, .. } => {
3600 let content = children
3601 .iter()
3602 .map(|child| child.to_markdown())
3603 .collect::<Vec<_>>()
3604 .join("\n\n");
3605
3606 content
3607 .lines()
3608 .map(|line| format!("> {}", line))
3609 .collect::<Vec<_>>()
3610 .join("\n")
3611 }
3612 BlockNode::List {
3613 children,
3614 ordered,
3615 start,
3616 ..
3617 } => children
3618 .iter()
3619 .enumerate()
3620 .map(|(i, child)| {
3621 let prefix = if *ordered {
3622 format!("{}. ", ordered_list_ordinal(*start, i))
3623 } else {
3624 "- ".to_string()
3625 };
3626 format!("{}{}", prefix, child.to_markdown())
3627 })
3628 .collect::<Vec<_>>()
3629 .join("\n"),
3630 BlockNode::ListItem {
3631 children, checked, ..
3632 } => {
3633 let checkbox = if let Some(checked) = checked {
3634 if *checked { "[x] " } else { "[ ] " }
3635 } else {
3636 ""
3637 };
3638 format!(
3639 "{}{}",
3640 checkbox,
3641 children
3642 .iter()
3643 .map(|child| child.to_markdown())
3644 .collect::<Vec<_>>()
3645 .join("\n")
3646 )
3647 }
3648 BlockNode::CodeBlock(code_block) => {
3649 format!(
3650 "```{}\n{}\n```",
3651 code_block.lang.clone().unwrap_or_default(),
3652 code_block.code()
3653 )
3654 }
3655 BlockNode::Table(table) => table.to_markdown(),
3656 BlockNode::Break { html, .. } => {
3657 if *html {
3658 "<br>".to_string()
3659 } else {
3660 "\n".to_string()
3661 }
3662 }
3663 BlockNode::HorizontalRule { .. } => "---".to_string(),
3664 BlockNode::Custom { node, .. } => node.to_markdown(),
3665 BlockNode::Definition {
3666 identifier,
3667 url,
3668 title,
3669 ..
3670 } => {
3671 if let Some(title) = title {
3672 format!("[{}]: {} \"{}\"", identifier, url, title)
3673 } else {
3674 format!("[{}]: {}", identifier, url)
3675 }
3676 }
3677 BlockNode::Unknown { .. } => "".to_string(),
3678 }
3679 .trim()
3680 .to_string()
3681 }
3682}
3683
3684impl BlockNode {
3685 fn render_list_item_row(
3686 content: AnyElement,
3687 ix: usize,
3688 options: NodeRenderOptions,
3689 checked: Option<bool>,
3690 style: &TextViewStyle,
3691 line_height: Pixels,
3692 indent: Pixels,
3693 ) -> Div {
3694 h_flex()
3695 .w_full()
3696 .min_w_0()
3697 .relative()
3698 .items_start()
3699 .content_start()
3700 .when(!options.todo && checked.is_none(), |this| {
3701 this.child(
3705 div()
3706 .flex_none()
3707 .min_w(indent)
3708 .pr(indent * 0.3)
3709 .text_right()
3710 .text_color(style.muted_foreground())
3711 .refine_style(style.list_style())
3712 .child(
3713 list_item_prefix(
3714 ix,
3715 options.list_start,
3716 options.ordered,
3717 options.depth,
3718 )
3719 .trim_end()
3720 .to_string(),
3721 ),
3722 )
3723 })
3724 .when_some(checked, |this, checked| {
3725 let check_svg = if style.is_dark() {
3727 CHECK_SVG_DARK
3728 } else {
3729 CHECK_SVG_LIGHT
3730 };
3731 this.child(
3732 div()
3733 .flex()
3734 .mr_2()
3735 .h(line_height)
3736 .flex_none()
3737 .items_center()
3738 .justify_center()
3739 .child(
3740 div()
3741 .flex()
3742 .size(rems(0.9375))
3743 .items_center()
3744 .justify_center()
3745 .rounded(px(4.))
3746 .border(px(1.5))
3747 .border_color(
3748 style
3749 .list_style()
3750 .text
3751 .color
3752 .unwrap_or(style.muted_foreground()),
3753 )
3754 .when(checked, |this| {
3755 this.bg(style.link()).border_color(style.link()).child(
3756 img(Arc::new(Image::from_bytes(
3757 ImageFormat::Svg,
3758 check_svg.to_vec(),
3759 )))
3760 .size(rems(0.75)),
3761 )
3762 }),
3763 ),
3764 )
3765 })
3766 .child(
3767 div()
3768 .flex_auto()
3771 .min_w_0()
3772 .overflow_hidden()
3773 .when(checked == Some(true), |this| {
3775 this.text_color(style.muted_foreground())
3776 })
3777 .child(content),
3778 )
3779 }
3780
3781 fn render_list_item(
3782 item: &BlockNode,
3783 ix: usize,
3784 options: NodeRenderOptions,
3785 node_cx: &NodeContext,
3786 window: &mut Window,
3787 cx: &mut App,
3788 ) -> AnyElement {
3789 match item {
3790 BlockNode::ListItem {
3791 children,
3792 spread,
3793 checked,
3794 ..
3795 } => {
3796 let body = body_font_size(node_cx, window);
3797 let indent = body * 1.4;
3798 div()
3799 .w_full()
3800 .min_w_0()
3801 .when(ix > 0, |this| this.pt(body * 0.25))
3803 .when(*spread, |this| this.child(div()))
3804 .children({
3805 let mut items: Vec<Div> = Vec::with_capacity(children.len());
3806
3807 for (child_ix, child) in children.iter().enumerate() {
3808 match child {
3809 BlockNode::Paragraph { .. } => {
3810 let last_not_list = child_ix > 0
3811 && !matches!(
3812 children[child_ix - 1],
3813 BlockNode::List { .. }
3814 );
3815
3816 let text = child.render_block(
3817 NodeRenderOptions {
3818 depth: options.depth + 1,
3819 todo: checked.is_some(),
3820 is_last: true,
3821 ..options
3822 },
3823 node_cx,
3824 window,
3825 cx,
3826 );
3827
3828 if last_not_list {
3832 if let Some(preceding_row) = items.pop() {
3833 items.push(
3834 div().child(preceding_row).child(
3835 div()
3836 .w_full()
3837 .pl(indent)
3838 .overflow_hidden()
3839 .child(text),
3840 ),
3841 );
3842 continue;
3843 }
3844 }
3845
3846 items.push(Self::render_list_item_row(
3847 text,
3848 ix,
3849 options,
3850 *checked,
3851 &node_cx.style,
3852 window.line_height(),
3853 indent,
3854 ));
3855 }
3856 BlockNode::List { .. } => {
3857 items.push(div().ml(indent).child(child.render_block(
3858 NodeRenderOptions {
3859 depth: options.depth + 1,
3860 todo: checked.is_some(),
3861 is_last: true,
3862 ..options
3863 },
3864 node_cx,
3865 window,
3866 cx,
3867 )));
3868 }
3869 BlockNode::Root { .. }
3870 | BlockNode::Heading { .. }
3871 | BlockNode::Blockquote { .. }
3872 | BlockNode::CodeBlock(_)
3873 | BlockNode::Custom { .. }
3874 | BlockNode::Table(_)
3875 | BlockNode::HorizontalRule { .. } => {
3876 let block = child.render_block(
3877 NodeRenderOptions {
3878 depth: options.depth + 1,
3879 todo: checked.is_some(),
3880 is_last: true,
3881 ..options
3882 },
3883 node_cx,
3884 window,
3885 cx,
3886 );
3887
3888 if child_ix == 0 {
3889 items.push(Self::render_list_item_row(
3890 block,
3891 ix,
3892 options,
3893 *checked,
3894 &node_cx.style,
3895 window.line_height(),
3896 indent,
3897 ));
3898 } else {
3899 items.push(
3903 div()
3904 .w_full()
3905 .min_w_0()
3906 .pl(indent)
3907 .overflow_hidden()
3908 .child(block),
3909 );
3910 }
3911 }
3912 BlockNode::ListItem { .. }
3913 | BlockNode::Break { .. }
3914 | BlockNode::Definition { .. }
3915 | BlockNode::Unknown => {}
3916 }
3917 }
3918 items
3919 })
3920 .into_any_element()
3921 }
3922 _ => div().into_any_element(),
3923 }
3924 }
3925
3926 fn render_table(
3948 item: &BlockNode,
3949 options: &NodeRenderOptions,
3950 node_cx: &NodeContext,
3951 text_size: Pixels,
3952 window: &mut Window,
3953 cx: &mut App,
3954 ) -> impl IntoElement {
3955 const CELL_WRAP_MAX_LINES: f32 = 2.0;
3961 const CELL_WRAP_MIN_PX: f32 = 160.0;
3962 const CELL_WRAP_MAX_PX: f32 = 480.0;
3963 const TABLE_BORDER_PX: f32 = 2.0; let table = match item {
3966 BlockNode::Table(table) => table,
3967 _ => return div().into_any_element(),
3968 };
3969
3970 let style = &node_cx.style;
3971 let col_count = table
3972 .children
3973 .iter()
3974 .map(|row| row.children.len())
3975 .max()
3976 .unwrap_or(0);
3977 let col_w = measure_table_columns(table, col_count, node_cx, window, cx);
3978 let scroll = matches!(style.table().overflow.x, Some(Overflow::Scroll));
3979 let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
3980 let rem_size = window.rem_size();
3981 let wrap_min_px = f32::from(rems(4.).to_pixels(rem_size));
3982 let padding_delta = table_cell_padding_x(style, rem_size) - CELL_PAD_PX;
3984 let scale_factor = window.scale_factor();
3988 let device_ceil = |width: f32| (width * scale_factor).ceil() / scale_factor;
3989 let columns: Vec<TableColumn> = col_w
3990 .iter()
3991 .enumerate()
3992 .map(|(ix, &w)| {
3993 let w = w + padding_delta;
3994 let floor = if scroll && nowrap {
3995 w
3996 } else if scroll {
3997 (w / CELL_WRAP_MAX_LINES)
3998 .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
3999 .min(w)
4000 } else {
4001 w.min(wrap_min_px)
4002 };
4003 let divider = if ix + 1 < col_count {
4004 CELL_BORDER_PX
4005 } else {
4006 0.
4007 };
4008 let max = device_ceil(w - divider);
4009 TableColumn {
4010 min: device_ceil(floor - divider).min(max),
4011 max,
4012 divider,
4013 }
4014 })
4015 .collect();
4016
4017 let rows = Self::render_table_rows(table, &columns, node_cx, window, cx);
4018 let frame = StyleRefinement::default()
4019 .text_size(text_size)
4020 .bg(style
4021 .table_background()
4022 .unwrap_or(cx.theme().tokens.colors.surface))
4023 .border_1()
4024 .border_color(style.border())
4025 .refine_style(style.table());
4026 let body = if scroll {
4027 let scroll_handle = window
4028 .use_keyed_state(
4029 block_element_id("table-scroll", table.span, options.ix),
4030 cx,
4031 |_, _| ScrollHandle::default(),
4032 )
4033 .read(cx)
4034 .clone();
4035 let min_total_w = columns
4036 .iter()
4037 .map(|column| column.min + column.divider)
4038 .sum::<f32>()
4039 + TABLE_BORDER_PX;
4040 horizontal_scroll_area(
4050 block_element_id("table", table.span, options.ix),
4051 &scroll_handle,
4052 &frame,
4053 div().min_w_full().w(px(min_total_w)).children(rows),
4059 )
4060 .into_any_element()
4061 } else {
4062 div()
4063 .w_full()
4064 .overflow_hidden()
4065 .refine_style(&frame)
4066 .children(rows)
4067 .into_any_element()
4068 };
4069
4070 div()
4071 .pb(block_gap(options, style))
4072 .w_full()
4073 .child(body)
4074 .children(node_cx.table_actions.clone().map(|f| {
4081 div()
4082 .id(block_element_id("table-actions", table.span, options.ix))
4083 .mt_1()
4084 .child(f(&table.cached_table_data(), window, cx))
4085 }))
4086 .into_any_element()
4087 }
4088
4089 fn table_row_corner_radii(style: &TextViewStyle, window: &Window) -> [Option<Pixels>; 4] {
4101 let rem_size = window.rem_size();
4102 let inset = |radius: Option<gpui::AbsoluteLength>| {
4103 radius.map(|radius| (radius.to_pixels(rem_size) - px(1.)).max(px(0.)))
4104 };
4105 let radii = &style.table().corner_radii;
4106 [
4107 inset(radii.top_left),
4108 inset(radii.top_right),
4109 inset(radii.bottom_left),
4110 inset(radii.bottom_right),
4111 ]
4112 }
4113
4114 fn render_table_rows(
4128 table: &Table,
4129 columns: &[TableColumn],
4130 node_cx: &NodeContext,
4131 window: &mut Window,
4132 cx: &mut App,
4133 ) -> Vec<AnyElement> {
4134 const CELL_GROW: f32 = 1e6;
4140 const SLACK_GROW: f32 = 1.0;
4141
4142 let style = &node_cx.style;
4143 let col_count = columns.len();
4144 let total_w = columns.iter().map(|column| column.max).sum::<f32>().max(1.);
4145 let mut slack_style = StyleRefinement::default();
4148 slack_style.background = style.table_cell().background.clone();
4149 slack_style.border_color = style.table_cell().border_color;
4150 let slack = |grow: f32| {
4151 div()
4152 .flex_basis(px(0.))
4153 .flex_grow(grow)
4154 .flex_shrink(0.)
4155 .border_color(style.border())
4156 .refine_style(&slack_style)
4157 };
4158
4159 let row_count = table.children.len();
4160 let [top_left, top_right, bottom_left, bottom_right] =
4161 Self::table_row_corner_radii(style, window);
4162 let mut rows = Vec::with_capacity(row_count);
4163 let mut cell_ordinal = 0;
4164 for (row_ix, row) in table.children.iter().enumerate() {
4165 let mut cells = Vec::with_capacity(col_count * 2);
4166 for (ix, column) in columns.iter().enumerate() {
4167 let is_last_col = ix + 1 == col_count;
4168 let align = table.column_align(ix);
4169 let content = row.children.get(ix).map(|cell| {
4170 let fade_key = table
4171 .span
4172 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
4173 cell_ordinal += 1;
4174 cell.children.render(fade_key, node_cx, window, cx)
4175 });
4176
4177 let grow = SLACK_GROW * column.max / total_w;
4178 let (lead, trail) = match align {
4179 ColumnumnAlign::None | ColumnumnAlign::Left => (0., grow),
4180 ColumnumnAlign::Center => (grow / 2., grow / 2.),
4181 ColumnumnAlign::Right => (grow, 0.),
4182 };
4183 if lead > 0. {
4184 cells.push(slack(lead).into_any_element());
4185 }
4186 cells.push(
4187 TableCellElement(
4188 div()
4189 .flex_basis(px(0.))
4190 .flex_grow(CELL_GROW)
4191 .min_w(px(column.min))
4192 .max_w(px(column.max))
4193 .overflow_hidden()
4194 .when(align == ColumnumnAlign::Center, |this| this.text_center())
4195 .when(align == ColumnumnAlign::Right, |this| this.text_right())
4196 .px_3()
4197 .py(px(7.))
4198 .refine_style(&style.table_cell())
4199 .children(content)
4200 .into_any_element(),
4201 )
4202 .into_any_element(),
4203 );
4204 if trail > 0. || !is_last_col {
4205 cells.push(
4206 slack(trail)
4207 .when(!is_last_col, |this| this.border_r_1())
4208 .into_any_element(),
4209 );
4210 }
4211 }
4212 rows.push(
4213 div()
4214 .w_full()
4215 .when(row_ix < row_count - 1, |this| this.border_b_1())
4216 .border_color(style.border())
4217 .flex()
4218 .flex_row()
4219 .when(row_ix == 0, |this| {
4223 this.bg(style.code_background())
4224 .text_color(style.foreground())
4225 .when_some(top_left, |this, radius| this.rounded_tl(radius))
4226 .when_some(top_right, |this, radius| this.rounded_tr(radius))
4227 .refine_style(&style.table_head())
4228 })
4229 .when(row_ix + 1 == row_count, |this| {
4230 this.when_some(bottom_left, |this, radius| this.rounded_bl(radius))
4231 .when_some(bottom_right, |this, radius| this.rounded_br(radius))
4232 })
4233 .children(cells)
4234 .into_any_element(),
4235 );
4236 }
4237 rows
4238 }
4239
4240 pub(crate) fn render_block(
4241 &self,
4242 options: NodeRenderOptions,
4243 node_cx: &NodeContext,
4244 window: &mut Window,
4245 cx: &mut App,
4246 ) -> AnyElement {
4247 let mb = block_gap(&options, &node_cx.style);
4248
4249 match self {
4250 BlockNode::Root { children, .. } => div()
4251 .children(children.into_iter().enumerate().map(move |(ix, node)| {
4252 let (is_first, prev) = match flow_position(children, ix) {
4256 (true, _) => (options.is_first, options.prev),
4257 (false, prev) => (false, prev),
4258 };
4259 node.render_block(
4260 NodeRenderOptions {
4261 ix,
4262 is_first,
4263 prev,
4264 is_last: options.is_last && ix + 1 == children.len(),
4265 ..options
4266 },
4267 node_cx,
4268 window,
4269 cx,
4270 )
4271 }))
4272 .into_any_element(),
4273 BlockNode::Paragraph(paragraph) => gapped(
4274 paragraph.render(
4275 paragraph.span.map(|span| TextLeafKey::block(span.start)),
4276 node_cx,
4277 window,
4278 cx,
4279 ),
4280 mb,
4281 ),
4282 BlockNode::Heading {
4283 level,
4284 children,
4285 span,
4286 } => {
4287 let (_, line_height, section_gap) = heading_metrics(*level);
4288 let refinement = node_cx.style.heading(*level);
4289 let text_size = heading_size(*level, node_cx, window);
4290 let top = if options.is_first || options.depth > 0 {
4296 px(0.)
4297 } else {
4298 match options.prev {
4299 PrevBlock::Heading(_) => text_size * 0.25,
4300 PrevBlock::Rule => (text_size * section_gap
4301 - rule_space(body_font_size(node_cx, window)))
4302 .max(px(0.)),
4303 PrevBlock::Other => (text_size * section_gap
4304 - node_cx.style.paragraph_gap().to_pixels(window.rem_size()))
4305 .max(px(0.)),
4306 }
4307 };
4308 let bottom = if options.is_last {
4309 px(0.)
4310 } else {
4311 text_size * HEADING_BOTTOM_GAP
4312 };
4313
4314 div()
4315 .pt(top)
4316 .pb(bottom)
4317 .whitespace_normal()
4318 .text_size(text_size)
4319 .line_height(relative(line_height))
4320 .font_weight(FontWeight::SEMIBOLD)
4321 .when(*level >= 6, |this| {
4322 this.text_color(node_cx.style.muted_foreground())
4323 })
4324 .refine_style(&refinement)
4325 .child(children.render(
4326 span.map(|span| TextLeafKey::block(span.start)),
4327 node_cx,
4328 window,
4329 cx,
4330 ))
4331 .into_any_element()
4332 }
4333 BlockNode::Blockquote { children, .. } => gapped(
4334 div()
4335 .w_full()
4336 .text_color(node_cx.style.muted_foreground())
4337 .border_l_2()
4338 .border_color(node_cx.style.border())
4339 .pl(body_font_size(node_cx, window) * 0.9)
4340 .refine_style(node_cx.style.blockquote_style())
4341 .children({
4343 let children_len = children.len();
4344 children.into_iter().enumerate().map(move |(index, c)| {
4345 let is_last = index == children_len - 1;
4346 let (is_first, prev) = flow_position(children, index);
4347 window.with_text_style(
4348 Some(node_cx.style.blockquote_style().text.clone()),
4349 |window| {
4350 c.render_block(
4351 NodeRenderOptions {
4352 is_first,
4353 prev,
4354 ..options.is_last(is_last)
4355 },
4356 node_cx,
4357 window,
4358 cx,
4359 )
4360 },
4361 )
4362 })
4363 })
4364 .into_any_element(),
4365 mb,
4366 ),
4367 BlockNode::List {
4368 children,
4369 ordered,
4370 start,
4371 ..
4372 } => div()
4373 .w_full()
4374 .min_w_0()
4375 .pb(mb)
4376 .children({
4377 let mut items = Vec::with_capacity(children.len());
4378 let mut item_index = 0;
4379 for (ix, item) in children.into_iter().enumerate() {
4380 let is_item = item.is_list_item();
4381
4382 items.push(Self::render_list_item(
4383 item,
4384 item_index,
4385 NodeRenderOptions {
4386 ix,
4387 ordered: *ordered,
4388 list_start: *start,
4389 ..options
4390 },
4391 node_cx,
4392 window,
4393 cx,
4394 ));
4395
4396 if is_item {
4397 item_index += 1;
4398 }
4399 }
4400 items
4401 })
4402 .into_any_element(),
4403 BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
4404 BlockNode::Custom { node, state } => {
4405 let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
4406 Some(rendered) => rendered,
4407 None => div().child(node.as_text().to_string()).into_any_element(),
4408 };
4409
4410 div()
4411 .pb(mb)
4412 .child(CustomBlockElement {
4413 content: inner,
4414 state: state.clone(),
4415 })
4416 .into_any_element()
4417 }
4418 BlockNode::Table { .. } => {
4419 let text_size = body_font_size(node_cx, window) * (14. / 15.);
4424 window.with_text_style(
4425 Some(TextStyleRefinement {
4426 font_size: Some(text_size.into()),
4427 ..Default::default()
4428 }),
4429 |window| {
4430 Self::render_table(self, &options, node_cx, text_size, window, cx)
4431 .into_any_element()
4432 },
4433 )
4434 }
4435 BlockNode::HorizontalRule { .. } => {
4436 let space = rule_space(body_font_size(node_cx, window));
4440 let top = if options.is_first || options.depth > 0 {
4441 px(0.)
4442 } else {
4443 let above = match options.prev {
4444 PrevBlock::Rule => space,
4445 PrevBlock::Heading(level) => {
4446 heading_size(level, node_cx, window) * HEADING_BOTTOM_GAP
4447 }
4448 PrevBlock::Other => {
4449 node_cx.style.paragraph_gap().to_pixels(window.rem_size())
4450 }
4451 };
4452 (space - above).max(px(0.))
4453 };
4454 div()
4455 .pt(top)
4456 .when(!options.is_last, |this| this.pb(space))
4457 .child(div().bg(node_cx.style.border()).h(px(1.)))
4458 .into_any_element()
4459 }
4460 BlockNode::Break { .. } => div().into_any_element(),
4461 BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
4462 _ => {
4463 if cfg!(debug_assertions) {
4464 tracing::warn!("unknown implementation: {:?}", self);
4465 }
4466
4467 div().into_any_element()
4468 }
4469 }
4470 }
4471}
4472
4473#[cfg(test)]
4474mod tests {
4475 use super::*;
4476
4477 #[test]
4478 fn block_plugin_selection_copies_both_formats_and_clears_synchronously() {
4479 let source = "$$\nx\n$$";
4480 let mut node = MarkdownNode::new("math", ()).text("x").markdown(source);
4481 node.set_span(Some(Span {
4482 start: 0,
4483 end: source.len(),
4484 }));
4485 let state = BlockState::default();
4486 let same_content = BlockNode::Custom {
4487 node: node.clone(),
4488 state: BlockState::default(),
4489 };
4490 let block = BlockNode::Custom {
4491 node,
4492 state: state.clone(),
4493 };
4494 let document = crate::text::document::ParsedDocument {
4495 source: source.into(),
4496 blocks: Arc::new(vec![block.clone()]),
4497 };
4498 assert!(!block.has_selection());
4499 state.set_selection(Some(BlockSelection::Full));
4500 assert_eq!(block, same_content);
4502 for blocks in [None, Some(0..=0)] {
4503 assert_eq!(
4504 document.selected_text(SelectionFormat::Plain, blocks.clone()),
4505 "x\n"
4506 );
4507 assert_eq!(
4508 document.selected_text(SelectionFormat::Source, blocks),
4509 source
4510 );
4511 }
4512 assert_eq!(document.selected_source_range(), Some(0..source.len()));
4513
4514 block.clear_selection();
4516 assert!(!document.blocks[0].has_selection());
4517 assert_eq!(
4518 document.selected_text(SelectionFormat::Plain, Some(0..=0)),
4519 ""
4520 );
4521 assert_eq!(document.selected_source_range(), None);
4522 }
4523
4524 #[test]
4525 fn block_plugin_empty_endpoint_suppresses_virtualized_copy_fallback() {
4526 let state = BlockState::default();
4527 let block = BlockNode::Custom {
4528 node: MarkdownNode::new("math", ())
4529 .text("formula")
4530 .markdown("$$formula$$"),
4531 state: state.clone(),
4532 };
4533 let after = CodeBlock::from_code("after", None::<SharedString>);
4534 after.set_selection(0..5);
4535 let document = crate::text::document::ParsedDocument {
4536 blocks: Arc::new(vec![block, BlockNode::CodeBlock(after)]),
4537 ..Default::default()
4538 };
4539 assert!(
4540 document
4541 .selected_text(SelectionFormat::Plain, Some(0..=1))
4542 .contains("formula")
4543 );
4544 state.set_selection(Some(BlockSelection::Empty));
4545 assert!(document.blocks[0].has_selection());
4546 for format in [SelectionFormat::Plain, SelectionFormat::Source] {
4547 let text = document.selected_text(format, Some(0..=1));
4548 assert!(text.contains("after"));
4549 assert!(!text.contains("formula"));
4550 }
4551 }
4552
4553 #[test]
4554 fn block_plugin_geometry_selects_whole_blocks_in_both_drag_directions() {
4555 let bounds = Bounds::new(gpui::point(px(10.), px(20.)), gpui::size(px(100.), px(40.)));
4556 let inside = bounds.center();
4557 let above = gpui::point(px(0.), px(0.));
4558 let below = gpui::point(px(0.), px(80.));
4559 for (start, end) in [(above, inside), (inside, below), (above, below)] {
4560 assert!(custom_block_is_selected(bounds, start, end));
4561 assert!(custom_block_is_selected(bounds, end, start));
4562 }
4563 assert!(!custom_block_is_selected(bounds, inside, inside));
4564 for (start, end) in [
4567 (inside, inside + gpui::point(px(1.), px(0.))),
4568 (
4569 bounds.origin,
4570 bounds.bottom_right() - gpui::point(px(1.), px(1.)),
4571 ),
4572 ] {
4573 assert!(!custom_block_is_selected(bounds, start, end));
4574 assert!(!custom_block_is_selected(bounds, end, start));
4575 }
4576 assert!(!custom_block_is_selected(
4577 bounds,
4578 gpui::point(px(150.), px(30.)),
4579 gpui::point(px(160.), px(40.))
4580 ));
4581 }
4582
4583 #[test]
4584 fn selected_inline_objects_coalesce_surrounding_emphasis() {
4585 for (object_mark, expected) in [
4586 (TextMark::default().italic(), "*fore $x$ aft*"),
4587 (TextMark::default().italic().bold(), "*fore **$x$** aft*"),
4588 ] {
4589 let italic = TextMark::default().italic();
4590 let before = InlineNode::new("before ").marks(vec![(0..7, italic.clone())]);
4591 let formula =
4592 InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"))
4593 .marks(vec![(0..1, object_mark)]);
4594 let after = InlineNode::new(" after").marks(vec![(0..6, italic)]);
4595 {
4596 let mut state = formula.state.lock().unwrap();
4597 state.text = "before ".into();
4598 state.selection = Some((2..7).into());
4599 }
4600 *formula.custom_selection.lock().unwrap() = true;
4601 let paragraph = Paragraph {
4602 children: vec![before, formula, after],
4603 ..Default::default()
4604 };
4605 {
4606 let mut state = paragraph.state.lock().unwrap();
4607 state.text = " after".into();
4608 state.selection = Some((0..4).into());
4609 }
4610 assert_eq!(paragraph.selected_source(), expected);
4611 }
4612 }
4613
4614 #[test]
4615 fn consecutive_inline_objects_copy_atomically_without_neighboring_text() {
4616 let first =
4617 InlineNode::custom(MarkdownNode::new("math", ()).text("甲²").markdown("$甲^2$"));
4618 let second = InlineNode::custom(MarkdownNode::new("math", ()).text("b").markdown("$b$"));
4619 *first.custom_selection.lock().unwrap() = true;
4620 *second.custom_selection.lock().unwrap() = true;
4621 let paragraph = Paragraph {
4622 children: vec![first, second],
4623 ..Default::default()
4624 };
4625 assert_eq!(paragraph.selected_text(), "甲²b");
4626 assert_eq!(paragraph.selected_source(), "$甲^2$$b$");
4627 assert!(paragraph.has_selection());
4628 paragraph.clear_selection();
4629 assert_eq!(paragraph.selected_text(), "");
4630 assert_eq!(paragraph.selected_source(), "");
4631 assert!(!paragraph.has_selection());
4632 }
4633
4634 #[test]
4635 fn selected_inline_object_interleaves_runs_and_preserves_enclosing_mark() {
4636 let before = InlineNode::new("中文 ");
4637 let formula =
4638 InlineNode::custom(MarkdownNode::new("math", ()).text("x²").markdown("$x^2$"))
4639 .marks(vec![(0..3, TextMark::default().bold())]);
4640 let after = InlineNode::new(" English");
4641 {
4642 let mut preceding = formula.state.lock().unwrap();
4643 preceding.text = "中文 ".into();
4644 preceding.selection = Some((3..7).into());
4645 }
4646 *formula.custom_selection.lock().unwrap() = true;
4647 let paragraph = Paragraph {
4648 children: vec![before, formula, after],
4649 ..Default::default()
4650 };
4651 {
4652 let mut trailing = paragraph.state.lock().unwrap();
4653 trailing.text = " English".into();
4654 trailing.selection = Some((0..4).into());
4655 }
4656 assert_eq!(paragraph.selected_text(), "文 x² Eng");
4657 assert_eq!(paragraph.selected_source(), "文 **$x^2$** Eng");
4658 }
4659
4660 #[test]
4661 fn mark_sets_a_readable_text_color_with_its_background() {
4662 use gpui::{Empty, TestApp};
4663 let mut app = TestApp::new();
4664 let mut window = app.open_window(|_, _| Empty);
4665 window.update(|_, _, cx| {
4666 cx.set_global(crate::Theme::default());
4667 let node_cx = NodeContext::default();
4668
4669 let yellow: Hsla = gpui::rgb(0xfef08a).into();
4672 let style = mark_highlight(&TextMark::default().highlight(yellow), &node_cx, cx).style;
4673 assert_eq!(style.background_color, Some(yellow));
4674 assert_eq!(style.color, Some(gpui::black()));
4675
4676 let navy: Hsla = gpui::rgb(0x1e3a8a).into();
4678 let style = mark_highlight(&TextMark::default().highlight(navy), &node_cx, cx).style;
4679 assert_eq!(style.color, Some(gpui::white()));
4680
4681 let translucent_black: Hsla = gpui::rgba(0x00000080).into();
4684 let translucent_white: Hsla = gpui::rgba(0xffffff4d).into();
4685 assert_eq!(
4686 mark_text_color(translucent_black, gpui::white()),
4687 gpui::black()
4688 );
4689 assert_eq!(
4690 mark_text_color(translucent_white, gpui::black()),
4691 gpui::white()
4692 );
4693 let transparent: Hsla = gpui::rgba(0x00000000).into();
4694 let style =
4695 mark_highlight(&TextMark::default().highlight(transparent), &node_cx, cx).style;
4696 assert_eq!(style.color, None);
4697
4698 let style = mark_highlight(&TextMark::default().bold(), &node_cx, cx).style;
4700 assert_eq!(style.color, None);
4701 });
4702 }
4703
4704 #[test]
4705 fn translucent_mark_cache_tracks_theme_background() {
4706 use gpui::{Empty, TestApp};
4707 let mut app = TestApp::new();
4708 let mut window = app.open_window(|_, _| Empty);
4709 window.update(|_, _, cx| {
4710 cx.set_global(crate::Theme::default());
4711 let paragraph = Paragraph {
4712 children: vec![InlineNode::new("mark").marks(vec![(
4713 0..4,
4714 TextMark::default().highlight(gpui::rgba(0x00000033).into()),
4715 )])],
4716 ..Default::default()
4717 };
4718 let context = NodeContext::default();
4719 crate::Theme::global_mut(cx).tokens.colors.background = gpui::white();
4720 let (_, light, _) = paragraph.plain_render(&context, cx);
4721 assert_eq!(light[0].1.style.color, Some(gpui::black()));
4722 crate::Theme::global_mut(cx).tokens.colors.background = gpui::black();
4723 let (_, dark, _) = paragraph.plain_render(&context, cx);
4724 assert_eq!(dark[0].1.style.color, Some(gpui::white()));
4725 });
4726 }
4727
4728 #[test]
4729 fn custom_inline_inherits_marks_and_resolves_link_references() {
4730 use gpui::{Empty, TestApp};
4731 let mut app = TestApp::new();
4732 let mut window = app.open_window(|_, _| Empty);
4733 window.update(|_, _, cx| {
4734 cx.set_global(crate::Theme::default());
4735 let mut node_cx = NodeContext::default();
4736 let mut mark = TextMark::default().bold();
4737 mark.italic = true;
4738 mark.strikethrough = true;
4739 mark.link = Some(LinkMark {
4740 identifier: Some("ref".into()),
4741 ..Default::default()
4742 });
4743 node_cx.link_refs.insert(
4744 "ref".into(),
4745 LinkMark {
4746 url: "https://example.com".into(),
4747 ..Default::default()
4748 },
4749 );
4750 let paragraph = Paragraph {
4751 children: vec![
4752 InlineNode::custom(MarkdownNode::new("test", ()).text("x"))
4753 .marks(vec![(0..1, mark)]),
4754 ],
4755 ..Default::default()
4756 };
4757 let items = paragraph.inline_flow_items(None, &[], &[], &node_cx, cx);
4758 let InlineFlowItem::Object { style, link, .. } = &items[0] else {
4759 panic!()
4760 };
4761 assert_eq!(style.font_weight, Some(FontWeight::BOLD));
4762 assert_eq!(style.font_style, Some(FontStyle::Italic));
4763 assert!(style.strikethrough.is_some());
4764 assert_eq!(link.as_ref().unwrap().url.as_ref(), "https://example.com");
4765 });
4766 }
4767
4768 #[test]
4769 fn table_column_uses_prepared_inline_metrics_after_resource_update() {
4770 use crate::text::InlineElement;
4771 use crate::text::inline::test_draw::in_prepaint;
4772 use gpui::{Image, ImageFormat, TestApp};
4773 let mut app = TestApp::new();
4774 let width = Arc::new(std::sync::atomic::AtomicUsize::new(400));
4775 let render_width = width.clone();
4776 let mut node_cx = NodeContext::default();
4777 node_cx.markdown_extensions = Arc::new(MarkdownExtensions::default().plugin(
4778 crate::text::markdown_ext::TestInlinePlugin::new("test").render_with(
4779 move |_, _, _, _| {
4780 Some(
4781 InlineElement::new(
4782 gpui::img(Arc::new(Image::from_bytes(
4783 ImageFormat::Svg,
4784 b"<svg/>".to_vec(),
4785 )))
4786 .w(px(
4787 render_width.load(std::sync::atomic::Ordering::Relaxed) as f32
4788 ))
4789 .h(px(20.)),
4790 )
4791 .with_baseline(px(15.)),
4792 )
4793 },
4794 ),
4795 ));
4796 let table = table_of(
4797 vec![
4798 vec![plain_cell("ordinary text")],
4799 vec![TableCell {
4800 children: Paragraph {
4801 children: vec![InlineNode::custom(MarkdownNode::new("test", ()).text("x"))],
4802 ..Default::default()
4803 },
4804 width: None,
4805 }],
4806 ],
4807 vec![],
4808 );
4809 in_prepaint(&mut app, move |window, cx| {
4810 let mut cached = None;
4811 for expected in [400, 600, 10, 400] {
4812 width.store(expected, std::sync::atomic::Ordering::Relaxed);
4813 let measured = measure_table_columns(&table, 1, &node_cx, window, cx)[0];
4814 let current = cached_column_widths(&table);
4815 assert_eq!(
4816 measured,
4817 current.widths[0].max(expected as f32 + CELL_PAD_PX)
4818 );
4819 assert_eq!(current.custom_cells, vec![(1, 0)]);
4820 if let Some(cached) = cached.replace(current.clone()) {
4821 assert!(Arc::ptr_eq(&cached, ¤t));
4822 }
4823 }
4824 });
4825 }
4826
4827 fn cached_column_widths(table: &Table) -> Arc<TableColumnWidths> {
4828 table.column_widths_cache.0.lock().unwrap().clone().unwrap()
4829 }
4830
4831 #[test]
4832 fn blockquote_typography_reaches_nested_table_measurement() {
4833 use crate::text::inline::test_draw::in_prepaint;
4834 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
4835 use gpui::TestApp;
4836
4837 let quote = BlockNode::Blockquote {
4838 children: vec![BlockNode::Table(table_of(
4839 vec![vec![plain_cell("0123456789")]],
4840 vec![],
4841 ))],
4842 span: None,
4843 };
4844 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4845 in_prepaint(&mut app, move |window, cx| {
4846 cx.set_global(crate::Theme::default());
4847 window.with_text_style(
4848 Some(
4849 StyleRefinement::default()
4850 .font_family(BODY)
4851 .text_size(px(15.))
4852 .text,
4853 ),
4854 |window| {
4855 for (refinement, expected_width) in [
4856 (StyleRefinement::default(), 94.),
4857 (
4858 StyleRefinement::default()
4859 .font_family(MONO)
4860 .text_size(px(30.))
4861 .font_weight(FontWeight::BOLD),
4862 199.,
4863 ),
4864 ] {
4865 let node_cx = NodeContext {
4866 style: Arc::new(
4867 TextViewStyle::default().with_blockquote_style(refinement),
4868 ),
4869 body_font_size: px(15.),
4870 ..Default::default()
4871 };
4872 quote.render_block(NodeRenderOptions::default(), &node_cx, window, cx);
4873 let BlockNode::Blockquote { children, .. } = "e else {
4874 unreachable!()
4875 };
4876 let BlockNode::Table(table) = &children[0] else {
4877 unreachable!()
4878 };
4879 assert_eq!(cached_column_widths(table).widths, vec![expected_width]);
4881 }
4882 },
4883 );
4884 });
4885 }
4886
4887 #[test]
4888 fn table_columns_cache_preserves_plain_widths_and_clones() {
4889 use crate::text::inline::test_draw::in_prepaint;
4890 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem, record_shaped_lines};
4891 use gpui::{Refineable as _, TestApp, TextStyle};
4892
4893 let long = "é界".repeat(2048);
4894 let table = table_of(
4895 vec![
4896 vec![plain_cell(&format!(" \t{long} \nshort\n ")), plain_cell("")],
4897 vec![plain_cell("short")],
4898 vec![],
4899 vec![plain_cell(""), plain_cell("a\nabcdefghijklmnop")],
4900 ],
4901 vec![],
4902 );
4903 let uncached = table.clone();
4904 let node_cx = NodeContext::default();
4905 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
4906 let (table, node_cx, cached) = in_prepaint(&mut app, move |window, cx| {
4907 window.set_rem_size(px(20.));
4908 let style = TextStyle {
4909 font_family: BODY.into(),
4910 font_size: rems(1.).into(),
4911 ..Default::default()
4912 };
4913 let cached =
4914 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
4915 let (widths, shaped) = record_shaped_lines(|| {
4916 measure_table_columns(&table, 2, &node_cx, window, cx)
4917 });
4918 assert_eq!(widths, vec![40960. + CELL_PAD_PX + CELL_BORDER_PX, 184.]);
4919 assert!(shaped.iter().any(|line| line == &long));
4920 assert_eq!(
4921 table, uncached,
4922 "derived widths do not change table equality"
4923 );
4924 assert!(uncached.column_widths_cache.0.lock().unwrap().is_none());
4925
4926 let cached = cached_column_widths(&table);
4927 let cloned = table.clone();
4928 for same in [&table, &cloned] {
4929 let (again, shaped) = record_shaped_lines(|| {
4930 measure_table_columns(same, 2, &node_cx, window, cx)
4931 });
4932 assert_eq!(again, widths);
4933 assert!(shaped.is_empty());
4934 assert!(Arc::ptr_eq(&cached, &cached_column_widths(same)));
4935 }
4936
4937 assert_eq!(
4939 measure_table_columns(&cloned, 3, &node_cx, window, cx),
4940 vec![widths[0], widths[1] + CELL_BORDER_PX, CELL_MIN_PX]
4941 );
4942 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&cloned)));
4943 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4944 assert!(
4945 measure_table_columns(&Table::default(), 0, &node_cx, window, cx)
4946 .is_empty()
4947 );
4948 cached
4949 });
4950 (table, node_cx, cached)
4951 });
4952 in_prepaint(&mut app, move |window, cx| {
4955 window.set_rem_size(cached.rem_size);
4956 window.with_text_style(
4957 Some(cached.text_styles.body.subtract(&Default::default())),
4958 |window| {
4959 measure_table_columns(&table, 2, &node_cx, window, cx);
4960 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&table)));
4961 },
4962 );
4963 });
4964 }
4965
4966 #[test]
4967 fn table_columns_cache_tracks_typography() {
4968 use crate::text::inline::test_draw::in_prepaint;
4969 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
4970 use gpui::{Refineable as _, TestApp, TextStyle};
4971
4972 let code = "0123456789";
4973 let mut paragraph = Paragraph::default();
4974 paragraph
4975 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
4976 let table = table_of(
4977 vec![vec![
4978 plain_cell(code),
4979 TableCell {
4980 children: paragraph,
4981 width: None,
4982 },
4983 ]],
4984 vec![],
4985 );
4986 let base = TextStyle {
4987 font_family: BODY.into(),
4988 font_size: rems(1.).into(),
4989 ..Default::default()
4990 };
4991 let mut larger = base.clone();
4992 larger.font_size = rems(2.).into();
4993 let mut bold = base.clone();
4994 bold.font_weight = FontWeight::BOLD;
4995 let mut mono = base.clone();
4996 mono.font_family = MONO.into();
4997 let normal_code = HighlightStyle::default();
4998 let bold_code = HighlightStyle {
4999 font_weight: Some(FontWeight::BOLD),
5000 ..Default::default()
5001 };
5002 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5003 in_prepaint(&mut app, move |window, cx| {
5004 let mut previous = None;
5005 for (style, rem_size, family, inline_code, expected) in [
5006 (base.clone(), 20., MONO, normal_code, [125., 207.]),
5007 (base.clone(), 30., MONO, normal_code, [175., 294.5]),
5008 (larger, 20., MONO, normal_code, [225., 382.]),
5009 (bold, 20., MONO, normal_code, [175., 250.75]),
5010 (mono, 20., MONO, normal_code, [225., 207.]),
5011 (base.clone(), 20., MONO, normal_code, [125., 207.]),
5012 (base.clone(), 20., BODY, normal_code, [125., 119.5]),
5013 (base.clone(), 20., MONO, normal_code, [125., 207.]),
5014 (base, 20., MONO, bold_code, [125., 250.75]),
5015 ] {
5016 let mut theme = crate::Theme::default();
5017 theme.tokens.typography.mono = family.into();
5018 cx.set_global(theme);
5019 window.set_rem_size(px(rem_size));
5020 let node_cx = NodeContext {
5021 style: Arc::new(TextViewStyle::default().with_inline_code(inline_code)),
5022 ..Default::default()
5023 };
5024 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
5025 let widths = measure_table_columns(&table, 2, &node_cx, window, cx);
5026 for (width, expected) in widths.iter().zip(expected) {
5027 assert!((width - expected).abs() < 0.01, "{width} != {expected}");
5028 }
5029 let cached = cached_column_widths(&table);
5030 if let Some(previous) = previous.replace(cached.clone()) {
5031 assert!(!Arc::ptr_eq(&previous, &cached));
5032 }
5033 assert_eq!(
5034 measure_table_columns(&table, 2, &node_cx, window, cx),
5035 widths
5036 );
5037 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
5038 });
5039 }
5040 });
5041 }
5042
5043 #[test]
5044 fn table_columns_cache_is_rebuilt_for_reparsed_tables() {
5045 use crate::text::format::markdown;
5046 use crate::text::inline::test_draw::in_prepaint;
5047 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem};
5048 use gpui::{Refineable as _, TestApp, TextStyle};
5049
5050 let mut node_cx = NodeContext::default();
5051 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5052 in_prepaint(&mut app, move |window, cx| {
5053 let style = TextStyle {
5054 font_family: BODY.into(),
5055 font_size: px(20.).into(),
5056 ..Default::default()
5057 };
5058 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
5059 let mut previous = None;
5060 for (text, expected) in [("longer cell", 134.), ("x", CELL_MIN_PX)] {
5061 let source = format!("| h |\n| - |\n| {text} |\n");
5062 let document = markdown::parse(&source, &mut node_cx).unwrap();
5063 let BlockNode::Table(table) = &document.blocks[0] else {
5064 panic!("expected a parsed table");
5065 };
5066 assert!(table.column_widths_cache.0.lock().unwrap().is_none());
5067 assert_eq!(
5068 measure_table_columns(table, 1, &node_cx, window, cx),
5069 vec![expected]
5070 );
5071 let cached = cached_column_widths(table);
5072 if let Some(previous) = previous.replace(cached.clone()) {
5073 assert!(!Arc::ptr_eq(&previous, &cached));
5074 }
5075 assert_eq!(document.source.as_ref(), source);
5076 }
5077 });
5078 });
5079 }
5080
5081 #[test]
5085 fn table_column_of_inline_code_cells_fits_the_mono_width() {
5086 use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
5087 use gpui::{Empty, TestApp};
5088
5089 let code = "method_name()";
5090 let mut paragraph = Paragraph::default();
5091 paragraph
5092 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
5093 let table = Table {
5094 children: vec![TableRow {
5095 children: vec![TableCell {
5096 children: paragraph,
5097 width: None,
5098 }],
5099 }],
5100 column_aligns: vec![],
5101 span: None,
5102 table_data_cache: TableDataCache::default(),
5103 column_widths_cache: TableColumnWidthsCache::default(),
5104 };
5105 let node_cx = NodeContext::default();
5106
5107 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5108 let mut window = app.open_window(|_, _| Empty);
5109 let (col_w, font_size) = window.update(|_, window, cx| {
5110 let mut theme = crate::Theme::default();
5111 theme.tokens.typography.mono = MONO.into();
5112 cx.set_global(theme);
5113 let font_size = window.text_style().font_size.to_pixels(window.rem_size());
5114 (
5115 measure_table_columns(&table, 1, &node_cx, window, cx),
5116 font_size,
5117 )
5118 });
5119
5120 let mono_w = f32::from(WideMonoTextSystem::width_of(code, MONO, font_size * 0.875));
5121 assert!(
5122 (col_w[0]
5123 - (mono_w + CELL_PAD_PX + crate::text::inline_flow::INLINE_CODE_PADDING * 2.))
5124 .abs()
5125 < 0.01,
5126 "col_w {} must fit the mono width {} plus padding {}",
5127 col_w[0],
5128 mono_w,
5129 CELL_PAD_PX
5130 );
5131 }
5132
5133 #[test]
5134 fn code_block_highlights_are_cached_by_highlighter_identity() {
5135 use std::sync::atomic::{AtomicUsize, Ordering};
5136
5137 let calls = Arc::new(AtomicUsize::new(0));
5138 let calls_for_highlighter = calls.clone();
5139 let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
5140 calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
5141 Vec::new()
5142 });
5143 let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
5144
5145 block.highlighted_styles(&highlighter);
5146 block.highlighted_styles(&highlighter);
5147 assert_eq!(calls.load(Ordering::Relaxed), 1);
5148
5149 let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
5150 block.highlighted_styles(&replacement);
5151 assert!(Arc::ptr_eq(
5152 &block
5153 .highlight_cache
5154 .lock()
5155 .unwrap()
5156 .as_ref()
5157 .unwrap()
5158 .highlighter,
5159 &replacement
5160 ));
5161 }
5162
5163 #[test]
5164 fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
5165 let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
5169 vec![(
5170 0..2,
5171 HighlightStyle {
5172 color: Some(gpui::rgb(0x0000ff).into()),
5173 ..Default::default()
5174 },
5175 )]
5176 });
5177 let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
5178 vec![(
5179 0..2,
5180 HighlightStyle {
5181 color: Some(gpui::rgb(0xffff00).into()),
5182 ..Default::default()
5183 },
5184 )]
5185 });
5186 let block = CodeBlock::from_code("42", Some("json"));
5187
5188 let light_styles = block.highlighted_styles(&light);
5189 let dark_styles = block.highlighted_styles(&dark);
5190
5191 assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
5192 assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
5193 assert_eq!(block.code(), "42", "the document must survive the swap");
5194 }
5195
5196 #[test]
5197 fn reconstruct_markdown_wraps_marked_runs() {
5198 let marks = vec![(0..4, TextMark::default().bold())];
5200 assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
5201 assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
5203 }
5204
5205 #[test]
5206 fn reconstruct_markdown_emits_unmarked_text_verbatim() {
5207 let text = "a b c";
5209 let marks = vec![(2..3, TextMark::default().code())];
5210 assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
5211 assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
5213 }
5214
5215 #[test]
5216 fn reconstruct_markdown_handles_code_italic_strike_link() {
5217 assert_eq!(
5218 reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
5219 "`x`"
5220 );
5221 assert_eq!(
5222 reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
5223 "*x*"
5224 );
5225 assert_eq!(
5226 reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
5227 "~~x~~"
5228 );
5229 let link = TextMark::default().link(LinkMark {
5230 url: "https://example.com".into(),
5231 ..Default::default()
5232 });
5233 assert_eq!(
5234 reconstruct_markdown("x", &[(0..1, link)], 0..1),
5235 "[x](https://example.com)"
5236 );
5237 }
5238
5239 #[test]
5240 fn reconstruct_markdown_nested_bold_italic() {
5241 let mark = TextMark::default().bold().italic();
5243 assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
5245 }
5246
5247 fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
5251 let combined: String = children.iter().map(|c| c.text.to_string()).collect();
5252 let paragraph = Paragraph {
5253 span: None,
5254 children,
5255 link_refs: HashMap::new(),
5256 state: Arc::new(Mutex::new(InlineState::default())),
5257 render_cache: ParagraphRenderCache::default(),
5258 };
5259 if let Ok(mut state) = paragraph.state.lock() {
5260 state.set_text(combined.into());
5261 }
5262 paragraph
5263 }
5264
5265 fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
5266 if let Ok(mut state) = paragraph.state.lock() {
5267 state.selection = Some(range.into());
5268 }
5269 }
5270
5271 #[test]
5272 fn paragraph_selected_source_maps_partial_selection_across_runs() {
5273 let children = vec![
5275 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
5276 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5277 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
5278 ];
5279 let paragraph = paragraph_with_children(children);
5280
5281 set_paragraph_selection(¶graph, 0..(9 + 4 + 6));
5283 assert_eq!(paragraph.selected_source(), "This has **bold** text.");
5284
5285 set_paragraph_selection(¶graph, 5..16);
5288 assert_eq!(paragraph.selected_source(), "has **bold** te");
5289
5290 set_paragraph_selection(¶graph, 10..12);
5292 assert_eq!(paragraph.selected_source(), "**ol**");
5293 }
5294
5295 #[test]
5296 fn paragraph_selected_source_matches_text_when_no_marks() {
5297 let children =
5298 vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
5299 let paragraph = paragraph_with_children(children);
5300 set_paragraph_selection(¶graph, 0..11);
5301 assert_eq!(paragraph.selected_source(), "plain words");
5302 assert_eq!(paragraph.selected_text(), "plain words");
5303 }
5304
5305 fn copy_across_styles(blocks: Vec<BlockNode>) -> String {
5308 let document = crate::text::document::ParsedDocument {
5309 blocks: blocks.into(),
5310 ..Default::default()
5311 };
5312 document.selected_text(SelectionFormat::SourceAcrossStyles, None)
5313 }
5314
5315 #[test]
5316 fn source_across_styles_copies_plain_text_within_one_style() {
5317 let paragraph = || {
5319 paragraph_with_children(vec![
5320 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
5321 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5322 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
5323 ])
5324 };
5325 let copy = |range: Range<usize>| {
5326 let paragraph = paragraph();
5327 set_paragraph_selection(¶graph, range);
5328 copy_across_styles(vec![BlockNode::Paragraph(paragraph)])
5329 };
5330
5331 assert_eq!(copy(10..12), "ol\n");
5333 assert_eq!(copy(9..13), "bold\n");
5334 assert_eq!(copy(0..9), "This has \n");
5336 assert_eq!(copy(5..16), "has **bold** te");
5338 assert_eq!(copy(8..10), " **b**");
5339 }
5340
5341 #[test]
5342 fn source_across_styles_drops_the_url_of_a_link_selected_alone() {
5343 let link = TextMark::default().link(LinkMark {
5344 url: "https://example.com".into(),
5345 ..Default::default()
5346 });
5347 let paragraph = paragraph_with_children(vec![
5348 InlineNode::new("see ").marks(vec![(0..4, TextMark::default())]),
5349 InlineNode::new("the docs").marks(vec![(0..8, link)]),
5350 ]);
5351
5352 set_paragraph_selection(¶graph, 4..12);
5353 assert_eq!(
5354 copy_across_styles(vec![BlockNode::Paragraph(paragraph.clone())]),
5355 "the docs\n"
5356 );
5357
5358 set_paragraph_selection(¶graph, 0..12);
5359 assert_eq!(
5360 copy_across_styles(vec![BlockNode::Paragraph(paragraph)]),
5361 "see [the docs](https://example.com)"
5362 );
5363 }
5364
5365 #[test]
5366 fn source_across_styles_drops_the_markup_of_a_single_block() {
5367 let heading = BlockNode::Heading {
5368 level: 2,
5369 children: selected_paragraph("Title"),
5370 span: None,
5371 };
5372 assert_eq!(copy_across_styles(vec![heading]), "Title\n");
5373
5374 let code = CodeBlock::from_code("let a = 1;\nlet b = 2;", Some("rust"));
5375 code.set_selection(0..10);
5376 assert_eq!(
5377 copy_across_styles(vec![BlockNode::CodeBlock(code)]),
5378 "let a = 1;\n"
5379 );
5380
5381 let item = |text: &str| BlockNode::ListItem {
5382 children: vec![BlockNode::Paragraph(selected_paragraph(text))],
5383 spread: false,
5384 checked: None,
5385 span: None,
5386 };
5387 let unselected_item = BlockNode::ListItem {
5388 children: vec![BlockNode::Paragraph(paragraph_with_children(vec![
5389 InlineNode::new("two"),
5390 ]))],
5391 spread: false,
5392 checked: None,
5393 span: None,
5394 };
5395 let list = BlockNode::List {
5396 ordered: false,
5397 start: None,
5398 span: None,
5399 children: vec![item("one"), unselected_item],
5400 };
5401 assert_eq!(copy_across_styles(vec![list]), "one\n");
5402 }
5403
5404 #[test]
5405 fn source_across_styles_keeps_the_source_across_blocks() {
5406 let item = |text: &str| BlockNode::ListItem {
5407 children: vec![BlockNode::Paragraph(selected_paragraph(text))],
5408 spread: false,
5409 checked: None,
5410 span: None,
5411 };
5412 let list = BlockNode::List {
5413 ordered: false,
5414 start: None,
5415 span: None,
5416 children: vec![item("one"), item("two")],
5417 };
5418 assert_eq!(copy_across_styles(vec![list]), "- one\n- two");
5419
5420 assert_eq!(
5421 copy_across_styles(vec![
5422 BlockNode::Paragraph(selected_paragraph("start")),
5423 BlockNode::Paragraph(selected_paragraph("end")),
5424 ]),
5425 "start\n\nend"
5426 );
5427 }
5428
5429 #[test]
5430 fn source_across_styles_keeps_the_source_of_an_inline_object() {
5431 let formula = InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"));
5432 *formula.custom_selection.lock().unwrap() = true;
5433 let paragraph = Paragraph {
5434 children: vec![formula],
5435 ..Default::default()
5436 };
5437 assert_eq!(
5438 copy_across_styles(vec![BlockNode::Paragraph(paragraph)]),
5439 "$x$"
5440 );
5441 }
5442
5443 fn selected_paragraph(text: &str) -> Paragraph {
5444 let len = text.len();
5445 let paragraph = paragraph_with_children(vec![
5446 InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
5447 ]);
5448 set_paragraph_selection(¶graph, 0..len);
5449 paragraph
5450 }
5451
5452 #[test]
5453 fn heading_selected_source_prefixes_hashes() {
5454 let heading = BlockNode::Heading {
5455 level: 2,
5456 children: selected_paragraph("Title"),
5457 span: None,
5458 };
5459 assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
5460 assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
5462 }
5463
5464 #[test]
5465 fn unordered_list_selected_source_prefixes_dash() {
5466 let list = BlockNode::List {
5467 ordered: false,
5468 start: None,
5469 span: None,
5470 children: vec![
5471 BlockNode::ListItem {
5472 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
5473 spread: false,
5474 checked: None,
5475 span: None,
5476 },
5477 BlockNode::ListItem {
5478 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
5479 spread: false,
5480 checked: None,
5481 span: None,
5482 },
5483 ],
5484 };
5485 assert_eq!(
5486 list.selected_text(SelectionFormat::Source),
5487 "- one\n- two\n"
5488 );
5489 }
5490
5491 #[test]
5492 fn ordered_list_selected_source_preserves_start() {
5493 let list = BlockNode::List {
5494 ordered: true,
5495 start: Some(3),
5496 span: None,
5497 children: vec![
5498 BlockNode::ListItem {
5499 children: vec![BlockNode::Paragraph(selected_paragraph("three"))],
5500 spread: false,
5501 checked: None,
5502 span: None,
5503 },
5504 BlockNode::ListItem {
5505 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
5506 spread: false,
5507 checked: None,
5508 span: None,
5509 },
5510 ],
5511 };
5512 assert_eq!(list.to_markdown(), "3. three\n4. four");
5513 assert_eq!(
5514 list.selected_text(SelectionFormat::Source),
5515 "3. three\n4. four\n"
5516 );
5517 }
5518
5519 #[test]
5520 fn ordered_list_selected_source_preserves_nested_starts_and_continuations() {
5521 let nested = BlockNode::List {
5522 ordered: true,
5523 start: Some(4),
5524 span: None,
5525 children: vec![
5526 BlockNode::ListItem {
5527 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
5528 spread: false,
5529 checked: None,
5530 span: None,
5531 },
5532 BlockNode::ListItem {
5533 children: vec![BlockNode::Paragraph(selected_paragraph("again"))],
5534 spread: false,
5535 checked: None,
5536 span: None,
5537 },
5538 ],
5539 };
5540 let nested_list = BlockNode::List {
5541 ordered: true,
5542 start: Some(3),
5543 span: None,
5544 children: vec![
5545 BlockNode::ListItem {
5546 children: vec![BlockNode::Paragraph(selected_paragraph("three")), nested],
5547 spread: false,
5548 checked: None,
5549 span: None,
5550 },
5551 BlockNode::ListItem {
5552 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
5553 spread: false,
5554 checked: None,
5555 span: None,
5556 },
5557 ],
5558 };
5559 assert_eq!(
5560 nested_list.selected_text(SelectionFormat::Source),
5561 "3. three\n 4. nested\n 5. again\n4. four\n"
5562 );
5563
5564 let loose_list = BlockNode::List {
5565 ordered: true,
5566 start: Some(3),
5567 span: None,
5568 children: vec![
5569 BlockNode::ListItem {
5570 children: vec![
5571 BlockNode::Paragraph(selected_paragraph("loose")),
5572 BlockNode::Paragraph(selected_paragraph("continuation")),
5573 ],
5574 spread: true,
5575 checked: None,
5576 span: None,
5577 },
5578 BlockNode::ListItem {
5579 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
5580 spread: false,
5581 checked: None,
5582 span: None,
5583 },
5584 ],
5585 };
5586 assert_eq!(
5587 loose_list.selected_text(SelectionFormat::Source),
5588 "3. loose\n continuation\n4. four\n"
5589 );
5590 }
5591
5592 #[test]
5593 fn nested_list_selected_source_indents_sublists() {
5594 let nested = BlockNode::List {
5598 ordered: false,
5599 start: None,
5600 span: None,
5601 children: vec![BlockNode::ListItem {
5602 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
5603 spread: false,
5604 checked: None,
5605 span: None,
5606 }],
5607 };
5608 let list = BlockNode::List {
5609 ordered: false,
5610 start: None,
5611 span: None,
5612 children: vec![
5613 BlockNode::ListItem {
5614 children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
5615 spread: false,
5616 checked: None,
5617 span: None,
5618 },
5619 BlockNode::ListItem {
5620 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
5621 spread: false,
5622 checked: None,
5623 span: None,
5624 },
5625 ],
5626 };
5627 assert_eq!(
5628 list.selected_text(SelectionFormat::Source),
5629 "- one\n - nested\n- two\n"
5630 );
5631 }
5632
5633 #[test]
5634 fn task_list_selected_source_restores_checkboxes() {
5635 let list = BlockNode::List {
5636 ordered: false,
5637 start: None,
5638 span: None,
5639 children: vec![
5640 BlockNode::ListItem {
5641 children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
5642 spread: false,
5643 checked: Some(true),
5644 span: None,
5645 },
5646 BlockNode::ListItem {
5647 children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
5648 spread: false,
5649 checked: Some(false),
5650 span: None,
5651 },
5652 ],
5653 };
5654 assert_eq!(
5655 list.selected_text(SelectionFormat::Source),
5656 "- [x] done\n- [ ] todo\n"
5657 );
5658 }
5659
5660 #[test]
5661 fn blockquote_selected_source_prefixes_gt() {
5662 let quote = BlockNode::Blockquote {
5663 span: None,
5664 children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
5665 };
5666 assert_eq!(
5667 quote.selected_text(SelectionFormat::Source),
5668 "> quoted text\n"
5669 );
5670 }
5671
5672 #[test]
5673 fn table_selected_source_pipes_cells_with_alignment_row() {
5674 let cell = |text: &str| TableCell {
5675 children: selected_paragraph(text),
5676 width: None,
5677 };
5678 let table = Table {
5679 children: vec![
5680 TableRow {
5681 children: vec![cell("Name"), cell("Age")],
5682 },
5683 TableRow {
5684 children: vec![cell("Alice"), cell("30")],
5685 },
5686 ],
5687 column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5688 span: None,
5689 table_data_cache: TableDataCache::default(),
5690 column_widths_cache: TableColumnWidthsCache::default(),
5691 };
5692 let block = BlockNode::Table(table);
5693 assert_eq!(
5694 block.selected_text(SelectionFormat::Source),
5695 "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
5696 );
5697 }
5698
5699 #[test]
5702 fn a_selection_in_one_table_cell_copies_only_that_cell() {
5703 let table = table_of(
5704 vec![
5705 vec![plain_cell("Name"), plain_cell("Age")],
5706 vec![
5707 TableCell {
5708 children: selected_paragraph("Alice"),
5709 width: None,
5710 },
5711 plain_cell("30"),
5712 ],
5713 ],
5714 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5715 );
5716 let block = BlockNode::Table(table);
5717 assert_eq!(block.selected_text(SelectionFormat::Plain).trim(), "Alice");
5718 assert_eq!(block.selected_text(SelectionFormat::Source), "Alice\n");
5719 }
5720
5721 fn plain_cell(text: &str) -> TableCell {
5724 TableCell {
5725 children: Paragraph::new(text.to_string()),
5726 width: None,
5727 }
5728 }
5729
5730 fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
5731 Table {
5732 children: rows
5733 .into_iter()
5734 .map(|children| TableRow { children })
5735 .collect(),
5736 column_aligns,
5737 span: None,
5738 table_data_cache: TableDataCache::default(),
5739 column_widths_cache: TableColumnWidthsCache::default(),
5740 }
5741 }
5742
5743 #[test]
5744 fn table_to_markdown_pipes_cells_with_alignment_row() {
5745 let table = table_of(
5746 vec![
5747 vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
5748 vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
5749 ],
5750 vec![
5751 ColumnumnAlign::Left,
5752 ColumnumnAlign::Center,
5753 ColumnumnAlign::Right,
5754 ],
5755 );
5756
5757 assert_eq!(
5758 table.to_markdown(),
5759 "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
5760 );
5761 assert_eq!(
5763 BlockNode::Table(table.clone()).to_markdown(),
5764 table.to_markdown()
5765 );
5766 }
5767
5768 #[test]
5769 fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
5770 let table = table_of(
5771 vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
5772 vec![ColumnumnAlign::Left],
5773 );
5774
5775 assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
5776 }
5777
5778 #[test]
5779 fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
5780 let bold = TableCell {
5781 children: paragraph_with_children(vec![
5782 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5783 ]),
5784 width: None,
5785 };
5786 let table = table_of(
5787 vec![
5788 vec![plain_cell("a | b"), plain_cell("plain")],
5789 vec![plain_cell("c"), bold],
5790 ],
5791 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
5792 );
5793
5794 assert_eq!(
5795 table.to_markdown(),
5796 "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
5797 );
5798 }
5799
5800 #[test]
5801 fn table_data_snapshots_plain_cells_and_markdown() {
5802 let mut table = table_of(
5803 vec![
5804 vec![plain_cell(" Name "), plain_cell("Age")],
5805 vec![plain_cell("Alice"), plain_cell("30")],
5806 ],
5807 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
5808 );
5809 table.span = Some(Span { start: 4, end: 42 });
5810
5811 let data = table.table_data();
5812 assert_eq!(data.headers, vec!["Name", "Age"]);
5813 assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
5814 assert_eq!(data.markdown, table.to_markdown());
5815 assert_eq!(data.span, Some(4..42));
5816 }
5817
5818 #[test]
5819 fn cloned_table_keeps_cached_table_data() {
5820 let table = table_of(
5823 vec![vec![plain_cell("Name")], vec![plain_cell("Alice")]],
5824 vec![ColumnumnAlign::Left],
5825 );
5826 let data = table.cached_table_data();
5827
5828 let cloned = table.clone();
5829 assert!(Arc::ptr_eq(&data, &cloned.cached_table_data()));
5830 }
5831
5832 #[test]
5833 fn table_data_handles_tables_without_rows() {
5834 let header_only = table_of(
5836 vec![vec![plain_cell("Name"), plain_cell("Age")]],
5837 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
5838 );
5839 let data = header_only.table_data();
5840 assert_eq!(data.headers, vec!["Name", "Age"]);
5841 assert!(data.rows.is_empty());
5842 assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
5843
5844 assert_eq!(Table::default().table_data(), TableData::default());
5846 }
5847
5848 #[test]
5849 fn test_image_node_source() {
5850 use gpui::{ImageFormat, ImageSource, Resource};
5851
5852 fn image_node(url: &str) -> ImageNode {
5853 ImageNode {
5854 url: url.into(),
5855 ..Default::default()
5856 }
5857 }
5858
5859 fn assert_uri(url: &str) {
5863 match image_node(url).source() {
5864 ImageSource::Resource(Resource::Uri(uri)) => assert_eq!(uri.as_ref(), url),
5865 _ => panic!("expected Uri for {url:?}"),
5866 }
5867 }
5868 assert_uri("https://example.com/logo.png");
5869 assert_uri("http://example.com/logo.png");
5870 assert_uri("website/public/logo.svg");
5871 assert_uri("./images/a.png");
5872 assert_uri("../images/a.png");
5873 assert_uri("/absolute/path/logo.svg");
5874 assert_uri("file:///absolute/path/logo.svg");
5875 assert_uri(r"C:\images\logo.png");
5876 assert_uri("docs/a:b.png");
5877 assert_uri("data:text/plain;base64,aGVsbG8=");
5878
5879 let node = image_node("data:image/png;base64,iVBORw0KGgo=");
5882 let ImageSource::Image(first) = node.source() else {
5883 panic!("expected an embedded image");
5884 };
5885 assert_eq!(first.format(), ImageFormat::Png);
5886 assert_eq!(first.bytes(), b"\x89PNG\r\n\x1a\n");
5887 let ImageSource::Image(second) = node.source() else {
5888 panic!("expected an embedded image");
5889 };
5890 assert!(Arc::ptr_eq(&first, &second));
5891 }
5892
5893 fn image_paragraph(alt: &str, url: &str) -> Paragraph {
5894 let image = ImageNode {
5895 url: url.into(),
5896 alt: Some(alt.into()),
5897 ..Default::default()
5898 };
5899 Paragraph {
5900 span: None,
5901 children: vec![InlineNode::image(image)],
5902 link_refs: HashMap::new(),
5903 state: Arc::new(Mutex::new(InlineState::default())),
5904 render_cache: ParagraphRenderCache::default(),
5905 }
5906 }
5907
5908 #[test]
5911 fn marks_round_trip_through_reconstruction() {
5912 let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
5913
5914 assert_eq!(wrap(TextMark::default().bold()), "**x**");
5915 assert_eq!(wrap(TextMark::default().italic()), "*x*");
5916 assert_eq!(wrap(TextMark::default().code()), "`x`");
5917 assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
5918 assert_eq!(
5919 wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
5920 "==x=="
5921 );
5922 assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
5924
5925 assert_eq!(
5927 wrap(TextMark::default().link(LinkMark {
5928 url: "https://example.com".into(),
5929 title: Some("Tip".into()),
5930 ..Default::default()
5931 })),
5932 "[x](https://example.com \"Tip\")"
5933 );
5934 }
5935
5936 #[test]
5939 fn document_selected_source_slices_covered_blocks_from_the_source() {
5940 use crate::text::document::ParsedDocument;
5941
5942 let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
5945 let list = "3. _one_\n4. two";
5946 let list_start = source.find(list).unwrap();
5947 let rule_start = source.find("---").unwrap();
5948
5949 let document = ParsedDocument {
5950 source: source.into(),
5951 blocks: vec![
5952 BlockNode::Paragraph(selected_paragraph("start")),
5953 BlockNode::List {
5954 ordered: true,
5955 start: Some(3),
5956 children: vec![],
5957 span: Some(Span {
5958 start: list_start,
5959 end: list_start + list.len(),
5960 }),
5961 },
5962 BlockNode::HorizontalRule {
5963 span: Some(Span {
5964 start: rule_start,
5965 end: rule_start + 3,
5966 }),
5967 },
5968 BlockNode::Paragraph(selected_paragraph("end")),
5969 ]
5970 .into(),
5971 };
5972
5973 assert_eq!(
5974 document.selected_text(SelectionFormat::Source, None),
5975 "start\n\n3. _one_\n4. two\n\n---\n\nend"
5976 );
5977 }
5978
5979 #[test]
5980 fn document_selected_source_includes_enclosed_image() {
5981 use crate::text::document::ParsedDocument;
5982
5983 let source = "before\n\n\n\nafter";
5987 let image_markdown = "";
5988 let start = source.find(image_markdown).unwrap();
5989 let mut image = image_paragraph("alt", "https://example.com/i.png");
5990 image.span = Some(Span {
5991 start,
5992 end: start + image_markdown.len(),
5993 });
5994
5995 let document = ParsedDocument {
5996 source: source.into(),
5997 blocks: vec![
5998 BlockNode::Paragraph(selected_paragraph("before")),
5999 BlockNode::Paragraph(image),
6000 BlockNode::Paragraph(selected_paragraph("after")),
6001 ]
6002 .into(),
6003 };
6004 assert_eq!(
6005 document.selected_text(SelectionFormat::Source, None),
6006 "before\n\n\n\nafter"
6007 );
6008 }
6009
6010 #[test]
6011 fn document_selected_source_drops_unenclosed_image() {
6012 use crate::text::document::ParsedDocument;
6013
6014 let document = ParsedDocument {
6017 source: String::new().into(),
6018 blocks: vec![
6019 BlockNode::Paragraph(selected_paragraph("before")),
6020 BlockNode::Paragraph(image_paragraph("alt", "u")),
6021 ]
6022 .into(),
6023 };
6024 assert_eq!(
6025 document.selected_text(SelectionFormat::Source, None),
6026 "before"
6027 );
6028 }
6029
6030 fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
6031 let block = CodeBlock::new(
6032 code.to_string().into(),
6033 lang.map(|l| l.to_string().into()),
6034 None::<Span>,
6035 );
6036 if let Ok(mut state) = block.state.lock() {
6037 let len = state.text.len();
6038 state.selection = Some((0..len).into());
6039 }
6040 BlockNode::CodeBlock(block)
6041 }
6042
6043 #[test]
6044 fn code_block_selected_source_wraps_in_fence_with_lang() {
6045 let block = selected_code_block("let x = 1;\n", Some("rust"));
6046 let code = block.selected_text(SelectionFormat::Plain);
6047 let code_trimmed = code.trim_end_matches('\n');
6048 assert_eq!(
6052 block.selected_text(SelectionFormat::Source),
6053 format!("```rust\n{}\n```\n", code_trimmed)
6054 );
6055 assert!(
6056 block
6057 .selected_text(SelectionFormat::Source)
6058 .starts_with("```rust\n")
6059 );
6060 assert!(
6061 block
6062 .selected_text(SelectionFormat::Source)
6063 .trim_end()
6064 .ends_with("\n```")
6065 );
6066 }
6067
6068 #[test]
6069 fn code_block_selected_source_without_lang() {
6070 let block = selected_code_block("plain\n", None);
6071 let code_trimmed = block.selected_text(SelectionFormat::Plain);
6072 let code_trimmed = code_trimmed.trim_end_matches('\n');
6073 assert_eq!(
6074 block.selected_text(SelectionFormat::Source),
6075 format!("```\n{}\n```\n", code_trimmed)
6076 );
6077 }
6078
6079 #[test]
6080 fn document_selected_source_joins_blocks_with_blank_line() {
6081 use crate::text::document::ParsedDocument;
6082
6083 let document = ParsedDocument {
6087 source: String::new().into(),
6088 blocks: vec![
6089 BlockNode::Heading {
6090 level: 1,
6091 children: selected_paragraph("Title"),
6092 span: None,
6093 },
6094 BlockNode::Paragraph(selected_paragraph("A paragraph.")),
6095 selected_code_block("let x = 1;\n", Some("rust")),
6096 BlockNode::List {
6097 ordered: true,
6098 start: Some(1),
6099 span: None,
6100 children: vec![
6101 BlockNode::ListItem {
6102 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
6103 spread: false,
6104 checked: None,
6105 span: None,
6106 },
6107 BlockNode::ListItem {
6108 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
6109 spread: false,
6110 checked: None,
6111 span: None,
6112 },
6113 ],
6114 },
6115 ]
6116 .into(),
6117 };
6118
6119 assert_eq!(
6120 document.selected_text(SelectionFormat::Source, None),
6121 "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
6122 );
6123 }
6124
6125 #[test]
6126 fn code_block_equality_includes_code_content() {
6127 let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
6128 let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
6129
6130 assert_ne!(first, second);
6131 }
6132}