1pub mod html;
2mod mermaid;
3pub mod parser;
4mod path_range;
5
6use base64::Engine as _;
7use futures::FutureExt as _;
8use gpui::EdgesRefinement;
9use gpui::HitboxBehavior;
10use gpui::UnderlineStyle;
11use language::LanguageName;
12
13use log::Level;
14use mermaid::{
15 MermaidState, ParsedMarkdownMermaidDiagram, extract_mermaid_diagrams, render_mermaid_diagram,
16};
17pub use path_range::{LineCol, PathWithRange};
18use settings::Settings as _;
19use theme_settings::ThemeSettings;
20use ui::Checkbox;
21use ui::CopyButton;
22
23use std::borrow::Cow;
24use std::collections::BTreeMap;
25use std::iter;
26use std::mem;
27use std::ops::Range;
28use std::path::Path;
29use std::rc::Rc;
30use std::sync::Arc;
31use std::time::Duration;
32
33use collections::{HashMap, HashSet};
34use gpui::{
35 AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
36 FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
37 ImageFormat, ImageSource, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent,
38 MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle,
39 StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement,
40 actions, img, point, quad,
41};
42use language::{CharClassifier, Language, LanguageRegistry, Rope};
43use parser::CodeBlockMetadata;
44use parser::{
45 MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown_with_options,
46};
47use pulldown_cmark::Alignment;
48use sum_tree::TreeMap;
49use theme::SyntaxTheme;
50use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*};
51use util::ResultExt;
52
53use crate::parser::CodeBlockKind;
54
55/// A callback function that can be used to customize the style of links based on the destination URL.
56/// If the callback returns `None`, the default link style will be used.
57type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
58type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;
59type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;
60/// Defines custom style refinements for each heading level (H1-H6)
61#[derive(Clone, Default)]
62pub struct HeadingLevelStyles {
63 pub h1: Option<TextStyleRefinement>,
64 pub h2: Option<TextStyleRefinement>,
65 pub h3: Option<TextStyleRefinement>,
66 pub h4: Option<TextStyleRefinement>,
67 pub h5: Option<TextStyleRefinement>,
68 pub h6: Option<TextStyleRefinement>,
69}
70
71#[derive(Clone)]
72pub struct MarkdownStyle {
73 pub base_text_style: TextStyle,
74 pub container_style: StyleRefinement,
75 pub code_block: StyleRefinement,
76 pub code_block_overflow_x_scroll: bool,
77 pub inline_code: TextStyleRefinement,
78 pub block_quote: TextStyleRefinement,
79 pub link: TextStyleRefinement,
80 pub link_callback: Option<LinkStyleCallback>,
81 pub rule_color: Hsla,
82 pub block_quote_border_color: Hsla,
83 pub syntax: Arc<SyntaxTheme>,
84 pub selection_background_color: Hsla,
85 pub heading: StyleRefinement,
86 pub heading_level_styles: Option<HeadingLevelStyles>,
87 pub height_is_multiple_of_line_height: bool,
88 pub prevent_mouse_interaction: bool,
89 pub table_columns_min_size: bool,
90}
91
92impl Default for MarkdownStyle {
93 fn default() -> Self {
94 Self {
95 base_text_style: Default::default(),
96 container_style: Default::default(),
97 code_block: Default::default(),
98 code_block_overflow_x_scroll: false,
99 inline_code: Default::default(),
100 block_quote: Default::default(),
101 link: Default::default(),
102 link_callback: None,
103 rule_color: Default::default(),
104 block_quote_border_color: Default::default(),
105 syntax: Arc::new(SyntaxTheme::default()),
106 selection_background_color: Default::default(),
107 heading: Default::default(),
108 heading_level_styles: None,
109 height_is_multiple_of_line_height: false,
110 prevent_mouse_interaction: false,
111 table_columns_min_size: false,
112 }
113 }
114}
115
116pub enum MarkdownFont {
117 Agent,
118 Editor,
119}
120
121impl MarkdownStyle {
122 pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self {
123 let theme_settings = ThemeSettings::get_global(cx);
124 let colors = cx.theme().colors();
125
126 let buffer_font_weight = theme_settings.buffer_font.weight;
127 let (buffer_font_size, ui_font_size) = match font {
128 MarkdownFont::Agent => (
129 theme_settings.agent_buffer_font_size(cx),
130 theme_settings.agent_ui_font_size(cx),
131 ),
132 MarkdownFont::Editor => (
133 theme_settings.buffer_font_size(cx),
134 theme_settings.ui_font_size(cx),
135 ),
136 };
137
138 let text_color = colors.text;
139
140 let mut text_style = window.text_style();
141 let line_height = buffer_font_size * 1.75;
142
143 text_style.refine(&TextStyleRefinement {
144 font_family: Some(theme_settings.ui_font.family.clone()),
145 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
146 font_features: Some(theme_settings.ui_font.features.clone()),
147 font_size: Some(ui_font_size.into()),
148 line_height: Some(line_height.into()),
149 color: Some(text_color),
150 ..Default::default()
151 });
152
153 MarkdownStyle {
154 base_text_style: text_style.clone(),
155 syntax: cx.theme().syntax().clone(),
156 selection_background_color: colors.element_selection_background,
157 rule_color: colors.border,
158 block_quote_border_color: colors.border,
159 code_block_overflow_x_scroll: true,
160 heading_level_styles: Some(HeadingLevelStyles {
161 h1: Some(TextStyleRefinement {
162 font_size: Some(rems(1.15).into()),
163 ..Default::default()
164 }),
165 h2: Some(TextStyleRefinement {
166 font_size: Some(rems(1.1).into()),
167 ..Default::default()
168 }),
169 h3: Some(TextStyleRefinement {
170 font_size: Some(rems(1.05).into()),
171 ..Default::default()
172 }),
173 h4: Some(TextStyleRefinement {
174 font_size: Some(rems(1.).into()),
175 ..Default::default()
176 }),
177 h5: Some(TextStyleRefinement {
178 font_size: Some(rems(0.95).into()),
179 ..Default::default()
180 }),
181 h6: Some(TextStyleRefinement {
182 font_size: Some(rems(0.875).into()),
183 ..Default::default()
184 }),
185 }),
186 code_block: StyleRefinement {
187 padding: EdgesRefinement {
188 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
189 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
190 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
191 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
192 },
193 margin: EdgesRefinement {
194 top: Some(Length::Definite(px(8.).into())),
195 left: Some(Length::Definite(px(0.).into())),
196 right: Some(Length::Definite(px(0.).into())),
197 bottom: Some(Length::Definite(px(12.).into())),
198 },
199 border_style: Some(BorderStyle::Solid),
200 border_widths: EdgesRefinement {
201 top: Some(AbsoluteLength::Pixels(px(1.))),
202 left: Some(AbsoluteLength::Pixels(px(1.))),
203 right: Some(AbsoluteLength::Pixels(px(1.))),
204 bottom: Some(AbsoluteLength::Pixels(px(1.))),
205 },
206 border_color: Some(colors.border_variant),
207 background: Some(colors.editor_background.into()),
208 text: TextStyleRefinement {
209 font_family: Some(theme_settings.buffer_font.family.clone()),
210 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
211 font_features: Some(theme_settings.buffer_font.features.clone()),
212 font_size: Some(buffer_font_size.into()),
213 font_weight: Some(buffer_font_weight),
214 ..Default::default()
215 },
216 ..Default::default()
217 },
218 inline_code: TextStyleRefinement {
219 font_family: Some(theme_settings.buffer_font.family.clone()),
220 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
221 font_features: Some(theme_settings.buffer_font.features.clone()),
222 font_size: Some(buffer_font_size.into()),
223 font_weight: Some(buffer_font_weight),
224 background_color: Some(colors.editor_foreground.opacity(0.08)),
225 ..Default::default()
226 },
227 link: TextStyleRefinement {
228 background_color: Some(colors.editor_foreground.opacity(0.025)),
229 color: Some(colors.text_accent),
230 underline: Some(UnderlineStyle {
231 color: Some(colors.text_accent.opacity(0.5)),
232 thickness: px(1.),
233 ..Default::default()
234 }),
235 ..Default::default()
236 },
237 ..Default::default()
238 }
239 }
240
241 pub fn with_muted_text(mut self, cx: &App) -> Self {
242 let colors = cx.theme().colors();
243 self.base_text_style.color = colors.text_muted;
244 self
245 }
246}
247
248pub struct Markdown {
249 source: SharedString,
250 selection: Selection,
251 pressed_link: Option<RenderedLink>,
252 autoscroll_request: Option<usize>,
253 active_root_block: Option<usize>,
254 parsed_markdown: ParsedMarkdown,
255 images_by_source_offset: HashMap<usize, Arc<Image>>,
256 should_reparse: bool,
257 pending_parse: Option<Task<()>>,
258 focus_handle: FocusHandle,
259 language_registry: Option<Arc<LanguageRegistry>>,
260 fallback_code_block_language: Option<LanguageName>,
261 options: MarkdownOptions,
262 mermaid_state: MermaidState,
263 copied_code_blocks: HashSet<ElementId>,
264 code_block_scroll_handles: BTreeMap<usize, ScrollHandle>,
265 context_menu_selected_text: Option<String>,
266}
267
268#[derive(Clone, Copy, Default)]
269pub struct MarkdownOptions {
270 pub parse_links_only: bool,
271 pub parse_html: bool,
272 pub render_mermaid_diagrams: bool,
273}
274
275#[derive(Clone, Copy, PartialEq, Eq)]
276pub enum CopyButtonVisibility {
277 Hidden,
278 AlwaysVisible,
279 VisibleOnHover,
280}
281
282pub enum CodeBlockRenderer {
283 Default {
284 copy_button_visibility: CopyButtonVisibility,
285 border: bool,
286 },
287 Custom {
288 render: CodeBlockRenderFn,
289 /// A function that can modify the parent container after the code block
290 /// content has been appended as a child element.
291 transform: Option<CodeBlockTransformFn>,
292 },
293}
294
295pub type CodeBlockRenderFn = Arc<
296 dyn Fn(
297 &CodeBlockKind,
298 &ParsedMarkdown,
299 Range<usize>,
300 CodeBlockMetadata,
301 &mut Window,
302 &App,
303 ) -> Div,
304>;
305
306pub type CodeBlockTransformFn =
307 Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
308
309actions!(
310 markdown,
311 [
312 /// Copies the selected text to the clipboard.
313 Copy,
314 /// Copies the selected text as markdown to the clipboard.
315 CopyAsMarkdown
316 ]
317);
318
319enum EscapeAction {
320 PassThrough,
321 Nbsp(usize),
322 DoubleNewline,
323 PrefixBackslash,
324}
325
326impl EscapeAction {
327 fn output_len(&self) -> usize {
328 match self {
329 Self::PassThrough => 1,
330 Self::Nbsp(count) => count * '\u{00A0}'.len_utf8(),
331 Self::DoubleNewline => 2,
332 Self::PrefixBackslash => 2,
333 }
334 }
335
336 fn write_to(&self, c: char, output: &mut String) {
337 match self {
338 Self::PassThrough => output.push(c),
339 Self::Nbsp(count) => {
340 for _ in 0..*count {
341 output.push('\u{00A0}');
342 }
343 }
344 Self::DoubleNewline => {
345 output.push('\n');
346 output.push('\n');
347 }
348 Self::PrefixBackslash => {
349 // '\\' is a single backslash in Rust, e.g. '|' -> '\|'
350 output.push('\\');
351 output.push(c);
352 }
353 }
354 }
355}
356
357// Valid to operate on raw bytes since multi-byte UTF-8
358// sequences never contain ASCII-range bytes.
359struct MarkdownEscaper {
360 in_leading_whitespace: bool,
361}
362
363impl MarkdownEscaper {
364 const TAB_SIZE: usize = 4;
365
366 fn new() -> Self {
367 Self {
368 in_leading_whitespace: true,
369 }
370 }
371
372 fn next(&mut self, byte: u8) -> EscapeAction {
373 let action = if self.in_leading_whitespace && byte == b'\t' {
374 EscapeAction::Nbsp(Self::TAB_SIZE)
375 } else if self.in_leading_whitespace && byte == b' ' {
376 EscapeAction::Nbsp(1)
377 } else if byte == b'\n' {
378 EscapeAction::DoubleNewline
379 } else if byte.is_ascii_punctuation() {
380 EscapeAction::PrefixBackslash
381 } else {
382 EscapeAction::PassThrough
383 };
384
385 self.in_leading_whitespace =
386 byte == b'\n' || (self.in_leading_whitespace && (byte == b' ' || byte == b'\t'));
387 action
388 }
389}
390
391impl Markdown {
392 pub fn new(
393 source: SharedString,
394 language_registry: Option<Arc<LanguageRegistry>>,
395 fallback_code_block_language: Option<LanguageName>,
396 cx: &mut Context<Self>,
397 ) -> Self {
398 Self::new_with_options(
399 source,
400 language_registry,
401 fallback_code_block_language,
402 MarkdownOptions::default(),
403 cx,
404 )
405 }
406
407 pub fn new_with_options(
408 source: SharedString,
409 language_registry: Option<Arc<LanguageRegistry>>,
410 fallback_code_block_language: Option<LanguageName>,
411 options: MarkdownOptions,
412 cx: &mut Context<Self>,
413 ) -> Self {
414 let focus_handle = cx.focus_handle();
415 let mut this = Self {
416 source,
417 selection: Selection::default(),
418 pressed_link: None,
419 autoscroll_request: None,
420 active_root_block: None,
421 should_reparse: false,
422 images_by_source_offset: Default::default(),
423 parsed_markdown: ParsedMarkdown::default(),
424 pending_parse: None,
425 focus_handle,
426 language_registry,
427 fallback_code_block_language,
428 options,
429 mermaid_state: MermaidState::default(),
430 copied_code_blocks: HashSet::default(),
431 code_block_scroll_handles: BTreeMap::default(),
432 context_menu_selected_text: None,
433 };
434 this.parse(cx);
435 this
436 }
437
438 pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
439 Self::new_with_options(
440 source,
441 None,
442 None,
443 MarkdownOptions {
444 parse_links_only: true,
445 ..Default::default()
446 },
447 cx,
448 )
449 }
450
451 fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle {
452 self.code_block_scroll_handles
453 .entry(id)
454 .or_insert_with(ScrollHandle::new)
455 .clone()
456 }
457
458 fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) {
459 self.code_block_scroll_handles
460 .retain(|id, _| ids.contains(id));
461 }
462
463 fn clear_code_block_scroll_handles(&mut self) {
464 self.code_block_scroll_handles.clear();
465 }
466
467 fn autoscroll_code_block(&self, source_index: usize, cursor_position: Point<Pixels>) {
468 let Some((_, scroll_handle)) = self
469 .code_block_scroll_handles
470 .range(..=source_index)
471 .next_back()
472 else {
473 return;
474 };
475
476 let bounds = scroll_handle.bounds();
477 if cursor_position.y < bounds.top() || cursor_position.y > bounds.bottom() {
478 return;
479 }
480
481 let horizontal_delta = if cursor_position.x < bounds.left() {
482 bounds.left() - cursor_position.x
483 } else if cursor_position.x > bounds.right() {
484 bounds.right() - cursor_position.x
485 } else {
486 return;
487 };
488
489 let offset = scroll_handle.offset();
490 scroll_handle.set_offset(point(offset.x + horizontal_delta, offset.y));
491 }
492
493 pub fn is_parsing(&self) -> bool {
494 self.pending_parse.is_some()
495 }
496
497 pub fn source(&self) -> &str {
498 &self.source
499 }
500
501 pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
502 self.source = SharedString::new(self.source.to_string() + text);
503 self.parse(cx);
504 }
505
506 pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
507 self.source = source.into();
508 self.parse(cx);
509 }
510
511 pub fn request_autoscroll_to_source_index(
512 &mut self,
513 source_index: usize,
514 cx: &mut Context<Self>,
515 ) {
516 self.autoscroll_request = Some(source_index);
517 cx.refresh_windows();
518 }
519
520 pub fn set_active_root_for_source_index(
521 &mut self,
522 source_index: Option<usize>,
523 cx: &mut Context<Self>,
524 ) {
525 let active_root_block =
526 source_index.and_then(|index| self.parsed_markdown.root_block_for_source_index(index));
527 if self.active_root_block == active_root_block {
528 return;
529 }
530
531 self.active_root_block = active_root_block;
532 cx.notify();
533 }
534
535 pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
536 if source == self.source() {
537 return;
538 }
539 self.source = source;
540 self.selection = Selection::default();
541 self.autoscroll_request = None;
542 self.pending_parse = None;
543 self.should_reparse = false;
544 // Don't clear parsed_markdown here - keep existing content visible until new parse completes
545 self.parse(cx);
546 }
547
548 #[cfg(any(test, feature = "test-support"))]
549 pub fn parsed_markdown(&self) -> &ParsedMarkdown {
550 &self.parsed_markdown
551 }
552
553 pub fn escape(s: &str) -> Cow<'_, str> {
554 let output_len: usize = {
555 let mut escaper = MarkdownEscaper::new();
556 s.bytes().map(|byte| escaper.next(byte).output_len()).sum()
557 };
558
559 if output_len == s.len() {
560 return s.into();
561 }
562
563 let mut escaper = MarkdownEscaper::new();
564 let mut output = String::with_capacity(output_len);
565 for c in s.chars() {
566 escaper.next(c as u8).write_to(c, &mut output);
567 }
568 output.into()
569 }
570
571 pub fn selected_text(&self) -> Option<String> {
572 if self.selection.end <= self.selection.start {
573 None
574 } else {
575 Some(self.source[self.selection.start..self.selection.end].to_string())
576 }
577 }
578
579 fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
580 if self.selection.end <= self.selection.start {
581 return;
582 }
583 let text = text.text_for_range(self.selection.start..self.selection.end);
584 cx.write_to_clipboard(ClipboardItem::new_string(text));
585 }
586
587 fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) {
588 if let Some(text) = self.context_menu_selected_text.take() {
589 cx.write_to_clipboard(ClipboardItem::new_string(text));
590 return;
591 }
592 if self.selection.end <= self.selection.start {
593 return;
594 }
595 let text = self.source[self.selection.start..self.selection.end].to_string();
596 cx.write_to_clipboard(ClipboardItem::new_string(text));
597 }
598
599 fn capture_selection_for_context_menu(&mut self) {
600 self.context_menu_selected_text = self.selected_text();
601 }
602
603 fn parse(&mut self, cx: &mut Context<Self>) {
604 if self.source.is_empty() {
605 self.should_reparse = false;
606 self.pending_parse.take();
607 self.parsed_markdown = ParsedMarkdown {
608 source: self.source.clone(),
609 ..Default::default()
610 };
611 self.active_root_block = None;
612 self.images_by_source_offset.clear();
613 self.mermaid_state.clear();
614 cx.notify();
615 cx.refresh_windows();
616 return;
617 }
618
619 if self.pending_parse.is_some() {
620 self.should_reparse = true;
621 return;
622 }
623 self.should_reparse = false;
624 self.pending_parse = Some(self.start_background_parse(cx));
625 }
626
627 fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
628 let source = self.source.clone();
629 let should_parse_links_only = self.options.parse_links_only;
630 let should_parse_html = self.options.parse_html;
631 let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
632 let language_registry = self.language_registry.clone();
633 let fallback = self.fallback_code_block_language.clone();
634
635 let parsed = cx.background_spawn(async move {
636 if should_parse_links_only {
637 return (
638 ParsedMarkdown {
639 events: Arc::from(parse_links_only(source.as_ref())),
640 source,
641 languages_by_name: TreeMap::default(),
642 languages_by_path: TreeMap::default(),
643 root_block_starts: Arc::default(),
644 html_blocks: BTreeMap::default(),
645 mermaid_diagrams: BTreeMap::default(),
646 },
647 Default::default(),
648 );
649 }
650
651 let parsed = parse_markdown_with_options(&source, should_parse_html);
652 let events = parsed.events;
653 let language_names = parsed.language_names;
654 let paths = parsed.language_paths;
655 let root_block_starts = parsed.root_block_starts;
656 let html_blocks = parsed.html_blocks;
657 let mermaid_diagrams = if should_render_mermaid_diagrams {
658 extract_mermaid_diagrams(&source, &events)
659 } else {
660 BTreeMap::default()
661 };
662 let mut images_by_source_offset = HashMap::default();
663 let mut languages_by_name = TreeMap::default();
664 let mut languages_by_path = TreeMap::default();
665 if let Some(registry) = language_registry.as_ref() {
666 for name in language_names {
667 let language = if !name.is_empty() {
668 registry.language_for_name_or_extension(&name).left_future()
669 } else if let Some(fallback) = &fallback {
670 registry.language_for_name(fallback.as_ref()).right_future()
671 } else {
672 continue;
673 };
674 if let Ok(language) = language.await {
675 languages_by_name.insert(name, language);
676 }
677 }
678
679 for path in paths {
680 if let Ok(language) = registry
681 .load_language_for_file_path(Path::new(path.as_ref()))
682 .await
683 {
684 languages_by_path.insert(path, language);
685 }
686 }
687 }
688
689 for (range, event) in &events {
690 if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event
691 && let Some(data_url) = dest_url.strip_prefix("data:")
692 {
693 let Some((mime_info, data)) = data_url.split_once(',') else {
694 continue;
695 };
696 let Some((mime_type, encoding)) = mime_info.split_once(';') else {
697 continue;
698 };
699 let Some(format) = ImageFormat::from_mime_type(mime_type) else {
700 continue;
701 };
702 let is_base64 = encoding == "base64";
703 if is_base64
704 && let Some(bytes) = base64::prelude::BASE64_STANDARD
705 .decode(data)
706 .log_with_level(Level::Debug)
707 {
708 let image = Arc::new(Image::from_bytes(format, bytes));
709 images_by_source_offset.insert(range.start, image);
710 }
711 }
712 }
713
714 (
715 ParsedMarkdown {
716 source,
717 events: Arc::from(events),
718 languages_by_name,
719 languages_by_path,
720 root_block_starts: Arc::from(root_block_starts),
721 html_blocks,
722 mermaid_diagrams,
723 },
724 images_by_source_offset,
725 )
726 });
727
728 cx.spawn(async move |this, cx| {
729 let (parsed, images_by_source_offset) = parsed.await;
730
731 this.update(cx, |this, cx| {
732 this.parsed_markdown = parsed;
733 this.images_by_source_offset = images_by_source_offset;
734 if this.active_root_block.is_some_and(|block_index| {
735 block_index >= this.parsed_markdown.root_block_starts.len()
736 }) {
737 this.active_root_block = None;
738 }
739 if this.options.render_mermaid_diagrams {
740 let parsed_markdown = this.parsed_markdown.clone();
741 this.mermaid_state.update(&parsed_markdown, cx);
742 } else {
743 this.mermaid_state.clear();
744 }
745 this.pending_parse.take();
746 if this.should_reparse {
747 this.parse(cx);
748 }
749 cx.notify();
750 cx.refresh_windows();
751 })
752 .ok();
753 })
754 }
755}
756
757impl Focusable for Markdown {
758 fn focus_handle(&self, _cx: &App) -> FocusHandle {
759 self.focus_handle.clone()
760 }
761}
762
763#[derive(Debug, Default, Clone)]
764enum SelectMode {
765 #[default]
766 Character,
767 Word(Range<usize>),
768 Line(Range<usize>),
769 All,
770}
771
772#[derive(Clone, Default)]
773struct Selection {
774 start: usize,
775 end: usize,
776 reversed: bool,
777 pending: bool,
778 mode: SelectMode,
779}
780
781impl Selection {
782 fn set_head(&mut self, head: usize, rendered_text: &RenderedText) {
783 match &self.mode {
784 SelectMode::Character => {
785 if head < self.tail() {
786 if !self.reversed {
787 self.end = self.start;
788 self.reversed = true;
789 }
790 self.start = head;
791 } else {
792 if self.reversed {
793 self.start = self.end;
794 self.reversed = false;
795 }
796 self.end = head;
797 }
798 }
799 SelectMode::Word(original_range) | SelectMode::Line(original_range) => {
800 let head_range = if matches!(self.mode, SelectMode::Word(_)) {
801 rendered_text.surrounding_word_range(head)
802 } else {
803 rendered_text.surrounding_line_range(head)
804 };
805
806 if head < original_range.start {
807 self.start = head_range.start;
808 self.end = original_range.end;
809 self.reversed = true;
810 } else if head >= original_range.end {
811 self.start = original_range.start;
812 self.end = head_range.end;
813 self.reversed = false;
814 } else {
815 self.start = original_range.start;
816 self.end = original_range.end;
817 self.reversed = false;
818 }
819 }
820 SelectMode::All => {
821 self.start = 0;
822 self.end = rendered_text
823 .lines
824 .last()
825 .map(|line| line.source_end)
826 .unwrap_or(0);
827 self.reversed = false;
828 }
829 }
830 }
831
832 fn tail(&self) -> usize {
833 if self.reversed { self.end } else { self.start }
834 }
835}
836
837#[derive(Debug, Clone, Default)]
838pub struct ParsedMarkdown {
839 pub source: SharedString,
840 pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
841 pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
842 pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
843 pub root_block_starts: Arc<[usize]>,
844 pub(crate) html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
845 pub(crate) mermaid_diagrams: BTreeMap<usize, ParsedMarkdownMermaidDiagram>,
846}
847
848impl ParsedMarkdown {
849 pub fn source(&self) -> &SharedString {
850 &self.source
851 }
852
853 pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
854 &self.events
855 }
856
857 pub fn root_block_starts(&self) -> &Arc<[usize]> {
858 &self.root_block_starts
859 }
860
861 pub fn root_block_for_source_index(&self, source_index: usize) -> Option<usize> {
862 if self.root_block_starts.is_empty() {
863 return None;
864 }
865
866 let partition = self
867 .root_block_starts
868 .partition_point(|block_start| *block_start <= source_index);
869
870 Some(partition.saturating_sub(1))
871 }
872}
873
874pub enum AutoscrollBehavior {
875 /// Propagate the request up the element tree for the nearest
876 /// scrollable ancestor (e.g. `List`) to handle.
877 Propagate,
878 /// Directly control a specific scroll handle.
879 Controlled(ScrollHandle),
880}
881
882pub struct MarkdownElement {
883 markdown: Entity<Markdown>,
884 style: MarkdownStyle,
885 code_block_renderer: CodeBlockRenderer,
886 on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
887 on_source_click: Option<SourceClickCallback>,
888 on_checkbox_toggle: Option<CheckboxToggleCallback>,
889 image_resolver: Option<Box<dyn Fn(&str) -> Option<ImageSource>>>,
890 show_root_block_markers: bool,
891 autoscroll: AutoscrollBehavior,
892}
893
894impl MarkdownElement {
895 pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
896 Self {
897 markdown,
898 style,
899 code_block_renderer: CodeBlockRenderer::Default {
900 copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
901 border: false,
902 },
903 on_url_click: None,
904 on_source_click: None,
905 on_checkbox_toggle: None,
906 image_resolver: None,
907 show_root_block_markers: false,
908 autoscroll: AutoscrollBehavior::Propagate,
909 }
910 }
911
912 #[cfg(any(test, feature = "test-support"))]
913 pub fn rendered_text(
914 markdown: Entity<Markdown>,
915 cx: &mut gpui::VisualTestContext,
916 style: impl FnOnce(&Window, &App) -> MarkdownStyle,
917 ) -> String {
918 use gpui::size;
919
920 let (text, _) = cx.draw(
921 Default::default(),
922 size(px(600.0), px(600.0)),
923 |window, cx| Self::new(markdown, style(window, cx)),
924 );
925 text.text
926 .lines
927 .iter()
928 .map(|line| line.layout.wrapped_text())
929 .collect::<Vec<_>>()
930 .join("\n")
931 }
932
933 pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
934 self.code_block_renderer = variant;
935 self
936 }
937
938 pub fn on_url_click(
939 mut self,
940 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
941 ) -> Self {
942 self.on_url_click = Some(Box::new(handler));
943 self
944 }
945
946 pub fn on_source_click(
947 mut self,
948 handler: impl Fn(usize, usize, &mut Window, &mut App) -> bool + 'static,
949 ) -> Self {
950 self.on_source_click = Some(Box::new(handler));
951 self
952 }
953
954 pub fn on_checkbox_toggle(
955 mut self,
956 handler: impl Fn(Range<usize>, bool, &mut Window, &mut App) + 'static,
957 ) -> Self {
958 self.on_checkbox_toggle = Some(Rc::new(handler));
959 self
960 }
961
962 pub fn image_resolver(
963 mut self,
964 resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
965 ) -> Self {
966 self.image_resolver = Some(Box::new(resolver));
967 self
968 }
969
970 pub fn show_root_block_markers(mut self) -> Self {
971 self.show_root_block_markers = true;
972 self
973 }
974
975 pub fn scroll_handle(mut self, scroll_handle: ScrollHandle) -> Self {
976 self.autoscroll = AutoscrollBehavior::Controlled(scroll_handle);
977 self
978 }
979
980 fn push_markdown_image(
981 &self,
982 builder: &mut MarkdownElementBuilder,
983 range: &Range<usize>,
984 source: ImageSource,
985 width: Option<DefiniteLength>,
986 height: Option<DefiniteLength>,
987 ) {
988 builder.modify_current_div(|el| {
989 el.items_center().flex().flex_row().child(
990 img(source)
991 .max_w_full()
992 .when_some(height, |this, height| this.h(height))
993 .when_some(width, |this, width| this.w(width)),
994 )
995 });
996 let _ = range;
997 }
998
999 fn push_markdown_paragraph(
1000 &self,
1001 builder: &mut MarkdownElementBuilder,
1002 range: &Range<usize>,
1003 markdown_end: usize,
1004 ) {
1005 builder.push_div(
1006 div().when(!self.style.height_is_multiple_of_line_height, |el| {
1007 el.mb_2().line_height(rems(1.3))
1008 }),
1009 range,
1010 markdown_end,
1011 );
1012 }
1013
1014 fn push_markdown_heading(
1015 &self,
1016 builder: &mut MarkdownElementBuilder,
1017 level: pulldown_cmark::HeadingLevel,
1018 range: &Range<usize>,
1019 markdown_end: usize,
1020 ) {
1021 let mut heading = div().mb_2();
1022 heading = apply_heading_style(heading, level, self.style.heading_level_styles.as_ref());
1023
1024 let mut heading_style = self.style.heading.clone();
1025 let heading_text_style = heading_style.text_style().clone();
1026 heading.style().refine(&heading_style);
1027
1028 builder.push_text_style(heading_text_style);
1029 builder.push_div(heading, range, markdown_end);
1030 }
1031
1032 fn pop_markdown_heading(&self, builder: &mut MarkdownElementBuilder) {
1033 builder.pop_div();
1034 builder.pop_text_style();
1035 }
1036
1037 fn push_markdown_block_quote(
1038 &self,
1039 builder: &mut MarkdownElementBuilder,
1040 range: &Range<usize>,
1041 markdown_end: usize,
1042 ) {
1043 builder.push_text_style(self.style.block_quote.clone());
1044 builder.push_div(
1045 div()
1046 .pl_4()
1047 .mb_2()
1048 .border_l_4()
1049 .border_color(self.style.block_quote_border_color),
1050 range,
1051 markdown_end,
1052 );
1053 }
1054
1055 fn pop_markdown_block_quote(&self, builder: &mut MarkdownElementBuilder) {
1056 builder.pop_div();
1057 builder.pop_text_style();
1058 }
1059
1060 fn push_markdown_list_item(
1061 &self,
1062 builder: &mut MarkdownElementBuilder,
1063 bullet: AnyElement,
1064 range: &Range<usize>,
1065 markdown_end: usize,
1066 ) {
1067 builder.push_div(
1068 div()
1069 .when(!self.style.height_is_multiple_of_line_height, |el| {
1070 el.mb_1().gap_1().line_height(rems(1.3))
1071 })
1072 .h_flex()
1073 .items_start()
1074 .child(bullet),
1075 range,
1076 markdown_end,
1077 );
1078 // Without `w_0`, text doesn't wrap to the width of the container.
1079 builder.push_div(div().flex_1().w_0(), range, markdown_end);
1080 }
1081
1082 fn pop_markdown_list_item(&self, builder: &mut MarkdownElementBuilder) {
1083 builder.pop_div();
1084 builder.pop_div();
1085 }
1086
1087 fn paint_selection(
1088 &self,
1089 bounds: Bounds<Pixels>,
1090 rendered_text: &RenderedText,
1091 window: &mut Window,
1092 cx: &mut App,
1093 ) {
1094 let selection = self.markdown.read(cx).selection.clone();
1095 let selection_start = rendered_text.position_for_source_index(selection.start);
1096 let selection_end = rendered_text.position_for_source_index(selection.end);
1097 if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
1098 selection_start.zip(selection_end)
1099 {
1100 if start_position.y == end_position.y {
1101 window.paint_quad(quad(
1102 Bounds::from_corners(
1103 start_position,
1104 point(end_position.x, end_position.y + end_line_height),
1105 ),
1106 Pixels::ZERO,
1107 self.style.selection_background_color,
1108 Edges::default(),
1109 Hsla::transparent_black(),
1110 BorderStyle::default(),
1111 ));
1112 } else {
1113 window.paint_quad(quad(
1114 Bounds::from_corners(
1115 start_position,
1116 point(bounds.right(), start_position.y + start_line_height),
1117 ),
1118 Pixels::ZERO,
1119 self.style.selection_background_color,
1120 Edges::default(),
1121 Hsla::transparent_black(),
1122 BorderStyle::default(),
1123 ));
1124
1125 if end_position.y > start_position.y + start_line_height {
1126 window.paint_quad(quad(
1127 Bounds::from_corners(
1128 point(bounds.left(), start_position.y + start_line_height),
1129 point(bounds.right(), end_position.y),
1130 ),
1131 Pixels::ZERO,
1132 self.style.selection_background_color,
1133 Edges::default(),
1134 Hsla::transparent_black(),
1135 BorderStyle::default(),
1136 ));
1137 }
1138
1139 window.paint_quad(quad(
1140 Bounds::from_corners(
1141 point(bounds.left(), end_position.y),
1142 point(end_position.x, end_position.y + end_line_height),
1143 ),
1144 Pixels::ZERO,
1145 self.style.selection_background_color,
1146 Edges::default(),
1147 Hsla::transparent_black(),
1148 BorderStyle::default(),
1149 ));
1150 }
1151 }
1152 }
1153
1154 fn paint_mouse_listeners(
1155 &mut self,
1156 hitbox: &Hitbox,
1157 rendered_text: &RenderedText,
1158 window: &mut Window,
1159 cx: &mut App,
1160 ) {
1161 if self.style.prevent_mouse_interaction {
1162 return;
1163 }
1164
1165 let is_hovering_link = hitbox.is_hovered(window)
1166 && !self.markdown.read(cx).selection.pending
1167 && rendered_text
1168 .link_for_position(window.mouse_position())
1169 .is_some();
1170
1171 if !self.style.prevent_mouse_interaction {
1172 if is_hovering_link {
1173 window.set_cursor_style(CursorStyle::PointingHand, hitbox);
1174 } else {
1175 window.set_cursor_style(CursorStyle::IBeam, hitbox);
1176 }
1177 }
1178
1179 let on_open_url = self.on_url_click.take();
1180 let on_source_click = self.on_source_click.take();
1181
1182 self.on_mouse_event(window, cx, {
1183 let hitbox = hitbox.clone();
1184 move |markdown, event: &MouseDownEvent, phase, window, _| {
1185 if phase.capture()
1186 && event.button == MouseButton::Right
1187 && hitbox.is_hovered(window)
1188 {
1189 // Capture selected text so it survives until menu item is clicked
1190 markdown.capture_selection_for_context_menu();
1191 }
1192 }
1193 });
1194
1195 self.on_mouse_event(window, cx, {
1196 let rendered_text = rendered_text.clone();
1197 let hitbox = hitbox.clone();
1198 move |markdown, event: &MouseDownEvent, phase, window, cx| {
1199 if hitbox.is_hovered(window) {
1200 if phase.bubble() {
1201 if let Some(link) = rendered_text.link_for_position(event.position) {
1202 markdown.pressed_link = Some(link.clone());
1203 } else {
1204 let source_index =
1205 match rendered_text.source_index_for_position(event.position) {
1206 Ok(ix) | Err(ix) => ix,
1207 };
1208 if let Some(handler) = on_source_click.as_ref() {
1209 let blocked = handler(source_index, event.click_count, window, cx);
1210 if blocked {
1211 markdown.selection = Selection::default();
1212 markdown.pressed_link = None;
1213 window.prevent_default();
1214 cx.notify();
1215 return;
1216 }
1217 }
1218 let (range, mode) = match event.click_count {
1219 1 => {
1220 let range = source_index..source_index;
1221 (range, SelectMode::Character)
1222 }
1223 2 => {
1224 let range = rendered_text.surrounding_word_range(source_index);
1225 (range.clone(), SelectMode::Word(range))
1226 }
1227 3 => {
1228 let range = rendered_text.surrounding_line_range(source_index);
1229 (range.clone(), SelectMode::Line(range))
1230 }
1231 _ => {
1232 let range = 0..rendered_text
1233 .lines
1234 .last()
1235 .map(|line| line.source_end)
1236 .unwrap_or(0);
1237 (range, SelectMode::All)
1238 }
1239 };
1240 markdown.selection = Selection {
1241 start: range.start,
1242 end: range.end,
1243 reversed: false,
1244 pending: true,
1245 mode,
1246 };
1247 window.focus(&markdown.focus_handle, cx);
1248 }
1249
1250 window.prevent_default();
1251 cx.notify();
1252 }
1253 } else if phase.capture() && event.button == MouseButton::Left {
1254 markdown.selection = Selection::default();
1255 markdown.pressed_link = None;
1256 cx.notify();
1257 }
1258 }
1259 });
1260 self.on_mouse_event(window, cx, {
1261 let rendered_text = rendered_text.clone();
1262 let hitbox = hitbox.clone();
1263 let was_hovering_link = is_hovering_link;
1264 move |markdown, event: &MouseMoveEvent, phase, window, cx| {
1265 if phase.capture() {
1266 return;
1267 }
1268
1269 if markdown.selection.pending {
1270 let source_index = match rendered_text.source_index_for_position(event.position)
1271 {
1272 Ok(ix) | Err(ix) => ix,
1273 };
1274 markdown.selection.set_head(source_index, &rendered_text);
1275 markdown.autoscroll_code_block(source_index, event.position);
1276 markdown.autoscroll_request = Some(source_index);
1277 cx.notify();
1278 } else {
1279 let is_hovering_link = hitbox.is_hovered(window)
1280 && rendered_text.link_for_position(event.position).is_some();
1281 if is_hovering_link != was_hovering_link {
1282 cx.notify();
1283 }
1284 }
1285 }
1286 });
1287 self.on_mouse_event(window, cx, {
1288 let rendered_text = rendered_text.clone();
1289 move |markdown, event: &MouseUpEvent, phase, window, cx| {
1290 if phase.bubble() {
1291 if let Some(pressed_link) = markdown.pressed_link.take()
1292 && Some(&pressed_link) == rendered_text.link_for_position(event.position)
1293 {
1294 if let Some(open_url) = on_open_url.as_ref() {
1295 open_url(pressed_link.destination_url, window, cx);
1296 } else {
1297 cx.open_url(&pressed_link.destination_url);
1298 }
1299 }
1300 } else if markdown.selection.pending {
1301 markdown.selection.pending = false;
1302 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1303 {
1304 let text = rendered_text
1305 .text_for_range(markdown.selection.start..markdown.selection.end);
1306 cx.write_to_primary(ClipboardItem::new_string(text))
1307 }
1308 cx.notify();
1309 }
1310 }
1311 });
1312 }
1313
1314 fn autoscroll(
1315 &self,
1316 rendered_text: &RenderedText,
1317 window: &mut Window,
1318 cx: &mut App,
1319 ) -> Option<()> {
1320 let autoscroll_index = self
1321 .markdown
1322 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
1323 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
1324
1325 match &self.autoscroll {
1326 AutoscrollBehavior::Controlled(scroll_handle) => {
1327 let viewport = scroll_handle.bounds();
1328 let margin = line_height * 3.;
1329 let top_goal = viewport.top() + margin;
1330 let bottom_goal = viewport.bottom() - margin;
1331 let current_offset = scroll_handle.offset();
1332
1333 let new_offset_y = if position.y < top_goal {
1334 current_offset.y + (top_goal - position.y)
1335 } else if position.y + line_height > bottom_goal {
1336 current_offset.y + (bottom_goal - (position.y + line_height))
1337 } else {
1338 current_offset.y
1339 };
1340
1341 scroll_handle.set_offset(point(
1342 current_offset.x,
1343 new_offset_y.clamp(-scroll_handle.max_offset().y, Pixels::ZERO),
1344 ));
1345 }
1346 AutoscrollBehavior::Propagate => {
1347 let text_style = self.style.base_text_style.clone();
1348 let font_id = window.text_system().resolve_font(&text_style.font());
1349 let font_size = text_style.font_size.to_pixels(window.rem_size());
1350 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
1351 window.request_autoscroll(Bounds::from_corners(
1352 point(position.x - 3. * em_width, position.y - 3. * line_height),
1353 point(position.x + 3. * em_width, position.y + 3. * line_height),
1354 ));
1355 }
1356 }
1357 Some(())
1358 }
1359
1360 fn on_mouse_event<T: MouseEvent>(
1361 &self,
1362 window: &mut Window,
1363 _cx: &mut App,
1364 mut f: impl 'static
1365 + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
1366 ) {
1367 window.on_mouse_event({
1368 let markdown = self.markdown.downgrade();
1369 move |event, phase, window, cx| {
1370 markdown
1371 .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
1372 .log_err();
1373 }
1374 });
1375 }
1376}
1377
1378impl Styled for MarkdownElement {
1379 fn style(&mut self) -> &mut StyleRefinement {
1380 &mut self.style.container_style
1381 }
1382}
1383
1384impl Element for MarkdownElement {
1385 type RequestLayoutState = RenderedMarkdown;
1386 type PrepaintState = Hitbox;
1387
1388 fn id(&self) -> Option<ElementId> {
1389 None
1390 }
1391
1392 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1393 None
1394 }
1395
1396 fn request_layout(
1397 &mut self,
1398 _id: Option<&GlobalElementId>,
1399 _inspector_id: Option<&gpui::InspectorElementId>,
1400 window: &mut Window,
1401 cx: &mut App,
1402 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
1403 let mut builder = MarkdownElementBuilder::new(
1404 &self.style.container_style,
1405 self.style.base_text_style.clone(),
1406 self.style.syntax.clone(),
1407 );
1408 let (parsed_markdown, images, active_root_block, render_mermaid_diagrams, mermaid_state) = {
1409 let markdown = self.markdown.read(cx);
1410 (
1411 markdown.parsed_markdown.clone(),
1412 markdown.images_by_source_offset.clone(),
1413 markdown.active_root_block,
1414 markdown.options.render_mermaid_diagrams,
1415 markdown.mermaid_state.clone(),
1416 )
1417 };
1418 let markdown_end = if let Some(last) = parsed_markdown.events.last() {
1419 last.0.end
1420 } else {
1421 0
1422 };
1423 let mut code_block_ids = HashSet::default();
1424
1425 let mut current_img_block_range: Option<Range<usize>> = None;
1426 let mut handled_html_block = false;
1427 let mut rendered_mermaid_block = false;
1428 for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
1429 // Skip alt text for images that rendered
1430 if let Some(current_img_block_range) = ¤t_img_block_range
1431 && current_img_block_range.end > range.end
1432 {
1433 continue;
1434 }
1435
1436 if handled_html_block {
1437 if let MarkdownEvent::End(MarkdownTagEnd::HtmlBlock) = event {
1438 handled_html_block = false;
1439 } else {
1440 continue;
1441 }
1442 }
1443
1444 if rendered_mermaid_block {
1445 if matches!(event, MarkdownEvent::End(MarkdownTagEnd::CodeBlock)) {
1446 rendered_mermaid_block = false;
1447 }
1448 continue;
1449 }
1450
1451 match event {
1452 MarkdownEvent::RootStart => {
1453 if self.show_root_block_markers {
1454 builder.push_root_block(range, markdown_end);
1455 }
1456 }
1457 MarkdownEvent::RootEnd(root_block_index) => {
1458 if self.show_root_block_markers {
1459 builder.pop_root_block(
1460 active_root_block == Some(*root_block_index),
1461 cx.theme().colors().border,
1462 cx.theme().colors().border_variant,
1463 );
1464 }
1465 }
1466 MarkdownEvent::Start(tag) => {
1467 match tag {
1468 MarkdownTag::Image { dest_url, .. } => {
1469 if let Some(image) = images.get(&range.start) {
1470 current_img_block_range = Some(range.clone());
1471 self.push_markdown_image(
1472 &mut builder,
1473 range,
1474 image.clone().into(),
1475 None,
1476 None,
1477 );
1478 } else if let Some(source) = self
1479 .image_resolver
1480 .as_ref()
1481 .and_then(|resolve| resolve(dest_url.as_ref()))
1482 {
1483 current_img_block_range = Some(range.clone());
1484 self.push_markdown_image(&mut builder, range, source, None, None);
1485 }
1486 }
1487 MarkdownTag::Paragraph => {
1488 self.push_markdown_paragraph(&mut builder, range, markdown_end);
1489 }
1490 MarkdownTag::Heading { level, .. } => {
1491 self.push_markdown_heading(&mut builder, *level, range, markdown_end);
1492 }
1493 MarkdownTag::BlockQuote => {
1494 self.push_markdown_block_quote(&mut builder, range, markdown_end);
1495 }
1496 MarkdownTag::CodeBlock { kind, .. } => {
1497 if render_mermaid_diagrams
1498 && let Some(mermaid_diagram) =
1499 parsed_markdown.mermaid_diagrams.get(&range.start)
1500 {
1501 builder.push_sourced_element(
1502 mermaid_diagram.content_range.clone(),
1503 render_mermaid_diagram(
1504 mermaid_diagram,
1505 &mermaid_state,
1506 &self.style,
1507 ),
1508 );
1509 rendered_mermaid_block = true;
1510 continue;
1511 }
1512
1513 let language = match kind {
1514 CodeBlockKind::Fenced => None,
1515 CodeBlockKind::FencedLang(language) => {
1516 parsed_markdown.languages_by_name.get(language).cloned()
1517 }
1518 CodeBlockKind::FencedSrc(path_range) => parsed_markdown
1519 .languages_by_path
1520 .get(&path_range.path)
1521 .cloned(),
1522 _ => None,
1523 };
1524
1525 let is_indented = matches!(kind, CodeBlockKind::Indented);
1526 let scroll_handle = if self.style.code_block_overflow_x_scroll {
1527 code_block_ids.insert(range.start);
1528 Some(self.markdown.update(cx, |markdown, _| {
1529 markdown.code_block_scroll_handle(range.start)
1530 }))
1531 } else {
1532 None
1533 };
1534
1535 match (&self.code_block_renderer, is_indented) {
1536 (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
1537 // This is a parent container that we can position the copy button inside.
1538 let parent_container =
1539 div().group("code_block").relative().w_full();
1540
1541 let mut parent_container: AnyDiv = if let Some(scroll_handle) =
1542 scroll_handle.as_ref()
1543 {
1544 let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
1545 .id(("markdown-code-block-scrollbar", range.start))
1546 .tracked_scroll_handle(scroll_handle)
1547 .with_track_along(
1548 ScrollAxes::Horizontal,
1549 cx.theme().colors().editor_background,
1550 )
1551 .notify_content();
1552
1553 parent_container
1554 .rounded_lg()
1555 .custom_scrollbars(scrollbars, window, cx)
1556 .into()
1557 } else {
1558 parent_container.into()
1559 };
1560
1561 if let CodeBlockRenderer::Default { border: true, .. } =
1562 &self.code_block_renderer
1563 {
1564 parent_container = parent_container
1565 .rounded_md()
1566 .border_1()
1567 .border_color(cx.theme().colors().border_variant);
1568 }
1569
1570 parent_container.style().refine(&self.style.code_block);
1571 builder.push_div(parent_container, range, markdown_end);
1572
1573 let code_block = div()
1574 .id(("code-block", range.start))
1575 .rounded_lg()
1576 .map(|mut code_block| {
1577 if let Some(scroll_handle) = scroll_handle.as_ref() {
1578 code_block.style().restrict_scroll_to_axis =
1579 Some(true);
1580 code_block
1581 .flex()
1582 .overflow_x_scroll()
1583 .track_scroll(scroll_handle)
1584 } else {
1585 code_block.w_full()
1586 }
1587 });
1588
1589 builder.push_text_style(self.style.code_block.text.to_owned());
1590 builder.push_code_block(language);
1591 builder.push_div(code_block, range, markdown_end);
1592 }
1593 (CodeBlockRenderer::Custom { .. }, _) => {}
1594 }
1595 }
1596 MarkdownTag::HtmlBlock => {
1597 builder.push_div(div(), range, markdown_end);
1598 if let Some(block) = parsed_markdown.html_blocks.get(&range.start) {
1599 self.render_html_block(block, &mut builder, markdown_end, cx);
1600 handled_html_block = true;
1601 }
1602 }
1603 MarkdownTag::List(bullet_index) => {
1604 builder.push_list(*bullet_index);
1605 builder.push_div(div().pl_2p5(), range, markdown_end);
1606 }
1607 MarkdownTag::Item => {
1608 let bullet =
1609 if let Some((task_range, MarkdownEvent::TaskListMarker(checked))) =
1610 parsed_markdown.events.get(index.saturating_add(1))
1611 {
1612 let source = &parsed_markdown.source()[range.clone()];
1613 let checked = *checked;
1614 let toggle_state = if checked {
1615 ToggleState::Selected
1616 } else {
1617 ToggleState::Unselected
1618 };
1619
1620 let checkbox = Checkbox::new(
1621 ElementId::Name(source.to_string().into()),
1622 toggle_state,
1623 )
1624 .fill();
1625
1626 if let Some(on_toggle) = self.on_checkbox_toggle.clone() {
1627 let task_source_range = task_range.clone();
1628 checkbox
1629 .on_click(move |_state, window, cx| {
1630 on_toggle(
1631 task_source_range.clone(),
1632 !checked,
1633 window,
1634 cx,
1635 );
1636 })
1637 .into_any_element()
1638 } else {
1639 checkbox.visualization_only(true).into_any_element()
1640 }
1641 } else if let Some(bullet_index) = builder.next_bullet_index() {
1642 div().child(format!("{}.", bullet_index)).into_any_element()
1643 } else {
1644 div().child("•").into_any_element()
1645 };
1646 self.push_markdown_list_item(&mut builder, bullet, range, markdown_end);
1647 }
1648 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1649 font_style: Some(FontStyle::Italic),
1650 ..Default::default()
1651 }),
1652 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1653 font_weight: Some(FontWeight::BOLD),
1654 ..Default::default()
1655 }),
1656 MarkdownTag::Strikethrough => {
1657 builder.push_text_style(TextStyleRefinement {
1658 strikethrough: Some(StrikethroughStyle {
1659 thickness: px(1.),
1660 color: None,
1661 }),
1662 ..Default::default()
1663 })
1664 }
1665 MarkdownTag::Link { dest_url, .. } => {
1666 if builder.code_block_stack.is_empty() {
1667 builder.push_link(dest_url.clone(), range.clone());
1668 let style = self
1669 .style
1670 .link_callback
1671 .as_ref()
1672 .and_then(|callback| callback(dest_url, cx))
1673 .unwrap_or_else(|| self.style.link.clone());
1674 builder.push_text_style(style)
1675 }
1676 }
1677 MarkdownTag::MetadataBlock(_) => {}
1678 MarkdownTag::Table(alignments) => {
1679 builder.table.start(alignments.clone());
1680
1681 let column_count = alignments.len();
1682 builder.push_div(
1683 div()
1684 .id(("table", range.start))
1685 .grid()
1686 .grid_cols(column_count as u16)
1687 .when(self.style.table_columns_min_size, |this| {
1688 this.grid_cols_min_content(column_count as u16)
1689 })
1690 .when(!self.style.table_columns_min_size, |this| {
1691 this.grid_cols(column_count as u16)
1692 })
1693 .w_full()
1694 .mb_2()
1695 .border(px(1.5))
1696 .border_color(cx.theme().colors().border)
1697 .rounded_sm()
1698 .overflow_hidden(),
1699 range,
1700 markdown_end,
1701 );
1702 }
1703 MarkdownTag::TableHead => {
1704 builder.table.start_head();
1705 builder.push_text_style(TextStyleRefinement {
1706 font_weight: Some(FontWeight::SEMIBOLD),
1707 ..Default::default()
1708 });
1709 }
1710 MarkdownTag::TableRow => {
1711 builder.table.start_row();
1712 }
1713 MarkdownTag::TableCell => {
1714 let is_header = builder.table.in_head;
1715 let row_index = builder.table.row_index;
1716 let col_index = builder.table.col_index;
1717
1718 builder.push_div(
1719 div()
1720 .when(col_index > 0, |this| this.border_l_1())
1721 .when(row_index > 0, |this| this.border_t_1())
1722 .border_color(cx.theme().colors().border)
1723 .px_1()
1724 .py_0p5()
1725 .when(is_header, |this| {
1726 this.bg(cx.theme().colors().title_bar_background)
1727 })
1728 .when(!is_header && row_index % 2 == 1, |this| {
1729 this.bg(cx.theme().colors().panel_background)
1730 }),
1731 range,
1732 markdown_end,
1733 );
1734 }
1735 _ => log::debug!("unsupported markdown tag {:?}", tag),
1736 }
1737 }
1738 MarkdownEvent::End(tag) => match tag {
1739 MarkdownTagEnd::Image => {
1740 current_img_block_range.take();
1741 }
1742 MarkdownTagEnd::Paragraph => {
1743 builder.pop_div();
1744 }
1745 MarkdownTagEnd::Heading(_) => {
1746 self.pop_markdown_heading(&mut builder);
1747 }
1748 MarkdownTagEnd::BlockQuote(_kind) => {
1749 self.pop_markdown_block_quote(&mut builder);
1750 }
1751 MarkdownTagEnd::CodeBlock => {
1752 builder.trim_trailing_newline();
1753
1754 builder.pop_div();
1755 builder.pop_code_block();
1756 builder.pop_text_style();
1757
1758 if let CodeBlockRenderer::Default {
1759 copy_button_visibility,
1760 ..
1761 } = &self.code_block_renderer
1762 && *copy_button_visibility != CopyButtonVisibility::Hidden
1763 {
1764 builder.modify_current_div(|el| {
1765 let content_range = parser::extract_code_block_content_range(
1766 &parsed_markdown.source()[range.clone()],
1767 );
1768 let content_range = content_range.start + range.start
1769 ..content_range.end + range.start;
1770
1771 let code = parsed_markdown.source()[content_range].to_string();
1772 let codeblock = render_copy_code_block_button(
1773 range.end,
1774 code,
1775 self.markdown.clone(),
1776 );
1777 el.child(
1778 h_flex()
1779 .w_4()
1780 .absolute()
1781 .justify_end()
1782 .when_else(
1783 *copy_button_visibility
1784 == CopyButtonVisibility::VisibleOnHover,
1785 |this| {
1786 this.top_0()
1787 .right_0()
1788 .visible_on_hover("code_block")
1789 },
1790 |this| this.top_1p5().right_1p5(),
1791 )
1792 .child(codeblock),
1793 )
1794 });
1795 }
1796
1797 // Pop the parent container.
1798 builder.pop_div();
1799 }
1800 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1801 MarkdownTagEnd::List(_) => {
1802 builder.pop_list();
1803 builder.pop_div();
1804 }
1805 MarkdownTagEnd::Item => {
1806 self.pop_markdown_list_item(&mut builder);
1807 }
1808 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1809 MarkdownTagEnd::Strong => builder.pop_text_style(),
1810 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1811 MarkdownTagEnd::Link => {
1812 if builder.code_block_stack.is_empty() {
1813 builder.pop_text_style()
1814 }
1815 }
1816 MarkdownTagEnd::Table => {
1817 builder.pop_div();
1818 builder.table.end();
1819 }
1820 MarkdownTagEnd::TableHead => {
1821 builder.pop_text_style();
1822 builder.table.end_head();
1823 }
1824 MarkdownTagEnd::TableRow => {
1825 builder.table.end_row();
1826 }
1827 MarkdownTagEnd::TableCell => {
1828 builder.replace_pending_checkbox(range);
1829 builder.pop_div();
1830 builder.table.end_cell();
1831 }
1832 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1833 },
1834 MarkdownEvent::Text => {
1835 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1836 }
1837 MarkdownEvent::SubstitutedText(text) => {
1838 builder.push_text(text, range.clone());
1839 }
1840 MarkdownEvent::Code => {
1841 builder.push_text_style(self.style.inline_code.clone());
1842 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1843 builder.pop_text_style();
1844 }
1845 MarkdownEvent::Html => {
1846 let html = &parsed_markdown.source[range.clone()];
1847 if html.starts_with("<!--") {
1848 builder.html_comment = true;
1849 }
1850 if html.trim_end().ends_with("-->") {
1851 builder.html_comment = false;
1852 continue;
1853 }
1854 if builder.html_comment {
1855 continue;
1856 }
1857 builder.push_text(html, range.clone());
1858 }
1859 MarkdownEvent::InlineHtml => {
1860 let html = &parsed_markdown.source[range.clone()];
1861 if html.starts_with("<code>") {
1862 builder.push_text_style(self.style.inline_code.clone());
1863 continue;
1864 }
1865 if html.trim_end().starts_with("</code>") {
1866 builder.pop_text_style();
1867 continue;
1868 }
1869 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1870 }
1871 MarkdownEvent::Rule => {
1872 builder.push_div(
1873 div()
1874 .border_b_1()
1875 .my_2()
1876 .border_color(self.style.rule_color),
1877 range,
1878 markdown_end,
1879 );
1880 builder.pop_div()
1881 }
1882 MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
1883 MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
1884 MarkdownEvent::TaskListMarker(_) => {
1885 // handled inside the `MarkdownTag::Item` case
1886 }
1887 _ => log::debug!("unsupported markdown event {:?}", event),
1888 }
1889 }
1890 if self.style.code_block_overflow_x_scroll {
1891 let code_block_ids = code_block_ids;
1892 self.markdown.update(cx, move |markdown, _| {
1893 markdown.retain_code_block_scroll_handles(&code_block_ids);
1894 });
1895 } else {
1896 self.markdown
1897 .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1898 }
1899 let mut rendered_markdown = builder.build();
1900 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1901 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1902 (layout_id, rendered_markdown)
1903 }
1904
1905 fn prepaint(
1906 &mut self,
1907 _id: Option<&GlobalElementId>,
1908 _inspector_id: Option<&gpui::InspectorElementId>,
1909 bounds: Bounds<Pixels>,
1910 rendered_markdown: &mut Self::RequestLayoutState,
1911 window: &mut Window,
1912 cx: &mut App,
1913 ) -> Self::PrepaintState {
1914 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1915 window.set_focus_handle(&focus_handle, cx);
1916 window.set_view_id(self.markdown.entity_id());
1917
1918 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1919 rendered_markdown.element.prepaint(window, cx);
1920 self.autoscroll(&rendered_markdown.text, window, cx);
1921 hitbox
1922 }
1923
1924 fn paint(
1925 &mut self,
1926 _id: Option<&GlobalElementId>,
1927 _inspector_id: Option<&gpui::InspectorElementId>,
1928 bounds: Bounds<Pixels>,
1929 rendered_markdown: &mut Self::RequestLayoutState,
1930 hitbox: &mut Self::PrepaintState,
1931 window: &mut Window,
1932 cx: &mut App,
1933 ) {
1934 let mut context = KeyContext::default();
1935 context.add("Markdown");
1936 window.set_key_context(context);
1937 window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1938 let entity = self.markdown.clone();
1939 let text = rendered_markdown.text.clone();
1940 move |_, phase, window, cx| {
1941 let text = text.clone();
1942 if phase == DispatchPhase::Bubble {
1943 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1944 }
1945 }
1946 });
1947 window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
1948 let entity = self.markdown.clone();
1949 move |_, phase, window, cx| {
1950 if phase == DispatchPhase::Bubble {
1951 entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1952 }
1953 }
1954 });
1955
1956 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1957 rendered_markdown.element.paint(window, cx);
1958 self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1959 }
1960}
1961
1962fn apply_heading_style(
1963 mut heading: Div,
1964 level: pulldown_cmark::HeadingLevel,
1965 custom_styles: Option<&HeadingLevelStyles>,
1966) -> Div {
1967 heading = match level {
1968 pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
1969 pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
1970 pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
1971 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
1972 pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
1973 pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
1974 };
1975
1976 if let Some(styles) = custom_styles {
1977 let style_opt = match level {
1978 pulldown_cmark::HeadingLevel::H1 => &styles.h1,
1979 pulldown_cmark::HeadingLevel::H2 => &styles.h2,
1980 pulldown_cmark::HeadingLevel::H3 => &styles.h3,
1981 pulldown_cmark::HeadingLevel::H4 => &styles.h4,
1982 pulldown_cmark::HeadingLevel::H5 => &styles.h5,
1983 pulldown_cmark::HeadingLevel::H6 => &styles.h6,
1984 };
1985
1986 if let Some(style) = style_opt {
1987 heading.style().text = style.clone();
1988 }
1989 }
1990
1991 heading
1992}
1993
1994fn render_copy_code_block_button(
1995 id: usize,
1996 code: String,
1997 markdown: Entity<Markdown>,
1998) -> impl IntoElement {
1999 let id = ElementId::named_usize("copy-markdown-code", id);
2000
2001 CopyButton::new(id.clone(), code.clone()).custom_on_click({
2002 let markdown = markdown;
2003 move |_window, cx| {
2004 let id = id.clone();
2005 markdown.update(cx, |this, cx| {
2006 this.copied_code_blocks.insert(id.clone());
2007
2008 cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
2009
2010 cx.spawn(async move |this, cx| {
2011 cx.background_executor().timer(Duration::from_secs(2)).await;
2012
2013 cx.update(|cx| {
2014 this.update(cx, |this, cx| {
2015 this.copied_code_blocks.remove(&id);
2016 cx.notify();
2017 })
2018 })
2019 .ok();
2020 })
2021 .detach();
2022 });
2023 }
2024 })
2025}
2026
2027impl IntoElement for MarkdownElement {
2028 type Element = Self;
2029
2030 fn into_element(self) -> Self::Element {
2031 self
2032 }
2033}
2034
2035pub enum AnyDiv {
2036 Div(Div),
2037 Stateful(Stateful<Div>),
2038}
2039
2040impl AnyDiv {
2041 fn into_any_element(self) -> AnyElement {
2042 match self {
2043 Self::Div(div) => div.into_any_element(),
2044 Self::Stateful(div) => div.into_any_element(),
2045 }
2046 }
2047}
2048
2049impl From<Div> for AnyDiv {
2050 fn from(value: Div) -> Self {
2051 Self::Div(value)
2052 }
2053}
2054
2055impl From<Stateful<Div>> for AnyDiv {
2056 fn from(value: Stateful<Div>) -> Self {
2057 Self::Stateful(value)
2058 }
2059}
2060
2061impl Styled for AnyDiv {
2062 fn style(&mut self) -> &mut StyleRefinement {
2063 match self {
2064 Self::Div(div) => div.style(),
2065 Self::Stateful(div) => div.style(),
2066 }
2067 }
2068}
2069
2070impl ParentElement for AnyDiv {
2071 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
2072 match self {
2073 Self::Div(div) => div.extend(elements),
2074 Self::Stateful(div) => div.extend(elements),
2075 }
2076 }
2077}
2078
2079#[derive(Default)]
2080struct TableState {
2081 alignments: Vec<Alignment>,
2082 in_head: bool,
2083 row_index: usize,
2084 col_index: usize,
2085}
2086
2087impl TableState {
2088 fn start(&mut self, alignments: Vec<Alignment>) {
2089 self.alignments = alignments;
2090 self.in_head = false;
2091 self.row_index = 0;
2092 self.col_index = 0;
2093 }
2094
2095 fn end(&mut self) {
2096 self.alignments.clear();
2097 self.in_head = false;
2098 self.row_index = 0;
2099 self.col_index = 0;
2100 }
2101
2102 fn start_head(&mut self) {
2103 self.in_head = true;
2104 }
2105
2106 fn end_head(&mut self) {
2107 self.in_head = false;
2108 }
2109
2110 fn start_row(&mut self) {
2111 self.col_index = 0;
2112 }
2113
2114 fn end_row(&mut self) {
2115 self.row_index += 1;
2116 }
2117
2118 fn end_cell(&mut self) {
2119 self.col_index += 1;
2120 }
2121}
2122
2123struct MarkdownElementBuilder {
2124 div_stack: Vec<AnyDiv>,
2125 rendered_lines: Vec<RenderedLine>,
2126 pending_line: PendingLine,
2127 rendered_links: Vec<RenderedLink>,
2128 current_source_index: usize,
2129 html_comment: bool,
2130 base_text_style: TextStyle,
2131 text_style_stack: Vec<TextStyleRefinement>,
2132 code_block_stack: Vec<Option<Arc<Language>>>,
2133 list_stack: Vec<ListStackEntry>,
2134 table: TableState,
2135 syntax_theme: Arc<SyntaxTheme>,
2136}
2137
2138#[derive(Default)]
2139struct PendingLine {
2140 text: String,
2141 runs: Vec<TextRun>,
2142 source_mappings: Vec<SourceMapping>,
2143}
2144
2145struct ListStackEntry {
2146 bullet_index: Option<u64>,
2147}
2148
2149impl MarkdownElementBuilder {
2150 fn new(
2151 container_style: &StyleRefinement,
2152 base_text_style: TextStyle,
2153 syntax_theme: Arc<SyntaxTheme>,
2154 ) -> Self {
2155 Self {
2156 div_stack: vec![{
2157 let mut base_div = div();
2158 base_div.style().refine(container_style);
2159 base_div.debug_selector(|| "inner".into()).into()
2160 }],
2161 rendered_lines: Vec::new(),
2162 pending_line: PendingLine::default(),
2163 rendered_links: Vec::new(),
2164 current_source_index: 0,
2165 html_comment: false,
2166 base_text_style,
2167 text_style_stack: Vec::new(),
2168 code_block_stack: Vec::new(),
2169 list_stack: Vec::new(),
2170 table: TableState::default(),
2171 syntax_theme,
2172 }
2173 }
2174
2175 fn push_text_style(&mut self, style: TextStyleRefinement) {
2176 self.text_style_stack.push(style);
2177 }
2178
2179 fn text_style(&self) -> TextStyle {
2180 let mut style = self.base_text_style.clone();
2181 for refinement in &self.text_style_stack {
2182 style.refine(refinement);
2183 }
2184 style
2185 }
2186
2187 fn pop_text_style(&mut self) {
2188 self.text_style_stack.pop();
2189 }
2190
2191 fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
2192 let mut div = div.into();
2193 self.flush_text();
2194
2195 if range.start == 0 {
2196 // Remove the top margin on the first element.
2197 div.style().refine(&StyleRefinement {
2198 margin: gpui::EdgesRefinement {
2199 top: Some(Length::Definite(px(0.).into())),
2200 left: None,
2201 right: None,
2202 bottom: None,
2203 },
2204 ..Default::default()
2205 });
2206 }
2207
2208 if range.end == markdown_end {
2209 div.style().refine(&StyleRefinement {
2210 margin: gpui::EdgesRefinement {
2211 top: None,
2212 left: None,
2213 right: None,
2214 bottom: Some(Length::Definite(rems(0.).into())),
2215 },
2216 ..Default::default()
2217 });
2218 }
2219
2220 self.div_stack.push(div);
2221 }
2222
2223 fn push_root_block(&mut self, range: &Range<usize>, markdown_end: usize) {
2224 self.push_div(
2225 div().group("markdown-root-block").relative(),
2226 range,
2227 markdown_end,
2228 );
2229 self.push_div(div().pl_4(), range, markdown_end);
2230 }
2231
2232 fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
2233 self.flush_text();
2234 if let Some(div) = self.div_stack.pop() {
2235 self.div_stack.push(f(div));
2236 }
2237 }
2238
2239 fn pop_root_block(
2240 &mut self,
2241 is_active: bool,
2242 active_gutter_color: Hsla,
2243 hovered_gutter_color: Hsla,
2244 ) {
2245 self.pop_div();
2246 self.modify_current_div(|el| {
2247 el.child(
2248 div()
2249 .h_full()
2250 .w(px(4.0))
2251 .when(is_active, |this| this.bg(active_gutter_color))
2252 .group_hover("markdown-root-block", |this| {
2253 if is_active {
2254 this
2255 } else {
2256 this.bg(hovered_gutter_color)
2257 }
2258 })
2259 .rounded_xs()
2260 .absolute()
2261 .left_0()
2262 .top_0(),
2263 )
2264 });
2265 self.pop_div();
2266 }
2267
2268 fn pop_div(&mut self) {
2269 self.flush_text();
2270 let div = self.div_stack.pop().unwrap().into_any_element();
2271 self.div_stack.last_mut().unwrap().extend(iter::once(div));
2272 }
2273
2274 fn push_sourced_element(&mut self, source_range: Range<usize>, element: impl Into<AnyElement>) {
2275 self.flush_text();
2276 let anchor = self.render_source_anchor(source_range);
2277 self.div_stack.last_mut().unwrap().extend([{
2278 div()
2279 .relative()
2280 .child(anchor)
2281 .child(element.into())
2282 .into_any_element()
2283 }]);
2284 }
2285
2286 fn push_list(&mut self, bullet_index: Option<u64>) {
2287 self.list_stack.push(ListStackEntry { bullet_index });
2288 }
2289
2290 fn next_bullet_index(&mut self) -> Option<u64> {
2291 self.list_stack.last_mut().and_then(|entry| {
2292 let item_index = entry.bullet_index.as_mut()?;
2293 *item_index += 1;
2294 Some(*item_index - 1)
2295 })
2296 }
2297
2298 fn pop_list(&mut self) {
2299 self.list_stack.pop();
2300 }
2301
2302 fn push_code_block(&mut self, language: Option<Arc<Language>>) {
2303 self.code_block_stack.push(language);
2304 }
2305
2306 fn pop_code_block(&mut self) {
2307 self.code_block_stack.pop();
2308 }
2309
2310 fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
2311 self.rendered_links.push(RenderedLink {
2312 source_range,
2313 destination_url,
2314 });
2315 }
2316
2317 fn push_text(&mut self, text: &str, source_range: Range<usize>) {
2318 self.pending_line.source_mappings.push(SourceMapping {
2319 rendered_index: self.pending_line.text.len(),
2320 source_index: source_range.start,
2321 });
2322 self.pending_line.text.push_str(text);
2323 self.current_source_index = source_range.end;
2324
2325 if let Some(Some(language)) = self.code_block_stack.last() {
2326 let mut offset = 0;
2327 for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
2328 if range.start > offset {
2329 self.pending_line
2330 .runs
2331 .push(self.text_style().to_run(range.start - offset));
2332 }
2333
2334 let mut run_style = self.text_style();
2335 if let Some(highlight) = self.syntax_theme.get(highlight_id).cloned() {
2336 run_style = run_style.highlight(highlight);
2337 }
2338
2339 self.pending_line.runs.push(run_style.to_run(range.len()));
2340 offset = range.end;
2341 }
2342
2343 if offset < text.len() {
2344 self.pending_line
2345 .runs
2346 .push(self.text_style().to_run(text.len() - offset));
2347 }
2348 } else {
2349 self.pending_line
2350 .runs
2351 .push(self.text_style().to_run(text.len()));
2352 }
2353 }
2354
2355 fn trim_trailing_newline(&mut self) {
2356 if self.pending_line.text.ends_with('\n') {
2357 self.pending_line
2358 .text
2359 .truncate(self.pending_line.text.len() - 1);
2360 self.pending_line.runs.last_mut().unwrap().len -= 1;
2361 self.current_source_index -= 1;
2362 }
2363 }
2364
2365 fn replace_pending_checkbox(&mut self, source_range: &Range<usize>) {
2366 let trimmed = self.pending_line.text.trim();
2367 if trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]" {
2368 let checked = trimmed != "[ ]";
2369 self.pending_line = PendingLine::default();
2370 let checkbox = Checkbox::new(
2371 ElementId::Name(
2372 format!("table_checkbox_{}_{}", source_range.start, source_range.end).into(),
2373 ),
2374 if checked {
2375 ToggleState::Selected
2376 } else {
2377 ToggleState::Unselected
2378 },
2379 )
2380 .fill()
2381 .visualization_only(true)
2382 .into_any_element();
2383 self.div_stack.last_mut().unwrap().extend([checkbox]);
2384 }
2385 }
2386
2387 fn render_source_anchor(&mut self, source_range: Range<usize>) -> AnyElement {
2388 let mut text_style = self.base_text_style.clone();
2389 text_style.color = Hsla::transparent_black();
2390 let text = "\u{200B}";
2391 let styled_text = StyledText::new(text).with_runs(vec![text_style.to_run(text.len())]);
2392 self.rendered_lines.push(RenderedLine {
2393 layout: styled_text.layout().clone(),
2394 source_mappings: vec![SourceMapping {
2395 rendered_index: 0,
2396 source_index: source_range.start,
2397 }],
2398 source_end: source_range.end,
2399 language: None,
2400 });
2401 div()
2402 .absolute()
2403 .top_0()
2404 .left_0()
2405 .opacity(0.)
2406 .child(styled_text)
2407 .into_any_element()
2408 }
2409
2410 fn flush_text(&mut self) {
2411 let line = mem::take(&mut self.pending_line);
2412 if line.text.is_empty() {
2413 return;
2414 }
2415
2416 let text = StyledText::new(line.text).with_runs(line.runs);
2417 self.rendered_lines.push(RenderedLine {
2418 layout: text.layout().clone(),
2419 source_mappings: line.source_mappings,
2420 source_end: self.current_source_index,
2421 language: self.code_block_stack.last().cloned().flatten(),
2422 });
2423 self.div_stack.last_mut().unwrap().extend([text.into_any()]);
2424 }
2425
2426 fn build(mut self) -> RenderedMarkdown {
2427 debug_assert_eq!(self.div_stack.len(), 1);
2428 self.flush_text();
2429 RenderedMarkdown {
2430 element: self.div_stack.pop().unwrap().into_any_element(),
2431 text: RenderedText {
2432 lines: self.rendered_lines.into(),
2433 links: self.rendered_links.into(),
2434 },
2435 }
2436 }
2437}
2438
2439struct RenderedLine {
2440 layout: TextLayout,
2441 source_mappings: Vec<SourceMapping>,
2442 source_end: usize,
2443 language: Option<Arc<Language>>,
2444}
2445
2446impl RenderedLine {
2447 fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
2448 if source_index >= self.source_end {
2449 return self.layout.len();
2450 }
2451
2452 let mapping = match self
2453 .source_mappings
2454 .binary_search_by_key(&source_index, |probe| probe.source_index)
2455 {
2456 Ok(ix) => &self.source_mappings[ix],
2457 Err(ix) => &self.source_mappings[ix - 1],
2458 };
2459 (mapping.rendered_index + (source_index - mapping.source_index)).min(self.layout.len())
2460 }
2461
2462 fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
2463 if rendered_index >= self.layout.len() {
2464 return self.source_end;
2465 }
2466
2467 let mapping = match self
2468 .source_mappings
2469 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
2470 {
2471 Ok(ix) => &self.source_mappings[ix],
2472 Err(ix) => &self.source_mappings[ix - 1],
2473 };
2474 mapping.source_index + (rendered_index - mapping.rendered_index)
2475 }
2476
2477 /// Returns the source index for use as an exclusive range end at a word/selection boundary.
2478 /// When the rendered index is exactly at the start of a segment with a gap from the previous
2479 /// segment (e.g., after stripped markdown syntax like backticks), this returns the end of the
2480 /// previous segment rather than the start of the current one.
2481 fn source_index_for_exclusive_rendered_end(&self, rendered_index: usize) -> usize {
2482 if rendered_index >= self.layout.len() {
2483 return self.source_end;
2484 }
2485
2486 let ix = match self
2487 .source_mappings
2488 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
2489 {
2490 Ok(ix) => ix,
2491 Err(ix) => {
2492 return self.source_mappings[ix - 1].source_index
2493 + (rendered_index - self.source_mappings[ix - 1].rendered_index);
2494 }
2495 };
2496
2497 // Exact match at the start of a segment. Check if there's a gap from the previous segment.
2498 if ix > 0 {
2499 let prev_mapping = &self.source_mappings[ix - 1];
2500 let mapping = &self.source_mappings[ix];
2501 let prev_segment_len = mapping.rendered_index - prev_mapping.rendered_index;
2502 let prev_source_end = prev_mapping.source_index + prev_segment_len;
2503 if prev_source_end < mapping.source_index {
2504 return prev_source_end;
2505 }
2506 }
2507
2508 self.source_mappings[ix].source_index
2509 }
2510
2511 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
2512 let line_rendered_index;
2513 let out_of_bounds;
2514 match self.layout.index_for_position(position) {
2515 Ok(ix) => {
2516 line_rendered_index = ix;
2517 out_of_bounds = false;
2518 }
2519 Err(ix) => {
2520 line_rendered_index = ix;
2521 out_of_bounds = true;
2522 }
2523 };
2524 let source_index = self.source_index_for_rendered_index(line_rendered_index);
2525 if out_of_bounds {
2526 Err(source_index)
2527 } else {
2528 Ok(source_index)
2529 }
2530 }
2531}
2532
2533#[derive(Copy, Clone, Debug, Default)]
2534struct SourceMapping {
2535 rendered_index: usize,
2536 source_index: usize,
2537}
2538
2539pub struct RenderedMarkdown {
2540 element: AnyElement,
2541 text: RenderedText,
2542}
2543
2544#[derive(Clone)]
2545struct RenderedText {
2546 lines: Rc<[RenderedLine]>,
2547 links: Rc<[RenderedLink]>,
2548}
2549
2550#[derive(Debug, Clone, Eq, PartialEq)]
2551struct RenderedLink {
2552 source_range: Range<usize>,
2553 destination_url: SharedString,
2554}
2555
2556impl RenderedText {
2557 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
2558 let mut lines = self.lines.iter().peekable();
2559 let mut fallback_line: Option<&RenderedLine> = None;
2560
2561 while let Some(line) = lines.next() {
2562 let line_bounds = line.layout.bounds();
2563
2564 // Exact match: position is within bounds (handles overlapping bounds like table columns)
2565 if line_bounds.contains(&position) {
2566 return line.source_index_for_position(position);
2567 }
2568
2569 // Track fallback for Y-coordinate based matching
2570 if position.y <= line_bounds.bottom() && fallback_line.is_none() {
2571 fallback_line = Some(line);
2572 }
2573
2574 // Handle gap between lines
2575 if position.y > line_bounds.bottom() {
2576 if let Some(next_line) = lines.peek()
2577 && position.y < next_line.layout.bounds().top()
2578 {
2579 return Err(line.source_end);
2580 }
2581 }
2582 }
2583
2584 // Fall back to Y-coordinate matched line
2585 if let Some(line) = fallback_line {
2586 return line.source_index_for_position(position);
2587 }
2588
2589 Err(self.lines.last().map_or(0, |line| line.source_end))
2590 }
2591
2592 fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
2593 for line in self.lines.iter() {
2594 let line_source_start = line.source_mappings.first().unwrap().source_index;
2595 if source_index < line_source_start {
2596 break;
2597 } else if source_index > line.source_end {
2598 continue;
2599 } else {
2600 let line_height = line.layout.line_height();
2601 let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
2602 let position = line.layout.position_for_index(rendered_index_within_line)?;
2603 return Some((position, line_height));
2604 }
2605 }
2606 None
2607 }
2608
2609 fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
2610 for line in self.lines.iter() {
2611 if source_index > line.source_end {
2612 continue;
2613 }
2614
2615 let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
2616 let rendered_index_in_line =
2617 line.rendered_index_for_source_index(source_index) - line_rendered_start;
2618 let text = line.layout.text();
2619
2620 let scope = line.language.as_ref().map(|l| l.default_scope());
2621 let classifier = CharClassifier::new(scope);
2622
2623 let mut prev_chars = text[..rendered_index_in_line].chars().rev().peekable();
2624 let mut next_chars = text[rendered_index_in_line..].chars().peekable();
2625
2626 let word_kind = std::cmp::max(
2627 prev_chars.peek().map(|&c| classifier.kind(c)),
2628 next_chars.peek().map(|&c| classifier.kind(c)),
2629 );
2630
2631 let mut start = rendered_index_in_line;
2632 for c in prev_chars {
2633 if Some(classifier.kind(c)) == word_kind {
2634 start -= c.len_utf8();
2635 } else {
2636 break;
2637 }
2638 }
2639
2640 let mut end = rendered_index_in_line;
2641 for c in next_chars {
2642 if Some(classifier.kind(c)) == word_kind {
2643 end += c.len_utf8();
2644 } else {
2645 break;
2646 }
2647 }
2648
2649 return line.source_index_for_rendered_index(line_rendered_start + start)
2650 ..line.source_index_for_exclusive_rendered_end(line_rendered_start + end);
2651 }
2652
2653 source_index..source_index
2654 }
2655
2656 fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
2657 for line in self.lines.iter() {
2658 if source_index > line.source_end {
2659 continue;
2660 }
2661 let line_source_start = line.source_mappings.first().unwrap().source_index;
2662 return line_source_start..line.source_end;
2663 }
2664
2665 source_index..source_index
2666 }
2667
2668 fn text_for_range(&self, range: Range<usize>) -> String {
2669 let mut accumulator = String::new();
2670
2671 for line in self.lines.iter() {
2672 if range.start > line.source_end {
2673 continue;
2674 }
2675 let line_source_start = line.source_mappings.first().unwrap().source_index;
2676 if range.end < line_source_start {
2677 break;
2678 }
2679
2680 let text = line.layout.text();
2681
2682 let start = if range.start < line_source_start {
2683 0
2684 } else {
2685 line.rendered_index_for_source_index(range.start)
2686 };
2687 let end = if range.end > line.source_end {
2688 line.rendered_index_for_source_index(line.source_end)
2689 } else {
2690 line.rendered_index_for_source_index(range.end)
2691 }
2692 .min(text.len());
2693
2694 accumulator.push_str(&text[start..end]);
2695 accumulator.push('\n');
2696 }
2697 // Remove trailing newline
2698 accumulator.pop();
2699 accumulator
2700 }
2701
2702 fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
2703 let source_index = self.source_index_for_position(position).ok()?;
2704 self.links
2705 .iter()
2706 .find(|link| link.source_range.contains(&source_index))
2707 }
2708}
2709
2710#[cfg(test)]
2711mod tests {
2712 use super::*;
2713 use gpui::{TestAppContext, size};
2714 use language::{Language, LanguageConfig, LanguageMatcher};
2715 use std::sync::Arc;
2716
2717 fn ensure_theme_initialized(cx: &mut TestAppContext) {
2718 cx.update(|cx| {
2719 if !cx.has_global::<settings::SettingsStore>() {
2720 settings::init(cx);
2721 }
2722 if !cx.has_global::<theme::GlobalTheme>() {
2723 theme_settings::init(theme::LoadThemes::JustBase, cx);
2724 }
2725 });
2726 }
2727
2728 #[gpui::test]
2729 fn test_mappings(cx: &mut TestAppContext) {
2730 // Formatting.
2731 assert_mappings(
2732 &render_markdown("He*l*lo", cx),
2733 vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
2734 );
2735
2736 // Multiple lines.
2737 assert_mappings(
2738 &render_markdown("Hello\n\nWorld", cx),
2739 vec![
2740 vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
2741 vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
2742 ],
2743 );
2744
2745 // Multi-byte characters.
2746 assert_mappings(
2747 &render_markdown("αβγ\n\nδεζ", cx),
2748 vec![
2749 vec![(0, 0), (2, 2), (4, 4), (6, 6)],
2750 vec![(0, 8), (2, 10), (4, 12), (6, 14)],
2751 ],
2752 );
2753
2754 // Smart quotes.
2755 assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
2756 assert_mappings(
2757 &render_markdown("\"hey\"", cx),
2758 vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
2759 );
2760
2761 // HTML Comments are ignored
2762 assert_mappings(
2763 &render_markdown(
2764 "<!--\nrdoc-file=string.c\n- str.intern -> symbol\n- str.to_sym -> symbol\n-->\nReturns",
2765 cx,
2766 ),
2767 vec![vec![
2768 (0, 78),
2769 (1, 79),
2770 (2, 80),
2771 (3, 81),
2772 (4, 82),
2773 (5, 83),
2774 (6, 84),
2775 ]],
2776 );
2777 }
2778
2779 fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
2780 render_markdown_with_language_registry(markdown, None, cx)
2781 }
2782
2783 fn render_markdown_with_language_registry(
2784 markdown: &str,
2785 language_registry: Option<Arc<LanguageRegistry>>,
2786 cx: &mut TestAppContext,
2787 ) -> RenderedText {
2788 render_markdown_with_options(markdown, language_registry, MarkdownOptions::default(), cx)
2789 }
2790
2791 fn render_markdown_with_options(
2792 markdown: &str,
2793 language_registry: Option<Arc<LanguageRegistry>>,
2794 options: MarkdownOptions,
2795 cx: &mut TestAppContext,
2796 ) -> RenderedText {
2797 struct TestWindow;
2798
2799 impl Render for TestWindow {
2800 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2801 div()
2802 }
2803 }
2804
2805 ensure_theme_initialized(cx);
2806
2807 let (_, cx) = cx.add_window_view(|_, _| TestWindow);
2808 let markdown = cx.new(|cx| {
2809 Markdown::new_with_options(
2810 markdown.to_string().into(),
2811 language_registry,
2812 None,
2813 options,
2814 cx,
2815 )
2816 });
2817 cx.run_until_parked();
2818 let (rendered, _) = cx.draw(
2819 Default::default(),
2820 size(px(600.0), px(600.0)),
2821 |_window, _cx| {
2822 MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
2823 CodeBlockRenderer::Default {
2824 copy_button_visibility: CopyButtonVisibility::Hidden,
2825 border: false,
2826 },
2827 )
2828 },
2829 );
2830 rendered.text
2831 }
2832
2833 #[gpui::test]
2834 fn test_surrounding_word_range(cx: &mut TestAppContext) {
2835 let rendered = render_markdown("Hello world tesεζ", cx);
2836
2837 // Test word selection for "Hello"
2838 let word_range = rendered.surrounding_word_range(2); // Simulate click on 'l' in "Hello"
2839 let selected_text = rendered.text_for_range(word_range);
2840 assert_eq!(selected_text, "Hello");
2841
2842 // Test word selection for "world"
2843 let word_range = rendered.surrounding_word_range(7); // Simulate click on 'o' in "world"
2844 let selected_text = rendered.text_for_range(word_range);
2845 assert_eq!(selected_text, "world");
2846
2847 // Test word selection for "tesεζ"
2848 let word_range = rendered.surrounding_word_range(14); // Simulate click on 's' in "tesεζ"
2849 let selected_text = rendered.text_for_range(word_range);
2850 assert_eq!(selected_text, "tesεζ");
2851
2852 // Test word selection at word boundary (space)
2853 let word_range = rendered.surrounding_word_range(5); // Simulate click on space between "Hello" and "world", expect highlighting word to the left
2854 let selected_text = rendered.text_for_range(word_range);
2855 assert_eq!(selected_text, "Hello");
2856 }
2857
2858 #[gpui::test]
2859 fn test_surrounding_line_range(cx: &mut TestAppContext) {
2860 let rendered = render_markdown("First line\n\nSecond line\n\nThird lineεζ", cx);
2861
2862 // Test getting line range for first line
2863 let line_range = rendered.surrounding_line_range(5); // Simulate click somewhere in first line
2864 let selected_text = rendered.text_for_range(line_range);
2865 assert_eq!(selected_text, "First line");
2866
2867 // Test getting line range for second line
2868 let line_range = rendered.surrounding_line_range(13); // Simulate click at beginning in second line
2869 let selected_text = rendered.text_for_range(line_range);
2870 assert_eq!(selected_text, "Second line");
2871
2872 // Test getting line range for third line
2873 let line_range = rendered.surrounding_line_range(37); // Simulate click at end of third line with multi-byte chars
2874 let selected_text = rendered.text_for_range(line_range);
2875 assert_eq!(selected_text, "Third lineεζ");
2876 }
2877
2878 #[gpui::test]
2879 fn test_selection_head_movement(cx: &mut TestAppContext) {
2880 let rendered = render_markdown("Hello world test", cx);
2881
2882 let mut selection = Selection {
2883 start: 5,
2884 end: 5,
2885 reversed: false,
2886 pending: false,
2887 mode: SelectMode::Character,
2888 };
2889
2890 // Test forward selection
2891 selection.set_head(10, &rendered);
2892 assert_eq!(selection.start, 5);
2893 assert_eq!(selection.end, 10);
2894 assert!(!selection.reversed);
2895 assert_eq!(selection.tail(), 5);
2896
2897 // Test backward selection
2898 selection.set_head(2, &rendered);
2899 assert_eq!(selection.start, 2);
2900 assert_eq!(selection.end, 5);
2901 assert!(selection.reversed);
2902 assert_eq!(selection.tail(), 5);
2903
2904 // Test forward selection again from reversed state
2905 selection.set_head(15, &rendered);
2906 assert_eq!(selection.start, 5);
2907 assert_eq!(selection.end, 15);
2908 assert!(!selection.reversed);
2909 assert_eq!(selection.tail(), 5);
2910 }
2911
2912 #[gpui::test]
2913 fn test_word_selection_drag(cx: &mut TestAppContext) {
2914 let rendered = render_markdown("Hello world test", cx);
2915
2916 // Start with a simulated double-click on "world" (index 6-10)
2917 let word_range = rendered.surrounding_word_range(7); // Click on 'o' in "world"
2918 let mut selection = Selection {
2919 start: word_range.start,
2920 end: word_range.end,
2921 reversed: false,
2922 pending: true,
2923 mode: SelectMode::Word(word_range),
2924 };
2925
2926 // Drag forward to "test" - should expand selection to include "test"
2927 selection.set_head(13, &rendered); // Index in "test"
2928 assert_eq!(selection.start, 6); // Start of "world"
2929 assert_eq!(selection.end, 16); // End of "test"
2930 assert!(!selection.reversed);
2931 let selected_text = rendered.text_for_range(selection.start..selection.end);
2932 assert_eq!(selected_text, "world test");
2933
2934 // Drag backward to "Hello" - should expand selection to include "Hello"
2935 selection.set_head(2, &rendered); // Index in "Hello"
2936 assert_eq!(selection.start, 0); // Start of "Hello"
2937 assert_eq!(selection.end, 11); // End of "world" (original selection)
2938 assert!(selection.reversed);
2939 let selected_text = rendered.text_for_range(selection.start..selection.end);
2940 assert_eq!(selected_text, "Hello world");
2941
2942 // Drag back within original word - should revert to original selection
2943 selection.set_head(8, &rendered); // Back within "world"
2944 assert_eq!(selection.start, 6); // Start of "world"
2945 assert_eq!(selection.end, 11); // End of "world"
2946 assert!(!selection.reversed);
2947 let selected_text = rendered.text_for_range(selection.start..selection.end);
2948 assert_eq!(selected_text, "world");
2949 }
2950
2951 #[gpui::test]
2952 fn test_selection_with_markdown_formatting(cx: &mut TestAppContext) {
2953 let rendered = render_markdown(
2954 "This is **bold** text, this is *italic* text, use `code` here",
2955 cx,
2956 );
2957 let word_range = rendered.surrounding_word_range(10); // Inside "bold"
2958 let selected_text = rendered.text_for_range(word_range);
2959 assert_eq!(selected_text, "bold");
2960
2961 let word_range = rendered.surrounding_word_range(32); // Inside "italic"
2962 let selected_text = rendered.text_for_range(word_range);
2963 assert_eq!(selected_text, "italic");
2964
2965 let word_range = rendered.surrounding_word_range(51); // Inside "code"
2966 let selected_text = rendered.text_for_range(word_range);
2967 assert_eq!(selected_text, "code");
2968 }
2969
2970 #[gpui::test]
2971 fn test_table_column_selection(cx: &mut TestAppContext) {
2972 let rendered = render_markdown("| a | b |\n|---|---|\n| c | d |", cx);
2973
2974 assert!(rendered.lines.len() >= 2);
2975 let first_bounds = rendered.lines[0].layout.bounds();
2976 let second_bounds = rendered.lines[1].layout.bounds();
2977
2978 let first_index = match rendered.source_index_for_position(first_bounds.center()) {
2979 Ok(index) | Err(index) => index,
2980 };
2981 let second_index = match rendered.source_index_for_position(second_bounds.center()) {
2982 Ok(index) | Err(index) => index,
2983 };
2984
2985 let first_word = rendered.text_for_range(rendered.surrounding_word_range(first_index));
2986 let second_word = rendered.text_for_range(rendered.surrounding_word_range(second_index));
2987
2988 assert_eq!(first_word, "a");
2989 assert_eq!(second_word, "b");
2990 }
2991
2992 #[test]
2993 fn test_table_checkbox_detection() {
2994 let md = "| Done |\n|------|\n| [x] |\n| [ ] |";
2995 let events = crate::parser::parse_markdown_with_options(md, false).events;
2996
2997 let mut in_table = false;
2998 let mut cell_texts: Vec<String> = Vec::new();
2999 let mut current_cell = String::new();
3000
3001 for (range, event) in &events {
3002 match event {
3003 MarkdownEvent::Start(MarkdownTag::Table(_)) => in_table = true,
3004 MarkdownEvent::End(MarkdownTagEnd::Table) => in_table = false,
3005 MarkdownEvent::Start(MarkdownTag::TableCell) => current_cell.clear(),
3006 MarkdownEvent::End(MarkdownTagEnd::TableCell) => {
3007 if in_table {
3008 cell_texts.push(current_cell.clone());
3009 }
3010 }
3011 MarkdownEvent::Text if in_table => {
3012 current_cell.push_str(&md[range.clone()]);
3013 }
3014 _ => {}
3015 }
3016 }
3017
3018 let checkbox_cells: Vec<&String> = cell_texts
3019 .iter()
3020 .filter(|t| {
3021 let trimmed = t.trim();
3022 trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]"
3023 })
3024 .collect();
3025 assert_eq!(
3026 checkbox_cells.len(),
3027 2,
3028 "Expected 2 checkbox cells, got: {cell_texts:?}"
3029 );
3030 assert_eq!(checkbox_cells[0].trim(), "[x]");
3031 assert_eq!(checkbox_cells[1].trim(), "[ ]");
3032 }
3033
3034 #[gpui::test]
3035 fn test_inline_code_word_selection_excludes_backticks(cx: &mut TestAppContext) {
3036 // Test that double-clicking on inline code selects just the code content,
3037 // not the backticks. This verifies the fix for the bug where selecting
3038 // inline code would include the trailing backtick.
3039 let rendered = render_markdown("use `blah` here", cx);
3040
3041 // Source layout: "use `blah` here"
3042 // 0123456789...
3043 // The inline code "blah" is at source positions 5-8 (content range 5..9)
3044
3045 // Click inside "blah" - should select just "blah", not "blah`"
3046 let word_range = rendered.surrounding_word_range(6); // 'l' in "blah"
3047
3048 // text_for_range extracts from the rendered text (without backticks), so it
3049 // would return "blah" even with a wrong source range. We check it anyway.
3050 let selected_text = rendered.text_for_range(word_range.clone());
3051 assert_eq!(selected_text, "blah");
3052
3053 // The source range is what matters for copy_as_markdown and selected_text,
3054 // which extract directly from the source. With the bug, this would be 5..10
3055 // which includes the closing backtick at position 9.
3056 assert_eq!(word_range, 5..9);
3057 }
3058
3059 #[gpui::test]
3060 fn test_surrounding_word_range_respects_word_characters(cx: &mut TestAppContext) {
3061 let rendered = render_markdown("foo.bar() baz", cx);
3062
3063 // Double clicking on 'f' in "foo" - should select just "foo"
3064 let word_range = rendered.surrounding_word_range(0);
3065 let selected_text = rendered.text_for_range(word_range);
3066 assert_eq!(selected_text, "foo");
3067
3068 // Double clicking on 'b' in "bar" - should select just "bar"
3069 let word_range = rendered.surrounding_word_range(4);
3070 let selected_text = rendered.text_for_range(word_range);
3071 assert_eq!(selected_text, "bar");
3072
3073 // Double clicking on 'b' in "baz" - should select "baz"
3074 let word_range = rendered.surrounding_word_range(10);
3075 let selected_text = rendered.text_for_range(word_range);
3076 assert_eq!(selected_text, "baz");
3077
3078 // Double clicking selects word characters in code blocks
3079 let javascript_language = Arc::new(Language::new(
3080 LanguageConfig {
3081 name: "JavaScript".into(),
3082 matcher: LanguageMatcher {
3083 path_suffixes: vec!["js".to_string()],
3084 ..Default::default()
3085 },
3086 word_characters: ['$', '#'].into_iter().collect(),
3087 ..Default::default()
3088 },
3089 None,
3090 ));
3091
3092 let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
3093 language_registry.add(javascript_language);
3094
3095 let rendered = render_markdown_with_language_registry(
3096 "```javascript\n$foo #bar\n```",
3097 Some(language_registry),
3098 cx,
3099 );
3100
3101 let word_range = rendered.surrounding_word_range(14);
3102 let selected_text = rendered.text_for_range(word_range);
3103 assert_eq!(selected_text, "$foo");
3104
3105 let word_range = rendered.surrounding_word_range(19);
3106 let selected_text = rendered.text_for_range(word_range);
3107 assert_eq!(selected_text, "#bar");
3108 }
3109
3110 #[gpui::test]
3111 fn test_all_selection(cx: &mut TestAppContext) {
3112 let rendered = render_markdown("Hello world\n\nThis is a test\n\nwith multiple lines", cx);
3113
3114 let total_length = rendered
3115 .lines
3116 .last()
3117 .map(|line| line.source_end)
3118 .unwrap_or(0);
3119
3120 let mut selection = Selection {
3121 start: 0,
3122 end: total_length,
3123 reversed: false,
3124 pending: true,
3125 mode: SelectMode::All,
3126 };
3127
3128 selection.set_head(5, &rendered); // Try to set head in middle
3129 assert_eq!(selection.start, 0);
3130 assert_eq!(selection.end, total_length);
3131 assert!(!selection.reversed);
3132
3133 selection.set_head(25, &rendered); // Try to set head near end
3134 assert_eq!(selection.start, 0);
3135 assert_eq!(selection.end, total_length);
3136 assert!(!selection.reversed);
3137
3138 let selected_text = rendered.text_for_range(selection.start..selection.end);
3139 assert_eq!(
3140 selected_text,
3141 "Hello world\nThis is a test\nwith multiple lines"
3142 );
3143 }
3144
3145 fn nbsp(n: usize) -> String {
3146 "\u{00A0}".repeat(n)
3147 }
3148
3149 #[test]
3150 fn test_escape_plain_text() {
3151 assert_eq!(Markdown::escape("hello world"), "hello world");
3152 assert_eq!(Markdown::escape(""), "");
3153 assert_eq!(Markdown::escape("café ☕ naïve"), "café ☕ naïve");
3154 }
3155
3156 #[test]
3157 fn test_escape_punctuation() {
3158 assert_eq!(Markdown::escape("hello `world`"), r"hello \`world\`");
3159 assert_eq!(Markdown::escape("a|b"), r"a\|b");
3160 }
3161
3162 #[test]
3163 fn test_escape_leading_spaces() {
3164 assert_eq!(Markdown::escape(" hello"), [ (4), "hello"].concat());
3165 assert_eq!(
3166 Markdown::escape(" | { a: string }"),
3167 [ (4), r"\| \{ a\: string \}"].concat()
3168 );
3169 assert_eq!(
3170 Markdown::escape(" first\n second"),
3171 [ (2), "first\n\n",  (2), "second"].concat()
3172 );
3173 assert_eq!(Markdown::escape("hello world"), "hello world");
3174 }
3175
3176 #[test]
3177 fn test_escape_leading_tabs() {
3178 assert_eq!(Markdown::escape("\thello"), [ (4), "hello"].concat());
3179 assert_eq!(
3180 Markdown::escape("hello\n\t\tindented"),
3181 ["hello\n\n",  (8), "indented"].concat()
3182 );
3183 assert_eq!(
3184 Markdown::escape(" \t hello"),
3185 [ (1 + 4 + 1), "hello"].concat()
3186 );
3187 assert_eq!(Markdown::escape("hello\tworld"), "hello\tworld");
3188 }
3189
3190 #[test]
3191 fn test_escape_newlines() {
3192 assert_eq!(Markdown::escape("a\nb"), "a\n\nb");
3193 assert_eq!(Markdown::escape("a\n\nb"), "a\n\n\n\nb");
3194 assert_eq!(Markdown::escape("\nhello"), "\n\nhello");
3195 }
3196
3197 #[test]
3198 fn test_escape_multiline_diagnostic() {
3199 assert_eq!(
3200 Markdown::escape(" | { a: string }\n | { b: number }"),
3201 [
3202  (4),
3203 r"\| \{ a\: string \}",
3204 "\n\n",
3205  (4),
3206 r"\| \{ b\: number \}",
3207 ]
3208 .concat()
3209 );
3210 }
3211
3212 fn has_code_block(markdown: &str) -> bool {
3213 let parsed_data = parse_markdown_with_options(markdown, false);
3214 parsed_data
3215 .events
3216 .iter()
3217 .any(|(_, event)| matches!(event, MarkdownEvent::Start(MarkdownTag::CodeBlock { .. })))
3218 }
3219
3220 #[test]
3221 fn test_escape_output_len_matches_precomputed() {
3222 let cases = [
3223 "",
3224 "hello world",
3225 "hello `world`",
3226 " hello",
3227 " | { a: string }",
3228 "\thello",
3229 "hello\n\t\tindented",
3230 " \t hello",
3231 "hello\tworld",
3232 "a\nb",
3233 "a\n\nb",
3234 "\nhello",
3235 " | { a: string }\n | { b: number }",
3236 "café ☕ naïve",
3237 ];
3238 for input in cases {
3239 let mut escaper = MarkdownEscaper::new();
3240 let precomputed: usize = input.bytes().map(|b| escaper.next(b).output_len()).sum();
3241
3242 let mut escaper = MarkdownEscaper::new();
3243 let mut output = String::new();
3244 for c in input.chars() {
3245 escaper.next(c as u8).write_to(c, &mut output);
3246 }
3247
3248 assert_eq!(precomputed, output.len(), "length mismatch for {:?}", input);
3249 }
3250 }
3251
3252 #[test]
3253 fn test_escape_prevents_code_block() {
3254 let diagnostic = " | { a: string }";
3255 assert!(has_code_block(diagnostic));
3256 assert!(!has_code_block(&Markdown::escape(diagnostic)));
3257 }
3258
3259 #[track_caller]
3260 fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
3261 assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
3262 for (line_ix, line_mappings) in expected.into_iter().enumerate() {
3263 let line = &rendered.lines[line_ix];
3264
3265 assert!(
3266 line.source_mappings.windows(2).all(|mappings| {
3267 mappings[0].source_index < mappings[1].source_index
3268 && mappings[0].rendered_index < mappings[1].rendered_index
3269 }),
3270 "line {} has duplicate mappings: {:?}",
3271 line_ix,
3272 line.source_mappings
3273 );
3274
3275 for (rendered_ix, source_ix) in line_mappings {
3276 assert_eq!(
3277 line.source_index_for_rendered_index(rendered_ix),
3278 source_ix,
3279 "line {}, rendered_ix {}",
3280 line_ix,
3281 rendered_ix
3282 );
3283
3284 assert_eq!(
3285 line.rendered_index_for_source_index(source_ix),
3286 rendered_ix,
3287 "line {}, source_ix {}",
3288 line_ix,
3289 source_ix
3290 );
3291 }
3292 }
3293 }
3294}