From f4c1ffc3294ee1743d68e76d41c5ca0624edc4fe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 13 Apr 2021 14:58:10 +0200 Subject: [PATCH 01/16] Start on copy-paste --- gpui/src/app.rs | 4 +++ gpui/src/platform/mac/platform.rs | 14 ++++++++ gpui/src/platform/mod.rs | 1 + gpui/src/platform/test.rs | 4 +++ zed/src/editor/buffer/mod.rs | 15 ++++---- zed/src/editor/buffer_view.rs | 58 ++++++++++++++++++++++++++++++- 6 files changed, 87 insertions(+), 9 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 4f5fb7b905..61c7ce6bc9 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1215,6 +1215,10 @@ impl MutableAppContext { pub fn copy(&self, text: &str) { self.platform.copy(text); } + + pub fn paste(&self) -> Option { + self.platform.paste() + } } impl ReadModel for MutableAppContext { diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 4fc0f160a3..570d3ea14c 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -26,6 +26,7 @@ use std::{ path::PathBuf, ptr, rc::Rc, + slice, sync::Arc, }; @@ -299,6 +300,19 @@ impl platform::Platform for MacPlatform { } } + fn paste(&self) -> Option { + unsafe { + let pasteboard = NSPasteboard::generalPasteboard(nil); + let data = pasteboard.dataForType(NSPasteboardTypeString); + if data == nil { + 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())) + } + } + } + fn set_menus(&self, menus: Vec) { unsafe { let app: id = msg_send![APP_CLASS, sharedApplication]; diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index 52148ee27c..39439970d8 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -43,6 +43,7 @@ pub trait Platform { fn prompt_for_paths(&self, options: PathPromptOptions) -> Option>; fn quit(&self); fn copy(&self, text: &str); + fn paste(&self) -> Option; fn set_menus(&self, menus: Vec); } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index eed3709cba..19c015f688 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -73,6 +73,10 @@ impl super::Platform for Platform { } fn copy(&self, _: &str) {} + + fn paste(&self) -> Option { + None + } } impl Window { diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 7f06e1700f..3348199ad7 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 aa91e39ec5..b90b79fa12 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -1,6 +1,6 @@ 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; @@ -28,6 +28,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 +57,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); @@ -449,6 +455,56 @@ 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.app()).to_vec(); + { + 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"); + if start == end { + 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(); + } + text.extend(buffer.text_for_range(start..end).unwrap()); + } + } + self.update_selections(selections, ctx); + self.changed_selections(ctx); + self.insert(&String::new(), ctx); + self.end_transaction(ctx); + + ctx.app_mut().copy(&text); + } + + 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(); + for selection in self.selections(ctx.app()) { + let mut start = selection.start.to_point(buffer).expect("invalid start"); + let mut end = selection.end.to_point(buffer).expect("invalid end"); + if start == end { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(start.row + 1, 0)); + } + text.extend(buffer.text_for_range(start..end).unwrap()); + } + + ctx.app_mut().copy(&text); + } + + pub fn paste(&mut self, _: &(), ctx: &mut ViewContext) { + if let Some(text) = ctx.app_mut().paste() { + self.insert(&text, ctx); + } + } + pub fn undo(&mut self, _: &(), ctx: &mut ViewContext) { self.buffer .update(ctx, |buffer, ctx| buffer.undo(Some(ctx))); From 13514aae6c7dafa466618b48991ab1667dd1c46f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 13 Apr 2021 19:03:56 +0200 Subject: [PATCH 02/16] Allow metadata to be associated with text written to clipboard Co-Authored-By: Max Brunsfeld --- Cargo.lock | 15 +++ gpui/Cargo.toml | 3 +- gpui/src/app.rs | 10 +- gpui/src/clipboard.rs | 42 +++++++++ gpui/src/lib.rs | 2 + gpui/src/platform/mac/platform.rs | 137 ++++++++++++++++++++++++---- gpui/src/platform/mod.rs | 6 +- gpui/src/platform/test.rs | 6 +- zed/src/editor/buffer_view.rs | 12 +-- zed/src/workspace/workspace_view.rs | 7 +- 10 files changed, 204 insertions(+), 36 deletions(-) create mode 100644 gpui/src/clipboard.rs diff --git a/Cargo.lock b/Cargo.lock index c327163bce..42b66a0bac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,6 +924,7 @@ dependencies = [ "rand 0.8.3", "replace_with", "resvg", + "seahash", "serde", "serde_json", "simplelog", @@ -1712,6 +1713,20 @@ name = "serde" version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 7bf1913372..5bd32560c6 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -18,7 +18,8 @@ pathfinder_geometry = "0.5" rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" -serde = "1.0.125" +seahash = "4.1" +serde = { version = "1.0.125", features = ["derive"] } serde_json = "1.0.64" smallvec = "1.6.1" smol = "1.2" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 61c7ce6bc9..635817dae2 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -5,7 +5,7 @@ use crate::{ platform::{self, WindowOptions}, presenter::Presenter, util::post_inc, - AssetCache, AssetSource, FontCache, TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, TextLayoutCache, }; use anyhow::{anyhow, Result}; use async_std::sync::Condvar; @@ -1212,12 +1212,12 @@ impl MutableAppContext { } } - pub fn copy(&self, text: &str) { - self.platform.copy(text); + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item); } - pub fn paste(&self) -> Option { - self.platform.paste() + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() } } diff --git a/gpui/src/clipboard.rs b/gpui/src/clipboard.rs new file mode 100644 index 0000000000..b7e4b600ee --- /dev/null +++ b/gpui/src/clipboard.rs @@ -0,0 +1,42 @@ +use seahash::SeaHasher; +use serde::{Deserialize, Serialize}; +use std::hash::{Hash, Hasher}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardItem { + pub(crate) text: String, + pub(crate) metadata: Option, +} + +impl ClipboardItem { + pub fn new(text: String) -> Self { + Self { + text, + metadata: None, + } + } + + pub fn with_metadata(mut self, metadata: T) -> Self { + self.metadata = Some(serde_json::to_string(&metadata).unwrap()); + self + } + + pub fn text(&self) -> &String { + &self.text + } + + pub fn metadata(&self) -> Option + where + T: for<'a> Deserialize<'a>, + { + self.metadata + .as_ref() + .and_then(|m| serde_json::from_str(m).ok()) + } + + pub(crate) fn text_hash(text: &str) -> u64 { + let mut hasher = SeaHasher::new(); + text.hash(&mut hasher); + hasher.finish() + } +} diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index b60ce9b92d..ee8c544a83 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -7,6 +7,8 @@ pub use assets::*; pub mod elements; pub mod font_cache; pub use font_cache::FontCache; +mod clipboard; +pub use clipboard::ClipboardItem; pub mod fonts; pub mod geometry; mod presenter; diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 570d3ea14c..b7ce76f475 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window}; -use crate::{executor, keymap::Keystroke, platform, Event, Menu, MenuItem}; +use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem}; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, @@ -21,12 +21,13 @@ use ptr::null_mut; use std::{ any::Any, cell::RefCell, + convert::TryInto, ffi::{c_void, CStr}, os::raw::c_char, path::PathBuf, ptr, rc::Rc, - slice, + slice, str, sync::Arc, }; @@ -78,6 +79,9 @@ pub struct MacPlatform { fonts: Arc, callbacks: RefCell, menu_item_actions: RefCell>)>>, + pasteboard: id, + text_hash_pasteboard_type: id, + metadata_pasteboard_type: id, } #[derive(Default)] @@ -97,6 +101,9 @@ impl MacPlatform { fonts: Arc::new(FontSystem::new()), callbacks: Default::default(), menu_item_actions: Default::default(), + pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) }, + text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") }, + metadata_pasteboard_type: unsafe { ns_string("zed-metadata") }, } } @@ -177,6 +184,18 @@ impl MacPlatform { menu_bar } + + unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> { + let data = self.pasteboard.dataForType(kind); + if data == nil { + None + } else { + Some(slice::from_raw_parts( + data.bytes() as *mut u8, + data.length() as usize, + )) + } + } } impl platform::Platform for MacPlatform { @@ -287,28 +306,71 @@ impl platform::Platform for MacPlatform { } } - fn copy(&self, text: &str) { + fn write_to_clipboard(&self, item: ClipboardItem) { unsafe { - let data = NSData::dataWithBytes_length_( + self.pasteboard.clearContents(); + + let text_bytes = NSData::dataWithBytes_length_( nil, - text.as_ptr() as *const c_void, - text.len() as u64, + item.text.as_ptr() as *const c_void, + item.text.len() as u64, ); - let pasteboard = NSPasteboard::generalPasteboard(nil); - pasteboard.clearContents(); - pasteboard.setData_forType(data, NSPasteboardTypeString); + self.pasteboard + .setData_forType(text_bytes, NSPasteboardTypeString); + + if let Some(metadata) = item.metadata.as_ref() { + let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes(); + let hash_bytes = NSData::dataWithBytes_length_( + nil, + hash_bytes.as_ptr() as *const c_void, + hash_bytes.len() as u64, + ); + self.pasteboard + .setData_forType(hash_bytes, self.text_hash_pasteboard_type); + + let metadata_bytes = NSData::dataWithBytes_length_( + nil, + metadata.as_ptr() as *const c_void, + metadata.len() as u64, + ); + self.pasteboard + .setData_forType(metadata_bytes, self.metadata_pasteboard_type); + } } } - fn paste(&self) -> Option { + fn read_from_clipboard(&self) -> Option { unsafe { - let pasteboard = NSPasteboard::generalPasteboard(nil); - let data = pasteboard.dataForType(NSPasteboardTypeString); - if data == nil { - None + if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) { + let text = String::from_utf8_lossy(&text_bytes).to_string(); + let hash_bytes = self + .read_from_pasteboard(self.text_hash_pasteboard_type) + .and_then(|bytes| bytes.try_into().ok()) + .map(u64::from_be_bytes); + let metadata_bytes = self + .read_from_pasteboard(self.metadata_pasteboard_type) + .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok()); + + if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) { + if hash == ClipboardItem::text_hash(&text) { + Some(ClipboardItem { + text, + metadata: Some(metadata), + }) + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } } else { - let bytes = slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize); - Some(String::from_utf8_unchecked(bytes.to_vec())) + None } } } @@ -406,3 +468,46 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { unsafe fn ns_string(string: &str) -> id { NSString::alloc(nil).init_str(string).autorelease() } + +#[cfg(test)] +mod tests { + use crate::platform::Platform; + + use super::*; + + #[test] + fn test_clipboard() { + let platform = build_platform(); + assert_eq!(platform.read_from_clipboard(), None); + + let item = ClipboardItem::new("1".to_string()); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let text_from_other_app = "text from other app"; + unsafe { + let bytes = NSData::dataWithBytes_length_( + nil, + text_from_other_app.as_ptr() as *const c_void, + text_from_other_app.len() as u64, + ); + platform + .pasteboard + .setData_forType(bytes, NSPasteboardTypeString); + } + assert_eq!( + platform.read_from_clipboard(), + Some(ClipboardItem::new(text_from_other_app.to_string())) + ); + } + + fn build_platform() -> MacPlatform { + let mut platform = MacPlatform::new(); + platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) }; + platform + } +} diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index 39439970d8..5f34396a0e 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -15,7 +15,7 @@ use crate::{ vector::Vector2F, }, text_layout::Line, - Menu, Scene, + ClipboardItem, Menu, Scene, }; use async_task::Runnable; pub use event::Event; @@ -42,8 +42,8 @@ pub trait Platform { fn key_window_id(&self) -> Option; fn prompt_for_paths(&self, options: PathPromptOptions) -> Option>; fn quit(&self); - fn copy(&self, text: &str); - fn paste(&self) -> Option; + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_clipboard(&self) -> Option; fn set_menus(&self, menus: Vec); } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 19c015f688..536fb47da5 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -2,6 +2,8 @@ use pathfinder_geometry::vector::Vector2F; use std::sync::Arc; use std::{any::Any, rc::Rc}; +use crate::ClipboardItem; + struct Platform { dispatcher: Arc, fonts: Arc, @@ -72,9 +74,9 @@ impl super::Platform for Platform { None } - fn copy(&self, _: &str) {} + fn write_to_clipboard(&self, _: ClipboardItem) {} - fn paste(&self) -> Option { + fn read_from_clipboard(&self) -> Option { None } } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index b90b79fa12..9a0c2d1ce3 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -6,8 +6,8 @@ use crate::{settings::Settings, watch, workspace}; use anyhow::Result; use futures_core::future::LocalBoxFuture; use gpui::{ - fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, Element, - ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext, + fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, ClipboardItem, + Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext, WeakViewHandle, }; use gpui::{geometry::vector::Vector2F, TextLayoutCache}; @@ -479,7 +479,7 @@ impl BufferView { self.insert(&String::new(), ctx); self.end_transaction(ctx); - ctx.app_mut().copy(&text); + ctx.app_mut().write_to_clipboard(ClipboardItem::new(text)); } pub fn copy(&mut self, _: &(), ctx: &mut ViewContext) { @@ -496,12 +496,12 @@ impl BufferView { text.extend(buffer.text_for_range(start..end).unwrap()); } - ctx.app_mut().copy(&text); + ctx.app_mut().write_to_clipboard(ClipboardItem::new(text)); } pub fn paste(&mut self, _: &(), ctx: &mut ViewContext) { - if let Some(text) = ctx.app_mut().paste() { - self.insert(&text, ctx); + if let Some(item) = ctx.app_mut().read_from_clipboard() { + self.insert(item.text(), ctx); } } diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 6a9b3ca320..f43c6c4b9e 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -3,7 +3,7 @@ use crate::{settings::Settings, watch}; use futures_core::future::LocalBoxFuture; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, - Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, + ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, }; use log::{error, info}; use std::{collections::HashSet, path::PathBuf}; @@ -258,10 +258,11 @@ impl WorkspaceView { pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext) { match to_string_pretty(&ctx.debug_elements()) { Ok(json) => { - ctx.app_mut().copy(&json); + let kib = json.len() as f32 / 1024.; + ctx.app_mut().write_to_clipboard(ClipboardItem::new(json)); log::info!( "copied {:.1} KiB of element debug JSON to the clipboard", - json.len() as f32 / 1024. + kib ); } Err(error) => { From c83f02dd0469c460d0236366a5e4dcb60b48dc67 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 13 Apr 2021 16:51:28 -0700 Subject: [PATCH 03/16] Implement multi-selection copy/cut/paste --- gpui/src/platform/test.rs | 14 ++-- zed/src/editor/buffer_view.rs | 132 ++++++++++++++++++++++++++++++++-- 2 files changed, 134 insertions(+), 12 deletions(-) diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 536fb47da5..3baa0dab6e 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,12 +1,11 @@ -use pathfinder_geometry::vector::Vector2F; -use std::sync::Arc; -use std::{any::Any, rc::Rc}; - use crate::ClipboardItem; +use pathfinder_geometry::vector::Vector2F; +use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc}; struct Platform { dispatcher: Arc, fonts: Arc, + current_clipboard_item: RefCell>, } struct Dispatcher; @@ -24,6 +23,7 @@ impl Platform { Self { dispatcher: Arc::new(Dispatcher), fonts: Arc::new(super::current::FontSystem::new()), + current_clipboard_item: RefCell::new(None), } } } @@ -74,10 +74,12 @@ impl super::Platform for Platform { None } - fn write_to_clipboard(&self, _: ClipboardItem) {} + fn write_to_clipboard(&self, item: ClipboardItem) { + *self.current_clipboard_item.borrow_mut() = Some(item); + } fn read_from_clipboard(&self) -> Option { - None + self.current_clipboard_item.borrow().clone() } } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 9a0c2d1ce3..c10e99e21e 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -358,6 +358,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>, { @@ -459,6 +478,7 @@ impl BufferView { self.start_transaction(ctx); let mut text = String::new(); let mut selections = self.selections(ctx.app()).to_vec(); + let mut selection_lengths = Vec::with_capacity(selections.len()); { let buffer = self.buffer.read(ctx); let max_point = buffer.max_point(); @@ -471,7 +491,9 @@ impl BufferView { selection.start = buffer.anchor_before(start).unwrap(); selection.end = buffer.anchor_after(end).unwrap(); } + let prev_len = text.len(); text.extend(buffer.text_for_range(start..end).unwrap()); + selection_lengths.push(text.len() - prev_len); } } self.update_selections(selections, ctx); @@ -479,28 +501,71 @@ impl BufferView { self.insert(&String::new(), ctx); self.end_transaction(ctx); - ctx.app_mut().write_to_clipboard(ClipboardItem::new(text)); + ctx.app_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(selection_lengths)); } 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(); - for selection in self.selections(ctx.app()) { + let selections = self.selections(ctx.app()); + let mut selection_lengths = 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"); if start == end { start = Point::new(start.row, 0); end = cmp::min(max_point, Point::new(start.row + 1, 0)); } + let prev_len = text.len(); text.extend(buffer.text_for_range(start..end).unwrap()); + selection_lengths.push(text.len() - prev_len); } - ctx.app_mut().write_to_clipboard(ClipboardItem::new(text)); + ctx.app_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(selection_lengths)); } pub fn paste(&mut self, _: &(), ctx: &mut ViewContext) { if let Some(item) = ctx.app_mut().read_from_clipboard() { + let clipboard_text = item.text(); + if let Some(clipboard_slice_lengths) = item.metadata::>() { + // If there are the same number of selections as there were at the + // time that this clipboard data written, then paste one slice of the + // clipboard text into each of the current selections. + let selections = self.selections(ctx.app()).to_vec(); + if clipboard_slice_lengths.len() == selections.len() { + self.start_transaction(ctx); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut clipboard_offset = 0; + for (i, selection) in selections.iter().enumerate() { + let clipboard_length = clipboard_slice_lengths[i]; + let clipboard_slice = &clipboard_text + [clipboard_offset..(clipboard_offset + clipboard_length)]; + clipboard_offset = clipboard_offset + clipboard_length; + + self.buffer.update(ctx, |buffer, ctx| { + let char_count = clipboard_slice.chars().count(); + 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(); + let anchor = buffer.anchor_before(start + char_count).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); + return; + } + } self.insert(item.text(), ctx); } } @@ -1462,8 +1527,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.app()), @@ -1570,7 +1638,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), @@ -1592,6 +1660,58 @@ 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 " + ); + let ranges = view + .read(app) + .selections(app.as_ref()) + .iter() + .map(|selection| { + selection.start.to_offset(buffer.read(app)).unwrap() + ..selection.end.to_offset(buffer.read(app)).unwrap() + }) + .collect::>(); + assert_eq!(ranges, &[8..8, 19..19, 28..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 ) " + ); + }); + } + impl BufferView { fn selection_ranges(&self, app: &AppContext) -> Vec> { self.selections_in_range(DisplayPoint::zero()..self.max_point(app), app) From e08293507648a59837d68fec5df2a824b4642cd1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 13 Apr 2021 17:58:13 -0700 Subject: [PATCH 04/16] Handle 'full-line' clipboard items when pasting --- Cargo.lock | 1 + zed/Cargo.toml | 1 + zed/src/editor/buffer_view.rs | 76 ++++++++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42b66a0bac..35554ea1bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2286,6 +2286,7 @@ dependencies = [ "rand 0.8.3", "rust-embed", "seahash", + "serde", "serde_json", "simplelog", "smallvec", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 4ed0d1f1d5..57d4f26525 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_view.rs b/zed/src/editor/buffer_view.rs index c10e99e21e..9a1494ab21 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -12,6 +12,7 @@ use gpui::{ }; use gpui::{geometry::vector::Vector2F, TextLayoutCache}; use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; use std::{ @@ -108,6 +109,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())); @@ -478,14 +485,15 @@ impl BufferView { self.start_transaction(ctx); let mut text = String::new(); let mut selections = self.selections(ctx.app()).to_vec(); - let mut selection_lengths = Vec::with_capacity(selections.len()); + 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"); - if start == 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(); @@ -493,7 +501,10 @@ impl BufferView { } let prev_len = text.len(); text.extend(buffer.text_for_range(start..end).unwrap()); - selection_lengths.push(text.len() - prev_len); + clipboard_selections.push(ClipboardSelection { + len: text.len() - prev_len, + is_entire_line, + }); } } self.update_selections(selections, ctx); @@ -502,7 +513,7 @@ impl BufferView { self.end_transaction(ctx); ctx.app_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(selection_lengths)); + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); } pub fn copy(&mut self, _: &(), ctx: &mut ViewContext) { @@ -514,13 +525,17 @@ impl BufferView { 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"); - if start == 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 prev_len = text.len(); text.extend(buffer.text_for_range(start..end).unwrap()); - selection_lengths.push(text.len() - prev_len); + selection_lengths.push(ClipboardSelection { + len: text.len() - prev_len, + is_entire_line, + }); } ctx.app_mut() @@ -530,29 +545,54 @@ impl BufferView { pub fn paste(&mut self, _: &(), ctx: &mut ViewContext) { if let Some(item) = ctx.app_mut().read_from_clipboard() { let clipboard_text = item.text(); - if let Some(clipboard_slice_lengths) = item.metadata::>() { + if let Some(clipboard_selections) = item.metadata::>() { // If there are the same number of selections as there were at the - // time that this clipboard data written, then paste one slice of the + // time that this clipboard data was written, then paste one slice of the // clipboard text into each of the current selections. let selections = self.selections(ctx.app()).to_vec(); - if clipboard_slice_lengths.len() == selections.len() { + if clipboard_selections.len() == selections.len() { self.start_transaction(ctx); let mut new_selections = Vec::with_capacity(selections.len()); let mut clipboard_offset = 0; for (i, selection) in selections.iter().enumerate() { - let clipboard_length = clipboard_slice_lengths[i]; + let clipboard_selection = &clipboard_selections[i]; let clipboard_slice = &clipboard_text - [clipboard_offset..(clipboard_offset + clipboard_length)]; - clipboard_offset = clipboard_offset + clipboard_length; + [clipboard_offset..(clipboard_offset + 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 char_count = clipboard_slice.chars().count(); - 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(); - let anchor = buffer.anchor_before(start + char_count).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 + char_count).unwrap(); + } + new_selections.push(Selection { start: anchor.clone(), end: anchor, From 4a395314b2ca380ad59a35ff1dfffa71855d2c02 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Apr 2021 11:15:55 +0200 Subject: [PATCH 05/16] Handle paste correctly when there is only one full-line in the clipboard --- zed/src/editor/buffer_view.rs | 163 ++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 68 deletions(-) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 9a1494ab21..656cb4ecf1 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -18,6 +18,7 @@ use smol::Timer; use std::{ cmp::{self, Ordering}, fmt::Write, + iter::FromIterator, ops::Range, sync::Arc, time::Duration, @@ -499,10 +500,13 @@ impl BufferView { selection.start = buffer.anchor_before(start).unwrap(); selection.end = buffer.anchor_after(end).unwrap(); } - let prev_len = text.len(); - text.extend(buffer.text_for_range(start..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: text.len() - prev_len, + len, is_entire_line, }); } @@ -521,7 +525,7 @@ impl BufferView { let max_point = buffer.max_point(); let mut text = String::new(); let selections = self.selections(ctx.app()); - let mut selection_lengths = Vec::with_capacity(selections.len()); + 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"); @@ -530,86 +534,109 @@ impl BufferView { start = Point::new(start.row, 0); end = cmp::min(max_point, Point::new(start.row + 1, 0)); } - let prev_len = text.len(); - text.extend(buffer.text_for_range(start..end).unwrap()); - selection_lengths.push(ClipboardSelection { - len: text.len() - prev_len, + 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.app_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(selection_lengths)); + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); } pub fn paste(&mut self, _: &(), ctx: &mut ViewContext) { if let Some(item) = ctx.app_mut().read_from_clipboard() { let clipboard_text = item.text(); if let Some(clipboard_selections) = item.metadata::>() { - // 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. - let selections = self.selections(ctx.app()).to_vec(); - if clipboard_selections.len() == selections.len() { - self.start_transaction(ctx); - let mut new_selections = Vec::with_capacity(selections.len()); - let mut clipboard_offset = 0; - for (i, selection) in selections.iter().enumerate() { - let clipboard_selection = &clipboard_selections[i]; - let clipboard_slice = &clipboard_text - [clipboard_offset..(clipboard_offset + 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 char_count = clipboard_slice.chars().count(); - 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 + char_count).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); - return; + let selections_len = self.selections(ctx.app()).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 over and over into each of the current selections so that + // we can position it before the 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); } - self.insert(item.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.app()).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))); From 6a181ac692915fede07fee17a6efc4ecd0ac1a8f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Apr 2021 11:54:58 +0200 Subject: [PATCH 06/16] Add test for copying/cutting/pasting full lines --- zed/src/editor/buffer_view.rs | 109 ++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 10 deletions(-) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 656cb4ecf1..8affb9f3b9 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -1752,16 +1752,14 @@ mod tests { view.read(app).text(app.as_ref()), "two one four three six five " ); - let ranges = view - .read(app) - .selections(app.as_ref()) - .iter() - .map(|selection| { - selection.start.to_offset(buffer.read(app)).unwrap() - ..selection.end.to_offset(buffer.read(app)).unwrap() - }) - .collect::>(); - assert_eq!(ranges, &[8..8, 19..19, 28..28]); + 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 @@ -1776,6 +1774,97 @@ mod tests { 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), + ] + ); }); } From a1053f782086edda49d1b2ca7a2514894d4e289f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Apr 2021 11:59:39 +0200 Subject: [PATCH 07/16] :memo: --- zed/src/editor/buffer_view.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 8affb9f3b9..83d580a69b 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -562,8 +562,8 @@ impl BufferView { } 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 over and over into each of the current selections so that - // we can position it before the selections that are empty. + // 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(), From f755cbbe98341b1b1857361d9e7d309c8b3e2e52 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Apr 2021 14:47:18 +0200 Subject: [PATCH 08/16] Dispatch global actions only once when triggering a menu item Previously we would dispatch the same global action more than once because we would invoke `dispatch_action_any` _and_ `dispatch_global_action_any`. However, the former already takes care of going through the global action handlers when no entity in the dispatch path handled the action. --- gpui/src/app.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index b35c5ba0b4..cc309d58cb 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -141,12 +141,13 @@ impl App { { let presenter = presenter.clone(); let path = presenter.borrow().dispatch_path(ctx.as_ref()); - if ctx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&())) { - return; - } + ctx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&())); + } else { + ctx.dispatch_global_action_any(command, arg.unwrap_or(&())); } + } else { + ctx.dispatch_global_action_any(command, arg.unwrap_or(&())); } - ctx.dispatch_global_action_any(command, arg.unwrap_or(&())); })); app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0)); From cf23b0e4a2e34fa33c9f9d3e461d4b7156b078ef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Apr 2021 16:30:03 +0200 Subject: [PATCH 09/16] Prompt for paths asynchronously to avoid double borrow --- Cargo.lock | 1 + gpui/Cargo.toml | 1 + gpui/src/app.rs | 18 +++++++++++- gpui/src/platform/mac/platform.rs | 46 +++++++++++++++++++------------ gpui/src/platform/mod.rs | 6 +++- gpui/src/platform/test.rs | 7 +++-- zed/src/workspace/mod.rs | 26 ++++++++--------- 7 files changed, 70 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35554ea1bd..5692f38a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -903,6 +903,7 @@ dependencies = [ "async-std", "async-task", "bindgen", + "block", "cc", "cocoa", "core-foundation", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 5bd32560c6..d87883bab6 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -37,6 +37,7 @@ simplelog = "0.9" [target.'cfg(target_os = "macos")'.dependencies] anyhow = "1" +block = "0.1" cocoa = "0.24" core-foundation = "0.9" core-graphics = "0.22.2" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 59e4471d07..738ecd79ce 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, ClipboardItem, FontCache, TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; use async_std::sync::Condvar; @@ -570,6 +570,22 @@ impl MutableAppContext { self.platform.set_menus(menus); } + pub fn prompt_for_paths(&self, options: PathPromptOptions, done_fn: F) + where + F: 'static + FnOnce(Option>, &mut MutableAppContext), + { + let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); + let foreground = self.foreground.clone(); + self.platform().prompt_for_paths( + options, + Box::new(move |paths| { + foreground + .spawn(async move { (done_fn)(paths, &mut *app.borrow_mut()) }) + .detach(); + }), + ); + } + pub fn dispatch_action( &mut self, window_id: usize, diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index b7ce76f475..e9bf34d684 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -1,5 +1,6 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window}; use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem}; +use block::ConcreteBlock; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, @@ -20,7 +21,7 @@ use objc::{ use ptr::null_mut; use std::{ any::Any, - cell::RefCell, + cell::{Cell, RefCell}, convert::TryInto, ffi::{c_void, CStr}, os::raw::c_char, @@ -267,31 +268,40 @@ impl platform::Platform for MacPlatform { fn prompt_for_paths( &self, options: platform::PathPromptOptions, - ) -> Option> { + done_fn: Box>)>, + ) { unsafe { let panel = NSOpenPanel::openPanel(nil); panel.setCanChooseDirectories_(options.directories.to_objc()); panel.setCanChooseFiles_(options.files.to_objc()); panel.setAllowsMultipleSelection_(options.multiple.to_objc()); panel.setResolvesAliases_(false.to_objc()); - let response = panel.runModal(); - if response == NSModalResponse::NSModalResponseOk { - let mut result = Vec::new(); - let urls = panel.URLs(); - for i in 0..urls.count() { - let url = urls.objectAtIndex(i); - let string = url.absoluteString(); - let string = std::ffi::CStr::from_ptr(string.UTF8String()) - .to_string_lossy() - .to_string(); - if let Some(path) = string.strip_prefix("file://") { - result.push(PathBuf::from(path)); + let done_fn = Cell::new(Some(done_fn)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let result = if response == NSModalResponse::NSModalResponseOk { + let mut result = Vec::new(); + let urls = panel.URLs(); + for i in 0..urls.count() { + let url = urls.objectAtIndex(i); + let string = url.absoluteString(); + let string = std::ffi::CStr::from_ptr(string.UTF8String()) + .to_string_lossy() + .to_string(); + if let Some(path) = string.strip_prefix("file://") { + result.push(PathBuf::from(path)); + } } + Some(result) + } else { + None + }; + + if let Some(done_fn) = done_fn.take() { + (done_fn)(result); } - Some(result) - } else { - None - } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; } } diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index 5f34396a0e..b98d3a687b 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -40,7 +40,11 @@ pub trait Platform { executor: Rc, ) -> Box; fn key_window_id(&self) -> Option; - fn prompt_for_paths(&self, options: PathPromptOptions) -> Option>; + fn prompt_for_paths( + &self, + options: PathPromptOptions, + done_fn: Box>)>, + ); fn quit(&self); fn write_to_clipboard(&self, item: ClipboardItem); fn read_from_clipboard(&self) -> Option; diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 3baa0dab6e..878449a021 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -70,8 +70,11 @@ impl super::Platform for Platform { fn quit(&self) {} - fn prompt_for_paths(&self, _: super::PathPromptOptions) -> Option> { - None + fn prompt_for_paths( + &self, + _: super::PathPromptOptions, + _: Box>)>, + ) { } fn write_to_clipboard(&self, item: ClipboardItem) { diff --git a/zed/src/workspace/mod.rs b/zed/src/workspace/mod.rs index d9e9039994..005b4b2435 100644 --- a/zed/src/workspace/mod.rs +++ b/zed/src/workspace/mod.rs @@ -29,19 +29,19 @@ pub struct OpenParams { } fn open(settings: &Receiver, ctx: &mut MutableAppContext) { - if let Some(paths) = ctx.platform().prompt_for_paths(PathPromptOptions { - files: true, - directories: true, - multiple: true, - }) { - ctx.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths, - settings: settings.clone(), - }, - ); - } + let settings = settings.clone(); + ctx.prompt_for_paths( + PathPromptOptions { + files: true, + directories: true, + multiple: true, + }, + move |paths, ctx| { + if let Some(paths) = paths { + ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings }); + } + }, + ); } fn open_paths(params: &OpenParams, app: &mut MutableAppContext) { From 4cef25eff8b247d0cbbc4f6cc7ef8ba2ba96ad21 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 Apr 2021 19:15:08 -0600 Subject: [PATCH 10/16] Replace easy-parallel with scoped-pool for path searches The easy-parallel crate spawned new threads on each call, which was resulting in way too many threads. Co-Authored-By: Brooks Swinnerton <934497+bswinnerton@users.noreply.github.com> --- Cargo.lock | 32 +++++++++++++++++++++++++++++++- gpui/Cargo.toml | 3 ++- gpui/src/app.rs | 6 ++++++ gpui/src/lib.rs | 1 + zed/src/file_finder.rs | 3 ++- zed/src/worktree/fuzzy.rs | 16 ++++++++-------- zed/src/worktree/worktree.rs | 6 ++++-- 7 files changed, 54 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5692f38a16..119ba96e46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,6 +498,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crossbeam" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be" + [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -925,6 +931,7 @@ dependencies = [ "rand 0.8.3", "replace_with", "resvg", + "scoped-pool", "seahash", "serde", "serde_json", @@ -1067,7 +1074,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" dependencies = [ - "scopeguard", + "scopeguard 1.1.0", ] [[package]] @@ -1676,12 +1683,29 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-pool" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a3a15e704545ce59ed2b5c60a5d32bda4d7869befb8b36667b658a6c00b43" +dependencies = [ + "crossbeam", + "scopeguard 0.1.2", + "variance", +] + [[package]] name = "scoped-tls" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +[[package]] +name = "scopeguard" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a076157c1e2dc561d8de585151ee6965d910dd4dcb5dabb7ae3e83981a6c57" + [[package]] name = "scopeguard" version = "1.1.0" @@ -2074,6 +2098,12 @@ dependencies = [ "ctor", ] +[[package]] +name = "variance" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abfc2be1fb59663871379ea884fd81de80c496f2274e021c01d6fe56cd77b05" + [[package]] name = "vec-arena" version = "1.0.0" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index d87883bab6..0f11bd1de2 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -18,8 +18,9 @@ pathfinder_geometry = "0.5" rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" +scoped-pool = "1.0.0" seahash = "4.1" -serde = { version = "1.0.125", features = ["derive"] } +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 738ecd79ce..6453e4124e 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -414,6 +414,7 @@ impl MutableAppContext { windows: HashMap::new(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background: Arc::new(executor::Background::new()), + scoped_pool: scoped_pool::Pool::new(num_cpus::get()), }, actions: HashMap::new(), global_actions: HashMap::new(), @@ -1336,6 +1337,7 @@ pub struct AppContext { windows: HashMap, background: Arc, ref_counts: Arc>, + scoped_pool: scoped_pool::Pool, } impl AppContext { @@ -1374,6 +1376,10 @@ impl AppContext { pub fn background_executor(&self) -> &Arc { &self.background } + + pub fn scoped_pool(&self) -> &scoped_pool::Pool { + &self.scoped_pool + } } impl ReadModel for AppContext { diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index ee8c544a83..edf14bc65c 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -29,3 +29,4 @@ pub use presenter::{ AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; +pub use scoped_pool; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index b27189814c..654017556d 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -347,8 +347,9 @@ impl FileFinder { fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { let worktrees = self.worktrees(ctx.as_ref()); let search_id = util::post_inc(&mut self.search_count); + let pool = ctx.app().scoped_pool().clone(); let task = ctx.background_executor().spawn(async move { - let matches = match_paths(worktrees.as_slice(), &query, false, false, 100); + let matches = match_paths(worktrees.as_slice(), &query, false, false, 100, pool); (search_id, matches) }); diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index e6baeec962..c4a3d451ed 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -1,4 +1,4 @@ -use easy_parallel::Parallel; +use gpui::scoped_pool; use super::char_bag::CharBag; @@ -54,6 +54,7 @@ pub fn match_paths( include_ignored: bool, smart_case: bool, max_results: usize, + pool: scoped_pool::Pool, ) -> Vec { let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); @@ -68,10 +69,9 @@ pub fn match_paths( let segment_size = (path_count + cpus - 1) / cpus; let mut segment_results = (0..cpus).map(|_| BinaryHeap::new()).collect::>(); - Parallel::new() - .each( - segment_results.iter_mut().enumerate(), - |(segment_idx, results)| { + pool.scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + scope.execute(move || { let segment_start = segment_idx * segment_size; let segment_end = segment_start + segment_size; @@ -115,9 +115,9 @@ pub fn match_paths( } tree_start = tree_end; } - }, - ) - .run(); + }) + } + }); let mut results = segment_results .into_iter() diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index 2e63ad117f..60fab4c5a9 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -11,7 +11,7 @@ use crate::{ use anyhow::{anyhow, Result}; use crossbeam_channel as channel; use easy_parallel::Parallel; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::RwLock; use smol::prelude::*; @@ -606,6 +606,7 @@ pub fn match_paths( include_ignored: bool, smart_case: bool, max_results: usize, + pool: scoped_pool::Pool, ) -> Vec { let tree_states = trees.iter().map(|tree| tree.0.read()).collect::>(); fuzzy::match_paths( @@ -634,6 +635,7 @@ pub fn match_paths( include_ignored, smart_case, max_results, + pool, ) } @@ -674,7 +676,7 @@ mod test { app.read(|ctx| { let tree = tree.read(ctx); assert_eq!(tree.file_count(), 4); - let results = match_paths(&[tree.clone()], "bna", false, false, 10) + let results = match_paths(&[tree.clone()], "bna", false, false, 10, ctx.scoped_pool().clone()) .iter() .map(|result| tree.entry_path(result.entry_id)) .collect::, _>>() From e97ce4ff587a69ce10134295d3cd895acc1f62a1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 Apr 2021 09:11:11 -0600 Subject: [PATCH 11/16] Fix after method rename Co-Authored-By: Antonio Scandurra --- zed/src/file_finder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 654017556d..e153951bf1 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -347,7 +347,7 @@ impl FileFinder { fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { let worktrees = self.worktrees(ctx.as_ref()); let search_id = util::post_inc(&mut self.search_count); - let pool = ctx.app().scoped_pool().clone(); + let pool = ctx.as_ref().scoped_pool().clone(); let task = ctx.background_executor().spawn(async move { let matches = match_paths(worktrees.as_slice(), &query, false, false, 100, pool); (search_id, matches) From f4538e9eb5d509fa78da99c01bde74647a3be9ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 14 Apr 2021 19:02:44 +0200 Subject: [PATCH 12/16] Generalize pasting when number of selections doesn't match clipboard's Co-Authored-By: Nathan Sobo --- zed/src/editor/buffer/anchor.rs | 22 ++++++ zed/src/editor/buffer/mod.rs | 6 ++ zed/src/editor/buffer_view.rs | 134 +++++++++++++------------------- 3 files changed, 81 insertions(+), 81 deletions(-) diff --git a/zed/src/editor/buffer/anchor.rs b/zed/src/editor/buffer/anchor.rs index b0e2f0269c..60106daba5 100644 --- a/zed/src/editor/buffer/anchor.rs +++ b/zed/src/editor/buffer/anchor.rs @@ -69,6 +69,28 @@ impl Anchor { .then_with(|| self_bias.cmp(other_bias)), }) } + + pub fn bias_left(&self, buffer: &Buffer) -> Result { + match self { + Anchor::Start + | Anchor::Middle { + bias: AnchorBias::Left, + .. + } => Ok(self.clone()), + _ => buffer.anchor_before(self), + } + } + + pub fn bias_right(&self, buffer: &Buffer) -> Result { + match self { + Anchor::End + | Anchor::Middle { + bias: AnchorBias::Right, + .. + } => Ok(self.clone()), + _ => buffer.anchor_after(self), + } + } } pub trait AnchorRangeExt { diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index fc633ed03f..7bbc4db4d9 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -2264,6 +2264,12 @@ impl ToOffset for Anchor { } } +impl<'a> ToOffset for &'a Anchor { + fn to_offset(&self, buffer: &Buffer) -> Result { + Ok(buffer.summary_for_anchor(self)?.chars) + } +} + pub trait ToPoint { fn to_point(&self, buffer: &Buffer) -> Result; } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index f36f88c422..5cef38f232 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -375,7 +375,7 @@ impl BufferView { let mut selections = Vec::new(); for range in ranges { selections.push(Selection { - start: buffer.anchor_after(range.start)?, + start: buffer.anchor_before(range.start)?, end: buffer.anchor_before(range.end)?, reversed: false, goal_column: None, @@ -394,7 +394,7 @@ impl BufferView { let mut selections = Vec::new(); for range in ranges { selections.push(Selection { - start: map.anchor_after(range.start, Bias::Left, ctx.as_ref())?, + start: map.anchor_before(range.start, Bias::Left, ctx.as_ref())?, end: map.anchor_before(range.end, Bias::Left, ctx.as_ref())?, reversed: false, goal_column: None, @@ -503,7 +503,7 @@ impl BufferView { 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(); + selection.end = buffer.anchor_before(end).unwrap(); } let mut len = 0; for ch in buffer.text_for_range(start..end).unwrap() { @@ -557,91 +557,63 @@ impl BufferView { 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); + if let Some(mut clipboard_selections) = item.metadata::>() { + let selections = self.selections(ctx.as_ref()).to_vec(); + if clipboard_selections.len() != selections.len() { + let merged_selection = ClipboardSelection { + len: clipboard_selections.iter().map(|s| s.len).sum(), + is_entire_line: clipboard_selections.iter().all(|s| s.is_entire_line), + }; + clipboard_selections.clear(); + clipboard_selections.push(merged_selection); } + + self.start_transaction(ctx); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut clipboard_chars = clipboard_text.chars().cycle(); + for (selection, clipboard_selection) in + selections.iter().zip(clipboard_selections.iter().cycle()) + { + let to_insert = + String::from_iter(clipboard_chars.by_ref().take(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(); + + // 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 new_selection_start = selection.end.bias_right(buffer).unwrap(); + if selection_start == selection_end && clipboard_selection.is_entire_line { + let line_start = Point::new(selection_start.row, 0); + buffer + .edit(Some(line_start..line_start), to_insert, Some(ctx)) + .unwrap(); + } else { + buffer + .edit(Some(&selection.start..&selection.end), to_insert, Some(ctx)) + .unwrap(); + }; + + let new_selection_start = new_selection_start.bias_left(buffer).unwrap(); + new_selections.push(Selection { + start: new_selection_start.clone(), + end: new_selection_start, + reversed: false, + goal_column: None, + }); + }); + } + self.update_selections(new_selections, ctx); + self.end_transaction(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))); From f5752969abb96e3c10bdfd332a792561a50dfd66 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Apr 2021 15:09:38 -0700 Subject: [PATCH 13/16] Include constraints in element tree JSON debug output Co-Authored-By: Nathan Sobo --- gpui/src/elements/constrained_box.rs | 2 +- gpui/src/elements/new.rs | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/gpui/src/elements/constrained_box.rs b/gpui/src/elements/constrained_box.rs index 95b6d29637..47e83e27a9 100644 --- a/gpui/src/elements/constrained_box.rs +++ b/gpui/src/elements/constrained_box.rs @@ -91,6 +91,6 @@ impl Element for ConstrainedBox { _: &Self::PaintState, ctx: &DebugContext, ) -> json::Value { - json!({"type": "ConstrainedBox", "constraint": self.constraint.to_json(), "child": self.child.debug(ctx)}) + json!({"type": "ConstrainedBox", "set_constraint": self.constraint.to_json(), "child": self.child.debug(ctx)}) } } diff --git a/gpui/src/elements/new.rs b/gpui/src/elements/new.rs index 3521a5fab0..e45a71e16a 100644 --- a/gpui/src/elements/new.rs +++ b/gpui/src/elements/new.rs @@ -4,6 +4,7 @@ use crate::{ SizeConstraint, }; use core::panic; +use json::ToJson; use replace_with::replace_with_or_abort; use std::{any::Any, borrow::Cow}; @@ -90,11 +91,13 @@ pub enum Lifecycle { }, PostLayout { element: T, + constraint: SizeConstraint, size: Vector2F, layout: T::LayoutState, }, PostPaint { element: T, + constraint: SizeConstraint, bounds: RectF, layout: T::LayoutState, paint: T::PaintState, @@ -119,6 +122,7 @@ impl AnyElement for Lifecycle { result = Some(size); Lifecycle::PostLayout { element, + constraint, size, layout, } @@ -132,6 +136,7 @@ impl AnyElement for Lifecycle { element, size, layout, + .. } = self { element.after_layout(*size, layout, ctx); @@ -144,6 +149,7 @@ impl AnyElement for Lifecycle { replace_with_or_abort(self, |me| { if let Lifecycle::PostLayout { mut element, + constraint, size, mut layout, } = me @@ -152,6 +158,7 @@ impl AnyElement for Lifecycle { let paint = element.paint(bounds, &mut layout, ctx); Lifecycle::PostPaint { element, + constraint, bounds, layout, paint, @@ -168,6 +175,7 @@ impl AnyElement for Lifecycle { bounds, layout, paint, + .. } = self { element.dispatch_event(event, *bounds, layout, paint, ctx) @@ -196,10 +204,25 @@ impl AnyElement for Lifecycle { match self { Lifecycle::PostPaint { element, + constraint, bounds, layout, paint, - } => element.debug(*bounds, layout, paint, ctx), + } => { + let mut value = element.debug(*bounds, layout, paint, ctx); + if let json::Value::Object(map) = &mut value { + let mut new_map: crate::json::Map = + Default::default(); + if let Some(typ) = map.remove("type") { + new_map.insert("type".into(), typ); + } + new_map.insert("constraint".into(), constraint.to_json()); + new_map.append(map); + json::Value::Object(new_map) + } else { + value + } + } _ => panic!("invalid element lifecycle state"), } } From 36699dc09550e6b86b85451b7f0e830ee34466b6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Apr 2021 15:11:56 -0700 Subject: [PATCH 14/16] Avoid setting constrain min to infinity in Flex layout Co-Authored-By: Nathan Sobo --- gpui/src/elements/flex.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index db87a80dbf..c9426c713c 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -64,8 +64,16 @@ impl Element for Flex { if let Some(flex) = Self::child_flex(&child) { total_flex += flex; } else { - let child_constraint = - SizeConstraint::strict_along(cross_axis, constraint.max_along(cross_axis)); + let child_constraint = match self.axis { + Axis::Horizontal => SizeConstraint::new( + vec2f(0.0, constraint.min.y()), + vec2f(INFINITY, constraint.max.y()), + ), + Axis::Vertical => SizeConstraint::new( + vec2f(constraint.min.x(), 0.0), + vec2f(constraint.max.x(), INFINITY), + ), + }; let size = child.layout(child_constraint, ctx); fixed_space += size.along(self.axis); cross_axis_max = cross_axis_max.max(size.along(cross_axis)); @@ -85,11 +93,11 @@ impl Element for Flex { let child_max = space_per_flex * flex; let child_constraint = match self.axis { Axis::Horizontal => SizeConstraint::new( - vec2f(0.0, constraint.max.y()), + vec2f(0.0, constraint.min.y()), vec2f(child_max, constraint.max.y()), ), Axis::Vertical => SizeConstraint::new( - vec2f(constraint.max.x(), 0.0), + vec2f(constraint.min.x(), 0.0), vec2f(constraint.max.x(), child_max), ), }; From 3f71867af8e07fb412437a550fe1aafa74f730a4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Apr 2021 15:14:40 -0700 Subject: [PATCH 15/16] Improve styling of tabs * Enforce a min width per tab * Center the title within tab, regardless of icon * Render icon over the top of the tab title * Ensure there is always a fixed minimum amount of filler to the right of all tabs Co-Authored-By: Nathan Sobo --- gpui/src/elements/align.rs | 11 +++- gpui/src/elements/constrained_box.rs | 12 ++++ gpui/src/elements/container.rs | 12 ++++ gpui/src/elements/flex.rs | 10 ++- zed/src/file_finder.rs | 2 +- zed/src/workspace/pane.rs | 93 ++++++++++++++++------------ 6 files changed, 95 insertions(+), 45 deletions(-) diff --git a/gpui/src/elements/align.rs b/gpui/src/elements/align.rs index a879870cab..8bb59169e2 100644 --- a/gpui/src/elements/align.rs +++ b/gpui/src/elements/align.rs @@ -3,7 +3,7 @@ use crate::{ LayoutContext, PaintContext, SizeConstraint, }; use json::ToJson; -use pathfinder_geometry::vector::{vec2f, Vector2F}; +use pathfinder_geometry::vector::Vector2F; use serde_json::json; pub struct Align { @@ -19,8 +19,13 @@ impl Align { } } - pub fn top_center(mut self) -> Self { - self.alignment = vec2f(0.0, -1.0); + pub fn top(mut self) -> Self { + self.alignment.set_y(-1.0); + self + } + + pub fn right(mut self) -> Self { + self.alignment.set_x(1.0); self } } diff --git a/gpui/src/elements/constrained_box.rs b/gpui/src/elements/constrained_box.rs index 47e83e27a9..a705be3612 100644 --- a/gpui/src/elements/constrained_box.rs +++ b/gpui/src/elements/constrained_box.rs @@ -23,6 +23,11 @@ impl ConstrainedBox { } } + pub fn with_min_width(mut self, min_width: f32) -> Self { + self.constraint.min.set_x(min_width); + self + } + pub fn with_max_width(mut self, max_width: f32) -> Self { self.constraint.max.set_x(max_width); self @@ -33,6 +38,12 @@ impl ConstrainedBox { self } + pub fn with_width(mut self, width: f32) -> Self { + self.constraint.min.set_x(width); + self.constraint.max.set_x(width); + self + } + pub fn with_height(mut self, height: f32) -> Self { self.constraint.min.set_y(height); self.constraint.max.set_y(height); @@ -51,6 +62,7 @@ impl Element for ConstrainedBox { ) -> (Vector2F, Self::LayoutState) { constraint.min = constraint.min.max(self.constraint.min); constraint.max = constraint.max.min(self.constraint.max); + constraint.max = constraint.max.max(constraint.min); let size = self.child.layout(constraint, ctx); (size, ()) } diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 9554f3b353..d228c54d07 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -43,6 +43,18 @@ impl Container { self } + pub fn with_horizontal_padding(mut self, padding: f32) -> Self { + self.padding.left = padding; + self.padding.right = padding; + self + } + + pub fn with_vertical_padding(mut self, padding: f32) -> Self { + self.padding.top = padding; + self.padding.bottom = padding; + self + } + pub fn with_uniform_padding(mut self, padding: f32) -> Self { self.padding = Padding { top: padding, diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index c9426c713c..cbce89ed2f 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -1,4 +1,4 @@ -use std::any::Any; +use std::{any::Any, f32::INFINITY}; use crate::{ json::{self, ToJson, Value}, @@ -88,9 +88,13 @@ impl Element for Flex { let mut remaining_space = constraint.max_along(self.axis) - fixed_space; let mut remaining_flex = total_flex; for child in &mut self.children { - let space_per_flex = remaining_space / remaining_flex; if let Some(flex) = Self::child_flex(&child) { - let child_max = space_per_flex * flex; + let child_max = if remaining_flex == 0.0 { + remaining_space + } else { + let space_per_flex = remaining_space / remaining_flex; + space_per_flex * flex + }; let child_constraint = match self.axis { Axis::Horizontal => SizeConstraint::new( vec2f(0.0, constraint.min.y()), diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index e153951bf1..954b31b892 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -77,7 +77,7 @@ impl View for FileFinder { .with_max_height(400.0) .boxed(), ) - .top_center() + .top() .named("file finder") } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index e0b0109ea4..08c9864fa0 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -192,33 +192,28 @@ impl Pane { let padding = 6.; let mut container = Container::new( - Align::new( - Flex::row() - .with_child( + Stack::new() + .with_child( + Align::new( Label::new(title, settings.ui_font_family, settings.ui_font_size) .boxed(), ) - .with_child( - Container::new( - LineBox::new( - settings.ui_font_family, - settings.ui_font_size, - ConstrainedBox::new(Self::render_modified_icon( - item.is_dirty(app), - )) - .with_max_width(12.) - .boxed(), - ) + .boxed(), + ) + .with_child( + LineBox::new( + settings.ui_font_family, + settings.ui_font_size, + Align::new(Self::render_modified_icon(item.is_dirty(app))) + .right() .boxed(), - ) - .with_margin_left(20.) - .boxed(), ) .boxed(), - ) - .boxed(), + ) + .boxed(), ) - .with_uniform_padding(padding) + .with_vertical_padding(padding) + .with_horizontal_padding(10.) .with_border(border); if ix == self.active_item { @@ -240,6 +235,7 @@ impl Pane { }) .boxed(), ) + .with_min_width(80.0) .with_max_width(264.0) .boxed(), ) @@ -247,9 +243,29 @@ impl Pane { ); } + // Ensure there's always a minimum amount of space after the last tab, + // so that the tab's border doesn't abut the window's border. + row.add_child( + ConstrainedBox::new( + Container::new( + LineBox::new( + settings.ui_font_family, + settings.ui_font_size, + Empty::new().boxed(), + ) + .boxed(), + ) + .with_uniform_padding(6.0) + .with_border(Border::bottom(1.0, border_color)) + .boxed(), + ) + .with_min_width(20.) + .named("fixed-filler"), + ); + row.add_child( Expanded::new( - 1.0, + 0.0, Container::new( LineBox::new( settings.ui_font_family, @@ -269,23 +285,24 @@ impl Pane { } fn render_modified_icon(is_modified: bool) -> ElementBox { - Canvas::new(move |bounds, ctx| { - if is_modified { - let padding = if bounds.height() < bounds.width() { - vec2f(bounds.width() - bounds.height(), 0.0) - } else { - vec2f(0.0, bounds.height() - bounds.width()) - }; - let square = RectF::new(bounds.origin() + padding / 2., bounds.size() - padding); - ctx.scene.push_quad(Quad { - bounds: square, - background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()), - border: Default::default(), - corner_radius: square.width() / 2., - }); - } - }) - .boxed() + let diameter = 8.; + ConstrainedBox::new( + Canvas::new(move |bounds, ctx| { + if is_modified { + let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); + ctx.scene.push_quad(Quad { + bounds: square, + background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()), + border: Default::default(), + corner_radius: diameter / 2., + }); + } + }) + .boxed(), + ) + .with_width(diameter) + .with_height(diameter) + .named("tab-right-icon") } } From da68bd6c2b6d91a6800a6e11d252c183927758e2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 Apr 2021 21:56:24 -0600 Subject: [PATCH 16/16] Try to run our CI on a mac mini in my closet --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b177817a36..20b194966b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ env: jobs: tests: name: Tests - runs-on: macos-latest + runs-on: self-hosted steps: - name: Checkout repo uses: actions/checkout@v2