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