Copy/paste images into editors (Mac only) (#15782)

For future reference: WIP branch of copy/pasting a mixture of images and
text: https://github.com/zed-industries/zed/tree/copy-paste-images -
we'll come back to that one after landing this one.

Release Notes:

- You can now paste images into the Assistant Panel to include them as
context. Currently works only on Mac, and with Anthropic models. Future
support is planned for more models, operating systems, and image
clipboard operations.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Jason <jason@zed.dev>
Co-authored-by: Kyle <kylek@zed.dev>
This commit is contained in:
Richard Feldman 2024-08-13 13:18:25 -04:00 committed by GitHub
parent e3b0de5dda
commit b1a581e81b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 2983 additions and 1708 deletions

View file

@ -2,8 +2,8 @@ use futures::Future;
use git::blame::BlameEntry;
use git::Oid;
use gpui::{
Asset, ClipboardItem, Element, ParentElement, Render, ScrollHandle, StatefulInteractiveElement,
WeakView, WindowContext,
AppContext, Asset, ClipboardItem, Element, ParentElement, Render, ScrollHandle,
StatefulInteractiveElement, WeakView,
};
use settings::Settings;
use std::hash::Hash;
@ -35,7 +35,7 @@ impl<'a> CommitAvatar<'a> {
let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha);
let element = match cx.use_cached_asset::<CommitAvatarAsset>(&avatar_url) {
let element = match cx.use_asset::<CommitAvatarAsset>(&avatar_url) {
// Loading or no avatar found
None | Some(None) => Icon::new(IconName::Person)
.color(Color::Muted)
@ -73,7 +73,7 @@ impl Asset for CommitAvatarAsset {
fn load(
source: Self::Source,
cx: &mut WindowContext,
cx: &mut AppContext,
) -> impl Future<Output = Self::Output> + Send + 'static {
let client = cx.http_client();
@ -242,9 +242,9 @@ impl Render for BlameEntryTooltip {
.icon_color(Color::Muted)
.on_click(move |_, cx| {
cx.stop_propagation();
cx.write_to_clipboard(ClipboardItem::new(
full_sha.clone(),
))
cx.write_to_clipboard(
ClipboardItem::new_string(full_sha.clone()),
)
}),
),
),

View file

@ -69,13 +69,13 @@ use git::blame::GitBlame;
use git::diff_hunk_to_display;
use gpui::{
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle, FocusOutEvent,
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle,
UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle,
WeakView, WindowContext,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
ClipboardItem, Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle,
FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText,
KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render,
SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
WeakFocusHandle, WeakView, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@ -2304,7 +2304,7 @@ impl Editor {
}
if !text.is_empty() {
cx.write_to_primary(ClipboardItem::new(text));
cx.write_to_primary(ClipboardItem::new_string(text));
}
}
@ -6585,7 +6585,10 @@ impl Editor {
s.select(selections);
});
this.insert("", cx);
cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
text,
clipboard_selections,
));
});
}
@ -6624,7 +6627,10 @@ impl Editor {
}
}
cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
text,
clipboard_selections,
));
}
pub fn do_paste(
@ -6708,13 +6714,21 @@ impl Editor {
pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.read_from_clipboard() {
self.do_paste(
item.text(),
item.metadata::<Vec<ClipboardSelection>>(),
true,
cx,
)
};
let entries = item.entries();
match entries.first() {
// For now, we only support applying metadata if there's one string. In the future, we can incorporate all the selections
// of all the pasted entries.
Some(ClipboardEntry::String(clipboard_string)) if entries.len() == 1 => self
.do_paste(
clipboard_string.text(),
clipboard_string.metadata_json::<Vec<ClipboardSelection>>(),
true,
cx,
),
_ => self.do_paste(&item.text().unwrap_or_default(), None, true, cx),
}
}
}
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
@ -10535,7 +10549,7 @@ impl Editor {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
if let Some(path) = file.abs_path(cx).to_str() {
cx.write_to_clipboard(ClipboardItem::new(path.to_string()));
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
}
}
}
@ -10545,7 +10559,7 @@ impl Editor {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
if let Some(path) = file.path().to_str() {
cx.write_to_clipboard(ClipboardItem::new(path.to_string()));
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
}
}
}
@ -10735,7 +10749,7 @@ impl Editor {
match permalink {
Ok(permalink) => {
cx.write_to_clipboard(ClipboardItem::new(permalink.to_string()));
cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
}
Err(err) => {
let message = format!("Failed to copy permalink: {err}");
@ -11671,7 +11685,7 @@ impl Editor {
let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else {
return;
};
cx.write_to_clipboard(ClipboardItem::new(lines));
cx.write_to_clipboard(ClipboardItem::new_string(lines));
}
pub fn inlay_hint_cache(&self) -> &InlayHintCache {
@ -12938,7 +12952,9 @@ pub fn diagnostic_block_renderer(
.visible_on_hover(group_id.clone())
.on_click({
let message = diagnostic.message.clone();
move |_click, cx| cx.write_to_clipboard(ClipboardItem::new(message.clone()))
move |_click, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
}
})
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
)

View file

@ -3956,8 +3956,9 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
the lazy dog"});
cx.update_editor(|e, cx| e.copy(&Copy, cx));
assert_eq!(
cx.read_from_clipboard().map(|item| item.text().to_owned()),
Some("fox jumps over\n".to_owned())
cx.read_from_clipboard()
.and_then(|item| item.text().as_deref().map(str::to_string)),
Some("fox jumps over\n".to_string())
);
// Paste with three selections, noticing how the copied full-line selection is inserted

View file

@ -642,7 +642,7 @@ impl EditorElement {
}
#[cfg(target_os = "linux")]
if let Some(item) = cx.read_from_primary() {
if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
@ -655,7 +655,7 @@ impl EditorElement {
},
cx,
);
editor.insert(item.text(), cx);
editor.insert(&text, cx);
}
cx.stop_propagation()
}
@ -4290,7 +4290,7 @@ fn deploy_blame_entry_context_menu(
let sha = format!("{}", blame_entry.sha);
menu.on_blur_subscription(Subscription::new(|| {}))
.entry("Copy commit SHA", None, move |cx| {
cx.write_to_clipboard(ClipboardItem::new(sha.clone()));
cx.write_to_clipboard(ClipboardItem::new_string(sha.clone()));
})
.when_some(
details.and_then(|details| details.permalink.clone()),