diff --git a/Cargo.lock b/Cargo.lock index c327163bcecdc58b769689498c2b3843e3444a81..35554ea1bd4ad094fa0f53d338482e4949a4d472 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" @@ -2271,6 +2286,7 @@ dependencies = [ "rand 0.8.3", "rust-embed", "seahash", + "serde", "serde_json", "simplelog", "smallvec", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 7bf1913372825f5ebcef0641b462f8caf3c60116..5bd32560c67e4f27039b6163a7498ee87b4379f5 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 b35c5ba0b46a20a27dad652e116ec60262d3cc57..6ae71c769be8f40e01c3170db5a4acc4f73a3311 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,8 +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 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 0000000000000000000000000000000000000000..b7e4b600ee96042df0272952bef0b1bac2f540b3 --- /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 b60ce9b92d1ba0719517cae8e5892ba79ff872bc..ee8c544a83ef66ff46971983e5a4cb7c8342365c 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 4fc0f160a316bfeaacfd57b48eaaba08e7be1109..b7ce76f475a25cb931a27e1d24d1b0c290123825 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,11 +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, str, sync::Arc, }; @@ -77,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)] @@ -96,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") }, } } @@ -176,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 { @@ -286,16 +306,72 @@ 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 read_from_clipboard(&self) -> Option { + unsafe { + 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 { + None + } } } @@ -392,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 52148ee27c03a05a006280b4cdbd83039ff7a091..5f34396a0ee18e1a7bcebf372acf33a95a354fc2 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,7 +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 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 eed3709cba75924dcd99473865ad82b3456e48c1..3baa0dab6eb88e8af9f8168a2525085c5bcb4de3 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,10 +1,11 @@ +use crate::ClipboardItem; use pathfinder_geometry::vector::Vector2F; -use std::sync::Arc; -use std::{any::Any, rc::Rc}; +use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc}; struct Platform { dispatcher: Arc, fonts: Arc, + current_clipboard_item: RefCell>, } struct Dispatcher; @@ -22,6 +23,7 @@ impl Platform { Self { dispatcher: Arc::new(Dispatcher), fonts: Arc::new(super::current::FontSystem::new()), + current_clipboard_item: RefCell::new(None), } } } @@ -72,7 +74,13 @@ impl super::Platform for Platform { None } - fn copy(&self, _: &str) {} + fn write_to_clipboard(&self, item: ClipboardItem) { + *self.current_clipboard_item.borrow_mut() = Some(item); + } + + fn read_from_clipboard(&self) -> Option { + self.current_clipboard_item.borrow().clone() + } } impl Window { diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 4ed0d1f1d5af6eee81fb7a481115334539d1bfa3..57d4f265257fa68b110e9b5b778627163dec9297 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -30,6 +30,7 @@ rand = "0.8.3" rust-embed = "5.9.0" seahash = "4.1" simplelog = "0.9" +serde = { version = "1", features = ["derive"] } smallvec = "1.6.1" smol = "1.2.5" diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index d69e1d96e2e6e003c999ea48f58fe8edad633730..fc633ed03fa5f648045699f80a239a7f343e145d 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -563,10 +563,13 @@ impl Buffer { self.chars().collect() } - pub fn text_for_range(&self, range: Range) -> Result { + pub fn text_for_range<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> Result> { let start = range.start.to_offset(self)?; let end = range.end.to_offset(self)?; - Ok(self.chars_at(start)?.take(end - start).collect()) + Ok(self.chars_at(start)?.take(end - start)) } pub fn chars(&self) -> CharIter { @@ -2470,13 +2473,9 @@ mod tests { let old_len = old_range.end - old_range.start; let new_len = new_range.end - new_range.start; let old_start = (old_range.start as isize + delta) as usize; - + let new_text: String = buffer.text_for_range(new_range).unwrap().collect(); old_buffer - .edit( - Some(old_start..old_start + old_len), - buffer.text_for_range(new_range).unwrap(), - None, - ) + .edit(Some(old_start..old_start + old_len), new_text, None) .unwrap(); delta += new_len as isize - old_len as isize; diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index f1855c9128d17ecc7d62b26cf67050e3dc659868..f36f88c422d3aa4e50a6321484b41c5efcf07dae 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -1,22 +1,24 @@ use super::{ buffer, movement, Anchor, Bias, Buffer, BufferElement, DisplayMap, DisplayPoint, Point, - Selection, SelectionSetId, ToOffset, + Selection, SelectionSetId, ToOffset, ToPoint, }; 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}; use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; use std::{ cmp::{self, Ordering}, fmt::Write, + iter::FromIterator, ops::Range, sync::Arc, time::Duration, @@ -28,6 +30,9 @@ pub fn init(app: &mut MutableAppContext) { app.add_bindings(vec![ Binding::new("backspace", "buffer:backspace", Some("BufferView")), Binding::new("enter", "buffer:newline", Some("BufferView")), + Binding::new("cmd-x", "buffer:cut", Some("BufferView")), + Binding::new("cmd-c", "buffer:copy", Some("BufferView")), + Binding::new("cmd-v", "buffer:paste", Some("BufferView")), Binding::new("cmd-z", "buffer:undo", Some("BufferView")), Binding::new("cmd-shift-Z", "buffer:redo", Some("BufferView")), Binding::new("up", "buffer:move_up", Some("BufferView")), @@ -54,6 +59,9 @@ pub fn init(app: &mut MutableAppContext) { app.add_action("buffer:insert", BufferView::insert); app.add_action("buffer:newline", BufferView::newline); app.add_action("buffer:backspace", BufferView::backspace); + app.add_action("buffer:cut", BufferView::cut); + app.add_action("buffer:copy", BufferView::copy); + app.add_action("buffer:paste", BufferView::paste); app.add_action("buffer:undo", BufferView::undo); app.add_action("buffer:redo", BufferView::redo); app.add_action("buffer:move_up", BufferView::move_up); @@ -102,6 +110,12 @@ pub struct BufferView { single_line: bool, } +#[derive(Serialize, Deserialize)] +struct ClipboardSelection { + len: usize, + is_entire_line: bool, +} + impl BufferView { pub fn single_line(settings: watch::Receiver, ctx: &mut ViewContext) -> Self { let buffer = ctx.add_model(|_| Buffer::new(0, String::new())); @@ -354,6 +368,25 @@ impl BufferView { #[cfg(test)] fn select_ranges<'a, T>(&mut self, ranges: T, ctx: &mut ViewContext) -> Result<()> + where + T: IntoIterator>, + { + let buffer = self.buffer.read(ctx); + let mut selections = Vec::new(); + for range in ranges { + selections.push(Selection { + start: buffer.anchor_after(range.start)?, + end: buffer.anchor_before(range.end)?, + reversed: false, + goal_column: None, + }); + } + self.update_selections(selections, ctx); + Ok(()) + } + + #[cfg(test)] + fn select_display_ranges<'a, T>(&mut self, ranges: T, ctx: &mut ViewContext) -> Result<()> where T: IntoIterator>, { @@ -454,6 +487,161 @@ impl BufferView { self.end_transaction(ctx); } + pub fn cut(&mut self, _: &(), ctx: &mut ViewContext) { + self.start_transaction(ctx); + let mut text = String::new(); + let mut selections = self.selections(ctx.as_ref()).to_vec(); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let buffer = self.buffer.read(ctx); + let max_point = buffer.max_point(); + for selection in &mut selections { + let mut start = selection.start.to_point(buffer).expect("invalid start"); + let mut end = selection.end.to_point(buffer).expect("invalid end"); + let is_entire_line = start == end; + if is_entire_line { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(start.row + 1, 0)); + selection.start = buffer.anchor_before(start).unwrap(); + selection.end = buffer.anchor_after(end).unwrap(); + } + let mut len = 0; + for ch in buffer.text_for_range(start..end).unwrap() { + text.push(ch); + len += 1; + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + }); + } + } + self.update_selections(selections, ctx); + self.changed_selections(ctx); + self.insert(&String::new(), ctx); + self.end_transaction(ctx); + + ctx.as_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + } + + pub fn copy(&mut self, _: &(), ctx: &mut ViewContext) { + let buffer = self.buffer.read(ctx); + let max_point = buffer.max_point(); + let mut text = String::new(); + let selections = self.selections(ctx.as_ref()); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + for selection in selections { + let mut start = selection.start.to_point(buffer).expect("invalid start"); + let mut end = selection.end.to_point(buffer).expect("invalid end"); + let is_entire_line = start == end; + if is_entire_line { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(start.row + 1, 0)); + } + let mut len = 0; + for ch in buffer.text_for_range(start..end).unwrap() { + text.push(ch); + len += 1; + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + }); + } + + ctx.as_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + } + + pub fn paste(&mut self, _: &(), ctx: &mut ViewContext) { + if let Some(item) = ctx.as_mut().read_from_clipboard() { + let clipboard_text = item.text(); + if let Some(clipboard_selections) = item.metadata::>() { + let selections_len = self.selections(ctx.as_ref()).len(); + if clipboard_selections.len() == selections_len { + // If there are the same number of selections as there were at the time that + // this clipboard data was written, then paste one slice of the clipboard text + // into each of the current selections. + self.multiline_paste(clipboard_text.chars(), clipboard_selections.iter(), ctx); + } else if clipboard_selections.len() == 1 && clipboard_selections[0].is_entire_line + { + // If there was only one selection in the clipboard but it spanned the whole + // line, then paste it into each of the current selections so that we can + // position it before those selections that are empty. + self.multiline_paste( + clipboard_text.chars().cycle(), + clipboard_selections.iter().cycle(), + ctx, + ); + } else { + self.insert(clipboard_text, ctx); + } + } else { + self.insert(clipboard_text, ctx); + } + } + } + + fn multiline_paste<'a>( + &mut self, + mut clipboard_text: impl Iterator, + clipboard_selections: impl Iterator, + ctx: &mut ViewContext, + ) { + self.start_transaction(ctx); + let selections = self.selections(ctx.as_ref()).to_vec(); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut clipboard_offset = 0; + for (selection, clipboard_selection) in selections.iter().zip(clipboard_selections) { + let clipboard_slice = + String::from_iter(clipboard_text.by_ref().take(clipboard_selection.len)); + clipboard_offset = clipboard_offset + clipboard_selection.len; + + self.buffer.update(ctx, |buffer, ctx| { + let selection_start = selection.start.to_point(buffer).unwrap(); + let selection_end = selection.end.to_point(buffer).unwrap(); + let max_point = buffer.max_point(); + + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let anchor; + if selection_start == selection_end && clipboard_selection.is_entire_line { + let start_point = Point::new(selection_start.row, 0); + let start = start_point.to_offset(buffer).unwrap(); + let new_position = cmp::min( + max_point, + Point::new(start_point.row + 1, selection_start.column), + ); + buffer + .edit(Some(start..start), clipboard_slice, Some(ctx)) + .unwrap(); + anchor = buffer.anchor_before(new_position).unwrap(); + } else { + let start = selection.start.to_offset(buffer).unwrap(); + let end = selection.end.to_offset(buffer).unwrap(); + buffer + .edit(Some(start..end), clipboard_slice, Some(ctx)) + .unwrap(); + anchor = buffer + .anchor_before(start + clipboard_selection.len) + .unwrap(); + } + + new_selections.push(Selection { + start: anchor.clone(), + end: anchor, + reversed: false, + goal_column: None, + }); + }); + } + self.update_selections(new_selections, ctx); + self.end_transaction(ctx); + } + pub fn undo(&mut self, _: &(), ctx: &mut ViewContext) { self.buffer .update(ctx, |buffer, ctx| buffer.undo(Some(ctx))); @@ -1417,8 +1605,11 @@ mod tests { app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); view.update(app, |view, ctx| { - view.select_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], ctx) - .unwrap(); + view.select_display_ranges( + &[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], + ctx, + ) + .unwrap(); view.fold(&(), ctx); assert_eq!( view.text(ctx.as_ref()), @@ -1525,7 +1716,7 @@ mod tests { app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); view.update(app, |view, ctx| { - view.select_ranges( + view.select_display_ranges( &[ // an empty selection - the preceding character is deleted DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), @@ -1547,6 +1738,147 @@ mod tests { }) } + #[test] + fn test_clipboard() { + App::test((), |app| { + let buffer = app.add_model(|_| Buffer::new(0, "one two three four five six ")); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let view = app + .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)) + .1; + + // Cut with three selections. Clipboard text is divided into three slices. + view.update(app, |view, ctx| { + view.select_ranges(&[0..4, 8..14, 19..24], ctx).unwrap(); + view.cut(&(), ctx); + }); + assert_eq!(view.read(app).text(app.as_ref()), "two four six "); + + // Paste with three cursors. Each cursor pastes one slice of the clipboard text. + view.update(app, |view, ctx| { + view.select_ranges(&[4..4, 9..9, 13..13], ctx).unwrap(); + view.paste(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "two one four three six five " + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8), + DisplayPoint::new(0, 19)..DisplayPoint::new(0, 19), + DisplayPoint::new(0, 28)..DisplayPoint::new(0, 28) + ] + ); + + // Paste again but with only two cursors. Since the number of cursors doesn't + // match the number of slices in the clipboard, the entire clipboard text + // is pasted at each cursor. + view.update(app, |view, ctx| { + view.select_ranges(&[0..0, 28..28], ctx).unwrap(); + view.insert(&"( ".to_string(), ctx); + view.paste(&(), ctx); + view.insert(&") ".to_string(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "( one three five ) two one four three six five ( one three five ) " + ); + + view.update(app, |view, ctx| { + view.select_ranges(&[0..0], ctx).unwrap(); + view.insert(&"123\n4567\n89\n".to_string(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "123\n4567\n89\n( one three five ) two one four three six five ( one three five ) " + ); + + // Cut with three selections, one of which is full-line. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.cut(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "13\n9\n( one three five ) two one four three six five ( one three five ) " + ); + + // Paste with three selections, noticing how the copied selection that was full-line + // gets inserted before the second cursor. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), + ], + ctx, + ) + .unwrap(); + view.paste(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) " + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), + ] + ); + + // Copy with a single cursor only, which writes the whole line into the clipboard. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], + ctx, + ) + .unwrap(); + view.copy(&(), ctx); + }); + + // Paste with three selections, noticing how the copied full-line selection is inserted + // before the empty selections but replaces the selection that is non-empty. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.paste(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "123\n123\n123\n67\n123\n9\n( 8ne three five ) two one four three six five ( one three five ) " + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), + ] + ); + }); + } + impl BufferView { fn selection_ranges(&self, app: &AppContext) -> Vec> { self.selections_in_range(DisplayPoint::zero()..self.max_point(app), app) diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 23612116c3beae50f6d18a267413008808f39054..6c466ce6475ee2858063576c0288c517bb437827 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.as_mut().copy(&json); + let kib = json.len() as f32 / 1024.; + ctx.as_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) => {