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