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,
}),
),