## 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>
152 lines
5.5 KiB
Rust
152 lines
5.5 KiB
Rust
use gpui::{prelude::FluentBuilder as _, *};
|
|
use gpui_component::{
|
|
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},
|
|
text::TextView,
|
|
};
|
|
use gpui_component_assets::Assets;
|
|
use gpui_component_story::Open;
|
|
|
|
pub struct Example {
|
|
input_state: Entity<InputState>,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
const EXAMPLE: &str = include_str!("./fixtures/test.md");
|
|
|
|
impl Example {
|
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
let input_state = cx.new(|cx| {
|
|
InputState::new(window, cx)
|
|
.code_editor(Language::Markdown)
|
|
.line_number(true)
|
|
.tab_size(TabSize {
|
|
tab_size: 2,
|
|
..Default::default()
|
|
})
|
|
.searchable(true)
|
|
.placeholder("Enter your Markdown here...")
|
|
.default_value(EXAMPLE)
|
|
});
|
|
|
|
let _subscriptions = vec![cx.subscribe(&input_state, |_, _, _: &InputEvent, _| {})];
|
|
|
|
Self {
|
|
input_state,
|
|
_subscriptions,
|
|
}
|
|
}
|
|
|
|
fn on_action_open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
|
|
let path = cx.prompt_for_paths(PathPromptOptions {
|
|
files: true,
|
|
directories: true,
|
|
multiple: false,
|
|
prompt: Some("Select a Markdown file".into()),
|
|
});
|
|
|
|
let input_state = self.input_state.clone();
|
|
cx.spawn_in(window, async move |_, window| {
|
|
let path = path.await.ok()?.ok()??.iter().next()?.clone();
|
|
|
|
let content = std::fs::read_to_string(&path).ok()?;
|
|
|
|
window
|
|
.update(|window, cx| {
|
|
_ = input_state.update(cx, |this, cx| {
|
|
this.set_value(content, window, cx);
|
|
});
|
|
})
|
|
.ok();
|
|
|
|
Some(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
|
cx.new(|cx| Self::new(window, cx))
|
|
}
|
|
}
|
|
|
|
impl Render for Example {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
.id("editor")
|
|
.size_full()
|
|
.on_action(cx.listener(Self::on_action_open))
|
|
.child(
|
|
h_resizable("container")
|
|
.child(
|
|
resizable_panel().child(
|
|
div()
|
|
.id("source")
|
|
.size_full()
|
|
.font_family(cx.theme().mono_font_family.clone())
|
|
.text_size(cx.theme().mono_font_size)
|
|
.child(
|
|
Input::new(&self.input_state)
|
|
.h_full()
|
|
.p_0()
|
|
.border_0()
|
|
.focus_bordered(false),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
resizable_panel().child(
|
|
TextView::markdown(
|
|
"preview",
|
|
self.input_state.read(cx).value().clone(),
|
|
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)
|
|
.selectable(true),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let app = Application::new().with_assets(Assets);
|
|
|
|
app.run(move |cx| {
|
|
gpui_component_story::init(cx);
|
|
cx.activate(true);
|
|
|
|
gpui_component_story::create_new_window("Markdown Editor", Example::view, cx);
|
|
});
|
|
}
|