mirror of
https://github.com/zed-industries/zed.git
synced 2024-10-23 06:56:33 +00:00
repl: Set up a way to copy output from the REPL (#16649)
Closes #15494 Simple copy button to copy an individual output since selection is a bit more work. <img width="790" alt="image" src="https://github.com/user-attachments/assets/4a7d8b69-70cc-428e-8fe3-b95386d341ee"> Release Notes: - repl: Copy output from the REPL using a button --------- Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
26f2369fa6
commit
80c25960dd
4 changed files with 231 additions and 23 deletions
|
@ -30,7 +30,7 @@ impl AssetSource for () {
|
|||
|
||||
/// A unique identifier for the image cache
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct ImageId(usize);
|
||||
pub struct ImageId(pub usize);
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) struct RenderImageParams {
|
||||
|
|
|
@ -1016,6 +1016,13 @@ impl ClipboardItem {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new ClipboardItem::Image with the given image with no associated metadata
|
||||
pub fn new_image(image: &Image) -> Self {
|
||||
Self {
|
||||
entries: vec![ClipboardEntry::Image(image.clone())],
|
||||
}
|
||||
}
|
||||
|
||||
/// Concatenates together all the ClipboardString entries in the item.
|
||||
/// Returns None if there were no ClipboardString entries.
|
||||
pub fn text(&self) -> Option<String> {
|
||||
|
@ -1084,10 +1091,11 @@ pub enum ImageFormat {
|
|||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Image {
|
||||
/// The image format the bytes represent (e.g. PNG)
|
||||
format: ImageFormat,
|
||||
pub format: ImageFormat,
|
||||
/// The raw image bytes
|
||||
bytes: Vec<u8>,
|
||||
id: u64,
|
||||
pub bytes: Vec<u8>,
|
||||
/// The unique ID for the image
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
impl Hash for Image {
|
||||
|
|
|
@ -5,8 +5,8 @@ use crate::stdio::TerminalOutput;
|
|||
use anyhow::Result;
|
||||
use base64::prelude::*;
|
||||
use gpui::{
|
||||
img, percentage, Animation, AnimationExt, AnyElement, FontWeight, Render, RenderImage, Task,
|
||||
TextRun, Transformation, View,
|
||||
img, percentage, Animation, AnimationExt, AnyElement, ClipboardItem, FontWeight, Image,
|
||||
ImageFormat, Render, RenderImage, Task, TextRun, Transformation, View,
|
||||
};
|
||||
use runtimelib::datatable::TableSchema;
|
||||
use runtimelib::media::datatable::TabularDataResource;
|
||||
|
@ -14,7 +14,7 @@ use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
|
|||
use serde_json::Value;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext};
|
||||
use ui::{div, prelude::*, v_flex, IntoElement, Styled, Tooltip, ViewContext};
|
||||
|
||||
use markdown_preview::{
|
||||
markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
|
||||
|
@ -34,8 +34,14 @@ fn rank_mime_type(mimetype: &MimeType) -> usize {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) trait SupportsClipboard {
|
||||
fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem>;
|
||||
fn has_clipboard_content(&self, cx: &WindowContext) -> bool;
|
||||
}
|
||||
|
||||
/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
|
||||
pub struct ImageView {
|
||||
clipboard_image: Arc<Image>,
|
||||
height: u32,
|
||||
width: u32,
|
||||
image: Arc<RenderImage>,
|
||||
|
@ -78,7 +84,27 @@ impl ImageView {
|
|||
|
||||
let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]);
|
||||
|
||||
let format = match format {
|
||||
image::ImageFormat::Png => ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => ImageFormat::Jpeg,
|
||||
image::ImageFormat::Gif => ImageFormat::Gif,
|
||||
image::ImageFormat::WebP => ImageFormat::Webp,
|
||||
image::ImageFormat::Tiff => ImageFormat::Tiff,
|
||||
image::ImageFormat::Bmp => ImageFormat::Bmp,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!("unsupported image format"));
|
||||
}
|
||||
};
|
||||
|
||||
// Convert back to a GPUI image for use with the clipboard
|
||||
let clipboard_image = Arc::new(Image {
|
||||
format,
|
||||
bytes,
|
||||
id: gpui_image_data.id.0 as u64,
|
||||
});
|
||||
|
||||
return Ok(ImageView {
|
||||
clipboard_image,
|
||||
height,
|
||||
width,
|
||||
image: Arc::new(gpui_image_data),
|
||||
|
@ -86,11 +112,22 @@ impl ImageView {
|
|||
}
|
||||
}
|
||||
|
||||
impl SupportsClipboard for ImageView {
|
||||
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
|
||||
Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
|
||||
}
|
||||
|
||||
fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// TableView renders a static table inline in a buffer.
|
||||
/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
|
||||
pub struct TableView {
|
||||
pub table: TabularDataResource,
|
||||
pub widths: Vec<Pixels>,
|
||||
cached_clipboard_content: ClipboardItem,
|
||||
}
|
||||
|
||||
fn cell_content(row: &Value, field: &str) -> String {
|
||||
|
@ -151,7 +188,68 @@ impl TableView {
|
|||
widths.push(width)
|
||||
}
|
||||
|
||||
Self { table, widths }
|
||||
let cached_clipboard_content = Self::create_clipboard_content(&table);
|
||||
|
||||
Self {
|
||||
table,
|
||||
widths,
|
||||
cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_markdown(s: &str) -> String {
|
||||
s.replace('|', "\\|")
|
||||
.replace('*', "\\*")
|
||||
.replace('_', "\\_")
|
||||
.replace('`', "\\`")
|
||||
.replace('[', "\\[")
|
||||
.replace(']', "\\]")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
fn create_clipboard_content(table: &TabularDataResource) -> String {
|
||||
let data = match table.data.as_ref() {
|
||||
Some(data) => data,
|
||||
None => &Vec::new(),
|
||||
};
|
||||
let schema = table.schema.clone();
|
||||
|
||||
let mut markdown = format!(
|
||||
"| {} |\n",
|
||||
table
|
||||
.schema
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| field.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
);
|
||||
|
||||
markdown.push_str("|---");
|
||||
for _ in 1..table.schema.fields.len() {
|
||||
markdown.push_str("|---");
|
||||
}
|
||||
markdown.push_str("|\n");
|
||||
|
||||
let body = data
|
||||
.iter()
|
||||
.map(|record: &Value| {
|
||||
let row_content = schema
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| Self::escape_markdown(&cell_content(record, &field.name)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
row_content.join(" | ")
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
for row in body {
|
||||
markdown.push_str(&format!("| {} |\n", row));
|
||||
}
|
||||
|
||||
markdown
|
||||
}
|
||||
|
||||
pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
|
||||
|
@ -242,6 +340,16 @@ impl TableView {
|
|||
}
|
||||
}
|
||||
|
||||
impl SupportsClipboard for TableView {
|
||||
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
|
||||
Some(self.cached_clipboard_content.clone())
|
||||
}
|
||||
|
||||
fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Userspace error from the kernel
|
||||
pub struct ErrorView {
|
||||
pub ename: String,
|
||||
|
@ -288,34 +396,48 @@ impl ErrorView {
|
|||
}
|
||||
|
||||
pub struct MarkdownView {
|
||||
raw_text: String,
|
||||
contents: Option<ParsedMarkdown>,
|
||||
parsing_markdown_task: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl MarkdownView {
|
||||
pub fn from(text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||
let task = cx.spawn(|markdown, mut cx| async move {
|
||||
let task = cx.spawn(|markdown_view, mut cx| {
|
||||
let text = text.clone();
|
||||
let parsed = cx
|
||||
.background_executor()
|
||||
.spawn(async move { parse_markdown(&text, None, None).await });
|
||||
|
||||
let content = parsed.await;
|
||||
async move {
|
||||
let content = parsed.await;
|
||||
|
||||
markdown.update(&mut cx, |markdown, cx| {
|
||||
markdown.parsing_markdown_task.take();
|
||||
markdown.contents = Some(content);
|
||||
cx.notify();
|
||||
})
|
||||
markdown_view.update(&mut cx, |markdown, cx| {
|
||||
markdown.parsing_markdown_task.take();
|
||||
markdown.contents = Some(content);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
raw_text: text.clone(),
|
||||
contents: None,
|
||||
parsing_markdown_task: Some(task),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportsClipboard for MarkdownView {
|
||||
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
|
||||
Some(ClipboardItem::new_string(self.raw_text.clone()))
|
||||
}
|
||||
|
||||
fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MarkdownView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Some(parsed) = self.contents.as_ref() else {
|
||||
|
@ -360,6 +482,34 @@ impl Output {
|
|||
}
|
||||
}
|
||||
|
||||
impl SupportsClipboard for Output {
|
||||
fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem> {
|
||||
match &self.content {
|
||||
OutputContent::Plain(terminal) => terminal.clipboard_content(cx),
|
||||
OutputContent::Stream(terminal) => terminal.clipboard_content(cx),
|
||||
OutputContent::Image(image) => image.clipboard_content(cx),
|
||||
OutputContent::ErrorOutput(error) => error.traceback.clipboard_content(cx),
|
||||
OutputContent::Message(_) => None,
|
||||
OutputContent::Table(table) => table.clipboard_content(cx),
|
||||
OutputContent::Markdown(markdown) => markdown.read(cx).clipboard_content(cx),
|
||||
OutputContent::ClearOutputWaitMarker => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_clipboard_content(&self, cx: &WindowContext) -> bool {
|
||||
match &self.content {
|
||||
OutputContent::Plain(terminal) => terminal.has_clipboard_content(cx),
|
||||
OutputContent::Stream(terminal) => terminal.has_clipboard_content(cx),
|
||||
OutputContent::Image(image) => image.has_clipboard_content(cx),
|
||||
OutputContent::ErrorOutput(error) => error.traceback.has_clipboard_content(cx),
|
||||
OutputContent::Message(_) => false,
|
||||
OutputContent::Table(table) => table.has_clipboard_content(cx),
|
||||
OutputContent::Markdown(markdown) => markdown.read(cx).has_clipboard_content(cx),
|
||||
OutputContent::ClearOutputWaitMarker => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum OutputContent {
|
||||
Plain(TerminalOutput),
|
||||
Stream(TerminalOutput),
|
||||
|
@ -638,11 +788,42 @@ impl Render for ExecutionView {
|
|||
|
||||
div()
|
||||
.w_full()
|
||||
.children(
|
||||
self.outputs
|
||||
.iter()
|
||||
.filter_map(|output| output.content.render(cx)),
|
||||
)
|
||||
.children(self.outputs.iter().enumerate().map(|(index, output)| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_start()
|
||||
.child(
|
||||
div().flex_1().child(
|
||||
output
|
||||
.content
|
||||
.render(cx)
|
||||
.unwrap_or_else(|| div().into_any_element()),
|
||||
),
|
||||
)
|
||||
.when(output.has_clipboard_content(cx), |el| {
|
||||
let clipboard_content = output.clipboard_content(cx);
|
||||
|
||||
el.child(
|
||||
div().pl_1().child(
|
||||
IconButton::new(
|
||||
ElementId::Name(format!("copy-output-{}", index).into()),
|
||||
IconName::Copy,
|
||||
)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.tooltip(move |cx| Tooltip::text("Copy Output", cx))
|
||||
.on_click(cx.listener(
|
||||
move |_, _, cx| {
|
||||
if let Some(clipboard_content) = clipboard_content.as_ref()
|
||||
{
|
||||
cx.write_to_clipboard(clipboard_content.clone());
|
||||
// todo!(): let the user know that the content was copied
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.children(match self.status {
|
||||
ExecutionStatus::Executing => vec![status],
|
||||
ExecutionStatus::Queued => vec![status],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::outputs::ExecutionView;
|
||||
use alacritty_terminal::{term::Config, vte::ansi::Processor};
|
||||
use gpui::{canvas, size, AnyElement, FontStyle, TextStyle, WhiteSpace};
|
||||
use crate::outputs::{ExecutionView, SupportsClipboard};
|
||||
use alacritty_terminal::{grid::Dimensions as _, term::Config, vte::ansi::Processor};
|
||||
use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace};
|
||||
use settings::Settings as _;
|
||||
use std::mem;
|
||||
use terminal::ZedListener;
|
||||
|
@ -181,3 +181,22 @@ impl TerminalOutput {
|
|||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportsClipboard for TerminalOutput {
|
||||
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
|
||||
let start = alacritty_terminal::index::Point::new(
|
||||
alacritty_terminal::index::Line(0),
|
||||
alacritty_terminal::index::Column(0),
|
||||
);
|
||||
let end = alacritty_terminal::index::Point::new(
|
||||
alacritty_terminal::index::Line(self.handler.screen_lines() as i32 - 1),
|
||||
alacritty_terminal::index::Column(self.handler.columns() - 1),
|
||||
);
|
||||
let text = self.handler.bounds_to_string(start, end);
|
||||
Some(ClipboardItem::new_string(text.trim().into()))
|
||||
}
|
||||
|
||||
fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue