1use rust_i18n::t;
9
10use crate::input::multi_cursor::{
11 add_cursor_above, add_cursor_at_next_match, add_cursor_below, AddCursorResult,
12};
13use crate::model::buffer_position::byte_to_2d;
14use crate::model::event::{CursorId, Event};
15use crate::primitives::word_navigation::{
16 find_vi_word_end, find_word_start_left, find_word_start_right,
17};
18
19use super::Editor;
20
21impl Editor {
33 pub fn copy_selection(&mut self) {
38 let has_block_selection = self
40 .active_cursors()
41 .iter()
42 .any(|(_, cursor)| cursor.has_block_selection());
43
44 if has_block_selection {
45 let text = self.copy_block_selection_text();
47 if !text.is_empty() {
48 self.clipboard.copy(text);
49 self.status_message = Some(t!("clipboard.copied").to_string());
50 }
51 return;
52 }
53
54 let has_selection = self
56 .active_cursors()
57 .iter()
58 .any(|(_, cursor)| cursor.selection_range().is_some());
59
60 if has_selection {
61 let ranges: Vec<_> = self
63 .active_cursors()
64 .iter()
65 .filter_map(|(_, cursor)| cursor.selection_range())
66 .collect();
67
68 let mut text = String::new();
69 let state = self.active_state_mut();
70 for range in ranges {
71 if !text.is_empty() {
72 text.push('\n');
73 }
74 let range_text = state.get_text_range(range.start, range.end);
75 text.push_str(&range_text);
76 }
77
78 if !text.is_empty() {
79 self.clipboard.copy(text);
80 self.status_message = Some(t!("clipboard.copied").to_string());
81 }
82 } else {
83 let estimated_line_length = 80;
85 let mut text = String::new();
86
87 let positions: Vec<_> = self
89 .active_cursors()
90 .iter()
91 .map(|(_, c)| c.position)
92 .collect();
93 let state = self.active_state_mut();
94
95 for pos in positions {
96 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
97 if let Some((_start, content)) = iter.next_line() {
98 if !text.is_empty() {
99 text.push('\n');
100 }
101 text.push_str(&content);
102 }
103 }
104
105 if !text.is_empty() {
106 self.clipboard.copy(text);
107 self.status_message = Some(t!("clipboard.copied_line").to_string());
108 }
109 }
110 }
111
112 fn copy_block_selection_text(&mut self) -> String {
121 let estimated_line_length = 120;
122
123 let block_infos: Vec<_> = self
125 .active_cursors()
126 .iter()
127 .filter_map(|(_, cursor)| {
128 if !cursor.has_block_selection() {
129 return None;
130 }
131 let block_anchor = cursor.block_anchor?;
132 let anchor_byte = cursor.anchor?; let cursor_byte = cursor.position;
134 Some((block_anchor, anchor_byte, cursor_byte))
135 })
136 .collect();
137
138 let mut result = String::new();
139
140 for (block_anchor, anchor_byte, cursor_byte) in block_infos {
141 let cursor_2d = {
143 let state = self.active_state();
144 byte_to_2d(&state.buffer, cursor_byte)
145 };
146
147 let min_col = block_anchor.column.min(cursor_2d.column);
149 let max_col = block_anchor.column.max(cursor_2d.column);
150
151 let start_byte = anchor_byte.min(cursor_byte);
153 let end_byte = anchor_byte.max(cursor_byte);
154
155 let state = self.active_state_mut();
157 let mut iter = state
158 .buffer
159 .line_iterator(start_byte, estimated_line_length);
160
161 let mut lines_text = Vec::new();
163 loop {
164 let line_start = iter.current_position();
165
166 if line_start > end_byte {
168 break;
169 }
170
171 if let Some((_offset, line_content)) = iter.next_line() {
172 let content_without_newline = line_content.trim_end_matches(&['\n', '\r'][..]);
175 let chars: Vec<char> = content_without_newline.chars().collect();
176
177 let extracted: String = chars
179 .iter()
180 .skip(min_col)
181 .take(max_col.saturating_sub(min_col))
182 .collect();
183
184 lines_text.push(extracted);
185
186 if line_start + line_content.len() > end_byte {
188 break;
189 }
190 } else {
191 break;
192 }
193 }
194
195 if !result.is_empty() && !lines_text.is_empty() {
197 result.push('\n');
198 }
199 result.push_str(&lines_text.join("\n"));
200 }
201
202 result
203 }
204
205 pub fn copy_selection_with_theme(&mut self, theme_name: &str) {
210 let has_selection = self
212 .active_cursors()
213 .iter()
214 .any(|(_, cursor)| cursor.selection_range().is_some());
215
216 if !has_selection {
217 self.status_message = Some(t!("clipboard.no_selection").to_string());
218 return;
219 }
220
221 if theme_name.is_empty() {
223 self.start_copy_with_formatting_prompt();
224 return;
225 }
226 use crate::services::styled_html::render_styled_html;
227
228 let theme = match self.theme_registry.get_cloned(theme_name) {
230 Some(t) => t,
231 None => {
232 self.status_message = Some(format!("Theme '{}' not found", theme_name));
233 return;
234 }
235 };
236
237 let ranges: Vec<_> = self
239 .active_cursors()
240 .iter()
241 .filter_map(|(_, cursor)| cursor.selection_range())
242 .collect();
243
244 if ranges.is_empty() {
245 self.status_message = Some(t!("clipboard.no_selection").to_string());
246 return;
247 }
248
249 let min_offset = ranges.iter().map(|r| r.start).min().unwrap_or(0);
251 let max_offset = ranges.iter().map(|r| r.end).max().unwrap_or(0);
252
253 let (text, highlight_spans) = {
255 let state = self.active_state_mut();
256
257 let mut text = String::new();
259 for range in &ranges {
260 if !text.is_empty() {
261 text.push('\n');
262 }
263 let range_text = state.get_text_range(range.start, range.end);
264 text.push_str(&range_text);
265 }
266
267 if text.is_empty() {
268 (text, Vec::new())
269 } else {
270 let highlight_spans = state.highlighter.highlight_viewport(
272 &state.buffer,
273 min_offset,
274 max_offset,
275 &theme,
276 0, );
278 (text, highlight_spans)
279 }
280 };
281
282 if text.is_empty() {
283 self.status_message = Some(t!("clipboard.no_text").to_string());
284 return;
285 }
286
287 let adjusted_spans: Vec<_> = if ranges.len() == 1 {
289 let base_offset = ranges[0].start;
290 highlight_spans
291 .into_iter()
292 .filter_map(|span| {
293 if span.range.end <= base_offset || span.range.start >= ranges[0].end {
294 return None;
295 }
296 let start = span.range.start.saturating_sub(base_offset);
297 let end = (span.range.end - base_offset).min(text.len());
298 if start < end {
299 Some(crate::primitives::highlighter::HighlightSpan {
300 range: start..end,
301 color: span.color,
302 category: span.category,
303 })
304 } else {
305 None
306 }
307 })
308 .collect()
309 } else {
310 Vec::new()
311 };
312
313 let html = render_styled_html(&text, &adjusted_spans, &theme);
315
316 if self.clipboard.copy_html(&html, &text) {
318 self.status_message =
319 Some(t!("clipboard.copied_with_theme", theme = theme_name).to_string());
320 } else {
321 self.clipboard.copy(text);
322 self.status_message = Some(t!("clipboard.copied_plain").to_string());
323 }
324 }
325
326 fn start_copy_with_formatting_prompt(&mut self) {
328 use crate::view::prompt::PromptType;
329
330 let available_themes = self.theme_registry.list();
331 let resolved_current = self
334 .theme_registry
335 .resolve_key(&self.config.theme.0)
336 .unwrap_or_else(|| self.config.theme.0.clone());
337 let current_theme_key = resolved_current.as_str();
338
339 let current_index = available_themes
341 .iter()
342 .position(|info| info.key == *current_theme_key)
343 .or_else(|| {
344 let normalized = crate::view::theme::normalize_theme_name(current_theme_key);
345 available_themes.iter().position(|info| {
346 crate::view::theme::normalize_theme_name(&info.name) == normalized
347 })
348 })
349 .unwrap_or(0);
350
351 let suggestions: Vec<crate::input::commands::Suggestion> = available_themes
352 .iter()
353 .map(|info| {
354 let is_current = Some(info) == available_themes.get(current_index);
355 let description = if is_current {
356 Some(format!("{} (current)", info.key))
357 } else {
358 Some(info.key.clone())
359 };
360 crate::input::commands::Suggestion {
361 text: info.name.clone(),
362 description,
363 value: Some(info.key.clone()),
364 disabled: false,
365 keybinding: None,
366 source: None,
367 }
368 })
369 .collect();
370
371 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
372 "Copy with theme: ".to_string(),
373 PromptType::CopyWithFormattingTheme,
374 suggestions,
375 ));
376
377 if let Some(prompt) = self.prompt.as_mut() {
378 if !prompt.suggestions.is_empty() {
379 prompt.selected_suggestion = Some(current_index);
380 prompt.input = current_theme_key.to_string();
381 prompt.cursor_pos = prompt.input.len();
382 }
383 }
384 }
385
386 pub fn cut_selection(&mut self) {
390 let has_selection = self
392 .active_cursors()
393 .iter()
394 .any(|(_, cursor)| cursor.selection_range().is_some());
395
396 self.copy_selection();
398
399 if has_selection {
400 let mut deletions: Vec<_> = self
403 .active_cursors()
404 .iter()
405 .filter_map(|(_, c)| c.selection_range())
406 .collect();
407 deletions.sort_by_key(|r| r.start);
409
410 let primary_id = self.active_cursors().primary_id();
411 let state = self.active_state_mut();
412 let events: Vec<_> = deletions
413 .iter()
414 .rev()
415 .map(|range| {
416 let deleted_text = state.get_text_range(range.start, range.end);
417 Event::Delete {
418 range: range.clone(),
419 deleted_text,
420 cursor_id: primary_id,
421 }
422 })
423 .collect();
424
425 if events.len() > 1 {
427 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Cut".to_string()) {
429 self.active_event_log_mut().append(bulk_edit);
430 }
431 } else if let Some(event) = events.into_iter().next() {
432 self.log_and_apply_event(&event);
433 }
434
435 if !deletions.is_empty() {
436 self.status_message = Some(t!("clipboard.cut").to_string());
437 }
438 } else {
439 let estimated_line_length = 80;
441
442 let positions: Vec<_> = self
445 .active_cursors()
446 .iter()
447 .map(|(_, c)| c.position)
448 .collect();
449 let mut deletions: Vec<_> = {
450 let state = self.active_state_mut();
451 positions
452 .into_iter()
453 .filter_map(|pos| {
454 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
455 let line_start = iter.current_position();
456 iter.next_line().map(|(_start, content)| {
457 let line_end = line_start + content.len();
458 line_start..line_end
459 })
460 })
461 .collect()
462 };
463 deletions.sort_by_key(|r| r.start);
465
466 let primary_id = self.active_cursors().primary_id();
467 let state = self.active_state_mut();
468 let events: Vec<_> = deletions
469 .iter()
470 .rev()
471 .map(|range| {
472 let deleted_text = state.get_text_range(range.start, range.end);
473 Event::Delete {
474 range: range.clone(),
475 deleted_text,
476 cursor_id: primary_id,
477 }
478 })
479 .collect();
480
481 if events.len() > 1 {
483 if let Some(bulk_edit) =
485 self.apply_events_as_bulk_edit(events, "Cut line".to_string())
486 {
487 self.active_event_log_mut().append(bulk_edit);
488 }
489 } else if let Some(event) = events.into_iter().next() {
490 self.log_and_apply_event(&event);
491 }
492
493 if !deletions.is_empty() {
494 self.status_message = Some(t!("clipboard.cut_line").to_string());
495 }
496 }
497 }
498
499 pub fn paste(&mut self) {
507 let text = match self.clipboard.paste() {
509 Some(text) => text,
510 None => return,
511 };
512
513 self.paste_text(text);
515 }
516
517 pub fn paste_text(&mut self, paste_text: String) {
527 if paste_text.is_empty() {
528 return;
529 }
530
531 let normalized = paste_text.replace("\r\n", "\n").replace('\r', "\n");
534
535 if let Some(prompt) = self.prompt.as_mut() {
537 prompt.insert_str(&normalized);
538 self.update_prompt_suggestions();
539 self.status_message = Some(t!("clipboard.pasted").to_string());
540 return;
541 }
542
543 if self.terminal_mode {
545 self.send_terminal_input(normalized.as_bytes());
546 return;
547 }
548
549 let buffer_line_ending = self.active_state().buffer.line_ending();
551 let paste_text = match buffer_line_ending {
552 crate::model::buffer::LineEnding::LF => normalized,
553 crate::model::buffer::LineEnding::CRLF => normalized.replace('\n', "\r\n"),
554 crate::model::buffer::LineEnding::CR => normalized.replace('\n', "\r"),
555 };
556
557 let mut events = Vec::new();
558
559 let mut cursor_data: Vec<_> = self
561 .active_cursors()
562 .iter()
563 .map(|(cursor_id, cursor)| {
564 let selection = cursor.selection_range();
565 let insert_position = selection
566 .as_ref()
567 .map(|r| r.start)
568 .unwrap_or(cursor.position);
569 (cursor_id, selection, insert_position)
570 })
571 .collect();
572 cursor_data.sort_by_key(|(_, _, pos)| std::cmp::Reverse(*pos));
573
574 let cursor_data_with_text: Vec<_> = {
576 let state = self.active_state_mut();
577 cursor_data
578 .into_iter()
579 .map(|(cursor_id, selection, insert_position)| {
580 let deleted_text = selection
581 .as_ref()
582 .map(|r| state.get_text_range(r.start, r.end));
583 (cursor_id, selection, insert_position, deleted_text)
584 })
585 .collect()
586 };
587
588 for (cursor_id, selection, insert_position, deleted_text) in cursor_data_with_text {
590 if let (Some(range), Some(text)) = (selection, deleted_text) {
591 events.push(Event::Delete {
592 range,
593 deleted_text: text,
594 cursor_id,
595 });
596 }
597 events.push(Event::Insert {
598 position: insert_position,
599 text: paste_text.clone(),
600 cursor_id,
601 });
602 }
603
604 if events.len() > 1 {
606 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Paste".to_string()) {
608 self.active_event_log_mut().append(bulk_edit);
609 }
610 } else if let Some(event) = events.into_iter().next() {
611 self.log_and_apply_event(&event);
612 }
613
614 self.status_message = Some(t!("clipboard.pasted").to_string());
615 }
616
617 #[doc(hidden)]
621 pub fn set_clipboard_for_test(&mut self, text: String) {
622 self.clipboard.set_internal(text);
623 self.clipboard.set_internal_only(true);
624 }
625
626 #[doc(hidden)]
629 pub fn paste_for_test(&mut self) {
630 let paste_text = match self.clipboard.paste_internal() {
632 Some(text) => text,
633 None => return,
634 };
635
636 self.paste_text(paste_text);
638 }
639
640 #[doc(hidden)]
643 pub fn clipboard_content_for_test(&self) -> String {
644 self.clipboard.get_internal().to_string()
645 }
646
647 pub fn add_cursor_at_next_match(&mut self) {
650 let cursors = self.active_cursors().clone();
651 let state = self.active_state_mut();
652 match add_cursor_at_next_match(state, &cursors) {
653 AddCursorResult::Success {
654 cursor,
655 total_cursors,
656 } => {
657 let next_id = CursorId(self.active_cursors().count());
659 let event = Event::AddCursor {
660 cursor_id: next_id,
661 position: cursor.position,
662 anchor: cursor.anchor,
663 };
664
665 self.active_event_log_mut().append(event.clone());
667 self.apply_event_to_active_buffer(&event);
668
669 self.status_message =
670 Some(t!("clipboard.added_cursor_match", count = total_cursors).to_string());
671 }
672 AddCursorResult::WordSelected {
673 word_start,
674 word_end,
675 } => {
676 let primary_id = self.active_cursors().primary_id();
678 let primary = self.active_cursors().primary();
679 let event = Event::MoveCursor {
680 cursor_id: primary_id,
681 old_position: primary.position,
682 new_position: word_end,
683 old_anchor: primary.anchor,
684 new_anchor: Some(word_start),
685 old_sticky_column: primary.sticky_column,
686 new_sticky_column: 0,
687 };
688
689 self.active_event_log_mut().append(event.clone());
691 self.apply_event_to_active_buffer(&event);
692 }
693 AddCursorResult::Failed { message } => {
694 self.status_message = Some(message);
695 }
696 }
697 }
698
699 pub fn add_cursor_above(&mut self) {
701 let cursors = self.active_cursors().clone();
702 let state = self.active_state_mut();
703 match add_cursor_above(state, &cursors) {
704 AddCursorResult::Success {
705 cursor,
706 total_cursors,
707 } => {
708 let next_id = CursorId(self.active_cursors().count());
710 let event = Event::AddCursor {
711 cursor_id: next_id,
712 position: cursor.position,
713 anchor: cursor.anchor,
714 };
715
716 self.active_event_log_mut().append(event.clone());
718 self.apply_event_to_active_buffer(&event);
719
720 self.status_message =
721 Some(t!("clipboard.added_cursor_above", count = total_cursors).to_string());
722 }
723 AddCursorResult::Failed { message } => {
724 self.status_message = Some(message);
725 }
726 AddCursorResult::WordSelected { .. } => unreachable!(),
727 }
728 }
729
730 pub fn add_cursor_below(&mut self) {
732 let cursors = self.active_cursors().clone();
733 let state = self.active_state_mut();
734 match add_cursor_below(state, &cursors) {
735 AddCursorResult::Success {
736 cursor,
737 total_cursors,
738 } => {
739 let next_id = CursorId(self.active_cursors().count());
741 let event = Event::AddCursor {
742 cursor_id: next_id,
743 position: cursor.position,
744 anchor: cursor.anchor,
745 };
746
747 self.active_event_log_mut().append(event.clone());
749 self.apply_event_to_active_buffer(&event);
750
751 self.status_message =
752 Some(t!("clipboard.added_cursor_below", count = total_cursors).to_string());
753 }
754 AddCursorResult::Failed { message } => {
755 self.status_message = Some(message);
756 }
757 AddCursorResult::WordSelected { .. } => unreachable!(),
758 }
759 }
760
761 pub fn yank_word_forward(&mut self) {
767 let cursor_positions: Vec<_> = self
768 .active_cursors()
769 .iter()
770 .map(|(_, c)| c.position)
771 .collect();
772 let ranges: Vec<_> = {
773 let state = self.active_state();
774 cursor_positions
775 .into_iter()
776 .filter_map(|start| {
777 let end = find_word_start_right(&state.buffer, start);
778 if end > start {
779 Some(start..end)
780 } else {
781 None
782 }
783 })
784 .collect()
785 };
786
787 if ranges.is_empty() {
788 return;
789 }
790
791 let mut text = String::new();
793 let state = self.active_state_mut();
794 for range in ranges {
795 if !text.is_empty() {
796 text.push('\n');
797 }
798 let range_text = state.get_text_range(range.start, range.end);
799 text.push_str(&range_text);
800 }
801
802 if !text.is_empty() {
803 let len = text.len();
804 self.clipboard.copy(text);
805 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
806 }
807 }
808
809 pub fn yank_vi_word_end(&mut self) {
811 let cursor_positions: Vec<_> = self
812 .active_cursors()
813 .iter()
814 .map(|(_, c)| c.position)
815 .collect();
816 let ranges: Vec<_> = {
817 let state = self.active_state();
818 cursor_positions
819 .into_iter()
820 .filter_map(|start| {
821 let word_end = find_vi_word_end(&state.buffer, start);
822 let end = (word_end + 1).min(state.buffer.len());
823 if end > start {
824 Some(start..end)
825 } else {
826 None
827 }
828 })
829 .collect()
830 };
831
832 if ranges.is_empty() {
833 return;
834 }
835
836 let mut text = String::new();
837 let state = self.active_state_mut();
838 for range in ranges {
839 if !text.is_empty() {
840 text.push('\n');
841 }
842 let range_text = state.get_text_range(range.start, range.end);
843 text.push_str(&range_text);
844 }
845
846 if !text.is_empty() {
847 let len = text.len();
848 self.clipboard.copy(text);
849 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
850 }
851 }
852
853 pub fn yank_word_backward(&mut self) {
855 let cursor_positions: Vec<_> = self
856 .active_cursors()
857 .iter()
858 .map(|(_, c)| c.position)
859 .collect();
860 let ranges: Vec<_> = {
861 let state = self.active_state();
862 cursor_positions
863 .into_iter()
864 .filter_map(|end| {
865 let start = find_word_start_left(&state.buffer, end);
866 if start < end {
867 Some(start..end)
868 } else {
869 None
870 }
871 })
872 .collect()
873 };
874
875 if ranges.is_empty() {
876 return;
877 }
878
879 let mut text = String::new();
880 let state = self.active_state_mut();
881 for range in ranges {
882 if !text.is_empty() {
883 text.push('\n');
884 }
885 let range_text = state.get_text_range(range.start, range.end);
886 text.push_str(&range_text);
887 }
888
889 if !text.is_empty() {
890 let len = text.len();
891 self.clipboard.copy(text);
892 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
893 }
894 }
895
896 pub fn yank_to_line_end(&mut self) {
898 let estimated_line_length = 80;
899
900 let cursor_positions: Vec<_> = self
902 .active_cursors()
903 .iter()
904 .map(|(_, cursor)| cursor.position)
905 .collect();
906
907 let state = self.active_state_mut();
909 let mut ranges = Vec::new();
910 for pos in cursor_positions {
911 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
912 let line_start = iter.current_position();
913 if let Some((_start, content)) = iter.next_line() {
914 let content_len = content.trim_end_matches(&['\n', '\r'][..]).len();
916 let line_end = line_start + content_len;
917 if pos < line_end {
918 ranges.push(pos..line_end);
919 }
920 }
921 }
922
923 if ranges.is_empty() {
924 return;
925 }
926
927 let mut text = String::new();
928 for range in ranges {
929 if !text.is_empty() {
930 text.push('\n');
931 }
932 let range_text = state.get_text_range(range.start, range.end);
933 text.push_str(&range_text);
934 }
935
936 if !text.is_empty() {
937 let len = text.len();
938 self.clipboard.copy(text);
939 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
940 }
941 }
942
943 pub fn yank_to_line_start(&mut self) {
945 let estimated_line_length = 80;
946
947 let cursor_positions: Vec<_> = self
949 .active_cursors()
950 .iter()
951 .map(|(_, cursor)| cursor.position)
952 .collect();
953
954 let state = self.active_state_mut();
956 let mut ranges = Vec::new();
957 for pos in cursor_positions {
958 let iter = state.buffer.line_iterator(pos, estimated_line_length);
959 let line_start = iter.current_position();
960 if pos > line_start {
961 ranges.push(line_start..pos);
962 }
963 }
964
965 if ranges.is_empty() {
966 return;
967 }
968
969 let mut text = String::new();
970 for range in ranges {
971 if !text.is_empty() {
972 text.push('\n');
973 }
974 let range_text = state.get_text_range(range.start, range.end);
975 text.push_str(&range_text);
976 }
977
978 if !text.is_empty() {
979 let len = text.len();
980 self.clipboard.copy(text);
981 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
982 }
983 }
984}