1use std::borrow::Cow;
33use std::collections::BTreeMap;
34use std::ops::Range;
35use std::sync::{Arc, LazyLock};
36
37use collections::{HashMap, HashSet};
38use gpui::{
39 App, AppContext as _, ClipboardItem, Context, Div, FocusHandle, Focusable, Image, Pixels,
40 Point, ScrollHandle, SharedString, Task, Window, actions, point,
41};
42use language::{DefaultLanguageRegistry, Language, LanguageRegistry};
43
44use crate::element::AnyDiv;
45use crate::parser::{
46 CodeBlockKind, CodeBlockMetadata, MarkdownEvent, MarkdownTag, ParsedMetadataBlock,
47 parse_links_only, parse_markdown_with_options,
48};
49use crate::rendered::{RenderedFootnoteRef, RenderedLink, RenderedText};
50
51actions!(
52 markdown,
53 [
54 Copy,
56 CopyAsMarkdown
58 ]
59);
60
61static LANGUAGES: LazyLock<DefaultLanguageRegistry> = LazyLock::new(DefaultLanguageRegistry::new);
64
65fn language_name_to_extension(name: &str) -> Option<&'static str> {
70 const ALIASES: &[(&str, &str)] = &[
71 ("rust", "rs"),
72 ("rs", "rs"),
73 ("javascript", "js"),
74 ("js", "js"),
75 ("jsx", "js"),
76 ("mjs", "js"),
77 ("cjs", "js"),
78 ("typescript", "ts"),
79 ("ts", "ts"),
80 ("mts", "ts"),
81 ("cts", "ts"),
82 ("tsx", "tsx"),
83 ("json", "json"),
84 ("json5", "json"),
85 ("jsonc", "json"),
86 ("markdown", "md"),
87 ("md", "md"),
88 ];
89 ALIASES
90 .iter()
91 .find(|(alias, _)| name.eq_ignore_ascii_case(alias))
92 .map(|(_, extension)| *extension)
93}
94
95pub(crate) fn resolve_code_block_language(kind: &CodeBlockKind) -> Option<&'static Language> {
99 let extension = match kind {
100 CodeBlockKind::FencedLang(name) => language_name_to_extension(name.as_ref())?,
101 CodeBlockKind::FencedSrc(path_range) => std::path::Path::new(path_range.path.as_ref())
102 .extension()
103 .and_then(|extension| extension.to_str())?,
104 CodeBlockKind::Fenced | CodeBlockKind::Indented => return None,
105 };
106 LANGUAGES.language_for_extension(extension)
107}
108
109pub struct Markdown {
110 source: SharedString,
111 pub(crate) selection: Selection,
112 pub(crate) pressed_link: Option<RenderedLink>,
113 pub(crate) pressed_footnote_ref: Option<RenderedFootnoteRef>,
114 pub(crate) autoscroll_request: Option<usize>,
115 active_root_block: Option<usize>,
116 pub(crate) parsed_markdown: ParsedMarkdown,
117 pub(crate) images_by_source_offset: HashMap<usize, Arc<Image>>,
118 should_reparse: bool,
119 pending_parse: Option<Task<()>>,
120 pub(crate) focus_handle: FocusHandle,
121 pub(crate) options: MarkdownOptions,
122 wrapped_code_blocks: HashSet<usize>,
123 code_block_scroll_handles: BTreeMap<usize, ScrollHandle>,
124 context_menu_link: Option<SharedString>,
125 context_menu_selected_text: Option<SharedString>,
126 context_menu_selected_markdown: Option<SharedString>,
127 pub(crate) search_highlights: Vec<Range<usize>>,
128 pub(crate) active_search_highlight: Option<usize>,
129}
130
131#[derive(Clone, Copy, Default)]
132pub struct MarkdownOptions {
133 pub parse_links_only: bool,
134 pub parse_html: bool,
135 pub parse_heading_slugs: bool,
136 pub render_metadata_blocks: bool,
137}
138
139pub enum CodeBlockRenderer {
140 Default {
141 copy_button_visibility: crate::style::CopyButtonVisibility,
142 wrap_button_visibility: crate::style::WrapButtonVisibility,
143 border: bool,
144 },
145 Custom {
146 render: CodeBlockRenderFn,
147 transform: Option<CodeBlockTransformFn>,
150 },
151}
152
153pub type CodeBlockRenderFn = Arc<
154 dyn Fn(
155 &CodeBlockKind,
156 &ParsedMarkdown,
157 Range<usize>,
158 CodeBlockMetadata,
159 &mut Window,
160 &App,
161 ) -> Div,
162>;
163
164pub type CodeBlockTransformFn =
165 Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
166
167enum EscapeAction {
168 PassThrough,
169 Nbsp(usize),
170 DoubleNewline,
171 PrefixBackslash,
172}
173
174impl EscapeAction {
175 fn output_len(&self, c: char) -> usize {
176 match self {
177 Self::PassThrough => c.len_utf8(),
178 Self::Nbsp(count) => count * '\u{00A0}'.len_utf8(),
179 Self::DoubleNewline => 2,
180 Self::PrefixBackslash => '\\'.len_utf8() + c.len_utf8(),
181 }
182 }
183
184 fn write_to(&self, c: char, output: &mut String) {
185 match self {
186 Self::PassThrough => output.push(c),
187 Self::Nbsp(count) => {
188 for _ in 0..*count {
189 output.push('\u{00A0}');
190 }
191 }
192 Self::DoubleNewline => {
193 output.push('\n');
194 output.push('\n');
195 }
196 Self::PrefixBackslash => {
197 output.push('\\');
199 output.push(c);
200 }
201 }
202 }
203}
204
205struct MarkdownEscaper {
206 in_leading_whitespace: bool,
207}
208
209impl MarkdownEscaper {
210 const TAB_SIZE: usize = 4;
211
212 fn new() -> Self {
213 Self {
214 in_leading_whitespace: true,
215 }
216 }
217
218 fn next(&mut self, c: char) -> EscapeAction {
219 let action = if self.in_leading_whitespace && c == '\t' {
220 EscapeAction::Nbsp(Self::TAB_SIZE)
221 } else if self.in_leading_whitespace && c == ' ' {
222 EscapeAction::Nbsp(1)
223 } else if c == '\n' {
224 EscapeAction::DoubleNewline
225 } else if c.is_ascii_punctuation() {
226 EscapeAction::PrefixBackslash
227 } else {
228 EscapeAction::PassThrough
229 };
230
231 self.in_leading_whitespace =
232 c == '\n' || (self.in_leading_whitespace && (c == ' ' || c == '\t'));
233 action
234 }
235}
236
237impl Markdown {
238 pub fn new(source: SharedString, cx: &mut Context<Self>) -> Self {
239 Self::new_with_options(source, MarkdownOptions::default(), cx)
240 }
241
242 pub fn new_with_options(
243 source: SharedString,
244 options: MarkdownOptions,
245 cx: &mut Context<Self>,
246 ) -> Self {
247 let focus_handle = cx.focus_handle();
248
249 let mut this = Self {
250 source,
251 selection: Selection::default(),
252 pressed_link: None,
253 pressed_footnote_ref: None,
254 autoscroll_request: None,
255 active_root_block: None,
256 should_reparse: false,
257 images_by_source_offset: Default::default(),
258 parsed_markdown: ParsedMarkdown::default(),
259 pending_parse: None,
260 focus_handle,
261 options,
262 wrapped_code_blocks: HashSet::default(),
263 code_block_scroll_handles: BTreeMap::default(),
264 context_menu_link: None,
265 context_menu_selected_text: None,
266 context_menu_selected_markdown: None,
267 search_highlights: Vec::new(),
268 active_search_highlight: None,
269 };
270 this.parse(cx);
271 this
272 }
273
274 pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
275 Self::new_with_options(
276 source,
277 MarkdownOptions {
278 parse_links_only: true,
279 ..Default::default()
280 },
281 cx,
282 )
283 }
284
285 pub(crate) fn is_code_block_wrapped(&self, id: usize) -> bool {
286 self.wrapped_code_blocks.contains(&id)
287 }
288
289 pub(crate) fn toggle_code_block_wrap(&mut self, id: usize) {
290 if !self.wrapped_code_blocks.remove(&id) {
291 self.wrapped_code_blocks.insert(id);
292 }
293 }
294
295 pub(crate) fn code_block_scroll_handle(&mut self, id: usize) -> Option<ScrollHandle> {
296 (!self.is_code_block_wrapped(id)).then(|| {
297 self.code_block_scroll_handles
298 .entry(id)
299 .or_insert_with(ScrollHandle::new)
300 .clone()
301 })
302 }
303
304 pub(crate) fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) {
305 self.code_block_scroll_handles
306 .retain(|id, _| ids.contains(id));
307 }
308
309 pub(crate) fn clear_code_block_scroll_handles(&mut self) {
310 self.code_block_scroll_handles.clear();
311 }
312
313 pub(crate) fn autoscroll_code_block(
314 &self,
315 source_index: usize,
316 cursor_position: Point<Pixels>,
317 ) {
318 let Some((_, scroll_handle)) = self
319 .code_block_scroll_handles
320 .range(..=source_index)
321 .next_back()
322 else {
323 return;
324 };
325
326 let bounds = scroll_handle.bounds();
327 if cursor_position.y < bounds.top() || cursor_position.y > bounds.bottom() {
328 return;
329 }
330
331 let horizontal_delta = if cursor_position.x < bounds.left() {
332 bounds.left() - cursor_position.x
333 } else if cursor_position.x > bounds.right() {
334 bounds.right() - cursor_position.x
335 } else {
336 return;
337 };
338
339 let offset = scroll_handle.offset();
340 scroll_handle.set_offset(point(offset.x + horizontal_delta, offset.y));
341 }
342
343 pub fn is_parsing(&self) -> bool {
344 self.pending_parse.is_some()
345 }
346
347 pub fn scroll_to_heading(&mut self, slug: &str, cx: &mut Context<Self>) -> Option<usize> {
348 if let Some(source_index) = self.parsed_markdown.heading_slugs.get(slug).copied() {
349 self.autoscroll_request = Some(source_index);
350 cx.notify();
351 Some(source_index)
352 } else {
353 None
354 }
355 }
356
357 pub fn source(&self) -> &SharedString {
358 &self.source
359 }
360
361 pub fn first_code_block_language(&self) -> Option<&'static Language> {
365 self.parsed_markdown.events.iter().find_map(|(_, event)| {
366 let MarkdownEvent::Start(MarkdownTag::CodeBlock { kind, .. }) = event else {
367 return None;
368 };
369 resolve_code_block_language(kind)
370 })
371 }
372
373 pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
374 self.source = SharedString::new(self.source.to_string() + text);
375 self.parse(cx);
376 }
377
378 pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
379 self.source = source.into();
380 self.parse(cx);
381 }
382
383 pub fn request_autoscroll_to_source_index(
384 &mut self,
385 source_index: usize,
386 cx: &mut Context<Self>,
387 ) {
388 self.autoscroll_request = Some(source_index);
389 cx.refresh_windows();
390 }
391
392 pub(crate) fn footnote_definition_content_start(&self, label: &SharedString) -> Option<usize> {
393 self.parsed_markdown
394 .footnote_definitions
395 .get(label)
396 .copied()
397 }
398
399 pub fn set_active_root_for_source_index(
400 &mut self,
401 source_index: Option<usize>,
402 cx: &mut Context<Self>,
403 ) {
404 let active_root_block =
405 source_index.and_then(|index| self.parsed_markdown.root_block_for_source_index(index));
406 if self.active_root_block == active_root_block {
407 return;
408 }
409
410 self.active_root_block = active_root_block;
411 cx.notify();
412 }
413
414 pub(crate) fn active_root_block(&self) -> Option<usize> {
415 self.active_root_block
416 }
417
418 pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
419 if &source == self.source() {
420 return;
421 }
422 self.source = source;
423 self.selection = Selection::default();
424 self.autoscroll_request = None;
425 self.pending_parse = None;
426 self.should_reparse = false;
427 self.search_highlights.clear();
428 self.active_search_highlight = None;
429 self.parse(cx);
431 }
432
433 #[cfg(any(test, feature = "test-support"))]
434 pub fn parsed_markdown(&self) -> &ParsedMarkdown {
435 &self.parsed_markdown
436 }
437
438 pub fn escape(s: &str) -> Cow<'_, str> {
439 let output_len: usize = {
440 let mut escaper = MarkdownEscaper::new();
441 s.chars().map(|c| escaper.next(c).output_len(c)).sum()
442 };
443
444 if output_len == s.len() {
445 return s.into();
446 }
447
448 let mut escaper = MarkdownEscaper::new();
449 let mut output = String::with_capacity(output_len);
450 for c in s.chars() {
451 escaper.next(c).write_to(c, &mut output);
452 }
453 output.into()
454 }
455
456 pub fn selected_text(&self) -> Option<String> {
457 if self.selection.end <= self.selection.start {
458 None
459 } else {
460 Some(self.source[self.selection.start..self.selection.end].to_string())
461 }
462 }
463
464 pub fn set_search_highlights(
465 &mut self,
466 highlights: Vec<Range<usize>>,
467 active: Option<usize>,
468 cx: &mut Context<Self>,
469 ) {
470 debug_assert!(
471 highlights
472 .windows(2)
473 .all(|ranges| (ranges[0].start, ranges[0].end) <= (ranges[1].start, ranges[1].end))
474 );
475 self.search_highlights = highlights;
476 self.active_search_highlight =
477 active.filter(|active| *active < self.search_highlights.len());
478 cx.notify();
479 }
480
481 pub fn clear_search_highlights(&mut self, cx: &mut Context<Self>) {
482 if !self.search_highlights.is_empty() || self.active_search_highlight.is_some() {
483 self.search_highlights.clear();
484 self.active_search_highlight = None;
485 cx.notify();
486 }
487 }
488
489 pub fn set_active_search_highlight(&mut self, active: Option<usize>, cx: &mut Context<Self>) {
490 let active = active.filter(|active| *active < self.search_highlights.len());
491 if self.active_search_highlight != active {
492 self.active_search_highlight = active;
493 cx.notify();
494 }
495 }
496
497 pub fn search_highlights(&self) -> &[Range<usize>] {
498 &self.search_highlights
499 }
500
501 pub fn active_search_highlight(&self) -> Option<usize> {
502 self.active_search_highlight
503 }
504
505 pub(crate) fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
506 if self.selection.end <= self.selection.start {
507 return;
508 }
509 let text = text.text_for_range(self.selection.start..self.selection.end);
510 cx.write_to_clipboard(ClipboardItem::new_string(text));
511 }
512
513 pub(crate) fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) {
514 if let Some(text) = self.context_menu_selected_markdown.take() {
515 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
516 return;
517 }
518 if self.selection.end <= self.selection.start {
519 return;
520 }
521 let text = self.source[self.selection.start..self.selection.end].to_string();
522 cx.write_to_clipboard(ClipboardItem::new_string(text));
523 }
524
525 pub(crate) fn capture_for_context_menu(
526 &mut self,
527 link: Option<SharedString>,
528 rendered_text: Option<&RenderedText>,
529 ) {
530 let range = self.selection.start..self.selection.end;
531 if range.end > range.start {
532 self.context_menu_selected_markdown =
533 Some(SharedString::new(&self.source[range.clone()]));
534 self.context_menu_selected_text = rendered_text
535 .map(|text| text.text_for_range(range))
536 .map(SharedString::new)
537 .or_else(|| self.context_menu_selected_markdown.clone());
538 } else {
539 self.context_menu_selected_markdown = None;
540 self.context_menu_selected_text = None;
541 }
542 self.context_menu_link = link;
543 }
544
545 pub fn context_menu_link(&self) -> Option<&SharedString> {
549 self.context_menu_link.as_ref()
550 }
551
552 pub fn context_menu_selected_text(&self) -> Option<&SharedString> {
555 self.context_menu_selected_text.as_ref()
556 }
557
558 pub fn context_menu_selected_markdown(&self) -> Option<&SharedString> {
561 self.context_menu_selected_markdown.as_ref()
562 }
563
564 fn parse(&mut self, cx: &mut Context<Self>) {
565 if self.source.is_empty() {
566 self.should_reparse = false;
567 self.pending_parse.take();
568 self.parsed_markdown = ParsedMarkdown {
569 source: self.source.clone(),
570 ..Default::default()
571 };
572 self.active_root_block = None;
573 self.images_by_source_offset.clear();
574 cx.notify();
575 cx.refresh_windows();
576 return;
577 }
578
579 if self.pending_parse.is_some() {
580 self.should_reparse = true;
581 return;
582 }
583 self.should_reparse = false;
584 self.pending_parse = Some(self.start_background_parse(cx));
585 }
586
587 fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
588 let source = self.source.clone();
589 let should_parse_links_only = self.options.parse_links_only;
590 let should_parse_html = self.options.parse_html;
591 let should_parse_heading_slugs = self.options.parse_heading_slugs;
592 let should_parse_metadata_blocks = self.options.render_metadata_blocks;
593
594 let parsed = cx.background_spawn(async move {
595 if should_parse_links_only {
596 return ParsedMarkdown {
597 events: Arc::from(parse_links_only(source.as_ref())),
598 source,
599 root_block_starts: Arc::default(),
600 html_blocks: BTreeMap::default(),
601 metadata_blocks: BTreeMap::default(),
602 heading_slugs: HashMap::default(),
603 footnote_definitions: HashMap::default(),
604 };
605 }
606
607 let parsed = parse_markdown_with_options(
608 &source,
609 should_parse_html,
610 should_parse_heading_slugs,
611 should_parse_metadata_blocks,
612 );
613
614 ParsedMarkdown {
615 source,
616 events: Arc::from(parsed.events),
617 root_block_starts: Arc::from(parsed.root_block_starts),
618 html_blocks: parsed.html_blocks,
619 metadata_blocks: parsed.metadata_blocks,
620 heading_slugs: parsed.heading_slugs,
621 footnote_definitions: parsed.footnote_definitions,
622 }
623 });
624
625 cx.spawn(async move |this, cx| {
626 let parsed = parsed.await;
627
628 this.update(cx, |this, cx| {
629 this.parsed_markdown = parsed;
630 if this.active_root_block.is_some_and(|block_index| {
631 block_index >= this.parsed_markdown.root_block_starts.len()
632 }) {
633 this.active_root_block = None;
634 }
635 this.pending_parse.take();
636 if this.should_reparse {
637 this.parse(cx);
638 }
639 cx.notify();
640 cx.refresh_windows();
641 })
642 .ok();
643 })
644 }
645}
646
647impl Focusable for Markdown {
648 fn focus_handle(&self, _cx: &App) -> FocusHandle {
649 self.focus_handle.clone()
650 }
651}
652
653#[derive(Debug, Default, Clone)]
654pub(crate) enum SelectMode {
655 #[default]
656 Character,
657 Word(Range<usize>),
658 Line(Range<usize>),
659 All,
660}
661
662#[derive(Clone, Default)]
663pub(crate) struct Selection {
664 pub(crate) start: usize,
665 pub(crate) end: usize,
666 pub(crate) reversed: bool,
667 pub(crate) pending: bool,
668 pub(crate) mode: SelectMode,
669}
670
671impl Selection {
672 pub(crate) fn set_head(&mut self, head: usize, rendered_text: &RenderedText) {
673 match &self.mode {
674 SelectMode::Character => {
675 if head < self.tail() {
676 if !self.reversed {
677 self.end = self.start;
678 self.reversed = true;
679 }
680 self.start = head;
681 } else {
682 if self.reversed {
683 self.start = self.end;
684 self.reversed = false;
685 }
686 self.end = head;
687 }
688 }
689 SelectMode::Word(original_range) | SelectMode::Line(original_range) => {
690 let head_range = if matches!(self.mode, SelectMode::Word(_)) {
691 rendered_text.surrounding_word_range(head)
692 } else {
693 rendered_text.surrounding_line_range(head)
694 };
695
696 if head < original_range.start {
697 self.start = head_range.start;
698 self.end = original_range.end;
699 self.reversed = true;
700 } else if head >= original_range.end {
701 self.start = original_range.start;
702 self.end = head_range.end;
703 self.reversed = false;
704 } else {
705 self.start = original_range.start;
706 self.end = original_range.end;
707 self.reversed = false;
708 }
709 }
710 SelectMode::All => {
711 self.start = 0;
712 self.end = rendered_text
713 .lines
714 .last()
715 .map(|line| line.source_end)
716 .unwrap_or(0);
717 self.reversed = false;
718 }
719 }
720 }
721
722 pub(crate) fn tail(&self) -> usize {
723 if self.reversed { self.end } else { self.start }
724 }
725}
726
727#[derive(Debug, Clone, Default)]
728pub struct ParsedMarkdown {
729 pub source: SharedString,
730 pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
731 pub root_block_starts: Arc<[usize]>,
732 pub(crate) html_blocks: BTreeMap<usize, crate::parser::html::html_parser::ParsedHtmlBlock>,
733 pub(crate) metadata_blocks: BTreeMap<usize, ParsedMetadataBlock>,
734 pub heading_slugs: HashMap<SharedString, usize>,
735 pub footnote_definitions: HashMap<SharedString, usize>,
736}
737
738impl ParsedMarkdown {
739 pub fn source(&self) -> &SharedString {
740 &self.source
741 }
742
743 pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
744 &self.events
745 }
746
747 pub fn root_block_starts(&self) -> &Arc<[usize]> {
748 &self.root_block_starts
749 }
750
751 pub fn root_block_for_source_index(&self, source_index: usize) -> Option<usize> {
752 if self.root_block_starts.is_empty() {
753 return None;
754 }
755
756 let partition = self
757 .root_block_starts
758 .partition_point(|block_start| *block_start <= source_index);
759
760 Some(partition.saturating_sub(1))
761 }
762}
763
764pub enum AutoscrollBehavior {
765 Propagate,
768 Controlled(ScrollHandle),
770}