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, TaskToggleEvent, TaskToggleHandlerFn,
24 document::{NodeRenderOptions, PrevBlock, flow_position},
25 inline::{
26 Inline, InlineHighlight, InlineState, code_insets, combine_highlights, fade_highlights,
27 foreground_highlights, 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 Surface {
76 name: SharedString,
78 children: Vec<BlockNode>,
79 span: Option<Span>,
81 },
82 List {
83 children: Vec<BlockNode>,
85 ordered: bool,
86 start: Option<u32>,
88 span: Option<Span>,
89 },
90 ListItem {
91 children: Vec<BlockNode>,
92 spread: bool,
93 checked: Option<bool>,
95 task_marker: Option<Span>,
97 span: Option<Span>,
98 },
99 CodeBlock(CodeBlock),
100 Custom {
102 node: MarkdownNode,
103 state: BlockState,
104 },
105 Table(Table),
106 Break {
107 html: bool,
108 span: Option<Span>,
109 },
110 HorizontalRule {
111 span: Option<Span>,
112 },
113 Definition {
115 identifier: SharedString,
116 url: SharedString,
117 title: Option<SharedString>,
118 span: Option<Span>,
119 },
120 Unknown,
121}
122
123#[derive(Debug, Clone, Default)]
128pub struct BlockState {
129 selection: Arc<Mutex<Option<BlockSelection>>>,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134enum BlockSelection {
135 Empty,
136 Full,
137}
138
139impl BlockState {
140 fn selection(&self) -> Option<BlockSelection> {
141 self.selection.lock().ok().and_then(|selection| *selection)
142 }
143
144 fn set_selection(&self, value: Option<BlockSelection>) {
145 if let Ok(mut selection) = self.selection.lock() {
146 *selection = value;
147 }
148 }
149
150 fn is_selected(&self) -> bool {
151 self.selection() == Some(BlockSelection::Full)
152 }
153
154 fn has_selection_observation(&self) -> bool {
155 self.selection().is_some()
156 }
157
158 fn clear_selection(&self) {
159 self.set_selection(None);
160 }
161}
162
163impl PartialEq for BlockState {
164 fn eq(&self, _: &Self) -> bool {
165 true
167 }
168}
169
170pub(super) struct CustomBlockSelectionProjection {
171 state: BlockState,
172 bounds: Bounds<Pixels>,
173}
174
175impl CustomBlockSelectionProjection {
176 pub(super) fn project(&self, start: Point<Pixels>, end: Point<Pixels>) {
177 self.state
178 .set_selection(custom_block_selection(self.bounds, start, end));
179 }
180
181 pub(super) fn deselect(&self) {
182 self.state.set_selection(None);
183 }
184}
185
186fn custom_block_selection(
187 bounds: Bounds<Pixels>,
188 start: Point<Pixels>,
189 end: Point<Pixels>,
190) -> Option<BlockSelection> {
191 if bounds.size.width <= Pixels::ZERO
192 || bounds.size.height <= Pixels::ZERO
193 || bounds.bottom() <= start.y.min(end.y)
194 || bounds.top() > start.y.max(end.y)
195 {
196 return None;
197 }
198 Some(if custom_block_is_selected(bounds, start, end) {
199 BlockSelection::Full
200 } else {
201 BlockSelection::Empty
202 })
203}
204
205struct TableCellElement(AnyElement);
208
209impl IntoElement for TableCellElement {
210 type Element = Self;
211
212 fn into_element(self) -> Self {
213 self
214 }
215}
216
217impl Element for TableCellElement {
218 type RequestLayoutState = ();
219 type PrepaintState = ();
220
221 fn id(&self) -> Option<ElementId> {
222 None
223 }
224
225 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
226 None
227 }
228
229 fn request_layout(
230 &mut self,
231 _: Option<&GlobalElementId>,
232 _: Option<&InspectorElementId>,
233 window: &mut Window,
234 cx: &mut App,
235 ) -> (LayoutId, ()) {
236 (self.0.request_layout(window, cx), ())
237 }
238
239 fn prepaint(
240 &mut self,
241 _: Option<&GlobalElementId>,
242 _: Option<&InspectorElementId>,
243 bounds: Bounds<Pixels>,
244 _: &mut (),
245 window: &mut Window,
246 cx: &mut App,
247 ) {
248 if let Some(view) = GlobalState::global(cx).text_view_state() {
251 view.read(cx)
252 .selection_adapter
253 .record_table_cell(bounds, cx);
254 }
255 GlobalState::global(cx).push_table_cell(bounds);
256 self.0.prepaint(window, cx);
257 GlobalState::global(cx).pop_table_cell();
258 }
259
260 fn paint(
261 &mut self,
262 _: Option<&GlobalElementId>,
263 _: Option<&InspectorElementId>,
264 bounds: Bounds<Pixels>,
265 _: &mut (),
266 _: &mut (),
267 window: &mut Window,
268 cx: &mut App,
269 ) {
270 GlobalState::global(cx).push_table_cell(bounds);
271 self.0.paint(window, cx);
272 GlobalState::global(cx).pop_table_cell();
273 }
274}
275
276struct CustomBlockElement {
278 content: AnyElement,
279 state: BlockState,
280 reveal: Option<RevealAt>,
283}
284
285impl IntoElement for CustomBlockElement {
286 type Element = Self;
287
288 fn into_element(self) -> Self {
289 self
290 }
291}
292
293impl Element for CustomBlockElement {
294 type RequestLayoutState = ();
295 type PrepaintState = ();
296
297 fn id(&self) -> Option<ElementId> {
298 None
299 }
300
301 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
302 None
303 }
304
305 fn request_layout(
306 &mut self,
307 _: Option<&GlobalElementId>,
308 _: Option<&InspectorElementId>,
309 window: &mut Window,
310 cx: &mut App,
311 ) -> (LayoutId, ()) {
312 (self.content.request_layout(window, cx), ())
313 }
314
315 fn prepaint(
316 &mut self,
317 _: Option<&GlobalElementId>,
318 _: Option<&InspectorElementId>,
319 bounds: Bounds<Pixels>,
320 _: &mut (),
321 window: &mut Window,
322 cx: &mut App,
323 ) {
324 self.content.prepaint(window, cx);
325 if let Some(reveal) = &self.reveal {
326 let slack = px(0.5);
329 let view = window.content_mask().bounds;
330 let inside =
331 bounds.top() >= view.top() - slack && bounds.bottom() <= view.bottom() + slack;
332 let fills =
335 bounds.top() <= view.top() + slack && bounds.bottom() >= view.bottom() - slack;
336 let visible = inside || fills;
337 if !visible {
338 window.request_autoscroll(bounds);
339 }
340 reveal.report(bounds, visible);
341 }
342 }
343
344 fn paint(
345 &mut self,
346 _: Option<&GlobalElementId>,
347 _: Option<&InspectorElementId>,
348 bounds: Bounds<Pixels>,
349 _: &mut (),
350 _: &mut (),
351 window: &mut Window,
352 cx: &mut App,
353 ) {
354 let view = GlobalState::global(cx).text_view_state();
355 let selection = view.as_ref().and_then(|view| {
356 let state = view.read(cx);
357 if !state.is_selectable() {
358 return None;
359 }
360 if state.is_all_selected() {
361 return Some(BlockSelection::Full);
362 }
363 if state.preserve_inline_selection {
364 return self.state.selection();
365 }
366 let (start, end) = state.painted_selection_points(cx)?;
367 custom_block_selection(bounds, start, end)
368 });
369 self.state.set_selection(selection);
370 if let Some(view) = &view {
371 if view.read(cx).is_selectable() {
372 view.read(cx).selection_adapter.register_copy_projection(
373 super::selection_adapter::CopyProjection::CustomBlock(
374 CustomBlockSelectionProjection {
375 state: self.state.clone(),
376 bounds,
377 },
378 ),
379 cx,
380 );
381 }
382 let visible = bounds.intersect(&window.content_mask().bounds);
383 if view.read(cx).is_selectable()
384 && visible.size.width > Pixels::ZERO
385 && visible.size.height > Pixels::ZERO
386 {
387 view.read(cx)
388 .selection_adapter
389 .register_inline(vec![visible]);
390 }
391 }
392 self.content.paint(window, cx);
393 if selection == Some(BlockSelection::Full) {
394 let color = view.as_ref().unwrap().read(cx).text_view_style.selection();
395 window.paint_quad(gpui::fill(bounds, color));
396 }
397 }
398}
399
400fn custom_block_is_selected(
406 bounds: Bounds<Pixels>,
407 start: Point<Pixels>,
408 end: Point<Pixels>,
409) -> bool {
410 let (start_inside, end_inside) = (bounds.contains(&start), bounds.contains(&end));
411 start != end
412 && !(start_inside && end_inside)
413 && (start_inside
414 || end_inside
415 || point_in_text_selection(
416 bounds.origin,
417 bounds.size.width,
418 start,
419 end,
420 bounds.size.height,
421 ))
422}
423
424#[derive(Clone, Copy, Debug, PartialEq, Eq)]
428pub(super) enum SelectionStyles {
429 None,
430 One,
431 Mixed,
432}
433
434impl SelectionStyles {
435 fn merge(&mut self, other: Self) {
438 *self = match (*self, other) {
439 (Self::None, other) | (other, Self::None) => other,
440 _ => Self::Mixed,
441 };
442 }
443}
444
445#[derive(Clone, Copy)]
446enum BlockTextKind {
447 All,
448 Selected,
449 SelectedSource,
452}
453
454impl BlockNode {
455 pub(super) fn is_list_item(&self) -> bool {
456 matches!(self, Self::ListItem { .. })
457 }
458
459 fn draws_nothing(&self) -> bool {
461 match self {
462 Self::Definition { .. } | Self::Break { .. } | Self::Unknown => true,
463 Self::Root { children, .. } => children.iter().all(Self::draws_nothing),
464 Self::Paragraph(paragraph) => paragraph.children().iter().all(|inline| {
465 inline.as_image().is_none()
466 && inline.as_custom().is_none()
467 && inline.text().trim().is_empty()
468 }),
469 _ => false,
470 }
471 }
472
473 pub(super) fn compact(self) -> BlockNode {
475 match self {
476 Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
477 _ => self,
478 }
479 }
480
481 pub fn span(&self) -> Option<Span> {
483 match self {
484 BlockNode::Root { span, .. } => *span,
485 BlockNode::Paragraph(paragraph) => paragraph.span,
486 BlockNode::Heading { span, .. } => *span,
487 BlockNode::Blockquote { span, .. } => *span,
488 BlockNode::Surface { span, .. } => *span,
489 BlockNode::List { span, .. } => *span,
490 BlockNode::ListItem { span, .. } => *span,
491 BlockNode::CodeBlock(code_block) => code_block.span,
492 BlockNode::Custom { node, .. } => node.span,
493 BlockNode::Table(table) => table.span,
494 BlockNode::Break { span, .. } => *span,
495 BlockNode::HorizontalRule { span, .. } => *span,
496 BlockNode::Definition { span, .. } => *span,
497 BlockNode::Unknown { .. } => None,
498 }
499 }
500
501 pub(super) fn text(&self) -> String {
502 self.text_by_kind(BlockTextKind::All)
503 }
504
505 pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
510 self.text_by_kind(match format {
511 SelectionFormat::Plain => BlockTextKind::Selected,
512 SelectionFormat::Source | SelectionFormat::SourceAcrossStyles => {
513 BlockTextKind::SelectedSource
514 }
515 })
516 }
517
518 pub(super) fn selection_styles(&self) -> SelectionStyles {
520 let mut styles = SelectionStyles::None;
521 match self {
522 BlockNode::Root { children, .. }
523 | BlockNode::Blockquote { children, .. }
524 | BlockNode::Surface { children, .. }
525 | BlockNode::List { children, .. }
526 | BlockNode::ListItem { children, .. } => {
527 for child in children {
528 styles.merge(child.selection_styles());
529 }
530 }
531 BlockNode::Paragraph(paragraph) => styles = paragraph.selection_styles(),
532 BlockNode::Heading { children, .. } => styles = children.selection_styles(),
533 BlockNode::Table(table) => {
534 for row in &table.children {
535 for cell in &row.children {
536 styles.merge(cell.children.selection_styles());
537 }
538 }
539 }
540 BlockNode::CodeBlock(code_block) => {
541 if code_block.has_selection() {
542 styles = SelectionStyles::One;
543 }
544 }
545 BlockNode::Custom { state, .. } => {
547 if state.is_selected() {
548 styles = SelectionStyles::Mixed;
549 }
550 }
551 BlockNode::Definition { .. }
552 | BlockNode::Break { .. }
553 | BlockNode::HorizontalRule { .. }
554 | BlockNode::Unknown => {}
555 }
556 styles
557 }
558
559 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
560 let mut selected = SourceRangeSelection::Unselected;
561 match self {
562 BlockNode::Root { children, .. }
563 | BlockNode::Blockquote { children, .. }
564 | BlockNode::Surface { children, .. }
565 | BlockNode::List { children, .. }
566 | BlockNode::ListItem { children, .. } => {
567 for child in children {
568 selected.merge(child.selected_source_range());
569 }
570 }
571 BlockNode::Paragraph(paragraph) => selected = paragraph.selected_source_range(),
572 BlockNode::Heading { children, .. } => selected = children.selected_source_range(),
573 BlockNode::Table(table) => {
574 for row in &table.children {
575 for cell in &row.children {
576 selected.merge(cell.children.selected_source_range());
577 }
578 }
579 }
580 BlockNode::CodeBlock(code_block) => selected = code_block.selected_source_range(),
581 BlockNode::Custom { node, state } => {
582 if state.is_selected() {
583 selected = node
584 .source_range()
585 .map_or(SourceRangeSelection::Unmapped, SourceRangeSelection::Mapped);
586 }
587 }
588 BlockNode::Definition { .. }
589 | BlockNode::Break { .. }
590 | BlockNode::HorizontalRule { .. }
591 | BlockNode::Unknown => {}
592 }
593 selected
594 }
595
596 fn text_by_kind(&self, kind: BlockTextKind) -> String {
597 let mut text = String::new();
598 match self {
599 BlockNode::Root { children, .. } => {
600 let block_text = Self::children_text(children, kind);
601 if !block_text.is_empty() {
602 text.push_str(&block_text);
603 text.push('\n');
604 }
605 }
606 BlockNode::Paragraph(paragraph) => {
607 let block_text = match kind {
608 BlockTextKind::All => paragraph.text(),
609 BlockTextKind::Selected => paragraph.selected_text(),
610 BlockTextKind::SelectedSource => paragraph.selected_source(),
611 };
612 if !block_text.is_empty() {
613 text.push_str(&block_text);
614 text.push('\n');
615 }
616 }
617 BlockNode::Heading {
618 level, children, ..
619 } => {
620 let block_text = match kind {
621 BlockTextKind::All => children.text(),
622 BlockTextKind::Selected => children.selected_text(),
623 BlockTextKind::SelectedSource => children.selected_source(),
624 };
625 if !block_text.is_empty() {
626 if matches!(kind, BlockTextKind::SelectedSource) {
629 text.push_str(&"#".repeat(*level as usize));
630 text.push(' ');
631 }
632 text.push_str(&block_text);
633 text.push('\n');
634 }
635 }
636 BlockNode::List {
637 children,
638 ordered,
639 start,
640 ..
641 } => {
642 if matches!(kind, BlockTextKind::SelectedSource) {
643 text.push_str(&list_selected_source(children, *ordered, *start, ""));
646 } else {
647 text.push_str(&Self::children_text(children, kind));
648 }
649 }
650 BlockNode::ListItem { children, .. } => {
651 text.push_str(&Self::children_text(children, kind));
652 }
653 BlockNode::Blockquote { children, .. } => {
654 let block_text = Self::children_text(children, kind);
655
656 if !block_text.is_empty() {
657 if matches!(kind, BlockTextKind::SelectedSource) {
658 text.push_str("ed_source(None, &block_text));
659 } else {
660 text.push_str(&block_text);
661 }
662 text.push('\n');
663 }
664 }
665 BlockNode::Surface { name, children, .. } => {
666 let block_text = Self::children_text(children, kind);
667 if matches!(kind, BlockTextKind::SelectedSource) {
668 if !block_text.is_empty() {
671 text.push_str("ed_source(Some(name), &block_text));
672 text.push('\n');
673 }
674 } else {
675 text.push_str(&block_text);
676 }
677 }
678 BlockNode::Table(table) => {
679 if matches!(kind, BlockTextKind::SelectedSource) {
680 let block_text = table_selected_source(table);
681 if !block_text.is_empty() {
682 text.push_str(&block_text);
683 text.push('\n');
684 }
685 } else {
686 let mut block_text = String::new();
687 for row in table.children.iter() {
688 let mut row_texts = vec![];
689 for cell in row.children.iter() {
690 match kind {
691 BlockTextKind::All => row_texts.push(cell.children.text()),
692 _ => {
696 let selected = cell.children.selected_text();
697 if !selected.is_empty() {
698 row_texts.push(selected);
699 }
700 }
701 }
702 }
703 if !row_texts.is_empty() {
704 block_text.push_str(&row_texts.join(" "));
705 block_text.push('\n');
706 }
707 }
708
709 if !block_text.is_empty() {
710 text.push_str(&block_text);
711 text.push('\n');
712 }
713 }
714 }
715 BlockNode::CodeBlock(code_block) => {
716 let block_text = match kind {
717 BlockTextKind::All => code_block.text(),
718 BlockTextKind::Selected => code_block.selected_text(),
719 BlockTextKind::SelectedSource => code_block.selected_source(),
720 };
721 if !block_text.is_empty() {
722 text.push_str(&block_text);
723 text.push('\n');
724 }
725 }
726 BlockNode::Custom { node, state } => {
727 if matches!(kind, BlockTextKind::All) || state.is_selected() {
728 let content = match kind {
729 BlockTextKind::SelectedSource => Cow::Owned(node.to_markdown()),
730 _ => Cow::Borrowed(node.as_text()),
731 };
732 if !content.is_empty() {
733 text.push_str(&content);
734 text.push('\n');
735 }
736 }
737 }
738 BlockNode::Definition { .. }
739 | BlockNode::Break { .. }
740 | BlockNode::HorizontalRule { .. }
741 | BlockNode::Unknown { .. } => {}
742 }
743
744 text
745 }
746
747 fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
748 let mut text = String::new();
749 for child in children.iter() {
750 text.push_str(&child.text_by_kind(kind));
751 }
752
753 text
754 }
755
756 pub(super) fn has_selection(&self) -> bool {
767 match self {
768 BlockNode::Root { children, .. }
769 | BlockNode::Blockquote { children, .. }
770 | BlockNode::Surface { children, .. }
771 | BlockNode::List { children, .. }
772 | BlockNode::ListItem { children, .. } => {
773 children.iter().any(|child| child.has_selection())
774 }
775 BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
776 BlockNode::Heading { children, .. } => children.has_selection(),
777 BlockNode::Table(table) => table.children.iter().any(|row| {
778 row.children
779 .iter()
780 .any(|cell| cell.children.has_selection())
781 }),
782 BlockNode::CodeBlock(code_block) => code_block.has_selection(),
783 BlockNode::Custom { state, .. } => {
784 state.has_selection_observation()
786 }
787 BlockNode::Definition { .. }
788 | BlockNode::Break { .. }
789 | BlockNode::HorizontalRule { .. }
790 | BlockNode::Unknown { .. } => false,
791 }
792 }
793
794 pub(super) fn clear_selection(&self) {
795 match self {
796 BlockNode::Root { children, .. }
797 | BlockNode::Blockquote { children, .. }
798 | BlockNode::Surface { children, .. }
799 | BlockNode::List { children, .. }
800 | BlockNode::ListItem { children, .. } => {
801 for child in children.iter() {
802 child.clear_selection();
803 }
804 }
805 BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
806 BlockNode::Heading { children, .. } => children.clear_selection(),
807 BlockNode::Table(table) => {
808 for row in table.children.iter() {
809 for cell in row.children.iter() {
810 cell.children.clear_selection();
811 }
812 }
813 }
814 BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
815 BlockNode::Custom { state, .. } => {
816 state.clear_selection();
817 }
818 BlockNode::Definition { .. }
819 | BlockNode::Break { .. }
820 | BlockNode::HorizontalRule { .. }
821 | BlockNode::Unknown { .. } => {}
822 }
823 }
824}
825
826#[allow(unused)]
827#[derive(Debug, Default, Clone, PartialEq)]
828pub struct LinkMark {
829 pub url: SharedString,
830 pub identifier: Option<SharedString>,
832 pub title: Option<SharedString>,
833}
834
835#[derive(Debug, Default, Clone, PartialEq)]
836pub struct TextMark {
837 pub bold: bool,
838 pub italic: bool,
839 pub strikethrough: bool,
840 pub underline: bool,
841 pub code: bool,
842 pub highlight: Option<Hsla>,
846 pub link: Option<LinkMark>,
847}
848
849impl TextMark {
850 pub fn bold(mut self) -> Self {
851 self.bold = true;
852 self
853 }
854
855 pub fn italic(mut self) -> Self {
856 self.italic = true;
857 self
858 }
859
860 pub fn strikethrough(mut self) -> Self {
861 self.strikethrough = true;
862 self
863 }
864
865 pub fn underline(mut self) -> Self {
866 self.underline = true;
867 self
868 }
869
870 pub fn code(mut self) -> Self {
871 self.code = true;
872 self
873 }
874
875 pub fn highlight(mut self, color: Hsla) -> Self {
877 self.highlight = Some(color);
878 self
879 }
880
881 pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
882 self.link = Some(link.into());
883 self
884 }
885
886 pub fn merge(&mut self, other: TextMark) {
887 self.bold |= other.bold;
888 self.italic |= other.italic;
889 self.strikethrough |= other.strikethrough;
890 self.underline |= other.underline;
891 self.code |= other.code;
892 if other.highlight.is_some() {
893 self.highlight = other.highlight;
894 }
895 if let Some(link) = other.link {
896 self.link = Some(link);
897 }
898 }
899}
900
901#[derive(Debug, Default, Copy, Clone, PartialEq)]
903pub struct Span {
904 pub start: usize,
905 pub end: usize,
906}
907
908#[allow(unused)]
909#[derive(Default, Clone)]
910pub struct ImageNode {
911 pub url: SharedUri,
912 pub link: Option<LinkMark>,
913 pub title: Option<SharedString>,
914 pub alt: Option<SharedString>,
915 pub width: Option<DefiniteLength>,
916 pub height: Option<DefiniteLength>,
917 pub(crate) span: Option<Span>,
918 pub(super) embedded: OnceLock<Option<Arc<Image>>>,
921}
922
923impl ImageNode {
924 pub fn title(&self) -> String {
925 self.title
926 .clone()
927 .unwrap_or_else(|| self.alt.clone().unwrap_or_default())
928 .to_string()
929 }
930
931 pub(super) fn source(&self) -> ImageSource {
939 match self.embedded.get_or_init(|| data_url_image(&self.url)) {
940 Some(image) => ImageSource::Image(image.clone()),
941 None => self.url.clone().into(),
942 }
943 }
944}
945
946impl std::fmt::Debug for ImageNode {
947 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
948 f.debug_struct("ImageNode")
949 .field("url", &self.url)
950 .field("link", &self.link)
951 .field("title", &self.title)
952 .field("alt", &self.alt)
953 .field("width", &self.width)
954 .field("height", &self.height)
955 .field("span", &self.span)
956 .finish()
957 }
958}
959
960impl PartialEq for ImageNode {
961 fn eq(&self, other: &Self) -> bool {
962 self.url == other.url
963 && self.link == other.link
964 && self.title == other.title
965 && self.alt == other.alt
966 && self.width == other.width
967 && self.height == other.height
968 && self.span == other.span
969 }
970}
971
972#[derive(Debug, Clone, PartialEq)]
973pub(crate) struct SourceSegment {
974 pub(crate) rendered: Range<usize>,
975 pub(crate) source: Range<usize>,
976 pub(crate) linear: bool,
981}
982
983pub(crate) enum SourceRangeSelection {
984 Unselected,
985 Mapped(Range<usize>),
986 Unmapped,
987}
988
989impl SourceRangeSelection {
990 pub(crate) fn merge(&mut self, other: Self) {
991 match (&mut *self, other) {
992 (_, Self::Unselected) => {}
993 (_, Self::Unmapped) => *self = Self::Unmapped,
994 (Self::Unselected, mapped @ Self::Mapped(_)) => *self = mapped,
995 (Self::Mapped(selected), Self::Mapped(range)) => {
996 selected.start = selected.start.min(range.start);
997 selected.end = selected.end.max(range.end);
998 }
999 (Self::Unmapped, Self::Mapped(_)) => {}
1000 }
1001 }
1002
1003 pub(crate) fn into_range(self) -> Option<Range<usize>> {
1004 match self {
1005 Self::Mapped(range) => Some(range),
1006 Self::Unselected | Self::Unmapped => None,
1007 }
1008 }
1009}
1010
1011fn source_range_for_segments(
1012 segments: &[SourceSegment],
1013 selection: Range<usize>,
1014) -> Option<Range<usize>> {
1015 fn mapped_source_start(segment: &SourceSegment, rendered_start: usize) -> usize {
1016 if segment.linear {
1017 segment.source.start + rendered_start.saturating_sub(segment.rendered.start)
1018 } else {
1019 segment.source.start
1020 }
1021 }
1022
1023 fn mapped_source_end(segment: &SourceSegment, rendered_end: usize) -> usize {
1024 if segment.linear {
1025 segment.source.start
1026 + rendered_end
1027 .min(segment.rendered.end)
1028 .saturating_sub(segment.rendered.start)
1029 } else {
1030 segment.source.end
1031 }
1032 }
1033
1034 if selection.start >= selection.end {
1035 return None;
1036 }
1037
1038 let mut overlapping = segments.iter().filter(|segment| {
1039 segment.rendered.start < selection.end && segment.rendered.end > selection.start
1040 });
1041 let first = overlapping.next()?;
1042 if first.rendered.start > selection.start {
1043 return None;
1044 }
1045 let mut rendered_end = first.rendered.end;
1046 let source_start = mapped_source_start(first, selection.start);
1047 let mut source_end = mapped_source_end(first, selection.end);
1048 for segment in overlapping {
1049 if segment.rendered.start > rendered_end {
1050 return None;
1051 }
1052 rendered_end = rendered_end.max(segment.rendered.end);
1053 source_end = mapped_source_end(segment, selection.end);
1054 }
1055 (rendered_end >= selection.end).then_some(source_start..source_end)
1056}
1057
1058#[derive(Default, Clone, Debug)]
1061pub struct InlineNode {
1062 pub(crate) text: SharedString,
1064 pub(crate) image: Option<ImageNode>,
1065 pub(crate) custom: Option<MarkdownNode>,
1066 custom_selection: Arc<Mutex<bool>>,
1067 pub(crate) marks: Vec<(Range<usize>, TextMark)>,
1069 pub(crate) source_segments: Vec<SourceSegment>,
1071
1072 pub(super) state: Arc<Mutex<InlineState>>,
1073}
1074
1075impl PartialEq for InlineNode {
1076 fn eq(&self, other: &Self) -> bool {
1077 self.text == other.text
1078 && self.image == other.image
1079 && self.custom == other.custom
1080 && self.marks == other.marks
1081 && self.source_segments == other.source_segments
1082 }
1083}
1084
1085fn set_leaf_key(state: &mut Arc<Mutex<InlineState>>, key: u64) {
1086 let text = state
1087 .lock()
1088 .map(|state| state.text.clone())
1089 .unwrap_or_default();
1090 *state = Arc::new(Mutex::new(InlineState {
1093 text,
1094 editor_key: Some(key),
1095 selection_rows: None,
1096 cleanup_senders: Vec::new(),
1097 ..Default::default()
1098 }));
1099}
1100
1101pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
1108 if text.is_empty() {
1109 return String::new();
1110 }
1111
1112 let mut out = text.to_string();
1113 if mark.code {
1114 out = format!("`{}`", out);
1115 }
1116 if mark.italic {
1117 out = format!("*{}*", out);
1118 }
1119 if mark.bold {
1120 out = format!("**{}**", out);
1121 }
1122 if mark.strikethrough {
1123 out = format!("~~{}~~", out);
1124 }
1125 if mark.underline {
1126 out = format!("<u>{}</u>", out);
1129 }
1130 if mark.highlight.is_some() {
1131 out = format!("=={}==", out);
1132 }
1133 if let Some(link) = &mark.link {
1134 out = match &link.title {
1135 Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
1136 None => format!("[{}]({})", out, link.url),
1137 };
1138 }
1139 out
1140}
1141
1142#[derive(Default)]
1146struct MarkdownSource {
1147 pieces: Vec<(String, Vec<TextMark>)>,
1148 has_objects: bool,
1151}
1152
1153impl MarkdownSource {
1154 fn push_str(&mut self, source: &str) {
1155 self.push_marked(source, &TextMark::default());
1156 }
1157
1158 fn push_marked(&mut self, source: &str, mark: &TextMark) {
1159 if source.is_empty() {
1160 return;
1161 }
1162 let mut layers = Vec::new();
1165 if let Some(link) = &mark.link {
1166 layers.push(TextMark {
1167 link: Some(link.clone()),
1168 ..Default::default()
1169 });
1170 }
1171 if let Some(highlight) = mark.highlight {
1172 layers.push(TextMark {
1173 highlight: Some(highlight),
1174 ..Default::default()
1175 });
1176 }
1177 if mark.underline {
1178 layers.push(TextMark::default().underline());
1179 }
1180 if mark.strikethrough {
1181 layers.push(TextMark::default().strikethrough());
1182 }
1183 if mark.bold {
1184 layers.push(TextMark::default().bold());
1185 }
1186 if mark.italic {
1187 layers.push(TextMark::default().italic());
1188 }
1189 if mark.code {
1190 layers.push(TextMark::default().code());
1191 }
1192 self.pieces.push((source.to_string(), layers));
1193 }
1194
1195 fn push_text(
1196 &mut self,
1197 text: &str,
1198 marks: &[(Range<usize>, TextMark)],
1199 selection: Range<usize>,
1200 ) {
1201 let start = selection.start.min(text.len());
1202 let end = selection.end.min(text.len());
1203 if start >= end {
1204 return;
1205 }
1206 let mut cursor = start;
1207 for (range, mark) in marks {
1208 let lo = range.start.max(start);
1209 let hi = range.end.min(end);
1210 if lo >= hi {
1211 continue;
1212 }
1213 if cursor < lo {
1214 self.push_str(&text[cursor..lo]);
1215 }
1216 self.push_marked(&text[lo..hi], mark);
1217 cursor = hi;
1218 }
1219 if cursor < end {
1220 self.push_str(&text[cursor..end]);
1221 }
1222 }
1223
1224 fn push_images(&mut self, images: &[String]) {
1225 if images.is_empty() {
1226 return;
1227 }
1228 self.has_objects = true;
1229 self.push_str(&images.join(""));
1230 }
1231
1232 fn push_object(&mut self, node: &InlineNode) {
1233 self.has_objects = true;
1234 let mut mark = TextMark::default();
1235 for (_, layer) in &node.marks {
1236 mark.merge(layer.clone());
1237 }
1238 self.push_marked(&node.custom.as_ref().unwrap().to_markdown(), &mark);
1239 }
1240
1241 fn is_empty(&self) -> bool {
1242 self.pieces.is_empty()
1243 }
1244
1245 fn styles(&self) -> SelectionStyles {
1246 if self.is_empty() {
1247 SelectionStyles::None
1248 } else if !self.has_objects && self.pieces.windows(2).all(|pair| pair[0].1 == pair[1].1) {
1249 SelectionStyles::One
1250 } else {
1251 SelectionStyles::Mixed
1252 }
1253 }
1254
1255 fn finish(mut self) -> String {
1256 fn write(pieces: &mut [(String, Vec<TextMark>)]) -> String {
1257 let mut out = String::new();
1258 let mut offset = 0;
1259 while offset < pieces.len() {
1260 let Some((mark, count)) = pieces[offset]
1261 .1
1262 .iter()
1263 .map(|mark| {
1264 let count = pieces[offset..]
1265 .iter()
1266 .take_while(|(_, layers)| layers.contains(mark))
1267 .count();
1268 (mark.clone(), count)
1269 })
1270 .reduce(|best, candidate| {
1271 if candidate.1 > best.1 {
1272 candidate
1273 } else {
1274 best
1275 }
1276 })
1277 else {
1278 out.push_str(&pieces[offset].0);
1279 offset += 1;
1280 continue;
1281 };
1282 let group = &mut pieces[offset..offset + count];
1283 for (_, layers) in group.iter_mut() {
1284 layers.retain(|layer| layer != &mark);
1285 }
1286 let content = write(group);
1287 out.push_str(&wrap_with_mark(&content, &mark));
1288 offset += count;
1289 }
1290 out
1291 }
1292 write(&mut self.pieces)
1293 }
1294}
1295
1296#[derive(Default)]
1299struct RunSelection {
1300 emitted: bool,
1301 at_start: bool,
1302 at_end: bool,
1303}
1304
1305fn emit_run(
1309 state: &Arc<Mutex<InlineState>>,
1310 run: &[(usize, &InlineNode)],
1311 pending_images: &mut Vec<String>,
1312 out: &mut MarkdownSource,
1313) -> RunSelection {
1314 let mut selected = RunSelection::default();
1315 let Ok(state) = state.lock() else {
1316 return selected;
1317 };
1318 let Some(selection) = &state.selection else {
1319 return selected;
1320 };
1321 if selection.start >= selection.end {
1322 return selected;
1323 }
1324
1325 selected.at_start = selection.start == 0;
1326 selected.at_end = selection.end >= state.text.len();
1327
1328 for (start, child) in run {
1329 let end = start + child.text.len();
1330 let lo = selection.start.max(*start);
1331 let hi = selection.end.min(end);
1332 if lo >= hi {
1333 continue;
1334 }
1335
1336 if !selected.emitted {
1337 if selected.at_start {
1338 out.push_images(pending_images);
1339 }
1340 pending_images.clear();
1341 }
1342 selected.emitted = true;
1343
1344 out.push_text(&child.text, &child.marks, (lo - start)..(hi - start));
1345 }
1346
1347 selected
1348}
1349
1350fn image_markdown(image: &ImageNode) -> String {
1352 let alt = image.alt.clone().unwrap_or_default();
1353 let title = image
1354 .title
1355 .clone()
1356 .map_or(String::new(), |title| format!(" \"{}\"", title));
1357 format!("", alt, image.url, title)
1358}
1359
1360#[cfg(test)]
1369pub(crate) fn reconstruct_markdown(
1370 text: &str,
1371 marks: &[(Range<usize>, TextMark)],
1372 selection: Range<usize>,
1373) -> String {
1374 let mut source = MarkdownSource::default();
1375 source.push_text(text, marks, selection);
1376 source.finish()
1377}
1378
1379fn table_selected_source(table: &Table) -> String {
1387 let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
1388
1389 let mut selected = table
1390 .children
1391 .iter()
1392 .flat_map(|row| row.children.iter())
1393 .map(cell_source)
1394 .filter(|source| !source.trim().is_empty());
1395 match (selected.next(), selected.next()) {
1396 (None, _) => return String::new(),
1397 (Some(only), None) => return only.trim().to_string(),
1398 _ => {}
1399 }
1400
1401 let mut lines: Vec<String> = Vec::new();
1402 for (row_ix, row) in table.children.iter().enumerate() {
1403 let cells: Vec<String> = row
1404 .children
1405 .iter()
1406 .map(|cell| cell_source(cell).trim().to_string())
1407 .collect();
1408 lines.push(format!("| {} |", cells.join(" | ")));
1409
1410 if row_ix == 0 {
1413 let aligns: Vec<String> = (0..row.children.len())
1414 .map(|ix| {
1415 match table.column_align(ix) {
1416 ColumnumnAlign::None | ColumnumnAlign::Left => ":--",
1417 ColumnumnAlign::Center => ":-:",
1418 ColumnumnAlign::Right => "--:",
1419 }
1420 .to_string()
1421 })
1422 .collect();
1423 lines.push(format!("| {} |", aligns.join(" | ")));
1424 }
1425 }
1426
1427 lines.join("\n")
1428}
1429
1430fn quoted_source(marker: Option<&SharedString>, text: &str) -> String {
1434 marker
1435 .map(|name| format!("[!{name}]"))
1436 .into_iter()
1437 .chain(text.trim_end_matches('\n').lines().map(str::to_string))
1438 .map(|line| {
1439 if line.is_empty() {
1440 ">".to_string()
1441 } else {
1442 format!("> {line}")
1443 }
1444 })
1445 .collect::<Vec<_>>()
1446 .join("\n")
1447}
1448
1449fn list_selected_source(
1459 children: &[BlockNode],
1460 ordered: bool,
1461 start: Option<u32>,
1462 indent: &str,
1463) -> String {
1464 let mut out = String::new();
1465 let mut item_ix = 0usize;
1466
1467 for child in children {
1468 let BlockNode::ListItem {
1469 children: item_children,
1470 checked,
1471 ..
1472 } = child
1473 else {
1474 continue;
1475 };
1476
1477 let marker = if ordered {
1478 format!("{}. ", ordered_list_ordinal(start, item_ix))
1479 } else {
1480 "- ".to_string()
1481 };
1482 let checkbox = match checked {
1483 Some(true) => "[x] ",
1484 Some(false) => "[ ] ",
1485 None => "",
1486 };
1487 let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
1488
1489 let mut content = String::new();
1492 let mut nested = String::new();
1493 for sub in item_children {
1494 if let BlockNode::List {
1495 children: sub_children,
1496 ordered: sub_ordered,
1497 start: sub_start,
1498 ..
1499 } = sub
1500 {
1501 nested.push_str(&list_selected_source(
1502 sub_children,
1503 *sub_ordered,
1504 *sub_start,
1505 &child_indent,
1506 ));
1507 } else {
1508 content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
1509 }
1510 }
1511 let content = content.trim_end_matches('\n');
1512
1513 if content.is_empty() && nested.is_empty() {
1514 item_ix += 1;
1515 continue;
1516 }
1517
1518 if content.is_empty() {
1519 out.push_str(indent);
1521 out.push_str(&marker);
1522 out.push_str(checkbox.trim_end());
1523 out.push('\n');
1524 } else {
1525 let mut lines = content.lines();
1528 if let Some(first) = lines.next() {
1529 out.push_str(indent);
1530 out.push_str(&marker);
1531 out.push_str(checkbox);
1532 out.push_str(first);
1533 out.push('\n');
1534 }
1535 for line in lines {
1536 out.push_str(&child_indent);
1537 out.push_str(line);
1538 out.push('\n');
1539 }
1540 }
1541 out.push_str(&nested);
1542 item_ix += 1;
1543 }
1544
1545 out
1546}
1547
1548impl InlineNode {
1549 pub fn new(text: impl Into<SharedString>) -> Self {
1551 Self {
1552 text: text.into(),
1553 image: None,
1554 custom: None,
1555 custom_selection: Arc::default(),
1556 marks: vec![],
1557 source_segments: vec![],
1558 state: Arc::new(Mutex::new(InlineState::default())),
1559 }
1560 }
1561
1562 pub fn custom(node: MarkdownNode) -> Self {
1564 let mut this = Self::new(node.as_text().to_string());
1565 this.custom = Some(node);
1566 this
1567 }
1568
1569 pub fn image(image: ImageNode) -> Self {
1571 let mut this = Self::new("");
1572 this.image = Some(image);
1573 this
1574 }
1575
1576 pub fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
1578 self.marks = marks;
1579 self
1580 }
1581
1582 pub fn text(&self) -> &str {
1584 &self.text
1585 }
1586
1587 pub fn as_image(&self) -> Option<&ImageNode> {
1589 self.image.as_ref()
1590 }
1591
1592 pub fn as_custom(&self) -> Option<&MarkdownNode> {
1594 self.custom.as_ref()
1595 }
1596
1597 pub fn mark_runs(&self) -> &[(Range<usize>, TextMark)] {
1599 &self.marks
1600 }
1601
1602 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
1603 self.source_segments = source_segments;
1604 self
1605 }
1606
1607 fn selected_source_range(&self, selection: Range<usize>) -> Option<Range<usize>> {
1608 source_range_for_segments(&self.source_segments, selection)
1609 }
1610}
1611
1612#[derive(Debug, Clone, Default)]
1617pub struct Paragraph {
1618 pub(super) span: Option<Span>,
1619 pub(super) children: Vec<InlineNode>,
1620 pub(super) link_refs: HashMap<SharedString, SharedString>,
1624
1625 pub(crate) state: Arc<Mutex<InlineState>>,
1626 pub(super) render_cache: ParagraphRenderCache,
1629}
1630
1631#[derive(Default)]
1639pub(super) struct ParagraphRenderCache(Mutex<Option<Arc<ParagraphRender>>>);
1640
1641impl Clone for ParagraphRenderCache {
1642 fn clone(&self) -> Self {
1643 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1644 Self(Mutex::new(cached))
1645 }
1646}
1647
1648impl std::fmt::Debug for ParagraphRenderCache {
1649 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1650 f.write_str("ParagraphRenderCache")
1651 }
1652}
1653
1654struct ParagraphRender {
1664 style: Arc<TextViewStyle>,
1665 mono_font: SharedString,
1666 background: Hsla,
1667 text: SharedString,
1668 highlights: Vec<(Range<usize>, InlineHighlight)>,
1669 links: Vec<(Range<usize>, LinkMark)>,
1671}
1672
1673impl PartialEq for Paragraph {
1674 fn eq(&self, other: &Self) -> bool {
1675 self.span == other.span
1676 && self.children == other.children
1677 && self.link_refs == other.link_refs
1678 }
1679}
1680
1681impl Paragraph {
1682 pub(crate) fn new(text: String) -> Self {
1683 Self {
1684 span: None,
1685 children: vec![InlineNode::new(&text)],
1686 link_refs: HashMap::new(),
1687 state: Arc::new(Mutex::new(InlineState::default())),
1688 render_cache: ParagraphRenderCache::default(),
1689 }
1690 }
1691
1692 fn plain_render(
1695 &self,
1696 node_cx: &NodeContext,
1697 cx: &App,
1698 ) -> (
1699 SharedString,
1700 Vec<(Range<usize>, InlineHighlight)>,
1701 Vec<(Range<usize>, LinkMark)>,
1702 ) {
1703 let mono_font = cx.theme().tokens.typography.mono.clone();
1704 let background = cx.theme().tokens.colors.background;
1705 if let Ok(cache) = self.render_cache.0.lock()
1706 && let Some(cached) = cache.as_ref()
1707 && (Arc::ptr_eq(&cached.style, &node_cx.style) || *cached.style == *node_cx.style)
1708 && cached.mono_font == mono_font
1709 && cached.background == background
1710 {
1711 return (
1712 cached.text.clone(),
1713 foreground_highlights(
1714 cached.highlights.clone(),
1715 &cached.text,
1716 0,
1717 node_cx.foregrounds(self.leaf_key()),
1718 ),
1719 cached.links.clone(),
1720 );
1721 }
1722
1723 let mut text = String::new();
1724 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1725 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1726 let mut offset = 0;
1727 for inline_node in &self.children {
1728 let text_len = inline_node.text.len();
1729 text.push_str(&inline_node.text);
1730 let mut node_highlights = vec![];
1731 for (range, style) in &inline_node.marks {
1732 let inner_range = (offset + range.start)..(offset + range.end);
1733 let mut highlight = mark_highlight(style, node_cx, cx);
1734 if let Some(link_mark) = style.link.clone() {
1735 highlight.style.color = Some(node_cx.style.link());
1736 highlight.style.underline = Some(link_underline(&node_cx.style));
1737 links.push((inner_range.clone(), link_mark));
1738 }
1739 node_highlights.push((inner_range, highlight));
1740 }
1741 append_node_highlights(&mut highlights, node_highlights);
1742 offset += text_len;
1743 }
1744 let text = SharedString::from(text);
1745 if let Ok(mut cache) = self.render_cache.0.lock() {
1746 *cache = Some(Arc::new(ParagraphRender {
1747 style: node_cx.style.clone(),
1748 mono_font,
1749 background,
1750 text: text.clone(),
1751 highlights: highlights.clone(),
1752 links: links.clone(),
1753 }));
1754 }
1755 let highlights =
1756 foreground_highlights(highlights, &text, 0, node_cx.foregrounds(self.leaf_key()));
1757 (text, highlights, links)
1758 }
1759
1760 pub(super) fn selected_text(&self) -> String {
1761 let mut text = String::new();
1762
1763 for c in self.children.iter() {
1764 let Ok(state) = c.state.lock() else {
1765 continue;
1766 };
1767 if let Some(selection) = &state.selection {
1768 text.push_str(&state.text[selection.start..selection.end]);
1769 }
1770 if let Some(custom) = &c.custom
1771 && c.custom_selection.lock().is_ok_and(|selected| *selected)
1772 {
1773 text.push_str(custom.as_text());
1774 }
1775 }
1776
1777 if let Ok(state) = self.state.lock()
1778 && let Some(selection) = &state.selection
1779 {
1780 text.push_str(&state.text[selection.start..selection.end]);
1781 }
1782
1783 text
1784 }
1785
1786 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1788 let mut selected = SourceRangeSelection::Unselected;
1789 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1790 let mut offset = 0;
1791 let mut pending_images: Vec<Option<Span>> = Vec::new();
1792 let mut enters_image = true;
1793
1794 let include_run = |state: &Arc<Mutex<InlineState>>,
1795 run: &[(usize, &InlineNode)]|
1796 -> (SourceRangeSelection, RunSelection) {
1797 let Ok(state) = state.lock() else {
1798 return (SourceRangeSelection::Unmapped, RunSelection::default());
1799 };
1800 let Some(selection) = state.selection else {
1801 return (SourceRangeSelection::Unselected, RunSelection::default());
1802 };
1803 if selection.start >= selection.end {
1804 return (SourceRangeSelection::Unselected, RunSelection::default());
1805 }
1806
1807 let mut run_selection = RunSelection {
1808 at_start: selection.start == 0,
1809 at_end: selection.end >= state.text.len(),
1810 ..Default::default()
1811 };
1812 let mut mapped = SourceRangeSelection::Unselected;
1813 let mut rendered_end = selection.start;
1814 for (start, child) in run {
1815 let end = start + child.text.len();
1816 let lo = selection.start.max(*start);
1817 let hi = selection.end.min(end);
1818 if lo >= hi {
1819 continue;
1820 }
1821 run_selection.emitted = true;
1822 if lo > rendered_end {
1823 return (SourceRangeSelection::Unmapped, run_selection);
1824 }
1825 let Some(range) = child.selected_source_range((lo - start)..(hi - start)) else {
1826 return (SourceRangeSelection::Unmapped, run_selection);
1827 };
1828 mapped.merge(SourceRangeSelection::Mapped(range));
1829 rendered_end = rendered_end.max(hi);
1830 }
1831 if rendered_end < selection.end {
1832 (SourceRangeSelection::Unmapped, run_selection)
1833 } else {
1834 (mapped, run_selection)
1835 }
1836 };
1837
1838 let merge_images = |selected: &mut SourceRangeSelection, images: &mut Vec<Option<Span>>| {
1839 for span in images.drain(..) {
1840 selected.merge(
1841 span.map(|span| SourceRangeSelection::Mapped(span.start..span.end))
1842 .unwrap_or(SourceRangeSelection::Unmapped),
1843 );
1844 }
1845 };
1846
1847 for child in &self.children {
1848 if child.custom.is_some() {
1849 let (run_range, run_selection) = include_run(&child.state, &run);
1850 if run_selection.emitted && run_selection.at_start {
1851 merge_images(&mut selected, &mut pending_images);
1852 }
1853 selected.merge(run_range);
1854
1855 match child.custom_selection.lock() {
1856 Ok(value) if *value => {
1857 if run.is_empty() || (run_selection.emitted && run_selection.at_end) {
1858 merge_images(&mut selected, &mut pending_images);
1859 }
1860 selected.merge(
1861 child
1862 .custom
1863 .as_ref()
1864 .and_then(MarkdownNode::source_range)
1865 .map(SourceRangeSelection::Mapped)
1866 .unwrap_or(SourceRangeSelection::Unmapped),
1867 );
1868 enters_image = true;
1869 }
1870 Ok(_) => enters_image = false,
1871 Err(_) => {
1872 selected.merge(SourceRangeSelection::Unmapped);
1873 enters_image = false;
1874 }
1875 }
1876 pending_images.clear();
1877 run.clear();
1878 offset = 0;
1879 continue;
1880 }
1881 if let Some(image) = &child.image {
1882 let run_before = !run.is_empty();
1883 let (run_range, run_selection) = include_run(&child.state, &run);
1884 if run_selection.emitted && run_selection.at_start {
1885 merge_images(&mut selected, &mut pending_images);
1886 }
1887 selected.merge(run_range);
1888 if run_before {
1889 enters_image = run_selection.emitted && run_selection.at_end;
1890 }
1891 if enters_image {
1892 pending_images.push(image.span);
1893 } else {
1894 pending_images.clear();
1895 }
1896 run.clear();
1897 offset = 0;
1898 continue;
1899 }
1900 run.push((offset, child));
1901 offset += child.text.len();
1902 }
1903
1904 let (trailing_range, trailing) = include_run(&self.state, &run);
1905 if trailing.emitted && trailing.at_start {
1906 merge_images(&mut selected, &mut pending_images);
1907 }
1908 selected.merge(trailing_range);
1909 if !trailing.emitted
1910 && enters_image
1911 && !matches!(selected, SourceRangeSelection::Unselected)
1912 {
1913 merge_images(&mut selected, &mut pending_images);
1914 }
1915 selected
1916 }
1917
1918 pub(super) fn selected_source(&self) -> String {
1938 self.selected_markdown().finish()
1939 }
1940
1941 pub(super) fn selection_styles(&self) -> SelectionStyles {
1943 self.selected_markdown().styles()
1944 }
1945
1946 fn selected_markdown(&self) -> MarkdownSource {
1949 let mut source = MarkdownSource::default();
1950 let mut pending_images: Vec<String> = Vec::new();
1951 let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1952 let mut offset = 0;
1953 let mut enters_image = true;
1954
1955 for child in self.children.iter() {
1956 if child.custom.is_some() {
1957 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1958 let object_selected = child
1959 .custom_selection
1960 .lock()
1961 .is_ok_and(|selected| *selected);
1962 if object_selected {
1963 if run.is_empty() || (selected.emitted && selected.at_end) {
1964 source.push_images(&pending_images);
1965 }
1966 source.push_object(child);
1967 }
1968 pending_images.clear();
1969 enters_image = object_selected;
1970 run.clear();
1971 offset = 0;
1972 continue;
1973 }
1974 let Some(image) = &child.image else {
1975 run.push((offset, child));
1976 offset += child.text.len();
1977 continue;
1978 };
1979
1980 let run_before = !run.is_empty();
1982 let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1983 if run_before {
1984 enters_image = selected.emitted && selected.at_end;
1985 }
1986 if enters_image {
1987 pending_images.push(image_markdown(image));
1988 } else {
1989 pending_images.clear();
1990 }
1991
1992 run.clear();
1993 offset = 0;
1994 }
1995
1996 let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
1997 if !trailing.emitted && enters_image && !source.is_empty() {
1999 source.push_images(&pending_images);
2000 }
2001
2002 source
2003 }
2004
2005 pub(super) fn text(&self) -> String {
2006 let mut text = String::new();
2007 for node in self.children.iter() {
2008 text.push_str(&node.text);
2009 }
2010 text
2011 }
2012
2013 pub(super) fn has_selection(&self) -> bool {
2017 self.children.iter().any(|c| {
2018 c.state.lock().is_ok_and(|state| state.selection.is_some())
2019 || c.custom_selection.lock().is_ok_and(|selected| *selected)
2020 }) || self
2021 .state
2022 .lock()
2023 .is_ok_and(|state| state.selection.is_some())
2024 }
2025
2026 pub(super) fn clear_selection(&self) {
2027 for c in self.children.iter() {
2028 if let Ok(mut selected) = c.custom_selection.lock() {
2029 *selected = false;
2030 }
2031 if let Ok(mut state) = c.state.lock() {
2032 state.selection = None;
2033 }
2034 }
2035
2036 if let Ok(mut state) = self.state.lock() {
2037 state.selection = None;
2038 }
2039 }
2040}
2041
2042#[derive(Debug, Clone, Default, PartialEq)]
2044pub struct Table {
2045 pub(crate) children: Vec<TableRow>,
2046 pub(crate) column_aligns: Vec<ColumnumnAlign>,
2047 pub(crate) span: Option<Span>,
2048 pub(crate) table_data_cache: TableDataCache,
2051 pub(crate) column_widths_cache: TableColumnWidthsCache,
2052}
2053
2054#[derive(Default)]
2062pub(crate) struct TableDataCache(Mutex<Option<Arc<TableData>>>);
2063
2064impl Clone for TableDataCache {
2065 fn clone(&self) -> Self {
2066 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
2067 Self(Mutex::new(cached))
2068 }
2069}
2070
2071impl PartialEq for TableDataCache {
2072 fn eq(&self, _: &Self) -> bool {
2073 true
2074 }
2075}
2076
2077impl std::fmt::Debug for TableDataCache {
2078 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2079 f.write_str("TableDataCache")
2080 }
2081}
2082
2083#[derive(Default)]
2086pub(crate) struct TableColumnWidthsCache(Mutex<Option<Arc<TableColumnWidths>>>);
2087
2088impl Clone for TableColumnWidthsCache {
2089 fn clone(&self) -> Self {
2090 let cached = self.0.lock().ok().and_then(|cache| cache.clone());
2091 Self(Mutex::new(cached))
2092 }
2093}
2094
2095impl PartialEq for TableColumnWidthsCache {
2096 fn eq(&self, _: &Self) -> bool {
2097 true
2098 }
2099}
2100
2101impl std::fmt::Debug for TableColumnWidthsCache {
2102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2103 f.write_str("TableColumnWidthsCache")
2104 }
2105}
2106
2107fn text_system_id(window: &Window) -> usize {
2108 std::rc::Rc::as_ptr(window.text_system()) as usize
2109}
2110
2111struct TableColumnWidths {
2112 text_system: usize,
2116 text_styles: TableTextStyles,
2118 rem_size: Pixels,
2119 mono_family: SharedString,
2120 inline_code: HighlightStyle,
2121 widths: Vec<f32>,
2123 custom_cells: Vec<(usize, usize)>,
2124}
2125
2126#[derive(Clone, PartialEq)]
2130struct TableTextStyles {
2131 head: gpui::TextStyle,
2132 body: gpui::TextStyle,
2133 head_refinement: TextStyleRefinement,
2134 body_refinement: TextStyleRefinement,
2135}
2136
2137impl TableTextStyles {
2138 fn new(style: &TextViewStyle, window: &Window) -> Self {
2139 let mut body_refinement = style.table().text.clone();
2140 let mut head_refinement = body_refinement.clone();
2141 head_refinement.refine(&style.table_head().text);
2142 head_refinement.refine(&style.table_cell().text);
2143 body_refinement.refine(&style.table_cell().text);
2144 let mut head = window.text_style();
2145 let mut body = head.clone();
2146 head.refine(&head_refinement);
2147 body.refine(&body_refinement);
2148 Self {
2149 head,
2150 body,
2151 head_refinement,
2152 body_refinement,
2153 }
2154 }
2155
2156 fn row(&self, row_ix: usize) -> &gpui::TextStyle {
2157 if row_ix == 0 { &self.head } else { &self.body }
2158 }
2159
2160 fn row_refinement(&self, row_ix: usize) -> &TextStyleRefinement {
2161 if row_ix == 0 {
2162 &self.head_refinement
2163 } else {
2164 &self.body_refinement
2165 }
2166 }
2167}
2168
2169#[derive(Debug, Clone, Default, PartialEq)]
2172pub struct TableData {
2173 pub headers: Vec<String>,
2175 pub rows: Vec<Vec<String>>,
2178 pub markdown: String,
2180 pub span: Option<Range<usize>>,
2187}
2188
2189impl Table {
2190 pub fn new(rows: Vec<Vec<Paragraph>>, column_aligns: Vec<ColumnumnAlign>) -> Self {
2192 Self {
2193 children: rows
2194 .into_iter()
2195 .map(|row| TableRow {
2196 children: row
2197 .into_iter()
2198 .map(|children| TableCell {
2199 children,
2200 width: None,
2201 })
2202 .collect(),
2203 })
2204 .collect(),
2205 column_aligns,
2206 ..Default::default()
2207 }
2208 }
2209
2210 pub fn rows(&self) -> impl Iterator<Item = impl Iterator<Item = &Paragraph>> {
2212 self.children
2213 .iter()
2214 .map(|row| row.children.iter().map(|cell| &cell.children))
2215 }
2216
2217 pub fn column_aligns(&self) -> &[ColumnumnAlign] {
2219 &self.column_aligns
2220 }
2221
2222 pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
2223 self.column_aligns.get(index).copied().unwrap_or_default()
2224 }
2225
2226 pub(crate) fn to_markdown(&self) -> String {
2233 let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
2234
2235 for (row_ix, row) in self.children.iter().enumerate() {
2236 let cells: Vec<String> = row
2237 .children
2238 .iter()
2239 .map(|cell| {
2240 cell.children
2241 .to_markdown()
2242 .trim()
2243 .replace('\n', " ")
2244 .replace('|', "\\|")
2245 })
2246 .collect();
2247 lines.push(format!("| {} |", cells.join(" | ")));
2248
2249 if row_ix == 0 {
2252 let aligns: Vec<String> = (0..row.children.len())
2253 .map(|ix| {
2254 match self.column_align(ix) {
2255 ColumnumnAlign::None | ColumnumnAlign::Left => ":--",
2256 ColumnumnAlign::Center => ":-:",
2257 ColumnumnAlign::Right => "--:",
2258 }
2259 .to_string()
2260 })
2261 .collect();
2262 lines.push(format!("| {} |", aligns.join(" | ")));
2263 }
2264 }
2265
2266 lines.join("\n")
2267 }
2268
2269 pub(crate) fn table_data(&self) -> TableData {
2272 let row_text = |row: &TableRow| {
2273 row.children
2274 .iter()
2275 .map(|cell| cell.children.text().trim().to_string())
2276 .collect::<Vec<_>>()
2277 };
2278
2279 TableData {
2280 headers: self.children.first().map(row_text).unwrap_or_default(),
2281 rows: self.children.iter().skip(1).map(row_text).collect(),
2282 markdown: self.to_markdown(),
2283 span: self.span.map(|span| span.start..span.end),
2284 }
2285 }
2286
2287 fn cached_table_data(&self) -> Arc<TableData> {
2292 if let Ok(cache) = self.table_data_cache.0.lock()
2293 && let Some(cached) = cache.as_ref()
2294 {
2295 return cached.clone();
2296 }
2297
2298 let data = Arc::new(self.table_data());
2299 if let Ok(mut cache) = self.table_data_cache.0.lock() {
2300 *cache = Some(data.clone());
2301 }
2302 data
2303 }
2304}
2305
2306#[derive(Debug, Default, Copy, Clone, PartialEq)]
2308pub enum ColumnumnAlign {
2309 #[default]
2311 None,
2312 Left,
2313 Center,
2314 Right,
2315}
2316
2317impl From<mdast::AlignKind> for ColumnumnAlign {
2318 fn from(value: mdast::AlignKind) -> Self {
2319 match value {
2320 mdast::AlignKind::None => ColumnumnAlign::None,
2321 mdast::AlignKind::Left => ColumnumnAlign::Left,
2322 mdast::AlignKind::Center => ColumnumnAlign::Center,
2323 mdast::AlignKind::Right => ColumnumnAlign::Right,
2324 }
2325 }
2326}
2327
2328#[derive(Debug, Clone, Default, PartialEq)]
2329pub(crate) struct TableRow {
2330 pub children: Vec<TableCell>,
2331}
2332
2333#[derive(Debug, Clone, Default, PartialEq)]
2334pub(crate) struct TableCell {
2335 pub children: Paragraph,
2336 pub width: Option<DefiniteLength>,
2337}
2338
2339impl Paragraph {
2340 pub(crate) fn take(&mut self) -> Paragraph {
2341 std::mem::replace(
2342 self,
2343 Paragraph {
2344 span: None,
2345 children: vec![],
2346 link_refs: Default::default(),
2347 state: Arc::new(Mutex::new(InlineState::default())),
2348 render_cache: ParagraphRenderCache::default(),
2349 },
2350 )
2351 }
2352
2353 pub(crate) fn is_image(&self) -> bool {
2354 false
2355 }
2356
2357 pub(crate) fn set_span(&mut self, span: Span) {
2358 self.span = Some(span);
2359 }
2360
2361 pub(crate) fn push_str(&mut self, text: &str) {
2362 self.children.push(
2363 InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
2364 );
2365 self.invalidate_render_cache();
2366 }
2367
2368 pub fn push(&mut self, text: InlineNode) {
2370 self.children.push(text);
2371 self.invalidate_render_cache();
2372 }
2373
2374 pub fn children(&self) -> &[InlineNode] {
2376 &self.children
2377 }
2378
2379 pub fn with_leaf_key(mut self, key: u64) -> Self {
2384 set_leaf_key(&mut self.state, key);
2385 self
2386 }
2387
2388 pub(crate) fn push_image(&mut self, image: ImageNode) {
2389 self.children.push(InlineNode::image(image));
2390 self.invalidate_render_cache();
2391 }
2392
2393 fn invalidate_render_cache(&mut self) {
2395 self.render_cache = ParagraphRenderCache::default();
2396 }
2397
2398 pub(crate) fn is_empty(&self) -> bool {
2399 self.children.is_empty()
2400 || self
2401 .children
2402 .iter()
2403 .all(|node| node.text.is_empty() && node.image.is_none())
2404 }
2405
2406 pub(crate) fn text_len(&self) -> usize {
2408 self.children
2409 .iter()
2410 .map(|node| node.text.len())
2411 .sum::<usize>()
2412 }
2413
2414 pub(crate) fn merge(&mut self, other: Self) {
2415 self.children.extend(other.children);
2416 self.invalidate_render_cache();
2417 }
2418}
2419
2420#[derive(Debug, Clone)]
2421pub struct CodeBlock {
2422 lang: Option<SharedString>,
2423 meta: Option<SharedString>,
2424 state: Arc<Mutex<InlineState>>,
2425 highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
2426 pub(crate) source_segments: Vec<SourceSegment>,
2429 pub span: Option<Span>,
2430}
2431
2432struct CachedCodeBlockHighlights {
2433 highlighter: Arc<CodeBlockHighlighterFn>,
2434 styles: Vec<(Range<usize>, HighlightStyle)>,
2435}
2436
2437impl std::fmt::Debug for CachedCodeBlockHighlights {
2438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2439 f.debug_struct("CachedCodeBlockHighlights")
2440 .field("styles", &self.styles)
2441 .finish_non_exhaustive()
2442 }
2443}
2444
2445impl PartialEq for CodeBlock {
2446 fn eq(&self, other: &Self) -> bool {
2447 self.lang == other.lang
2448 && self.meta == other.meta
2449 && self.code() == other.code()
2450 && self.span == other.span
2451 }
2452}
2453
2454impl CodeBlock {
2455 pub fn lang(&self) -> Option<SharedString> {
2457 self.lang.clone()
2458 }
2459
2460 pub fn meta(&self) -> Option<SharedString> {
2462 self.meta.clone()
2463 }
2464
2465 pub fn with_meta(mut self, meta: Option<SharedString>) -> Self {
2468 self.meta = meta;
2469 self
2470 }
2471
2472 pub fn with_leaf_key(mut self, key: u64) -> Self {
2475 set_leaf_key(&mut self.state, key);
2476 self
2477 }
2478
2479 pub fn code(&self) -> SharedString {
2481 self.state
2482 .lock()
2483 .map(|state| state.text.clone())
2484 .unwrap_or_default()
2485 }
2486
2487 pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
2493 Self::new(code.into(), lang.map(Into::into), None::<Span>)
2494 }
2495
2496 pub fn new(
2498 code: SharedString,
2499 lang: Option<SharedString>,
2500 span: Option<impl Into<Span>>,
2501 ) -> Self {
2502 let state = Arc::new(Mutex::new(InlineState::default()));
2503 if let Ok(mut state) = state.lock() {
2504 state.set_text(code);
2505 }
2506
2507 Self {
2508 lang,
2509 meta: None,
2510 state,
2511 highlight_cache: Arc::new(Mutex::new(None)),
2512 source_segments: vec![],
2513 span: span.map(|s| s.into()),
2514 }
2515 }
2516
2517 pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
2518 self.source_segments = source_segments;
2519 self
2520 }
2521
2522 #[cfg(test)]
2523 pub(crate) fn set_selection(&self, selection: Range<usize>) {
2524 if let Ok(mut state) = self.state.lock() {
2525 state.selection = Some(selection.into());
2526 }
2527 }
2528
2529 pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
2530 let Ok(state) = self.state.lock() else {
2531 return SourceRangeSelection::Unmapped;
2532 };
2533 let Some(selection) = state.selection else {
2534 return SourceRangeSelection::Unselected;
2535 };
2536 if selection.start >= selection.end {
2537 return SourceRangeSelection::Unselected;
2538 }
2539 source_range_for_segments(&self.source_segments, selection.start..selection.end)
2540 .map(SourceRangeSelection::Mapped)
2541 .unwrap_or(SourceRangeSelection::Unmapped)
2542 }
2543
2544 fn highlighted_styles(
2545 &self,
2546 highlighter: &Arc<CodeBlockHighlighterFn>,
2547 ) -> Vec<(Range<usize>, HighlightStyle)> {
2548 if let Ok(cache) = self.highlight_cache.lock()
2549 && let Some(cache) = cache.as_ref()
2550 && Arc::ptr_eq(&cache.highlighter, highlighter)
2551 {
2552 return cache.styles.clone();
2553 }
2554
2555 let code_len = self.code().len();
2556 let styles = highlighter(self)
2557 .into_iter()
2558 .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
2559 .collect::<Vec<_>>();
2560 if let Ok(mut cache) = self.highlight_cache.lock() {
2561 *cache = Some(CachedCodeBlockHighlights {
2562 highlighter: highlighter.clone(),
2563 styles: styles.clone(),
2564 });
2565 }
2566 styles
2567 }
2568
2569 pub(super) fn selected_text(&self) -> String {
2570 let mut text = String::new();
2571 if let Ok(state) = self.state.lock()
2572 && let Some(selection) = &state.selection
2573 {
2574 text.push_str(&state.text[selection.start..selection.end]);
2575 }
2576 text
2577 }
2578
2579 pub(super) fn selected_source(&self) -> String {
2586 let code = self.selected_text();
2587 if code.is_empty() {
2588 return String::new();
2589 }
2590 let lang = self.lang.clone().unwrap_or_default();
2591 let code = code.trim_end_matches('\n');
2594 format!("```{}\n{}\n```", lang, code)
2595 }
2596
2597 pub(super) fn text(&self) -> String {
2598 self.state
2599 .lock()
2600 .map(|state| state.text.to_string())
2601 .unwrap_or_default()
2602 }
2603
2604 pub(super) fn has_selection(&self) -> bool {
2608 self.state
2609 .lock()
2610 .is_ok_and(|state| state.selection.is_some())
2611 }
2612
2613 pub(super) fn clear_selection(&self) {
2614 if let Ok(mut state) = self.state.lock() {
2615 state.selection = None;
2616 }
2617 }
2618
2619 fn render(
2620 &self,
2621 options: &NodeRenderOptions,
2622 node_cx: &NodeContext,
2623 window: &mut Window,
2624 cx: &mut App,
2625 ) -> AnyElement {
2626 let style = &node_cx.style;
2627 let leaf_key = self.span.map(|span| TextLeafKey::block(span.start));
2628 let lang_label = self
2633 .lang()
2634 .filter(|lang| !lang.is_empty() && node_cx.code_block_actions.is_none())
2635 .map(|lang| {
2636 div()
2637 .absolute()
2638 .top(px(7.))
2639 .right_3()
2640 .font_family(cx.theme().tokens.typography.sans.clone())
2641 .text_size(cx.theme().tokens.typography.xs.size)
2642 .line_height(px(16.))
2643 .text_color(style.muted_foreground())
2644 .child(lang)
2645 });
2646
2647 let block = div()
2648 .w_full()
2649 .min_w_0()
2650 .px_4()
2651 .pt(if lang_label.is_some() {
2652 px(28.)
2653 } else {
2654 px(14.)
2655 })
2656 .pb(px(14.))
2657 .bg(style.code_background())
2658 .border_1()
2659 .border_color(style.border())
2660 .font_family(cx.theme().tokens.typography.mono.clone())
2661 .text_size(cx.theme().tokens.typography.mono_md.size)
2662 .line_height(relative(1.6))
2663 .relative()
2664 .refine_style(&style.code_block())
2665 .child(
2666 Inline::new(
2667 self.state.clone(),
2668 vec![],
2669 fade_highlights(
2670 node_cx
2671 .code_block_highlighter
2672 .as_ref()
2673 .map(|highlighter| self.highlighted_styles(highlighter))
2674 .unwrap_or_default()
2675 .into_iter()
2676 .map(|(range, style)| (range, InlineHighlight::from(style)))
2677 .collect(),
2678 node_cx.stream_fades(leaf_key),
2679 ),
2680 node_cx.link_click_handler.clone(),
2681 )
2682 .range_backgrounds(node_cx.range_backgrounds(leaf_key).to_vec())
2683 .reveal(node_cx.reveal_at(leaf_key, 0, self.code().len())),
2684 );
2685 let actions = node_cx.code_block_actions.clone().map(|actions| {
2686 div()
2687 .id("actions")
2688 .absolute()
2689 .top_2()
2690 .right_2()
2691 .bg(style.code_background())
2692 .rounded(cx.theme().tokens.radius.md)
2693 .child(actions(&self, window, cx))
2694 });
2695 let id = block_element_id("codeblock", self.span, options.ix);
2698 let block = if matches!(style.code_block().overflow.y, Some(Overflow::Scroll)) {
2699 let scroll_handle = window
2700 .use_keyed_state(
2701 block_element_id("codeblock-scroll", self.span, options.ix),
2702 cx,
2703 |_, _| ScrollHandle::default(),
2704 )
2705 .read(cx)
2706 .clone();
2707 div()
2714 .id(id.clone())
2715 .w_full()
2716 .min_w_0()
2717 .relative()
2718 .child(block.id("scroll").track_scroll(&scroll_handle))
2719 .child(ScrollableMask::new(Axis::Vertical, &scroll_handle).id(id))
2720 .child(
2721 div()
2722 .absolute()
2723 .inset_0()
2724 .child(Scrollbar::vertical(&scroll_handle)),
2725 )
2726 .children(actions)
2727 .children(lang_label)
2728 .into_any_element()
2729 } else {
2730 match actions {
2732 Some(actions) => block.id(id).child(actions).into_any_element(),
2733 None => block.children(lang_label).into_any_element(),
2734 }
2735 };
2736
2737 gapped(
2738 block,
2739 if options.is_last {
2740 rems(0.)
2741 } else {
2742 style.paragraph_gap()
2743 },
2744 )
2745 }
2746}
2747
2748#[derive(Default, Clone)]
2750pub(crate) struct NodeContext {
2751 pub(crate) offset: usize,
2754 pub(crate) link_refs: HashMap<SharedString, LinkMark>,
2755 pub(crate) style: Arc<TextViewStyle>,
2756 pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
2757 pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
2758 pub(crate) table_actions: Option<Arc<TableActionsFn>>,
2759 pub(crate) image_source: Option<Arc<super::text_view::ImageSourceFn>>,
2760 pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
2761 pub(crate) task_toggle_handler: Option<Arc<TaskToggleHandlerFn>>,
2762 pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
2763 pub(crate) stream_fade: Option<Arc<StreamFadeFrame>>,
2765 pub(crate) range_highlights: Option<Arc<RangeHighlightFrame>>,
2767 pub(crate) foregrounds: Option<Arc<HashMap<u64, Vec<(Range<usize>, Hsla)>>>>,
2769 pub(crate) reveal: Option<RevealRequest>,
2771 pub(crate) body_font_size: Pixels,
2774}
2775
2776impl NodeContext {
2777 fn foregrounds(&self, key: Option<u64>) -> &[(Range<usize>, Hsla)] {
2778 key.and_then(|key| self.foregrounds.as_ref()?.get(&key))
2779 .map_or(&[], Vec::as_slice)
2780 }
2781
2782 fn image_source(&self, image: &ImageNode) -> ImageSource {
2783 match &self.image_source {
2784 Some(resolve) => resolve(&image.url),
2785 None => image.source(),
2786 }
2787 }
2788
2789 pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
2790 self.link_refs.insert(identifier, link);
2791 }
2792
2793 fn stream_fades(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, f32)] {
2795 match (&self.stream_fade, key) {
2796 (Some(frame), Some(key)) => frame.fades(key).unwrap_or_default(),
2797 _ => &[],
2798 }
2799 }
2800
2801 fn range_backgrounds(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, Hsla)] {
2804 match (&self.range_highlights, key) {
2805 (Some(frame), Some(key)) => frame.backgrounds(key),
2806 _ => &[],
2807 }
2808 }
2809
2810 fn reveal_at(&self, key: Option<TextLeafKey>, start: usize, end: usize) -> Option<RevealAt> {
2813 self.reveal.as_ref()?.at(key, start, end)
2814 }
2815}
2816
2817impl PartialEq for NodeContext {
2818 fn eq(&self, other: &Self) -> bool {
2819 self.link_refs == other.link_refs && self.style == other.style
2820 }
2823}
2824
2825fn mark_text_color(highlight: Hsla, background: Hsla) -> Hsla {
2828 fn channel(value: f32) -> f32 {
2829 if value <= 0.04045 {
2830 value / 12.92
2831 } else {
2832 ((value + 0.055) / 1.055).powf(2.4)
2833 }
2834 }
2835 let rgb = background.to_rgb().blend(highlight.to_rgb());
2836 let luminance = 0.2126 * channel(rgb.r) + 0.7152 * channel(rgb.g) + 0.0722 * channel(rgb.b);
2837 if (luminance + 0.05) * (luminance + 0.05) > 0.0525 {
2840 gpui::black()
2841 } else {
2842 gpui::white()
2843 }
2844}
2845
2846fn mark_highlight(mark: &TextMark, node_cx: &NodeContext, cx: &App) -> InlineHighlight {
2849 let mut highlight = HighlightStyle::default();
2850 if mark.bold {
2851 highlight.font_weight = Some(FontWeight::BOLD);
2852 }
2853 if mark.italic {
2854 highlight.font_style = Some(FontStyle::Italic);
2855 }
2856 if mark.strikethrough {
2857 highlight.strikethrough = Some(gpui::StrikethroughStyle {
2858 thickness: gpui::px(1.),
2859 ..Default::default()
2860 });
2861 }
2862 if mark.underline {
2863 highlight.underline = Some(gpui::UnderlineStyle {
2864 thickness: gpui::px(1.),
2865 ..Default::default()
2866 });
2867 }
2868 let mut font_family = None;
2869 if mark.code {
2870 highlight = highlight.highlight(node_cx.style.inline_code_highlight());
2871 let style = node_cx.style.inline_code_style();
2872 highlight = highlight.highlight(HighlightStyle {
2873 color: style.text.color,
2874 font_weight: style.text.font_weight,
2875 font_style: style.text.font_style,
2876 underline: style.text.underline,
2877 strikethrough: style.text.strikethrough,
2878 ..Default::default()
2879 });
2880 font_family = Some(
2881 style
2882 .text
2883 .font_family
2884 .clone()
2885 .unwrap_or_else(|| cx.theme().tokens.typography.mono.clone()),
2886 );
2887 }
2888 if let Some(color) = mark.highlight {
2889 highlight.background_color = Some(color);
2890 if color.a > 0.0 {
2896 highlight.color = Some(mark_text_color(color, cx.theme().tokens.colors.background));
2897 }
2898 }
2899 InlineHighlight {
2900 style: highlight,
2901 font_family,
2902 font_size_scale: mark.code.then_some(0.875),
2903 code_style: mark.code.then(|| node_cx.style.inline_code_style().clone()),
2904 }
2905}
2906
2907fn append_node_highlights(
2908 highlights: &mut Vec<(Range<usize>, InlineHighlight)>,
2909 node_highlights: Vec<(Range<usize>, InlineHighlight)>,
2910) {
2911 if highlights.last().is_some_and(|(last, _)| {
2914 node_highlights
2915 .iter()
2916 .any(|(range, _)| !range.is_empty() && range.start < last.end)
2917 }) {
2918 *highlights = combine_highlights(std::mem::take(highlights), node_highlights);
2919 } else {
2920 highlights.extend(combine_highlights([], node_highlights));
2921 }
2922}
2923
2924impl Paragraph {
2925 fn inline_highlights(
2929 &self,
2930 node_cx: &NodeContext,
2931 cx: &App,
2932 ) -> Vec<(Range<usize>, InlineHighlight)> {
2933 let mut highlights = vec![];
2934 let mut offset = 0;
2935 for inline_node in &self.children {
2936 let node_highlights = inline_node
2937 .marks
2938 .iter()
2939 .map(|(range, mark)| {
2940 (
2941 (offset + range.start)..(offset + range.end),
2942 mark_highlight(mark, node_cx, cx),
2943 )
2944 })
2945 .collect::<Vec<_>>();
2946 append_node_highlights(&mut highlights, node_highlights);
2947 offset += inline_node.text.len();
2948 }
2949 highlights
2950 }
2951
2952 fn render(
2956 &self,
2957 fade_key: Option<TextLeafKey>,
2958 node_cx: &NodeContext,
2959 _window: &mut Window,
2960 cx: &mut App,
2961 ) -> AnyElement {
2962 let children = &self.children;
2963 let fades = node_cx.stream_fades(fade_key);
2964 let backgrounds = node_cx.range_backgrounds(fade_key);
2965
2966 if self.should_render_inline_flow() {
2967 return InlineFlow::new(
2968 self.element_id(fade_key),
2969 self.inline_flow_items(fade_key, fades, backgrounds, node_cx, cx),
2970 node_cx.link_click_handler.clone(),
2971 )
2972 .editor_key(self.leaf_key())
2973 .into_any_element();
2974 }
2975
2976 let has_image = children.iter().any(|child| child.image.is_some());
2977 if !has_image {
2980 let (text, highlights, mut links) = self.plain_render(node_cx, cx);
2981 if text.is_empty() && self.leaf_key().is_none() {
2984 return div().into_any_element();
2985 }
2986 for (_, link_mark) in &mut links {
2987 if let Some(identifier) = link_mark.identifier.as_ref()
2988 && let Some(mark) = node_cx.link_refs.get(identifier)
2989 {
2990 *link_mark = mark.clone();
2991 }
2992 }
2993 let highlights = fade_highlights(highlights, &slice_fades(fades, 0, text.len()));
2994 let backgrounds = slice_backgrounds(backgrounds, 0, text.len());
2995 let reveal = node_cx.reveal_at(fade_key, 0, text.len());
2996 if let Ok(mut state) = self.state.lock() {
2997 state.set_text(text);
2998 }
2999 return Inline::new(
3000 self.state.clone(),
3001 links,
3002 highlights,
3003 node_cx.link_click_handler.clone(),
3004 )
3005 .range_backgrounds(backgrounds)
3006 .reveal(reveal)
3007 .into_any_element();
3008 }
3009
3010 let mut child_nodes: Vec<AnyElement> = vec![];
3011
3012 let mut text = String::new();
3013 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
3014 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
3015 let mut offset = 0;
3016 let mut consumed = 0;
3019
3020 for (ix, inline_node) in children.iter().enumerate() {
3021 let text_len = inline_node.text.len();
3022 text.push_str(&inline_node.text);
3023
3024 if let Some(image) = &inline_node.image {
3025 if text.len() > 0 {
3026 if let Ok(mut state) = inline_node.state.lock() {
3027 state.set_text(text.clone().into());
3028 }
3029 child_nodes.push(
3030 Inline::new(
3031 inline_node.state.clone(),
3032 links.clone(),
3033 fade_highlights(
3034 highlights.clone(),
3035 &slice_fades(fades, consumed, consumed + text.len()),
3036 ),
3037 node_cx.link_click_handler.clone(),
3038 )
3039 .range_backgrounds(slice_backgrounds(
3040 backgrounds,
3041 consumed,
3042 consumed + text.len(),
3043 ))
3044 .reveal(node_cx.reveal_at(fade_key, consumed, consumed + text.len()))
3045 .into_any_element(),
3046 );
3047 }
3048 let link_click_handler = node_cx.link_click_handler.clone();
3049 child_nodes.push(
3050 img(node_cx.image_source(image))
3051 .id(ix)
3052 .object_fit(ObjectFit::Contain)
3053 .max_w(relative(1.))
3054 .when_some(image.width, |this, width| this.w(width))
3055 .when_some(image.link.clone(), |this, link| {
3056 let link_click_handler = link_click_handler.clone();
3057 let aux_link = link.clone();
3058 let aux_link_click_handler = link_click_handler.clone();
3059 this.cursor_pointer()
3060 .on_click(move |event, window, cx| {
3061 crate::TextSelection::end(window, cx);
3062 cx.stop_propagation();
3063 handle_link_click(
3064 &link_click_handler,
3065 link.url.clone(),
3066 event.clone(),
3067 window,
3068 cx,
3069 );
3070 })
3071 .on_aux_click(move |event, window, cx| {
3072 crate::TextSelection::end(window, cx);
3073 cx.stop_propagation();
3074 handle_link_click(
3075 &aux_link_click_handler,
3076 aux_link.url.clone(),
3077 event.clone(),
3078 window,
3079 cx,
3080 );
3081 })
3082 })
3083 .into_any_element(),
3084 );
3085
3086 consumed += text.len();
3087 text.clear();
3088 links.clear();
3089 highlights.clear();
3090 offset = 0;
3091 } else {
3092 let mut node_highlights = vec![];
3093 for (range, style) in &inline_node.marks {
3094 let inner_range = (offset + range.start)..(offset + range.end);
3095 let mut highlight = mark_highlight(style, node_cx, cx);
3096
3097 if let Some(mut link_mark) = style.link.clone() {
3098 highlight.style.color = Some(node_cx.style.link());
3099 highlight.style.underline = Some(link_underline(&node_cx.style));
3100
3101 if let Some(identifier) = link_mark.identifier.as_ref() {
3103 if let Some(mark) = node_cx.link_refs.get(identifier) {
3104 link_mark = mark.clone();
3105 }
3106 }
3107
3108 links.push((inner_range.clone(), link_mark));
3109 }
3110
3111 node_highlights.push((inner_range, highlight));
3112 }
3113
3114 append_node_highlights(&mut highlights, node_highlights);
3115 offset += text_len;
3116 }
3117 }
3118
3119 if text.len() > 0 {
3121 let text_end = consumed + text.len();
3122 let highlights = fade_highlights(highlights, &slice_fades(fades, consumed, text_end));
3123 if let Ok(mut state) = self.state.lock() {
3124 state.set_text(text.into());
3125 }
3126 child_nodes.push(
3127 Inline::new(
3128 self.state.clone(),
3129 links,
3130 highlights,
3131 node_cx.link_click_handler.clone(),
3132 )
3133 .range_backgrounds(slice_backgrounds(backgrounds, consumed, text_end))
3134 .reveal(node_cx.reveal_at(fade_key, consumed, text_end))
3135 .into_any_element(),
3136 );
3137 }
3138
3139 if !has_image {
3143 return child_nodes
3144 .pop()
3145 .unwrap_or_else(|| div().into_any_element());
3146 }
3147
3148 div()
3149 .id(self.element_id(fade_key))
3150 .children(child_nodes)
3151 .into_any_element()
3152 }
3153
3154 fn element_id(&self, fade_key: Option<TextLeafKey>) -> ElementId {
3162 match fade_key {
3163 Some(key) => ElementId::from(key),
3164 None => ElementId::NamedInteger(
3165 "p".into(),
3166 Arc::as_ptr(&self.state) as *const () as usize as u64,
3167 ),
3168 }
3169 }
3170
3171 fn leaf_key(&self) -> Option<u64> {
3173 self.state.lock().ok().and_then(|state| state.editor_key)
3174 }
3175
3176 fn should_render_inline_flow(&self) -> bool {
3177 let has_image = self.children.iter().any(|child| child.image.is_some());
3178 let has_text = self.children.iter().any(|child| !child.text.is_empty());
3179 if has_image && self.leaf_key().is_some() {
3183 return true;
3184 }
3185 self.children.iter().any(|child| child.custom.is_some())
3186 || (has_image && has_text)
3187 || self
3188 .children
3189 .iter()
3190 .any(|child| child.marks.iter().any(|(_, mark)| mark.code))
3191 }
3192
3193 fn inline_flow_items(
3194 &self,
3195 leaf_key: Option<TextLeafKey>,
3196 fades: &[(Range<usize>, f32)],
3197 backgrounds: &[(Range<usize>, Hsla)],
3198 node_cx: &NodeContext,
3199 cx: &mut App,
3200 ) -> Vec<InlineFlowItem> {
3201 let mut items = Vec::new();
3202 let mut text = String::new();
3203 let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
3204 let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
3205 let mut offset = 0;
3206 let mut consumed = 0;
3209
3210 for inline_node in &self.children {
3211 if let Some(node) = &inline_node.custom {
3212 if let Ok(mut state) = inline_node.state.lock() {
3213 state.set_text(text.clone().into());
3214 }
3215 if !text.is_empty() {
3216 let item_fades = slice_fades(fades, consumed, consumed + text.len());
3217 let item_backgrounds =
3218 slice_backgrounds(backgrounds, consumed, consumed + text.len());
3219 let item_reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
3220 consumed += text.len();
3221 items.push(InlineFlowItem::Text {
3222 state: inline_node.state.clone(),
3223 text: std::mem::take(&mut text).into(),
3224 links: std::mem::take(&mut links),
3225 highlights: fade_highlights(std::mem::take(&mut highlights), &item_fades),
3226 backgrounds: item_backgrounds,
3227 reveal: item_reveal,
3228 });
3229 }
3230 let mut object_style = HighlightStyle::default();
3231 let mut object_link = None;
3232 for (_, mark) in &inline_node.marks {
3233 object_style = object_style.highlight(mark_highlight(mark, node_cx, cx).style);
3234 if let Some(link) = &mark.link {
3235 object_link = Some(
3236 link.identifier
3237 .as_ref()
3238 .and_then(|id| node_cx.link_refs.get(id))
3239 .unwrap_or(link)
3240 .clone(),
3241 );
3242 object_style.color = Some(node_cx.style.link());
3243 object_style.underline = Some(link_underline(&node_cx.style));
3244 }
3245 }
3246 let rendered_node = node.clone();
3247 let extensions = node_cx.markdown_extensions.clone();
3248 items.push(InlineFlowItem::Object {
3249 text: node.shared_text(),
3250 accessibility_label: node.shared_accessibility_name(),
3251 id: node.source_range().map_or(items.len(), |range| range.start),
3252 renderer: Arc::new(move |context, window, cx| {
3253 extensions.render_inline(&rendered_node, context, window, cx)
3254 }),
3255 selected: inline_node.custom_selection.clone(),
3256 style: object_style,
3257 link: object_link,
3258 });
3259 consumed += inline_node.text.len();
3260 offset = 0;
3261 continue;
3262 }
3263 let text_len = inline_node.text.len();
3264 text.push_str(&inline_node.text);
3265
3266 if let Some(image) = &inline_node.image {
3267 if !text.is_empty() {
3268 if let Ok(mut state) = inline_node.state.lock() {
3269 state.set_text(text.clone().into());
3270 }
3271 items.push(InlineFlowItem::Text {
3272 state: inline_node.state.clone(),
3273 text: text.clone().into(),
3274 links: links.clone(),
3275 highlights: fade_highlights(
3276 highlights.clone(),
3277 &slice_fades(fades, consumed, consumed + text.len()),
3278 ),
3279 backgrounds: slice_backgrounds(
3280 backgrounds,
3281 consumed,
3282 consumed + text.len(),
3283 ),
3284 reveal: node_cx.reveal_at(leaf_key, consumed, consumed + text.len()),
3285 });
3286 }
3287
3288 items.push(InlineFlowItem::Image {
3289 source: node_cx.image_source(image),
3290 link: image.link.clone(),
3291 title: image.title(),
3292 width: image.width,
3293 height: image.height,
3294 });
3295
3296 consumed += text.len();
3297 text.clear();
3298 links.clear();
3299 highlights.clear();
3300 offset = 0;
3301 } else {
3302 let mut node_highlights = vec![];
3303 for (range, style) in &inline_node.marks {
3304 let inner_range = (offset + range.start)..(offset + range.end);
3305 let mut highlight = mark_highlight(style, node_cx, cx);
3306
3307 if let Some(mut link_mark) = style.link.clone() {
3308 highlight.style.color = Some(node_cx.style.link());
3309 highlight.style.underline = Some(link_underline(&node_cx.style));
3310
3311 if let Some(identifier) = link_mark.identifier.as_ref()
3312 && let Some(mark) = node_cx.link_refs.get(identifier)
3313 {
3314 link_mark = mark.clone();
3315 }
3316
3317 links.push((inner_range.clone(), link_mark));
3318 }
3319
3320 node_highlights.push((inner_range, highlight));
3321 }
3322
3323 append_node_highlights(&mut highlights, node_highlights);
3324 offset += text_len;
3325 }
3326 }
3327
3328 if !text.is_empty() {
3329 if let Ok(mut state) = self.state.lock() {
3330 state.set_text(text.clone().into());
3331 }
3332 let highlights = fade_highlights(
3333 highlights,
3334 &slice_fades(fades, consumed, consumed + text.len()),
3335 );
3336 let backgrounds = slice_backgrounds(backgrounds, consumed, consumed + text.len());
3337 let reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
3338 items.push(InlineFlowItem::Text {
3339 state: self.state.clone(),
3340 text: text.into(),
3341 links,
3342 highlights,
3343 backgrounds,
3344 reveal,
3345 });
3346 }
3347
3348 let foregrounds = node_cx.foregrounds(self.leaf_key());
3349 if !foregrounds.is_empty() {
3350 let mut offset = 0;
3351 for item in &mut items {
3352 match item {
3353 InlineFlowItem::Text {
3354 text, highlights, ..
3355 } => {
3356 *highlights = foreground_highlights(
3357 std::mem::take(highlights),
3358 text,
3359 offset,
3360 foregrounds,
3361 );
3362 offset += text.len();
3363 }
3364 InlineFlowItem::Object { style, .. } => {
3365 let end = offset + '\u{FFFC}'.len_utf8();
3366 if let Some((_, color)) = foregrounds
3367 .iter()
3368 .rev()
3369 .find(|(range, _)| range.start <= offset && end <= range.end)
3370 {
3371 style.color = Some(*color);
3372 }
3373 offset = end;
3374 }
3375 InlineFlowItem::Image { .. } => offset += '\u{FFFC}'.len_utf8(),
3376 }
3377 }
3378 }
3379 items
3380 }
3381}
3382
3383fn block_element_id(kind: &'static str, span: Option<Span>, ix: usize) -> ElementId {
3388 (kind, span.map_or(ix, |span| span.start)).into()
3389}
3390
3391fn link_underline(style: &TextViewStyle) -> gpui::UnderlineStyle {
3394 gpui::UnderlineStyle {
3395 thickness: px(1.),
3396 color: Some(style.link().opacity(0.4)),
3397 ..Default::default()
3398 }
3399}
3400
3401fn body_font_size(node_cx: &NodeContext, window: &Window) -> Pixels {
3408 if node_cx.body_font_size > Pixels::ZERO {
3409 node_cx.body_font_size
3410 } else {
3411 window.text_style().font_size.to_pixels(window.rem_size())
3412 }
3413}
3414
3415fn heading_metrics(level: u8) -> (f32, f32, f32) {
3420 match level {
3421 1 => (1.8, 1.25, 1.1),
3422 2 => (4. / 3., 1.35, 1.6),
3423 3 => (17. / 15., 1.35, 1.4),
3424 4 => (1., 1.35, 1.4),
3425 5 => (14. / 15., 1.35, 1.4),
3426 _ => (13. / 15., 1.35, 1.4),
3427 }
3428}
3429
3430const HEADING_BOTTOM_GAP: f32 = 0.35;
3432
3433fn heading_size(level: u8, node_cx: &NodeContext, window: &Window) -> Pixels {
3436 node_cx.style.heading(level).text.font_size.map_or_else(
3437 || body_font_size(node_cx, window) * heading_metrics(level).0,
3438 |size| size.to_pixels(window.rem_size()),
3439 )
3440}
3441
3442fn rule_space(body_font_size: Pixels) -> Pixels {
3444 body_font_size * 1.6
3445}
3446
3447fn block_gap(options: &NodeRenderOptions, style: &TextViewStyle) -> Rems {
3449 if options.in_list || options.is_last {
3450 rems(0.)
3451 } else {
3452 style.paragraph_gap()
3453 }
3454}
3455
3456fn gapped(block: AnyElement, gap: Rems) -> AnyElement {
3459 if gap.is_zero() {
3460 block
3461 } else {
3462 div().pb(gap).child(block).into_any_element()
3463 }
3464}
3465
3466fn slice_fades(
3468 fades: &[(Range<usize>, f32)],
3469 start: usize,
3470 end: usize,
3471) -> Vec<(Range<usize>, f32)> {
3472 slice_ranges(fades, start, end, |range, fade_out| (range, *fade_out))
3473}
3474
3475fn slice_backgrounds(
3478 backgrounds: &[(Range<usize>, Hsla)],
3479 start: usize,
3480 end: usize,
3481) -> Vec<(Range<usize>, Hsla)> {
3482 slice_ranges(backgrounds, start, end, |range, color| (range, *color))
3483}
3484
3485struct TableColumn {
3487 max: f32,
3489 min: f32,
3491 divider: f32,
3492}
3493
3494fn table_cell_padding_x(style: &TextViewStyle, rem_size: Pixels) -> f32 {
3497 let padding = &style.table_cell().padding;
3498 let side = |length: Option<DefiniteLength>| match length {
3499 Some(DefiniteLength::Absolute(length)) => f32::from(length.to_pixels(rem_size)),
3500 _ => CELL_PAD_PX / 2.,
3501 };
3502 side(padding.left) + side(padding.right)
3503}
3504
3505const CELL_PAD_PX: f32 = 24.0; const CELL_MIN_PX: f32 = 48.0;
3507const CELL_BORDER_PX: f32 = 1.0; fn measure_table_columns(
3514 table: &Table,
3515 col_count: usize,
3516 node_cx: &NodeContext,
3517 window: &mut Window,
3518 cx: &mut App,
3519) -> Vec<f32> {
3520 let text_styles = TableTextStyles::new(&node_cx.style, window);
3521 let rem_size = window.rem_size();
3522 let mono_family = cx.theme().tokens.typography.mono.clone();
3523 let inline_code = node_cx.style.inline_code_highlight();
3524 let cached = table.column_widths_cache.0.lock().ok().and_then(|cache| {
3525 cache
3526 .as_ref()
3527 .filter(|cached| {
3528 cached.widths.len() == col_count
3529 && cached.text_styles == text_styles
3530 && cached.rem_size == rem_size
3531 && cached.mono_family == mono_family
3532 && cached.inline_code == inline_code
3533 && cached.text_system == text_system_id(window)
3534 })
3535 .cloned()
3536 });
3537 let measured = cached.unwrap_or_else(|| {
3538 let (widths, custom_cells) =
3539 measure_table_text_columns(table, col_count, &text_styles, node_cx, window, cx);
3540 let measured = Arc::new(TableColumnWidths {
3541 text_system: text_system_id(window),
3542 text_styles: text_styles.clone(),
3543 rem_size,
3544 mono_family,
3545 inline_code,
3546 widths,
3547 custom_cells,
3548 });
3549 if let Ok(mut cache) = table.column_widths_cache.0.lock() {
3550 *cache = Some(measured.clone());
3551 }
3552 measured
3553 });
3554
3555 let mut col_w = measured.widths.clone();
3559 for &(row_ix, ix) in &measured.custom_cells {
3560 let cell = &table.children[row_ix].children[ix];
3561 let items = cell.children.inline_flow_items(None, &[], &[], node_cx, cx);
3562 let style = text_styles.row_refinement(row_ix).clone();
3563 let width = window.with_text_style(Some(style), |window| {
3564 super::inline_flow::intrinsic_width(&items, window, cx)
3565 });
3566 let border = if ix + 1 < col_count {
3567 CELL_BORDER_PX
3568 } else {
3569 0.
3570 };
3571 col_w[ix] = col_w[ix].max(f32::from(width) + CELL_PAD_PX + border);
3572 }
3573 col_w
3574}
3575
3576fn measure_table_text_columns(
3579 table: &Table,
3580 col_count: usize,
3581 text_styles: &TableTextStyles,
3582 node_cx: &NodeContext,
3583 window: &mut Window,
3584 cx: &mut App,
3585) -> (Vec<f32>, Vec<(usize, usize)>) {
3586 let mut col_w = vec![CELL_MIN_PX; col_count];
3587 let mut custom_cells = Vec::new();
3588 for (row_ix, row) in table.children.iter().enumerate() {
3589 let text_style = text_styles.row(row_ix);
3590 let font_size = text_style.font_size.to_pixels(window.rem_size());
3591 for (ix, cell) in row.children.iter().enumerate() {
3592 let Some(slot) = col_w.get_mut(ix) else {
3593 continue;
3594 };
3595 if cell
3596 .children
3597 .children
3598 .iter()
3599 .any(|node| node.custom.is_some() || node.image.is_some())
3600 {
3601 custom_cells.push((row_ix, ix));
3602 continue;
3603 }
3604 let text = cell.children.text();
3605 let highlights = cell.children.inline_highlights(node_cx, cx);
3606 let mut w = 0.0_f32;
3607 let mut line_start = 0;
3608 for line in text.split('\n') {
3609 let start = line_start + (line.len() - line.trim_start().len());
3610 let line_end = line_start + line.len();
3611 line_start = line_end + 1;
3612 let line = line.trim();
3613 if line.is_empty() {
3614 continue;
3615 }
3616 let end = start + line.len();
3617 let line_highlights = highlights
3618 .iter()
3619 .filter_map(|(range, highlight)| {
3620 let clipped = range.start.max(start)..range.end.min(end);
3621 (clipped.start < clipped.end).then(|| {
3622 (
3623 clipped.start - start..clipped.end - start,
3624 highlight.clone(),
3625 )
3626 })
3627 })
3628 .collect::<Vec<_>>();
3629 let mut line_w = gpui::Pixels::ZERO;
3630 for (range, scale) in
3631 text_size_ranges(line.len(), &line_highlights, font_size, window.rem_size())
3632 {
3633 let highlights = slice_ranges(
3634 &line_highlights,
3635 range.start,
3636 range.end,
3637 |range, highlight| (range, highlight.clone()),
3638 );
3639 let insets = code_insets(&highlights, font_size * scale, window.rem_size());
3640 line_w += insets.left + insets.right;
3641 let runs = text_runs(range.len(), text_style, &highlights);
3642 line_w += window
3643 .text_system()
3644 .layout_line(&line[range], font_size * scale, &runs, None)
3645 .width;
3646 }
3647 w = w.max(f32::from(line_w));
3648 }
3649 let border = if ix + 1 < col_count {
3652 CELL_BORDER_PX
3653 } else {
3654 0.
3655 };
3656 *slot = slot.max(w + CELL_PAD_PX + border);
3657 }
3658 }
3659 (col_w, custom_cells)
3660}
3661
3662impl Paragraph {
3663 fn to_markdown(&self) -> String {
3664 if self.children.iter().any(|node| node.custom.is_some()) {
3665 let mut source = MarkdownSource::default();
3666 for node in &self.children {
3667 if node.custom.is_some() {
3668 source.push_object(node);
3669 } else {
3670 source.push_text(&node.text, &node.marks, 0..node.text.len());
3671 if let Some(image) = &node.image {
3672 source.push_str(&image_markdown(image));
3673 }
3674 }
3675 }
3676 let mut text = source.finish();
3677 text.push_str("\n\n");
3678 return text;
3679 }
3680 let mut text = self
3681 .children
3682 .iter()
3683 .map(|text_node| {
3684 let mut text = text_node.text.to_string();
3685 for (range, style) in &text_node.marks {
3686 if style.bold {
3687 text = format!("**{}**", &text_node.text[range.clone()]);
3688 }
3689 if style.italic {
3690 text = format!("*{}*", &text_node.text[range.clone()]);
3691 }
3692 if style.strikethrough {
3693 text = format!("~~{}~~", &text_node.text[range.clone()]);
3694 }
3695 if style.code {
3696 text = format!("`{}`", &text_node.text[range.clone()]);
3697 }
3698 if style.highlight.is_some() {
3699 text = format!("=={}==", &text_node.text[range.clone()]);
3700 }
3701 if let Some(link) = &style.link {
3702 text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
3703 }
3704 }
3705
3706 if let Some(image) = &text_node.image {
3707 let alt = image.alt.clone().unwrap_or_default();
3708 let title = image
3709 .title
3710 .clone()
3711 .map_or(String::new(), |t| format!(" \"{}\"", t));
3712 text.push_str(&format!("", alt, image.url, title))
3713 }
3714
3715 text
3716 })
3717 .collect::<Vec<_>>()
3718 .join("");
3719
3720 text.push_str("\n\n");
3721 text
3722 }
3723}
3724
3725impl BlockNode {
3726 #[allow(dead_code)]
3730 pub(crate) fn to_markdown(&self) -> String {
3731 match self {
3732 BlockNode::Root { children, .. } => children
3733 .iter()
3734 .map(|child| child.to_markdown())
3735 .collect::<Vec<_>>()
3736 .join("\n\n"),
3737 BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
3738 BlockNode::Heading {
3739 level, children, ..
3740 } => {
3741 let hashes = "#".repeat(*level as usize);
3742 format!("{} {}", hashes, children.to_markdown())
3743 }
3744 BlockNode::Blockquote { children, .. } => {
3745 let content = children
3746 .iter()
3747 .map(|child| child.to_markdown())
3748 .collect::<Vec<_>>()
3749 .join("\n\n");
3750
3751 content
3752 .lines()
3753 .map(|line| format!("> {}", line))
3754 .collect::<Vec<_>>()
3755 .join("\n")
3756 }
3757 BlockNode::Surface { name, children, .. } => {
3758 let content = children
3759 .iter()
3760 .map(|child| child.to_markdown())
3761 .collect::<Vec<_>>()
3762 .join("\n\n");
3763 quoted_source(Some(name), &content)
3764 }
3765 BlockNode::List {
3766 children,
3767 ordered,
3768 start,
3769 ..
3770 } => children
3771 .iter()
3772 .enumerate()
3773 .map(|(i, child)| {
3774 let prefix = if *ordered {
3775 format!("{}. ", ordered_list_ordinal(*start, i))
3776 } else {
3777 "- ".to_string()
3778 };
3779 format!("{}{}", prefix, child.to_markdown())
3780 })
3781 .collect::<Vec<_>>()
3782 .join("\n"),
3783 BlockNode::ListItem {
3784 children, checked, ..
3785 } => {
3786 let checkbox = if let Some(checked) = checked {
3787 if *checked { "[x] " } else { "[ ] " }
3788 } else {
3789 ""
3790 };
3791 format!(
3792 "{}{}",
3793 checkbox,
3794 children
3795 .iter()
3796 .map(|child| child.to_markdown())
3797 .collect::<Vec<_>>()
3798 .join("\n")
3799 )
3800 }
3801 BlockNode::CodeBlock(code_block) => {
3802 format!(
3803 "```{}\n{}\n```",
3804 code_block.lang.clone().unwrap_or_default(),
3805 code_block.code()
3806 )
3807 }
3808 BlockNode::Table(table) => table.to_markdown(),
3809 BlockNode::Break { html, .. } => {
3810 if *html {
3811 "<br>".to_string()
3812 } else {
3813 "\n".to_string()
3814 }
3815 }
3816 BlockNode::HorizontalRule { .. } => "---".to_string(),
3817 BlockNode::Custom { node, .. } => node.to_markdown(),
3818 BlockNode::Definition {
3819 identifier,
3820 url,
3821 title,
3822 ..
3823 } => {
3824 if let Some(title) = title {
3825 format!("[{}]: {} \"{}\"", identifier, url, title)
3826 } else {
3827 format!("[{}]: {}", identifier, url)
3828 }
3829 }
3830 BlockNode::Unknown { .. } => "".to_string(),
3831 }
3832 .trim()
3833 .to_string()
3834 }
3835}
3836
3837impl BlockNode {
3838 #[allow(clippy::too_many_arguments)]
3841 fn render_list_item_row(
3842 content: AnyElement,
3843 ix: usize,
3844 options: NodeRenderOptions,
3845 checked: Option<bool>,
3846 task_toggle: Option<(Arc<TaskToggleHandlerFn>, TaskToggleEvent)>,
3847 style: &TextViewStyle,
3848 line_height: Pixels,
3849 indent: Pixels,
3850 ) -> Div {
3851 h_flex()
3852 .w_full()
3853 .min_w_0()
3854 .relative()
3855 .items_start()
3856 .content_start()
3857 .when(!options.todo && checked.is_none(), |this| {
3858 this.child(
3862 div()
3863 .flex_none()
3864 .min_w(indent)
3865 .pr(indent * 0.3)
3866 .text_right()
3867 .text_color(style.muted_foreground())
3868 .refine_style(style.list_style())
3869 .child(
3870 list_item_prefix(
3871 ix,
3872 options.list_start,
3873 options.ordered,
3874 options.depth,
3875 )
3876 .trim_end()
3877 .to_string(),
3878 ),
3879 )
3880 })
3881 .when_some(checked, |this, checked| {
3882 let check_svg = if style.is_dark() {
3884 CHECK_SVG_DARK
3885 } else {
3886 CHECK_SVG_LIGHT
3887 };
3888 this.child(
3889 div()
3890 .flex()
3891 .mr_2()
3892 .h(line_height)
3893 .flex_none()
3894 .items_center()
3895 .justify_center()
3896 .child(
3897 div()
3898 .flex()
3899 .size(rems(0.9375))
3900 .items_center()
3901 .justify_center()
3902 .rounded(px(4.))
3903 .border(px(1.5))
3904 .border_color(
3905 style
3906 .list_style()
3907 .text
3908 .color
3909 .unwrap_or(style.muted_foreground()),
3910 )
3911 .when(checked, |this| {
3912 this.bg(style.link()).border_color(style.link()).child(
3913 img(Arc::new(Image::from_bytes(
3914 ImageFormat::Svg,
3915 check_svg.to_vec(),
3916 )))
3917 .size(rems(0.75)),
3918 )
3919 }),
3920 )
3921 .map(|checkbox| match task_toggle {
3922 Some((handler, event)) => checkbox
3923 .id(("task-toggle", event.item_range().start))
3924 .on_click(move |_, window, cx| {
3925 crate::TextSelection::end(window, cx);
3926 cx.stop_propagation();
3927 handler(&event, window, cx);
3928 })
3929 .into_any_element(),
3930 None => checkbox.into_any_element(),
3931 }),
3932 )
3933 })
3934 .child(
3935 div()
3936 .flex_auto()
3939 .min_w_0()
3940 .overflow_hidden()
3941 .when(checked == Some(true), |this| {
3943 this.text_color(style.muted_foreground())
3944 })
3945 .child(content),
3946 )
3947 }
3948
3949 fn render_list_item(
3950 item: &BlockNode,
3951 ix: usize,
3952 options: NodeRenderOptions,
3953 node_cx: &NodeContext,
3954 window: &mut Window,
3955 cx: &mut App,
3956 ) -> AnyElement {
3957 match item {
3958 BlockNode::ListItem {
3959 children,
3960 spread,
3961 checked,
3962 task_marker,
3963 span,
3964 } => {
3965 let body = body_font_size(node_cx, window);
3966 let indent = body * 1.4;
3967 let task_toggle = match (&node_cx.task_toggle_handler, checked, span, task_marker) {
3968 (Some(handler), Some(checked), Some(item), Some(marker)) => Some((
3969 handler.clone(),
3970 TaskToggleEvent::new(
3971 !checked,
3972 item.start..item.end,
3973 marker.start..marker.end,
3974 ),
3975 )),
3976 _ => None,
3977 };
3978 div()
3979 .w_full()
3980 .min_w_0()
3981 .when(ix > 0, |this| this.pt(body * 0.25))
3983 .when(*spread, |this| this.child(div()))
3984 .children({
3985 let mut items: Vec<Div> = Vec::with_capacity(children.len());
3986
3987 for (child_ix, child) in children.iter().enumerate() {
3988 match child {
3989 BlockNode::Paragraph { .. } => {
3990 let last_not_list = child_ix > 0
3991 && !matches!(
3992 children[child_ix - 1],
3993 BlockNode::List { .. }
3994 );
3995
3996 let text = child.render_block(
3997 NodeRenderOptions {
3998 depth: options.depth + 1,
3999 todo: checked.is_some(),
4000 is_last: true,
4001 ..options
4002 },
4003 node_cx,
4004 window,
4005 cx,
4006 );
4007
4008 if last_not_list {
4012 if let Some(preceding_row) = items.pop() {
4013 items.push(
4014 div().child(preceding_row).child(
4015 div()
4016 .w_full()
4017 .pl(indent)
4018 .overflow_hidden()
4019 .child(text),
4020 ),
4021 );
4022 continue;
4023 }
4024 }
4025
4026 items.push(Self::render_list_item_row(
4027 text,
4028 ix,
4029 options,
4030 *checked,
4031 task_toggle.clone(),
4032 &node_cx.style,
4033 window.line_height(),
4034 indent,
4035 ));
4036 }
4037 BlockNode::List { .. } => {
4038 items.push(div().ml(indent).child(child.render_block(
4039 NodeRenderOptions {
4040 depth: options.depth + 1,
4041 todo: checked.is_some(),
4042 is_last: true,
4043 ..options
4044 },
4045 node_cx,
4046 window,
4047 cx,
4048 )));
4049 }
4050 BlockNode::Root { .. }
4051 | BlockNode::Heading { .. }
4052 | BlockNode::Blockquote { .. }
4053 | BlockNode::Surface { .. }
4054 | BlockNode::CodeBlock(_)
4055 | BlockNode::Custom { .. }
4056 | BlockNode::Table(_)
4057 | BlockNode::HorizontalRule { .. } => {
4058 let block = child.render_block(
4059 NodeRenderOptions {
4060 depth: options.depth + 1,
4061 todo: checked.is_some(),
4062 is_last: true,
4063 ..options
4064 },
4065 node_cx,
4066 window,
4067 cx,
4068 );
4069
4070 if child_ix == 0 {
4071 items.push(Self::render_list_item_row(
4072 block,
4073 ix,
4074 options,
4075 *checked,
4076 task_toggle.clone(),
4077 &node_cx.style,
4078 window.line_height(),
4079 indent,
4080 ));
4081 } else {
4082 items.push(
4086 div()
4087 .w_full()
4088 .min_w_0()
4089 .pl(indent)
4090 .overflow_hidden()
4091 .child(block),
4092 );
4093 }
4094 }
4095 BlockNode::ListItem { .. }
4096 | BlockNode::Break { .. }
4097 | BlockNode::Definition { .. }
4098 | BlockNode::Unknown => {}
4099 }
4100 }
4101 items
4102 })
4103 .into_any_element()
4104 }
4105 _ => div().into_any_element(),
4106 }
4107 }
4108
4109 fn render_table(
4131 item: &BlockNode,
4132 options: &NodeRenderOptions,
4133 node_cx: &NodeContext,
4134 text_size: Pixels,
4135 window: &mut Window,
4136 cx: &mut App,
4137 ) -> impl IntoElement {
4138 const CELL_WRAP_MAX_LINES: f32 = 2.0;
4144 const CELL_WRAP_MIN_PX: f32 = 160.0;
4145 const CELL_WRAP_MAX_PX: f32 = 480.0;
4146 const TABLE_BORDER_PX: f32 = 2.0; let table = match item {
4149 BlockNode::Table(table) => table,
4150 _ => return div().into_any_element(),
4151 };
4152
4153 let style = &node_cx.style;
4154 let col_count = table
4155 .children
4156 .iter()
4157 .map(|row| row.children.len())
4158 .max()
4159 .unwrap_or(0);
4160 let col_w = measure_table_columns(table, col_count, node_cx, window, cx);
4161 let scroll = matches!(style.table().overflow.x, Some(Overflow::Scroll));
4162 let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
4163 let rem_size = window.rem_size();
4164 let wrap_min_px = f32::from(rems(4.).to_pixels(rem_size));
4165 let padding_delta = table_cell_padding_x(style, rem_size) - CELL_PAD_PX;
4167 let scale_factor = window.scale_factor();
4171 let device_ceil = |width: f32| (width * scale_factor).ceil() / scale_factor;
4172 let columns: Vec<TableColumn> = col_w
4173 .iter()
4174 .enumerate()
4175 .map(|(ix, &w)| {
4176 let w = w + padding_delta;
4177 let floor = if scroll && nowrap {
4178 w
4179 } else if scroll {
4180 (w / CELL_WRAP_MAX_LINES)
4181 .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
4182 .min(w)
4183 } else {
4184 w.min(wrap_min_px)
4185 };
4186 let divider = if ix + 1 < col_count {
4187 CELL_BORDER_PX
4188 } else {
4189 0.
4190 };
4191 let max = device_ceil(w - divider);
4192 TableColumn {
4193 min: device_ceil(floor - divider).min(max),
4194 max,
4195 divider,
4196 }
4197 })
4198 .collect();
4199
4200 let rows = Self::render_table_rows(table, &columns, node_cx, window, cx);
4201 let frame = StyleRefinement::default()
4202 .text_size(text_size)
4203 .bg(style
4204 .table_background()
4205 .unwrap_or(cx.theme().tokens.colors.surface))
4206 .border_1()
4207 .border_color(style.border())
4208 .refine_style(style.table());
4209 let body = if scroll {
4210 let scroll_handle = window
4211 .use_keyed_state(
4212 block_element_id("table-scroll", table.span, options.ix),
4213 cx,
4214 |_, _| ScrollHandle::default(),
4215 )
4216 .read(cx)
4217 .clone();
4218 let min_total_w = columns
4219 .iter()
4220 .map(|column| column.min + column.divider)
4221 .sum::<f32>()
4222 + TABLE_BORDER_PX;
4223 horizontal_scroll_area(
4233 block_element_id("table", table.span, options.ix),
4234 &scroll_handle,
4235 &frame,
4236 div().min_w_full().w(px(min_total_w)).children(rows),
4242 )
4243 .into_any_element()
4244 } else {
4245 div()
4246 .id(block_element_id("table", table.span, options.ix))
4249 .w_full()
4250 .overflow_hidden()
4251 .refine_style(&frame)
4252 .children(rows)
4253 .into_any_element()
4254 };
4255
4256 div()
4257 .pb(block_gap(options, style))
4258 .w_full()
4259 .child(body)
4260 .children(node_cx.table_actions.clone().map(|f| {
4267 div()
4268 .id(block_element_id("table-actions", table.span, options.ix))
4269 .mt_1()
4270 .child(f(&table.cached_table_data(), window, cx))
4271 }))
4272 .into_any_element()
4273 }
4274
4275 fn table_row_corner_radii(style: &TextViewStyle, window: &Window) -> [Option<Pixels>; 4] {
4287 let rem_size = window.rem_size();
4288 let inset = |radius: Option<gpui::AbsoluteLength>| {
4289 radius.map(|radius| (radius.to_pixels(rem_size) - px(1.)).max(px(0.)))
4290 };
4291 let radii = &style.table().corner_radii;
4292 [
4293 inset(radii.top_left),
4294 inset(radii.top_right),
4295 inset(radii.bottom_left),
4296 inset(radii.bottom_right),
4297 ]
4298 }
4299
4300 fn render_table_rows(
4314 table: &Table,
4315 columns: &[TableColumn],
4316 node_cx: &NodeContext,
4317 window: &mut Window,
4318 cx: &mut App,
4319 ) -> Vec<AnyElement> {
4320 const CELL_GROW: f32 = 1e6;
4326 const SLACK_GROW: f32 = 1.0;
4327
4328 let style = &node_cx.style;
4329 let col_count = columns.len();
4330 let total_w = columns.iter().map(|column| column.max).sum::<f32>().max(1.);
4331 let mut slack_style = StyleRefinement::default();
4334 slack_style.background = style.table_cell().background.clone();
4335 slack_style.border_color = style.table_cell().border_color;
4336 let slack = |grow: f32| {
4337 div()
4338 .flex_basis(px(0.))
4339 .flex_grow(grow)
4340 .flex_shrink(0.)
4341 .border_color(style.border())
4342 .refine_style(&slack_style)
4343 };
4344
4345 let row_count = table.children.len();
4346 let [top_left, top_right, bottom_left, bottom_right] =
4347 Self::table_row_corner_radii(style, window);
4348 let mut rows = Vec::with_capacity(row_count);
4349 let mut cell_ordinal = 0;
4350 for (row_ix, row) in table.children.iter().enumerate() {
4351 let mut cells = Vec::with_capacity(col_count * 2);
4352 for (ix, column) in columns.iter().enumerate() {
4353 let is_last_col = ix + 1 == col_count;
4354 let align = table.column_align(ix);
4355 let content = row.children.get(ix).map(|cell| {
4356 let fade_key = table
4357 .span
4358 .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
4359 cell_ordinal += 1;
4360 cell.children.render(fade_key, node_cx, window, cx)
4361 });
4362
4363 let grow = SLACK_GROW * column.max / total_w;
4364 let (lead, trail) = match align {
4365 ColumnumnAlign::None | ColumnumnAlign::Left => (0., grow),
4366 ColumnumnAlign::Center => (grow / 2., grow / 2.),
4367 ColumnumnAlign::Right => (grow, 0.),
4368 };
4369 if lead > 0. {
4370 cells.push(slack(lead).into_any_element());
4371 }
4372 cells.push(
4373 TableCellElement(
4374 div()
4375 .flex_basis(px(0.))
4376 .flex_grow(CELL_GROW)
4377 .min_w(px(column.min))
4378 .max_w(px(column.max))
4379 .overflow_hidden()
4380 .when(align == ColumnumnAlign::Center, |this| {
4386 this.flex().flex_col().items_center()
4387 })
4388 .when(align == ColumnumnAlign::Right, |this| {
4389 this.flex().flex_col().items_end()
4390 })
4391 .px_3()
4392 .py(px(7.))
4393 .refine_style(&style.table_cell())
4394 .children(content)
4395 .into_any_element(),
4396 )
4397 .into_any_element(),
4398 );
4399 if trail > 0. || !is_last_col {
4400 cells.push(
4401 slack(trail)
4402 .when(!is_last_col, |this| this.border_r_1())
4403 .into_any_element(),
4404 );
4405 }
4406 }
4407 rows.push(
4408 div()
4409 .id(("row", row_ix))
4412 .w_full()
4413 .when(row_ix < row_count - 1, |this| this.border_b_1())
4414 .border_color(style.border())
4415 .flex()
4416 .flex_row()
4417 .when(row_ix == 0, |this| {
4421 this.bg(style.code_background())
4422 .text_color(style.foreground())
4423 .when_some(top_left, |this, radius| this.rounded_tl(radius))
4424 .when_some(top_right, |this, radius| this.rounded_tr(radius))
4425 .refine_style(&style.table_head())
4426 })
4427 .when(row_ix + 1 == row_count, |this| {
4428 this.when_some(bottom_left, |this, radius| this.rounded_bl(radius))
4429 .when_some(bottom_right, |this, radius| this.rounded_br(radius))
4430 })
4431 .when_some(
4433 style.table_row_hover().filter(|_| row_ix > 0),
4434 |this, color| this.hover(|this| this.bg(color)),
4435 )
4436 .children(cells)
4437 .into_any_element(),
4438 );
4439 }
4440 rows
4441 }
4442
4443 pub(crate) fn render_block(
4444 &self,
4445 options: NodeRenderOptions,
4446 node_cx: &NodeContext,
4447 window: &mut Window,
4448 cx: &mut App,
4449 ) -> AnyElement {
4450 let mb = block_gap(&options, &node_cx.style);
4451
4452 match self {
4453 BlockNode::Root { children, .. } => div()
4454 .children(children.into_iter().enumerate().map(move |(ix, node)| {
4455 let (is_first, prev) = match flow_position(children, ix) {
4459 (true, _) => (options.is_first, options.prev),
4460 (false, prev) => (false, prev),
4461 };
4462 node.render_block(
4463 NodeRenderOptions {
4464 ix,
4465 is_first,
4466 prev,
4467 is_last: options.is_last && ix + 1 == children.len(),
4468 ..options
4469 },
4470 node_cx,
4471 window,
4472 cx,
4473 )
4474 }))
4475 .into_any_element(),
4476 BlockNode::Paragraph(paragraph) => gapped(
4477 paragraph.render(
4478 paragraph.span.map(|span| TextLeafKey::block(span.start)),
4479 node_cx,
4480 window,
4481 cx,
4482 ),
4483 mb,
4484 ),
4485 BlockNode::Heading {
4486 level,
4487 children,
4488 span,
4489 } => {
4490 let (_, line_height, section_gap) = heading_metrics(*level);
4491 let refinement = node_cx.style.heading(*level);
4492 let text_size = heading_size(*level, node_cx, window);
4493 let top = if options.is_first || options.depth > 0 {
4499 px(0.)
4500 } else {
4501 match options.prev {
4502 PrevBlock::Heading(_) => text_size * 0.25,
4503 PrevBlock::Rule => (text_size * section_gap
4504 - rule_space(body_font_size(node_cx, window)))
4505 .max(px(0.)),
4506 PrevBlock::Other => (text_size * section_gap
4507 - node_cx.style.paragraph_gap().to_pixels(window.rem_size()))
4508 .max(px(0.)),
4509 }
4510 };
4511 let bottom = if options.is_last {
4512 px(0.)
4513 } else {
4514 text_size * HEADING_BOTTOM_GAP
4515 };
4516
4517 div()
4518 .pt(top)
4519 .pb(bottom)
4520 .whitespace_normal()
4521 .text_size(text_size)
4522 .line_height(relative(line_height))
4523 .font_weight(FontWeight::SEMIBOLD)
4524 .when(*level >= 6, |this| {
4525 this.text_color(node_cx.style.muted_foreground())
4526 })
4527 .refine_style(&refinement)
4528 .child(children.render(
4529 span.map(|span| TextLeafKey::block(span.start)),
4530 node_cx,
4531 window,
4532 cx,
4533 ))
4534 .into_any_element()
4535 }
4536 BlockNode::Blockquote { children, .. } => gapped(
4537 div()
4538 .w_full()
4539 .text_color(node_cx.style.muted_foreground())
4540 .border_l_2()
4541 .border_color(node_cx.style.border())
4542 .pl(body_font_size(node_cx, window) * 0.9)
4543 .refine_style(node_cx.style.blockquote_style())
4544 .children({
4546 let children_len = children.len();
4547 children.into_iter().enumerate().map(move |(index, c)| {
4548 let is_last = index == children_len - 1;
4549 let (is_first, prev) = flow_position(children, index);
4550 window.with_text_style(
4551 Some(node_cx.style.blockquote_style().text.clone()),
4552 |window| {
4553 c.render_block(
4554 NodeRenderOptions {
4555 is_first,
4556 prev,
4557 ..options.is_last(is_last)
4558 },
4559 node_cx,
4560 window,
4561 cx,
4562 )
4563 },
4564 )
4565 })
4566 })
4567 .into_any_element(),
4568 mb,
4569 ),
4570 BlockNode::Surface { name, children, .. } => {
4571 if children.iter().all(Self::draws_nothing) {
4572 return div().into_any_element();
4574 }
4575 let mut container = node_cx
4579 .markdown_extensions
4580 .render_block_surface(name, window, cx)
4581 .unwrap_or_else(div)
4582 .map(|mut this| {
4583 let style = this.style();
4584 let unset_width = style.size.width.is_none();
4585 let unset_min_width = style.min_size.width.is_none();
4586 this.when(unset_width, |this| this.w_full())
4587 .when(unset_min_width, |this| this.min_w_0())
4588 });
4589 let text_style = container.style().text.clone();
4592 let blocks = window.with_text_style(Some(text_style), |window| {
4593 children
4594 .iter()
4595 .enumerate()
4596 .map(|(ix, child)| {
4597 let (is_first, prev) = flow_position(children, ix);
4598 child.render_block(
4599 NodeRenderOptions {
4600 is_first,
4601 prev,
4602 ..options
4603 .is_last(children[ix + 1..].iter().all(Self::draws_nothing))
4604 },
4605 node_cx,
4606 window,
4607 cx,
4608 )
4609 })
4610 .collect::<Vec<_>>()
4611 });
4612 gapped(container.children(blocks).into_any_element(), mb)
4613 }
4614 BlockNode::List {
4615 children,
4616 ordered,
4617 start,
4618 ..
4619 } => div()
4620 .w_full()
4621 .min_w_0()
4622 .pb(mb)
4623 .children({
4624 let mut items = Vec::with_capacity(children.len());
4625 let mut item_index = 0;
4626 for (ix, item) in children.into_iter().enumerate() {
4627 let is_item = item.is_list_item();
4628
4629 items.push(Self::render_list_item(
4630 item,
4631 item_index,
4632 NodeRenderOptions {
4633 ix,
4634 ordered: *ordered,
4635 list_start: *start,
4636 ..options
4637 },
4638 node_cx,
4639 window,
4640 cx,
4641 ));
4642
4643 if is_item {
4644 item_index += 1;
4645 }
4646 }
4647 items
4648 })
4649 .into_any_element(),
4650 BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
4651 BlockNode::Custom { node, state } => {
4652 let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
4653 Some(rendered) => rendered,
4654 None => div().child(node.as_text().to_string()).into_any_element(),
4655 };
4656
4657 div()
4658 .pb(mb)
4659 .child(CustomBlockElement {
4660 content: inner,
4661 state: state.clone(),
4662 reveal: node_cx.reveal_at(
4663 node.span.map(|span| TextLeafKey::block(span.start)),
4664 0,
4665 1,
4666 ),
4667 })
4668 .into_any_element()
4669 }
4670 BlockNode::Table { .. } => {
4671 let text_size = body_font_size(node_cx, window) * (14. / 15.);
4676 window.with_text_style(
4677 Some(TextStyleRefinement {
4678 font_size: Some(text_size.into()),
4679 ..Default::default()
4680 }),
4681 |window| {
4682 Self::render_table(self, &options, node_cx, text_size, window, cx)
4683 .into_any_element()
4684 },
4685 )
4686 }
4687 BlockNode::HorizontalRule { .. } => {
4688 let space = rule_space(body_font_size(node_cx, window));
4692 let top = if options.is_first || options.depth > 0 {
4693 px(0.)
4694 } else {
4695 let above = match options.prev {
4696 PrevBlock::Rule => space,
4697 PrevBlock::Heading(level) => {
4698 heading_size(level, node_cx, window) * HEADING_BOTTOM_GAP
4699 }
4700 PrevBlock::Other => {
4701 node_cx.style.paragraph_gap().to_pixels(window.rem_size())
4702 }
4703 };
4704 (space - above).max(px(0.))
4705 };
4706 div()
4707 .pt(top)
4708 .when(!options.is_last, |this| this.pb(space))
4709 .child(div().bg(node_cx.style.border()).h(px(1.)))
4710 .into_any_element()
4711 }
4712 BlockNode::Break { .. } => div().into_any_element(),
4713 BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
4714 _ => {
4715 if cfg!(debug_assertions) {
4716 tracing::warn!("unknown implementation: {:?}", self);
4717 }
4718
4719 div().into_any_element()
4720 }
4721 }
4722 }
4723}
4724
4725#[cfg(test)]
4726mod tests {
4727 use super::*;
4728
4729 #[test]
4730 fn paragraph_highlights_preserve_overlaps_and_utf8_boundaries() {
4731 let mut app = gpui::TestApp::new();
4732 let mut window = app.open_window(|_, _| gpui::Empty);
4733 window.update(|_, _, cx| {
4734 cx.set_global(crate::Theme::default());
4735 let context = NodeContext::default();
4736 let link = LinkMark {
4737 url: "https://example.com".into(),
4738 ..Default::default()
4739 };
4740 let paragraph = Paragraph {
4741 children: vec![
4742 InlineNode::new("a中😀").marks(vec![
4743 (0..8, TextMark::default().bold()),
4744 (1..8, TextMark::default().italic()),
4745 (4..8, TextMark::default().code()),
4746 ]),
4747 InlineNode::new("文b").marks(vec![
4748 (0..4, TextMark::default().link(link.clone())),
4749 (0..3, TextMark::default().bold()),
4750 ]),
4751 ],
4752 ..Default::default()
4753 };
4754 let measured = paragraph.inline_highlights(&context, cx);
4755 let (text, highlights, links) = paragraph.plain_render(&context, cx);
4756 assert_eq!(text.as_ref(), "a中😀文b");
4757 assert_eq!(links, vec![(8..12, link)]);
4758 for spans in [&measured, &highlights] {
4759 assert_eq!(
4760 spans
4761 .iter()
4762 .map(|(range, _)| range.clone())
4763 .collect::<Vec<_>>(),
4764 vec![0..1, 1..4, 4..8, 8..11, 11..12]
4765 );
4766 for ix in [1, 2] {
4767 assert_eq!(spans[ix].1.style.font_weight, Some(FontWeight::BOLD));
4768 assert_eq!(spans[ix].1.style.font_style, Some(FontStyle::Italic));
4769 }
4770 assert!(spans[2].1.code_style.is_some());
4771 assert_eq!(spans[2].1.font_size_scale, Some(0.875));
4772 assert_eq!(spans[3].1.style.font_weight, Some(FontWeight::BOLD));
4773 assert_eq!(spans[3].1.style.font_style, None);
4774 assert!(spans[3].1.code_style.is_none());
4775 assert_eq!(spans[4].1.style.font_weight, None);
4776 }
4777 assert!(highlights[3..].iter().all(|(_, highlight)| {
4778 highlight.style.color == Some(context.style.link())
4779 && highlight.style.underline.is_some()
4780 }));
4781 let items = paragraph.inline_flow_items(None, &[], &[], &context, cx);
4782 let [
4783 InlineFlowItem::Text {
4784 text: flow_text,
4785 highlights: flow_highlights,
4786 links: flow_links,
4787 ..
4788 },
4789 ] = items.as_slice()
4790 else {
4791 panic!("one text item");
4792 };
4793 assert_eq!(flow_text, &text);
4794 assert_eq!(flow_highlights, &highlights);
4795 assert_eq!(flow_links, &links);
4796
4797 let paragraph = Paragraph {
4799 children: vec![
4800 InlineNode::new("a").marks(vec![(0..2, TextMark::default().bold())]),
4801 InlineNode::new("bc").marks(vec![(0..2, TextMark::default().italic())]),
4802 ],
4803 ..Default::default()
4804 };
4805 let highlights = paragraph.inline_highlights(&context, cx);
4806 assert_eq!(
4807 highlights
4808 .iter()
4809 .map(|(range, _)| range.clone())
4810 .collect::<Vec<_>>(),
4811 vec![0..1, 1..2, 2..3]
4812 );
4813 assert_eq!(highlights[1].1.style.font_weight, Some(FontWeight::BOLD));
4814 assert_eq!(highlights[1].1.style.font_style, Some(FontStyle::Italic));
4815 assert_eq!(highlights[2].1.style.font_weight, None);
4816 });
4817 }
4818
4819 #[test]
4820 fn paragraph_highlight_builders_merge_each_mark_once() {
4821 use super::super::inline::COMBINED_HIGHLIGHT_INPUTS;
4822
4823 let mut app = gpui::TestApp::new();
4824 let mut window = app.open_window(|_, _| gpui::Empty);
4825 window.update(|_, _, cx| {
4826 cx.set_global(crate::Theme::default());
4827 let context = NodeContext::default();
4828 let inputs = || COMBINED_HIGHLIGHT_INPUTS.with(|inputs| inputs.get());
4829 for node_count in [64, 128] {
4830 let paragraph = Paragraph {
4831 children: (0..node_count)
4832 .map(|_| {
4833 InlineNode::new("a中😀").marks(vec![
4834 (0..8, TextMark::default().bold()),
4835 (1..8, TextMark::default().italic()),
4836 (4..8, TextMark::default().code()),
4837 ])
4838 })
4839 .collect(),
4840 ..Default::default()
4841 };
4842 let before = inputs();
4843 paragraph.inline_highlights(&context, cx);
4844 assert_eq!(inputs() - before, 3 * node_count);
4845 let before = inputs();
4846 paragraph.plain_render(&context, cx);
4847 assert_eq!(inputs() - before, 3 * node_count);
4848 let before = inputs();
4849 paragraph.inline_flow_items(None, &[], &[], &context, cx);
4850 assert_eq!(inputs() - before, 3 * node_count);
4851 }
4852 });
4853 }
4854
4855 #[test]
4856 fn host_foregrounds_follow_the_render_context_without_entering_the_paragraph_cache() {
4857 let mut app = gpui::TestApp::new();
4858 let mut window = app.open_window(|_, _| gpui::Empty);
4859 window.update(|_, _, cx| {
4860 cx.set_global(crate::Theme::default());
4861 let link = LinkMark {
4862 url: "https://example.com".into(),
4863 ..Default::default()
4864 };
4865 let mut paragraph = Paragraph::default().with_leaf_key(7);
4866 paragraph.push(InlineNode::new("a中😀link").marks(vec![
4867 (0..12, TextMark::default().bold()),
4868 (1..12, TextMark::default().italic().link(link.clone())),
4869 ]));
4870 let original = paragraph.clone();
4871 let base = NodeContext::default();
4872 let (_, inherited, _) = paragraph.plain_render(&base, cx);
4873 let cached = paragraph.render_cache.0.lock().unwrap().clone().unwrap();
4874 for color in [gpui::red(), gpui::blue()] {
4875 let context = NodeContext {
4876 foregrounds: Some(Arc::new(HashMap::from([(7, vec![(0..12, color)])]))),
4877 ..base.clone()
4878 };
4879 let (text, highlights, links) = paragraph.plain_render(&context, cx);
4880 assert_eq!(text.as_ref(), "a中😀link");
4881 assert_eq!(links, vec![(1..12, link.clone())]);
4882 assert!(highlights.iter().all(|(_, h)| h.style.color == Some(color)));
4883 assert!(
4884 highlights
4885 .iter()
4886 .all(|(_, h)| h.style.font_weight == Some(FontWeight::BOLD))
4887 );
4888 assert!(
4889 highlights
4890 .iter()
4891 .filter(|(r, _)| r.start >= 1)
4892 .all(|(_, h)| h.style.font_style == Some(FontStyle::Italic)
4893 && h.style.underline.is_some())
4894 );
4895 assert!(Arc::ptr_eq(
4896 &cached,
4897 paragraph.render_cache.0.lock().unwrap().as_ref().unwrap()
4898 ));
4899 }
4900 let context = NodeContext {
4903 foregrounds: Some(Arc::new(HashMap::from([(7, vec![])]))),
4904 ..base.clone()
4905 };
4906 assert_eq!(paragraph.plain_render(&context, cx).1, inherited);
4907 let invalid = NodeContext {
4908 foregrounds: Some(Arc::new(HashMap::from([(7, vec![(2..3, gpui::red())])]))),
4909 ..base.clone()
4910 };
4911 assert_eq!(paragraph.plain_render(&invalid, cx).1, inherited);
4912 let other_leaf = NodeContext {
4913 foregrounds: Some(Arc::new(HashMap::from([(8, vec![(0..12, gpui::red())])]))),
4914 ..base
4915 };
4916 assert_eq!(paragraph.plain_render(&other_leaf, cx).1, inherited);
4917 assert_eq!(paragraph, original);
4918 });
4919 }
4920
4921 #[test]
4922 fn host_foregrounds_override_mixed_links_and_preserve_atom_byte_coordinates() {
4923 let mut app = gpui::TestApp::new();
4924 let mut window = app.open_window(|_, _| gpui::Empty);
4925 window.update(|_, _, cx| {
4926 cx.set_global(crate::Theme::default());
4927 let link = LinkMark {
4928 url: "https://example.com".into(),
4929 ..Default::default()
4930 };
4931 let mut paragraph = Paragraph::default().with_leaf_key(7);
4932 paragraph.push(InlineNode::new("a中😀link").marks(vec![
4933 (0..12, TextMark::default().bold()),
4934 (1..12, TextMark::default().link(link.clone())),
4935 (4..8, TextMark::default().code()),
4936 ]));
4937 paragraph.push(
4938 InlineNode::custom(MarkdownNode::new("atom", ()).text("long fallback"))
4939 .marks(vec![(0..13, TextMark::default().link(link.clone()))]),
4940 );
4941 paragraph.push(InlineNode::new("尾"));
4942 paragraph.push(InlineNode::image(ImageNode::default()));
4943 paragraph.push(InlineNode::new("z"));
4944 let context = NodeContext {
4945 foregrounds: Some(Arc::new(HashMap::from([(
4946 7,
4947 vec![
4948 (0..22, gpui::red()),
4949 (15..18, gpui::blue()),
4950 (21..22, gpui::blue()),
4951 ],
4952 )]))),
4953 ..Default::default()
4954 };
4955 let items = paragraph.inline_flow_items(None, &[], &[], &context, cx);
4956 assert_eq!(items.len(), 5);
4957 let InlineFlowItem::Text {
4958 text,
4959 highlights,
4960 links,
4961 ..
4962 } = &items[0]
4963 else {
4964 panic!("text");
4965 };
4966 assert_eq!(text.as_ref(), "a中😀link");
4967 assert_eq!(links, &vec![(1..12, link.clone())]);
4968 assert!(
4969 highlights
4970 .iter()
4971 .all(|(_, h)| h.style.color == Some(gpui::red()))
4972 );
4973 assert!(
4974 highlights
4975 .iter()
4976 .any(|(r, h)| r.start == 4 && h.code_style.is_some())
4977 );
4978 let InlineFlowItem::Object {
4979 style,
4980 link: actual_link,
4981 ..
4982 } = &items[1]
4983 else {
4984 panic!("object");
4985 };
4986 assert_eq!(style.color, Some(gpui::red()));
4987 assert_eq!(actual_link.as_ref(), Some(&link));
4988 assert!(style.underline.is_some());
4989 for ix in [2, 4] {
4990 let InlineFlowItem::Text { highlights, .. } = &items[ix] else {
4991 panic!("trailing text");
4992 };
4993 assert!(
4994 highlights
4995 .iter()
4996 .all(|(_, h)| h.style.color == Some(gpui::blue()))
4997 );
4998 assert!(!highlights.is_empty());
4999 }
5000 });
5001 }
5002
5003 #[test]
5004 fn block_plugin_selection_copies_both_formats_and_clears_synchronously() {
5005 let source = "$$\nx\n$$";
5006 let mut node = MarkdownNode::new("math", ()).text("x").markdown(source);
5007 node.set_span(Some(Span {
5008 start: 0,
5009 end: source.len(),
5010 }));
5011 let state = BlockState::default();
5012 let same_content = BlockNode::Custom {
5013 node: node.clone(),
5014 state: BlockState::default(),
5015 };
5016 let block = BlockNode::Custom {
5017 node,
5018 state: state.clone(),
5019 };
5020 let document = crate::text::document::ParsedDocument {
5021 source: source.into(),
5022 blocks: Arc::new(vec![block.clone()]),
5023 };
5024 assert!(!block.has_selection());
5025 state.set_selection(Some(BlockSelection::Full));
5026 assert_eq!(block, same_content);
5028 for blocks in [None, Some(0..=0)] {
5029 assert_eq!(
5030 document.selected_text(SelectionFormat::Plain, blocks.clone()),
5031 "x\n"
5032 );
5033 assert_eq!(
5034 document.selected_text(SelectionFormat::Source, blocks),
5035 source
5036 );
5037 }
5038 assert_eq!(document.selected_source_range(), Some(0..source.len()));
5039
5040 block.clear_selection();
5042 assert!(!document.blocks[0].has_selection());
5043 assert_eq!(
5044 document.selected_text(SelectionFormat::Plain, Some(0..=0)),
5045 ""
5046 );
5047 assert_eq!(document.selected_source_range(), None);
5048 }
5049
5050 #[test]
5051 fn block_plugin_empty_endpoint_suppresses_virtualized_copy_fallback() {
5052 let state = BlockState::default();
5053 let block = BlockNode::Custom {
5054 node: MarkdownNode::new("math", ())
5055 .text("formula")
5056 .markdown("$$formula$$"),
5057 state: state.clone(),
5058 };
5059 let after = CodeBlock::from_code("after", None::<SharedString>);
5060 after.set_selection(0..5);
5061 let document = crate::text::document::ParsedDocument {
5062 blocks: Arc::new(vec![block, BlockNode::CodeBlock(after)]),
5063 ..Default::default()
5064 };
5065 assert!(
5066 document
5067 .selected_text(SelectionFormat::Plain, Some(0..=1))
5068 .contains("formula")
5069 );
5070 state.set_selection(Some(BlockSelection::Empty));
5071 assert!(document.blocks[0].has_selection());
5072 for format in [SelectionFormat::Plain, SelectionFormat::Source] {
5073 let text = document.selected_text(format, Some(0..=1));
5074 assert!(text.contains("after"));
5075 assert!(!text.contains("formula"));
5076 }
5077 }
5078
5079 #[test]
5080 fn block_plugin_geometry_selects_whole_blocks_in_both_drag_directions() {
5081 let bounds = Bounds::new(gpui::point(px(10.), px(20.)), gpui::size(px(100.), px(40.)));
5082 let inside = bounds.center();
5083 let above = gpui::point(px(0.), px(0.));
5084 let below = gpui::point(px(0.), px(80.));
5085 for (start, end) in [(above, inside), (inside, below), (above, below)] {
5086 assert!(custom_block_is_selected(bounds, start, end));
5087 assert!(custom_block_is_selected(bounds, end, start));
5088 }
5089 assert!(!custom_block_is_selected(bounds, inside, inside));
5090 for (start, end) in [
5093 (inside, inside + gpui::point(px(1.), px(0.))),
5094 (
5095 bounds.origin,
5096 bounds.bottom_right() - gpui::point(px(1.), px(1.)),
5097 ),
5098 ] {
5099 assert!(!custom_block_is_selected(bounds, start, end));
5100 assert!(!custom_block_is_selected(bounds, end, start));
5101 }
5102 assert!(!custom_block_is_selected(
5103 bounds,
5104 gpui::point(px(150.), px(30.)),
5105 gpui::point(px(160.), px(40.))
5106 ));
5107 }
5108
5109 #[test]
5110 fn selected_inline_objects_coalesce_surrounding_emphasis() {
5111 for (object_mark, expected) in [
5112 (TextMark::default().italic(), "*fore $x$ aft*"),
5113 (TextMark::default().italic().bold(), "*fore **$x$** aft*"),
5114 ] {
5115 let italic = TextMark::default().italic();
5116 let before = InlineNode::new("before ").marks(vec![(0..7, italic.clone())]);
5117 let formula =
5118 InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"))
5119 .marks(vec![(0..1, object_mark)]);
5120 let after = InlineNode::new(" after").marks(vec![(0..6, italic)]);
5121 {
5122 let mut state = formula.state.lock().unwrap();
5123 state.text = "before ".into();
5124 state.selection = Some((2..7).into());
5125 }
5126 *formula.custom_selection.lock().unwrap() = true;
5127 let paragraph = Paragraph {
5128 children: vec![before, formula, after],
5129 ..Default::default()
5130 };
5131 {
5132 let mut state = paragraph.state.lock().unwrap();
5133 state.text = " after".into();
5134 state.selection = Some((0..4).into());
5135 }
5136 assert_eq!(paragraph.selected_source(), expected);
5137 }
5138 }
5139
5140 #[test]
5141 fn consecutive_inline_objects_copy_atomically_without_neighboring_text() {
5142 let first =
5143 InlineNode::custom(MarkdownNode::new("math", ()).text("甲²").markdown("$甲^2$"));
5144 let second = InlineNode::custom(MarkdownNode::new("math", ()).text("b").markdown("$b$"));
5145 *first.custom_selection.lock().unwrap() = true;
5146 *second.custom_selection.lock().unwrap() = true;
5147 let paragraph = Paragraph {
5148 children: vec![first, second],
5149 ..Default::default()
5150 };
5151 assert_eq!(paragraph.selected_text(), "甲²b");
5152 assert_eq!(paragraph.selected_source(), "$甲^2$$b$");
5153 assert!(paragraph.has_selection());
5154 paragraph.clear_selection();
5155 assert_eq!(paragraph.selected_text(), "");
5156 assert_eq!(paragraph.selected_source(), "");
5157 assert!(!paragraph.has_selection());
5158 }
5159
5160 #[test]
5161 fn selected_inline_object_interleaves_runs_and_preserves_enclosing_mark() {
5162 let before = InlineNode::new("中文 ");
5163 let formula =
5164 InlineNode::custom(MarkdownNode::new("math", ()).text("x²").markdown("$x^2$"))
5165 .marks(vec![(0..3, TextMark::default().bold())]);
5166 let after = InlineNode::new(" English");
5167 {
5168 let mut preceding = formula.state.lock().unwrap();
5169 preceding.text = "中文 ".into();
5170 preceding.selection = Some((3..7).into());
5171 }
5172 *formula.custom_selection.lock().unwrap() = true;
5173 let paragraph = Paragraph {
5174 children: vec![before, formula, after],
5175 ..Default::default()
5176 };
5177 {
5178 let mut trailing = paragraph.state.lock().unwrap();
5179 trailing.text = " English".into();
5180 trailing.selection = Some((0..4).into());
5181 }
5182 assert_eq!(paragraph.selected_text(), "文 x² Eng");
5183 assert_eq!(paragraph.selected_source(), "文 **$x^2$** Eng");
5184 }
5185
5186 #[test]
5187 fn mark_sets_a_readable_text_color_with_its_background() {
5188 use gpui::{Empty, TestApp};
5189 let mut app = TestApp::new();
5190 let mut window = app.open_window(|_, _| Empty);
5191 window.update(|_, _, cx| {
5192 cx.set_global(crate::Theme::default());
5193 let node_cx = NodeContext::default();
5194
5195 let yellow: Hsla = gpui::rgb(0xfef08a).into();
5198 let style = mark_highlight(&TextMark::default().highlight(yellow), &node_cx, cx).style;
5199 assert_eq!(style.background_color, Some(yellow));
5200 assert_eq!(style.color, Some(gpui::black()));
5201
5202 let navy: Hsla = gpui::rgb(0x1e3a8a).into();
5204 let style = mark_highlight(&TextMark::default().highlight(navy), &node_cx, cx).style;
5205 assert_eq!(style.color, Some(gpui::white()));
5206
5207 let translucent_black: Hsla = gpui::rgba(0x00000080).into();
5210 let translucent_white: Hsla = gpui::rgba(0xffffff4d).into();
5211 assert_eq!(
5212 mark_text_color(translucent_black, gpui::white()),
5213 gpui::black()
5214 );
5215 assert_eq!(
5216 mark_text_color(translucent_white, gpui::black()),
5217 gpui::white()
5218 );
5219 let transparent: Hsla = gpui::rgba(0x00000000).into();
5220 let style =
5221 mark_highlight(&TextMark::default().highlight(transparent), &node_cx, cx).style;
5222 assert_eq!(style.color, None);
5223
5224 let style = mark_highlight(&TextMark::default().bold(), &node_cx, cx).style;
5226 assert_eq!(style.color, None);
5227 });
5228 }
5229
5230 #[test]
5231 fn translucent_mark_cache_tracks_theme_background() {
5232 use gpui::{Empty, TestApp};
5233 let mut app = TestApp::new();
5234 let mut window = app.open_window(|_, _| Empty);
5235 window.update(|_, _, cx| {
5236 cx.set_global(crate::Theme::default());
5237 let paragraph = Paragraph {
5238 children: vec![InlineNode::new("mark").marks(vec![(
5239 0..4,
5240 TextMark::default().highlight(gpui::rgba(0x00000033).into()),
5241 )])],
5242 ..Default::default()
5243 };
5244 let context = NodeContext::default();
5245 crate::Theme::global_mut(cx).tokens.colors.background = gpui::white();
5246 let (_, light, _) = paragraph.plain_render(&context, cx);
5247 assert_eq!(light[0].1.style.color, Some(gpui::black()));
5248 crate::Theme::global_mut(cx).tokens.colors.background = gpui::black();
5249 let (_, dark, _) = paragraph.plain_render(&context, cx);
5250 assert_eq!(dark[0].1.style.color, Some(gpui::white()));
5251 });
5252 }
5253
5254 #[test]
5255 fn custom_inline_inherits_marks_and_resolves_link_references() {
5256 use gpui::{Empty, TestApp};
5257 let mut app = TestApp::new();
5258 let mut window = app.open_window(|_, _| Empty);
5259 window.update(|_, _, cx| {
5260 cx.set_global(crate::Theme::default());
5261 let mut node_cx = NodeContext::default();
5262 let mut mark = TextMark::default().bold();
5263 mark.italic = true;
5264 mark.strikethrough = true;
5265 mark.link = Some(LinkMark {
5266 identifier: Some("ref".into()),
5267 ..Default::default()
5268 });
5269 node_cx.link_refs.insert(
5270 "ref".into(),
5271 LinkMark {
5272 url: "https://example.com".into(),
5273 ..Default::default()
5274 },
5275 );
5276 let paragraph = Paragraph {
5277 children: vec![
5278 InlineNode::custom(MarkdownNode::new("test", ()).text("x"))
5279 .marks(vec![(0..1, mark)]),
5280 ],
5281 ..Default::default()
5282 };
5283 let items = paragraph.inline_flow_items(None, &[], &[], &node_cx, cx);
5284 let InlineFlowItem::Object { style, link, .. } = &items[0] else {
5285 panic!()
5286 };
5287 assert_eq!(style.font_weight, Some(FontWeight::BOLD));
5288 assert_eq!(style.font_style, Some(FontStyle::Italic));
5289 assert!(style.strikethrough.is_some());
5290 assert_eq!(link.as_ref().unwrap().url.as_ref(), "https://example.com");
5291 });
5292 }
5293
5294 #[test]
5295 fn table_column_uses_prepared_inline_metrics_after_resource_update() {
5296 use crate::text::InlineElement;
5297 use crate::text::inline::test_draw::in_prepaint;
5298 use gpui::{Image, ImageFormat, TestApp};
5299 let mut app = TestApp::new();
5300 let width = Arc::new(std::sync::atomic::AtomicUsize::new(400));
5301 let render_width = width.clone();
5302 let mut node_cx = NodeContext::default();
5303 node_cx.markdown_extensions = Arc::new(MarkdownExtensions::default().plugin(
5304 crate::text::markdown_ext::TestInlinePlugin::new("test").render_with(
5305 move |_, _, _, _| {
5306 Some(
5307 InlineElement::new(
5308 gpui::img(Arc::new(Image::from_bytes(
5309 ImageFormat::Svg,
5310 b"<svg/>".to_vec(),
5311 )))
5312 .w(px(
5313 render_width.load(std::sync::atomic::Ordering::Relaxed) as f32
5314 ))
5315 .h(px(20.)),
5316 )
5317 .with_baseline(px(15.)),
5318 )
5319 },
5320 ),
5321 ));
5322 let table = table_of(
5323 vec![
5324 vec![plain_cell("ordinary text")],
5325 vec![TableCell {
5326 children: Paragraph {
5327 children: vec![InlineNode::custom(MarkdownNode::new("test", ()).text("x"))],
5328 ..Default::default()
5329 },
5330 width: None,
5331 }],
5332 ],
5333 vec![],
5334 );
5335 in_prepaint(&mut app, move |window, cx| {
5336 let mut cached = None;
5337 for expected in [400, 600, 10, 400] {
5338 width.store(expected, std::sync::atomic::Ordering::Relaxed);
5339 let measured = measure_table_columns(&table, 1, &node_cx, window, cx)[0];
5340 let current = cached_column_widths(&table);
5341 assert_eq!(
5342 measured,
5343 current.widths[0].max(expected as f32 + CELL_PAD_PX)
5344 );
5345 assert_eq!(current.custom_cells, vec![(1, 0)]);
5346 if let Some(cached) = cached.replace(current.clone()) {
5347 assert!(Arc::ptr_eq(&cached, ¤t));
5348 }
5349 }
5350 });
5351 }
5352
5353 fn cached_column_widths(table: &Table) -> Arc<TableColumnWidths> {
5354 table.column_widths_cache.0.lock().unwrap().clone().unwrap()
5355 }
5356
5357 #[test]
5358 fn blockquote_typography_reaches_nested_table_measurement() {
5359 use crate::text::inline::test_draw::in_prepaint;
5360 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
5361 use gpui::TestApp;
5362
5363 let quote = BlockNode::Blockquote {
5364 children: vec![BlockNode::Table(table_of(
5365 vec![vec![plain_cell("0123456789")]],
5366 vec![],
5367 ))],
5368 span: None,
5369 };
5370 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5371 in_prepaint(&mut app, move |window, cx| {
5372 cx.set_global(crate::Theme::default());
5373 window.with_text_style(
5374 Some(
5375 StyleRefinement::default()
5376 .font_family(BODY)
5377 .text_size(px(15.))
5378 .text,
5379 ),
5380 |window| {
5381 for (refinement, expected_width) in [
5382 (StyleRefinement::default(), 94.),
5383 (
5384 StyleRefinement::default()
5385 .font_family(MONO)
5386 .text_size(px(30.))
5387 .font_weight(FontWeight::BOLD),
5388 199.,
5389 ),
5390 ] {
5391 let node_cx = NodeContext {
5392 style: Arc::new(
5393 TextViewStyle::default().with_blockquote_style(refinement),
5394 ),
5395 body_font_size: px(15.),
5396 ..Default::default()
5397 };
5398 quote.render_block(NodeRenderOptions::default(), &node_cx, window, cx);
5399 let BlockNode::Blockquote { children, .. } = "e else {
5400 unreachable!()
5401 };
5402 let BlockNode::Table(table) = &children[0] else {
5403 unreachable!()
5404 };
5405 assert_eq!(cached_column_widths(table).widths, vec![expected_width]);
5407 }
5408 },
5409 );
5410 });
5411 }
5412
5413 #[test]
5414 fn block_surface_typography_reaches_nested_table_measurement() {
5415 use crate::text::MarkdownExtensions;
5416 use crate::text::inline::test_draw::in_prepaint;
5417 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
5418 use gpui::TestApp;
5419
5420 let surface = BlockNode::Surface {
5421 name: "large".into(),
5422 children: vec![BlockNode::Table(table_of(
5423 vec![vec![plain_cell("0123456789")]],
5424 vec![],
5425 ))],
5426 span: None,
5427 };
5428 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5429 in_prepaint(&mut app, move |window, cx| {
5430 cx.set_global(crate::Theme::default());
5431 window.with_text_style(
5432 Some(
5433 StyleRefinement::default()
5434 .font_family(BODY)
5435 .text_size(px(15.))
5436 .text,
5437 ),
5438 |window| {
5439 let node_cx = NodeContext {
5440 body_font_size: px(15.),
5441 markdown_extensions: Arc::new(MarkdownExtensions::default().block_surface(
5442 "large",
5443 |_, _| {
5444 div()
5445 .font_family(MONO)
5446 .text_size(px(30.))
5447 .font_weight(FontWeight::BOLD)
5448 },
5449 )),
5450 ..Default::default()
5451 };
5452 surface.render_block(NodeRenderOptions::default(), &node_cx, window, cx);
5453 let BlockNode::Surface { children, .. } = &surface else {
5454 unreachable!()
5455 };
5456 let BlockNode::Table(table) = &children[0] else {
5457 unreachable!()
5458 };
5459 assert_eq!(cached_column_widths(table).widths, vec![199.]);
5461 },
5462 );
5463 });
5464 }
5465
5466 #[test]
5467 fn table_columns_cache_preserves_plain_widths_and_clones() {
5468 use crate::text::inline::test_draw::in_prepaint;
5469 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem, record_shaped_lines};
5470 use gpui::{Refineable as _, TestApp, TextStyle};
5471
5472 let long = "é界".repeat(2048);
5473 let table = table_of(
5474 vec![
5475 vec![plain_cell(&format!(" \t{long} \nshort\n ")), plain_cell("")],
5476 vec![plain_cell("short")],
5477 vec![],
5478 vec![plain_cell(""), plain_cell("a\nabcdefghijklmnop")],
5479 ],
5480 vec![],
5481 );
5482 let uncached = table.clone();
5483 let node_cx = NodeContext::default();
5484 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5485 let (table, node_cx, cached) = in_prepaint(&mut app, move |window, cx| {
5486 window.set_rem_size(px(20.));
5487 let style = TextStyle {
5488 font_family: BODY.into(),
5489 font_size: rems(1.).into(),
5490 ..Default::default()
5491 };
5492 let cached =
5493 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
5494 let (widths, shaped) = record_shaped_lines(|| {
5495 measure_table_columns(&table, 2, &node_cx, window, cx)
5496 });
5497 assert_eq!(widths, vec![40960. + CELL_PAD_PX + CELL_BORDER_PX, 184.]);
5498 assert!(shaped.iter().any(|line| line == &long));
5499 assert_eq!(
5500 table, uncached,
5501 "derived widths do not change table equality"
5502 );
5503 assert!(uncached.column_widths_cache.0.lock().unwrap().is_none());
5504
5505 let cached = cached_column_widths(&table);
5506 let cloned = table.clone();
5507 for same in [&table, &cloned] {
5508 let (again, shaped) = record_shaped_lines(|| {
5509 measure_table_columns(same, 2, &node_cx, window, cx)
5510 });
5511 assert_eq!(again, widths);
5512 assert!(shaped.is_empty());
5513 assert!(Arc::ptr_eq(&cached, &cached_column_widths(same)));
5514 }
5515
5516 assert_eq!(
5518 measure_table_columns(&cloned, 3, &node_cx, window, cx),
5519 vec![widths[0], widths[1] + CELL_BORDER_PX, CELL_MIN_PX]
5520 );
5521 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&cloned)));
5522 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
5523 assert!(
5524 measure_table_columns(&Table::default(), 0, &node_cx, window, cx)
5525 .is_empty()
5526 );
5527 cached
5528 });
5529 (table, node_cx, cached)
5530 });
5531 in_prepaint(&mut app, move |window, cx| {
5534 window.set_rem_size(cached.rem_size);
5535 window.with_text_style(
5536 Some(cached.text_styles.body.subtract(&Default::default())),
5537 |window| {
5538 measure_table_columns(&table, 2, &node_cx, window, cx);
5539 assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&table)));
5540 },
5541 );
5542 });
5543 }
5544
5545 #[test]
5546 fn table_columns_cache_tracks_typography() {
5547 use crate::text::inline::test_draw::in_prepaint;
5548 use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
5549 use gpui::{Refineable as _, TestApp, TextStyle};
5550
5551 let code = "0123456789";
5552 let mut paragraph = Paragraph::default();
5553 paragraph
5554 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
5555 let table = table_of(
5556 vec![vec![
5557 plain_cell(code),
5558 TableCell {
5559 children: paragraph,
5560 width: None,
5561 },
5562 ]],
5563 vec![],
5564 );
5565 let base = TextStyle {
5566 font_family: BODY.into(),
5567 font_size: rems(1.).into(),
5568 ..Default::default()
5569 };
5570 let mut larger = base.clone();
5571 larger.font_size = rems(2.).into();
5572 let mut bold = base.clone();
5573 bold.font_weight = FontWeight::BOLD;
5574 let mut mono = base.clone();
5575 mono.font_family = MONO.into();
5576 let normal_code = HighlightStyle::default();
5577 let bold_code = HighlightStyle {
5578 font_weight: Some(FontWeight::BOLD),
5579 ..Default::default()
5580 };
5581 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5582 in_prepaint(&mut app, move |window, cx| {
5583 let mut previous = None;
5584 for (style, rem_size, family, inline_code, expected) in [
5585 (base.clone(), 20., MONO, normal_code, [125., 207.]),
5586 (base.clone(), 30., MONO, normal_code, [175., 294.5]),
5587 (larger, 20., MONO, normal_code, [225., 382.]),
5588 (bold, 20., MONO, normal_code, [175., 250.75]),
5589 (mono, 20., MONO, normal_code, [225., 207.]),
5590 (base.clone(), 20., MONO, normal_code, [125., 207.]),
5591 (base.clone(), 20., BODY, normal_code, [125., 119.5]),
5592 (base.clone(), 20., MONO, normal_code, [125., 207.]),
5593 (base, 20., MONO, bold_code, [125., 250.75]),
5594 ] {
5595 let mut theme = crate::Theme::default();
5596 theme.tokens.typography.mono = family.into();
5597 cx.set_global(theme);
5598 window.set_rem_size(px(rem_size));
5599 let node_cx = NodeContext {
5600 style: Arc::new(TextViewStyle::default().with_inline_code(inline_code)),
5601 ..Default::default()
5602 };
5603 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
5604 let widths = measure_table_columns(&table, 2, &node_cx, window, cx);
5605 for (width, expected) in widths.iter().zip(expected) {
5606 assert!((width - expected).abs() < 0.01, "{width} != {expected}");
5607 }
5608 let cached = cached_column_widths(&table);
5609 if let Some(previous) = previous.replace(cached.clone()) {
5610 assert!(!Arc::ptr_eq(&previous, &cached));
5611 }
5612 assert_eq!(
5613 measure_table_columns(&table, 2, &node_cx, window, cx),
5614 widths
5615 );
5616 assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
5617 });
5618 }
5619 });
5620 }
5621
5622 #[test]
5623 fn table_columns_cache_is_rebuilt_for_reparsed_tables() {
5624 use crate::text::format::markdown;
5625 use crate::text::inline::test_draw::in_prepaint;
5626 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem};
5627 use gpui::{Refineable as _, TestApp, TextStyle};
5628
5629 let mut node_cx = NodeContext::default();
5630 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5631 in_prepaint(&mut app, move |window, cx| {
5632 let style = TextStyle {
5633 font_family: BODY.into(),
5634 font_size: px(20.).into(),
5635 ..Default::default()
5636 };
5637 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
5638 let mut previous = None;
5639 for (text, expected) in [("longer cell", 134.), ("x", CELL_MIN_PX)] {
5640 let source = format!("| h |\n| - |\n| {text} |\n");
5641 let document = markdown::parse(&source, &mut node_cx).unwrap();
5642 let BlockNode::Table(table) = &document.blocks[0] else {
5643 panic!("expected a parsed table");
5644 };
5645 assert!(table.column_widths_cache.0.lock().unwrap().is_none());
5646 assert_eq!(
5647 measure_table_columns(table, 1, &node_cx, window, cx),
5648 vec![expected]
5649 );
5650 let cached = cached_column_widths(table);
5651 if let Some(previous) = previous.replace(cached.clone()) {
5652 assert!(!Arc::ptr_eq(&previous, &cached));
5653 }
5654 assert_eq!(document.source.as_ref(), source);
5655 }
5656 });
5657 });
5658 }
5659
5660 #[test]
5664 fn table_column_of_inline_code_cells_fits_the_mono_width() {
5665 use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
5666 use gpui::{Empty, TestApp};
5667
5668 let code = "method_name()";
5669 let mut paragraph = Paragraph::default();
5670 paragraph
5671 .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
5672 let table = Table {
5673 children: vec![TableRow {
5674 children: vec![TableCell {
5675 children: paragraph,
5676 width: None,
5677 }],
5678 }],
5679 column_aligns: vec![],
5680 span: None,
5681 table_data_cache: TableDataCache::default(),
5682 column_widths_cache: TableColumnWidthsCache::default(),
5683 };
5684 let node_cx = NodeContext::default();
5685
5686 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5687 let mut window = app.open_window(|_, _| Empty);
5688 let (col_w, font_size) = window.update(|_, window, cx| {
5689 let mut theme = crate::Theme::default();
5690 theme.tokens.typography.mono = MONO.into();
5691 cx.set_global(theme);
5692 let font_size = window.text_style().font_size.to_pixels(window.rem_size());
5693 (
5694 measure_table_columns(&table, 1, &node_cx, window, cx),
5695 font_size,
5696 )
5697 });
5698
5699 let mono_w = f32::from(WideMonoTextSystem::width_of(code, MONO, font_size * 0.875));
5700 assert!(
5701 (col_w[0]
5702 - (mono_w + CELL_PAD_PX + crate::text::inline_flow::INLINE_CODE_PADDING * 2.))
5703 .abs()
5704 < 0.01,
5705 "col_w {} must fit the mono width {} plus padding {}",
5706 col_w[0],
5707 mono_w,
5708 CELL_PAD_PX
5709 );
5710 }
5711
5712 #[test]
5717 fn table_column_of_an_image_cell_fits_the_image_width() {
5718 use crate::text::inline::test_draw::in_prepaint;
5719 use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem};
5720 use gpui::{Refineable as _, TestApp, TextStyle};
5721
5722 let mut paragraph = Paragraph::default();
5723 paragraph.push(InlineNode::image(ImageNode {
5724 url: "https://example.com/photo.png".into(),
5725 width: Some(px(100.).into()),
5726 height: Some(px(50.).into()),
5727 ..Default::default()
5728 }));
5729 let table = Table {
5730 children: vec![TableRow {
5731 children: vec![TableCell {
5732 children: paragraph,
5733 width: None,
5734 }],
5735 }],
5736 column_aligns: vec![],
5737 span: None,
5738 table_data_cache: TableDataCache::default(),
5739 column_widths_cache: TableColumnWidthsCache::default(),
5740 };
5741 let node_cx = NodeContext::default();
5742
5743 let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
5744 in_prepaint(&mut app, move |window, cx| {
5745 let style = TextStyle {
5746 font_family: BODY.into(),
5747 font_size: px(20.).into(),
5748 ..Default::default()
5749 };
5750 window.with_text_style(Some(style.subtract(&Default::default())), |window| {
5751 assert_eq!(
5752 measure_table_columns(&table, 1, &node_cx, window, cx),
5753 vec![100. + CELL_PAD_PX]
5754 );
5755 });
5756 });
5757 }
5758
5759 #[test]
5760 fn code_block_highlights_are_cached_by_highlighter_identity() {
5761 use std::sync::atomic::{AtomicUsize, Ordering};
5762
5763 let calls = Arc::new(AtomicUsize::new(0));
5764 let calls_for_highlighter = calls.clone();
5765 let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
5766 calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
5767 Vec::new()
5768 });
5769 let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
5770
5771 block.highlighted_styles(&highlighter);
5772 block.highlighted_styles(&highlighter);
5773 assert_eq!(calls.load(Ordering::Relaxed), 1);
5774
5775 let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
5776 block.highlighted_styles(&replacement);
5777 assert!(Arc::ptr_eq(
5778 &block
5779 .highlight_cache
5780 .lock()
5781 .unwrap()
5782 .as_ref()
5783 .unwrap()
5784 .highlighter,
5785 &replacement
5786 ));
5787 }
5788
5789 #[test]
5790 fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
5791 let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
5795 vec![(
5796 0..2,
5797 HighlightStyle {
5798 color: Some(gpui::rgb(0x0000ff).into()),
5799 ..Default::default()
5800 },
5801 )]
5802 });
5803 let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
5804 vec![(
5805 0..2,
5806 HighlightStyle {
5807 color: Some(gpui::rgb(0xffff00).into()),
5808 ..Default::default()
5809 },
5810 )]
5811 });
5812 let block = CodeBlock::from_code("42", Some("json"));
5813
5814 let light_styles = block.highlighted_styles(&light);
5815 let dark_styles = block.highlighted_styles(&dark);
5816
5817 assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
5818 assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
5819 assert_eq!(block.code(), "42", "the document must survive the swap");
5820 }
5821
5822 #[test]
5823 fn reconstruct_markdown_wraps_marked_runs() {
5824 let marks = vec![(0..4, TextMark::default().bold())];
5826 assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
5827 assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
5829 }
5830
5831 #[test]
5832 fn reconstruct_markdown_emits_unmarked_text_verbatim() {
5833 let text = "a b c";
5835 let marks = vec![(2..3, TextMark::default().code())];
5836 assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
5837 assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
5839 }
5840
5841 #[test]
5842 fn reconstruct_markdown_handles_code_italic_strike_link() {
5843 assert_eq!(
5844 reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
5845 "`x`"
5846 );
5847 assert_eq!(
5848 reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
5849 "*x*"
5850 );
5851 assert_eq!(
5852 reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
5853 "~~x~~"
5854 );
5855 let link = TextMark::default().link(LinkMark {
5856 url: "https://example.com".into(),
5857 ..Default::default()
5858 });
5859 assert_eq!(
5860 reconstruct_markdown("x", &[(0..1, link)], 0..1),
5861 "[x](https://example.com)"
5862 );
5863 }
5864
5865 #[test]
5866 fn reconstruct_markdown_nested_bold_italic() {
5867 let mark = TextMark::default().bold().italic();
5869 assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
5871 }
5872
5873 fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
5877 let combined: String = children.iter().map(|c| c.text.to_string()).collect();
5878 let paragraph = Paragraph {
5879 span: None,
5880 children,
5881 link_refs: HashMap::new(),
5882 state: Arc::new(Mutex::new(InlineState::default())),
5883 render_cache: ParagraphRenderCache::default(),
5884 };
5885 if let Ok(mut state) = paragraph.state.lock() {
5886 state.set_text(combined.into());
5887 }
5888 paragraph
5889 }
5890
5891 fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
5892 if let Ok(mut state) = paragraph.state.lock() {
5893 state.selection = Some(range.into());
5894 }
5895 }
5896
5897 #[test]
5898 fn paragraph_selected_source_maps_partial_selection_across_runs() {
5899 let children = vec![
5901 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
5902 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5903 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
5904 ];
5905 let paragraph = paragraph_with_children(children);
5906
5907 set_paragraph_selection(¶graph, 0..(9 + 4 + 6));
5909 assert_eq!(paragraph.selected_source(), "This has **bold** text.");
5910
5911 set_paragraph_selection(¶graph, 5..16);
5914 assert_eq!(paragraph.selected_source(), "has **bold** te");
5915
5916 set_paragraph_selection(¶graph, 10..12);
5918 assert_eq!(paragraph.selected_source(), "**ol**");
5919 }
5920
5921 #[test]
5922 fn paragraph_selected_source_matches_text_when_no_marks() {
5923 let children =
5924 vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
5925 let paragraph = paragraph_with_children(children);
5926 set_paragraph_selection(¶graph, 0..11);
5927 assert_eq!(paragraph.selected_source(), "plain words");
5928 assert_eq!(paragraph.selected_text(), "plain words");
5929 }
5930
5931 fn copy_across_styles(blocks: Vec<BlockNode>) -> String {
5934 let document = crate::text::document::ParsedDocument {
5935 blocks: blocks.into(),
5936 ..Default::default()
5937 };
5938 document.selected_text(SelectionFormat::SourceAcrossStyles, None)
5939 }
5940
5941 #[test]
5942 fn source_across_styles_copies_plain_text_within_one_style() {
5943 let paragraph = || {
5945 paragraph_with_children(vec![
5946 InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
5947 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
5948 InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
5949 ])
5950 };
5951 let copy = |range: Range<usize>| {
5952 let paragraph = paragraph();
5953 set_paragraph_selection(¶graph, range);
5954 copy_across_styles(vec![BlockNode::Paragraph(paragraph)])
5955 };
5956
5957 assert_eq!(copy(10..12), "ol\n");
5959 assert_eq!(copy(9..13), "bold\n");
5960 assert_eq!(copy(0..9), "This has \n");
5962 assert_eq!(copy(5..16), "has **bold** te");
5964 assert_eq!(copy(8..10), " **b**");
5965 }
5966
5967 #[test]
5968 fn source_across_styles_drops_the_url_of_a_link_selected_alone() {
5969 let link = TextMark::default().link(LinkMark {
5970 url: "https://example.com".into(),
5971 ..Default::default()
5972 });
5973 let paragraph = paragraph_with_children(vec![
5974 InlineNode::new("see ").marks(vec![(0..4, TextMark::default())]),
5975 InlineNode::new("the docs").marks(vec![(0..8, link)]),
5976 ]);
5977
5978 set_paragraph_selection(¶graph, 4..12);
5979 assert_eq!(
5980 copy_across_styles(vec![BlockNode::Paragraph(paragraph.clone())]),
5981 "the docs\n"
5982 );
5983
5984 set_paragraph_selection(¶graph, 0..12);
5985 assert_eq!(
5986 copy_across_styles(vec![BlockNode::Paragraph(paragraph)]),
5987 "see [the docs](https://example.com)"
5988 );
5989 }
5990
5991 #[test]
5992 fn source_across_styles_drops_the_markup_of_a_single_block() {
5993 let heading = BlockNode::Heading {
5994 level: 2,
5995 children: selected_paragraph("Title"),
5996 span: None,
5997 };
5998 assert_eq!(copy_across_styles(vec![heading]), "Title\n");
5999
6000 let code = CodeBlock::from_code("let a = 1;\nlet b = 2;", Some("rust"));
6001 code.set_selection(0..10);
6002 assert_eq!(
6003 copy_across_styles(vec![BlockNode::CodeBlock(code)]),
6004 "let a = 1;\n"
6005 );
6006
6007 let item = |text: &str| BlockNode::ListItem {
6008 children: vec![BlockNode::Paragraph(selected_paragraph(text))],
6009 spread: false,
6010 checked: None,
6011 task_marker: None,
6012 span: None,
6013 };
6014 let unselected_item = BlockNode::ListItem {
6015 children: vec![BlockNode::Paragraph(paragraph_with_children(vec![
6016 InlineNode::new("two"),
6017 ]))],
6018 spread: false,
6019 checked: None,
6020 task_marker: None,
6021 span: None,
6022 };
6023 let list = BlockNode::List {
6024 ordered: false,
6025 start: None,
6026 span: None,
6027 children: vec![item("one"), unselected_item],
6028 };
6029 assert_eq!(copy_across_styles(vec![list]), "one\n");
6030 }
6031
6032 #[test]
6033 fn source_across_styles_keeps_the_source_across_blocks() {
6034 let item = |text: &str| BlockNode::ListItem {
6035 children: vec![BlockNode::Paragraph(selected_paragraph(text))],
6036 spread: false,
6037 checked: None,
6038 task_marker: None,
6039 span: None,
6040 };
6041 let list = BlockNode::List {
6042 ordered: false,
6043 start: None,
6044 span: None,
6045 children: vec![item("one"), item("two")],
6046 };
6047 assert_eq!(copy_across_styles(vec![list]), "- one\n- two");
6048
6049 assert_eq!(
6050 copy_across_styles(vec![
6051 BlockNode::Paragraph(selected_paragraph("start")),
6052 BlockNode::Paragraph(selected_paragraph("end")),
6053 ]),
6054 "start\n\nend"
6055 );
6056 }
6057
6058 #[test]
6059 fn source_across_styles_keeps_the_source_of_an_inline_object() {
6060 let formula = InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"));
6061 *formula.custom_selection.lock().unwrap() = true;
6062 let paragraph = Paragraph {
6063 children: vec![formula],
6064 ..Default::default()
6065 };
6066 assert_eq!(
6067 copy_across_styles(vec![BlockNode::Paragraph(paragraph)]),
6068 "$x$"
6069 );
6070 }
6071
6072 fn selected_paragraph(text: &str) -> Paragraph {
6073 let len = text.len();
6074 let paragraph = paragraph_with_children(vec![
6075 InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
6076 ]);
6077 set_paragraph_selection(¶graph, 0..len);
6078 paragraph
6079 }
6080
6081 #[test]
6082 fn heading_selected_source_prefixes_hashes() {
6083 let heading = BlockNode::Heading {
6084 level: 2,
6085 children: selected_paragraph("Title"),
6086 span: None,
6087 };
6088 assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
6089 assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
6091 }
6092
6093 #[test]
6094 fn unordered_list_selected_source_prefixes_dash() {
6095 let list = BlockNode::List {
6096 ordered: false,
6097 start: None,
6098 span: None,
6099 children: vec![
6100 BlockNode::ListItem {
6101 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
6102 spread: false,
6103 checked: None,
6104 task_marker: None,
6105 span: None,
6106 },
6107 BlockNode::ListItem {
6108 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
6109 spread: false,
6110 checked: None,
6111 task_marker: None,
6112 span: None,
6113 },
6114 ],
6115 };
6116 assert_eq!(
6117 list.selected_text(SelectionFormat::Source),
6118 "- one\n- two\n"
6119 );
6120 }
6121
6122 #[test]
6123 fn ordered_list_selected_source_preserves_start() {
6124 let list = BlockNode::List {
6125 ordered: true,
6126 start: Some(3),
6127 span: None,
6128 children: vec![
6129 BlockNode::ListItem {
6130 children: vec![BlockNode::Paragraph(selected_paragraph("three"))],
6131 spread: false,
6132 checked: None,
6133 task_marker: None,
6134 span: None,
6135 },
6136 BlockNode::ListItem {
6137 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
6138 spread: false,
6139 checked: None,
6140 task_marker: None,
6141 span: None,
6142 },
6143 ],
6144 };
6145 assert_eq!(list.to_markdown(), "3. three\n4. four");
6146 assert_eq!(
6147 list.selected_text(SelectionFormat::Source),
6148 "3. three\n4. four\n"
6149 );
6150 }
6151
6152 #[test]
6153 fn ordered_list_selected_source_preserves_nested_starts_and_continuations() {
6154 let nested = BlockNode::List {
6155 ordered: true,
6156 start: Some(4),
6157 span: None,
6158 children: vec![
6159 BlockNode::ListItem {
6160 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
6161 spread: false,
6162 checked: None,
6163 task_marker: None,
6164 span: None,
6165 },
6166 BlockNode::ListItem {
6167 children: vec![BlockNode::Paragraph(selected_paragraph("again"))],
6168 spread: false,
6169 checked: None,
6170 task_marker: None,
6171 span: None,
6172 },
6173 ],
6174 };
6175 let nested_list = BlockNode::List {
6176 ordered: true,
6177 start: Some(3),
6178 span: None,
6179 children: vec![
6180 BlockNode::ListItem {
6181 children: vec![BlockNode::Paragraph(selected_paragraph("three")), nested],
6182 spread: false,
6183 checked: None,
6184 task_marker: None,
6185 span: None,
6186 },
6187 BlockNode::ListItem {
6188 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
6189 spread: false,
6190 checked: None,
6191 task_marker: None,
6192 span: None,
6193 },
6194 ],
6195 };
6196 assert_eq!(
6197 nested_list.selected_text(SelectionFormat::Source),
6198 "3. three\n 4. nested\n 5. again\n4. four\n"
6199 );
6200
6201 let loose_list = BlockNode::List {
6202 ordered: true,
6203 start: Some(3),
6204 span: None,
6205 children: vec![
6206 BlockNode::ListItem {
6207 children: vec![
6208 BlockNode::Paragraph(selected_paragraph("loose")),
6209 BlockNode::Paragraph(selected_paragraph("continuation")),
6210 ],
6211 spread: true,
6212 checked: None,
6213 task_marker: None,
6214 span: None,
6215 },
6216 BlockNode::ListItem {
6217 children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
6218 spread: false,
6219 checked: None,
6220 task_marker: None,
6221 span: None,
6222 },
6223 ],
6224 };
6225 assert_eq!(
6226 loose_list.selected_text(SelectionFormat::Source),
6227 "3. loose\n continuation\n4. four\n"
6228 );
6229 }
6230
6231 #[test]
6232 fn nested_list_selected_source_indents_sublists() {
6233 let nested = BlockNode::List {
6237 ordered: false,
6238 start: None,
6239 span: None,
6240 children: vec![BlockNode::ListItem {
6241 children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
6242 spread: false,
6243 checked: None,
6244 task_marker: None,
6245 span: None,
6246 }],
6247 };
6248 let list = BlockNode::List {
6249 ordered: false,
6250 start: None,
6251 span: None,
6252 children: vec![
6253 BlockNode::ListItem {
6254 children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
6255 spread: false,
6256 checked: None,
6257 task_marker: None,
6258 span: None,
6259 },
6260 BlockNode::ListItem {
6261 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
6262 spread: false,
6263 checked: None,
6264 task_marker: None,
6265 span: None,
6266 },
6267 ],
6268 };
6269 assert_eq!(
6270 list.selected_text(SelectionFormat::Source),
6271 "- one\n - nested\n- two\n"
6272 );
6273 }
6274
6275 #[test]
6276 fn task_list_selected_source_restores_checkboxes() {
6277 let list = BlockNode::List {
6278 ordered: false,
6279 start: None,
6280 span: None,
6281 children: vec![
6282 BlockNode::ListItem {
6283 children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
6284 spread: false,
6285 checked: Some(true),
6286 task_marker: None,
6287 span: None,
6288 },
6289 BlockNode::ListItem {
6290 children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
6291 spread: false,
6292 checked: Some(false),
6293 task_marker: None,
6294 span: None,
6295 },
6296 ],
6297 };
6298 assert_eq!(
6299 list.selected_text(SelectionFormat::Source),
6300 "- [x] done\n- [ ] todo\n"
6301 );
6302 }
6303
6304 #[test]
6305 fn blockquote_selected_source_prefixes_gt() {
6306 let quote = BlockNode::Blockquote {
6307 span: None,
6308 children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
6309 };
6310 assert_eq!(
6311 quote.selected_text(SelectionFormat::Source),
6312 "> quoted text\n"
6313 );
6314 }
6315
6316 #[test]
6317 fn table_selected_source_pipes_cells_with_alignment_row() {
6318 let cell = |text: &str| TableCell {
6319 children: selected_paragraph(text),
6320 width: None,
6321 };
6322 let table = Table {
6323 children: vec![
6324 TableRow {
6325 children: vec![cell("Name"), cell("Age")],
6326 },
6327 TableRow {
6328 children: vec![cell("Alice"), cell("30")],
6329 },
6330 ],
6331 column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
6332 span: None,
6333 table_data_cache: TableDataCache::default(),
6334 column_widths_cache: TableColumnWidthsCache::default(),
6335 };
6336 let block = BlockNode::Table(table);
6337 assert_eq!(
6338 block.selected_text(SelectionFormat::Source),
6339 "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
6340 );
6341 }
6342
6343 #[test]
6346 fn a_selection_in_one_table_cell_copies_only_that_cell() {
6347 let table = table_of(
6348 vec![
6349 vec![plain_cell("Name"), plain_cell("Age")],
6350 vec![
6351 TableCell {
6352 children: selected_paragraph("Alice"),
6353 width: None,
6354 },
6355 plain_cell("30"),
6356 ],
6357 ],
6358 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
6359 );
6360 let block = BlockNode::Table(table);
6361 assert_eq!(block.selected_text(SelectionFormat::Plain).trim(), "Alice");
6362 assert_eq!(block.selected_text(SelectionFormat::Source), "Alice\n");
6363 }
6364
6365 fn plain_cell(text: &str) -> TableCell {
6368 TableCell {
6369 children: Paragraph::new(text.to_string()),
6370 width: None,
6371 }
6372 }
6373
6374 fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
6375 Table {
6376 children: rows
6377 .into_iter()
6378 .map(|children| TableRow { children })
6379 .collect(),
6380 column_aligns,
6381 span: None,
6382 table_data_cache: TableDataCache::default(),
6383 column_widths_cache: TableColumnWidthsCache::default(),
6384 }
6385 }
6386
6387 #[test]
6388 fn table_to_markdown_pipes_cells_with_alignment_row() {
6389 let table = table_of(
6390 vec![
6391 vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
6392 vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
6393 ],
6394 vec![
6395 ColumnumnAlign::Left,
6396 ColumnumnAlign::Center,
6397 ColumnumnAlign::Right,
6398 ],
6399 );
6400
6401 assert_eq!(
6402 table.to_markdown(),
6403 "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
6404 );
6405 assert_eq!(
6407 BlockNode::Table(table.clone()).to_markdown(),
6408 table.to_markdown()
6409 );
6410 }
6411
6412 #[test]
6413 fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
6414 let table = table_of(
6415 vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
6416 vec![ColumnumnAlign::Left],
6417 );
6418
6419 assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
6420 }
6421
6422 #[test]
6423 fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
6424 let bold = TableCell {
6425 children: paragraph_with_children(vec![
6426 InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
6427 ]),
6428 width: None,
6429 };
6430 let table = table_of(
6431 vec![
6432 vec![plain_cell("a | b"), plain_cell("plain")],
6433 vec![plain_cell("c"), bold],
6434 ],
6435 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
6436 );
6437
6438 assert_eq!(
6439 table.to_markdown(),
6440 "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
6441 );
6442 }
6443
6444 #[test]
6445 fn table_data_snapshots_plain_cells_and_markdown() {
6446 let mut table = table_of(
6447 vec![
6448 vec![plain_cell(" Name "), plain_cell("Age")],
6449 vec![plain_cell("Alice"), plain_cell("30")],
6450 ],
6451 vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
6452 );
6453 table.span = Some(Span { start: 4, end: 42 });
6454
6455 let data = table.table_data();
6456 assert_eq!(data.headers, vec!["Name", "Age"]);
6457 assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
6458 assert_eq!(data.markdown, table.to_markdown());
6459 assert_eq!(data.span, Some(4..42));
6460 }
6461
6462 #[test]
6463 fn cloned_table_keeps_cached_table_data() {
6464 let table = table_of(
6467 vec![vec![plain_cell("Name")], vec![plain_cell("Alice")]],
6468 vec![ColumnumnAlign::Left],
6469 );
6470 let data = table.cached_table_data();
6471
6472 let cloned = table.clone();
6473 assert!(Arc::ptr_eq(&data, &cloned.cached_table_data()));
6474 }
6475
6476 #[test]
6477 fn table_data_handles_tables_without_rows() {
6478 let header_only = table_of(
6480 vec![vec![plain_cell("Name"), plain_cell("Age")]],
6481 vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
6482 );
6483 let data = header_only.table_data();
6484 assert_eq!(data.headers, vec!["Name", "Age"]);
6485 assert!(data.rows.is_empty());
6486 assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
6487
6488 assert_eq!(Table::default().table_data(), TableData::default());
6490 }
6491
6492 #[test]
6493 fn test_image_node_source() {
6494 use gpui::{ImageFormat, ImageSource, Resource};
6495
6496 fn image_node(url: &str) -> ImageNode {
6497 ImageNode {
6498 url: url.into(),
6499 ..Default::default()
6500 }
6501 }
6502
6503 fn assert_uri(url: &str) {
6507 match image_node(url).source() {
6508 ImageSource::Resource(Resource::Uri(uri)) => assert_eq!(uri.as_ref(), url),
6509 _ => panic!("expected Uri for {url:?}"),
6510 }
6511 }
6512 assert_uri("https://example.com/logo.png");
6513 assert_uri("http://example.com/logo.png");
6514 assert_uri("website/public/logo.svg");
6515 assert_uri("./images/a.png");
6516 assert_uri("../images/a.png");
6517 assert_uri("/absolute/path/logo.svg");
6518 assert_uri("file:///absolute/path/logo.svg");
6519 assert_uri(r"C:\images\logo.png");
6520 assert_uri("docs/a:b.png");
6521 assert_uri("data:text/plain;base64,aGVsbG8=");
6522
6523 let node = image_node("data:image/png;base64,iVBORw0KGgo=");
6526 let ImageSource::Image(first) = node.source() else {
6527 panic!("expected an embedded image");
6528 };
6529 assert_eq!(first.format(), ImageFormat::Png);
6530 assert_eq!(first.bytes(), b"\x89PNG\r\n\x1a\n");
6531 let ImageSource::Image(second) = node.source() else {
6532 panic!("expected an embedded image");
6533 };
6534 assert!(Arc::ptr_eq(&first, &second));
6535 }
6536
6537 fn image_paragraph(alt: &str, url: &str) -> Paragraph {
6538 let image = ImageNode {
6539 url: url.into(),
6540 alt: Some(alt.into()),
6541 ..Default::default()
6542 };
6543 Paragraph {
6544 span: None,
6545 children: vec![InlineNode::image(image)],
6546 link_refs: HashMap::new(),
6547 state: Arc::new(Mutex::new(InlineState::default())),
6548 render_cache: ParagraphRenderCache::default(),
6549 }
6550 }
6551
6552 #[test]
6555 fn marks_round_trip_through_reconstruction() {
6556 let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
6557
6558 assert_eq!(wrap(TextMark::default().bold()), "**x**");
6559 assert_eq!(wrap(TextMark::default().italic()), "*x*");
6560 assert_eq!(wrap(TextMark::default().code()), "`x`");
6561 assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
6562 assert_eq!(
6563 wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
6564 "==x=="
6565 );
6566 assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
6568
6569 assert_eq!(
6571 wrap(TextMark::default().link(LinkMark {
6572 url: "https://example.com".into(),
6573 title: Some("Tip".into()),
6574 ..Default::default()
6575 })),
6576 "[x](https://example.com \"Tip\")"
6577 );
6578 }
6579
6580 #[test]
6583 fn document_selected_source_slices_covered_blocks_from_the_source() {
6584 use crate::text::document::ParsedDocument;
6585
6586 let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
6589 let list = "3. _one_\n4. two";
6590 let list_start = source.find(list).unwrap();
6591 let rule_start = source.find("---").unwrap();
6592
6593 let document = ParsedDocument {
6594 source: source.into(),
6595 blocks: vec![
6596 BlockNode::Paragraph(selected_paragraph("start")),
6597 BlockNode::List {
6598 ordered: true,
6599 start: Some(3),
6600 children: vec![],
6601 span: Some(Span {
6602 start: list_start,
6603 end: list_start + list.len(),
6604 }),
6605 },
6606 BlockNode::HorizontalRule {
6607 span: Some(Span {
6608 start: rule_start,
6609 end: rule_start + 3,
6610 }),
6611 },
6612 BlockNode::Paragraph(selected_paragraph("end")),
6613 ]
6614 .into(),
6615 };
6616
6617 assert_eq!(
6618 document.selected_text(SelectionFormat::Source, None),
6619 "start\n\n3. _one_\n4. two\n\n---\n\nend"
6620 );
6621 }
6622
6623 #[test]
6624 fn document_selected_source_includes_enclosed_image() {
6625 use crate::text::document::ParsedDocument;
6626
6627 let source = "before\n\n\n\nafter";
6631 let image_markdown = "";
6632 let start = source.find(image_markdown).unwrap();
6633 let mut image = image_paragraph("alt", "https://example.com/i.png");
6634 image.span = Some(Span {
6635 start,
6636 end: start + image_markdown.len(),
6637 });
6638
6639 let document = ParsedDocument {
6640 source: source.into(),
6641 blocks: vec![
6642 BlockNode::Paragraph(selected_paragraph("before")),
6643 BlockNode::Paragraph(image),
6644 BlockNode::Paragraph(selected_paragraph("after")),
6645 ]
6646 .into(),
6647 };
6648 assert_eq!(
6649 document.selected_text(SelectionFormat::Source, None),
6650 "before\n\n\n\nafter"
6651 );
6652 }
6653
6654 #[test]
6655 fn document_selected_source_drops_unenclosed_image() {
6656 use crate::text::document::ParsedDocument;
6657
6658 let document = ParsedDocument {
6661 source: String::new().into(),
6662 blocks: vec![
6663 BlockNode::Paragraph(selected_paragraph("before")),
6664 BlockNode::Paragraph(image_paragraph("alt", "u")),
6665 ]
6666 .into(),
6667 };
6668 assert_eq!(
6669 document.selected_text(SelectionFormat::Source, None),
6670 "before"
6671 );
6672 }
6673
6674 fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
6675 let block = CodeBlock::new(
6676 code.to_string().into(),
6677 lang.map(|l| l.to_string().into()),
6678 None::<Span>,
6679 );
6680 if let Ok(mut state) = block.state.lock() {
6681 let len = state.text.len();
6682 state.selection = Some((0..len).into());
6683 }
6684 BlockNode::CodeBlock(block)
6685 }
6686
6687 #[test]
6688 fn code_block_selected_source_wraps_in_fence_with_lang() {
6689 let block = selected_code_block("let x = 1;\n", Some("rust"));
6690 let code = block.selected_text(SelectionFormat::Plain);
6691 let code_trimmed = code.trim_end_matches('\n');
6692 assert_eq!(
6696 block.selected_text(SelectionFormat::Source),
6697 format!("```rust\n{}\n```\n", code_trimmed)
6698 );
6699 assert!(
6700 block
6701 .selected_text(SelectionFormat::Source)
6702 .starts_with("```rust\n")
6703 );
6704 assert!(
6705 block
6706 .selected_text(SelectionFormat::Source)
6707 .trim_end()
6708 .ends_with("\n```")
6709 );
6710 }
6711
6712 #[test]
6713 fn code_block_selected_source_without_lang() {
6714 let block = selected_code_block("plain\n", None);
6715 let code_trimmed = block.selected_text(SelectionFormat::Plain);
6716 let code_trimmed = code_trimmed.trim_end_matches('\n');
6717 assert_eq!(
6718 block.selected_text(SelectionFormat::Source),
6719 format!("```\n{}\n```\n", code_trimmed)
6720 );
6721 }
6722
6723 #[test]
6724 fn document_selected_source_joins_blocks_with_blank_line() {
6725 use crate::text::document::ParsedDocument;
6726
6727 let document = ParsedDocument {
6731 source: String::new().into(),
6732 blocks: vec![
6733 BlockNode::Heading {
6734 level: 1,
6735 children: selected_paragraph("Title"),
6736 span: None,
6737 },
6738 BlockNode::Paragraph(selected_paragraph("A paragraph.")),
6739 selected_code_block("let x = 1;\n", Some("rust")),
6740 BlockNode::List {
6741 ordered: true,
6742 start: Some(1),
6743 span: None,
6744 children: vec![
6745 BlockNode::ListItem {
6746 children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
6747 spread: false,
6748 checked: None,
6749 task_marker: None,
6750 span: None,
6751 },
6752 BlockNode::ListItem {
6753 children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
6754 spread: false,
6755 checked: None,
6756 task_marker: None,
6757 span: None,
6758 },
6759 ],
6760 },
6761 ]
6762 .into(),
6763 };
6764
6765 assert_eq!(
6766 document.selected_text(SelectionFormat::Source, None),
6767 "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
6768 );
6769 }
6770
6771 #[test]
6772 fn code_block_equality_includes_code_content() {
6773 let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
6774 let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
6775
6776 assert_ne!(first, second);
6777 }
6778}