Detailed changes
@@ -1077,11 +1077,9 @@ impl App {
self.platform.window_appearance()
}
- /// Writes data to the primary selection buffer.
- /// Only available on Linux.
- #[cfg(any(target_os = "linux", target_os = "freebsd"))]
- pub fn write_to_primary(&self, item: ClipboardItem) {
- self.platform.write_to_primary(item)
+ /// Reads data from the platform clipboard.
+ pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+ self.platform.read_from_clipboard()
}
/// Writes data to the platform clipboard.
@@ -1096,9 +1094,31 @@ impl App {
self.platform.read_from_primary()
}
- /// Reads data from the platform clipboard.
- pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
- self.platform.read_from_clipboard()
+ /// Writes data to the primary selection buffer.
+ /// Only available on Linux.
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ pub fn write_to_primary(&self, item: ClipboardItem) {
+ self.platform.write_to_primary(item)
+ }
+
+ /// Reads data from macOS's "Find" pasteboard.
+ ///
+ /// Used to share the current search string between apps.
+ ///
+ /// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
+ #[cfg(target_os = "macos")]
+ pub fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
+ self.platform.read_from_find_pasteboard()
+ }
+
+ /// Writes data to macOS's "Find" pasteboard.
+ ///
+ /// Used to share the current search string between apps.
+ ///
+ /// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
+ #[cfg(target_os = "macos")]
+ pub fn write_to_find_pasteboard(&self, item: ClipboardItem) {
+ self.platform.write_to_find_pasteboard(item)
}
/// Writes credentials to the platform keychain.
@@ -262,12 +262,18 @@ pub(crate) trait Platform: 'static {
fn set_cursor_style(&self, style: CursorStyle);
fn should_auto_hide_scrollbars(&self) -> bool;
- #[cfg(any(target_os = "linux", target_os = "freebsd"))]
- fn write_to_primary(&self, item: ClipboardItem);
+ fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn write_to_clipboard(&self, item: ClipboardItem);
+
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn read_from_primary(&self) -> Option<ClipboardItem>;
- fn read_from_clipboard(&self) -> Option<ClipboardItem>;
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ fn write_to_primary(&self, item: ClipboardItem);
+
+ #[cfg(target_os = "macos")]
+ fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
+ #[cfg(target_os = "macos")]
+ fn write_to_find_pasteboard(&self, item: ClipboardItem);
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
@@ -5,6 +5,7 @@ mod display;
mod display_link;
mod events;
mod keyboard;
+mod pasteboard;
#[cfg(feature = "screen-capture")]
mod screen_capture;
@@ -21,8 +22,6 @@ use metal_renderer as renderer;
#[cfg(feature = "macos-blade")]
use crate::platform::blade as renderer;
-mod attributed_string;
-
#[cfg(feature = "font-kit")]
mod open_type;
@@ -1,129 +0,0 @@
-use cocoa::base::id;
-use cocoa::foundation::NSRange;
-use objc::{class, msg_send, sel, sel_impl};
-
-/// The `cocoa` crate does not define NSAttributedString (and related Cocoa classes),
-/// which are needed for copying rich text (that is, text intermingled with images)
-/// to the clipboard. This adds access to those APIs.
-#[allow(non_snake_case)]
-pub trait NSAttributedString: Sized {
- unsafe fn alloc(_: Self) -> id {
- msg_send![class!(NSAttributedString), alloc]
- }
-
- unsafe fn init_attributed_string(self, string: id) -> id;
- unsafe fn appendAttributedString_(self, attr_string: id);
- unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
- unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
- unsafe fn string(self) -> id;
-}
-
-impl NSAttributedString for id {
- unsafe fn init_attributed_string(self, string: id) -> id {
- msg_send![self, initWithString: string]
- }
-
- unsafe fn appendAttributedString_(self, attr_string: id) {
- let _: () = msg_send![self, appendAttributedString: attr_string];
- }
-
- unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
- msg_send![self, RTFDFromRange: range documentAttributes: attrs]
- }
-
- unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
- msg_send![self, RTFFromRange: range documentAttributes: attrs]
- }
-
- unsafe fn string(self) -> id {
- msg_send![self, string]
- }
-}
-
-pub trait NSMutableAttributedString: NSAttributedString {
- unsafe fn alloc(_: Self) -> id {
- msg_send![class!(NSMutableAttributedString), alloc]
- }
-}
-
-impl NSMutableAttributedString for id {}
-
-#[cfg(test)]
-mod tests {
- use crate::platform::mac::ns_string;
-
- use super::*;
- use cocoa::appkit::NSImage;
- use cocoa::base::nil;
- use cocoa::foundation::NSAutoreleasePool;
- #[test]
- #[ignore] // This was SIGSEGV-ing on CI but not locally; need to investigate https://github.com/zed-industries/zed/actions/runs/10362363230/job/28684225486?pr=15782#step:4:1348
- fn test_nsattributed_string() {
- // TODO move these to parent module once it's actually ready to be used
- #[allow(non_snake_case)]
- pub trait NSTextAttachment: Sized {
- unsafe fn alloc(_: Self) -> id {
- msg_send![class!(NSTextAttachment), alloc]
- }
- }
-
- impl NSTextAttachment for id {}
-
- unsafe {
- let image: id = {
- let img: id = msg_send![class!(NSImage), alloc];
- let img: id = msg_send![img, initWithContentsOfFile: ns_string("test.jpeg")];
- let img: id = msg_send![img, autorelease];
- img
- };
- let _size = image.size();
-
- let string = ns_string("Test String");
- let attr_string = NSMutableAttributedString::alloc(nil)
- .init_attributed_string(string)
- .autorelease();
- let hello_string = ns_string("Hello World");
- let hello_attr_string = NSAttributedString::alloc(nil)
- .init_attributed_string(hello_string)
- .autorelease();
- attr_string.appendAttributedString_(hello_attr_string);
-
- let attachment: id = msg_send![NSTextAttachment::alloc(nil), autorelease];
- let _: () = msg_send![attachment, setImage: image];
- let image_attr_string =
- msg_send![class!(NSAttributedString), attributedStringWithAttachment: attachment];
- attr_string.appendAttributedString_(image_attr_string);
-
- let another_string = ns_string("Another String");
- let another_attr_string = NSAttributedString::alloc(nil)
- .init_attributed_string(another_string)
- .autorelease();
- attr_string.appendAttributedString_(another_attr_string);
-
- let _len: cocoa::foundation::NSUInteger = msg_send![attr_string, length];
-
- ///////////////////////////////////////////////////
- // pasteboard.clearContents();
-
- let rtfd_data = attr_string.RTFDFromRange_documentAttributes_(
- NSRange::new(0, msg_send![attr_string, length]),
- nil,
- );
- assert_ne!(rtfd_data, nil);
- // if rtfd_data != nil {
- // pasteboard.setData_forType(rtfd_data, NSPasteboardTypeRTFD);
- // }
-
- // let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
- // NSRange::new(0, attributed_string.length()),
- // nil,
- // );
- // if rtf_data != nil {
- // pasteboard.setData_forType(rtf_data, NSPasteboardTypeRTF);
- // }
-
- // let plain_text = attributed_string.string();
- // pasteboard.setString_forType(plain_text, NSPasteboardTypeString);
- }
- }
-}
@@ -0,0 +1,344 @@
+use core::slice;
+use std::ffi::c_void;
+
+use cocoa::{
+ appkit::{NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, NSPasteboardTypeTIFF},
+ base::{id, nil},
+ foundation::NSData,
+};
+use objc::{msg_send, runtime::Object, sel, sel_impl};
+use strum::IntoEnumIterator as _;
+
+use crate::{
+ ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, asset_cache::hash,
+ platform::mac::ns_string,
+};
+
+pub struct Pasteboard {
+ inner: id,
+ text_hash_type: id,
+ metadata_type: id,
+}
+
+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 {
+ Self {
+ inner,
+ text_hash_type: unsafe { ns_string("zed-text-hash") },
+ metadata_type: unsafe { ns_string("zed-metadata") },
+ }
+ }
+
+ pub fn read(&self) -> Option<ClipboardItem> {
+ // First, see if it's a string.
+ 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] {
+ let data = self.inner.dataForType(string_type);
+ if data == nil {
+ return None;
+ } else if data.bytes().is_null() {
+ // https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc
+ // "If the length of the NSData object is 0, this property returns nil."
+ return Some(self.read_string(&[]));
+ } else {
+ let bytes =
+ slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize);
+
+ return Some(self.read_string(bytes));
+ }
+ }
+
+ // If it wasn't a string, try the various supported image types.
+ for format in ImageFormat::iter() {
+ if let Some(item) = self.read_image(format) {
+ return Some(item);
+ }
+ }
+ }
+
+ // If it wasn't a string or a supported image type, give up.
+ None
+ }
+
+ fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> {
+ let mut 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
+ }
+ }
+ }
+
+ fn read_string(&self, text_bytes: &[u8]) -> ClipboardItem {
+ unsafe {
+ 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.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.to_vec()).ok()
+ } else {
+ None
+ }
+ });
+
+ ClipboardItem {
+ entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })],
+ }
+ }
+ }
+
+ unsafe fn data_for_type(&self, kind: id) -> Option<&[u8]> {
+ unsafe {
+ let data = self.inner.dataForType(kind);
+ if data == nil {
+ None
+ } else {
+ Some(slice::from_raw_parts(
+ data.bytes() as *mut u8,
+ data.length() as usize,
+ ))
+ }
+ }
+ }
+
+ 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(),
+ }
+ }
+}
+
+// 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
+ }
+
+ 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::NSPasteboardTypeString, foundation::NSData};
+
+ use crate::{ClipboardEntry, ClipboardItem, ClipboardString};
+
+ use super::*;
+
+ #[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()))
+ );
+ }
+}
@@ -1,29 +1,24 @@
use super::{
- BoolExt, MacKeyboardLayout, MacKeyboardMapper,
- attributed_string::{NSAttributedString, NSMutableAttributedString},
- events::key_to_native,
- ns_string, renderer,
+ BoolExt, MacKeyboardLayout, MacKeyboardMapper, events::key_to_native, ns_string, renderer,
};
use crate::{
- Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
- CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
- MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
- PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
- PlatformWindow, Result, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
+ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
+ KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu,
+ PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
+ PlatformTextSystem, PlatformWindow, Result, SystemMenuType, Task, WindowAppearance,
+ WindowParams, platform::mac::pasteboard::Pasteboard,
};
use anyhow::{Context as _, anyhow};
use block::ConcreteBlock;
use cocoa::{
appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
- NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
- NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeRTFD, NSPasteboardTypeString,
- NSPasteboardTypeTIFF, NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow,
+ NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSSavePanel,
+ NSVisualEffectState, NSVisualEffectView, NSWindow,
},
base::{BOOL, NO, YES, id, nil, selector},
foundation::{
- NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSRange, NSString,
- NSUInteger, NSURL,
+ NSArray, NSAutoreleasePool, NSBundle, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL,
},
};
use core_foundation::{
@@ -49,7 +44,6 @@ use ptr::null_mut;
use semver::Version;
use std::{
cell::Cell,
- convert::TryInto,
ffi::{CStr, OsStr, c_void},
os::{raw::c_char, unix::ffi::OsStrExt},
path::{Path, PathBuf},
@@ -58,7 +52,6 @@ use std::{
slice, str,
sync::{Arc, OnceLock},
};
-use strum::IntoEnumIterator;
use util::{
ResultExt,
command::{new_smol_command, new_std_command},
@@ -164,9 +157,8 @@ pub(crate) struct MacPlatformState {
text_system: Arc<dyn PlatformTextSystem>,
renderer_context: renderer::Context,
headless: bool,
- pasteboard: id,
- text_hash_pasteboard_type: id,
- metadata_pasteboard_type: id,
+ general_pasteboard: Pasteboard,
+ find_pasteboard: Pasteboard,
reopen: Option<Box<dyn FnMut()>>,
on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
quit: Option<Box<dyn FnMut()>>,
@@ -206,9 +198,8 @@ impl MacPlatform {
background_executor: BackgroundExecutor::new(dispatcher.clone()),
foreground_executor: ForegroundExecutor::new(dispatcher),
renderer_context: renderer::Context::default(),
- pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
- text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
- metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
+ general_pasteboard: Pasteboard::general(),
+ find_pasteboard: Pasteboard::find(),
reopen: None,
quit: None,
menu_command: None,
@@ -224,20 +215,6 @@ impl MacPlatform {
}))
}
- unsafe fn read_from_pasteboard(&self, pasteboard: *mut Object, kind: id) -> Option<&[u8]> {
- unsafe {
- let data = pasteboard.dataForType(kind);
- if data == nil {
- None
- } else {
- Some(slice::from_raw_parts(
- data.bytes() as *mut u8,
- data.length() as usize,
- ))
- }
- }
- }
-
unsafe fn create_menu_bar(
&self,
menus: &Vec<Menu>,
@@ -1034,119 +1011,24 @@ impl Platform for MacPlatform {
}
}
- fn write_to_clipboard(&self, item: ClipboardItem) {
- use crate::ClipboardEntry;
-
- unsafe {
- // We only want to use NSAttributedString if there are multiple entries to write.
- if item.entries.len() <= 1 {
- match item.entries.first() {
- Some(entry) => match entry {
- ClipboardEntry::String(string) => {
- self.write_plaintext_to_clipboard(string);
- }
- ClipboardEntry::Image(image) => {
- self.write_image_to_clipboard(image);
- }
- ClipboardEntry::ExternalPaths(_) => {}
- },
- None => {
- // Writing an empty list of entries just clears the clipboard.
- let state = self.0.lock();
- state.pasteboard.clearContents();
- }
- }
- } else {
- let mut any_images = false;
- let attributed_string = {
- let mut buf = NSMutableAttributedString::alloc(nil)
- // TODO can we skip this? Or at least part of it?
- .init_attributed_string(ns_string(""))
- .autorelease();
-
- for entry in item.entries {
- if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry
- {
- let to_append = NSAttributedString::alloc(nil)
- .init_attributed_string(ns_string(&text))
- .autorelease();
-
- buf.appendAttributedString_(to_append);
- }
- }
-
- buf
- };
-
- let state = self.0.lock();
- state.pasteboard.clearContents();
-
- // Only set rich text clipboard types if we actually have 1+ images to include.
- if any_images {
- let rtfd_data = attributed_string.RTFDFromRange_documentAttributes_(
- NSRange::new(0, msg_send![attributed_string, length]),
- nil,
- );
- if rtfd_data != nil {
- state
- .pasteboard
- .setData_forType(rtfd_data, NSPasteboardTypeRTFD);
- }
-
- let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
- NSRange::new(0, attributed_string.length()),
- nil,
- );
- if rtf_data != nil {
- state
- .pasteboard
- .setData_forType(rtf_data, NSPasteboardTypeRTF);
- }
- }
-
- let plain_text = attributed_string.string();
- state
- .pasteboard
- .setString_forType(plain_text, NSPasteboardTypeString);
- }
- }
- }
-
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
let state = self.0.lock();
- let pasteboard = state.pasteboard;
-
- // First, see if it's a string.
- unsafe {
- let types: id = pasteboard.types();
- let string_type: id = ns_string("public.utf8-plain-text");
-
- if msg_send![types, containsObject: string_type] {
- let data = pasteboard.dataForType(string_type);
- if data == nil {
- return None;
- } else if data.bytes().is_null() {
- // https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc
- // "If the length of the NSData object is 0, this property returns nil."
- return Some(self.read_string_from_clipboard(&state, &[]));
- } else {
- let bytes =
- slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize);
+ state.general_pasteboard.read()
+ }
- return Some(self.read_string_from_clipboard(&state, bytes));
- }
- }
+ fn write_to_clipboard(&self, item: ClipboardItem) {
+ let state = self.0.lock();
+ state.general_pasteboard.write(item);
+ }
- // If it wasn't a string, try the various supported image types.
- for format in ImageFormat::iter() {
- if let Some(item) = try_clipboard_image(pasteboard, format) {
- return Some(item);
- }
- }
- }
+ fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
+ let state = self.0.lock();
+ state.find_pasteboard.read()
+ }
- // If it wasn't a string or a supported image type, give up.
- None
+ fn write_to_find_pasteboard(&self, item: ClipboardItem) {
+ let state = self.0.lock();
+ state.find_pasteboard.write(item);
}
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
@@ -1255,116 +1137,6 @@ impl Platform for MacPlatform {
}
}
-impl MacPlatform {
- unsafe fn read_string_from_clipboard(
- &self,
- state: &MacPlatformState,
- text_bytes: &[u8],
- ) -> ClipboardItem {
- unsafe {
- let text = String::from_utf8_lossy(text_bytes).to_string();
- let metadata = self
- .read_from_pasteboard(state.pasteboard, state.text_hash_pasteboard_type)
- .and_then(|hash_bytes| {
- let hash_bytes = hash_bytes.try_into().ok()?;
- let hash = u64::from_be_bytes(hash_bytes);
- let metadata = self
- .read_from_pasteboard(state.pasteboard, state.metadata_pasteboard_type)?;
-
- if hash == ClipboardString::text_hash(&text) {
- String::from_utf8(metadata.to_vec()).ok()
- } else {
- None
- }
- });
-
- ClipboardItem {
- entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })],
- }
- }
- }
-
- unsafe fn write_plaintext_to_clipboard(&self, string: &ClipboardString) {
- unsafe {
- let state = self.0.lock();
- state.pasteboard.clearContents();
-
- let text_bytes = NSData::dataWithBytes_length_(
- nil,
- string.text.as_ptr() as *const c_void,
- string.text.len() as u64,
- );
- state
- .pasteboard
- .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,
- );
- state
- .pasteboard
- .setData_forType(hash_bytes, state.text_hash_pasteboard_type);
-
- let metadata_bytes = NSData::dataWithBytes_length_(
- nil,
- metadata.as_ptr() as *const c_void,
- metadata.len() as u64,
- );
- state
- .pasteboard
- .setData_forType(metadata_bytes, state.metadata_pasteboard_type);
- }
- }
- }
-
- unsafe fn write_image_to_clipboard(&self, image: &Image) {
- unsafe {
- let state = self.0.lock();
- state.pasteboard.clearContents();
-
- let bytes = NSData::dataWithBytes_length_(
- nil,
- image.bytes.as_ptr() as *const c_void,
- image.bytes.len() as u64,
- );
-
- state
- .pasteboard
- .setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
- }
- }
-}
-
-fn try_clipboard_image(pasteboard: id, format: ImageFormat) -> Option<ClipboardItem> {
- let mut ut_type: UTType = format.into();
-
- unsafe {
- let types: id = pasteboard.types();
- if msg_send![types, containsObject: ut_type.inner()] {
- let data = pasteboard.dataForType(ut_type.inner_mut());
- if data == nil {
- None
- } else {
- let bytes = Vec::from(slice::from_raw_parts(
- data.bytes() as *mut u8,
- data.length() as usize,
- ));
- let id = hash(&bytes);
-
- Some(ClipboardItem {
- entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
- })
- }
- } else {
- None
- }
- }
-}
-
unsafe fn path_from_objc(path: id) -> PathBuf {
let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
let bytes = unsafe { path.UTF8String() as *const u8 };
@@ -1605,120 +1377,3 @@ mod security {
pub const errSecUserCanceled: OSStatus = -128;
pub const errSecItemNotFound: OSStatus = -25300;
}
-
-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(),
- }
- }
-}
-
-// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/
-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
- }
-
- fn inner(&self) -> *const Object {
- self.0
- }
-
- fn inner_mut(&self) -> *mut Object {
- self.0 as *mut _
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::ClipboardItem;
-
- use super::*;
-
- #[test]
- fn test_clipboard() {
- let platform = build_platform();
- assert_eq!(platform.read_from_clipboard(), None);
-
- let item = ClipboardItem::new_string("1".to_string());
- platform.write_to_clipboard(item.clone());
- assert_eq!(platform.read_from_clipboard(), Some(item));
-
- let item = ClipboardItem {
- entries: vec![ClipboardEntry::String(
- ClipboardString::new("2".to_string()).with_json_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
- .0
- .lock()
- .pasteboard
- .setData_forType(bytes, NSPasteboardTypeString);
- }
- assert_eq!(
- platform.read_from_clipboard(),
- Some(ClipboardItem::new_string(text_from_other_app.to_string()))
- );
- }
-
- fn build_platform() -> MacPlatform {
- let platform = MacPlatform::new(false);
- platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
- platform
- }
-}
@@ -32,6 +32,8 @@ pub(crate) struct TestPlatform {
current_clipboard_item: Mutex<Option<ClipboardItem>>,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
current_primary_item: Mutex<Option<ClipboardItem>>,
+ #[cfg(target_os = "macos")]
+ current_find_pasteboard_item: Mutex<Option<ClipboardItem>>,
pub(crate) prompts: RefCell<TestPrompts>,
screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
pub opened_url: RefCell<Option<String>>,
@@ -117,6 +119,8 @@ impl TestPlatform {
current_clipboard_item: Mutex::new(None),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
current_primary_item: Mutex::new(None),
+ #[cfg(target_os = "macos")]
+ current_find_pasteboard_item: Mutex::new(None),
weak: weak.clone(),
opened_url: Default::default(),
#[cfg(target_os = "windows")]
@@ -398,9 +402,8 @@ impl Platform for TestPlatform {
false
}
- #[cfg(any(target_os = "linux", target_os = "freebsd"))]
- fn write_to_primary(&self, item: ClipboardItem) {
- *self.current_primary_item.lock() = Some(item);
+ fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+ self.current_clipboard_item.lock().clone()
}
fn write_to_clipboard(&self, item: ClipboardItem) {
@@ -412,8 +415,19 @@ impl Platform for TestPlatform {
self.current_primary_item.lock().clone()
}
- fn read_from_clipboard(&self) -> Option<ClipboardItem> {
- self.current_clipboard_item.lock().clone()
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ fn write_to_primary(&self, item: ClipboardItem) {
+ *self.current_primary_item.lock() = Some(item);
+ }
+
+ #[cfg(target_os = "macos")]
+ fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
+ self.current_find_pasteboard_item.lock().clone()
+ }
+
+ #[cfg(target_os = "macos")]
+ fn write_to_find_pasteboard(&self, item: ClipboardItem) {
+ *self.current_find_pasteboard_item.lock() = Some(item);
}
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
@@ -106,7 +106,10 @@ pub struct BufferSearchBar {
replacement_editor_focused: bool,
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
active_match_index: Option<usize>,
- active_searchable_item_subscription: Option<Subscription>,
+ #[cfg(target_os = "macos")]
+ active_searchable_item_subscriptions: Option<[Subscription; 2]>,
+ #[cfg(not(target_os = "macos"))]
+ active_searchable_item_subscriptions: Option<Subscription>,
active_search: Option<Arc<SearchQuery>>,
searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
pending_search: Option<Task<()>>,
@@ -472,7 +475,7 @@ impl ToolbarItemView for BufferSearchBar {
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
cx.notify();
- self.active_searchable_item_subscription.take();
+ self.active_searchable_item_subscriptions.take();
self.active_searchable_item.take();
self.pending_search.take();
@@ -482,18 +485,58 @@ impl ToolbarItemView for BufferSearchBar {
{
let this = cx.entity().downgrade();
- self.active_searchable_item_subscription =
- Some(searchable_item_handle.subscribe_to_search_events(
- window,
- cx,
- Box::new(move |search_event, window, cx| {
- if let Some(this) = this.upgrade() {
- this.update(cx, |this, cx| {
- this.on_active_searchable_item_event(search_event, window, cx)
- });
+ let search_event_subscription = searchable_item_handle.subscribe_to_search_events(
+ window,
+ cx,
+ Box::new(move |search_event, window, cx| {
+ if let Some(this) = this.upgrade() {
+ this.update(cx, |this, cx| {
+ this.on_active_searchable_item_event(search_event, window, cx)
+ });
+ }
+ }),
+ );
+
+ #[cfg(target_os = "macos")]
+ {
+ let item_focus_handle = searchable_item_handle.item_focus_handle(cx);
+
+ self.active_searchable_item_subscriptions = Some([
+ search_event_subscription,
+ cx.on_focus(&item_focus_handle, window, |this, window, cx| {
+ if this.query_editor_focused || this.replacement_editor_focused {
+ // no need to read pasteboard since focus came from toolbar
+ return;
}
+
+ cx.defer_in(window, |this, window, cx| {
+ if let Some(item) = cx.read_from_find_pasteboard()
+ && let Some(text) = item.text()
+ {
+ if this.query(cx) != text {
+ let search_options = item
+ .metadata()
+ .and_then(|m| m.parse().ok())
+ .and_then(SearchOptions::from_bits)
+ .unwrap_or(this.search_options);
+
+ drop(this.search(
+ &text,
+ Some(search_options),
+ true,
+ window,
+ cx,
+ ));
+ }
+ }
+ });
}),
- ));
+ ]);
+ }
+ #[cfg(not(target_os = "macos"))]
+ {
+ self.active_searchable_item_subscriptions = Some(search_event_subscription);
+ }
let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
self.active_searchable_item = Some(searchable_item_handle);
@@ -663,7 +706,7 @@ impl BufferSearchBar {
replacement_editor,
replacement_editor_focused: false,
active_searchable_item: None,
- active_searchable_item_subscription: None,
+ active_searchable_item_subscriptions: None,
active_match_index: None,
searchable_items_with_matches: Default::default(),
default_options: search_options,
@@ -904,11 +947,21 @@ impl BufferSearchBar {
});
self.set_search_options(options, cx);
self.clear_matches(window, cx);
+ #[cfg(target_os = "macos")]
+ self.update_find_pasteboard(cx);
cx.notify();
}
self.update_matches(!updated, add_to_history, window, cx)
}
+ #[cfg(target_os = "macos")]
+ pub fn update_find_pasteboard(&mut self, cx: &mut App) {
+ cx.write_to_find_pasteboard(gpui::ClipboardItem::new_string_with_metadata(
+ self.query(cx),
+ self.search_options.bits().to_string(),
+ ));
+ }
+
pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
if let Some(active_editor) = self.active_searchable_item.as_ref() {
let handle = active_editor.item_focus_handle(cx);
@@ -1098,11 +1151,12 @@ impl BufferSearchBar {
cx.spawn_in(window, async move |this, cx| {
if search.await.is_ok() {
this.update_in(cx, |this, window, cx| {
- this.activate_current_match(window, cx)
- })
- } else {
- Ok(())
+ this.activate_current_match(window, cx);
+ #[cfg(target_os = "macos")]
+ this.update_find_pasteboard(cx);
+ })?;
}
+ anyhow::Ok(())
})
.detach_and_log_err(cx);
}
@@ -1293,6 +1347,7 @@ impl BufferSearchBar {
.insert(active_searchable_item.downgrade(), matches);
this.update_match_index(window, cx);
+
if add_to_history {
this.search_history
.add(&mut this.search_history_cursor, query_text);