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