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