pub mod html;mod mermaid;pub mod parser;mod path_range; use base64::Engine as _;use futures::FutureExt as _;use gpui::EdgesRefinement;use gpui::HitboxBehavior;use gpui::UnderlineStyle;use language::LanguageName; use log::Level;use mermaid::{ MermaidState, ParsedMarkdownMermaidDiagram, extract_mermaid_diagrams, render_mermaid_diagram,};pub use path_range::{LineCol, PathWithRange};use settings::Settings as _;use theme_settings::ThemeSettings;use ui::Checkbox;use ui::CopyButton; use std::borrow::Cow;use std::collections::BTreeMap;use std::iter;use std::mem;use std::ops::Range;use std::path::Path;use std::rc::Rc;use std::sync::Arc;use std::time::Duration; use collections::{HashMap, HashSet};use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image, ImageFormat, ImageSource, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextAlign, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad,};use language::{CharClassifier, Language, LanguageRegistry, Rope};use parser::CodeBlockMetadata;use parser::{ MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown_with_options,};use pulldown_cmark::Alignment;use sum_tree::TreeMap;use theme::SyntaxTheme;use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*};use util::ResultExt; use crate::parser::CodeBlockKind; /// A callback function that can be used to customize the style of links based on the destination URL./// If the callback returns `None`, the default link style will be used.type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;/// Defines custom style refinements for each heading level (H1-H6)#[derive(Clone, Default)]pub struct HeadingLevelStyles { pub h1: Option<TextStyleRefinement>, pub h2: Option<TextStyleRefinement>, pub h3: Option<TextStyleRefinement>, pub h4: Option<TextStyleRefinement>, pub h5: Option<TextStyleRefinement>, pub h6: Option<TextStyleRefinement>,} #[derive(Clone)]pub struct MarkdownStyle { pub base_text_style: TextStyle, pub container_style: StyleRefinement, pub code_block: StyleRefinement, pub code_block_overflow_x_scroll: bool, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub link_callback: Option<LinkStyleCallback>, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla, pub heading: StyleRefinement, pub heading_level_styles: Option<HeadingLevelStyles>, pub height_is_multiple_of_line_height: bool, pub prevent_mouse_interaction: bool, pub table_columns_min_size: bool,} impl Default for MarkdownStyle { fn default() -> Self { Self { base_text_style: Default::default(), container_style: Default::default(), code_block: Default::default(), code_block_overflow_x_scroll: false, inline_code: Default::default(), block_quote: Default::default(), link: Default::default(), link_callback: None, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: Arc::new(SyntaxTheme::default()), selection_background_color: Default::default(), heading: Default::default(), heading_level_styles: None, height_is_multiple_of_line_height: false, prevent_mouse_interaction: false, table_columns_min_size: false, } }} pub enum MarkdownFont { Agent, Editor,} impl MarkdownStyle { pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); let buffer_font_weight = theme_settings.buffer_font.weight; let (buffer_font_size, ui_font_size) = match font { MarkdownFont::Agent => ( theme_settings.agent_buffer_font_size(cx), theme_settings.agent_ui_font_size(cx), ), MarkdownFont::Editor => ( theme_settings.buffer_font_size(cx), theme_settings.ui_font_size(cx), ), }; let text_color = colors.text; let mut text_style = window.text_style(); let line_height = buffer_font_size * 1.75; text_style.refine(&TextStyleRefinement { font_family: Some(theme_settings.ui_font.family.clone()), font_fallbacks: theme_settings.ui_font.fallbacks.clone(), font_features: Some(theme_settings.ui_font.features.clone()), font_size: Some(ui_font_size.into()), line_height: Some(line_height.into()), color: Some(text_color), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), selection_background_color: colors.element_selection_background, rule_color: colors.border, block_quote_border_color: colors.border, code_block_overflow_x_scroll: true, heading_level_styles: Some(HeadingLevelStyles { h1: Some(TextStyleRefinement { font_size: Some(rems(1.15).into()), ..Default::default() }), h2: Some(TextStyleRefinement { font_size: Some(rems(1.1).into()), ..Default::default() }), h3: Some(TextStyleRefinement { font_size: Some(rems(1.05).into()), ..Default::default() }), h4: Some(TextStyleRefinement { font_size: Some(rems(1.).into()), ..Default::default() }), h5: Some(TextStyleRefinement { font_size: Some(rems(0.95).into()), ..Default::default() }), h6: Some(TextStyleRefinement { font_size: Some(rems(0.875).into()), ..Default::default() }), }), code_block: StyleRefinement { padding: EdgesRefinement { top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), }, margin: EdgesRefinement { top: Some(Length::Definite(px(8.).into())), left: Some(Length::Definite(px(0.).into())), right: Some(Length::Definite(px(0.).into())), bottom: Some(Length::Definite(px(12.).into())), }, border_style: Some(BorderStyle::Solid), border_widths: EdgesRefinement { top: Some(AbsoluteLength::Pixels(px(1.))), left: Some(AbsoluteLength::Pixels(px(1.))), right: Some(AbsoluteLength::Pixels(px(1.))), bottom: Some(AbsoluteLength::Pixels(px(1.))), }, border_color: Some(colors.border_variant), background: Some(colors.editor_background.into()), text: TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), font_size: Some(buffer_font_size.into()), font_weight: Some(buffer_font_weight), ..Default::default() }, ..Default::default() }, inline_code: TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), font_size: Some(buffer_font_size.into()), font_weight: Some(buffer_font_weight), background_color: Some(colors.editor_foreground.opacity(0.08)), ..Default::default() }, link: TextStyleRefinement { background_color: Some(colors.editor_foreground.opacity(0.025)), color: Some(colors.text_accent), underline: Some(UnderlineStyle { color: Some(colors.text_accent.opacity(0.5)), thickness: px(1.), ..Default::default() }), ..Default::default() }, ..Default::default() } } pub fn with_muted_text(mut self, cx: &App) -> Self { let colors = cx.theme().colors(); self.base_text_style.color = colors.text_muted; self }} pub struct Markdown { source: SharedString, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, active_root_block: Option<usize>, parsed_markdown: ParsedMarkdown, images_by_source_offset: HashMap<usize, Arc<Image>>, should_reparse: bool, pending_parse: Option<Task<()>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, options: MarkdownOptions, mermaid_state: MermaidState, copied_code_blocks: HashSet<ElementId>, code_block_scroll_handles: BTreeMap<usize, ScrollHandle>, context_menu_selected_text: Option<String>, search_highlights: Vec<Range<usize>>, active_search_highlight: Option<usize>,} #[derive(Clone, Copy, Default)]pub struct MarkdownOptions { pub parse_links_only: bool, pub parse_html: bool, pub render_mermaid_diagrams: bool, pub parse_heading_slugs: bool,} #[derive(Clone, Copy, PartialEq, Eq)]pub enum CopyButtonVisibility { Hidden, AlwaysVisible, VisibleOnHover,} pub enum CodeBlockRenderer { Default { copy_button_visibility: CopyButtonVisibility, border: bool, }, Custom { render: CodeBlockRenderFn, /// A function that can modify the parent container after the code block /// content has been appended as a child element. transform: Option<CodeBlockTransformFn>, },} pub type CodeBlockRenderFn = Arc< dyn Fn( &CodeBlockKind, &ParsedMarkdown, Range<usize>, CodeBlockMetadata, &mut Window, &App, ) -> Div,>; pub type CodeBlockTransformFn = Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>; actions!( markdown, [ /// Copies the selected text to the clipboard. Copy, /// Copies the selected text as markdown to the clipboard. CopyAsMarkdown ]); enum EscapeAction { PassThrough, Nbsp(usize), DoubleNewline, PrefixBackslash,} impl EscapeAction { fn output_len(&self) -> usize { match self { Self::PassThrough => 1, Self::Nbsp(count) => count * '\u{00A0}'.len_utf8(), Self::DoubleNewline => 2, Self::PrefixBackslash => 2, } } fn write_to(&self, c: char, output: &mut String) { match self { Self::PassThrough => output.push(c), Self::Nbsp(count) => { for _ in 0..*count { output.push('\u{00A0}'); } } Self::DoubleNewline => { output.push('\n'); output.push('\n'); } Self::PrefixBackslash => { // '\\' is a single backslash in Rust, e.g. '|' -> '\|' output.push('\\'); output.push(c); } } }} // Valid to operate on raw bytes since multi-byte UTF-8// sequences never contain ASCII-range bytes.struct MarkdownEscaper { in_leading_whitespace: bool,} impl MarkdownEscaper { const TAB_SIZE: usize = 4; fn new() -> Self { Self { in_leading_whitespace: true, } } fn next(&mut self, byte: u8) -> EscapeAction { let action = if self.in_leading_whitespace && byte == b'\t' { EscapeAction::Nbsp(Self::TAB_SIZE) } else if self.in_leading_whitespace && byte == b' ' { EscapeAction::Nbsp(1) } else if byte == b'\n' { EscapeAction::DoubleNewline } else if byte.is_ascii_punctuation() { EscapeAction::PrefixBackslash } else { EscapeAction::PassThrough }; self.in_leading_whitespace = byte == b'\n' || (self.in_leading_whitespace && (byte == b' ' || byte == b'\t')); action }} impl Markdown { pub fn new( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, cx: &mut Context<Self>, ) -> Self { Self::new_with_options( source, language_registry, fallback_code_block_language, MarkdownOptions::default(), cx, ) } pub fn new_with_options( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, options: MarkdownOptions, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, active_root_block: None, should_reparse: false, images_by_source_offset: Default::default(), parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, fallback_code_block_language, options, mermaid_state: MermaidState::default(), copied_code_blocks: HashSet::default(), code_block_scroll_handles: BTreeMap::default(), context_menu_selected_text: None, search_highlights: Vec::new(), active_search_highlight: None, }; this.parse(cx); this } pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self { Self::new_with_options( source, None, None, MarkdownOptions { parse_links_only: true, ..Default::default() }, cx, ) } fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle { self.code_block_scroll_handles .entry(id) .or_insert_with(ScrollHandle::new) .clone() } fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) { self.code_block_scroll_handles .retain(|id, _| ids.contains(id)); } fn clear_code_block_scroll_handles(&mut self) { self.code_block_scroll_handles.clear(); } fn autoscroll_code_block(&self, source_index: usize, cursor_position: Point<Pixels>) { let Some((_, scroll_handle)) = self .code_block_scroll_handles .range(..=source_index) .next_back() else { return; }; let bounds = scroll_handle.bounds(); if cursor_position.y < bounds.top() || cursor_position.y > bounds.bottom() { return; } let horizontal_delta = if cursor_position.x < bounds.left() { bounds.left() - cursor_position.x } else if cursor_position.x > bounds.right() { bounds.right() - cursor_position.x } else { return; }; let offset = scroll_handle.offset(); scroll_handle.set_offset(point(offset.x + horizontal_delta, offset.y)); } pub fn is_parsing(&self) -> bool { self.pending_parse.is_some() }Lines 1–500 of 3439. Load more
pub mod html; mod mermaid; pub mod parser; mod path_range; use base64::Engine as _; use futures::FutureExt as _; use gpui::EdgesRefinement; use gpui::HitboxBehavior; use gpui::UnderlineStyle; use language::LanguageName; use log::Level; use mermaid::{ MermaidState, ParsedMarkdownMermaidDiagram, extract_mermaid_diagrams, render_mermaid_diagram, }; pub use path_range::{LineCol, PathWithRange}; use settings::Settings as _; use theme_settings::ThemeSettings; use ui::Checkbox; use ui::CopyButton; use std::borrow::Cow; use std::collections::BTreeMap; use std::iter; use std::mem; use std::ops::Range; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use collections::{HashMap, HashSet}; use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image, ImageFormat, ImageSource, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextAlign, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad, }; use language::{CharClassifier, Language, LanguageRegistry, Rope}; use parser::CodeBlockMetadata; use parser::{ MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown_with_options, }; use pulldown_cmark::Alignment; use sum_tree::TreeMap; use theme::SyntaxTheme; use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*}; use util::ResultExt; use crate::parser::CodeBlockKind; /// A callback function that can be used to customize the style of links based on the destination URL. /// If the callback returns `None`, the default link style will be used. type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>; type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>; type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>; /// Defines custom style refinements for each heading level (H1-H6) #[derive(Clone, Default)] pub struct HeadingLevelStyles { pub h1: Option<TextStyleRefinement>, pub h2: Option<TextStyleRefinement>, pub h3: Option<TextStyleRefinement>, pub h4: Option<TextStyleRefinement>, pub h5: Option<TextStyleRefinement>, pub h6: Option<TextStyleRefinement>, } #[derive(Clone)] pub struct MarkdownStyle { pub base_text_style: TextStyle, pub container_style: StyleRefinement, pub code_block: StyleRefinement, pub code_block_overflow_x_scroll: bool, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub link_callback: Option<LinkStyleCallback>, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla, pub heading: StyleRefinement, pub heading_level_styles: Option<HeadingLevelStyles>, pub height_is_multiple_of_line_height: bool, pub prevent_mouse_interaction: bool, pub table_columns_min_size: bool, } impl Default for MarkdownStyle { fn default() -> Self { Self { base_text_style: Default::default(), container_style: Default::default(), code_block: Default::default(), code_block_overflow_x_scroll: false, inline_code: Default::default(), block_quote: Default::default(), link: Default::default(), link_callback: None, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: Arc::new(SyntaxTheme::default()), selection_background_color: Default::default(), heading: Default::default(), heading_level_styles: None, height_is_multiple_of_line_height: false, prevent_mouse_interaction: false, table_columns_min_size: false, } } } pub enum MarkdownFont { Agent, Editor, } impl MarkdownStyle { pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); let buffer_font_weight = theme_settings.buffer_font.weight; let (buffer_font_size, ui_font_size) = match font { MarkdownFont::Agent => ( theme_settings.agent_buffer_font_size(cx), theme_settings.agent_ui_font_size(cx), ), MarkdownFont::Editor => ( theme_settings.buffer_font_size(cx), theme_settings.ui_font_size(cx), ), }; let text_color = colors.text; let mut text_style = window.text_style(); let line_height = buffer_font_size * 1.75; text_style.refine(&TextStyleRefinement { font_family: Some(theme_settings.ui_font.family.clone()), font_fallbacks: theme_settings.ui_font.fallbacks.clone(), font_features: Some(theme_settings.ui_font.features.clone()), font_size: Some(ui_font_size.into()), line_height: Some(line_height.into()), color: Some(text_color), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), selection_background_color: colors.element_selection_background, rule_color: colors.border, block_quote_border_color: colors.border, code_block_overflow_x_scroll: true, heading_level_styles: Some(HeadingLevelStyles { h1: Some(TextStyleRefinement { font_size: Some(rems(1.15).into()), ..Default::default() }), h2: Some(TextStyleRefinement { font_size: Some(rems(1.1).into()), ..Default::default() }), h3: Some(TextStyleRefinement { font_size: Some(rems(1.05).into()), ..Default::default() }), h4: Some(TextStyleRefinement { font_size: Some(rems(1.).into()), ..Default::default() }), h5: Some(TextStyleRefinement { font_size: Some(rems(0.95).into()), ..Default::default() }), h6: Some(TextStyleRefinement { font_size: Some(rems(0.875).into()), ..Default::default() }), }), code_block: StyleRefinement { padding: EdgesRefinement { top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), }, margin: EdgesRefinement { top: Some(Length::Definite(px(8.).into())), left: Some(Length::Definite(px(0.).into())), right: Some(Length::Definite(px(0.).into())), bottom: Some(Length::Definite(px(12.).into())), }, border_style: Some(BorderStyle::Solid), border_widths: EdgesRefinement { top: Some(AbsoluteLength::Pixels(px(1.))), left: Some(AbsoluteLength::Pixels(px(1.))), right: Some(AbsoluteLength::Pixels(px(1.))), bottom: Some(AbsoluteLength::Pixels(px(1.))), }, border_color: Some(colors.border_variant), background: Some(colors.editor_background.into()), text: TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), font_size: Some(buffer_font_size.into()), font_weight: Some(buffer_font_weight), ..Default::default() }, ..Default::default() }, inline_code: TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), font_size: Some(buffer_font_size.into()), font_weight: Some(buffer_font_weight), background_color: Some(colors.editor_foreground.opacity(0.08)), ..Default::default() }, link: TextStyleRefinement { background_color: Some(colors.editor_foreground.opacity(0.025)), color: Some(colors.text_accent), underline: Some(UnderlineStyle { color: Some(colors.text_accent.opacity(0.5)), thickness: px(1.), ..Default::default() }), ..Default::default() }, ..Default::default() } } pub fn with_muted_text(mut self, cx: &App) -> Self { let colors = cx.theme().colors(); self.base_text_style.color = colors.text_muted; self } } pub struct Markdown { source: SharedString, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, active_root_block: Option<usize>, parsed_markdown: ParsedMarkdown, images_by_source_offset: HashMap<usize, Arc<Image>>, should_reparse: bool, pending_parse: Option<Task<()>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, options: MarkdownOptions, mermaid_state: MermaidState, copied_code_blocks: HashSet<ElementId>, code_block_scroll_handles: BTreeMap<usize, ScrollHandle>, context_menu_selected_text: Option<String>, search_highlights: Vec<Range<usize>>, active_search_highlight: Option<usize>, } #[derive(Clone, Copy, Default)] pub struct MarkdownOptions { pub parse_links_only: bool, pub parse_html: bool, pub render_mermaid_diagrams: bool, pub parse_heading_slugs: bool, } #[derive(Clone, Copy, PartialEq, Eq)] pub enum CopyButtonVisibility { Hidden, AlwaysVisible, VisibleOnHover, } pub enum CodeBlockRenderer { Default { copy_button_visibility: CopyButtonVisibility, border: bool, }, Custom { render: CodeBlockRenderFn, /// A function that can modify the parent container after the code block /// content has been appended as a child element. transform: Option<CodeBlockTransformFn>, }, } pub type CodeBlockRenderFn = Arc< dyn Fn( &CodeBlockKind, &ParsedMarkdown, Range<usize>, CodeBlockMetadata, &mut Window, &App, ) -> Div, >; pub type CodeBlockTransformFn = Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>; actions!( markdown, [ /// Copies the selected text to the clipboard. Copy, /// Copies the selected text as markdown to the clipboard. CopyAsMarkdown ] ); enum EscapeAction { PassThrough, Nbsp(usize), DoubleNewline, PrefixBackslash, } impl EscapeAction { fn output_len(&self) -> usize { match self { Self::PassThrough => 1, Self::Nbsp(count) => count * '\u{00A0}'.len_utf8(), Self::DoubleNewline => 2, Self::PrefixBackslash => 2, } } fn write_to(&self, c: char, output: &mut String) { match self { Self::PassThrough => output.push(c), Self::Nbsp(count) => { for _ in 0..*count { output.push('\u{00A0}'); } } Self::DoubleNewline => { output.push('\n'); output.push('\n'); } Self::PrefixBackslash => { // '\\' is a single backslash in Rust, e.g. '|' -> '\|' output.push('\\'); output.push(c); } } } } // Valid to operate on raw bytes since multi-byte UTF-8 // sequences never contain ASCII-range bytes. struct MarkdownEscaper { in_leading_whitespace: bool, } impl MarkdownEscaper { const TAB_SIZE: usize = 4; fn new() -> Self { Self { in_leading_whitespace: true, } } fn next(&mut self, byte: u8) -> EscapeAction { let action = if self.in_leading_whitespace && byte == b'\t' { EscapeAction::Nbsp(Self::TAB_SIZE) } else if self.in_leading_whitespace && byte == b' ' { EscapeAction::Nbsp(1) } else if byte == b'\n' { EscapeAction::DoubleNewline } else if byte.is_ascii_punctuation() { EscapeAction::PrefixBackslash } else { EscapeAction::PassThrough }; self.in_leading_whitespace = byte == b'\n' || (self.in_leading_whitespace && (byte == b' ' || byte == b'\t')); action } } impl Markdown { pub fn new( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, cx: &mut Context<Self>, ) -> Self { Self::new_with_options( source, language_registry, fallback_code_block_language, MarkdownOptions::default(), cx, ) } pub fn new_with_options( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, options: MarkdownOptions, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, active_root_block: None, should_reparse: false, images_by_source_offset: Default::default(), parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, fallback_code_block_language, options, mermaid_state: MermaidState::default(), copied_code_blocks: HashSet::default(), code_block_scroll_handles: BTreeMap::default(), context_menu_selected_text: None, search_highlights: Vec::new(), active_search_highlight: None, }; this.parse(cx); this } pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self { Self::new_with_options( source, None, None, MarkdownOptions { parse_links_only: true, ..Default::default() }, cx, ) } fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle { self.code_block_scroll_handles .entry(id) .or_insert_with(ScrollHandle::new) .clone() } fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) { self.code_block_scroll_handles .retain(|id, _| ids.contains(id)); } fn clear_code_block_scroll_handles(&mut self) { self.code_block_scroll_handles.clear(); } fn autoscroll_code_block(&self, source_index: usize, cursor_position: Point<Pixels>) { let Some((_, scroll_handle)) = self .code_block_scroll_handles .range(..=source_index) .next_back() else { return; }; let bounds = scroll_handle.bounds(); if cursor_position.y < bounds.top() || cursor_position.y > bounds.bottom() { return; } let horizontal_delta = if cursor_position.x < bounds.left() { bounds.left() - cursor_position.x } else if cursor_position.x > bounds.right() { bounds.right() - cursor_position.x } else { return; }; let offset = scroll_handle.offset(); scroll_handle.set_offset(point(offset.x + horizontal_delta, offset.y)); } pub fn is_parsing(&self) -> bool { self.pending_parse.is_some() }