diff --git a/Cargo.lock b/Cargo.lock index c327163bce..42b66a0bac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,6 +924,7 @@ dependencies = [ "rand 0.8.3", "replace_with", "resvg", + "seahash", "serde", "serde_json", "simplelog", @@ -1712,6 +1713,20 @@ name = "serde" version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 7bf1913372..5bd32560c6 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -18,7 +18,8 @@ pathfinder_geometry = "0.5" rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" -serde = "1.0.125" +seahash = "4.1" +serde = { version = "1.0.125", features = ["derive"] } serde_json = "1.0.64" smallvec = "1.6.1" smol = "1.2" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 61c7ce6bc9..635817dae2 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -5,7 +5,7 @@ use crate::{ platform::{self, WindowOptions}, presenter::Presenter, util::post_inc, - AssetCache, AssetSource, FontCache, TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, TextLayoutCache, }; use anyhow::{anyhow, Result}; use async_std::sync::Condvar; @@ -1212,12 +1212,12 @@ impl MutableAppContext { } } - pub fn copy(&self, text: &str) { - self.platform.copy(text); + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item); } - pub fn paste(&self) -> Option { - self.platform.paste() + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() } } diff --git a/gpui/src/clipboard.rs b/gpui/src/clipboard.rs new file mode 100644 index 0000000000..b7e4b600ee --- /dev/null +++ b/gpui/src/clipboard.rs @@ -0,0 +1,42 @@ +use seahash::SeaHasher; +use serde::{Deserialize, Serialize}; +use std::hash::{Hash, Hasher}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardItem { + pub(crate) text: String, + pub(crate) metadata: Option, +} + +impl ClipboardItem { + pub fn new(text: String) -> Self { + Self { + text, + metadata: None, + } + } + + pub fn with_metadata(mut self, metadata: T) -> Self { + self.metadata = Some(serde_json::to_string(&metadata).unwrap()); + self + } + + pub fn text(&self) -> &String { + &self.text + } + + pub fn metadata(&self) -> Option + where + T: for<'a> Deserialize<'a>, + { + self.metadata + .as_ref() + .and_then(|m| serde_json::from_str(m).ok()) + } + + pub(crate) fn text_hash(text: &str) -> u64 { + let mut hasher = SeaHasher::new(); + text.hash(&mut hasher); + hasher.finish() + } +} diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index b60ce9b92d..ee8c544a83 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -7,6 +7,8 @@ pub use assets::*; pub mod elements; pub mod font_cache; pub use font_cache::FontCache; +mod clipboard; +pub use clipboard::ClipboardItem; pub mod fonts; pub mod geometry; mod presenter; diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 570d3ea14c..b7ce76f475 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window}; -use crate::{executor, keymap::Keystroke, platform, Event, Menu, MenuItem}; +use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem}; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, @@ -21,12 +21,13 @@ use ptr::null_mut; use std::{ any::Any, cell::RefCell, + convert::TryInto, ffi::{c_void, CStr}, os::raw::c_char, path::PathBuf, ptr, rc::Rc, - slice, + slice, str, sync::Arc, }; @@ -78,6 +79,9 @@ pub struct MacPlatform { fonts: Arc, callbacks: RefCell, menu_item_actions: RefCell>)>>, + pasteboard: id, + text_hash_pasteboard_type: id, + metadata_pasteboard_type: id, } #[derive(Default)] @@ -97,6 +101,9 @@ impl MacPlatform { fonts: Arc::new(FontSystem::new()), callbacks: Default::default(), menu_item_actions: Default::default(), + pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) }, + text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") }, + metadata_pasteboard_type: unsafe { ns_string("zed-metadata") }, } } @@ -177,6 +184,18 @@ impl MacPlatform { menu_bar } + + unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> { + let data = self.pasteboard.dataForType(kind); + if data == nil { + None + } else { + Some(slice::from_raw_parts( + data.bytes() as *mut u8, + data.length() as usize, + )) + } + } } impl platform::Platform for MacPlatform { @@ -287,28 +306,71 @@ impl platform::Platform for MacPlatform { } } - fn copy(&self, text: &str) { + fn write_to_clipboard(&self, item: ClipboardItem) { unsafe { - let data = NSData::dataWithBytes_length_( + self.pasteboard.clearContents(); + + let text_bytes = NSData::dataWithBytes_length_( nil, - text.as_ptr() as *const c_void, - text.len() as u64, + item.text.as_ptr() as *const c_void, + item.text.len() as u64, ); - let pasteboard = NSPasteboard::generalPasteboard(nil); - pasteboard.clearContents(); - pasteboard.setData_forType(data, NSPasteboardTypeString); + self.pasteboard + .setData_forType(text_bytes, NSPasteboardTypeString); + + if let Some(metadata) = item.metadata.as_ref() { + let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes(); + let hash_bytes = NSData::dataWithBytes_length_( + nil, + hash_bytes.as_ptr() as *const c_void, + hash_bytes.len() as u64, + ); + self.pasteboard + .setData_forType(hash_bytes, self.text_hash_pasteboard_type); + + let metadata_bytes = NSData::dataWithBytes_length_( + nil, + metadata.as_ptr() as *const c_void, + metadata.len() as u64, + ); + self.pasteboard + .setData_forType(metadata_bytes, self.metadata_pasteboard_type); + } } } - fn paste(&self) -> Option { + fn read_from_clipboard(&self) -> Option { unsafe { - let pasteboard = NSPasteboard::generalPasteboard(nil); - let data = pasteboard.dataForType(NSPasteboardTypeString); - if data == nil { - None + if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) { + let text = String::from_utf8_lossy(&text_bytes).to_string(); + let hash_bytes = self + .read_from_pasteboard(self.text_hash_pasteboard_type) + .and_then(|bytes| bytes.try_into().ok()) + .map(u64::from_be_bytes); + let metadata_bytes = self + .read_from_pasteboard(self.metadata_pasteboard_type) + .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok()); + + if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) { + if hash == ClipboardItem::text_hash(&text) { + Some(ClipboardItem { + text, + metadata: Some(metadata), + }) + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } } else { - let bytes = slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize); - Some(String::from_utf8_unchecked(bytes.to_vec())) + None } } } @@ -406,3 +468,46 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { unsafe fn ns_string(string: &str) -> id { NSString::alloc(nil).init_str(string).autorelease() } + +#[cfg(test)] +mod tests { + use crate::platform::Platform; + + use super::*; + + #[test] + fn test_clipboard() { + let platform = build_platform(); + assert_eq!(platform.read_from_clipboard(), None); + + let item = ClipboardItem::new("1".to_string()); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let text_from_other_app = "text from other app"; + unsafe { + let bytes = NSData::dataWithBytes_length_( + nil, + text_from_other_app.as_ptr() as *const c_void, + text_from_other_app.len() as u64, + ); + platform + .pasteboard + .setData_forType(bytes, NSPasteboardTypeString); + } + assert_eq!( + platform.read_from_clipboard(), + Some(ClipboardItem::new(text_from_other_app.to_string())) + ); + } + + fn build_platform() -> MacPlatform { + let mut platform = MacPlatform::new(); + platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) }; + platform + } +} diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index 39439970d8..5f34396a0e 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -15,7 +15,7 @@ use crate::{ vector::Vector2F, }, text_layout::Line, - Menu, Scene, + ClipboardItem, Menu, Scene, }; use async_task::Runnable; pub use event::Event; @@ -42,8 +42,8 @@ pub trait Platform { fn key_window_id(&self) -> Option; fn prompt_for_paths(&self, options: PathPromptOptions) -> Option>; fn quit(&self); - fn copy(&self, text: &str); - fn paste(&self) -> Option; + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_clipboard(&self) -> Option; fn set_menus(&self, menus: Vec); } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 19c015f688..536fb47da5 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -2,6 +2,8 @@ use pathfinder_geometry::vector::Vector2F; use std::sync::Arc; use std::{any::Any, rc::Rc}; +use crate::ClipboardItem; + struct Platform { dispatcher: Arc, fonts: Arc, @@ -72,9 +74,9 @@ impl super::Platform for Platform { None } - fn copy(&self, _: &str) {} + fn write_to_clipboard(&self, _: ClipboardItem) {} - fn paste(&self) -> Option { + fn read_from_clipboard(&self) -> Option { None } } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index b90b79fa12..9a0c2d1ce3 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -6,8 +6,8 @@ use crate::{settings::Settings, watch, workspace}; use anyhow::Result; use futures_core::future::LocalBoxFuture; use gpui::{ - fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, Element, - ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext, + fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, ClipboardItem, + Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext, WeakViewHandle, }; use gpui::{geometry::vector::Vector2F, TextLayoutCache}; @@ -479,7 +479,7 @@ impl BufferView { self.insert(&String::new(), ctx); self.end_transaction(ctx); - ctx.app_mut().copy(&text); + ctx.app_mut().write_to_clipboard(ClipboardItem::new(text)); } pub fn copy(&mut self, _: &(), ctx: &mut ViewContext) { @@ -496,12 +496,12 @@ impl BufferView { text.extend(buffer.text_for_range(start..end).unwrap()); } - ctx.app_mut().copy(&text); + ctx.app_mut().write_to_clipboard(ClipboardItem::new(text)); } pub fn paste(&mut self, _: &(), ctx: &mut ViewContext) { - if let Some(text) = ctx.app_mut().paste() { - self.insert(&text, ctx); + if let Some(item) = ctx.app_mut().read_from_clipboard() { + self.insert(item.text(), ctx); } } diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 6a9b3ca320..f43c6c4b9e 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -3,7 +3,7 @@ use crate::{settings::Settings, watch}; use futures_core::future::LocalBoxFuture; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, - Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, + ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, }; use log::{error, info}; use std::{collections::HashSet, path::PathBuf}; @@ -258,10 +258,11 @@ impl WorkspaceView { pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext) { match to_string_pretty(&ctx.debug_elements()) { Ok(json) => { - ctx.app_mut().copy(&json); + let kib = json.len() as f32 / 1024.; + ctx.app_mut().write_to_clipboard(ClipboardItem::new(json)); log::info!( "copied {:.1} KiB of element debug JSON to the clipboard", - json.len() as f32 / 1024. + kib ); } Err(error) => {