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