text_view: Adding optional code block actions to the TextView (#1725)
## Description
This PR Adds the ability for users of the library to provide code block
actions for code blocks rendered in markdown (or anything that uses the
TextViewStyle).
I tried to match the existing Fns:
```rs
pub fn heading_font_size<F>(mut self, f: F) -> Self
where
F: Fn(u8, Pixels) -> Pixels + Send + Sync + 'static,
{
self.heading_font_size = Some(Arc::new(f));
self
}
```
New code in text_view:
```rs
pub fn code_block_actions<F, E>(mut self, f: F) -> Self
where
F: Fn(SharedString, Option<SharedString>, &mut Window, &mut App) -> E
+ Send
+ Sync
+ 'static,
E: IntoElement,
{
self.code_block_actions = Some(Arc::new(move |code, lang, window, cx| {
f(code, lang, window, cx).into_any_element()
}));
self
}
```
Example on adding a simple copy button:
```rs
TextView::markdown("preview", content, window, cx)
.code_block_actions(|code, _lang, _window, _cx| {
Clipboard::new("copy").value(code)
})
```
## Screenshot
| Before | After |
| ---------------------------- | --------------------------- |
| <img width="630" height="527" alt="Screenshot 2025-12-01 at 8 37
50 PM"
src="https://github.com/user-attachments/assets/7ac3b8d9-da85-42f8-9d36-cc74848890ea"
/> | <img width="627" height="535" alt="Screenshot 2025-12-01 at 8 38
22 PM"
src="https://github.com/user-attachments/assets/cae67b43-bba8-4834-a060-4bd63d34aefd"
/> |
## How to Test
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce.
## Checklist
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(If any) is accurate.
- [x] Passed `cargo run` for story tests related to the changes.
- [ ] Tested macOS, Windows and Linux platforms performance (if the
change is platform-specific)
---------
Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
parent
b70a0633ab
commit
31bab9dcc3
4 changed files with 106 additions and 13 deletions
|
|
@ -1,6 +1,9 @@
|
|||
use gpui::*;
|
||||
use gpui::{prelude::FluentBuilder as _, *};
|
||||
use gpui_component::{
|
||||
ActiveTheme as _,
|
||||
ActiveTheme as _, IconName, Sizable as _,
|
||||
button::{Button, ButtonVariants as _},
|
||||
clipboard::Clipboard,
|
||||
h_flex,
|
||||
highlighter::Language,
|
||||
input::{Input, InputEvent, InputState, TabSize},
|
||||
resizable::{h_resizable, resizable_panel},
|
||||
|
|
@ -103,6 +106,30 @@ impl Render for Example {
|
|||
window,
|
||||
cx,
|
||||
)
|
||||
.code_block_actions(|code_block, _window, _cx| {
|
||||
let code = code_block.code();
|
||||
let lang = code_block.lang();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Clipboard::new("copy").value(code.clone()))
|
||||
.when_some(lang, |this, lang| {
|
||||
// Only show run terminal button for certain languages
|
||||
if lang.as_ref() == "rust" || lang.as_ref() == "python" {
|
||||
this.child(
|
||||
Button::new("run-terminal")
|
||||
.icon(IconName::SquareTerminal)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(move |_, _, _cx| {
|
||||
println!("Running {} code: {}", lang, code);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
})
|
||||
.flex_none()
|
||||
.p_5()
|
||||
.scrollable(true)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ use ropey::Rope;
|
|||
use crate::{
|
||||
ActiveTheme as _, Icon, IconName, StyledExt, h_flex,
|
||||
highlighter::{HighlightTheme, SyntaxHighlighter},
|
||||
text::inline::{Inline, InlineState},
|
||||
text::{
|
||||
CodeBlockActionsFn,
|
||||
inline::{Inline, InlineState},
|
||||
},
|
||||
tooltip::Tooltip,
|
||||
v_flex,
|
||||
};
|
||||
|
|
@ -304,7 +307,7 @@ impl Paragraph {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CodeBlock {
|
||||
pub struct CodeBlock {
|
||||
lang: Option<SharedString>,
|
||||
styles: Vec<(Range<usize>, HighlightStyle)>,
|
||||
state: Arc<Mutex<InlineState>>,
|
||||
|
|
@ -317,6 +320,16 @@ impl PartialEq for CodeBlock {
|
|||
}
|
||||
|
||||
impl CodeBlock {
|
||||
/// Get the language of the code block.
|
||||
pub fn lang(&self) -> Option<SharedString> {
|
||||
self.lang.clone()
|
||||
}
|
||||
|
||||
/// Get the code content of the code block.
|
||||
pub fn code(&self) -> SharedString {
|
||||
self.state.lock().unwrap().text.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
code: SharedString,
|
||||
lang: Option<SharedString>,
|
||||
|
|
@ -340,10 +353,6 @@ impl CodeBlock {
|
|||
}
|
||||
}
|
||||
|
||||
fn code(&self) -> SharedString {
|
||||
self.state.lock().unwrap().text.clone()
|
||||
}
|
||||
|
||||
pub(super) fn selected_text(&self) -> String {
|
||||
let mut text = String::new();
|
||||
let state = self.state.lock().unwrap();
|
||||
|
|
@ -358,7 +367,7 @@ impl CodeBlock {
|
|||
&self,
|
||||
options: &NodeRenderOptions,
|
||||
node_cx: &NodeContext,
|
||||
_: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let style = &node_cx.style;
|
||||
|
|
@ -380,17 +389,29 @@ impl CodeBlock {
|
|||
self.state.clone(),
|
||||
vec![],
|
||||
self.styles.clone(),
|
||||
)),
|
||||
))
|
||||
.when_some(node_cx.code_block_actions.clone(), |this, actions| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_2()
|
||||
.right_2()
|
||||
.bg(cx.theme().muted)
|
||||
.rounded(cx.theme().radius)
|
||||
.child(actions(&self, window, cx)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
/// A context for rendering nodes, contains link references.
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct NodeContext {
|
||||
pub(crate) link_refs: HashMap<SharedString, LinkMark>,
|
||||
pub(crate) style: TextViewStyle,
|
||||
pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
|
||||
}
|
||||
|
||||
impl NodeContext {
|
||||
|
|
@ -399,6 +420,13 @@ impl NodeContext {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for NodeContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.link_refs == other.link_refs && self.style == other.style
|
||||
// Note: code_block_buttons is intentionally not compared (closures can't be compared)
|
||||
}
|
||||
}
|
||||
|
||||
/// The AST Node of the rich text.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Node {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use gpui::{px, rems, Pixels, Rems, StyleRefinement};
|
||||
use gpui::{Pixels, Rems, StyleRefinement, px, rems};
|
||||
|
||||
use crate::highlighter::HighlightTheme;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use smol::stream::StreamExt;
|
|||
|
||||
use crate::highlighter::HighlightTheme;
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::text::node::CodeBlock;
|
||||
use crate::{ActiveTheme, StyledExt, v_flex};
|
||||
use crate::{
|
||||
global_state::GlobalState,
|
||||
|
|
@ -66,6 +67,10 @@ impl RenderOnce for TextViewElement {
|
|||
}
|
||||
}
|
||||
|
||||
/// Type for code block actions generator function.
|
||||
pub(crate) type CodeBlockActionsFn =
|
||||
dyn Fn(&CodeBlock, &mut Window, &mut App) -> AnyElement + Send + Sync;
|
||||
|
||||
/// A text view that can render Markdown or HTML.
|
||||
///
|
||||
/// ## Goals
|
||||
|
|
@ -91,6 +96,7 @@ pub struct TextView {
|
|||
style: StyleRefinement,
|
||||
selectable: bool,
|
||||
scrollable: bool,
|
||||
code_block_actions: Option<Arc<CodeBlockActionsFn>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
@ -122,9 +128,11 @@ struct UpdateFuture {
|
|||
rx: Pin<Box<smol::channel::Receiver<Update>>>,
|
||||
tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
|
||||
delay: Duration,
|
||||
code_block_actions: Option<Arc<CodeBlockActionsFn>>,
|
||||
}
|
||||
|
||||
impl UpdateFuture {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
type_: TextViewType,
|
||||
style: TextViewStyle,
|
||||
|
|
@ -133,6 +141,7 @@ impl UpdateFuture {
|
|||
rx: smol::channel::Receiver<Update>,
|
||||
tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
|
||||
delay: Duration,
|
||||
code_block_actions: Option<Arc<CodeBlockActionsFn>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
type_,
|
||||
|
|
@ -143,6 +152,7 @@ impl UpdateFuture {
|
|||
rx: Box::pin(rx),
|
||||
tx_result,
|
||||
delay,
|
||||
code_block_actions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +192,7 @@ impl Future for UpdateFuture {
|
|||
&self.current_text,
|
||||
self.current_style.clone(),
|
||||
&self.highlight_theme,
|
||||
&self.code_block_actions.clone(),
|
||||
);
|
||||
_ = self.tx_result.try_send(res);
|
||||
continue;
|
||||
|
|
@ -419,6 +430,7 @@ impl TextView {
|
|||
state,
|
||||
selectable: false,
|
||||
scrollable: false,
|
||||
code_block_actions: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -449,6 +461,7 @@ impl TextView {
|
|||
raw: html,
|
||||
selectable: false,
|
||||
scrollable: false,
|
||||
code_block_actions: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -510,6 +523,21 @@ impl TextView {
|
|||
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(selected_text.trim().to_string()));
|
||||
}
|
||||
|
||||
/// Set custom block actions for code blocks.
|
||||
///
|
||||
/// The closure receives the [`CodeBlock`],
|
||||
/// and returns an element to display.
|
||||
pub fn code_block_actions<F, E>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&CodeBlock, &mut Window, &mut App) -> E + Send + Sync + 'static,
|
||||
E: IntoElement,
|
||||
{
|
||||
self.code_block_actions = Some(Arc::new(move |code_block, window, cx| {
|
||||
f(&code_block, window, cx).into_any_element()
|
||||
}));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for TextView {
|
||||
|
|
@ -548,10 +576,17 @@ impl Element for TextView {
|
|||
{
|
||||
let style = *style;
|
||||
let highlight_theme = highlight_theme.clone();
|
||||
let code_block_actions = self.code_block_actions.clone();
|
||||
let (tx, rx) = smol::channel::unbounded::<Update>();
|
||||
let (tx_result, rx_result) =
|
||||
smol::channel::unbounded::<Result<ParsedContent, SharedString>>();
|
||||
let parsed_result = parse_content(type_, &text, style.clone(), &highlight_theme);
|
||||
let parsed_result = parse_content(
|
||||
type_,
|
||||
&text,
|
||||
style.clone(),
|
||||
&highlight_theme,
|
||||
&code_block_actions,
|
||||
);
|
||||
|
||||
self.state.update(cx, {
|
||||
let tx = tx.clone();
|
||||
|
|
@ -591,6 +626,7 @@ impl Element for TextView {
|
|||
rx,
|
||||
tx_result,
|
||||
Duration::from_millis(200),
|
||||
code_block_actions,
|
||||
))
|
||||
.detach();
|
||||
|
||||
|
|
@ -744,9 +780,11 @@ fn parse_content(
|
|||
text: &str,
|
||||
style: TextViewStyle,
|
||||
highlight_theme: &HighlightTheme,
|
||||
code_block_actions: &Option<Arc<CodeBlockActionsFn>>,
|
||||
) -> Result<ParsedContent, SharedString> {
|
||||
let mut node_cx = NodeContext {
|
||||
style: style.clone(),
|
||||
code_block_actions: code_block_actions.clone(),
|
||||
..NodeContext::default()
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue