Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/editor/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,15 @@ pub struct Undo;
#[derive(PartialEq, Clone, Default, Action)]
#[action(namespace = editor)]
pub struct Redo;

#[derive(PartialEq, Clone, Default, Action)]
#[action(namespace = editor)]
pub struct Cut;

#[derive(PartialEq, Clone, Default, Action)]
#[action(namespace = editor)]
pub struct Copy;

#[derive(PartialEq, Clone, Default, Action)]
#[action(namespace = editor)]
pub struct Paste;
99 changes: 98 additions & 1 deletion crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ mod tests;
pub use actions::*;

use gpui::{
App, Bounds, CursorStyle, Entity, EntityInputHandler, FocusHandle, Focusable,
App, Bounds, ClipboardItem, CursorStyle, Entity, EntityInputHandler, FocusHandle, Focusable,
InteractiveElement, MouseDownEvent, MouseMoveEvent, Pixels, Point, UTF16Selection, Window,
prelude::*,
};
Expand Down Expand Up @@ -450,6 +450,94 @@ impl Editor {
cx.notify();
}
}

pub fn cut(&mut self, _: &Cut, _window: &mut Window, cx: &mut Context<Self>) {
let range = self.selection.range();
let text = self
.buffer
.update(cx, |buffer, _| buffer.slice(range.clone()));
cx.write_to_clipboard(ClipboardItem::new_string(text));

if !self.selection.is_empty() {
let selection_before = self.selection;
let selection_after = Selection::cursor(self.selection.start);
let transaction_id = self.buffer.update(cx, |buffer, _| {
buffer.transaction(Instant::now(), |buffer, tx| {
buffer.remove(tx, range);
})
});

self.selection = selection_after;
if let Some(transaction) = self
.undo_stack
.iter_mut()
.find(|entry| entry.id == transaction_id)
{
transaction.selection_after = selection_after;
} else {
self.undo_stack.push_back(Transaction {
id: transaction_id,
selection_before,
selection_after,
});
self.redo_stack.clear();
}
cx.notify();
}
}

pub fn copy(&mut self, _: &Copy, _window: &mut Window, cx: &mut Context<Self>) {
let text = self.buffer.update(cx, |buffer, _| {
if self.selection.is_cursor() {
let row = buffer.offset_to_point(self.selection.start).row;
let line_start = buffer.point_to_offset(TextPoint { row, column: 0 });
let line_end = buffer.point_to_offset(TextPoint {
row,
column: buffer.line_len(row),
});
let mut line = buffer.slice(line_start..line_end);
if !line.ends_with('\n') {
line.push('\n');
}
line
} else {
buffer.slice(self.selection.range())
}
});

cx.write_to_clipboard(ClipboardItem::new_string(text));
}

pub fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
if let Some(item) = cx.read_from_clipboard() {
let text = item.text().unwrap_or_default();
let range = self.selection.range();
let selection_before = self.selection;
let selection_after = Selection::cursor(range.start + text.len());
let transaction_id = self.buffer.update(cx, |buffer, _| {
buffer.transaction(Instant::now(), |buffer, tx| {
buffer.replace(tx, range.clone(), &text);
})
});

self.selection = selection_after;
if let Some(transaction) = self
.undo_stack
.iter_mut()
.find(|entry| entry.id == transaction_id)
{
transaction.selection_after = selection_after;
} else {
self.undo_stack.push_back(Transaction {
id: transaction_id,
selection_before,
selection_after,
});
self.redo_stack.clear();
}
cx.notify();
}
}
}

impl Render for Editor {
Expand Down Expand Up @@ -508,6 +596,15 @@ impl Render for Editor {
.on_action(cx.listener(|editor, action: &Redo, window, cx| {
editor.redo(action, window, cx);
}))
.on_action(cx.listener(|editor, action: &Cut, window, cx| {
editor.cut(action, window, cx);
}))
.on_action(cx.listener(|editor, action: &Copy, window, cx| {
editor.copy(action, window, cx);
}))
.on_action(cx.listener(|editor, action: &Paste, window, cx| {
editor.paste(action, window, cx);
}))
.child(EditorElement::new(cx.entity().clone()))
}
}
Expand Down
176 changes: 174 additions & 2 deletions crates/editor/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ mod context;

pub use context::EditorTestContext;

use gpui::TestAppContext;
use gpui::{ClipboardItem, TestAppContext};
use indoc::indoc;

use crate::{Redo, Undo};
use crate::{Copy, Cut, Paste, Redo, Undo};
use buffer::{Buffer, FormatSpan, Selection};

/// Returns the effective formatting at an offset.
Expand Down Expand Up @@ -702,3 +702,175 @@ fn test_undo_redo_multiple(cx: &mut TestAppContext) {
jumps over the lazy dog
"});
}

#[gpui::test]
fn test_cut(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.set_state(indoc! {"
The «quickˇ» brown fox
jumps over the lazy dog
"});
cx.update_editor(|editor, window, cx| {
editor.cut(&Cut, window, cx);
});

let clipboard_text = cx
.read_from_clipboard()
.and_then(|item| item.text())
.unwrap();
assert_eq!(clipboard_text, "quick");
cx.assert_editor_state(indoc! {"
The ˇ brown fox
jumps over the lazy dog
"});
}

#[gpui::test]
fn test_copy(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.set_state(indoc! {"
The «quickˇ» brown fox
jumps over the lazy dog
"});
cx.update_editor(|editor, window, cx| {
editor.copy(&Copy, window, cx);
});

let clipboard_text = cx
.read_from_clipboard()
.and_then(|item| item.text())
.unwrap();
assert_eq!(clipboard_text, "quick");
cx.assert_editor_state(indoc! {"
The «quickˇ» brown fox
jumps over the lazy dog
"});
}

#[gpui::test]
fn test_copy_line(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.set_state("The quick ˇbrown fox\njumps over the lazy dog");
cx.update_editor(|editor, window, cx| {
editor.copy(&Copy, window, cx);
});
let clipboard_text = cx
.read_from_clipboard()
.and_then(|item| item.text())
.unwrap();
assert_eq!(clipboard_text, "The quick brown fox\n");

// Last line
cx.update_editor(|editor, window, cx| {
editor.change_selections(window, cx, |s| {
*s = Selection::cursor(35);
});
});
cx.assert_editor_state("The quick brown fox\njumps over the ˇlazy dog");
cx.update_editor(|editor, window, cx| {
editor.copy(&Copy, window, cx);
});
let clipboard_text = cx
.read_from_clipboard()
.and_then(|item| item.text())
.unwrap();
assert_eq!(clipboard_text, "jumps over the lazy dog\n");
}

#[gpui::test]
fn test_paste(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.write_to_clipboard(ClipboardItem::new_string("brown ".to_string()));
cx.set_state("The quick ˇfox");
cx.update_editor(|editor, window, cx| {
editor.paste(&Paste, window, cx);
});

cx.assert_editor_state("The quick brown ˇfox");
}

#[gpui::test]
fn test_paste_replace_selection(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.write_to_clipboard(ClipboardItem::new_string("brown".to_string()));
cx.set_state("The quick «blueˇ» fox");
cx.update_editor(|editor, window, cx| {
editor.paste(&Paste, window, cx);
});

cx.assert_editor_state("The quick brownˇ fox");
}

#[gpui::test]
fn test_cut_and_paste(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.set_state(indoc! {"
The quick «brownˇ» fox
jumps over the lazy dog
"});
cx.update_editor(|editor, window, cx| {
editor.cut(&Cut, window, cx);
});
cx.assert_editor_state(indoc! {"
The quick ˇ fox
jumps over the lazy dog
"});

cx.update_editor(|editor, window, cx| {
editor.paste(&Paste, window, cx);
});
cx.assert_editor_state(indoc! {"
The quick brownˇ fox
jumps over the lazy dog
"});
}

#[gpui::test]
fn test_undo_redo_cut(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.set_state("The quick «brownˇ» fox");
cx.update_editor(|editor, window, cx| {
editor.cut(&Cut, window, cx);
});
cx.assert_editor_state("The quick ˇ fox");

cx.update_editor(|editor, window, cx| {
editor.undo(&Undo, window, cx);
});
cx.assert_editor_state("The quick «brownˇ» fox");

cx.update_editor(|editor, window, cx| {
editor.redo(&Redo, window, cx);
});
cx.assert_editor_state("The quick ˇ fox");
}

#[gpui::test]
fn test_undo_redo_paste(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx);

cx.write_to_clipboard(ClipboardItem::new_string("brown ".to_string()));

cx.set_state("The quick ˇfox");
cx.update_editor(|editor, window, cx| {
editor.paste(&Paste, window, cx);
});
cx.assert_editor_state("The quick brown ˇfox");

cx.update_editor(|editor, window, cx| {
editor.undo(&Undo, window, cx);
});
cx.assert_editor_state("The quick ˇfox");

cx.update_editor(|editor, window, cx| {
editor.redo(&Redo, window, cx);
});
cx.assert_editor_state("The quick brown ˇfox");
}
10 changes: 9 additions & 1 deletion crates/editor/src/tests/context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use gpui::{Context, Entity, TestAppContext, VisualTestContext, Window};
use gpui::{ClipboardItem, Context, Entity, TestAppContext, VisualTestContext, Window};
use std::{ops::Deref, time::Instant};

use buffer::Selection;
Expand Down Expand Up @@ -91,4 +91,12 @@ impl EditorTestContext {
self.editor
.update_in(&mut self.cx, |editor, window, cx| f(editor, window, cx))
}

pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.cx.read_from_clipboard()
}

pub fn write_to_clipboard(&mut self, item: ClipboardItem) {
self.cx.write_to_clipboard(item);
}
}
8 changes: 6 additions & 2 deletions crates/ryuk/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use gpui::{
};

use editor::{
Backspace, Delete, DeleteToBeginningOfLine, DeleteToEndOfLine, MoveDown, MoveLeft, MoveRight,
MoveUp, Newline, Redo, ToggleBold, ToggleItalic, ToggleUnderline, Undo,
Backspace, Copy, Cut, Delete, DeleteToBeginningOfLine, DeleteToEndOfLine, MoveDown, MoveLeft,
MoveRight, MoveUp, Newline, Paste, Redo, ToggleBold, ToggleItalic, ToggleUnderline, Undo,
};
use workspace::Workspace;

Expand Down Expand Up @@ -35,6 +35,10 @@ fn main() {
KeyBinding::new("ctrl-k", DeleteToEndOfLine, None),
KeyBinding::new("cmd-z", Undo, None),
KeyBinding::new("cmd-shift-z", Redo, None),
// Clipboard
KeyBinding::new("cmd-x", Cut, None),
KeyBinding::new("cmd-c", Copy, None),
KeyBinding::new("cmd-v", Paste, None),
// Format
KeyBinding::new("cmd-b", ToggleBold, None),
KeyBinding::new("cmd-i", ToggleItalic, None),
Expand Down