Merge pull request #18 from zed-industries/copy-paste

Introduce cut/copy/paste support
This commit is contained in:
Antonio Scandurra
2021-04-14 15:49:30 +02:00
committed by GitHub
12 changed files with 560 additions and 34 deletions
Generated
+16
View File
@@ -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",
+2 -1
View File
@@ -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"
+7 -3
View File
@@ -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<ClipboardItem> {
self.platform.read_from_clipboard()
}
}
+42
View File
@@ -0,0 +1,42 @@
use seahash::SeaHasher;
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem {
pub(crate) text: String,
pub(crate) metadata: Option<String>,
}
impl ClipboardItem {
pub fn new(text: String) -> Self {
Self {
text,
metadata: None,
}
}
pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
self
}
pub fn text(&self) -> &String {
&self.text
}
pub fn metadata<T>(&self) -> Option<T>
where
T: for<'a> Deserialize<'a>,
{
self.metadata
.as_ref()
.and_then(|m| serde_json::from_str(m).ok())
}
pub(crate) fn text_hash(text: &str) -> u64 {
let mut hasher = SeaHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
}
+2
View File
@@ -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;
+127 -8
View File
@@ -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<FontSystem>,
callbacks: RefCell<Callbacks>,
menu_item_actions: RefCell<Vec<(String, Option<Box<dyn Any>>)>>,
pasteboard: id,
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
}
#[derive(Default)]
@@ -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<ClipboardItem> {
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
}
}
+3 -2
View File
@@ -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<usize>;
fn prompt_for_paths(&self, options: PathPromptOptions) -> Option<Vec<PathBuf>>;
fn quit(&self);
fn copy(&self, text: &str);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn set_menus(&self, menus: Vec<Menu>);
}
+11 -3
View File
@@ -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<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: RefCell<Option<ClipboardItem>>,
}
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<ClipboardItem> {
self.current_clipboard_item.borrow().clone()
}
}
impl Window {
+1
View File
@@ -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"
+7 -8
View File
@@ -563,10 +563,13 @@ impl Buffer {
self.chars().collect()
}
pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> Result<String> {
pub fn text_for_range<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Result<impl 'a + Iterator<Item = char>> {
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;
+338 -6
View File
@@ -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<Settings>, ctx: &mut ViewContext<Self>) -> 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<Self>) -> Result<()>
where
T: IntoIterator<Item = &'a Range<usize>>,
{
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<Self>) -> Result<()>
where
T: IntoIterator<Item = &'a Range<DisplayPoint>>,
{
@@ -454,6 +487,161 @@ impl BufferView {
self.end_transaction(ctx);
}
pub fn cut(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
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<Self>) {
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<Self>) {
if let Some(item) = ctx.as_mut().read_from_clipboard() {
let clipboard_text = item.text();
if let Some(clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
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<Item = char>,
clipboard_selections: impl Iterator<Item = &'a ClipboardSelection>,
ctx: &mut ViewContext<Self>,
) {
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>) {
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<Range<DisplayPoint>> {
self.selections_in_range(DisplayPoint::zero()..self.max_point(app), app)
+4 -3
View File
@@ -3,7 +3,7 @@ use crate::{settings::Settings, watch};
use futures_core::future::LocalBoxFuture;
use gpui::{
color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle,
ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle,
};
use log::{error, info};
use std::{collections::HashSet, path::PathBuf};
@@ -258,10 +258,11 @@ impl WorkspaceView {
pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
match to_string_pretty(&ctx.debug_elements()) {
Ok(json) => {
ctx.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) => {