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