1920 lines · 69.9 KB · Rust
RawDownload
pub mod parser;mod path_range; use base64::Engine as _;use futures::FutureExt as _;use gpui::HitboxBehavior;use language::LanguageName;use log::Level;pub use path_range::{LineCol, PathWithRange}; use std::borrow::Cow;use std::collections::HashMap;use std::collections::HashSet;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 gpui::{    AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,    FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,    ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,    Point, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,    TextStyle, TextStyleRefinement, actions, img, point, quad,};use language::{Language, LanguageRegistry, Rope};use parser::CodeBlockMetadata;use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};use pulldown_cmark::Alignment;use sum_tree::TreeMap;use theme::SyntaxTheme;use ui::{Tooltip, 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>>; /// 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 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 table_overflow_x_scroll: bool,    pub height_is_multiple_of_line_height: bool,} impl Default for MarkdownStyle {    fn default() -> Self {        Self {            base_text_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,            table_overflow_x_scroll: false,            height_is_multiple_of_line_height: false,        }    }} pub struct Markdown {    source: SharedString,    selection: Selection,    pressed_link: Option<RenderedLink>,    autoscroll_request: 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: Options,    copied_code_blocks: HashSet<ElementId>,} struct Options {    parse_links_only: bool,} pub enum CodeBlockRenderer {    Default {        copy_button: bool,        copy_button_on_hover: bool,        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, [Copy, CopyAsMarkdown]); impl Markdown {    pub fn new(        source: SharedString,        language_registry: Option<Arc<LanguageRegistry>>,        fallback_code_block_language: Option<LanguageName>,        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,            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: Options {                parse_links_only: false,            },            copied_code_blocks: HashSet::new(),        };        this.parse(cx);        this    }     pub fn new_text(source: SharedString, 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,            should_reparse: false,            parsed_markdown: ParsedMarkdown::default(),            images_by_source_offset: Default::default(),            pending_parse: None,            focus_handle,            language_registry: None,            fallback_code_block_language: None,            options: Options {                parse_links_only: true,            },            copied_code_blocks: HashSet::new(),        };        this.parse(cx);        this    }     pub fn is_parsing(&self) -> bool {        self.pending_parse.is_some()    }     pub fn source(&self) -> &str {        &self.source    }     pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {        self.source = SharedString::new(self.source.to_string() + text);        self.parse(cx);    }     pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {        self.source = source.into();        self.parse(cx);    }     pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {        if source == self.source() {            return;        }        self.source = source;        self.selection = Selection::default();        self.autoscroll_request = None;        self.pending_parse = None;        self.should_reparse = false;        self.parsed_markdown = ParsedMarkdown::default();        self.parse(cx);    }     #[cfg(any(test, feature = "test-support"))]    pub fn parsed_markdown(&self) -> &ParsedMarkdown {        &self.parsed_markdown    }     pub fn escape(s: &str) -> Cow<str> {        // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars.        let count = s            .bytes()            .filter(|c| *c == b'\n' || c.is_ascii_punctuation())            .count();        if count > 0 {            let mut output = String::with_capacity(s.len() + count);            let mut is_newline = false;            for c in s.chars() {                if is_newline && c == ' ' {                    continue;                }                is_newline = c == '\n';                if c == '\n' {                    output.push('\n')                } else if c.is_ascii_punctuation() {                    output.push('\\')                }                output.push(c)            }            output.into()        } else {            s.into()        }    }     fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {        if self.selection.end <= self.selection.start {            return;        }        let text = text.text_for_range(self.selection.start..self.selection.end);        cx.write_to_clipboard(ClipboardItem::new_string(text));    }     fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) {        if self.selection.end <= self.selection.start {            return;        }        let text = self.source[self.selection.start..self.selection.end].to_string();        cx.write_to_clipboard(ClipboardItem::new_string(text));    }     fn parse(&mut self, cx: &mut Context<Self>) {        if self.source.is_empty() {            return;        }         if self.pending_parse.is_some() {            self.should_reparse = true;            return;        }        self.should_reparse = false;        self.pending_parse = Some(self.start_background_parse(cx));    }     fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {        let source = self.source.clone();        let should_parse_links_only = self.options.parse_links_only;        let language_registry = self.language_registry.clone();        let fallback = self.fallback_code_block_language.clone();         let parsed = cx.background_spawn(async move {            if should_parse_links_only {                return (                    ParsedMarkdown {                        events: Arc::from(parse_links_only(source.as_ref())),                        source,                        languages_by_name: TreeMap::default(),                        languages_by_path: TreeMap::default(),                    },                    Default::default(),                );            }             let (events, language_names, paths) = parse_markdown(&source);            let mut images_by_source_offset = HashMap::default();            let mut languages_by_name = TreeMap::default();            let mut languages_by_path = TreeMap::default();            if let Some(registry) = language_registry.as_ref() {                for name in language_names {                    let language = if !name.is_empty() {                        registry.language_for_name_or_extension(&name).left_future()                    } else if let Some(fallback) = &fallback {                        registry.language_for_name(fallback.as_ref()).right_future()                    } else {                        continue;                    };                    if let Ok(language) = language.await {                        languages_by_name.insert(name, language);                    }                }                 for path in paths {                    if let Ok(language) = registry.language_for_file_path(&path).await {                        languages_by_path.insert(path, language);                    }                }            }             for (range, event) in &events {                if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event {                    if let Some(data_url) = dest_url.strip_prefix("data:") {                        let Some((mime_info, data)) = data_url.split_once(',') else {                            continue;                        };                        let Some((mime_type, encoding)) = mime_info.split_once(';') else {                            continue;                        };                        let Some(format) = ImageFormat::from_mime_type(mime_type) else {                            continue;                        };                        let is_base64 = encoding == "base64";                        if is_base64 {                            if let Some(bytes) = base64::prelude::BASE64_STANDARD                                .decode(data)                                .log_with_level(Level::Debug)                            {                                let image = Arc::new(Image::from_bytes(format, bytes));                                images_by_source_offset.insert(range.start, image);                            }                        }                    }                }            }             (                ParsedMarkdown {                    source,                    events: Arc::from(events),                    languages_by_name,                    languages_by_path,                },                images_by_source_offset,            )        });         cx.spawn(async move |this, cx| {            let (parsed, images_by_source_offset) = parsed.await;             this.update(cx, |this, cx| {                this.parsed_markdown = parsed;                this.images_by_source_offset = images_by_source_offset;                this.pending_parse.take();                if this.should_reparse {                    this.parse(cx);                }                cx.refresh_windows();            })            .ok();        })    }} impl Focusable for Markdown {    fn focus_handle(&self, _cx: &App) -> FocusHandle {        self.focus_handle.clone()    }} #[derive(Copy, Clone, Default, Debug)]struct Selection {    start: usize,    end: usize,    reversed: bool,    pending: bool,} impl Selection {    fn set_head(&mut self, head: usize) {        if head < self.tail() {            if !self.reversed {                self.end = self.start;                self.reversed = true;            }            self.start = head;        } else {            if self.reversed {                self.start = self.end;                self.reversed = false;            }            self.end = head;        }    }     fn tail(&self) -> usize {        if self.reversed { self.end } else { self.start }    }} #[derive(Clone, Default)]pub struct ParsedMarkdown {    pub source: SharedString,    pub events: Arc<[(Range<usize>, MarkdownEvent)]>,    pub languages_by_name: TreeMap<SharedString, Arc<Language>>,    pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,} impl ParsedMarkdown {    pub fn source(&self) -> &SharedString {        &self.source    }     pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {        &self.events    }} pub struct MarkdownElement {    markdown: Entity<Markdown>,    style: MarkdownStyle,    code_block_renderer: CodeBlockRenderer,    on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,} impl MarkdownElement {    pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {        Self {            markdown,            style,            code_block_renderer: CodeBlockRenderer::Default {                copy_button: true,                copy_button_on_hover: false,                border: false,            },            on_url_click: None,        }    }     #[cfg(any(test, feature = "test-support"))]    pub fn rendered_text(        markdown: Entity<Markdown>,        cx: &mut gpui::VisualTestContext,        style: impl FnOnce(&Window, &App) -> MarkdownStyle,    ) -> String {        use gpui::size;         let (text, _) = cx.draw(            Default::default(),            size(px(600.0), px(600.0)),            |window, cx| Self::new(markdown, style(window, cx)),        );        text.text            .lines            .iter()            .map(|line| line.layout.wrapped_text())            .collect::<Vec<_>>()            .join("\n")    }     pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {        self.code_block_renderer = variant;        self    }     pub fn on_url_click(        mut self,        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,    ) -> Self {        self.on_url_click = Some(Box::new(handler));        self    }     fn paint_selection(        &self,        bounds: Bounds<Pixels>,        rendered_text: &RenderedText,

Lines 1–500 of 1920.