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