1pub mod parser;
2mod path_range;
3
4use file_icons::FileIcons;
5use std::collections::{HashMap, HashSet};
6use std::iter;
7use std::mem;
8use std::ops::Range;
9use std::path::PathBuf;
10use std::rc::Rc;
11use std::sync::Arc;
12use std::time::Duration;
13
14use gpui::{
15 AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
16 FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext,
17 Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, Stateful,
18 StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle,
19 TextStyleRefinement, actions, point, quad,
20};
21use language::{Language, LanguageRegistry, Rope};
22use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
23use pulldown_cmark::Alignment;
24use theme::SyntaxTheme;
25use ui::{ButtonLike, Tooltip, prelude::*};
26use util::{ResultExt, TryFutureExt};
27use workspace::Workspace;
28
29use crate::parser::CodeBlockKind;
30
31/// A callback function that can be used to customize the style of links based on the destination URL.
32/// If the callback returns `None`, the default link style will be used.
33type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
34
35#[derive(Clone)]
36pub struct MarkdownStyle {
37 pub base_text_style: TextStyle,
38 pub code_block: StyleRefinement,
39 pub code_block_overflow_x_scroll: bool,
40 pub inline_code: TextStyleRefinement,
41 pub block_quote: TextStyleRefinement,
42 pub link: TextStyleRefinement,
43 pub link_callback: Option<LinkStyleCallback>,
44 pub rule_color: Hsla,
45 pub block_quote_border_color: Hsla,
46 pub syntax: Arc<SyntaxTheme>,
47 pub selection_background_color: Hsla,
48 pub heading: StyleRefinement,
49 pub table_overflow_x_scroll: bool,
50}
51
52impl Default for MarkdownStyle {
53 fn default() -> Self {
54 Self {
55 base_text_style: Default::default(),
56 code_block: Default::default(),
57 code_block_overflow_x_scroll: false,
58 inline_code: Default::default(),
59 block_quote: Default::default(),
60 link: Default::default(),
61 link_callback: None,
62 rule_color: Default::default(),
63 block_quote_border_color: Default::default(),
64 syntax: Arc::new(SyntaxTheme::default()),
65 selection_background_color: Default::default(),
66 heading: Default::default(),
67 table_overflow_x_scroll: false,
68 }
69 }
70}
71
72pub struct Markdown {
73 source: SharedString,
74 selection: Selection,
75 pressed_link: Option<RenderedLink>,
76 autoscroll_request: Option<usize>,
77 style: MarkdownStyle,
78 parsed_markdown: ParsedMarkdown,
79 should_reparse: bool,
80 pending_parse: Option<Task<Option<()>>>,
81 focus_handle: FocusHandle,
82 language_registry: Option<Arc<LanguageRegistry>>,
83 fallback_code_block_language: Option<String>,
84 open_url: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
85 options: Options,
86 copied_code_blocks: HashSet<ElementId>,
87}
88
89#[derive(Debug)]
90struct Options {
91 parse_links_only: bool,
92 copy_code_block_buttons: bool,
93}
94
95actions!(markdown, [Copy]);
96
97impl Markdown {
98 pub fn new(
99 source: SharedString,
100 style: MarkdownStyle,
101 language_registry: Option<Arc<LanguageRegistry>>,
102 fallback_code_block_language: Option<String>,
103 cx: &mut Context<Self>,
104 ) -> Self {
105 let focus_handle = cx.focus_handle();
106 let mut this = Self {
107 source,
108 selection: Selection::default(),
109 pressed_link: None,
110 autoscroll_request: None,
111 style,
112 should_reparse: false,
113 parsed_markdown: ParsedMarkdown::default(),
114 pending_parse: None,
115 focus_handle,
116 language_registry,
117 fallback_code_block_language,
118 options: Options {
119 parse_links_only: false,
120 copy_code_block_buttons: true,
121 },
122 open_url: None,
123 copied_code_blocks: HashSet::new(),
124 };
125 this.parse(cx);
126 this
127 }
128
129 pub fn open_url(
130 self,
131 open_url: impl Fn(SharedString, &mut Window, &mut App) + 'static,
132 ) -> Self {
133 Self {
134 open_url: Some(Box::new(open_url)),
135 ..self
136 }
137 }
138
139 pub fn new_text(source: SharedString, style: MarkdownStyle, cx: &mut Context<Self>) -> Self {
140 let focus_handle = cx.focus_handle();
141 let mut this = Self {
142 source,
143 selection: Selection::default(),
144 pressed_link: None,
145 autoscroll_request: None,
146 style,
147 should_reparse: false,
148 parsed_markdown: ParsedMarkdown::default(),
149 pending_parse: None,
150 focus_handle,
151 language_registry: None,
152 fallback_code_block_language: None,
153 options: Options {
154 parse_links_only: true,
155 copy_code_block_buttons: true,
156 },
157 open_url: None,
158 copied_code_blocks: HashSet::new(),
159 };
160 this.parse(cx);
161 this
162 }
163
164 pub fn source(&self) -> &str {
165 &self.source
166 }
167
168 pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
169 self.source = SharedString::new(self.source.to_string() + text);
170 self.parse(cx);
171 }
172
173 pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
174 if source == self.source() {
175 return;
176 }
177 self.source = source;
178 self.selection = Selection::default();
179 self.autoscroll_request = None;
180 self.pending_parse = None;
181 self.should_reparse = false;
182 self.parsed_markdown = ParsedMarkdown::default();
183 self.parse(cx);
184 }
185
186 pub fn parsed_markdown(&self) -> &ParsedMarkdown {
187 &self.parsed_markdown
188 }
189
190 fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
191 if self.selection.end <= self.selection.start {
192 return;
193 }
194 let text = text.text_for_range(self.selection.start..self.selection.end);
195 cx.write_to_clipboard(ClipboardItem::new_string(text));
196 }
197
198 fn parse(&mut self, cx: &mut Context<Self>) {
199 if self.source.is_empty() {
200 return;
201 }
202
203 if self.pending_parse.is_some() {
204 self.should_reparse = true;
205 return;
206 }
207
208 let source = self.source.clone();
209 let parse_text_only = self.options.parse_links_only;
210 let language_registry = self.language_registry.clone();
211 let fallback = self.fallback_code_block_language.clone();
212 let parsed = cx.background_spawn(async move {
213 if parse_text_only {
214 return anyhow::Ok(ParsedMarkdown {
215 events: Arc::from(parse_links_only(source.as_ref())),
216 source,
217 languages_by_name: HashMap::default(),
218 languages_by_path: HashMap::default(),
219 });
220 }
221 let (events, language_names, paths) = parse_markdown(&source);
222 let mut languages_by_name = HashMap::with_capacity(language_names.len());
223 let mut languages_by_path = HashMap::with_capacity(paths.len());
224 if let Some(registry) = language_registry.as_ref() {
225 for name in language_names {
226 let language = if !name.is_empty() {
227 registry.language_for_name(&name)
228 } else if let Some(fallback) = &fallback {
229 registry.language_for_name(fallback)
230 } else {
231 continue;
232 };
233 if let Ok(language) = language.await {
234 languages_by_name.insert(name, language);
235 }
236 }
237
238 for path in paths {
239 if let Ok(language) = registry.language_for_file_path(&path).await {
240 languages_by_path.insert(path, language);
241 }
242 }
243 }
244 anyhow::Ok(ParsedMarkdown {
245 source,
246 events: Arc::from(events),
247 languages_by_name,
248 languages_by_path,
249 })
250 });
251
252 self.should_reparse = false;
253 self.pending_parse = Some(cx.spawn(async move |this, cx| {
254 async move {
255 let parsed = parsed.await?;
256 this.update(cx, |this, cx| {
257 this.parsed_markdown = parsed;
258 this.pending_parse.take();
259 if this.should_reparse {
260 this.parse(cx);
261 }
262 cx.notify();
263 })
264 .ok();
265 anyhow::Ok(())
266 }
267 .log_err()
268 .await
269 }));
270 }
271
272 pub fn copy_code_block_buttons(mut self, should_copy: bool) -> Self {
273 self.options.copy_code_block_buttons = should_copy;
274 self
275 }
276}
277
278impl Render for Markdown {
279 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
280 MarkdownElement::new(cx.entity().clone(), self.style.clone())
281 }
282}
283
284impl Focusable for Markdown {
285 fn focus_handle(&self, _cx: &App) -> FocusHandle {
286 self.focus_handle.clone()
287 }
288}
289
290#[derive(Copy, Clone, Default, Debug)]
291struct Selection {
292 start: usize,
293 end: usize,
294 reversed: bool,
295 pending: bool,
296}
297
298impl Selection {
299 fn set_head(&mut self, head: usize) {
300 if head < self.tail() {
301 if !self.reversed {
302 self.end = self.start;
303 self.reversed = true;
304 }
305 self.start = head;
306 } else {
307 if self.reversed {
308 self.start = self.end;
309 self.reversed = false;
310 }
311 self.end = head;
312 }
313 }
314
315 fn tail(&self) -> usize {
316 if self.reversed { self.end } else { self.start }
317 }
318}
319
320#[derive(Default)]
321pub struct ParsedMarkdown {
322 source: SharedString,
323 events: Arc<[(Range<usize>, MarkdownEvent)]>,
324 languages_by_name: HashMap<SharedString, Arc<Language>>,
325 languages_by_path: HashMap<PathBuf, Arc<Language>>,
326}
327
328impl ParsedMarkdown {
329 pub fn source(&self) -> &SharedString {
330 &self.source
331 }
332
333 pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
334 &self.events
335 }
336}
337
338pub struct MarkdownElement {
339 markdown: Entity<Markdown>,
340 style: MarkdownStyle,
341}
342
343impl MarkdownElement {
344 fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
345 Self { markdown, style }
346 }
347
348 fn paint_selection(
349 &self,
350 bounds: Bounds<Pixels>,
351 rendered_text: &RenderedText,
352 window: &mut Window,
353 cx: &mut App,
354 ) {
355 let selection = self.markdown.read(cx).selection;
356 let selection_start = rendered_text.position_for_source_index(selection.start);
357 let selection_end = rendered_text.position_for_source_index(selection.end);
358
359 if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
360 selection_start.zip(selection_end)
361 {
362 if start_position.y == end_position.y {
363 window.paint_quad(quad(
364 Bounds::from_corners(
365 start_position,
366 point(end_position.x, end_position.y + end_line_height),
367 ),
368 Pixels::ZERO,
369 self.style.selection_background_color,
370 Edges::default(),
371 Hsla::transparent_black(),
372 BorderStyle::default(),
373 ));
374 } else {
375 window.paint_quad(quad(
376 Bounds::from_corners(
377 start_position,
378 point(bounds.right(), start_position.y + start_line_height),
379 ),
380 Pixels::ZERO,
381 self.style.selection_background_color,
382 Edges::default(),
383 Hsla::transparent_black(),
384 BorderStyle::default(),
385 ));
386
387 if end_position.y > start_position.y + start_line_height {
388 window.paint_quad(quad(
389 Bounds::from_corners(
390 point(bounds.left(), start_position.y + start_line_height),
391 point(bounds.right(), end_position.y),
392 ),
393 Pixels::ZERO,
394 self.style.selection_background_color,
395 Edges::default(),
396 Hsla::transparent_black(),
397 BorderStyle::default(),
398 ));
399 }
400
401 window.paint_quad(quad(
402 Bounds::from_corners(
403 point(bounds.left(), end_position.y),
404 point(end_position.x, end_position.y + end_line_height),
405 ),
406 Pixels::ZERO,
407 self.style.selection_background_color,
408 Edges::default(),
409 Hsla::transparent_black(),
410 BorderStyle::default(),
411 ));
412 }
413 }
414 }
415
416 fn paint_mouse_listeners(
417 &self,
418 hitbox: &Hitbox,
419 rendered_text: &RenderedText,
420 window: &mut Window,
421 cx: &mut App,
422 ) {
423 let is_hovering_link = hitbox.is_hovered(window)
424 && !self.markdown.read(cx).selection.pending
425 && rendered_text
426 .link_for_position(window.mouse_position())
427 .is_some();
428
429 if is_hovering_link {
430 window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
431 } else {
432 window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
433 }
434
435 self.on_mouse_event(window, cx, {
436 let rendered_text = rendered_text.clone();
437 let hitbox = hitbox.clone();
438 move |markdown, event: &MouseDownEvent, phase, window, cx| {
439 if hitbox.is_hovered(window) {
440 if phase.bubble() {
441 if let Some(link) = rendered_text.link_for_position(event.position) {
442 markdown.pressed_link = Some(link.clone());
443 } else {
444 let source_index =
445 match rendered_text.source_index_for_position(event.position) {
446 Ok(ix) | Err(ix) => ix,
447 };
448 let range = if event.click_count == 2 {
449 rendered_text.surrounding_word_range(source_index)
450 } else if event.click_count == 3 {
451 rendered_text.surrounding_line_range(source_index)
452 } else {
453 source_index..source_index
454 };
455 markdown.selection = Selection {
456 start: range.start,
457 end: range.end,
458 reversed: false,
459 pending: true,
460 };
461 window.focus(&markdown.focus_handle);
462 window.prevent_default();
463 }
464
465 cx.notify();
466 }
467 } else if phase.capture() {
468 markdown.selection = Selection::default();
469 markdown.pressed_link = None;
470 cx.notify();
471 }
472 }
473 });
474 self.on_mouse_event(window, cx, {
475 let rendered_text = rendered_text.clone();
476 let hitbox = hitbox.clone();
477 let was_hovering_link = is_hovering_link;
478 move |markdown, event: &MouseMoveEvent, phase, window, cx| {
479 if phase.capture() {
480 return;
481 }
482
483 if markdown.selection.pending {
484 let source_index = match rendered_text.source_index_for_position(event.position)
485 {
486 Ok(ix) | Err(ix) => ix,
487 };
488 markdown.selection.set_head(source_index);
489 markdown.autoscroll_request = Some(source_index);
490 cx.notify();
491 } else {
492 let is_hovering_link = hitbox.is_hovered(window)
493 && rendered_text.link_for_position(event.position).is_some();
494 if is_hovering_link != was_hovering_link {
495 cx.notify();
496 }
497 }
498 }
499 });
500 self.on_mouse_event(window, cx, {
501 let rendered_text = rendered_text.clone();
502 move |markdown, event: &MouseUpEvent, phase, window, cx| {
503 if phase.bubble() {
504 if let Some(pressed_link) = markdown.pressed_link.take() {
505 if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
506 if let Some(open_url) = markdown.open_url.as_mut() {
507 open_url(pressed_link.destination_url, window, cx);
508 } else {
509 cx.open_url(&pressed_link.destination_url);
510 }
511 }
512 }
513 } else if markdown.selection.pending {
514 markdown.selection.pending = false;
515 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
516 {
517 let text = rendered_text
518 .text_for_range(markdown.selection.start..markdown.selection.end);
519 cx.write_to_primary(ClipboardItem::new_string(text))
520 }
521 cx.notify();
522 }
523 }
524 });
525 }
526
527 fn autoscroll(
528 &self,
529 rendered_text: &RenderedText,
530 window: &mut Window,
531 cx: &mut App,
532 ) -> Option<()> {
533 let autoscroll_index = self
534 .markdown
535 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
536 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
537
538 let text_style = self.style.base_text_style.clone();
539 let font_id = window.text_system().resolve_font(&text_style.font());
540 let font_size = text_style.font_size.to_pixels(window.rem_size());
541 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
542 window.request_autoscroll(Bounds::from_corners(
543 point(position.x - 3. * em_width, position.y - 3. * line_height),
544 point(position.x + 3. * em_width, position.y + 3. * line_height),
545 ));
546 Some(())
547 }
548
549 fn on_mouse_event<T: MouseEvent>(
550 &self,
551 window: &mut Window,
552 _cx: &mut App,
553 mut f: impl 'static
554 + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
555 ) {
556 window.on_mouse_event({
557 let markdown = self.markdown.downgrade();
558 move |event, phase, window, cx| {
559 markdown
560 .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
561 .log_err();
562 }
563 });
564 }
565}
566
567impl Element for MarkdownElement {
568 type RequestLayoutState = RenderedMarkdown;
569 type PrepaintState = Hitbox;
570
571 fn id(&self) -> Option<ElementId> {
572 None
573 }
574
575 fn request_layout(
576 &mut self,
577 _id: Option<&GlobalElementId>,
578 window: &mut Window,
579 cx: &mut App,
580 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
581 let mut builder = MarkdownElementBuilder::new(
582 self.style.base_text_style.clone(),
583 self.style.syntax.clone(),
584 );
585 let parsed_markdown = &self.markdown.read(cx).parsed_markdown;
586 let markdown_end = if let Some(last) = parsed_markdown.events.last() {
587 last.0.end
588 } else {
589 0
590 };
591
592 let code_citation_id = SharedString::from("code-citation-link");
593 for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
594 match event {
595 MarkdownEvent::Start(tag) => {
596 match tag {
597 MarkdownTag::Paragraph => {
598 builder.push_div(
599 div().mb_2().line_height(rems(1.3)),
600 range,
601 markdown_end,
602 );
603 }
604 MarkdownTag::Heading { level, .. } => {
605 let mut heading = div().mb_2();
606 heading = match level {
607 pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
608 pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
609 pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
610 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
611 _ => heading,
612 };
613 heading.style().refine(&self.style.heading);
614 builder.push_text_style(
615 self.style.heading.text_style().clone().unwrap_or_default(),
616 );
617 builder.push_div(heading, range, markdown_end);
618 }
619 MarkdownTag::BlockQuote => {
620 builder.push_text_style(self.style.block_quote.clone());
621 builder.push_div(
622 div()
623 .pl_4()
624 .mb_2()
625 .border_l_4()
626 .border_color(self.style.block_quote_border_color),
627 range,
628 markdown_end,
629 );
630 }
631 MarkdownTag::CodeBlock(kind) => {
632 let language = match kind {
633 CodeBlockKind::Fenced => None,
634 CodeBlockKind::FencedLang(language) => {
635 parsed_markdown.languages_by_name.get(language).cloned()
636 }
637 CodeBlockKind::FencedSrc(path_range) => {
638 // If the path actually exists in the project, render a link to it.
639 if let Some(project_path) =
640 window.root::<Workspace>().flatten().and_then(|workspace| {
641 workspace
642 .read(cx)
643 .project()
644 .read(cx)
645 .find_project_path(&path_range.path, cx)
646 })
647 {
648 builder.flush_text();
649
650 builder.push_div(
651 div().relative().w_full(),
652 range,
653 markdown_end,
654 );
655
656 builder.modify_current_div(|el| {
657 let file_icon =
658 FileIcons::get_icon(&project_path.path, cx)
659 .map(|path| {
660 Icon::from_path(path)
661 .color(Color::Muted)
662 .into_any_element()
663 })
664 .unwrap_or_else(|| {
665 IconButton::new(
666 "file-path-icon",
667 IconName::File,
668 )
669 .shape(ui::IconButtonShape::Square)
670 .into_any_element()
671 });
672
673 el.child(
674 ButtonLike::new(ElementId::NamedInteger(
675 code_citation_id.clone(),
676 index,
677 ))
678 .child(
679 div()
680 .mb_1()
681 .flex()
682 .items_center()
683 .gap_1()
684 .child(file_icon)
685 .child(
686 Label::new(
687 project_path
688 .path
689 .display()
690 .to_string(),
691 )
692 .color(Color::Muted)
693 .underline(),
694 ),
695 )
696 .on_click({
697 let click_path = project_path.clone();
698 move |_, window, cx| {
699 if let Some(workspace) =
700 window.root::<Workspace>().flatten()
701 {
702 workspace.update(cx, |workspace, cx| {
703 workspace
704 .open_path(
705 click_path.clone(),
706 None,
707 true,
708 window,
709 cx,
710 )
711 .detach_and_log_err(cx);
712 })
713 }
714 }
715 }),
716 )
717 });
718
719 builder.pop_div();
720 }
721
722 parsed_markdown
723 .languages_by_path
724 .get(&path_range.path)
725 .cloned()
726 }
727 _ => None,
728 };
729
730 // This is a parent container that we can position the copy button inside.
731 builder.push_div(div().relative().w_full(), range, markdown_end);
732
733 let mut code_block = div()
734 .id(("code-block", range.start))
735 .rounded_lg()
736 .map(|mut code_block| {
737 if self.style.code_block_overflow_x_scroll {
738 code_block.style().restrict_scroll_to_axis = Some(true);
739 code_block.flex().overflow_x_scroll()
740 } else {
741 code_block.w_full()
742 }
743 });
744 code_block.style().refine(&self.style.code_block);
745 if let Some(code_block_text_style) = &self.style.code_block.text {
746 builder.push_text_style(code_block_text_style.to_owned());
747 }
748 builder.push_code_block(language);
749 builder.push_div(code_block, range, markdown_end);
750 }
751 MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
752 MarkdownTag::List(bullet_index) => {
753 builder.push_list(*bullet_index);
754 builder.push_div(div().pl_4(), range, markdown_end);
755 }
756 MarkdownTag::Item => {
757 let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
758 format!("{}.", bullet_index)
759 } else {
760 "•".to_string()
761 };
762 builder.push_div(
763 div()
764 .mb_1()
765 .h_flex()
766 .items_start()
767 .gap_1()
768 .line_height(rems(1.3))
769 .child(bullet),
770 range,
771 markdown_end,
772 );
773 // Without `w_0`, text doesn't wrap to the width of the container.
774 builder.push_div(div().flex_1().w_0(), range, markdown_end);
775 }
776 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
777 font_style: Some(FontStyle::Italic),
778 ..Default::default()
779 }),
780 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
781 font_weight: Some(FontWeight::BOLD),
782 ..Default::default()
783 }),
784 MarkdownTag::Strikethrough => {
785 builder.push_text_style(TextStyleRefinement {
786 strikethrough: Some(StrikethroughStyle {
787 thickness: px(1.),
788 color: None,
789 }),
790 ..Default::default()
791 })
792 }
793 MarkdownTag::Link { dest_url, .. } => {
794 if builder.code_block_stack.is_empty() {
795 builder.push_link(dest_url.clone(), range.clone());
796 let style = self
797 .style
798 .link_callback
799 .as_ref()
800 .and_then(|callback| callback(dest_url, cx))
801 .unwrap_or_else(|| self.style.link.clone());
802 builder.push_text_style(style)
803 }
804 }
805 MarkdownTag::MetadataBlock(_) => {}
806 MarkdownTag::Table(alignments) => {
807 builder.table_alignments = alignments.clone();
808 builder.push_div(
809 div()
810 .id(("table", range.start))
811 .flex()
812 .border_1()
813 .border_color(cx.theme().colors().border)
814 .rounded_sm()
815 .when(self.style.table_overflow_x_scroll, |mut table| {
816 table.style().restrict_scroll_to_axis = Some(true);
817 table.overflow_x_scroll()
818 }),
819 range,
820 markdown_end,
821 );
822 // This inner `v_flex` is so the table rows will stack vertically without disrupting the `overflow_x_scroll`.
823 builder.push_div(div().v_flex().flex_grow(), range, markdown_end);
824 }
825 MarkdownTag::TableHead => {
826 builder.push_div(
827 div()
828 .flex()
829 .justify_between()
830 .border_b_1()
831 .border_color(cx.theme().colors().border),
832 range,
833 markdown_end,
834 );
835 builder.push_text_style(TextStyleRefinement {
836 font_weight: Some(FontWeight::BOLD),
837 ..Default::default()
838 });
839 }
840 MarkdownTag::TableRow => {
841 builder.push_div(
842 div().h_flex().justify_between().px_1().py_0p5(),
843 range,
844 markdown_end,
845 );
846 }
847 MarkdownTag::TableCell => {
848 let column_count = builder.table_alignments.len();
849
850 builder.push_div(
851 div()
852 .flex()
853 .px_1()
854 .w(relative(1. / column_count as f32))
855 .truncate(),
856 range,
857 markdown_end,
858 );
859 }
860 _ => log::debug!("unsupported markdown tag {:?}", tag),
861 }
862 }
863 MarkdownEvent::End(tag) => match tag {
864 MarkdownTagEnd::Paragraph => {
865 builder.pop_div();
866 }
867 MarkdownTagEnd::Heading(_) => {
868 builder.pop_div();
869 builder.pop_text_style()
870 }
871 MarkdownTagEnd::BlockQuote(_kind) => {
872 builder.pop_text_style();
873 builder.pop_div()
874 }
875 MarkdownTagEnd::CodeBlock => {
876 builder.trim_trailing_newline();
877
878 builder.pop_div();
879 builder.pop_code_block();
880 if self.style.code_block.text.is_some() {
881 builder.pop_text_style();
882 }
883
884 if self.markdown.read(cx).options.copy_code_block_buttons {
885 builder.flush_text();
886 builder.modify_current_div(|el| {
887 let id =
888 ElementId::NamedInteger("copy-markdown-code".into(), range.end);
889 let was_copied =
890 self.markdown.read(cx).copied_code_blocks.contains(&id);
891 let copy_button = div().absolute().top_1().right_1().w_5().child(
892 IconButton::new(
893 id.clone(),
894 if was_copied {
895 IconName::Check
896 } else {
897 IconName::Copy
898 },
899 )
900 .icon_color(Color::Muted)
901 .shape(ui::IconButtonShape::Square)
902 .tooltip(Tooltip::text("Copy Code"))
903 .on_click({
904 let id = id.clone();
905 let markdown = self.markdown.clone();
906 let code = without_fences(
907 parsed_markdown.source()[range.clone()].trim(),
908 )
909 .to_string();
910 move |_event, _window, cx| {
911 let id = id.clone();
912 markdown.update(cx, |this, cx| {
913 this.copied_code_blocks.insert(id.clone());
914
915 cx.write_to_clipboard(ClipboardItem::new_string(
916 code.clone(),
917 ));
918
919 cx.spawn(async move |this, cx| {
920 cx.background_executor()
921 .timer(Duration::from_secs(2))
922 .await;
923
924 cx.update(|cx| {
925 this.update(cx, |this, cx| {
926 this.copied_code_blocks.remove(&id);
927 cx.notify();
928 })
929 })
930 .ok();
931 })
932 .detach();
933 });
934 }
935 }),
936 );
937
938 el.child(copy_button)
939 });
940 }
941
942 // Pop the parent container.
943 builder.pop_div();
944 }
945 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
946 MarkdownTagEnd::List(_) => {
947 builder.pop_list();
948 builder.pop_div();
949 }
950 MarkdownTagEnd::Item => {
951 builder.pop_div();
952 builder.pop_div();
953 }
954 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
955 MarkdownTagEnd::Strong => builder.pop_text_style(),
956 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
957 MarkdownTagEnd::Link => {
958 if builder.code_block_stack.is_empty() {
959 builder.pop_text_style()
960 }
961 }
962 MarkdownTagEnd::Table => {
963 builder.pop_div();
964 builder.pop_div();
965 builder.table_alignments.clear();
966 }
967 MarkdownTagEnd::TableHead => {
968 builder.pop_div();
969 builder.pop_text_style();
970 }
971 MarkdownTagEnd::TableRow => {
972 builder.pop_div();
973 }
974 MarkdownTagEnd::TableCell => {
975 builder.pop_div();
976 }
977 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
978 },
979 MarkdownEvent::Text => {
980 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
981 }
982 MarkdownEvent::SubstitutedText(text) => {
983 builder.push_text(text, range.start);
984 }
985 MarkdownEvent::Code => {
986 builder.push_text_style(self.style.inline_code.clone());
987 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
988 builder.pop_text_style();
989 }
990 MarkdownEvent::Html => {
991 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
992 }
993 MarkdownEvent::InlineHtml => {
994 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
995 }
996 MarkdownEvent::Rule => {
997 builder.push_div(
998 div()
999 .border_b_1()
1000 .my_2()
1001 .border_color(self.style.rule_color),
1002 range,
1003 markdown_end,
1004 );
1005 builder.pop_div()
1006 }
1007 MarkdownEvent::SoftBreak => builder.push_text(" ", range.start),
1008 MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
1009 _ => log::error!("unsupported markdown event {:?}", event),
1010 }
1011 }
1012 let mut rendered_markdown = builder.build();
1013 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1014 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1015 (layout_id, rendered_markdown)
1016 }
1017
1018 fn prepaint(
1019 &mut self,
1020 _id: Option<&GlobalElementId>,
1021 bounds: Bounds<Pixels>,
1022 rendered_markdown: &mut Self::RequestLayoutState,
1023 window: &mut Window,
1024 cx: &mut App,
1025 ) -> Self::PrepaintState {
1026 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1027 window.set_focus_handle(&focus_handle, cx);
1028
1029 let hitbox = window.insert_hitbox(bounds, false);
1030 rendered_markdown.element.prepaint(window, cx);
1031 self.autoscroll(&rendered_markdown.text, window, cx);
1032 hitbox
1033 }
1034
1035 fn paint(
1036 &mut self,
1037 _id: Option<&GlobalElementId>,
1038 bounds: Bounds<Pixels>,
1039 rendered_markdown: &mut Self::RequestLayoutState,
1040 hitbox: &mut Self::PrepaintState,
1041 window: &mut Window,
1042 cx: &mut App,
1043 ) {
1044 let mut context = KeyContext::default();
1045 context.add("Markdown");
1046 window.set_key_context(context);
1047 let entity = self.markdown.clone();
1048 window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1049 let text = rendered_markdown.text.clone();
1050 move |_, phase, window, cx| {
1051 let text = text.clone();
1052 if phase == DispatchPhase::Bubble {
1053 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1054 }
1055 }
1056 });
1057
1058 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1059 rendered_markdown.element.paint(window, cx);
1060 self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1061 }
1062}
1063
1064impl IntoElement for MarkdownElement {
1065 type Element = Self;
1066
1067 fn into_element(self) -> Self::Element {
1068 self
1069 }
1070}
1071
1072enum AnyDiv {
1073 Div(Div),
1074 Stateful(Stateful<Div>),
1075}
1076
1077impl AnyDiv {
1078 fn into_any_element(self) -> AnyElement {
1079 match self {
1080 Self::Div(div) => div.into_any_element(),
1081 Self::Stateful(div) => div.into_any_element(),
1082 }
1083 }
1084}
1085
1086impl From<Div> for AnyDiv {
1087 fn from(value: Div) -> Self {
1088 Self::Div(value)
1089 }
1090}
1091
1092impl From<Stateful<Div>> for AnyDiv {
1093 fn from(value: Stateful<Div>) -> Self {
1094 Self::Stateful(value)
1095 }
1096}
1097
1098impl Styled for AnyDiv {
1099 fn style(&mut self) -> &mut StyleRefinement {
1100 match self {
1101 Self::Div(div) => div.style(),
1102 Self::Stateful(div) => div.style(),
1103 }
1104 }
1105}
1106
1107impl ParentElement for AnyDiv {
1108 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1109 match self {
1110 Self::Div(div) => div.extend(elements),
1111 Self::Stateful(div) => div.extend(elements),
1112 }
1113 }
1114}
1115
1116struct MarkdownElementBuilder {
1117 div_stack: Vec<AnyDiv>,
1118 rendered_lines: Vec<RenderedLine>,
1119 pending_line: PendingLine,
1120 rendered_links: Vec<RenderedLink>,
1121 current_source_index: usize,
1122 base_text_style: TextStyle,
1123 text_style_stack: Vec<TextStyleRefinement>,
1124 code_block_stack: Vec<Option<Arc<Language>>>,
1125 list_stack: Vec<ListStackEntry>,
1126 table_alignments: Vec<Alignment>,
1127 syntax_theme: Arc<SyntaxTheme>,
1128}
1129
1130#[derive(Default)]
1131struct PendingLine {
1132 text: String,
1133 runs: Vec<TextRun>,
1134 source_mappings: Vec<SourceMapping>,
1135}
1136
1137struct ListStackEntry {
1138 bullet_index: Option<u64>,
1139}
1140
1141impl MarkdownElementBuilder {
1142 fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
1143 Self {
1144 div_stack: vec![div().debug_selector(|| "inner".into()).into()],
1145 rendered_lines: Vec::new(),
1146 pending_line: PendingLine::default(),
1147 rendered_links: Vec::new(),
1148 current_source_index: 0,
1149 base_text_style,
1150 text_style_stack: Vec::new(),
1151 code_block_stack: Vec::new(),
1152 list_stack: Vec::new(),
1153 table_alignments: Vec::new(),
1154 syntax_theme,
1155 }
1156 }
1157
1158 fn push_text_style(&mut self, style: TextStyleRefinement) {
1159 self.text_style_stack.push(style);
1160 }
1161
1162 fn text_style(&self) -> TextStyle {
1163 let mut style = self.base_text_style.clone();
1164 for refinement in &self.text_style_stack {
1165 style.refine(refinement);
1166 }
1167 style
1168 }
1169
1170 fn pop_text_style(&mut self) {
1171 self.text_style_stack.pop();
1172 }
1173
1174 fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
1175 let mut div = div.into();
1176 self.flush_text();
1177
1178 if range.start == 0 {
1179 // Remove the top margin on the first element.
1180 div.style().refine(&StyleRefinement {
1181 margin: gpui::EdgesRefinement {
1182 top: Some(Length::Definite(px(0.).into())),
1183 left: None,
1184 right: None,
1185 bottom: None,
1186 },
1187 ..Default::default()
1188 });
1189 }
1190
1191 if range.end == markdown_end {
1192 div.style().refine(&StyleRefinement {
1193 margin: gpui::EdgesRefinement {
1194 top: None,
1195 left: None,
1196 right: None,
1197 bottom: Some(Length::Definite(rems(0.).into())),
1198 },
1199 ..Default::default()
1200 });
1201 }
1202
1203 self.div_stack.push(div);
1204 }
1205
1206 fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1207 self.flush_text();
1208 if let Some(div) = self.div_stack.pop() {
1209 self.div_stack.push(f(div));
1210 }
1211 }
1212
1213 fn pop_div(&mut self) {
1214 self.flush_text();
1215 let div = self.div_stack.pop().unwrap().into_any_element();
1216 self.div_stack.last_mut().unwrap().extend(iter::once(div));
1217 }
1218
1219 fn push_list(&mut self, bullet_index: Option<u64>) {
1220 self.list_stack.push(ListStackEntry { bullet_index });
1221 }
1222
1223 fn next_bullet_index(&mut self) -> Option<u64> {
1224 self.list_stack.last_mut().and_then(|entry| {
1225 let item_index = entry.bullet_index.as_mut()?;
1226 *item_index += 1;
1227 Some(*item_index - 1)
1228 })
1229 }
1230
1231 fn pop_list(&mut self) {
1232 self.list_stack.pop();
1233 }
1234
1235 fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1236 self.code_block_stack.push(language);
1237 }
1238
1239 fn pop_code_block(&mut self) {
1240 self.code_block_stack.pop();
1241 }
1242
1243 fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1244 self.rendered_links.push(RenderedLink {
1245 source_range,
1246 destination_url,
1247 });
1248 }
1249
1250 fn push_text(&mut self, text: &str, source_index: usize) {
1251 self.pending_line.source_mappings.push(SourceMapping {
1252 rendered_index: self.pending_line.text.len(),
1253 source_index,
1254 });
1255 self.pending_line.text.push_str(text);
1256 self.current_source_index = source_index + text.len();
1257
1258 if let Some(Some(language)) = self.code_block_stack.last() {
1259 let mut offset = 0;
1260 for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1261 if range.start > offset {
1262 self.pending_line
1263 .runs
1264 .push(self.text_style().to_run(range.start - offset));
1265 }
1266
1267 let mut run_style = self.text_style();
1268 if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1269 run_style = run_style.highlight(highlight);
1270 }
1271 self.pending_line.runs.push(run_style.to_run(range.len()));
1272 offset = range.end;
1273 }
1274
1275 if offset < text.len() {
1276 self.pending_line
1277 .runs
1278 .push(self.text_style().to_run(text.len() - offset));
1279 }
1280 } else {
1281 self.pending_line
1282 .runs
1283 .push(self.text_style().to_run(text.len()));
1284 }
1285 }
1286
1287 fn trim_trailing_newline(&mut self) {
1288 if self.pending_line.text.ends_with('\n') {
1289 self.pending_line
1290 .text
1291 .truncate(self.pending_line.text.len() - 1);
1292 self.pending_line.runs.last_mut().unwrap().len -= 1;
1293 self.current_source_index -= 1;
1294 }
1295 }
1296
1297 fn flush_text(&mut self) {
1298 let line = mem::take(&mut self.pending_line);
1299 if line.text.is_empty() {
1300 return;
1301 }
1302
1303 let text = StyledText::new(line.text).with_runs(line.runs);
1304 self.rendered_lines.push(RenderedLine {
1305 layout: text.layout().clone(),
1306 source_mappings: line.source_mappings,
1307 source_end: self.current_source_index,
1308 });
1309 self.div_stack.last_mut().unwrap().extend([text.into_any()]);
1310 }
1311
1312 fn build(mut self) -> RenderedMarkdown {
1313 debug_assert_eq!(self.div_stack.len(), 1);
1314 self.flush_text();
1315 RenderedMarkdown {
1316 element: self.div_stack.pop().unwrap().into_any_element(),
1317 text: RenderedText {
1318 lines: self.rendered_lines.into(),
1319 links: self.rendered_links.into(),
1320 },
1321 }
1322 }
1323}
1324
1325struct RenderedLine {
1326 layout: TextLayout,
1327 source_mappings: Vec<SourceMapping>,
1328 source_end: usize,
1329}
1330
1331impl RenderedLine {
1332 fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
1333 let mapping = match self
1334 .source_mappings
1335 .binary_search_by_key(&source_index, |probe| probe.source_index)
1336 {
1337 Ok(ix) => &self.source_mappings[ix],
1338 Err(ix) => &self.source_mappings[ix - 1],
1339 };
1340 mapping.rendered_index + (source_index - mapping.source_index)
1341 }
1342
1343 fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
1344 let mapping = match self
1345 .source_mappings
1346 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
1347 {
1348 Ok(ix) => &self.source_mappings[ix],
1349 Err(ix) => &self.source_mappings[ix - 1],
1350 };
1351 mapping.source_index + (rendered_index - mapping.rendered_index)
1352 }
1353
1354 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1355 let line_rendered_index;
1356 let out_of_bounds;
1357 match self.layout.index_for_position(position) {
1358 Ok(ix) => {
1359 line_rendered_index = ix;
1360 out_of_bounds = false;
1361 }
1362 Err(ix) => {
1363 line_rendered_index = ix;
1364 out_of_bounds = true;
1365 }
1366 };
1367 let source_index = self.source_index_for_rendered_index(line_rendered_index);
1368 if out_of_bounds {
1369 Err(source_index)
1370 } else {
1371 Ok(source_index)
1372 }
1373 }
1374}
1375
1376#[derive(Copy, Clone, Debug, Default)]
1377struct SourceMapping {
1378 rendered_index: usize,
1379 source_index: usize,
1380}
1381
1382pub struct RenderedMarkdown {
1383 element: AnyElement,
1384 text: RenderedText,
1385}
1386
1387#[derive(Clone)]
1388struct RenderedText {
1389 lines: Rc<[RenderedLine]>,
1390 links: Rc<[RenderedLink]>,
1391}
1392
1393#[derive(Clone, Eq, PartialEq)]
1394struct RenderedLink {
1395 source_range: Range<usize>,
1396 destination_url: SharedString,
1397}
1398
1399impl RenderedText {
1400 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1401 let mut lines = self.lines.iter().peekable();
1402
1403 while let Some(line) = lines.next() {
1404 let line_bounds = line.layout.bounds();
1405 if position.y > line_bounds.bottom() {
1406 if let Some(next_line) = lines.peek() {
1407 if position.y < next_line.layout.bounds().top() {
1408 return Err(line.source_end);
1409 }
1410 }
1411
1412 continue;
1413 }
1414
1415 return line.source_index_for_position(position);
1416 }
1417
1418 Err(self.lines.last().map_or(0, |line| line.source_end))
1419 }
1420
1421 fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
1422 for line in self.lines.iter() {
1423 let line_source_start = line.source_mappings.first().unwrap().source_index;
1424 if source_index < line_source_start {
1425 break;
1426 } else if source_index > line.source_end {
1427 continue;
1428 } else {
1429 let line_height = line.layout.line_height();
1430 let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
1431 let position = line.layout.position_for_index(rendered_index_within_line)?;
1432 return Some((position, line_height));
1433 }
1434 }
1435 None
1436 }
1437
1438 fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
1439 for line in self.lines.iter() {
1440 if source_index > line.source_end {
1441 continue;
1442 }
1443
1444 let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
1445 let rendered_index_in_line =
1446 line.rendered_index_for_source_index(source_index) - line_rendered_start;
1447 let text = line.layout.text();
1448 let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
1449 idx + ' '.len_utf8()
1450 } else {
1451 0
1452 };
1453 let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
1454 rendered_index_in_line + idx
1455 } else {
1456 text.len()
1457 };
1458
1459 return line.source_index_for_rendered_index(line_rendered_start + previous_space)
1460 ..line.source_index_for_rendered_index(line_rendered_start + next_space);
1461 }
1462
1463 source_index..source_index
1464 }
1465
1466 fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
1467 for line in self.lines.iter() {
1468 if source_index > line.source_end {
1469 continue;
1470 }
1471 let line_source_start = line.source_mappings.first().unwrap().source_index;
1472 return line_source_start..line.source_end;
1473 }
1474
1475 source_index..source_index
1476 }
1477
1478 fn text_for_range(&self, range: Range<usize>) -> String {
1479 let mut ret = vec![];
1480
1481 for line in self.lines.iter() {
1482 if range.start > line.source_end {
1483 continue;
1484 }
1485 let line_source_start = line.source_mappings.first().unwrap().source_index;
1486 if range.end < line_source_start {
1487 break;
1488 }
1489
1490 let text = line.layout.text();
1491
1492 let start = if range.start < line_source_start {
1493 0
1494 } else {
1495 line.rendered_index_for_source_index(range.start)
1496 };
1497 let end = if range.end > line.source_end {
1498 line.rendered_index_for_source_index(line.source_end)
1499 } else {
1500 line.rendered_index_for_source_index(range.end)
1501 }
1502 .min(text.len());
1503
1504 ret.push(text[start..end].to_string());
1505 }
1506 ret.join("\n")
1507 }
1508
1509 fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1510 let source_index = self.source_index_for_position(position).ok()?;
1511 self.links
1512 .iter()
1513 .find(|link| link.source_range.contains(&source_index))
1514 }
1515}
1516
1517/// Some markdown blocks are indented, and others have e.g. ```rust … ``` around them.
1518/// If this block is fenced with backticks, strip them off (and the language name).
1519/// We use this when copying code blocks to the clipboard.
1520fn without_fences(mut markdown: &str) -> &str {
1521 if let Some(opening_backticks) = markdown.find("```") {
1522 markdown = &markdown[opening_backticks..];
1523
1524 // Trim off the next newline. This also trims off a language name if it's there.
1525 if let Some(newline) = markdown.find('\n') {
1526 markdown = &markdown[newline + 1..];
1527 }
1528 };
1529
1530 if let Some(closing_backticks) = markdown.rfind("```") {
1531 markdown = &markdown[..closing_backticks];
1532 };
1533
1534 markdown
1535}
1536
1537#[cfg(test)]
1538mod tests {
1539 use super::*;
1540
1541 #[test]
1542 fn test_without_fences() {
1543 let input = "```rust\nlet x = 5;\n```";
1544 assert_eq!(without_fences(input), "let x = 5;\n");
1545
1546 let input = " ```\nno language\n``` ";
1547 assert_eq!(without_fences(input), "no language\n");
1548
1549 let input = "plain text";
1550 assert_eq!(without_fences(input), "plain text");
1551
1552 let input = "```python\nprint('hello')\nprint('world')\n```";
1553 assert_eq!(without_fences(input), "print('hello')\nprint('world')\n");
1554 }
1555}