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