1pub mod parser;
2mod path_range;
3
4use base64::Engine as _;
5use futures::FutureExt as _;
6use gpui::HitboxBehavior;
7use language::LanguageName;
8use log::Level;
9pub use path_range::{LineCol, PathWithRange};
10use ui::Checkbox;
11
12use std::borrow::Cow;
13use std::iter;
14use std::mem;
15use std::ops::Range;
16use std::path::Path;
17use std::rc::Rc;
18use std::sync::Arc;
19use std::time::Duration;
20
21use collections::{HashMap, HashSet};
22use gpui::{
23 AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
24 FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
25 ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
26 Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task,
27 TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad,
28};
29use language::{Language, LanguageRegistry, Rope};
30use parser::CodeBlockMetadata;
31use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
32use pulldown_cmark::Alignment;
33use sum_tree::TreeMap;
34use theme::SyntaxTheme;
35use ui::{ScrollAxes, Scrollbars, Tooltip, WithScrollbar, prelude::*};
36use util::ResultExt;
37
38use crate::parser::CodeBlockKind;
39
40/// A callback function that can be used to customize the style of links based on the destination URL.
41/// If the callback returns `None`, the default link style will be used.
42type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
43
44/// Defines custom style refinements for each heading level (H1-H6)
45#[derive(Clone, Default)]
46pub struct HeadingLevelStyles {
47 pub h1: Option<TextStyleRefinement>,
48 pub h2: Option<TextStyleRefinement>,
49 pub h3: Option<TextStyleRefinement>,
50 pub h4: Option<TextStyleRefinement>,
51 pub h5: Option<TextStyleRefinement>,
52 pub h6: Option<TextStyleRefinement>,
53}
54
55#[derive(Clone)]
56pub struct MarkdownStyle {
57 pub base_text_style: TextStyle,
58 pub container_style: StyleRefinement,
59 pub code_block: StyleRefinement,
60 pub code_block_overflow_x_scroll: bool,
61 pub inline_code: TextStyleRefinement,
62 pub block_quote: TextStyleRefinement,
63 pub link: TextStyleRefinement,
64 pub link_callback: Option<LinkStyleCallback>,
65 pub rule_color: Hsla,
66 pub block_quote_border_color: Hsla,
67 pub syntax: Arc<SyntaxTheme>,
68 pub selection_background_color: Hsla,
69 pub heading: StyleRefinement,
70 pub heading_level_styles: Option<HeadingLevelStyles>,
71 pub height_is_multiple_of_line_height: bool,
72 pub prevent_mouse_interaction: bool,
73 pub table_columns_min_size: bool,
74}
75
76impl Default for MarkdownStyle {
77 fn default() -> Self {
78 Self {
79 base_text_style: Default::default(),
80 container_style: Default::default(),
81 code_block: Default::default(),
82 code_block_overflow_x_scroll: false,
83 inline_code: Default::default(),
84 block_quote: Default::default(),
85 link: Default::default(),
86 link_callback: None,
87 rule_color: Default::default(),
88 block_quote_border_color: Default::default(),
89 syntax: Arc::new(SyntaxTheme::default()),
90 selection_background_color: Default::default(),
91 heading: Default::default(),
92 heading_level_styles: None,
93 height_is_multiple_of_line_height: false,
94 prevent_mouse_interaction: false,
95 table_columns_min_size: false,
96 }
97 }
98}
99
100pub struct Markdown {
101 source: SharedString,
102 selection: Selection,
103 pressed_link: Option<RenderedLink>,
104 autoscroll_request: Option<usize>,
105 parsed_markdown: ParsedMarkdown,
106 images_by_source_offset: HashMap<usize, Arc<Image>>,
107 should_reparse: bool,
108 pending_parse: Option<Task<()>>,
109 focus_handle: FocusHandle,
110 language_registry: Option<Arc<LanguageRegistry>>,
111 fallback_code_block_language: Option<LanguageName>,
112 options: Options,
113 copied_code_blocks: HashSet<ElementId>,
114 code_block_scroll_handles: HashMap<usize, ScrollHandle>,
115}
116
117struct Options {
118 parse_links_only: bool,
119}
120
121pub enum CodeBlockRenderer {
122 Default {
123 copy_button: bool,
124 copy_button_on_hover: bool,
125 border: bool,
126 },
127 Custom {
128 render: CodeBlockRenderFn,
129 /// A function that can modify the parent container after the code block
130 /// content has been appended as a child element.
131 transform: Option<CodeBlockTransformFn>,
132 },
133}
134
135pub type CodeBlockRenderFn = Arc<
136 dyn Fn(
137 &CodeBlockKind,
138 &ParsedMarkdown,
139 Range<usize>,
140 CodeBlockMetadata,
141 &mut Window,
142 &App,
143 ) -> Div,
144>;
145
146pub type CodeBlockTransformFn =
147 Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
148
149actions!(
150 markdown,
151 [
152 /// Copies the selected text to the clipboard.
153 Copy,
154 ]
155);
156
157impl Markdown {
158 pub fn new(
159 source: SharedString,
160 language_registry: Option<Arc<LanguageRegistry>>,
161 fallback_code_block_language: Option<LanguageName>,
162 cx: &mut Context<Self>,
163 ) -> Self {
164 let focus_handle = cx.focus_handle();
165 let mut this = Self {
166 source,
167 selection: Selection::default(),
168 pressed_link: None,
169 autoscroll_request: None,
170 should_reparse: false,
171 images_by_source_offset: Default::default(),
172 parsed_markdown: ParsedMarkdown::default(),
173 pending_parse: None,
174 focus_handle,
175 language_registry,
176 fallback_code_block_language,
177 options: Options {
178 parse_links_only: false,
179 },
180 copied_code_blocks: HashSet::default(),
181 code_block_scroll_handles: HashMap::default(),
182 };
183 this.parse(cx);
184 this
185 }
186
187 pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
188 let focus_handle = cx.focus_handle();
189 let mut this = Self {
190 source,
191 selection: Selection::default(),
192 pressed_link: None,
193 autoscroll_request: None,
194 should_reparse: false,
195 parsed_markdown: ParsedMarkdown::default(),
196 images_by_source_offset: Default::default(),
197 pending_parse: None,
198 focus_handle,
199 language_registry: None,
200 fallback_code_block_language: None,
201 options: Options {
202 parse_links_only: true,
203 },
204 copied_code_blocks: HashSet::default(),
205 code_block_scroll_handles: HashMap::default(),
206 };
207 this.parse(cx);
208 this
209 }
210
211 fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle {
212 self.code_block_scroll_handles
213 .entry(id)
214 .or_insert_with(ScrollHandle::new)
215 .clone()
216 }
217
218 fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) {
219 self.code_block_scroll_handles
220 .retain(|id, _| ids.contains(id));
221 }
222
223 fn clear_code_block_scroll_handles(&mut self) {
224 self.code_block_scroll_handles.clear();
225 }
226
227 pub fn is_parsing(&self) -> bool {
228 self.pending_parse.is_some()
229 }
230
231 pub fn source(&self) -> &str {
232 &self.source
233 }
234
235 pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
236 self.source = SharedString::new(self.source.to_string() + text);
237 self.parse(cx);
238 }
239
240 pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
241 self.source = source.into();
242 self.parse(cx);
243 }
244
245 pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
246 if source == self.source() {
247 return;
248 }
249 self.source = source;
250 self.selection = Selection::default();
251 self.autoscroll_request = None;
252 self.pending_parse = None;
253 self.should_reparse = false;
254 // Don't clear parsed_markdown here - keep existing content visible until new parse completes
255 self.parse(cx);
256 }
257
258 #[cfg(any(test, feature = "test-support"))]
259 pub fn parsed_markdown(&self) -> &ParsedMarkdown {
260 &self.parsed_markdown
261 }
262
263 pub fn escape(s: &str) -> Cow<'_, str> {
264 // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars.
265 let count = s
266 .bytes()
267 .filter(|c| *c == b'\n' || c.is_ascii_punctuation())
268 .count();
269 if count > 0 {
270 let mut output = String::with_capacity(s.len() + count);
271 let mut is_newline = false;
272 for c in s.chars() {
273 if is_newline && c == ' ' {
274 continue;
275 }
276 is_newline = c == '\n';
277 if c == '\n' {
278 output.push('\n')
279 } else if c.is_ascii_punctuation() {
280 output.push('\\')
281 }
282 output.push(c)
283 }
284 output.into()
285 } else {
286 s.into()
287 }
288 }
289
290 fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
291 if self.selection.end <= self.selection.start {
292 return;
293 }
294 let text = text.text_for_range(self.selection.start..self.selection.end);
295 cx.write_to_clipboard(ClipboardItem::new_string(text));
296 }
297
298 fn parse(&mut self, cx: &mut Context<Self>) {
299 if self.source.is_empty() {
300 return;
301 }
302
303 if self.pending_parse.is_some() {
304 self.should_reparse = true;
305 return;
306 }
307 self.should_reparse = false;
308 self.pending_parse = Some(self.start_background_parse(cx));
309 }
310
311 fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
312 let source = self.source.clone();
313 let should_parse_links_only = self.options.parse_links_only;
314 let language_registry = self.language_registry.clone();
315 let fallback = self.fallback_code_block_language.clone();
316
317 let parsed = cx.background_spawn(async move {
318 if should_parse_links_only {
319 return (
320 ParsedMarkdown {
321 events: Arc::from(parse_links_only(source.as_ref())),
322 source,
323 languages_by_name: TreeMap::default(),
324 languages_by_path: TreeMap::default(),
325 },
326 Default::default(),
327 );
328 }
329
330 let (events, language_names, paths) = parse_markdown(&source);
331 let mut images_by_source_offset = HashMap::default();
332 let mut languages_by_name = TreeMap::default();
333 let mut languages_by_path = TreeMap::default();
334 if let Some(registry) = language_registry.as_ref() {
335 for name in language_names {
336 let language = if !name.is_empty() {
337 registry.language_for_name_or_extension(&name).left_future()
338 } else if let Some(fallback) = &fallback {
339 registry.language_for_name(fallback.as_ref()).right_future()
340 } else {
341 continue;
342 };
343 if let Ok(language) = language.await {
344 languages_by_name.insert(name, language);
345 }
346 }
347
348 for path in paths {
349 if let Ok(language) = registry
350 .load_language_for_file_path(Path::new(path.as_ref()))
351 .await
352 {
353 languages_by_path.insert(path, language);
354 }
355 }
356 }
357
358 for (range, event) in &events {
359 if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event
360 && let Some(data_url) = dest_url.strip_prefix("data:")
361 {
362 let Some((mime_info, data)) = data_url.split_once(',') else {
363 continue;
364 };
365 let Some((mime_type, encoding)) = mime_info.split_once(';') else {
366 continue;
367 };
368 let Some(format) = ImageFormat::from_mime_type(mime_type) else {
369 continue;
370 };
371 let is_base64 = encoding == "base64";
372 if is_base64
373 && let Some(bytes) = base64::prelude::BASE64_STANDARD
374 .decode(data)
375 .log_with_level(Level::Debug)
376 {
377 let image = Arc::new(Image::from_bytes(format, bytes));
378 images_by_source_offset.insert(range.start, image);
379 }
380 }
381 }
382
383 (
384 ParsedMarkdown {
385 source,
386 events: Arc::from(events),
387 languages_by_name,
388 languages_by_path,
389 },
390 images_by_source_offset,
391 )
392 });
393
394 cx.spawn(async move |this, cx| {
395 let (parsed, images_by_source_offset) = parsed.await;
396
397 this.update(cx, |this, cx| {
398 this.parsed_markdown = parsed;
399 this.images_by_source_offset = images_by_source_offset;
400 this.pending_parse.take();
401 if this.should_reparse {
402 this.parse(cx);
403 }
404 cx.refresh_windows();
405 })
406 .ok();
407 })
408 }
409}
410
411impl Focusable for Markdown {
412 fn focus_handle(&self, _cx: &App) -> FocusHandle {
413 self.focus_handle.clone()
414 }
415}
416
417#[derive(Debug, Default, Clone)]
418enum SelectMode {
419 #[default]
420 Character,
421 Word(Range<usize>),
422 Line(Range<usize>),
423 All,
424}
425
426#[derive(Clone, Default)]
427struct Selection {
428 start: usize,
429 end: usize,
430 reversed: bool,
431 pending: bool,
432 mode: SelectMode,
433}
434
435impl Selection {
436 fn set_head(&mut self, head: usize, rendered_text: &RenderedText) {
437 match &self.mode {
438 SelectMode::Character => {
439 if head < self.tail() {
440 if !self.reversed {
441 self.end = self.start;
442 self.reversed = true;
443 }
444 self.start = head;
445 } else {
446 if self.reversed {
447 self.start = self.end;
448 self.reversed = false;
449 }
450 self.end = head;
451 }
452 }
453 SelectMode::Word(original_range) | SelectMode::Line(original_range) => {
454 let head_range = if matches!(self.mode, SelectMode::Word(_)) {
455 rendered_text.surrounding_word_range(head)
456 } else {
457 rendered_text.surrounding_line_range(head)
458 };
459
460 if head < original_range.start {
461 self.start = head_range.start;
462 self.end = original_range.end;
463 self.reversed = true;
464 } else if head >= original_range.end {
465 self.start = original_range.start;
466 self.end = head_range.end;
467 self.reversed = false;
468 } else {
469 self.start = original_range.start;
470 self.end = original_range.end;
471 self.reversed = false;
472 }
473 }
474 SelectMode::All => {
475 self.start = 0;
476 self.end = rendered_text
477 .lines
478 .last()
479 .map(|line| line.source_end)
480 .unwrap_or(0);
481 self.reversed = false;
482 }
483 }
484 }
485
486 fn tail(&self) -> usize {
487 if self.reversed { self.end } else { self.start }
488 }
489}
490
491#[derive(Debug, Clone, Default)]
492pub struct ParsedMarkdown {
493 pub source: SharedString,
494 pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
495 pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
496 pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
497}
498
499impl ParsedMarkdown {
500 pub fn source(&self) -> &SharedString {
501 &self.source
502 }
503
504 pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
505 &self.events
506 }
507}
508
509pub struct MarkdownElement {
510 markdown: Entity<Markdown>,
511 style: MarkdownStyle,
512 code_block_renderer: CodeBlockRenderer,
513 on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
514}
515
516impl MarkdownElement {
517 pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
518 Self {
519 markdown,
520 style,
521 code_block_renderer: CodeBlockRenderer::Default {
522 copy_button: true,
523 copy_button_on_hover: false,
524 border: false,
525 },
526 on_url_click: None,
527 }
528 }
529
530 #[cfg(any(test, feature = "test-support"))]
531 pub fn rendered_text(
532 markdown: Entity<Markdown>,
533 cx: &mut gpui::VisualTestContext,
534 style: impl FnOnce(&Window, &App) -> MarkdownStyle,
535 ) -> String {
536 use gpui::size;
537
538 let (text, _) = cx.draw(
539 Default::default(),
540 size(px(600.0), px(600.0)),
541 |window, cx| Self::new(markdown, style(window, cx)),
542 );
543 text.text
544 .lines
545 .iter()
546 .map(|line| line.layout.wrapped_text())
547 .collect::<Vec<_>>()
548 .join("\n")
549 }
550
551 pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
552 self.code_block_renderer = variant;
553 self
554 }
555
556 pub fn on_url_click(
557 mut self,
558 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
559 ) -> Self {
560 self.on_url_click = Some(Box::new(handler));
561 self
562 }
563
564 fn paint_selection(
565 &self,
566 bounds: Bounds<Pixels>,
567 rendered_text: &RenderedText,
568 window: &mut Window,
569 cx: &mut App,
570 ) {
571 let selection = self.markdown.read(cx).selection.clone();
572 let selection_start = rendered_text.position_for_source_index(selection.start);
573 let selection_end = rendered_text.position_for_source_index(selection.end);
574 if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
575 selection_start.zip(selection_end)
576 {
577 if start_position.y == end_position.y {
578 window.paint_quad(quad(
579 Bounds::from_corners(
580 start_position,
581 point(end_position.x, end_position.y + end_line_height),
582 ),
583 Pixels::ZERO,
584 self.style.selection_background_color,
585 Edges::default(),
586 Hsla::transparent_black(),
587 BorderStyle::default(),
588 ));
589 } else {
590 window.paint_quad(quad(
591 Bounds::from_corners(
592 start_position,
593 point(bounds.right(), start_position.y + start_line_height),
594 ),
595 Pixels::ZERO,
596 self.style.selection_background_color,
597 Edges::default(),
598 Hsla::transparent_black(),
599 BorderStyle::default(),
600 ));
601
602 if end_position.y > start_position.y + start_line_height {
603 window.paint_quad(quad(
604 Bounds::from_corners(
605 point(bounds.left(), start_position.y + start_line_height),
606 point(bounds.right(), end_position.y),
607 ),
608 Pixels::ZERO,
609 self.style.selection_background_color,
610 Edges::default(),
611 Hsla::transparent_black(),
612 BorderStyle::default(),
613 ));
614 }
615
616 window.paint_quad(quad(
617 Bounds::from_corners(
618 point(bounds.left(), end_position.y),
619 point(end_position.x, end_position.y + end_line_height),
620 ),
621 Pixels::ZERO,
622 self.style.selection_background_color,
623 Edges::default(),
624 Hsla::transparent_black(),
625 BorderStyle::default(),
626 ));
627 }
628 }
629 }
630
631 fn paint_mouse_listeners(
632 &mut self,
633 hitbox: &Hitbox,
634 rendered_text: &RenderedText,
635 window: &mut Window,
636 cx: &mut App,
637 ) {
638 if self.style.prevent_mouse_interaction {
639 return;
640 }
641
642 let is_hovering_link = hitbox.is_hovered(window)
643 && !self.markdown.read(cx).selection.pending
644 && rendered_text
645 .link_for_position(window.mouse_position())
646 .is_some();
647
648 if !self.style.prevent_mouse_interaction {
649 if is_hovering_link {
650 window.set_cursor_style(CursorStyle::PointingHand, hitbox);
651 } else {
652 window.set_cursor_style(CursorStyle::IBeam, hitbox);
653 }
654 }
655
656 let on_open_url = self.on_url_click.take();
657
658 self.on_mouse_event(window, cx, {
659 let rendered_text = rendered_text.clone();
660 let hitbox = hitbox.clone();
661 move |markdown, event: &MouseDownEvent, phase, window, cx| {
662 if hitbox.is_hovered(window) {
663 if phase.bubble() {
664 if let Some(link) = rendered_text.link_for_position(event.position) {
665 markdown.pressed_link = Some(link.clone());
666 } else {
667 let source_index =
668 match rendered_text.source_index_for_position(event.position) {
669 Ok(ix) | Err(ix) => ix,
670 };
671 let (range, mode) = match event.click_count {
672 1 => {
673 let range = source_index..source_index;
674 (range, SelectMode::Character)
675 }
676 2 => {
677 let range = rendered_text.surrounding_word_range(source_index);
678 (range.clone(), SelectMode::Word(range))
679 }
680 3 => {
681 let range = rendered_text.surrounding_line_range(source_index);
682 (range.clone(), SelectMode::Line(range))
683 }
684 _ => {
685 let range = 0..rendered_text
686 .lines
687 .last()
688 .map(|line| line.source_end)
689 .unwrap_or(0);
690 (range, SelectMode::All)
691 }
692 };
693 markdown.selection = Selection {
694 start: range.start,
695 end: range.end,
696 reversed: false,
697 pending: true,
698 mode,
699 };
700 window.focus(&markdown.focus_handle);
701 }
702
703 window.prevent_default();
704 cx.notify();
705 }
706 } else if phase.capture() {
707 markdown.selection = Selection::default();
708 markdown.pressed_link = None;
709 cx.notify();
710 }
711 }
712 });
713 self.on_mouse_event(window, cx, {
714 let rendered_text = rendered_text.clone();
715 let hitbox = hitbox.clone();
716 let was_hovering_link = is_hovering_link;
717 move |markdown, event: &MouseMoveEvent, phase, window, cx| {
718 if phase.capture() {
719 return;
720 }
721
722 if markdown.selection.pending {
723 let source_index = match rendered_text.source_index_for_position(event.position)
724 {
725 Ok(ix) | Err(ix) => ix,
726 };
727 markdown.selection.set_head(source_index, &rendered_text);
728 markdown.autoscroll_request = Some(source_index);
729 cx.notify();
730 } else {
731 let is_hovering_link = hitbox.is_hovered(window)
732 && rendered_text.link_for_position(event.position).is_some();
733 if is_hovering_link != was_hovering_link {
734 cx.notify();
735 }
736 }
737 }
738 });
739 self.on_mouse_event(window, cx, {
740 let rendered_text = rendered_text.clone();
741 move |markdown, event: &MouseUpEvent, phase, window, cx| {
742 if phase.bubble() {
743 if let Some(pressed_link) = markdown.pressed_link.take()
744 && Some(&pressed_link) == rendered_text.link_for_position(event.position)
745 {
746 if let Some(open_url) = on_open_url.as_ref() {
747 open_url(pressed_link.destination_url, window, cx);
748 } else {
749 cx.open_url(&pressed_link.destination_url);
750 }
751 }
752 } else if markdown.selection.pending {
753 markdown.selection.pending = false;
754 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
755 {
756 let text = rendered_text
757 .text_for_range(markdown.selection.start..markdown.selection.end);
758 cx.write_to_primary(ClipboardItem::new_string(text))
759 }
760 cx.notify();
761 }
762 }
763 });
764 }
765
766 fn autoscroll(
767 &self,
768 rendered_text: &RenderedText,
769 window: &mut Window,
770 cx: &mut App,
771 ) -> Option<()> {
772 let autoscroll_index = self
773 .markdown
774 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
775 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
776
777 let text_style = self.style.base_text_style.clone();
778 let font_id = window.text_system().resolve_font(&text_style.font());
779 let font_size = text_style.font_size.to_pixels(window.rem_size());
780 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
781 window.request_autoscroll(Bounds::from_corners(
782 point(position.x - 3. * em_width, position.y - 3. * line_height),
783 point(position.x + 3. * em_width, position.y + 3. * line_height),
784 ));
785 Some(())
786 }
787
788 fn on_mouse_event<T: MouseEvent>(
789 &self,
790 window: &mut Window,
791 _cx: &mut App,
792 mut f: impl 'static
793 + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
794 ) {
795 window.on_mouse_event({
796 let markdown = self.markdown.downgrade();
797 move |event, phase, window, cx| {
798 markdown
799 .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
800 .log_err();
801 }
802 });
803 }
804}
805
806impl Styled for MarkdownElement {
807 fn style(&mut self) -> &mut StyleRefinement {
808 &mut self.style.container_style
809 }
810}
811
812impl Element for MarkdownElement {
813 type RequestLayoutState = RenderedMarkdown;
814 type PrepaintState = Hitbox;
815
816 fn id(&self) -> Option<ElementId> {
817 None
818 }
819
820 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
821 None
822 }
823
824 fn request_layout(
825 &mut self,
826 _id: Option<&GlobalElementId>,
827 _inspector_id: Option<&gpui::InspectorElementId>,
828 window: &mut Window,
829 cx: &mut App,
830 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
831 let mut builder = MarkdownElementBuilder::new(
832 &self.style.container_style,
833 self.style.base_text_style.clone(),
834 self.style.syntax.clone(),
835 );
836 let (parsed_markdown, images) = {
837 let markdown = self.markdown.read(cx);
838 (
839 markdown.parsed_markdown.clone(),
840 markdown.images_by_source_offset.clone(),
841 )
842 };
843 let markdown_end = if let Some(last) = parsed_markdown.events.last() {
844 last.0.end
845 } else {
846 0
847 };
848 let mut code_block_ids = HashSet::default();
849
850 let mut current_img_block_range: Option<Range<usize>> = None;
851 for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
852 // Skip alt text for images that rendered
853 if let Some(current_img_block_range) = ¤t_img_block_range
854 && current_img_block_range.end > range.end
855 {
856 continue;
857 }
858
859 match event {
860 MarkdownEvent::Start(tag) => {
861 match tag {
862 MarkdownTag::Image { .. } => {
863 if let Some(image) = images.get(&range.start) {
864 current_img_block_range = Some(range.clone());
865 builder.modify_current_div(|el| {
866 el.items_center()
867 .flex()
868 .flex_row()
869 .child(img(image.clone()))
870 });
871 }
872 }
873 MarkdownTag::Paragraph => {
874 builder.push_div(
875 div().when(!self.style.height_is_multiple_of_line_height, |el| {
876 el.mb_2().line_height(rems(1.3))
877 }),
878 range,
879 markdown_end,
880 );
881 }
882 MarkdownTag::Heading { level, .. } => {
883 let mut heading = div().mb_2();
884
885 heading = apply_heading_style(
886 heading,
887 *level,
888 self.style.heading_level_styles.as_ref(),
889 );
890
891 heading.style().refine(&self.style.heading);
892
893 let text_style = self.style.heading.text_style().clone();
894
895 builder.push_text_style(text_style);
896 builder.push_div(heading, range, markdown_end);
897 }
898 MarkdownTag::BlockQuote => {
899 builder.push_text_style(self.style.block_quote.clone());
900 builder.push_div(
901 div()
902 .pl_4()
903 .mb_2()
904 .border_l_4()
905 .border_color(self.style.block_quote_border_color),
906 range,
907 markdown_end,
908 );
909 }
910 MarkdownTag::CodeBlock { kind, .. } => {
911 let language = match kind {
912 CodeBlockKind::Fenced => None,
913 CodeBlockKind::FencedLang(language) => {
914 parsed_markdown.languages_by_name.get(language).cloned()
915 }
916 CodeBlockKind::FencedSrc(path_range) => parsed_markdown
917 .languages_by_path
918 .get(&path_range.path)
919 .cloned(),
920 _ => None,
921 };
922
923 let is_indented = matches!(kind, CodeBlockKind::Indented);
924 let scroll_handle = if self.style.code_block_overflow_x_scroll {
925 code_block_ids.insert(range.start);
926 Some(self.markdown.update(cx, |markdown, _| {
927 markdown.code_block_scroll_handle(range.start)
928 }))
929 } else {
930 None
931 };
932
933 match (&self.code_block_renderer, is_indented) {
934 (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
935 // This is a parent container that we can position the copy button inside.
936 let parent_container =
937 div().group("code_block").relative().w_full();
938
939 let mut parent_container: AnyDiv = if let Some(scroll_handle) =
940 scroll_handle.as_ref()
941 {
942 let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
943 .id(("markdown-code-block-scrollbar", range.start))
944 .tracked_scroll_handle(scroll_handle)
945 .with_track_along(
946 ScrollAxes::Horizontal,
947 cx.theme().colors().editor_background,
948 )
949 .notify_content();
950
951 parent_container
952 .rounded_lg()
953 .custom_scrollbars(scrollbars, window, cx)
954 .into()
955 } else {
956 parent_container.into()
957 };
958
959 if let CodeBlockRenderer::Default { border: true, .. } =
960 &self.code_block_renderer
961 {
962 parent_container = parent_container
963 .rounded_md()
964 .border_1()
965 .border_color(cx.theme().colors().border_variant);
966 }
967
968 parent_container.style().refine(&self.style.code_block);
969 builder.push_div(parent_container, range, markdown_end);
970
971 let code_block = div()
972 .id(("code-block", range.start))
973 .rounded_lg()
974 .map(|mut code_block| {
975 if let Some(scroll_handle) = scroll_handle.as_ref() {
976 code_block.style().restrict_scroll_to_axis =
977 Some(true);
978 code_block
979 .flex()
980 .overflow_x_scroll()
981 .track_scroll(scroll_handle)
982 } else {
983 code_block.w_full()
984 }
985 });
986
987 builder.push_text_style(self.style.code_block.text.to_owned());
988 builder.push_code_block(language);
989 builder.push_div(code_block, range, markdown_end);
990 }
991 (CodeBlockRenderer::Custom { .. }, _) => {}
992 }
993 }
994 MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
995 MarkdownTag::List(bullet_index) => {
996 builder.push_list(*bullet_index);
997 builder.push_div(div().pl_2p5(), range, markdown_end);
998 }
999 MarkdownTag::Item => {
1000 let bullet = if let Some((_, MarkdownEvent::TaskListMarker(checked))) =
1001 parsed_markdown.events.get(index.saturating_add(1))
1002 {
1003 let source = &parsed_markdown.source()[range.clone()];
1004
1005 Checkbox::new(
1006 ElementId::Name(source.to_string().into()),
1007 if *checked {
1008 ToggleState::Selected
1009 } else {
1010 ToggleState::Unselected
1011 },
1012 )
1013 .fill()
1014 .visualization_only(true)
1015 .into_any_element()
1016 } else if let Some(bullet_index) = builder.next_bullet_index() {
1017 div().child(format!("{}.", bullet_index)).into_any_element()
1018 } else {
1019 div().child("•").into_any_element()
1020 };
1021 builder.push_div(
1022 div()
1023 .when(!self.style.height_is_multiple_of_line_height, |el| {
1024 el.mb_1().gap_1().line_height(rems(1.3))
1025 })
1026 .h_flex()
1027 .items_start()
1028 .child(bullet),
1029 range,
1030 markdown_end,
1031 );
1032 // Without `w_0`, text doesn't wrap to the width of the container.
1033 builder.push_div(div().flex_1().w_0(), range, markdown_end);
1034 }
1035 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1036 font_style: Some(FontStyle::Italic),
1037 ..Default::default()
1038 }),
1039 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1040 font_weight: Some(FontWeight::BOLD),
1041 ..Default::default()
1042 }),
1043 MarkdownTag::Strikethrough => {
1044 builder.push_text_style(TextStyleRefinement {
1045 strikethrough: Some(StrikethroughStyle {
1046 thickness: px(1.),
1047 color: None,
1048 }),
1049 ..Default::default()
1050 })
1051 }
1052 MarkdownTag::Link { dest_url, .. } => {
1053 if builder.code_block_stack.is_empty() {
1054 builder.push_link(dest_url.clone(), range.clone());
1055 let style = self
1056 .style
1057 .link_callback
1058 .as_ref()
1059 .and_then(|callback| callback(dest_url, cx))
1060 .unwrap_or_else(|| self.style.link.clone());
1061 builder.push_text_style(style)
1062 }
1063 }
1064 MarkdownTag::MetadataBlock(_) => {}
1065 MarkdownTag::Table(alignments) => {
1066 builder.table_alignments = alignments.clone();
1067 builder.table_row_index = 0;
1068 builder.in_table_head = false;
1069
1070 let column_count = alignments.len();
1071 builder.push_div(
1072 div()
1073 .id(("table", range.start))
1074 .grid()
1075 .grid_cols(column_count as u16)
1076 .when(self.style.table_columns_min_size, |this| {
1077 this.grid_cols_min_content(column_count as u16)
1078 })
1079 .when(!self.style.table_columns_min_size, |this| {
1080 this.grid_cols(column_count as u16)
1081 })
1082 .size_full()
1083 .mb_2()
1084 .border_1()
1085 .border_color(cx.theme().colors().border)
1086 .rounded_sm()
1087 .overflow_hidden(),
1088 range,
1089 markdown_end,
1090 );
1091 }
1092 MarkdownTag::TableHead => {
1093 builder.in_table_head = true;
1094 builder.push_text_style(TextStyleRefinement {
1095 font_weight: Some(FontWeight::SEMIBOLD),
1096 ..Default::default()
1097 });
1098 }
1099 MarkdownTag::TableRow => {}
1100 MarkdownTag::TableCell => {
1101 let is_header = builder.in_table_head;
1102 let row_index = builder.table_row_index;
1103
1104 builder.push_div(
1105 div()
1106 .min_w_0()
1107 .border(px(0.5))
1108 .border_color(cx.theme().colors().border)
1109 .px_1()
1110 .py_0p5()
1111 .when(is_header, |this| {
1112 this.bg(cx.theme().colors().title_bar_background)
1113 })
1114 .when(!is_header && row_index % 2 == 1, |this| {
1115 this.bg(cx.theme().colors().panel_background)
1116 }),
1117 range,
1118 markdown_end,
1119 );
1120 }
1121 _ => log::debug!("unsupported markdown tag {:?}", tag),
1122 }
1123 }
1124 MarkdownEvent::End(tag) => match tag {
1125 MarkdownTagEnd::Image => {
1126 current_img_block_range.take();
1127 }
1128 MarkdownTagEnd::Paragraph => {
1129 builder.pop_div();
1130 }
1131 MarkdownTagEnd::Heading(_) => {
1132 builder.pop_div();
1133 builder.pop_text_style()
1134 }
1135 MarkdownTagEnd::BlockQuote(_kind) => {
1136 builder.pop_text_style();
1137 builder.pop_div()
1138 }
1139 MarkdownTagEnd::CodeBlock => {
1140 builder.trim_trailing_newline();
1141
1142 builder.pop_div();
1143 builder.pop_code_block();
1144 builder.pop_text_style();
1145
1146 if let CodeBlockRenderer::Default {
1147 copy_button: true, ..
1148 } = &self.code_block_renderer
1149 {
1150 builder.modify_current_div(|el| {
1151 let content_range = parser::extract_code_block_content_range(
1152 &parsed_markdown.source()[range.clone()],
1153 );
1154 let content_range = content_range.start + range.start
1155 ..content_range.end + range.start;
1156
1157 let code = parsed_markdown.source()[content_range].to_string();
1158 let codeblock = render_copy_code_block_button(
1159 range.end,
1160 code,
1161 self.markdown.clone(),
1162 cx,
1163 );
1164 el.child(
1165 h_flex()
1166 .w_4()
1167 .absolute()
1168 .top_1p5()
1169 .right_1p5()
1170 .justify_end()
1171 .child(codeblock),
1172 )
1173 });
1174 }
1175
1176 if let CodeBlockRenderer::Default {
1177 copy_button_on_hover: true,
1178 ..
1179 } = &self.code_block_renderer
1180 {
1181 builder.modify_current_div(|el| {
1182 let content_range = parser::extract_code_block_content_range(
1183 &parsed_markdown.source()[range.clone()],
1184 );
1185 let content_range = content_range.start + range.start
1186 ..content_range.end + range.start;
1187
1188 let code = parsed_markdown.source()[content_range].to_string();
1189 let codeblock = render_copy_code_block_button(
1190 range.end,
1191 code,
1192 self.markdown.clone(),
1193 cx,
1194 );
1195 el.child(
1196 h_flex()
1197 .w_4()
1198 .absolute()
1199 .top_0()
1200 .right_0()
1201 .justify_end()
1202 .visible_on_hover("code_block")
1203 .child(codeblock),
1204 )
1205 });
1206 }
1207
1208 // Pop the parent container.
1209 builder.pop_div();
1210 }
1211 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1212 MarkdownTagEnd::List(_) => {
1213 builder.pop_list();
1214 builder.pop_div();
1215 }
1216 MarkdownTagEnd::Item => {
1217 builder.pop_div();
1218 builder.pop_div();
1219 }
1220 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1221 MarkdownTagEnd::Strong => builder.pop_text_style(),
1222 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1223 MarkdownTagEnd::Link => {
1224 if builder.code_block_stack.is_empty() {
1225 builder.pop_text_style()
1226 }
1227 }
1228 MarkdownTagEnd::Table => {
1229 builder.pop_div();
1230 builder.table_alignments.clear();
1231 builder.in_table_head = false;
1232 builder.table_row_index = 0;
1233 }
1234 MarkdownTagEnd::TableHead => {
1235 builder.pop_text_style();
1236 builder.in_table_head = false;
1237 }
1238 MarkdownTagEnd::TableRow => {
1239 builder.table_row_index += 1;
1240 }
1241 MarkdownTagEnd::TableCell => {
1242 builder.pop_div();
1243 }
1244 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1245 },
1246 MarkdownEvent::Text => {
1247 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1248 }
1249 MarkdownEvent::SubstitutedText(text) => {
1250 builder.push_text(text, range.clone());
1251 }
1252 MarkdownEvent::Code => {
1253 builder.push_text_style(self.style.inline_code.clone());
1254 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1255 builder.pop_text_style();
1256 }
1257 MarkdownEvent::Html => {
1258 let html = &parsed_markdown.source[range.clone()];
1259 if html.starts_with("<!--") {
1260 builder.html_comment = true;
1261 }
1262 if html.trim_end().ends_with("-->") {
1263 builder.html_comment = false;
1264 continue;
1265 }
1266 if builder.html_comment {
1267 continue;
1268 }
1269 builder.push_text(html, range.clone());
1270 }
1271 MarkdownEvent::InlineHtml => {
1272 let html = &parsed_markdown.source[range.clone()];
1273 if html.starts_with("<code>") {
1274 builder.push_text_style(self.style.inline_code.clone());
1275 continue;
1276 }
1277 if html.trim_end().starts_with("</code>") {
1278 builder.pop_text_style();
1279 continue;
1280 }
1281 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1282 }
1283 MarkdownEvent::Rule => {
1284 builder.push_div(
1285 div()
1286 .border_b_1()
1287 .my_2()
1288 .border_color(self.style.rule_color),
1289 range,
1290 markdown_end,
1291 );
1292 builder.pop_div()
1293 }
1294 MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
1295 MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
1296 MarkdownEvent::TaskListMarker(_) => {
1297 // handled inside the `MarkdownTag::Item` case
1298 }
1299 _ => log::debug!("unsupported markdown event {:?}", event),
1300 }
1301 }
1302 if self.style.code_block_overflow_x_scroll {
1303 let code_block_ids = code_block_ids;
1304 self.markdown.update(cx, move |markdown, _| {
1305 markdown.retain_code_block_scroll_handles(&code_block_ids);
1306 });
1307 } else {
1308 self.markdown
1309 .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1310 }
1311 let mut rendered_markdown = builder.build();
1312 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1313 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1314 (layout_id, rendered_markdown)
1315 }
1316
1317 fn prepaint(
1318 &mut self,
1319 _id: Option<&GlobalElementId>,
1320 _inspector_id: Option<&gpui::InspectorElementId>,
1321 bounds: Bounds<Pixels>,
1322 rendered_markdown: &mut Self::RequestLayoutState,
1323 window: &mut Window,
1324 cx: &mut App,
1325 ) -> Self::PrepaintState {
1326 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1327 window.set_focus_handle(&focus_handle, cx);
1328 window.set_view_id(self.markdown.entity_id());
1329
1330 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1331 rendered_markdown.element.prepaint(window, cx);
1332 self.autoscroll(&rendered_markdown.text, window, cx);
1333 hitbox
1334 }
1335
1336 fn paint(
1337 &mut self,
1338 _id: Option<&GlobalElementId>,
1339 _inspector_id: Option<&gpui::InspectorElementId>,
1340 bounds: Bounds<Pixels>,
1341 rendered_markdown: &mut Self::RequestLayoutState,
1342 hitbox: &mut Self::PrepaintState,
1343 window: &mut Window,
1344 cx: &mut App,
1345 ) {
1346 let mut context = KeyContext::default();
1347 context.add("Markdown");
1348 window.set_key_context(context);
1349 window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1350 let entity = self.markdown.clone();
1351 let text = rendered_markdown.text.clone();
1352 move |_, phase, window, cx| {
1353 let text = text.clone();
1354 if phase == DispatchPhase::Bubble {
1355 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1356 }
1357 }
1358 });
1359
1360 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1361 rendered_markdown.element.paint(window, cx);
1362 self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1363 }
1364}
1365
1366fn apply_heading_style(
1367 mut heading: Div,
1368 level: pulldown_cmark::HeadingLevel,
1369 custom_styles: Option<&HeadingLevelStyles>,
1370) -> Div {
1371 heading = match level {
1372 pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
1373 pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
1374 pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
1375 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
1376 pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
1377 pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
1378 };
1379
1380 if let Some(styles) = custom_styles {
1381 let style_opt = match level {
1382 pulldown_cmark::HeadingLevel::H1 => &styles.h1,
1383 pulldown_cmark::HeadingLevel::H2 => &styles.h2,
1384 pulldown_cmark::HeadingLevel::H3 => &styles.h3,
1385 pulldown_cmark::HeadingLevel::H4 => &styles.h4,
1386 pulldown_cmark::HeadingLevel::H5 => &styles.h5,
1387 pulldown_cmark::HeadingLevel::H6 => &styles.h6,
1388 };
1389
1390 if let Some(style) = style_opt {
1391 heading.style().text = style.clone();
1392 }
1393 }
1394
1395 heading
1396}
1397
1398fn render_copy_code_block_button(
1399 id: usize,
1400 code: String,
1401 markdown: Entity<Markdown>,
1402 cx: &App,
1403) -> impl IntoElement {
1404 let id = ElementId::named_usize("copy-markdown-code", id);
1405 let was_copied = markdown.read(cx).copied_code_blocks.contains(&id);
1406 IconButton::new(
1407 id.clone(),
1408 if was_copied {
1409 IconName::Check
1410 } else {
1411 IconName::Copy
1412 },
1413 )
1414 .icon_color(Color::Muted)
1415 .icon_size(IconSize::Small)
1416 .style(ButtonStyle::Filled)
1417 .shape(ui::IconButtonShape::Square)
1418 .tooltip(Tooltip::text("Copy"))
1419 .on_click({
1420 let markdown = markdown;
1421 move |_event, _window, cx| {
1422 let id = id.clone();
1423 markdown.update(cx, |this, cx| {
1424 this.copied_code_blocks.insert(id.clone());
1425
1426 cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
1427
1428 cx.spawn(async move |this, cx| {
1429 cx.background_executor().timer(Duration::from_secs(2)).await;
1430
1431 cx.update(|cx| {
1432 this.update(cx, |this, cx| {
1433 this.copied_code_blocks.remove(&id);
1434 cx.notify();
1435 })
1436 })
1437 .ok();
1438 })
1439 .detach();
1440 });
1441 }
1442 })
1443}
1444
1445impl IntoElement for MarkdownElement {
1446 type Element = Self;
1447
1448 fn into_element(self) -> Self::Element {
1449 self
1450 }
1451}
1452
1453pub enum AnyDiv {
1454 Div(Div),
1455 Stateful(Stateful<Div>),
1456}
1457
1458impl AnyDiv {
1459 fn into_any_element(self) -> AnyElement {
1460 match self {
1461 Self::Div(div) => div.into_any_element(),
1462 Self::Stateful(div) => div.into_any_element(),
1463 }
1464 }
1465}
1466
1467impl From<Div> for AnyDiv {
1468 fn from(value: Div) -> Self {
1469 Self::Div(value)
1470 }
1471}
1472
1473impl From<Stateful<Div>> for AnyDiv {
1474 fn from(value: Stateful<Div>) -> Self {
1475 Self::Stateful(value)
1476 }
1477}
1478
1479impl Styled for AnyDiv {
1480 fn style(&mut self) -> &mut StyleRefinement {
1481 match self {
1482 Self::Div(div) => div.style(),
1483 Self::Stateful(div) => div.style(),
1484 }
1485 }
1486}
1487
1488impl ParentElement for AnyDiv {
1489 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1490 match self {
1491 Self::Div(div) => div.extend(elements),
1492 Self::Stateful(div) => div.extend(elements),
1493 }
1494 }
1495}
1496
1497struct MarkdownElementBuilder {
1498 div_stack: Vec<AnyDiv>,
1499 rendered_lines: Vec<RenderedLine>,
1500 pending_line: PendingLine,
1501 rendered_links: Vec<RenderedLink>,
1502 current_source_index: usize,
1503 html_comment: bool,
1504 base_text_style: TextStyle,
1505 text_style_stack: Vec<TextStyleRefinement>,
1506 code_block_stack: Vec<Option<Arc<Language>>>,
1507 list_stack: Vec<ListStackEntry>,
1508 table_alignments: Vec<Alignment>,
1509 in_table_head: bool,
1510 table_row_index: usize,
1511 syntax_theme: Arc<SyntaxTheme>,
1512}
1513
1514#[derive(Default)]
1515struct PendingLine {
1516 text: String,
1517 runs: Vec<TextRun>,
1518 source_mappings: Vec<SourceMapping>,
1519}
1520
1521struct ListStackEntry {
1522 bullet_index: Option<u64>,
1523}
1524
1525impl MarkdownElementBuilder {
1526 fn new(
1527 container_style: &StyleRefinement,
1528 base_text_style: TextStyle,
1529 syntax_theme: Arc<SyntaxTheme>,
1530 ) -> Self {
1531 Self {
1532 div_stack: vec![{
1533 let mut base_div = div();
1534 base_div.style().refine(container_style);
1535 base_div.debug_selector(|| "inner".into()).into()
1536 }],
1537 rendered_lines: Vec::new(),
1538 pending_line: PendingLine::default(),
1539 rendered_links: Vec::new(),
1540 current_source_index: 0,
1541 html_comment: false,
1542 base_text_style,
1543 text_style_stack: Vec::new(),
1544 code_block_stack: Vec::new(),
1545 list_stack: Vec::new(),
1546 table_alignments: Vec::new(),
1547 in_table_head: false,
1548 table_row_index: 0,
1549 syntax_theme,
1550 }
1551 }
1552
1553 fn push_text_style(&mut self, style: TextStyleRefinement) {
1554 self.text_style_stack.push(style);
1555 }
1556
1557 fn text_style(&self) -> TextStyle {
1558 let mut style = self.base_text_style.clone();
1559 for refinement in &self.text_style_stack {
1560 style.refine(refinement);
1561 }
1562 style
1563 }
1564
1565 fn pop_text_style(&mut self) {
1566 self.text_style_stack.pop();
1567 }
1568
1569 fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
1570 let mut div = div.into();
1571 self.flush_text();
1572
1573 if range.start == 0 {
1574 // Remove the top margin on the first element.
1575 div.style().refine(&StyleRefinement {
1576 margin: gpui::EdgesRefinement {
1577 top: Some(Length::Definite(px(0.).into())),
1578 left: None,
1579 right: None,
1580 bottom: None,
1581 },
1582 ..Default::default()
1583 });
1584 }
1585
1586 if range.end == markdown_end {
1587 div.style().refine(&StyleRefinement {
1588 margin: gpui::EdgesRefinement {
1589 top: None,
1590 left: None,
1591 right: None,
1592 bottom: Some(Length::Definite(rems(0.).into())),
1593 },
1594 ..Default::default()
1595 });
1596 }
1597
1598 self.div_stack.push(div);
1599 }
1600
1601 fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1602 self.flush_text();
1603 if let Some(div) = self.div_stack.pop() {
1604 self.div_stack.push(f(div));
1605 }
1606 }
1607
1608 fn pop_div(&mut self) {
1609 self.flush_text();
1610 let div = self.div_stack.pop().unwrap().into_any_element();
1611 self.div_stack.last_mut().unwrap().extend(iter::once(div));
1612 }
1613
1614 fn push_list(&mut self, bullet_index: Option<u64>) {
1615 self.list_stack.push(ListStackEntry { bullet_index });
1616 }
1617
1618 fn next_bullet_index(&mut self) -> Option<u64> {
1619 self.list_stack.last_mut().and_then(|entry| {
1620 let item_index = entry.bullet_index.as_mut()?;
1621 *item_index += 1;
1622 Some(*item_index - 1)
1623 })
1624 }
1625
1626 fn pop_list(&mut self) {
1627 self.list_stack.pop();
1628 }
1629
1630 fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1631 self.code_block_stack.push(language);
1632 }
1633
1634 fn pop_code_block(&mut self) {
1635 self.code_block_stack.pop();
1636 }
1637
1638 fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1639 self.rendered_links.push(RenderedLink {
1640 source_range,
1641 destination_url,
1642 });
1643 }
1644
1645 fn push_text(&mut self, text: &str, source_range: Range<usize>) {
1646 self.pending_line.source_mappings.push(SourceMapping {
1647 rendered_index: self.pending_line.text.len(),
1648 source_index: source_range.start,
1649 });
1650 self.pending_line.text.push_str(text);
1651 self.current_source_index = source_range.end;
1652
1653 if let Some(Some(language)) = self.code_block_stack.last() {
1654 let mut offset = 0;
1655 for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1656 if range.start > offset {
1657 self.pending_line
1658 .runs
1659 .push(self.text_style().to_run(range.start - offset));
1660 }
1661
1662 let mut run_style = self.text_style();
1663 if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1664 run_style = run_style.highlight(highlight);
1665 }
1666 self.pending_line.runs.push(run_style.to_run(range.len()));
1667 offset = range.end;
1668 }
1669
1670 if offset < text.len() {
1671 self.pending_line
1672 .runs
1673 .push(self.text_style().to_run(text.len() - offset));
1674 }
1675 } else {
1676 self.pending_line
1677 .runs
1678 .push(self.text_style().to_run(text.len()));
1679 }
1680 }
1681
1682 fn trim_trailing_newline(&mut self) {
1683 if self.pending_line.text.ends_with('\n') {
1684 self.pending_line
1685 .text
1686 .truncate(self.pending_line.text.len() - 1);
1687 self.pending_line.runs.last_mut().unwrap().len -= 1;
1688 self.current_source_index -= 1;
1689 }
1690 }
1691
1692 fn flush_text(&mut self) {
1693 let line = mem::take(&mut self.pending_line);
1694 if line.text.is_empty() {
1695 return;
1696 }
1697
1698 let text = StyledText::new(line.text).with_runs(line.runs);
1699 self.rendered_lines.push(RenderedLine {
1700 layout: text.layout().clone(),
1701 source_mappings: line.source_mappings,
1702 source_end: self.current_source_index,
1703 });
1704 self.div_stack.last_mut().unwrap().extend([text.into_any()]);
1705 }
1706
1707 fn build(mut self) -> RenderedMarkdown {
1708 debug_assert_eq!(self.div_stack.len(), 1);
1709 self.flush_text();
1710 RenderedMarkdown {
1711 element: self.div_stack.pop().unwrap().into_any_element(),
1712 text: RenderedText {
1713 lines: self.rendered_lines.into(),
1714 links: self.rendered_links.into(),
1715 },
1716 }
1717 }
1718}
1719
1720struct RenderedLine {
1721 layout: TextLayout,
1722 source_mappings: Vec<SourceMapping>,
1723 source_end: usize,
1724}
1725
1726impl RenderedLine {
1727 fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
1728 if source_index >= self.source_end {
1729 return self.layout.len();
1730 }
1731
1732 let mapping = match self
1733 .source_mappings
1734 .binary_search_by_key(&source_index, |probe| probe.source_index)
1735 {
1736 Ok(ix) => &self.source_mappings[ix],
1737 Err(ix) => &self.source_mappings[ix - 1],
1738 };
1739 mapping.rendered_index + (source_index - mapping.source_index)
1740 }
1741
1742 fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
1743 if rendered_index >= self.layout.len() {
1744 return self.source_end;
1745 }
1746
1747 let mapping = match self
1748 .source_mappings
1749 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
1750 {
1751 Ok(ix) => &self.source_mappings[ix],
1752 Err(ix) => &self.source_mappings[ix - 1],
1753 };
1754 mapping.source_index + (rendered_index - mapping.rendered_index)
1755 }
1756
1757 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1758 let line_rendered_index;
1759 let out_of_bounds;
1760 match self.layout.index_for_position(position) {
1761 Ok(ix) => {
1762 line_rendered_index = ix;
1763 out_of_bounds = false;
1764 }
1765 Err(ix) => {
1766 line_rendered_index = ix;
1767 out_of_bounds = true;
1768 }
1769 };
1770 let source_index = self.source_index_for_rendered_index(line_rendered_index);
1771 if out_of_bounds {
1772 Err(source_index)
1773 } else {
1774 Ok(source_index)
1775 }
1776 }
1777}
1778
1779#[derive(Copy, Clone, Debug, Default)]
1780struct SourceMapping {
1781 rendered_index: usize,
1782 source_index: usize,
1783}
1784
1785pub struct RenderedMarkdown {
1786 element: AnyElement,
1787 text: RenderedText,
1788}
1789
1790#[derive(Clone)]
1791struct RenderedText {
1792 lines: Rc<[RenderedLine]>,
1793 links: Rc<[RenderedLink]>,
1794}
1795
1796#[derive(Debug, Clone, Eq, PartialEq)]
1797struct RenderedLink {
1798 source_range: Range<usize>,
1799 destination_url: SharedString,
1800}
1801
1802impl RenderedText {
1803 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1804 let mut lines = self.lines.iter().peekable();
1805
1806 while let Some(line) = lines.next() {
1807 let line_bounds = line.layout.bounds();
1808 if position.y > line_bounds.bottom() {
1809 if let Some(next_line) = lines.peek()
1810 && position.y < next_line.layout.bounds().top()
1811 {
1812 return Err(line.source_end);
1813 }
1814
1815 continue;
1816 }
1817
1818 return line.source_index_for_position(position);
1819 }
1820
1821 Err(self.lines.last().map_or(0, |line| line.source_end))
1822 }
1823
1824 fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
1825 for line in self.lines.iter() {
1826 let line_source_start = line.source_mappings.first().unwrap().source_index;
1827 if source_index < line_source_start {
1828 break;
1829 } else if source_index > line.source_end {
1830 continue;
1831 } else {
1832 let line_height = line.layout.line_height();
1833 let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
1834 let position = line.layout.position_for_index(rendered_index_within_line)?;
1835 return Some((position, line_height));
1836 }
1837 }
1838 None
1839 }
1840
1841 fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
1842 for line in self.lines.iter() {
1843 if source_index > line.source_end {
1844 continue;
1845 }
1846
1847 let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
1848 let rendered_index_in_line =
1849 line.rendered_index_for_source_index(source_index) - line_rendered_start;
1850 let text = line.layout.text();
1851 let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
1852 idx + ' '.len_utf8()
1853 } else {
1854 0
1855 };
1856 let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
1857 rendered_index_in_line + idx
1858 } else {
1859 text.len()
1860 };
1861
1862 return line.source_index_for_rendered_index(line_rendered_start + previous_space)
1863 ..line.source_index_for_rendered_index(line_rendered_start + next_space);
1864 }
1865
1866 source_index..source_index
1867 }
1868
1869 fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
1870 for line in self.lines.iter() {
1871 if source_index > line.source_end {
1872 continue;
1873 }
1874 let line_source_start = line.source_mappings.first().unwrap().source_index;
1875 return line_source_start..line.source_end;
1876 }
1877
1878 source_index..source_index
1879 }
1880
1881 fn text_for_range(&self, range: Range<usize>) -> String {
1882 let mut ret = vec![];
1883
1884 for line in self.lines.iter() {
1885 if range.start > line.source_end {
1886 continue;
1887 }
1888 let line_source_start = line.source_mappings.first().unwrap().source_index;
1889 if range.end < line_source_start {
1890 break;
1891 }
1892
1893 let text = line.layout.text();
1894
1895 let start = if range.start < line_source_start {
1896 0
1897 } else {
1898 line.rendered_index_for_source_index(range.start)
1899 };
1900 let end = if range.end > line.source_end {
1901 line.rendered_index_for_source_index(line.source_end)
1902 } else {
1903 line.rendered_index_for_source_index(range.end)
1904 }
1905 .min(text.len());
1906
1907 ret.push(text[start..end].to_string());
1908 }
1909 ret.join("\n")
1910 }
1911
1912 fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1913 let source_index = self.source_index_for_position(position).ok()?;
1914 self.links
1915 .iter()
1916 .find(|link| link.source_range.contains(&source_index))
1917 }
1918}
1919
1920#[cfg(test)]
1921mod tests {
1922 use super::*;
1923 use gpui::{TestAppContext, size};
1924
1925 #[gpui::test]
1926 fn test_mappings(cx: &mut TestAppContext) {
1927 // Formatting.
1928 assert_mappings(
1929 &render_markdown("He*l*lo", cx),
1930 vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
1931 );
1932
1933 // Multiple lines.
1934 assert_mappings(
1935 &render_markdown("Hello\n\nWorld", cx),
1936 vec![
1937 vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
1938 vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
1939 ],
1940 );
1941
1942 // Multi-byte characters.
1943 assert_mappings(
1944 &render_markdown("αβγ\n\nδεζ", cx),
1945 vec![
1946 vec![(0, 0), (2, 2), (4, 4), (6, 6)],
1947 vec![(0, 8), (2, 10), (4, 12), (6, 14)],
1948 ],
1949 );
1950
1951 // Smart quotes.
1952 assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
1953 assert_mappings(
1954 &render_markdown("\"hey\"", cx),
1955 vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
1956 );
1957
1958 // HTML Comments are ignored
1959 assert_mappings(
1960 &render_markdown(
1961 "<!--\nrdoc-file=string.c\n- str.intern -> symbol\n- str.to_sym -> symbol\n-->\nReturns",
1962 cx,
1963 ),
1964 vec![vec![
1965 (0, 78),
1966 (1, 79),
1967 (2, 80),
1968 (3, 81),
1969 (4, 82),
1970 (5, 83),
1971 (6, 84),
1972 ]],
1973 );
1974 }
1975
1976 fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
1977 struct TestWindow;
1978
1979 impl Render for TestWindow {
1980 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1981 div()
1982 }
1983 }
1984
1985 let (_, cx) = cx.add_window_view(|_, _| TestWindow);
1986 let markdown = cx.new(|cx| Markdown::new(markdown.to_string().into(), None, None, cx));
1987 cx.run_until_parked();
1988 let (rendered, _) = cx.draw(
1989 Default::default(),
1990 size(px(600.0), px(600.0)),
1991 |_window, _cx| MarkdownElement::new(markdown, MarkdownStyle::default()),
1992 );
1993 rendered.text
1994 }
1995
1996 #[gpui::test]
1997 fn test_surrounding_word_range(cx: &mut TestAppContext) {
1998 let rendered = render_markdown("Hello world tesεζ", cx);
1999
2000 // Test word selection for "Hello"
2001 let word_range = rendered.surrounding_word_range(2); // Simulate click on 'l' in "Hello"
2002 let selected_text = rendered.text_for_range(word_range);
2003 assert_eq!(selected_text, "Hello");
2004
2005 // Test word selection for "world"
2006 let word_range = rendered.surrounding_word_range(7); // Simulate click on 'o' in "world"
2007 let selected_text = rendered.text_for_range(word_range);
2008 assert_eq!(selected_text, "world");
2009
2010 // Test word selection for "tesεζ"
2011 let word_range = rendered.surrounding_word_range(14); // Simulate click on 's' in "tesεζ"
2012 let selected_text = rendered.text_for_range(word_range);
2013 assert_eq!(selected_text, "tesεζ");
2014
2015 // Test word selection at word boundary (space)
2016 let word_range = rendered.surrounding_word_range(5); // Simulate click on space between "Hello" and "world", expect highlighting word to the left
2017 let selected_text = rendered.text_for_range(word_range);
2018 assert_eq!(selected_text, "Hello");
2019 }
2020
2021 #[gpui::test]
2022 fn test_surrounding_line_range(cx: &mut TestAppContext) {
2023 let rendered = render_markdown("First line\n\nSecond line\n\nThird lineεζ", cx);
2024
2025 // Test getting line range for first line
2026 let line_range = rendered.surrounding_line_range(5); // Simulate click somewhere in first line
2027 let selected_text = rendered.text_for_range(line_range);
2028 assert_eq!(selected_text, "First line");
2029
2030 // Test getting line range for second line
2031 let line_range = rendered.surrounding_line_range(13); // Simulate click at beginning in second line
2032 let selected_text = rendered.text_for_range(line_range);
2033 assert_eq!(selected_text, "Second line");
2034
2035 // Test getting line range for third line
2036 let line_range = rendered.surrounding_line_range(37); // Simulate click at end of third line with multi-byte chars
2037 let selected_text = rendered.text_for_range(line_range);
2038 assert_eq!(selected_text, "Third lineεζ");
2039 }
2040
2041 #[gpui::test]
2042 fn test_selection_head_movement(cx: &mut TestAppContext) {
2043 let rendered = render_markdown("Hello world test", cx);
2044
2045 let mut selection = Selection {
2046 start: 5,
2047 end: 5,
2048 reversed: false,
2049 pending: false,
2050 mode: SelectMode::Character,
2051 };
2052
2053 // Test forward selection
2054 selection.set_head(10, &rendered);
2055 assert_eq!(selection.start, 5);
2056 assert_eq!(selection.end, 10);
2057 assert!(!selection.reversed);
2058 assert_eq!(selection.tail(), 5);
2059
2060 // Test backward selection
2061 selection.set_head(2, &rendered);
2062 assert_eq!(selection.start, 2);
2063 assert_eq!(selection.end, 5);
2064 assert!(selection.reversed);
2065 assert_eq!(selection.tail(), 5);
2066
2067 // Test forward selection again from reversed state
2068 selection.set_head(15, &rendered);
2069 assert_eq!(selection.start, 5);
2070 assert_eq!(selection.end, 15);
2071 assert!(!selection.reversed);
2072 assert_eq!(selection.tail(), 5);
2073 }
2074
2075 #[gpui::test]
2076 fn test_word_selection_drag(cx: &mut TestAppContext) {
2077 let rendered = render_markdown("Hello world test", cx);
2078
2079 // Start with a simulated double-click on "world" (index 6-10)
2080 let word_range = rendered.surrounding_word_range(7); // Click on 'o' in "world"
2081 let mut selection = Selection {
2082 start: word_range.start,
2083 end: word_range.end,
2084 reversed: false,
2085 pending: true,
2086 mode: SelectMode::Word(word_range),
2087 };
2088
2089 // Drag forward to "test" - should expand selection to include "test"
2090 selection.set_head(13, &rendered); // Index in "test"
2091 assert_eq!(selection.start, 6); // Start of "world"
2092 assert_eq!(selection.end, 16); // End of "test"
2093 assert!(!selection.reversed);
2094 let selected_text = rendered.text_for_range(selection.start..selection.end);
2095 assert_eq!(selected_text, "world test");
2096
2097 // Drag backward to "Hello" - should expand selection to include "Hello"
2098 selection.set_head(2, &rendered); // Index in "Hello"
2099 assert_eq!(selection.start, 0); // Start of "Hello"
2100 assert_eq!(selection.end, 11); // End of "world" (original selection)
2101 assert!(selection.reversed);
2102 let selected_text = rendered.text_for_range(selection.start..selection.end);
2103 assert_eq!(selected_text, "Hello world");
2104
2105 // Drag back within original word - should revert to original selection
2106 selection.set_head(8, &rendered); // Back within "world"
2107 assert_eq!(selection.start, 6); // Start of "world"
2108 assert_eq!(selection.end, 11); // End of "world"
2109 assert!(!selection.reversed);
2110 let selected_text = rendered.text_for_range(selection.start..selection.end);
2111 assert_eq!(selected_text, "world");
2112 }
2113
2114 #[gpui::test]
2115 fn test_selection_with_markdown_formatting(cx: &mut TestAppContext) {
2116 let rendered = render_markdown(
2117 "This is **bold** text, this is *italic* text, use `code` here",
2118 cx,
2119 );
2120 let word_range = rendered.surrounding_word_range(10); // Inside "bold"
2121 let selected_text = rendered.text_for_range(word_range);
2122 assert_eq!(selected_text, "bold");
2123
2124 let word_range = rendered.surrounding_word_range(32); // Inside "italic"
2125 let selected_text = rendered.text_for_range(word_range);
2126 assert_eq!(selected_text, "italic");
2127
2128 let word_range = rendered.surrounding_word_range(51); // Inside "code"
2129 let selected_text = rendered.text_for_range(word_range);
2130 assert_eq!(selected_text, "code");
2131 }
2132
2133 #[gpui::test]
2134 fn test_all_selection(cx: &mut TestAppContext) {
2135 let rendered = render_markdown("Hello world\n\nThis is a test\n\nwith multiple lines", cx);
2136
2137 let total_length = rendered
2138 .lines
2139 .last()
2140 .map(|line| line.source_end)
2141 .unwrap_or(0);
2142
2143 let mut selection = Selection {
2144 start: 0,
2145 end: total_length,
2146 reversed: false,
2147 pending: true,
2148 mode: SelectMode::All,
2149 };
2150
2151 selection.set_head(5, &rendered); // Try to set head in middle
2152 assert_eq!(selection.start, 0);
2153 assert_eq!(selection.end, total_length);
2154 assert!(!selection.reversed);
2155
2156 selection.set_head(25, &rendered); // Try to set head near end
2157 assert_eq!(selection.start, 0);
2158 assert_eq!(selection.end, total_length);
2159 assert!(!selection.reversed);
2160
2161 let selected_text = rendered.text_for_range(selection.start..selection.end);
2162 assert_eq!(
2163 selected_text,
2164 "Hello world\nThis is a test\nwith multiple lines"
2165 );
2166 }
2167
2168 #[test]
2169 fn test_escape() {
2170 assert_eq!(Markdown::escape("hello `world`"), "hello \\`world\\`");
2171 assert_eq!(
2172 Markdown::escape("hello\n cool world"),
2173 "hello\n\ncool world"
2174 );
2175 }
2176
2177 #[track_caller]
2178 fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
2179 assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
2180 for (line_ix, line_mappings) in expected.into_iter().enumerate() {
2181 let line = &rendered.lines[line_ix];
2182
2183 assert!(
2184 line.source_mappings.windows(2).all(|mappings| {
2185 mappings[0].source_index < mappings[1].source_index
2186 && mappings[0].rendered_index < mappings[1].rendered_index
2187 }),
2188 "line {} has duplicate mappings: {:?}",
2189 line_ix,
2190 line.source_mappings
2191 );
2192
2193 for (rendered_ix, source_ix) in line_mappings {
2194 assert_eq!(
2195 line.source_index_for_rendered_index(rendered_ix),
2196 source_ix,
2197 "line {}, rendered_ix {}",
2198 line_ix,
2199 rendered_ix
2200 );
2201
2202 assert_eq!(
2203 line.rendered_index_for_source_index(source_ix),
2204 rendered_ix,
2205 "line {}, source_ix {}",
2206 line_ix,
2207 source_ix
2208 );
2209 }
2210 }
2211 }
2212}