1use rust_i18n::t;
9
10use crate::input::multi_cursor::{
11 add_cursor_above, add_cursor_at_next_match, add_cursor_below, line_end_positions_in_selection,
12 AddCursorResult,
13};
14use crate::model::buffer_position::byte_to_2d;
15use crate::model::cursor::Cursor;
16use crate::model::event::{CursorId, Event};
17use crate::primitives::word_navigation::{
18 find_vi_word_end, find_word_start_left, find_word_start_right,
19};
20
21use super::Editor;
22
23impl Editor {
35 pub fn copy_selection(&mut self) {
40 let has_block_selection = self
42 .active_cursors()
43 .iter()
44 .any(|(_, cursor)| cursor.has_block_selection());
45
46 if has_block_selection {
47 let text = self.copy_block_selection_text();
49 if !text.is_empty() {
50 self.clipboard.copy(text);
51 self.active_window_mut().status_message = Some(t!("clipboard.copied").to_string());
52 }
53 return;
54 }
55
56 let has_selection = self
58 .active_cursors()
59 .iter()
60 .any(|(_, cursor)| cursor.selection_range().is_some());
61
62 if has_selection {
63 let ranges: Vec<_> = self
65 .active_cursors()
66 .iter()
67 .filter_map(|(_, cursor)| cursor.selection_range())
68 .collect();
69
70 let mut text = String::new();
71 let state = self.active_state_mut();
72 for range in ranges {
73 if !text.is_empty() {
74 text.push('\n');
75 }
76 let range_text = state.get_text_range(range.start, range.end);
77 text.push_str(&range_text);
78 }
79
80 if !text.is_empty() {
81 self.clipboard.copy(text);
82 self.active_window_mut().status_message = Some(t!("clipboard.copied").to_string());
83 }
84 } else {
85 let estimated_line_length = 80;
87 let mut text = String::new();
88
89 let positions: Vec<_> = self
91 .active_cursors()
92 .iter()
93 .map(|(_, c)| c.position)
94 .collect();
95 let state = self.active_state_mut();
96
97 for pos in positions {
98 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
99 if let Some((_start, content)) = iter.next_line() {
100 if !text.is_empty() {
101 text.push('\n');
102 }
103 text.push_str(&content);
104 }
105 }
106
107 if !text.is_empty() {
108 self.clipboard.copy(text);
109 self.active_window_mut().status_message =
110 Some(t!("clipboard.copied_line").to_string());
111 }
112 }
113 }
114
115 fn copy_block_selection_text(&mut self) -> String {
124 let estimated_line_length = 120;
125
126 let block_infos: Vec<_> = self
128 .active_cursors()
129 .iter()
130 .filter_map(|(_, cursor)| {
131 if !cursor.has_block_selection() {
132 return None;
133 }
134 let block_anchor = cursor.block_anchor?;
135 let anchor_byte = cursor.anchor?; let cursor_byte = cursor.position;
137 Some((block_anchor, anchor_byte, cursor_byte))
138 })
139 .collect();
140
141 let mut result = String::new();
142
143 for (block_anchor, anchor_byte, cursor_byte) in block_infos {
144 let cursor_2d = {
146 let state = self.active_state();
147 byte_to_2d(&state.buffer, cursor_byte)
148 };
149
150 let min_col = block_anchor.column.min(cursor_2d.column);
152 let max_col = block_anchor.column.max(cursor_2d.column);
153
154 let start_byte = anchor_byte.min(cursor_byte);
156 let end_byte = anchor_byte.max(cursor_byte);
157
158 let state = self.active_state_mut();
160 let mut iter = state
161 .buffer
162 .line_iterator(start_byte, estimated_line_length);
163
164 let mut lines_text = Vec::new();
166 loop {
167 let line_start = iter.current_position();
168
169 if line_start > end_byte {
171 break;
172 }
173
174 if let Some((_offset, line_content)) = iter.next_line() {
175 let content_without_newline = line_content.trim_end_matches(&['\n', '\r'][..]);
178 let chars: Vec<char> = content_without_newline.chars().collect();
179
180 let extracted: String = chars
182 .iter()
183 .skip(min_col)
184 .take(max_col.saturating_sub(min_col))
185 .collect();
186
187 lines_text.push(extracted);
188
189 if line_start + line_content.len() > end_byte {
191 break;
192 }
193 } else {
194 break;
195 }
196 }
197
198 if !result.is_empty() && !lines_text.is_empty() {
200 result.push('\n');
201 }
202 result.push_str(&lines_text.join("\n"));
203 }
204
205 result
206 }
207
208 pub fn copy_selection_with_theme(&mut self, theme_name: &str) {
213 let has_selection = self
215 .active_cursors()
216 .iter()
217 .any(|(_, cursor)| cursor.selection_range().is_some());
218
219 if !has_selection {
220 self.active_window_mut().status_message =
221 Some(t!("clipboard.no_selection").to_string());
222 return;
223 }
224
225 if theme_name.is_empty() {
227 self.start_copy_with_formatting_prompt();
228 return;
229 }
230 use crate::services::styled_html::render_styled_html;
231
232 let theme = match self.theme_registry.get_cloned(theme_name) {
234 Some(t) => t,
235 None => {
236 self.active_window_mut().status_message =
237 Some(format!("Theme '{}' not found", theme_name));
238 return;
239 }
240 };
241
242 let ranges: Vec<_> = self
244 .active_cursors()
245 .iter()
246 .filter_map(|(_, cursor)| cursor.selection_range())
247 .collect();
248
249 if ranges.is_empty() {
250 self.active_window_mut().status_message =
251 Some(t!("clipboard.no_selection").to_string());
252 return;
253 }
254
255 let min_offset = ranges.iter().map(|r| r.start).min().unwrap_or(0);
257 let max_offset = ranges.iter().map(|r| r.end).max().unwrap_or(0);
258
259 let (text, highlight_spans) = {
261 let state = self.active_state_mut();
262
263 let mut text = String::new();
265 for range in &ranges {
266 if !text.is_empty() {
267 text.push('\n');
268 }
269 let range_text = state.get_text_range(range.start, range.end);
270 text.push_str(&range_text);
271 }
272
273 if text.is_empty() {
274 (text, Vec::new())
275 } else {
276 let highlight_spans = state.highlighter.highlight_viewport(
278 &state.buffer,
279 min_offset,
280 max_offset,
281 &theme,
282 0, );
284 (text, highlight_spans)
285 }
286 };
287
288 if text.is_empty() {
289 self.active_window_mut().status_message = Some(t!("clipboard.no_text").to_string());
290 return;
291 }
292
293 let adjusted_spans: Vec<_> = if ranges.len() == 1 {
295 let base_offset = ranges[0].start;
296 highlight_spans
297 .into_iter()
298 .filter_map(|span| {
299 if span.range.end <= base_offset || span.range.start >= ranges[0].end {
300 return None;
301 }
302 let start = span.range.start.saturating_sub(base_offset);
303 let end = (span.range.end - base_offset).min(text.len());
304 if start < end {
305 Some(crate::primitives::highlighter::HighlightSpan {
306 range: start..end,
307 color: span.color,
308 bg: None,
309 category: span.category,
310 })
311 } else {
312 None
313 }
314 })
315 .collect()
316 } else {
317 Vec::new()
318 };
319
320 let html = render_styled_html(&text, &adjusted_spans, &theme);
322
323 if self.clipboard.copy_html(&html, &text) {
325 self.active_window_mut().status_message =
326 Some(t!("clipboard.copied_with_theme", theme = theme_name).to_string());
327 } else {
328 self.clipboard.copy(text);
329 self.active_window_mut().status_message =
330 Some(t!("clipboard.copied_plain").to_string());
331 }
332 }
333
334 fn start_copy_with_formatting_prompt(&mut self) {
336 use crate::view::prompt::PromptType;
337
338 let available_themes = self.theme_registry.list();
339 let resolved_current = self
342 .theme_registry
343 .resolve_key(&self.config.theme.0)
344 .unwrap_or_else(|| self.config.theme.0.clone());
345 let current_theme_key = resolved_current.as_str();
346
347 let current_index = available_themes
349 .iter()
350 .position(|info| info.key == *current_theme_key)
351 .or_else(|| {
352 let normalized = crate::view::theme::normalize_theme_name(current_theme_key);
353 available_themes.iter().position(|info| {
354 crate::view::theme::normalize_theme_name(&info.name) == normalized
355 })
356 })
357 .unwrap_or(0);
358
359 let suggestions: Vec<crate::input::commands::Suggestion> = available_themes
360 .iter()
361 .map(|info| {
362 let is_current = Some(info) == available_themes.get(current_index);
363 let description = if is_current {
364 Some(format!("{} (current)", info.key))
365 } else {
366 Some(info.key.clone())
367 };
368 crate::input::commands::Suggestion {
369 text: info.name.clone(),
370 description,
371 value: Some(info.key.clone()),
372 disabled: false,
373 keybinding: None,
374 source: None,
375 }
376 })
377 .collect();
378
379 self.active_window_mut().prompt = Some(crate::view::prompt::Prompt::with_suggestions(
380 "Copy with theme: ".to_string(),
381 PromptType::CopyWithFormattingTheme,
382 suggestions,
383 ));
384
385 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
386 if !prompt.suggestions.is_empty() {
387 prompt.selected_suggestion = Some(current_index);
388 prompt.input = current_theme_key.to_string();
389 prompt.cursor_pos = prompt.input.len();
390 }
391 }
392 }
393
394 pub fn cut_selection(&mut self) {
398 let has_selection = self
400 .active_cursors()
401 .iter()
402 .any(|(_, cursor)| cursor.selection_range().is_some());
403
404 self.copy_selection();
406
407 if has_selection {
408 let mut deletions: Vec<_> = self
411 .active_cursors()
412 .iter()
413 .filter_map(|(_, c)| c.selection_range())
414 .collect();
415 deletions.sort_by_key(|r| r.start);
417
418 let primary_id = self.active_cursors().primary_id();
419 let state = self.active_state_mut();
420 let events: Vec<_> = deletions
421 .iter()
422 .rev()
423 .map(|range| {
424 let deleted_text = state.get_text_range(range.start, range.end);
425 Event::Delete {
426 range: range.clone(),
427 deleted_text,
428 cursor_id: primary_id,
429 }
430 })
431 .collect();
432
433 if events.len() > 1 {
435 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Cut".to_string()) {
437 self.active_event_log_mut().append(bulk_edit);
438 }
439 } else if let Some(event) = events.into_iter().next() {
440 self.log_and_apply_event(&event);
441 }
442
443 if !deletions.is_empty() {
444 self.active_window_mut().status_message = Some(t!("clipboard.cut").to_string());
445 }
446 } else {
447 let estimated_line_length = 80;
449
450 let positions: Vec<_> = self
453 .active_cursors()
454 .iter()
455 .map(|(_, c)| c.position)
456 .collect();
457 let mut deletions: Vec<_> = {
458 let state = self.active_state_mut();
459 positions
460 .into_iter()
461 .filter_map(|pos| {
462 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
463 let line_start = iter.current_position();
464 iter.next_line().map(|(_start, content)| {
465 let line_end = line_start + content.len();
466 line_start..line_end
467 })
468 })
469 .collect()
470 };
471 deletions.sort_by_key(|r| r.start);
473
474 let primary_id = self.active_cursors().primary_id();
475 let state = self.active_state_mut();
476 let events: Vec<_> = deletions
477 .iter()
478 .rev()
479 .map(|range| {
480 let deleted_text = state.get_text_range(range.start, range.end);
481 Event::Delete {
482 range: range.clone(),
483 deleted_text,
484 cursor_id: primary_id,
485 }
486 })
487 .collect();
488
489 if events.len() > 1 {
491 if let Some(bulk_edit) =
493 self.apply_events_as_bulk_edit(events, "Cut line".to_string())
494 {
495 self.active_event_log_mut().append(bulk_edit);
496 }
497 } else if let Some(event) = events.into_iter().next() {
498 self.log_and_apply_event(&event);
499 }
500
501 if !deletions.is_empty() {
502 self.active_window_mut().status_message =
503 Some(t!("clipboard.cut_line").to_string());
504 }
505 }
506 }
507
508 pub fn paste(&mut self) {
516 let text = match self.clipboard.paste() {
518 Some(text) => text,
519 None => return,
520 };
521
522 self.paste_text(text);
524 }
525
526 pub fn paste_text(&mut self, paste_text: String) {
540 if paste_text.is_empty() {
541 return;
542 }
543
544 let normalized = paste_text.replace("\r\n", "\n").replace('\r', "\n");
547
548 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
550 prompt.insert_str(&normalized);
551 self.update_prompt_suggestions();
552 self.active_window_mut().status_message = Some(t!("clipboard.pasted").to_string());
553 return;
554 }
555
556 if self.active_window().terminal_mode {
558 self.active_window_mut()
559 .send_terminal_input(normalized.as_bytes());
560 return;
561 }
562
563 let mut cursor_data: Vec<_> = self
565 .active_cursors()
566 .iter()
567 .map(|(cursor_id, cursor)| {
568 let selection = cursor.selection_range();
569 let insert_position = selection
570 .as_ref()
571 .map(|r| r.start)
572 .unwrap_or(cursor.position);
573 (cursor_id, selection, insert_position)
574 })
575 .collect();
576 cursor_data.sort_by_key(|(_, _, pos)| std::cmp::Reverse(*pos));
577
578 let mut lines_for_distribution: Vec<&str> = normalized.split('\n').collect();
583 if lines_for_distribution.len() > 1 && lines_for_distribution.last() == Some(&"") {
584 lines_for_distribution.pop();
585 }
586 let use_column_paste = cursor_data.len() > 1
587 && lines_for_distribution.len() > 1
588 && lines_for_distribution.len() == cursor_data.len();
589
590 let paste_text_full = match self.active_state().buffer.line_ending() {
593 crate::model::buffer::LineEnding::LF => normalized.clone(),
594 crate::model::buffer::LineEnding::CRLF => normalized.replace('\n', "\r\n"),
595 crate::model::buffer::LineEnding::CR => normalized.replace('\n', "\r"),
596 };
597
598 let cursor_data_with_text: Vec<_> = {
600 let state = self.active_state_mut();
601 cursor_data
602 .into_iter()
603 .map(|(cursor_id, selection, insert_position)| {
604 let deleted_text = selection
605 .as_ref()
606 .map(|r| state.get_text_range(r.start, r.end));
607 (cursor_id, selection, insert_position, deleted_text)
608 })
609 .collect()
610 };
611
612 let total = cursor_data_with_text.len();
620 let mut events = Vec::new();
621 for (i, (cursor_id, selection, insert_position, deleted_text)) in
622 cursor_data_with_text.into_iter().enumerate()
623 {
624 if let (Some(range), Some(text)) = (selection, deleted_text) {
625 events.push(Event::Delete {
626 range,
627 deleted_text: text,
628 cursor_id,
629 });
630 }
631 let text = if use_column_paste {
632 lines_for_distribution[total - 1 - i].to_string()
633 } else {
634 paste_text_full.clone()
635 };
636 events.push(Event::Insert {
637 position: insert_position,
638 text,
639 cursor_id,
640 });
641 }
642
643 if events.len() > 1 {
645 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Paste".to_string()) {
647 self.active_event_log_mut().append(bulk_edit);
648 }
649 } else if let Some(event) = events.into_iter().next() {
650 self.log_and_apply_event(&event);
651 }
652
653 self.active_window_mut().status_message = Some(t!("clipboard.pasted").to_string());
654 }
655
656 #[doc(hidden)]
660 pub fn set_clipboard_for_test(&mut self, text: String) {
661 self.clipboard.set_internal(text);
662 self.clipboard.set_internal_only(true);
663 }
664
665 #[doc(hidden)]
668 pub fn paste_for_test(&mut self) {
669 let paste_text = match self.clipboard.paste_internal() {
671 Some(text) => text,
672 None => return,
673 };
674
675 self.paste_text(paste_text);
677 }
678
679 #[doc(hidden)]
682 pub fn clipboard_content_for_test(&self) -> String {
683 self.clipboard.get_internal().to_string()
684 }
685
686 pub fn copy_buffer_path(&mut self, buffer_id: crate::model::event::BufferId, relative: bool) {
697 let path = self
698 .buffers()
699 .get(&buffer_id)
700 .and_then(|state| state.buffer.file_path().map(|p| p.to_path_buf()));
701 let Some(path) = path else {
702 self.active_window_mut().status_message =
703 Some(t!("clipboard.no_file_path").to_string());
704 return;
705 };
706
707 let path_str = if relative {
708 path.strip_prefix(&self.working_dir)
709 .unwrap_or(&path)
710 .to_string_lossy()
711 .into_owned()
712 } else {
713 path.to_string_lossy().into_owned()
714 };
715
716 self.clipboard.copy(path_str.clone());
717 self.active_window_mut().status_message =
718 Some(t!("clipboard.copied_path", path = &path_str).to_string());
719 }
720
721 pub fn copy_active_buffer_path(&mut self, relative: bool) {
723 let buffer_id = self.active_buffer();
724 self.copy_buffer_path(buffer_id, relative);
725 }
726
727 pub fn add_cursor_at_next_match(&mut self) {
736 if let Some(range) = self.active_window().search_match_at_primary_cursor() {
737 let primary_id = self.active_cursors().primary_id();
738 let primary = self.active_cursors().primary();
739 let event = Event::MoveCursor {
740 cursor_id: primary_id,
741 old_position: primary.position,
742 new_position: range.end,
743 old_anchor: primary.anchor,
744 new_anchor: Some(range.start),
745 old_sticky_column: primary.sticky_column,
746 new_sticky_column: 0,
747 };
748 self.active_event_log_mut().append(event.clone());
749 self.apply_event_to_active_buffer(&event);
750 return;
751 }
752
753 let cursors = self.active_cursors().clone();
754 let state = self.active_state_mut();
755 match add_cursor_at_next_match(state, &cursors) {
756 AddCursorResult::Success {
757 cursor,
758 total_cursors,
759 } => {
760 let next_id = CursorId(self.active_cursors().count());
762 let event = Event::AddCursor {
763 cursor_id: next_id,
764 position: cursor.position,
765 anchor: cursor.anchor,
766 };
767
768 self.active_event_log_mut().append(event.clone());
770 self.apply_event_to_active_buffer(&event);
771
772 self.active_window_mut().status_message =
773 Some(t!("clipboard.added_cursor_match", count = total_cursors).to_string());
774 }
775 AddCursorResult::WordSelected {
776 word_start,
777 word_end,
778 } => {
779 let primary_id = self.active_cursors().primary_id();
781 let primary = self.active_cursors().primary();
782 let event = Event::MoveCursor {
783 cursor_id: primary_id,
784 old_position: primary.position,
785 new_position: word_end,
786 old_anchor: primary.anchor,
787 new_anchor: Some(word_start),
788 old_sticky_column: primary.sticky_column,
789 new_sticky_column: 0,
790 };
791
792 self.active_event_log_mut().append(event.clone());
794 self.apply_event_to_active_buffer(&event);
795 }
796 AddCursorResult::Failed { message } => {
797 self.active_window_mut().status_message = Some(message);
798 }
799 }
800 }
801
802 pub fn add_cursor_above(&mut self) {
804 let cursors = self.active_cursors().clone();
805 let state = self.active_state_mut();
806 match add_cursor_above(state, &cursors) {
807 AddCursorResult::Success {
808 cursor,
809 total_cursors,
810 } => {
811 let next_id = CursorId(self.active_cursors().count());
813 let event = Event::AddCursor {
814 cursor_id: next_id,
815 position: cursor.position,
816 anchor: cursor.anchor,
817 };
818
819 self.active_event_log_mut().append(event.clone());
821 self.apply_event_to_active_buffer(&event);
822
823 self.active_window_mut().status_message =
824 Some(t!("clipboard.added_cursor_above", count = total_cursors).to_string());
825 }
826 AddCursorResult::Failed { message } => {
827 self.active_window_mut().status_message = Some(message);
828 }
829 AddCursorResult::WordSelected { .. } => unreachable!(),
830 }
831 }
832
833 pub fn add_cursor_below(&mut self) {
835 let cursors = self.active_cursors().clone();
836 let state = self.active_state_mut();
837 match add_cursor_below(state, &cursors) {
838 AddCursorResult::Success {
839 cursor,
840 total_cursors,
841 } => {
842 let next_id = CursorId(self.active_cursors().count());
844 let event = Event::AddCursor {
845 cursor_id: next_id,
846 position: cursor.position,
847 anchor: cursor.anchor,
848 };
849
850 self.active_event_log_mut().append(event.clone());
852 self.apply_event_to_active_buffer(&event);
853
854 self.active_window_mut().status_message =
855 Some(t!("clipboard.added_cursor_below", count = total_cursors).to_string());
856 }
857 AddCursorResult::Failed { message } => {
858 self.active_window_mut().status_message = Some(message);
859 }
860 AddCursorResult::WordSelected { .. } => unreachable!(),
861 }
862 }
863
864 pub fn add_cursors_to_line_ends(&mut self) {
871 let cursors = self.active_cursors().clone();
872 let state = self.active_state_mut();
873 let positions = line_end_positions_in_selection(state, &cursors);
874
875 if positions.is_empty() {
876 self.active_window_mut().status_message =
877 Some(t!("clipboard.added_cursors_to_line_ends_failed").to_string());
878 return;
879 }
880
881 let mut existing: Vec<(CursorId, Cursor)> =
886 cursors.iter().map(|(id, c)| (id, *c)).collect();
887 existing.sort_by_key(|(_, c)| c.position);
888
889 let mut events: Vec<Event> = Vec::new();
890 let reuse = existing.len().min(positions.len());
891
892 for i in 0..reuse {
893 let (cursor_id, cur) = existing[i];
894 let target = positions[i];
895 events.push(Event::MoveCursor {
896 cursor_id,
897 old_position: cur.position,
898 new_position: target,
899 old_anchor: cur.anchor,
900 new_anchor: None,
901 old_sticky_column: cur.sticky_column,
902 new_sticky_column: 0,
903 });
904 }
905
906 for &(cursor_id, cur) in existing.iter().skip(reuse) {
909 events.push(Event::RemoveCursor {
910 cursor_id,
911 position: cur.position,
912 anchor: cur.anchor,
913 });
914 }
915
916 let next_free_id = cursors
920 .iter()
921 .map(|(id, _)| id.0)
922 .max()
923 .map(|m| m + 1)
924 .unwrap_or(0);
925 for (i, &pos) in positions.iter().enumerate().skip(reuse) {
926 let new_id = CursorId(next_free_id + i - reuse);
927 events.push(Event::AddCursor {
928 cursor_id: new_id,
929 position: pos,
930 anchor: None,
931 });
932 }
933
934 let total = positions.len();
935 let batch = Event::Batch {
936 events,
937 description: "Add cursors to line ends".to_string(),
938 };
939 self.active_event_log_mut().append(batch.clone());
940 self.apply_event_to_active_buffer(&batch);
941
942 self.active_window_mut().status_message =
943 Some(t!("clipboard.added_cursors_to_line_ends", count = total).to_string());
944 }
945
946 pub fn yank_word_forward(&mut self) {
952 let cursor_positions: Vec<_> = self
953 .active_cursors()
954 .iter()
955 .map(|(_, c)| c.position)
956 .collect();
957 let ranges: Vec<_> = {
958 let state = self.active_state();
959 cursor_positions
960 .into_iter()
961 .filter_map(|start| {
962 let end = find_word_start_right(&state.buffer, start);
963 if end > start {
964 Some(start..end)
965 } else {
966 None
967 }
968 })
969 .collect()
970 };
971
972 if ranges.is_empty() {
973 return;
974 }
975
976 let mut text = String::new();
978 let state = self.active_state_mut();
979 for range in ranges {
980 if !text.is_empty() {
981 text.push('\n');
982 }
983 let range_text = state.get_text_range(range.start, range.end);
984 text.push_str(&range_text);
985 }
986
987 if !text.is_empty() {
988 let len = text.len();
989 self.clipboard.copy(text);
990 self.active_window_mut().status_message =
991 Some(t!("clipboard.yanked", count = len).to_string());
992 }
993 }
994
995 pub fn yank_vi_word_end(&mut self) {
997 let cursor_positions: Vec<_> = self
998 .active_cursors()
999 .iter()
1000 .map(|(_, c)| c.position)
1001 .collect();
1002 let ranges: Vec<_> = {
1003 let state = self.active_state();
1004 cursor_positions
1005 .into_iter()
1006 .filter_map(|start| {
1007 let word_end = find_vi_word_end(&state.buffer, start);
1008 let end = (word_end + 1).min(state.buffer.len());
1009 if end > start {
1010 Some(start..end)
1011 } else {
1012 None
1013 }
1014 })
1015 .collect()
1016 };
1017
1018 if ranges.is_empty() {
1019 return;
1020 }
1021
1022 let mut text = String::new();
1023 let state = self.active_state_mut();
1024 for range in ranges {
1025 if !text.is_empty() {
1026 text.push('\n');
1027 }
1028 let range_text = state.get_text_range(range.start, range.end);
1029 text.push_str(&range_text);
1030 }
1031
1032 if !text.is_empty() {
1033 let len = text.len();
1034 self.clipboard.copy(text);
1035 self.active_window_mut().status_message =
1036 Some(t!("clipboard.yanked", count = len).to_string());
1037 }
1038 }
1039
1040 pub fn yank_word_backward(&mut self) {
1042 let cursor_positions: Vec<_> = self
1043 .active_cursors()
1044 .iter()
1045 .map(|(_, c)| c.position)
1046 .collect();
1047 let ranges: Vec<_> = {
1048 let state = self.active_state();
1049 cursor_positions
1050 .into_iter()
1051 .filter_map(|end| {
1052 let start = find_word_start_left(&state.buffer, end);
1053 if start < end {
1054 Some(start..end)
1055 } else {
1056 None
1057 }
1058 })
1059 .collect()
1060 };
1061
1062 if ranges.is_empty() {
1063 return;
1064 }
1065
1066 let mut text = String::new();
1067 let state = self.active_state_mut();
1068 for range in ranges {
1069 if !text.is_empty() {
1070 text.push('\n');
1071 }
1072 let range_text = state.get_text_range(range.start, range.end);
1073 text.push_str(&range_text);
1074 }
1075
1076 if !text.is_empty() {
1077 let len = text.len();
1078 self.clipboard.copy(text);
1079 self.active_window_mut().status_message =
1080 Some(t!("clipboard.yanked", count = len).to_string());
1081 }
1082 }
1083
1084 pub fn yank_to_line_end(&mut self) {
1086 let estimated_line_length = 80;
1087
1088 let cursor_positions: Vec<_> = self
1090 .active_cursors()
1091 .iter()
1092 .map(|(_, cursor)| cursor.position)
1093 .collect();
1094
1095 let state = self.active_state_mut();
1097 let mut ranges = Vec::new();
1098 for pos in cursor_positions {
1099 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
1100 let line_start = iter.current_position();
1101 if let Some((_start, content)) = iter.next_line() {
1102 let content_len = content.trim_end_matches(&['\n', '\r'][..]).len();
1104 let line_end = line_start + content_len;
1105 if pos < line_end {
1106 ranges.push(pos..line_end);
1107 }
1108 }
1109 }
1110
1111 if ranges.is_empty() {
1112 return;
1113 }
1114
1115 let mut text = String::new();
1116 for range in ranges {
1117 if !text.is_empty() {
1118 text.push('\n');
1119 }
1120 let range_text = state.get_text_range(range.start, range.end);
1121 text.push_str(&range_text);
1122 }
1123
1124 if !text.is_empty() {
1125 let len = text.len();
1126 self.clipboard.copy(text);
1127 self.active_window_mut().status_message =
1128 Some(t!("clipboard.yanked", count = len).to_string());
1129 }
1130 }
1131
1132 pub fn yank_to_line_start(&mut self) {
1134 let estimated_line_length = 80;
1135
1136 let cursor_positions: Vec<_> = self
1138 .active_cursors()
1139 .iter()
1140 .map(|(_, cursor)| cursor.position)
1141 .collect();
1142
1143 let state = self.active_state_mut();
1145 let mut ranges = Vec::new();
1146 for pos in cursor_positions {
1147 let iter = state.buffer.line_iterator(pos, estimated_line_length);
1148 let line_start = iter.current_position();
1149 if pos > line_start {
1150 ranges.push(line_start..pos);
1151 }
1152 }
1153
1154 if ranges.is_empty() {
1155 return;
1156 }
1157
1158 let mut text = String::new();
1159 for range in ranges {
1160 if !text.is_empty() {
1161 text.push('\n');
1162 }
1163 let range_text = state.get_text_range(range.start, range.end);
1164 text.push_str(&range_text);
1165 }
1166
1167 if !text.is_empty() {
1168 let len = text.len();
1169 self.clipboard.copy(text);
1170 self.active_window_mut().status_message =
1171 Some(t!("clipboard.yanked", count = len).to_string());
1172 }
1173 }
1174}