mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
Allow metadata to be associated with text written to clipboard
Co-Authored-By: Max Brunsfeld <max@zed.dev>
This commit is contained in:
parent
f4c1ffc329
commit
13514aae6c
15
Cargo.lock
generated
15
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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<String> {
|
||||
self.platform.paste()
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
}
|
||||
}
|
||||
|
||||
|
42
gpui/src/clipboard.rs
Normal file
42
gpui/src/clipboard.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
impl ClipboardItem {
|
||||
pub fn new(text: String) -> Self {
|
||||
Self {
|
||||
text,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_metadata<T: Serialize>(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<T>(&self) -> Option<T>
|
||||
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()
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<FontSystem>,
|
||||
callbacks: RefCell<Callbacks>,
|
||||
menu_item_actions: RefCell<Vec<(String, Option<Box<dyn Any>>)>>,
|
||||
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<String> {
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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<usize>;
|
||||
fn prompt_for_paths(&self, options: PathPromptOptions) -> Option<Vec<PathBuf>>;
|
||||
fn quit(&self);
|
||||
fn copy(&self, text: &str);
|
||||
fn paste(&self) -> Option<String>;
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn set_menus(&self, menus: Vec<Menu>);
|
||||
}
|
||||
|
||||
|
@ -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<dyn super::Dispatcher>,
|
||||
fonts: Arc<dyn super::FontSystem>,
|
||||
@ -72,9 +74,9 @@ impl super::Platform for Platform {
|
||||
None
|
||||
}
|
||||
|
||||
fn copy(&self, _: &str) {}
|
||||
fn write_to_clipboard(&self, _: ClipboardItem) {}
|
||||
|
||||
fn paste(&self) -> Option<String> {
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -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<Self>) {
|
||||
@ -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<Self>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Self>) {
|
||||
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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user