Skip to content

Commit 446fb1d

Browse files
authored
Implement vi-mode "Yank" (copy): (#868)
- Vi Command; - Editor Command; - Editor methods; - Mode changes back to Normal after yank
1 parent 6cbdaa4 commit 446fb1d

File tree

5 files changed

+439
-2
lines changed

5 files changed

+439
-2
lines changed

src/core_editor/editor.rs

+261
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,57 @@ impl Editor {
120120
EditCommand::CutSelection => self.cut_selection_to_cut_buffer(),
121121
EditCommand::CopySelection => self.copy_selection_to_cut_buffer(),
122122
EditCommand::Paste => self.paste_cut_buffer(),
123+
EditCommand::CopyFromStart => self.copy_from_start(),
124+
EditCommand::CopyFromLineStart => self.copy_from_line_start(),
125+
EditCommand::CopyToEnd => self.copy_from_end(),
126+
EditCommand::CopyToLineEnd => self.copy_to_line_end(),
127+
EditCommand::CopyWordLeft => self.copy_word_left(),
128+
EditCommand::CopyBigWordLeft => self.copy_big_word_left(),
129+
EditCommand::CopyWordRight => self.copy_word_right(),
130+
EditCommand::CopyBigWordRight => self.copy_big_word_right(),
131+
EditCommand::CopyWordRightToNext => self.copy_word_right_to_next(),
132+
EditCommand::CopyBigWordRightToNext => self.copy_big_word_right_to_next(),
133+
EditCommand::CopyRightUntil(c) => self.copy_right_until_char(*c, false, true),
134+
EditCommand::CopyRightBefore(c) => self.copy_right_until_char(*c, true, true),
135+
EditCommand::CopyLeftUntil(c) => self.copy_left_until_char(*c, false, true),
136+
EditCommand::CopyLeftBefore(c) => self.copy_left_until_char(*c, true, true),
137+
EditCommand::CopyCurrentLine => {
138+
let range = self.line_buffer.current_line_range();
139+
let copy_slice = &self.line_buffer.get_buffer()[range];
140+
if !copy_slice.is_empty() {
141+
self.cut_buffer.set(copy_slice, ClipboardMode::Lines);
142+
}
143+
}
144+
EditCommand::CopyLeft => {
145+
let insertion_offset = self.line_buffer.insertion_point();
146+
if insertion_offset > 0 {
147+
let left_index = self.line_buffer.grapheme_left_index();
148+
let copy_range = left_index..insertion_offset;
149+
self.cut_buffer.set(
150+
&self.line_buffer.get_buffer()[copy_range],
151+
ClipboardMode::Normal,
152+
);
153+
}
154+
}
155+
EditCommand::CopyRight => {
156+
let insertion_offset = self.line_buffer.insertion_point();
157+
let right_index = self.line_buffer.grapheme_right_index();
158+
if right_index > insertion_offset {
159+
let copy_range = insertion_offset..right_index;
160+
self.cut_buffer.set(
161+
&self.line_buffer.get_buffer()[copy_range],
162+
ClipboardMode::Normal,
163+
);
164+
}
165+
}
123166
#[cfg(feature = "system_clipboard")]
124167
EditCommand::CutSelectionSystem => self.cut_selection_to_system(),
125168
#[cfg(feature = "system_clipboard")]
126169
EditCommand::CopySelectionSystem => self.copy_selection_to_system(),
127170
#[cfg(feature = "system_clipboard")]
128171
EditCommand::PasteSystem => self.paste_from_system(),
129172
EditCommand::CutInside { left, right } => self.cut_inside(*left, *right),
173+
EditCommand::YankInside { left, right } => self.yank_inside(*left, *right),
130174
}
131175
if !matches!(command.edit_type(), EditType::MoveCursor { select: true }) {
132176
self.selection_anchor = None;
@@ -687,6 +731,165 @@ impl Editor {
687731
}
688732
}
689733
}
734+
735+
pub(crate) fn copy_from_start(&mut self) {
736+
let insertion_offset = self.line_buffer.insertion_point();
737+
if insertion_offset > 0 {
738+
self.cut_buffer.set(
739+
&self.line_buffer.get_buffer()[..insertion_offset],
740+
ClipboardMode::Normal,
741+
);
742+
}
743+
}
744+
745+
pub(crate) fn copy_from_line_start(&mut self) {
746+
let previous_offset = self.line_buffer.insertion_point();
747+
let start_offset = {
748+
let temp_pos = self.line_buffer.insertion_point();
749+
self.line_buffer.move_to_line_start();
750+
let start = self.line_buffer.insertion_point();
751+
self.line_buffer.set_insertion_point(temp_pos);
752+
start
753+
};
754+
let copy_range = start_offset..previous_offset;
755+
let copy_slice = &self.line_buffer.get_buffer()[copy_range];
756+
if !copy_slice.is_empty() {
757+
self.cut_buffer.set(copy_slice, ClipboardMode::Normal);
758+
}
759+
}
760+
761+
pub(crate) fn copy_from_end(&mut self) {
762+
let copy_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..];
763+
if !copy_slice.is_empty() {
764+
self.cut_buffer.set(copy_slice, ClipboardMode::Normal);
765+
}
766+
}
767+
768+
pub(crate) fn copy_to_line_end(&mut self) {
769+
let copy_slice = &self.line_buffer.get_buffer()
770+
[self.line_buffer.insertion_point()..self.line_buffer.find_current_line_end()];
771+
if !copy_slice.is_empty() {
772+
self.cut_buffer.set(copy_slice, ClipboardMode::Normal);
773+
}
774+
}
775+
776+
pub(crate) fn copy_word_left(&mut self) {
777+
let insertion_offset = self.line_buffer.insertion_point();
778+
let left_index = self.line_buffer.word_left_index();
779+
if left_index < insertion_offset {
780+
let copy_range = left_index..insertion_offset;
781+
self.cut_buffer.set(
782+
&self.line_buffer.get_buffer()[copy_range],
783+
ClipboardMode::Normal,
784+
);
785+
}
786+
}
787+
788+
pub(crate) fn copy_big_word_left(&mut self) {
789+
let insertion_offset = self.line_buffer.insertion_point();
790+
let left_index = self.line_buffer.big_word_left_index();
791+
if left_index < insertion_offset {
792+
let copy_range = left_index..insertion_offset;
793+
self.cut_buffer.set(
794+
&self.line_buffer.get_buffer()[copy_range],
795+
ClipboardMode::Normal,
796+
);
797+
}
798+
}
799+
800+
pub(crate) fn copy_word_right(&mut self) {
801+
let insertion_offset = self.line_buffer.insertion_point();
802+
let right_index = self.line_buffer.word_right_index();
803+
if right_index > insertion_offset {
804+
let copy_range = insertion_offset..right_index;
805+
self.cut_buffer.set(
806+
&self.line_buffer.get_buffer()[copy_range],
807+
ClipboardMode::Normal,
808+
);
809+
}
810+
}
811+
812+
pub(crate) fn copy_big_word_right(&mut self) {
813+
let insertion_offset = self.line_buffer.insertion_point();
814+
let right_index = self.line_buffer.next_whitespace();
815+
if right_index > insertion_offset {
816+
let copy_range = insertion_offset..right_index;
817+
self.cut_buffer.set(
818+
&self.line_buffer.get_buffer()[copy_range],
819+
ClipboardMode::Normal,
820+
);
821+
}
822+
}
823+
824+
pub(crate) fn copy_word_right_to_next(&mut self) {
825+
let insertion_offset = self.line_buffer.insertion_point();
826+
let right_index = self.line_buffer.word_right_start_index();
827+
if right_index > insertion_offset {
828+
let copy_range = insertion_offset..right_index;
829+
self.cut_buffer.set(
830+
&self.line_buffer.get_buffer()[copy_range],
831+
ClipboardMode::Normal,
832+
);
833+
}
834+
}
835+
836+
pub(crate) fn copy_big_word_right_to_next(&mut self) {
837+
let insertion_offset = self.line_buffer.insertion_point();
838+
let right_index = self.line_buffer.big_word_right_start_index();
839+
if right_index > insertion_offset {
840+
let copy_range = insertion_offset..right_index;
841+
self.cut_buffer.set(
842+
&self.line_buffer.get_buffer()[copy_range],
843+
ClipboardMode::Normal,
844+
);
845+
}
846+
}
847+
848+
pub(crate) fn copy_right_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
849+
if let Some(index) = self.line_buffer.find_char_right(c, current_line) {
850+
let extra = if before_char { 0 } else { c.len_utf8() };
851+
let copy_slice =
852+
&self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..index + extra];
853+
if !copy_slice.is_empty() {
854+
self.cut_buffer.set(copy_slice, ClipboardMode::Normal);
855+
}
856+
}
857+
}
858+
859+
pub(crate) fn copy_left_until_char(&mut self, c: char, before_char: bool, current_line: bool) {
860+
if let Some(index) = self.line_buffer.find_char_left(c, current_line) {
861+
let extra = if before_char { c.len_utf8() } else { 0 };
862+
let copy_slice =
863+
&self.line_buffer.get_buffer()[index + extra..self.line_buffer.insertion_point()];
864+
if !copy_slice.is_empty() {
865+
self.cut_buffer.set(copy_slice, ClipboardMode::Normal);
866+
}
867+
}
868+
}
869+
870+
/// Yank text strictly between matching `left_char` and `right_char`.
871+
/// Copies it into the cut buffer without removing anything.
872+
/// Leaves the buffer unchanged and restores the original cursor.
873+
pub(crate) fn yank_inside(&mut self, left_char: char, right_char: char) {
874+
let old_pos = self.insertion_point();
875+
let buffer_len = self.line_buffer.len();
876+
877+
if let Some((lp, rp)) =
878+
self.line_buffer
879+
.find_matching_pair(left_char, right_char, self.insertion_point())
880+
{
881+
let inside_start = lp + left_char.len_utf8();
882+
if inside_start < rp && rp <= buffer_len {
883+
let inside_slice = &self.line_buffer.get_buffer()[inside_start..rp];
884+
if !inside_slice.is_empty() {
885+
self.cut_buffer.set(inside_slice, ClipboardMode::Normal);
886+
}
887+
}
888+
}
889+
890+
// Always restore the cursor position
891+
self.line_buffer.set_insertion_point(old_pos);
892+
}
690893
}
691894

692895
fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) {
@@ -1004,4 +1207,62 @@ mod test {
10041207
assert_eq!(editor.insertion_point(), 4);
10051208
assert_eq!(editor.cut_buffer.get().0, "bar()qux");
10061209
}
1210+
1211+
#[test]
1212+
fn test_yank_inside_brackets() {
1213+
let mut editor = editor_with("foo(bar)baz");
1214+
editor.move_to_position(5, false); // Move inside brackets
1215+
editor.yank_inside('(', ')');
1216+
assert_eq!(editor.get_buffer(), "foo(bar)baz"); // Buffer shouldn't change
1217+
assert_eq!(editor.insertion_point(), 5); // Cursor should return to original position
1218+
1219+
// Test yanked content by pasting
1220+
editor.paste_cut_buffer();
1221+
assert_eq!(editor.get_buffer(), "foo(bbarar)baz");
1222+
1223+
// Test with cursor outside brackets
1224+
let mut editor = editor_with("foo(bar)baz");
1225+
editor.move_to_position(0, false);
1226+
editor.yank_inside('(', ')');
1227+
assert_eq!(editor.get_buffer(), "foo(bar)baz");
1228+
assert_eq!(editor.insertion_point(), 0);
1229+
}
1230+
1231+
#[test]
1232+
fn test_yank_inside_quotes() {
1233+
let mut editor = editor_with("foo\"bar\"baz");
1234+
editor.move_to_position(5, false); // Move inside quotes
1235+
editor.yank_inside('"', '"');
1236+
assert_eq!(editor.get_buffer(), "foo\"bar\"baz"); // Buffer shouldn't change
1237+
assert_eq!(editor.insertion_point(), 5); // Cursor should return to original position
1238+
assert_eq!(editor.cut_buffer.get().0, "bar");
1239+
1240+
// Test with no matching quotes
1241+
let mut editor = editor_with("foo bar baz");
1242+
editor.move_to_position(4, false);
1243+
editor.yank_inside('"', '"');
1244+
assert_eq!(editor.get_buffer(), "foo bar baz");
1245+
assert_eq!(editor.insertion_point(), 4);
1246+
assert_eq!(editor.cut_buffer.get().0, "");
1247+
}
1248+
1249+
#[test]
1250+
fn test_yank_inside_nested() {
1251+
let mut editor = editor_with("foo(bar(baz)qux)quux");
1252+
editor.move_to_position(8, false); // Move inside inner brackets
1253+
editor.yank_inside('(', ')');
1254+
assert_eq!(editor.get_buffer(), "foo(bar(baz)qux)quux"); // Buffer shouldn't change
1255+
assert_eq!(editor.insertion_point(), 8);
1256+
assert_eq!(editor.cut_buffer.get().0, "baz");
1257+
1258+
// Test yanked content by pasting
1259+
editor.paste_cut_buffer();
1260+
assert_eq!(editor.get_buffer(), "foo(bar(bazbaz)qux)quux");
1261+
1262+
editor.move_to_position(4, false); // Move inside outer brackets
1263+
editor.yank_inside('(', ')');
1264+
assert_eq!(editor.get_buffer(), "foo(bar(bazbaz)qux)quux");
1265+
assert_eq!(editor.insertion_point(), 4);
1266+
assert_eq!(editor.cut_buffer.get().0, "bar(bazbaz)qux");
1267+
}
10071268
}

0 commit comments

Comments
 (0)