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