use core::slice;use std::ffi::{CStr, c_void};use std::path::PathBuf; use cocoa::{ appkit::{ NSFilenamesPboardType, NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, NSPasteboardTypeTIFF, }, base::{id, nil}, foundation::{NSArray, NSData, NSFastEnumeration, NSString},};use objc::{msg_send, rc::StrongPtr, runtime::Object, sel, sel_impl};use smallvec::SmallVec;use strum::IntoEnumIterator as _; use crate::ns_string;use gpui::{ ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,}; pub struct Pasteboard { inner: StrongPtr, text_hash_type: StrongPtr, metadata_type: StrongPtr,} impl Pasteboard { pub fn general() -> Self { unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) } } pub fn find() -> Self { unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) } } #[cfg(test)] pub fn unique() -> Self { unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) } } unsafe fn new(inner: id) -> Self { // These constructors return autoreleased objects, but a Pasteboard can // outlive the autorelease pool in which it was created. Self { inner: unsafe { StrongPtr::retain(inner) }, text_hash_type: unsafe { StrongPtr::retain(ns_string("zed-text-hash")) }, metadata_type: unsafe { StrongPtr::retain(ns_string("zed-metadata")) }, } } pub fn read(&self) -> Option<ClipboardItem> { unsafe { // Check for file paths first let filenames = NSPasteboard::propertyListForType(*self.inner, NSFilenamesPboardType); if filenames != nil && NSArray::count(filenames) > 0 { let mut paths = SmallVec::new(); for file in filenames.iter() { let f = NSString::UTF8String(file); let path = CStr::from_ptr(f).to_string_lossy().into_owned(); paths.push(PathBuf::from(path)); } if !paths.is_empty() { let mut entries = vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths))]; // Also include the string representation so text editors can // paste the path as text. if let Some(string_item) = self.read_string_from_pasteboard() { entries.push(string_item); } return Some(ClipboardItem { entries }); } } // Next, check for a plain string. if let Some(string_entry) = self.read_string_from_pasteboard() { return Some(ClipboardItem { entries: vec![string_entry], }); } // Finally, try the various supported image types. for format in ImageFormat::iter() { if let Some(item) = self.read_image(format) { return Some(item); } } } None } fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> { let ut_type: UTType = format.into(); unsafe { let types: id = self.inner.types(); if msg_send![types, containsObject: ut_type.inner()] { self.data_for_type(ut_type.inner_mut()).map(|bytes| { let bytes = bytes.to_vec(); let id = hash(&bytes); ClipboardItem { entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], } }) } else { None } } } unsafe fn read_string_from_pasteboard(&self) -> Option<ClipboardEntry> { unsafe { let pasteboard_types: id = self.inner.types(); let string_type: id = ns_string("public.utf8-plain-text"); if !msg_send![pasteboard_types, containsObject: string_type] { return None; } let text_bytes = self.data_for_type(string_type)?; let text = String::from_utf8_lossy(&text_bytes).to_string(); let metadata = self .data_for_type(*self.text_hash_type) .and_then(|hash_bytes| { let hash_bytes = hash_bytes.as_slice().try_into().ok()?; let hash = u64::from_be_bytes(hash_bytes); let metadata = self.data_for_type(*self.metadata_type)?; if hash == ClipboardString::text_hash(&text) { String::from_utf8(metadata).ok() } else { None } }); Some(ClipboardEntry::String(ClipboardString { text, metadata })) } } unsafe fn data_for_type(&self, kind: id) -> Option<Vec<u8>> { unsafe { let data = self.inner.dataForType(kind); if data == nil { None } else if data.bytes().is_null() { Some(Vec::new()) } else { Some( slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize).to_vec(), ) } } } pub fn write(&self, item: ClipboardItem) { unsafe { match item.entries.as_slice() { [] => { // Writing an empty list of entries just clears the clipboard. self.inner.clearContents(); } [ClipboardEntry::String(string)] => { self.write_plaintext(string); } [ClipboardEntry::Image(image)] => { self.write_image(image); } [ClipboardEntry::ExternalPaths(_)] => {} _ => { // Agus NB: We're currently only writing string entries to the clipboard when we have more than one. // // This was the existing behavior before I refactored the outer clipboard code: // https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110 // // Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor. let mut combined = ClipboardString { text: String::new(), metadata: None, }; for entry in item.entries { match entry { ClipboardEntry::String(text) => { combined.text.push_str(&text.text()); if combined.metadata.is_none() { combined.metadata = text.metadata; } } _ => {} } } self.write_plaintext(&combined); } } } } fn write_plaintext(&self, string: &ClipboardString) { unsafe { self.inner.clearContents(); let text_bytes = NSData::dataWithBytes_length_( nil, string.text.as_ptr() as *const c_void, string.text.len() as u64, ); self.inner .setData_forType(text_bytes, NSPasteboardTypeString); if let Some(metadata) = string.metadata.as_ref() { let hash_bytes = ClipboardString::text_hash(&string.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.inner.setData_forType(hash_bytes, *self.text_hash_type); let metadata_bytes = NSData::dataWithBytes_length_( nil, metadata.as_ptr() as *const c_void, metadata.len() as u64, ); self.inner .setData_forType(metadata_bytes, *self.metadata_type); } } } unsafe fn write_image(&self, image: &Image) { unsafe { self.inner.clearContents(); let bytes = NSData::dataWithBytes_length_( nil, image.bytes.as_ptr() as *const c_void, image.bytes.len() as u64, ); self.inner .setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut()); } }} #[link(name = "AppKit", kind = "framework")]unsafe extern "C" { /// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc) pub static NSPasteboardNameFind: id;} impl From<ImageFormat> for UTType { fn from(value: ImageFormat) -> Self { match value { ImageFormat::Png => Self::png(), ImageFormat::Jpeg => Self::jpeg(), ImageFormat::Tiff => Self::tiff(), ImageFormat::Webp => Self::webp(), ImageFormat::Gif => Self::gif(), ImageFormat::Bmp => Self::bmp(), ImageFormat::Svg => Self::svg(), ImageFormat::Ico => Self::ico(), ImageFormat::Pnm => Self::pnm(), } }} // See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/pub struct UTType(id); impl UTType { pub fn png() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType } pub fn jpeg() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg Self(unsafe { ns_string("public.jpeg") }) } pub fn gif() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif Self(unsafe { ns_string("com.compuserve.gif") }) } pub fn webp() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp Self(unsafe { ns_string("org.webmproject.webp") }) } pub fn bmp() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp Self(unsafe { ns_string("com.microsoft.bmp") }) } pub fn svg() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg Self(unsafe { ns_string("public.svg-image") }) } pub fn ico() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico Self(unsafe { ns_string("com.microsoft.ico") }) } pub fn tiff() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType } pub fn pnm() -> Self { //https://en.wikipedia.org/w/index.php?title=Netpbm&oldid=1336679433 under Uniform Type Identifier Self(unsafe { ns_string("public.pbm") }) } fn inner(&self) -> *const Object { self.0 } pub fn inner_mut(&self) -> *mut Object { self.0 as *mut _ }} #[cfg(test)]mod tests { use cocoa::{ appkit::{NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeString}, base::{id, nil}, foundation::{NSArray, NSData}, }; use std::ffi::c_void; use gpui::{ClipboardEntry, ClipboardItem, ClipboardString, ImageFormat}; use objc::rc::autoreleasepool; use super::*; unsafe fn simulate_external_file_copy(pasteboard: &Pasteboard, paths: &[&str]) { unsafe { let ns_paths: Vec<id> = paths.iter().map(|p| ns_string(p)).collect(); let ns_array = NSArray::arrayWithObjects(nil, &ns_paths); let mut types = vec![NSFilenamesPboardType]; types.push(NSPasteboardTypeString); let types_array = NSArray::arrayWithObjects(nil, &types); pasteboard.inner.declareTypes_owner(types_array, nil); pasteboard .inner .setPropertyList_forType(ns_array, NSFilenamesPboardType); let joined = paths.join("\n"); let bytes = NSData::dataWithBytes_length_( nil, joined.as_ptr() as *const c_void, joined.len() as u64, ); pasteboard .inner .setData_forType(bytes, NSPasteboardTypeString); } } #[test] fn test_string() { let pasteboard = Pasteboard::unique(); assert_eq!(pasteboard.read(), None); let item = ClipboardItem::new_string("1".to_string()); pasteboard.write(item.clone()); assert_eq!(pasteboard.read(), Some(item)); let item = ClipboardItem { entries: vec![ClipboardEntry::String( ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]), )], }; pasteboard.write(item.clone()); assert_eq!(pasteboard.read(), 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, ); pasteboard .inner .setData_forType(bytes, NSPasteboardTypeString); } assert_eq!( pasteboard.read(), Some(ClipboardItem::new_string(text_from_other_app.to_string())) ); } #[test] fn test_custom_types_survive_creation_autorelease_pool() { let pasteboard = autoreleasepool(|| unsafe { Pasteboard::new(nil) }); unsafe { let text_hash_type = CStr::from_ptr(NSString::UTF8String(*pasteboard.text_hash_type)); let metadata_type = CStr::from_ptr(NSString::UTF8String(*pasteboard.metadata_type)); assert_eq!(text_hash_type.to_bytes(), b"zed-text-hash"); assert_eq!(metadata_type.to_bytes(), b"zed-metadata"); } } #[test] fn test_read_external_path() { let pasteboard = Pasteboard::unique(); unsafe { simulate_external_file_copy(&pasteboard, &["/test.txt"]); } let item = pasteboard.read().expect("should read clipboard item"); // Test both ExternalPaths and String entries exist assert_eq!(item.entries.len(), 2); // Test first entry is ExternalPaths match &item.entries[0] { ClipboardEntry::ExternalPaths(ep) => { assert_eq!(ep.paths(), &[PathBuf::from("/test.txt")]); } other => panic!("expected ExternalPaths, got {:?}", other), } // Test second entry is String match &item.entries[1] { ClipboardEntry::String(s) => { assert_eq!(s.text(), "/test.txt"); } other => panic!("expected String, got {:?}", other), } } #[test] fn test_read_external_paths_with_spaces() { let pasteboard = Pasteboard::unique(); let paths = ["/some file with spaces.txt"]; unsafe { simulate_external_file_copy(&pasteboard, &paths); } let item = pasteboard.read().expect("should read clipboard item"); match &item.entries[0] { ClipboardEntry::ExternalPaths(ep) => { assert_eq!(ep.paths(), &[PathBuf::from("/some file with spaces.txt")]); } other => panic!("expected ExternalPaths, got {:?}", other), } } #[test] fn test_read_multiple_external_paths() { let pasteboard = Pasteboard::unique(); let paths = ["/file.txt", "/image.png"]; unsafe { simulate_external_file_copy(&pasteboard, &paths); } let item = pasteboard.read().expect("should read clipboard item"); assert_eq!(item.entries.len(), 2); // Test both ExternalPaths and String entries exist match &item.entries[0] { ClipboardEntry::ExternalPaths(ep) => { assert_eq!( ep.paths(), &[PathBuf::from("/file.txt"), PathBuf::from("/image.png"),] ); } other => panic!("expected ExternalPaths, got {:?}", other), } match &item.entries[1] { ClipboardEntry::String(s) => { assert_eq!(s.text(), "/file.txt\n/image.png"); assert_eq!(s.metadata, None); } other => panic!("expected String, got {:?}", other), } } #[test]Lines 1–500 of 538. Load more
use core::slice; use std::ffi::{CStr, c_void}; use std::path::PathBuf; use cocoa::{ appkit::{ NSFilenamesPboardType, NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, NSPasteboardTypeTIFF, }, base::{id, nil}, foundation::{NSArray, NSData, NSFastEnumeration, NSString}, }; use objc::{msg_send, rc::StrongPtr, runtime::Object, sel, sel_impl}; use smallvec::SmallVec; use strum::IntoEnumIterator as _; use crate::ns_string; use gpui::{ ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash, }; pub struct Pasteboard { inner: StrongPtr, text_hash_type: StrongPtr, metadata_type: StrongPtr, } impl Pasteboard { pub fn general() -> Self { unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) } } pub fn find() -> Self { unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) } } #[cfg(test)] pub fn unique() -> Self { unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) } } unsafe fn new(inner: id) -> Self { // These constructors return autoreleased objects, but a Pasteboard can // outlive the autorelease pool in which it was created. Self { inner: unsafe { StrongPtr::retain(inner) }, text_hash_type: unsafe { StrongPtr::retain(ns_string("zed-text-hash")) }, metadata_type: unsafe { StrongPtr::retain(ns_string("zed-metadata")) }, } } pub fn read(&self) -> Option<ClipboardItem> { unsafe { // Check for file paths first let filenames = NSPasteboard::propertyListForType(*self.inner, NSFilenamesPboardType); if filenames != nil && NSArray::count(filenames) > 0 { let mut paths = SmallVec::new(); for file in filenames.iter() { let f = NSString::UTF8String(file); let path = CStr::from_ptr(f).to_string_lossy().into_owned(); paths.push(PathBuf::from(path)); } if !paths.is_empty() { let mut entries = vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths))]; // Also include the string representation so text editors can // paste the path as text. if let Some(string_item) = self.read_string_from_pasteboard() { entries.push(string_item); } return Some(ClipboardItem { entries }); } } // Next, check for a plain string. if let Some(string_entry) = self.read_string_from_pasteboard() { return Some(ClipboardItem { entries: vec![string_entry], }); } // Finally, try the various supported image types. for format in ImageFormat::iter() { if let Some(item) = self.read_image(format) { return Some(item); } } } None } fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> { let ut_type: UTType = format.into(); unsafe { let types: id = self.inner.types(); if msg_send![types, containsObject: ut_type.inner()] { self.data_for_type(ut_type.inner_mut()).map(|bytes| { let bytes = bytes.to_vec(); let id = hash(&bytes); ClipboardItem { entries: vec![ClipboardEntry::Image(Image { format, bytes, id })], } }) } else { None } } } unsafe fn read_string_from_pasteboard(&self) -> Option<ClipboardEntry> { unsafe { let pasteboard_types: id = self.inner.types(); let string_type: id = ns_string("public.utf8-plain-text"); if !msg_send![pasteboard_types, containsObject: string_type] { return None; } let text_bytes = self.data_for_type(string_type)?; let text = String::from_utf8_lossy(&text_bytes).to_string(); let metadata = self .data_for_type(*self.text_hash_type) .and_then(|hash_bytes| { let hash_bytes = hash_bytes.as_slice().try_into().ok()?; let hash = u64::from_be_bytes(hash_bytes); let metadata = self.data_for_type(*self.metadata_type)?; if hash == ClipboardString::text_hash(&text) { String::from_utf8(metadata).ok() } else { None } }); Some(ClipboardEntry::String(ClipboardString { text, metadata })) } } unsafe fn data_for_type(&self, kind: id) -> Option<Vec<u8>> { unsafe { let data = self.inner.dataForType(kind); if data == nil { None } else if data.bytes().is_null() { Some(Vec::new()) } else { Some( slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize).to_vec(), ) } } } pub fn write(&self, item: ClipboardItem) { unsafe { match item.entries.as_slice() { [] => { // Writing an empty list of entries just clears the clipboard. self.inner.clearContents(); } [ClipboardEntry::String(string)] => { self.write_plaintext(string); } [ClipboardEntry::Image(image)] => { self.write_image(image); } [ClipboardEntry::ExternalPaths(_)] => {} _ => { // Agus NB: We're currently only writing string entries to the clipboard when we have more than one. // // This was the existing behavior before I refactored the outer clipboard code: // https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110 // // Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor. let mut combined = ClipboardString { text: String::new(), metadata: None, }; for entry in item.entries { match entry { ClipboardEntry::String(text) => { combined.text.push_str(&text.text()); if combined.metadata.is_none() { combined.metadata = text.metadata; } } _ => {} } } self.write_plaintext(&combined); } } } } fn write_plaintext(&self, string: &ClipboardString) { unsafe { self.inner.clearContents(); let text_bytes = NSData::dataWithBytes_length_( nil, string.text.as_ptr() as *const c_void, string.text.len() as u64, ); self.inner .setData_forType(text_bytes, NSPasteboardTypeString); if let Some(metadata) = string.metadata.as_ref() { let hash_bytes = ClipboardString::text_hash(&string.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.inner.setData_forType(hash_bytes, *self.text_hash_type); let metadata_bytes = NSData::dataWithBytes_length_( nil, metadata.as_ptr() as *const c_void, metadata.len() as u64, ); self.inner .setData_forType(metadata_bytes, *self.metadata_type); } } } unsafe fn write_image(&self, image: &Image) { unsafe { self.inner.clearContents(); let bytes = NSData::dataWithBytes_length_( nil, image.bytes.as_ptr() as *const c_void, image.bytes.len() as u64, ); self.inner .setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut()); } } } #[link(name = "AppKit", kind = "framework")] unsafe extern "C" { /// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc) pub static NSPasteboardNameFind: id; } impl From<ImageFormat> for UTType { fn from(value: ImageFormat) -> Self { match value { ImageFormat::Png => Self::png(), ImageFormat::Jpeg => Self::jpeg(), ImageFormat::Tiff => Self::tiff(), ImageFormat::Webp => Self::webp(), ImageFormat::Gif => Self::gif(), ImageFormat::Bmp => Self::bmp(), ImageFormat::Svg => Self::svg(), ImageFormat::Ico => Self::ico(), ImageFormat::Pnm => Self::pnm(), } } } // See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ pub struct UTType(id); impl UTType { pub fn png() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType } pub fn jpeg() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg Self(unsafe { ns_string("public.jpeg") }) } pub fn gif() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif Self(unsafe { ns_string("com.compuserve.gif") }) } pub fn webp() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp Self(unsafe { ns_string("org.webmproject.webp") }) } pub fn bmp() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp Self(unsafe { ns_string("com.microsoft.bmp") }) } pub fn svg() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg Self(unsafe { ns_string("public.svg-image") }) } pub fn ico() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico Self(unsafe { ns_string("com.microsoft.ico") }) } pub fn tiff() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType } pub fn pnm() -> Self { //https://en.wikipedia.org/w/index.php?title=Netpbm&oldid=1336679433 under Uniform Type Identifier Self(unsafe { ns_string("public.pbm") }) } fn inner(&self) -> *const Object { self.0 } pub fn inner_mut(&self) -> *mut Object { self.0 as *mut _ } } #[cfg(test)] mod tests { use cocoa::{ appkit::{NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeString}, base::{id, nil}, foundation::{NSArray, NSData}, }; use std::ffi::c_void; use gpui::{ClipboardEntry, ClipboardItem, ClipboardString, ImageFormat}; use objc::rc::autoreleasepool; use super::*; unsafe fn simulate_external_file_copy(pasteboard: &Pasteboard, paths: &[&str]) { unsafe { let ns_paths: Vec<id> = paths.iter().map(|p| ns_string(p)).collect(); let ns_array = NSArray::arrayWithObjects(nil, &ns_paths); let mut types = vec![NSFilenamesPboardType]; types.push(NSPasteboardTypeString); let types_array = NSArray::arrayWithObjects(nil, &types); pasteboard.inner.declareTypes_owner(types_array, nil); pasteboard .inner .setPropertyList_forType(ns_array, NSFilenamesPboardType); let joined = paths.join("\n"); let bytes = NSData::dataWithBytes_length_( nil, joined.as_ptr() as *const c_void, joined.len() as u64, ); pasteboard .inner .setData_forType(bytes, NSPasteboardTypeString); } } #[test] fn test_string() { let pasteboard = Pasteboard::unique(); assert_eq!(pasteboard.read(), None); let item = ClipboardItem::new_string("1".to_string()); pasteboard.write(item.clone()); assert_eq!(pasteboard.read(), Some(item)); let item = ClipboardItem { entries: vec![ClipboardEntry::String( ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]), )], }; pasteboard.write(item.clone()); assert_eq!(pasteboard.read(), 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, ); pasteboard .inner .setData_forType(bytes, NSPasteboardTypeString); } assert_eq!( pasteboard.read(), Some(ClipboardItem::new_string(text_from_other_app.to_string())) ); } #[test] fn test_custom_types_survive_creation_autorelease_pool() { let pasteboard = autoreleasepool(|| unsafe { Pasteboard::new(nil) }); unsafe { let text_hash_type = CStr::from_ptr(NSString::UTF8String(*pasteboard.text_hash_type)); let metadata_type = CStr::from_ptr(NSString::UTF8String(*pasteboard.metadata_type)); assert_eq!(text_hash_type.to_bytes(), b"zed-text-hash"); assert_eq!(metadata_type.to_bytes(), b"zed-metadata"); } } #[test] fn test_read_external_path() { let pasteboard = Pasteboard::unique(); unsafe { simulate_external_file_copy(&pasteboard, &["/test.txt"]); } let item = pasteboard.read().expect("should read clipboard item"); // Test both ExternalPaths and String entries exist assert_eq!(item.entries.len(), 2); // Test first entry is ExternalPaths match &item.entries[0] { ClipboardEntry::ExternalPaths(ep) => { assert_eq!(ep.paths(), &[PathBuf::from("/test.txt")]); } other => panic!("expected ExternalPaths, got {:?}", other), } // Test second entry is String match &item.entries[1] { ClipboardEntry::String(s) => { assert_eq!(s.text(), "/test.txt"); } other => panic!("expected String, got {:?}", other), } } #[test] fn test_read_external_paths_with_spaces() { let pasteboard = Pasteboard::unique(); let paths = ["/some file with spaces.txt"]; unsafe { simulate_external_file_copy(&pasteboard, &paths); } let item = pasteboard.read().expect("should read clipboard item"); match &item.entries[0] { ClipboardEntry::ExternalPaths(ep) => { assert_eq!(ep.paths(), &[PathBuf::from("/some file with spaces.txt")]); } other => panic!("expected ExternalPaths, got {:?}", other), } } #[test] fn test_read_multiple_external_paths() { let pasteboard = Pasteboard::unique(); let paths = ["/file.txt", "/image.png"]; unsafe { simulate_external_file_copy(&pasteboard, &paths); } let item = pasteboard.read().expect("should read clipboard item"); assert_eq!(item.entries.len(), 2); // Test both ExternalPaths and String entries exist match &item.entries[0] { ClipboardEntry::ExternalPaths(ep) => { assert_eq!( ep.paths(), &[PathBuf::from("/file.txt"), PathBuf::from("/image.png"),] ); } other => panic!("expected ExternalPaths, got {:?}", other), } match &item.entries[1] { ClipboardEntry::String(s) => { assert_eq!(s.text(), "/file.txt\n/image.png"); assert_eq!(s.metadata, None); } other => panic!("expected String, got {:?}", other), } } #[test]