2651 lines · 98.2 KB · Rust
RawDownload
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;pub use path_range::{LineCol, PathWithRange};use settings::Settings as _;use theme::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, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent,    MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText,    Task, 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};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>>; /// 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,            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>,    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>,    code_block_scroll_handles: BTreeMap<usize, ScrollHandle>,    context_menu_selected_text: Option<String>,} 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,    [        /// Copies the selected text to the clipboard.        Copy,        /// Copies the selected text as markdown to the clipboard.        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::default(),            code_block_scroll_handles: BTreeMap::default(),            context_menu_selected_text: None,        };        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::default(),            code_block_scroll_handles: BTreeMap::default(),            context_menu_selected_text: None,        };        this.parse(cx);        this    }     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()    }     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;        // Don't clear parsed_markdown here - keep existing content visible until new parse completes        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()        }    }     pub fn selected_text(&self) -> Option<String> {        if self.selection.end <= self.selection.start {            None        } else {            Some(self.source[self.selection.start..self.selection.end].to_string())        }    }     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(&mut self, _: &mut Window, cx: &mut Context<Self>) {        if let Some(text) = self.context_menu_selected_text.take() {            cx.write_to_clipboard(ClipboardItem::new_string(text));            return;        }        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 capture_selection_for_context_menu(&mut self) {        self.context_menu_selected_text = self.selected_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));    }

Lines 1–500 of 2651.