diff --git a/assets/icons/text_unwrap.svg b/assets/icons/text_unwrap.svg new file mode 100644 index 00000000000..1dda70014be --- /dev/null +++ b/assets/icons/text_unwrap.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/text_wrap.svg b/assets/icons/text_wrap.svg new file mode 100644 index 00000000000..64ec35a2941 --- /dev/null +++ b/assets/icons/text_wrap.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index 8801379578f..695e2beb440 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -508,6 +508,7 @@ impl AcpTools { } else { CopyButtonVisibility::Hidden }, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }, ), diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 96d6476b9a3..b8e72840be6 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -3133,6 +3133,11 @@ fn render_agent_markdown( }) .unwrap_or_default(); MarkdownElement::new(markdown, style) + .code_block_renderer(markdown::CodeBlockRenderer::Default { + copy_button_visibility: markdown::CopyButtonVisibility::VisibleOnHover, + wrap_button_visibility: markdown::WrapButtonVisibility::VisibleOnHover, + border: false, + }) .image_resolver(move |dest_url| resolve_agent_image(dest_url, &worktree_roots)) .on_url_click(move |text, window, cx| { thread_view::open_link(text, &workspace, window, cx); diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 343c21bbec8..cc182096d18 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -5991,6 +5991,7 @@ impl ThreadView { .render_markdown(command, style, cx) .code_block_renderer(CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }); let copy_button = CopyButton::new("copy-command", command_text) diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index 21da60b5161..e1068e9c3be 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -240,6 +240,7 @@ impl DiagnosticBlock { ) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }) .on_url_click({ diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index e63f60f4e73..d54cc667c3e 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1260,6 +1260,7 @@ impl CompletionsMenu { MarkdownElement::new(markdown, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }) .on_url_click(open_markdown_url), diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6474170aace..cfee889ac27 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1043,6 +1043,7 @@ impl InfoPopover { MarkdownElement::new(markdown, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }) .on_url_click(open_markdown_url) @@ -1157,6 +1158,7 @@ impl DiagnosticPopover { ) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }) .on_url_click( diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 6305fc73e44..6ec2fe6152c 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -409,6 +409,8 @@ impl SignatureHelpPopover { ) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: + markdown::WrapButtonVisibility::Hidden, border: false, }) .on_url_click(open_markdown_url), @@ -421,6 +423,8 @@ impl SignatureHelpPopover { MarkdownElement::new(description, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: + markdown::WrapButtonVisibility::Hidden, border: false, }) .on_url_click(open_markdown_url), diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 83a7a1e9c4e..1559622fc98 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -239,6 +239,8 @@ pub enum IconName { Terminal, TerminalAlt, TextSnippet, + TextWrap, + TextUnwrap, ThinkingMode, ThinkingModeOff, Thread, diff --git a/crates/markdown/src/html/html_rendering.rs b/crates/markdown/src/html/html_rendering.rs index 6387164922c..25e869625fb 100644 --- a/crates/markdown/src/html/html_rendering.rs +++ b/crates/markdown/src/html/html_rendering.rs @@ -561,6 +561,8 @@ mod tests { }); } + use crate::WrapButtonVisibility; + fn render_markdown_text(markdown: &str, cx: &mut TestAppContext) -> crate::RenderedText { struct TestWindow; @@ -582,6 +584,7 @@ mod tests { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: WrapButtonVisibility::Hidden, border: false, }, ) @@ -642,6 +645,7 @@ mod tests { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: WrapButtonVisibility::Hidden, border: false, }, ) diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 44e87f55677..1f763d390b9 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -334,6 +334,7 @@ pub struct Markdown { mermaid_state: MermaidState, mermaid_showing_code: HashSet, copied_code_blocks: HashSet, + wrapped_code_blocks: HashSet, code_block_scroll_handles: BTreeMap, context_menu_link: Option, context_menu_selected_text: Option, @@ -356,9 +357,17 @@ pub enum CopyButtonVisibility { VisibleOnHover, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WrapButtonVisibility { + Hidden, + AlwaysVisible, + VisibleOnHover, +} + pub enum CodeBlockRenderer { Default { copy_button_visibility: CopyButtonVisibility, + wrap_button_visibility: WrapButtonVisibility, border: bool, }, Custom { @@ -505,6 +514,7 @@ impl Markdown { mermaid_state: MermaidState::default(), mermaid_showing_code: HashSet::default(), copied_code_blocks: HashSet::default(), + wrapped_code_blocks: HashSet::default(), code_block_scroll_handles: BTreeMap::default(), context_menu_link: None, context_menu_selected_text: None, @@ -528,6 +538,16 @@ impl Markdown { ) } + fn is_code_block_wrapped(&self, id: usize) -> bool { + self.wrapped_code_blocks.contains(&id) + } + + fn toggle_code_block_wrap(&mut self, id: usize) { + if !self.wrapped_code_blocks.remove(&id) { + self.wrapped_code_blocks.insert(id); + } + } + fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle { self.code_block_scroll_handles .entry(id) @@ -1073,6 +1093,7 @@ impl MarkdownElement { style, code_block_renderer: CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::VisibleOnHover, + wrap_button_visibility: WrapButtonVisibility::Hidden, border: false, }, on_url_click: None, @@ -1891,11 +1912,18 @@ impl Element for MarkdownElement { parent_container.style().refine(&self.style.code_block); builder.push_div(parent_container, range, markdown_end); + let is_wrapped = + self.markdown.read(cx).is_code_block_wrapped(range.start); + let code_block = div() .id(("code-block", range.start)) .rounded_lg() .map(|mut code_block| { - if let Some(scroll_handle) = scroll_handle.as_ref() { + if is_wrapped { + code_block.w_full() + } else if let Some(scroll_handle) = + scroll_handle.as_ref() + { code_block.style().restrict_scroll_to_axis = Some(true); code_block @@ -2134,10 +2162,14 @@ impl Element for MarkdownElement { if let CodeBlockRenderer::Default { copy_button_visibility, + wrap_button_visibility, .. } = &self.code_block_renderer - && *copy_button_visibility != CopyButtonVisibility::Hidden + && (*copy_button_visibility != CopyButtonVisibility::Hidden + || *wrap_button_visibility != WrapButtonVisibility::Hidden) { + let copy_button_visibility = *copy_button_visibility; + let wrap_button_visibility = *wrap_button_visibility; builder.modify_current_div(|el| { let content_range = parser::extract_code_block_content_range( &parsed_markdown.source()[range.clone()], @@ -2146,28 +2178,48 @@ impl Element for MarkdownElement { ..content_range.end + range.start; let code = parsed_markdown.source()[content_range].to_string(); - let codeblock = render_copy_code_block_button( - range.end, - code, - self.markdown.clone(), - ); - el.child( - h_flex() - .w_4() - .absolute() - .justify_end() - .when_else( - *copy_button_visibility - == CopyButtonVisibility::VisibleOnHover, - |this| { - this.top_0() - .right_0() - .visible_on_hover("code_block") - }, - |this| this.top_1p5().right_1p5(), - ) - .child(codeblock), - ) + + let any_hover = copy_button_visibility + == CopyButtonVisibility::VisibleOnHover + || wrap_button_visibility + == WrapButtonVisibility::VisibleOnHover; + let any_always = copy_button_visibility + == CopyButtonVisibility::AlwaysVisible + || wrap_button_visibility + == WrapButtonVisibility::AlwaysVisible; + let use_hover = any_hover && !any_always; + + let mut button_row = h_flex() + .gap_0p5() + .absolute() + .bg(cx.theme().colors().editor_background) + .when_else( + use_hover, + |this| { + this.top_1().right_1().visible_on_hover("code_block") + }, + |this| this.top_1p5().right_1p5(), + ); + + if wrap_button_visibility != WrapButtonVisibility::Hidden { + let is_wrapped = + self.markdown.read(cx).is_code_block_wrapped(range.start); + button_row = button_row.child(render_wrap_code_block_button( + range.start, + is_wrapped, + self.markdown.clone(), + )); + } + + if copy_button_visibility != CopyButtonVisibility::Hidden { + button_row = button_row.child(render_copy_code_block_button( + range.end, + code, + self.markdown.clone(), + )); + } + + el.child(button_row) }); } @@ -2418,6 +2470,29 @@ fn apply_heading_style( heading } +fn render_wrap_code_block_button( + id: usize, + is_wrapped: bool, + markdown: Entity, +) -> impl IntoElement { + let (icon, tooltip) = if is_wrapped { + (IconName::TextUnwrap, "Unwrap Content") + } else { + (IconName::TextWrap, "Wrap Content") + }; + + IconButton::new(("wrap-code-block", id), icon) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text(tooltip)) + .on_click(move |_event, _window, cx| { + markdown.update(cx, |markdown, cx| { + markdown.toggle_code_block_wrap(id); + cx.notify(); + }); + }) +} + fn render_copy_code_block_button( id: usize, code: String, @@ -3434,6 +3509,7 @@ mod tests { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: WrapButtonVisibility::Hidden, border: false, }, ) diff --git a/crates/markdown/src/mermaid.rs b/crates/markdown/src/mermaid.rs index 250edeea3a5..019cb6d78ad 100644 --- a/crates/markdown/src/mermaid.rs +++ b/crates/markdown/src/mermaid.rs @@ -585,7 +585,7 @@ mod tests { }; use crate::{ CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownOptions, - MarkdownStyle, + MarkdownStyle, WrapButtonVisibility, }; use collections::HashMap; use gpui::{Context, Hsla, IntoElement, Render, RenderImage, TestAppContext, Window, size}; @@ -644,6 +644,7 @@ mod tests { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: WrapButtonVisibility::Hidden, border: false, }, ) @@ -924,6 +925,7 @@ mod tests { MarkdownElement::new(markdown.clone(), MarkdownStyle::default()) .code_block_renderer(CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: WrapButtonVisibility::Hidden, border: false, }) }, diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 4413e6b0f12..38ce126badb 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -623,6 +623,7 @@ impl MarkdownPreviewView { let mut markdown_element = MarkdownElement::new(self.markdown.clone(), markdown_style) .code_block_renderer(CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::VisibleOnHover, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }) .scroll_handle(self.scroll_handle.clone()) diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index ce54765e3ff..689160b435f 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -402,6 +402,7 @@ impl Render for LanguageServerPrompt { .text_size(TextSize::Small.rems(cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button_visibility: CopyButtonVisibility::Hidden, + wrap_button_visibility: markdown::WrapButtonVisibility::Hidden, border: false, }) .on_url_click(|link, _, cx| cx.open_url(&link)), diff --git a/crates/zed/src/zed/telemetry_log.rs b/crates/zed/src/zed/telemetry_log.rs index 7df7e83d258..062fd5f0f28 100644 --- a/crates/zed/src/zed/telemetry_log.rs +++ b/crates/zed/src/zed/telemetry_log.rs @@ -12,7 +12,10 @@ use gpui::{ StyleRefinement, Task, TextStyleRefinement, Window, list, prelude::*, }; use language::LanguageRegistry; -use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle}; +use markdown::{ + CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle, + WrapButtonVisibility, +}; use project::Project; use settings::Settings; use telemetry_events::{Event, EventWrapper}; @@ -429,6 +432,7 @@ impl TelemetryLogView { } else { CopyButtonVisibility::Hidden }, + wrap_button_visibility: WrapButtonVisibility::Hidden, border: false, }), ),