1use std::{
11 cell::RefCell,
12 collections::HashMap,
13 hash::{DefaultHasher, Hash, Hasher},
14 ops::Range,
15 path::Path,
16 rc::Rc,
17};
18
19use gpui::{
20 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
21 ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
22 StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
23 canvas, div, font, img, point, prelude::*, px, quad, size,
24};
25use theme::{TextStyle, Theme, Typeset};
26
27use crate::{
28 block,
29 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
30 layout::Layout,
31 preview,
32 select::{Cursor, Selection},
33 typography::Typography,
34};
35
36mod code;
38mod column;
39mod layouts;
40mod media;
41mod table;
42mod text;
43
44pub use code::*;
45use column::*;
46pub use layouts::*;
47use media::*;
48use table::*;
49pub use text::*;
50
51const BLOCK_GAP: f32 = 12.0;
52const LIST_GAP: f32 = 4.0;
53const INDENT_WIDTH: f32 = 22.0;
55const MARKER_WIDTH: f32 = 18.0;
57const MARKER_GAP: f32 = 8.0;
58const CODE_PADDING_X: f32 = 12.0;
60const CODE_PADDING_Y: f32 = 10.0;
61pub const PLAIN_LANGUAGE: &str = "Plain";
64const CARET_WIDTH: f32 = 1.5;
67const INLINE_CODE_RADIUS: f32 = 4.5;
70const INLINE_CODE_PAD_X: f32 = 2.0;
71const INLINE_CODE_INSET_Y: f32 = 2.0;
72const MENTION_CARD_WIDTH: f32 = 320.0;
74const CARD_HEIGHT: f32 = 116.0;
78const CARD_IMAGE_WIDTH: f32 = 180.0;
79const CARD_COVER_HEIGHT: f32 = 200.0;
80const CARD_PADDING: f32 = 14.0;
81const CARD_BORDER: f32 = 1.0;
82const CARD_ICON: f32 = 16.0;
83const CARD_COVER: f32 = 44.0;
84const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
86const CAPTION_GAP: f32 = 4.0;
87const IMAGE_EMPTY: &str = "Add an image";
89const CAPTION_HINT: &str = "Write a caption";
90const TABLE_CELL_PADDING: f32 = 12.0;
93const TABLE_DIVIDER: f32 = 1.0;
94const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
97const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
99
100#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
111pub enum Caption {
112 #[default]
114 Shown,
115 Hidden,
117}
118
119#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
123pub enum CopyButton {
124 #[default]
126 Shown,
127 Hidden,
130}
131
132#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
138#[non_exhaustive]
139pub enum Annotation {
140 #[default]
142 Open,
143 Resolved,
145 Active,
147 Highlight(crate::HighlightColor),
150 Match,
152 Current,
154}
155
156impl Annotation {
157 fn wash(self, theme: &Theme, highlight: crate::HighlightPaint) -> Hsla {
158 match self {
159 Self::Open => theme.warning.opacity(0.20),
160 Self::Resolved => theme.warning.opacity(0.08),
161 Self::Active => theme.warning.opacity(0.38),
162 Self::Highlight(color) => highlight(color, theme),
163 Self::Match => theme.accent.opacity(0.22),
164 Self::Current => theme.accent.opacity(0.48),
165 }
166 }
167}
168
169pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
174
175pub type OnImage = Rc<dyn Fn(usize, &mut Window, &mut App)>;
177
178#[derive(Clone)]
184pub enum Toggle {
185 Handled(OnToggle),
188 HitTested,
194}
195
196#[derive(Clone)]
202pub struct Editing<'a> {
203 pub selection: Option<Selection>,
206 pub caret_on: bool,
209 pub layouts: Option<&'a BlockLayouts>,
215 pub annotations: &'a [(Selection, Annotation)],
217 pub placeholder: Option<SharedString>,
219 pub caption: Caption,
220 pub typography: Option<Typography>,
224 pub toggle: Option<Toggle>,
227 pub image: Option<OnImage>,
231 pub copy: CopyButton,
233 pub base: Option<&'a Path>,
236 pub keep: &'a [usize],
241 pub scroll: Option<&'a gpui::ScrollHandle>,
245}
246
247impl Default for Editing<'_> {
248 fn default() -> Self {
249 Self {
250 selection: None,
251 caret_on: true,
254 layouts: None,
255 annotations: &[],
256 placeholder: None,
257 caption: Caption::default(),
258 typography: None,
259 toggle: None,
260 image: None,
261 copy: CopyButton::default(),
262 base: None,
263 keep: &[],
264 scroll: None,
265 }
266 }
267}
268
269#[derive(Clone, Copy)]
275struct Overlay<'a> {
276 block: usize,
277 part: Part,
278 selection: Option<Selection>,
279 caret_on: bool,
280 layouts: Option<&'a BlockLayouts>,
281 annotations: &'a [(Selection, Annotation)],
283 placeholder: Option<&'a SharedString>,
286 caption: Caption,
287 toggle: Option<&'a Toggle>,
290 image: Option<&'a OnImage>,
291 copy: CopyButton,
292 base: Option<&'a Path>,
293 highlight: crate::HighlightPaint,
294}
295
296impl<'a> Overlay<'a> {
297 fn at(self, part: Part) -> Self {
298 Self { part, ..self }
299 }
300
301 fn here(&self) -> Cursor {
302 Cursor::new(self.block, self.part, 0)
303 }
304
305 fn caret_painted(&self) -> Option<usize> {
311 self.caret_on.then(|| self.caret()).flatten()
312 }
313
314 fn caret(&self) -> Option<usize> {
316 self.selection
317 .map(|selection| selection.head)
318 .filter(|head| head.block == self.block && head.part == self.part)
319 .map(|head| head.offset)
320 }
321
322 fn selected(&self, len: usize) -> Option<Range<usize>> {
324 self.clip(self.selection?, len)
325 }
326
327 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
330 self.annotations
331 .iter()
332 .filter_map(|(range, kind)| {
333 Some((self.clip(*range, len)?, kind.wash(theme, self.highlight)))
334 })
335 .collect()
336 }
337
338 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
344 if selection.is_collapsed() {
345 return None;
346 }
347 let (start, end) = selection.ordered();
348 let here = self.here();
349 let (first, last) = (
350 Cursor::new(start.block, start.part, 0),
351 Cursor::new(end.block, end.part, 0),
352 );
353 if here < first || here > last {
354 return None;
355 }
356 let from = if here == first { start.offset } else { 0 };
357 let to = if here == last { end.offset } else { len };
358 (from < to).then_some(from..to.min(len))
359 }
360
361 fn covers_block(&self) -> bool {
365 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
366 return false;
367 };
368 let (start, end) = selection.ordered();
369 start.block < self.block && self.block < end.block
370 }
371}
372
373pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
379 if url.contains("://") {
380 return SharedString::from(url.to_string()).into();
381 }
382 match base {
385 Some(base) => base.join(url).into(),
386 None => std::path::PathBuf::from(url).into(),
387 }
388}
389
390pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
392 let doc = crate::parse_with(source, &crate::Marks::of(cx));
393 render(&doc, Caption::default(), window, cx)
394}
395
396pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
398 render_with(
399 doc,
400 Editing {
401 caption,
402 ..Editing::default()
403 },
404 window,
405 cx,
406 )
407}
408
409pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
417 let Editing {
418 selection,
419 caret_on,
420 layouts,
421 annotations,
422 placeholder,
423 caption,
424 typography,
425 toggle,
426 image,
427 copy,
428 base,
429 keep,
430 scroll,
431 } = editing;
432 let theme = Theme::of(cx).clone();
435 let typography = typography.unwrap_or_else(|| Typography::of(cx));
436 let highlight = crate::marks::highlight_paint_of(cx);
437 let gaps: Vec<Pixels> = doc
438 .blocks
439 .iter()
440 .enumerate()
441 .map(|(ix, block)| {
442 px(match doc.blocks.get(ix.wrapping_sub(1)) {
443 None => 0.0,
444 Some(previous) if tight(previous, block) => LIST_GAP,
445 Some(_) => BLOCK_GAP,
446 })
447 })
448 .collect();
449
450 let Some(layouts) = layouts else {
451 let mut column = div().flex().flex_col();
452 for (ix, block) in doc.blocks.iter().enumerate() {
453 let overlay = Overlay {
454 block: ix,
455 part: Part::Body,
456 selection,
457 caret_on,
458 layouts: None,
459 annotations,
460 placeholder: placeholder.as_ref(),
461 caption,
462 toggle: toggle.as_ref(),
463 image: image.as_ref(),
464 copy,
465 base,
466 highlight,
467 };
468 column = column
469 .child(block_box(block, overlay, &typography, &theme, window, cx).mt(gaps[ix]));
470 }
471 return column.into_any_element();
472 };
473
474 let keys: Vec<u64> = doc
475 .blocks
476 .iter()
477 .map(|block| block_key(block, &typography))
478 .collect();
479 layouts.prune(&keys);
480 let guesses: Vec<Guess> = doc
481 .blocks
482 .iter()
483 .map(|block| guess(block, &typography))
484 .collect();
485 let mut kept = keep.to_vec();
486 kept.extend(selection.map(|selection| selection.head.block));
487 kept.extend(selection.map(|selection| selection.anchor.block));
488 let owned = Owned {
489 blocks: doc.blocks.clone(),
490 selection,
491 caret_on,
492 layouts: layouts.clone(),
493 annotations: annotations.to_vec(),
494 placeholder,
495 caption,
496 toggle,
497 image,
498 copy,
499 base: base.map(Path::to_path_buf),
500 highlight,
501 typography,
502 theme,
503 };
504 Column {
505 layouts: layouts.clone(),
506 keys: keys.into(),
507 gaps: gaps.into(),
508 guesses: guesses.into(),
509 keep: kept,
510 scroll: scroll.cloned(),
511 item_of: Box::new(|at| at.block),
512 build: Box::new(move |ix, window, cx| {
513 let overlay = Overlay {
514 block: ix,
515 part: Part::Body,
516 selection: owned.selection,
517 caret_on: owned.caret_on,
518 layouts: Some(&owned.layouts),
519 annotations: &owned.annotations,
520 placeholder: owned.placeholder.as_ref(),
521 caption: owned.caption,
522 toggle: owned.toggle.as_ref(),
523 image: owned.image.as_ref(),
524 copy: owned.copy,
525 base: owned.base.as_deref(),
526 highlight: owned.highlight,
527 };
528 block_box(
529 &owned.blocks[ix],
530 overlay,
531 &owned.typography,
532 &owned.theme,
533 window,
534 cx,
535 )
536 .into_any_element()
537 }),
538 }
539 .into_any_element()
540}
541
542struct Owned {
544 blocks: Vec<Block>,
545 selection: Option<Selection>,
546 caret_on: bool,
547 layouts: BlockLayouts,
548 annotations: Vec<(Selection, Annotation)>,
549 placeholder: Option<SharedString>,
550 caption: Caption,
551 toggle: Option<Toggle>,
552 image: Option<OnImage>,
553 copy: CopyButton,
554 base: Option<std::path::PathBuf>,
555 highlight: crate::HighlightPaint,
556 typography: Typography,
557 theme: Theme,
558}
559
560fn block_box(
563 block: &Block,
564 overlay: Overlay,
565 typography: &Typography,
566 theme: &Theme,
567 window: &mut Window,
568 cx: &mut App,
569) -> gpui::Div {
570 let ix = overlay.block;
571 let frame = overlay.layouts.map(|layouts| {
574 let layouts = layouts.clone();
575 canvas(
576 move |bounds, _, _| layouts.record_block(ix, bounds),
577 |_, _, _, _| (),
578 )
579 .absolute()
580 .size_full()
581 });
582 div()
588 .w_full()
589 .pl(px(block.indent as f32 * INDENT_WIDTH))
590 .child(
591 div()
592 .w_full()
593 .relative()
594 .children(frame)
595 .when(overlay.covers_block() && block.opaque(), |el| {
599 el.rounded(px(4.0)).bg(theme.selection)
600 })
601 .child(block_element(block, overlay, typography, theme, window, cx)),
602 )
603}
604
605fn block_key(block: &Block, typography: &Typography) -> u64 {
608 let mut hasher = DefaultHasher::new();
609 block.hash(&mut hasher);
610 typography.body.size().to_bits().hash(&mut hasher);
611 typography.body.line_height().to_bits().hash(&mut hasher);
612 hasher.finish()
613}
614
615fn guess(block: &Block, typography: &Typography) -> Guess {
617 let body = px(typography.body.line_height());
618 let prose = |chars: usize, line: Pixels| Guess {
619 chars,
620 line,
621 rows: 0,
622 extra: px(0.0),
623 indent: px(0.0),
624 };
625 let guess = match &block.kind {
626 BlockKind::Paragraph(text)
627 | BlockKind::Bullet(text)
628 | BlockKind::Ordered { text, .. }
629 | BlockKind::Task { text, .. }
630 | BlockKind::Quote { text, .. } => prose(text.text.len(), body),
631 BlockKind::Heading { level, text } => prose(
632 text.text.len(),
633 px(typography.heading(*level).line_height()),
634 ),
635 BlockKind::Code { code, .. } => Guess {
636 rows: code.text.lines().count().max(1),
637 line: px(typography.code.line_height()),
638 extra: px(2.0 * CODE_PADDING_Y) + body,
639 ..prose(0, body)
640 },
641 BlockKind::Table { rows, .. } => Guess {
642 rows: rows.len() + 1,
643 extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
644 ..prose(0, body)
645 },
646 BlockKind::Image { alt, .. } => Guess {
647 extra: px(240.0),
648 ..prose(alt.text.len(), px(typography.caption.line_height()))
649 },
650 BlockKind::Bookmark { .. } => Guess {
651 extra: body * 3.0,
652 ..prose(0, body)
653 },
654 BlockKind::Rule => Guess {
655 extra: body,
656 ..prose(0, px(0.0))
657 },
658 };
659 Guess {
660 indent: px(block.indent as f32 * INDENT_WIDTH),
661 ..guess
662 }
663}
664
665fn tight(previous: &Block, next: &Block) -> bool {
667 let marker = |block: &Block| {
668 matches!(
669 block.kind,
670 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
671 )
672 };
673 marker(previous) && (marker(next) || next.indent > previous.indent)
674}
675
676fn block_element(
677 block: &Block,
678 overlay: Overlay,
679 typography: &Typography,
680 theme: &Theme,
681 window: &mut Window,
682 cx: &mut App,
683) -> AnyElement {
684 let body = overlay.at(Part::Body);
685 match &block.kind {
686 BlockKind::Paragraph(text) => text_element(
687 text,
688 typography.body.size(),
689 typography.body.line_height(),
690 FontWeight::NORMAL,
691 body,
692 theme,
693 cx,
694 ),
695 BlockKind::Heading { level, text } => {
696 let heading = typography.heading(*level);
697 text_element(
698 text,
699 heading.size(),
700 heading.line_height(),
701 heading.weight,
702 body,
703 theme,
704 cx,
705 )
706 }
707 BlockKind::Bullet(text) => {
708 marker_row(disc(typography, theme), text, body, typography, theme, cx)
709 }
710 BlockKind::Ordered { number, text } => marker_row(
711 div()
712 .flex_none()
713 .w(px(MARKER_WIDTH))
714 .text_size(px(typography.body.size()))
715 .line_height(px(typography.body.line_height()))
716 .text_color(theme.text_muted)
717 .child(SharedString::from(format!("{number}.")))
718 .into_any_element(),
719 text,
720 body,
721 typography,
722 theme,
723 cx,
724 ),
725 BlockKind::Task { checked, text } => marker_row(
726 checkbox(*checked, overlay, typography, theme),
727 text,
728 body,
729 typography,
730 theme,
731 cx,
732 ),
733 BlockKind::Quote { kind, text } => div()
734 .border_l_2()
735 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
736 .pl(px(12.0))
737 .pr(px(10.0))
738 .py(px(2.0))
739 .text_color(theme.text_muted)
740 .children(kind.map(|kind| {
741 div()
742 .pb(px(2.0))
743 .text_size(px(typography.body.size()))
744 .line_height(px(typography.body.line_height()))
745 .font_weight(FontWeight::SEMIBOLD)
746 .text_color(alert_color(kind, theme))
747 .child(kind.label())
748 }))
749 .child(text_element(
750 text,
751 typography.body.size(),
752 typography.body.line_height(),
753 FontWeight::NORMAL,
754 body,
755 theme,
756 cx,
757 ))
758 .into_any_element(),
759 BlockKind::Code { language, code } => {
760 let overlay = overlay.at(Part::Code);
761 let painted = overlay
765 .caret()
766 .is_none()
767 .then(|| block::render(language.as_deref(), &code.text, window, cx))
768 .flatten();
769 match painted {
770 Some(element) => div()
773 .when(overlay.covers_block(), |el| {
774 el.rounded(px(4.0)).bg(theme.selection)
775 })
776 .child(element)
777 .into_any_element(),
778 None => code_block(
779 language.as_deref(),
780 &code.text,
781 overlay,
782 typography,
783 theme,
784 window,
785 cx,
786 ),
787 }
788 }
789 BlockKind::Image { url, alt, width } => {
790 image(url, alt, *width, overlay, typography, theme, cx)
791 }
792 BlockKind::Bookmark { url, form } => {
793 bookmark(overlay.block, url, *form, typography, theme, cx)
794 }
795 BlockKind::Table {
796 align,
797 header,
798 rows,
799 } => table(align, header, rows, overlay, typography, theme, window, cx),
800 BlockKind::Rule => div()
801 .h(px(1.0))
802 .w_full()
803 .bg(theme.border)
804 .into_any_element(),
805 }
806}
807
808fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
810 div()
811 .flex_none()
812 .w(px(MARKER_WIDTH))
813 .h(px(typography.body.line_height()))
814 .flex()
815 .items_center()
816 .child(
817 div()
818 .ml(px(1.0))
819 .w(px(5.0))
820 .h(px(5.0))
821 .rounded_full()
822 .bg(theme.text_faint),
823 )
824 .into_any_element()
825}
826
827fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
828 let ix = overlay.block;
829 let mut box_ = div()
830 .relative()
831 .w(px(13.0))
832 .h(px(13.0))
833 .rounded(px(3.5))
834 .border_1()
835 .flex()
836 .items_center()
837 .justify_center();
838 box_ = if checked {
839 box_.bg(theme.solid)
840 .border_color(theme.solid)
841 .text_style(TextStyle::Caption)
842 .text_color(theme.on_solid)
843 .child("✓")
844 } else {
845 box_.border_color(theme.border_strong)
846 };
847 box_ = box_.children(overlay.layouts.map(|layouts| {
850 let layouts = layouts.clone();
851 canvas(
852 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
853 |_, _, _, _| (),
854 )
855 .absolute()
856 .size_full()
857 }));
858 if overlay.toggle.is_some() {
861 box_ = box_.cursor_pointer();
862 }
863 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
864 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
865 cx.stop_propagation();
869 toggle(ix, window, cx);
870 });
871 }
872
873 div()
874 .flex_none()
875 .w(px(MARKER_WIDTH))
876 .h(px(typography.body.line_height()))
877 .flex()
878 .items_center()
879 .child(box_)
880 .into_any_element()
881}
882
883fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
885 match kind {
886 QuoteKind::Note => theme.accent,
887 QuoteKind::Tip => theme.success,
888 QuoteKind::Important => theme.busy,
889 QuoteKind::Warning => theme.warning,
890 QuoteKind::Caution => theme.danger,
891 }
892}
893
894fn marker_row(
895 marker: AnyElement,
896 text: &Text,
897 overlay: Overlay,
898 typography: &Typography,
899 theme: &Theme,
900 cx: &App,
901) -> AnyElement {
902 div()
903 .flex()
904 .flex_row()
905 .gap(px(MARKER_GAP))
906 .child(marker)
907 .child(div().flex_1().min_w_0().child(text_element(
908 text,
909 typography.body.size(),
910 typography.body.line_height(),
911 FontWeight::NORMAL,
912 overlay,
913 theme,
914 cx,
915 )))
916 .into_any_element()
917}