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 current_theme_key = &self.config.theme.0;
332
333 let current_index = available_themes
335 .iter()
336 .position(|info| info.key == *current_theme_key)
337 .or_else(|| {
338 let normalized = crate::view::theme::normalize_theme_name(current_theme_key);
339 available_themes.iter().position(|info| {
340 crate::view::theme::normalize_theme_name(&info.name) == normalized
341 })
342 })
343 .unwrap_or(0);
344
345 let suggestions: Vec<crate::input::commands::Suggestion> = available_themes
346 .iter()
347 .map(|info| {
348 let is_current = Some(info) == available_themes.get(current_index);
349 let description = if is_current {
350 Some(format!("{} (current)", info.key))
351 } else {
352 Some(info.key.clone())
353 };
354 crate::input::commands::Suggestion {
355 text: info.name.clone(),
356 description,
357 value: Some(info.key.clone()),
358 disabled: false,
359 keybinding: None,
360 source: None,
361 }
362 })
363 .collect();
364
365 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
366 "Copy with theme: ".to_string(),
367 PromptType::CopyWithFormattingTheme,
368 suggestions,
369 ));
370
371 if let Some(prompt) = self.prompt.as_mut() {
372 if !prompt.suggestions.is_empty() {
373 prompt.selected_suggestion = Some(current_index);
374 prompt.input = current_theme_key.to_string();
375 prompt.cursor_pos = prompt.input.len();
376 }
377 }
378 }
379
380 pub fn cut_selection(&mut self) {
384 let has_selection = self
386 .active_cursors()
387 .iter()
388 .any(|(_, cursor)| cursor.selection_range().is_some());
389
390 self.copy_selection();
392
393 if has_selection {
394 let mut deletions: Vec<_> = self
397 .active_cursors()
398 .iter()
399 .filter_map(|(_, c)| c.selection_range())
400 .collect();
401 deletions.sort_by_key(|r| r.start);
403
404 let primary_id = self.active_cursors().primary_id();
405 let state = self.active_state_mut();
406 let events: Vec<_> = deletions
407 .iter()
408 .rev()
409 .map(|range| {
410 let deleted_text = state.get_text_range(range.start, range.end);
411 Event::Delete {
412 range: range.clone(),
413 deleted_text,
414 cursor_id: primary_id,
415 }
416 })
417 .collect();
418
419 if events.len() > 1 {
421 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Cut".to_string()) {
423 self.active_event_log_mut().append(bulk_edit);
424 }
425 } else if let Some(event) = events.into_iter().next() {
426 self.log_and_apply_event(&event);
427 }
428
429 if !deletions.is_empty() {
430 self.status_message = Some(t!("clipboard.cut").to_string());
431 }
432 } else {
433 let estimated_line_length = 80;
435
436 let positions: Vec<_> = self
439 .active_cursors()
440 .iter()
441 .map(|(_, c)| c.position)
442 .collect();
443 let mut deletions: Vec<_> = {
444 let state = self.active_state_mut();
445 positions
446 .into_iter()
447 .filter_map(|pos| {
448 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
449 let line_start = iter.current_position();
450 iter.next_line().map(|(_start, content)| {
451 let line_end = line_start + content.len();
452 line_start..line_end
453 })
454 })
455 .collect()
456 };
457 deletions.sort_by_key(|r| r.start);
459
460 let primary_id = self.active_cursors().primary_id();
461 let state = self.active_state_mut();
462 let events: Vec<_> = deletions
463 .iter()
464 .rev()
465 .map(|range| {
466 let deleted_text = state.get_text_range(range.start, range.end);
467 Event::Delete {
468 range: range.clone(),
469 deleted_text,
470 cursor_id: primary_id,
471 }
472 })
473 .collect();
474
475 if events.len() > 1 {
477 if let Some(bulk_edit) =
479 self.apply_events_as_bulk_edit(events, "Cut line".to_string())
480 {
481 self.active_event_log_mut().append(bulk_edit);
482 }
483 } else if let Some(event) = events.into_iter().next() {
484 self.log_and_apply_event(&event);
485 }
486
487 if !deletions.is_empty() {
488 self.status_message = Some(t!("clipboard.cut_line").to_string());
489 }
490 }
491 }
492
493 pub fn paste(&mut self) {
501 let text = match self.clipboard.paste() {
503 Some(text) => text,
504 None => return,
505 };
506
507 self.paste_text(text);
509 }
510
511 pub fn paste_text(&mut self, paste_text: String) {
521 if paste_text.is_empty() {
522 return;
523 }
524
525 let normalized = paste_text.replace("\r\n", "\n").replace('\r', "\n");
528
529 if let Some(prompt) = self.prompt.as_mut() {
531 prompt.insert_str(&normalized);
532 self.update_prompt_suggestions();
533 self.status_message = Some(t!("clipboard.pasted").to_string());
534 return;
535 }
536
537 if self.terminal_mode {
539 self.send_terminal_input(normalized.as_bytes());
540 return;
541 }
542
543 let buffer_line_ending = self.active_state().buffer.line_ending();
545 let paste_text = match buffer_line_ending {
546 crate::model::buffer::LineEnding::LF => normalized,
547 crate::model::buffer::LineEnding::CRLF => normalized.replace('\n', "\r\n"),
548 crate::model::buffer::LineEnding::CR => normalized.replace('\n', "\r"),
549 };
550
551 let mut events = Vec::new();
552
553 let mut cursor_data: Vec<_> = self
555 .active_cursors()
556 .iter()
557 .map(|(cursor_id, cursor)| {
558 let selection = cursor.selection_range();
559 let insert_position = selection
560 .as_ref()
561 .map(|r| r.start)
562 .unwrap_or(cursor.position);
563 (cursor_id, selection, insert_position)
564 })
565 .collect();
566 cursor_data.sort_by_key(|(_, _, pos)| std::cmp::Reverse(*pos));
567
568 let cursor_data_with_text: Vec<_> = {
570 let state = self.active_state_mut();
571 cursor_data
572 .into_iter()
573 .map(|(cursor_id, selection, insert_position)| {
574 let deleted_text = selection
575 .as_ref()
576 .map(|r| state.get_text_range(r.start, r.end));
577 (cursor_id, selection, insert_position, deleted_text)
578 })
579 .collect()
580 };
581
582 for (cursor_id, selection, insert_position, deleted_text) in cursor_data_with_text {
584 if let (Some(range), Some(text)) = (selection, deleted_text) {
585 events.push(Event::Delete {
586 range,
587 deleted_text: text,
588 cursor_id,
589 });
590 }
591 events.push(Event::Insert {
592 position: insert_position,
593 text: paste_text.clone(),
594 cursor_id,
595 });
596 }
597
598 if events.len() > 1 {
600 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Paste".to_string()) {
602 self.active_event_log_mut().append(bulk_edit);
603 }
604 } else if let Some(event) = events.into_iter().next() {
605 self.log_and_apply_event(&event);
606 }
607
608 self.status_message = Some(t!("clipboard.pasted").to_string());
609 }
610
611 #[doc(hidden)]
615 pub fn set_clipboard_for_test(&mut self, text: String) {
616 self.clipboard.set_internal(text);
617 self.clipboard.set_internal_only(true);
618 }
619
620 #[doc(hidden)]
623 pub fn paste_for_test(&mut self) {
624 let paste_text = match self.clipboard.paste_internal() {
626 Some(text) => text,
627 None => return,
628 };
629
630 self.paste_text(paste_text);
632 }
633
634 #[doc(hidden)]
637 pub fn clipboard_content_for_test(&self) -> String {
638 self.clipboard.get_internal().to_string()
639 }
640
641 pub fn add_cursor_at_next_match(&mut self) {
644 let cursors = self.active_cursors().clone();
645 let state = self.active_state_mut();
646 match add_cursor_at_next_match(state, &cursors) {
647 AddCursorResult::Success {
648 cursor,
649 total_cursors,
650 } => {
651 let next_id = CursorId(self.active_cursors().count());
653 let event = Event::AddCursor {
654 cursor_id: next_id,
655 position: cursor.position,
656 anchor: cursor.anchor,
657 };
658
659 self.active_event_log_mut().append(event.clone());
661 self.apply_event_to_active_buffer(&event);
662
663 self.status_message =
664 Some(t!("clipboard.added_cursor_match", count = total_cursors).to_string());
665 }
666 AddCursorResult::WordSelected {
667 word_start,
668 word_end,
669 } => {
670 let primary_id = self.active_cursors().primary_id();
672 let primary = self.active_cursors().primary();
673 let event = Event::MoveCursor {
674 cursor_id: primary_id,
675 old_position: primary.position,
676 new_position: word_end,
677 old_anchor: primary.anchor,
678 new_anchor: Some(word_start),
679 old_sticky_column: primary.sticky_column,
680 new_sticky_column: 0,
681 };
682
683 self.active_event_log_mut().append(event.clone());
685 self.apply_event_to_active_buffer(&event);
686 }
687 AddCursorResult::Failed { message } => {
688 self.status_message = Some(message);
689 }
690 }
691 }
692
693 pub fn add_cursor_above(&mut self) {
695 let cursors = self.active_cursors().clone();
696 let state = self.active_state_mut();
697 match add_cursor_above(state, &cursors) {
698 AddCursorResult::Success {
699 cursor,
700 total_cursors,
701 } => {
702 let next_id = CursorId(self.active_cursors().count());
704 let event = Event::AddCursor {
705 cursor_id: next_id,
706 position: cursor.position,
707 anchor: cursor.anchor,
708 };
709
710 self.active_event_log_mut().append(event.clone());
712 self.apply_event_to_active_buffer(&event);
713
714 self.status_message =
715 Some(t!("clipboard.added_cursor_above", count = total_cursors).to_string());
716 }
717 AddCursorResult::Failed { message } => {
718 self.status_message = Some(message);
719 }
720 AddCursorResult::WordSelected { .. } => unreachable!(),
721 }
722 }
723
724 pub fn add_cursor_below(&mut self) {
726 let cursors = self.active_cursors().clone();
727 let state = self.active_state_mut();
728 match add_cursor_below(state, &cursors) {
729 AddCursorResult::Success {
730 cursor,
731 total_cursors,
732 } => {
733 let next_id = CursorId(self.active_cursors().count());
735 let event = Event::AddCursor {
736 cursor_id: next_id,
737 position: cursor.position,
738 anchor: cursor.anchor,
739 };
740
741 self.active_event_log_mut().append(event.clone());
743 self.apply_event_to_active_buffer(&event);
744
745 self.status_message =
746 Some(t!("clipboard.added_cursor_below", count = total_cursors).to_string());
747 }
748 AddCursorResult::Failed { message } => {
749 self.status_message = Some(message);
750 }
751 AddCursorResult::WordSelected { .. } => unreachable!(),
752 }
753 }
754
755 pub fn yank_word_forward(&mut self) {
761 let cursor_positions: Vec<_> = self
762 .active_cursors()
763 .iter()
764 .map(|(_, c)| c.position)
765 .collect();
766 let ranges: Vec<_> = {
767 let state = self.active_state();
768 cursor_positions
769 .into_iter()
770 .filter_map(|start| {
771 let end = find_word_start_right(&state.buffer, start);
772 if end > start {
773 Some(start..end)
774 } else {
775 None
776 }
777 })
778 .collect()
779 };
780
781 if ranges.is_empty() {
782 return;
783 }
784
785 let mut text = String::new();
787 let state = self.active_state_mut();
788 for range in ranges {
789 if !text.is_empty() {
790 text.push('\n');
791 }
792 let range_text = state.get_text_range(range.start, range.end);
793 text.push_str(&range_text);
794 }
795
796 if !text.is_empty() {
797 let len = text.len();
798 self.clipboard.copy(text);
799 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
800 }
801 }
802
803 pub fn yank_vi_word_end(&mut self) {
805 let cursor_positions: Vec<_> = self
806 .active_cursors()
807 .iter()
808 .map(|(_, c)| c.position)
809 .collect();
810 let ranges: Vec<_> = {
811 let state = self.active_state();
812 cursor_positions
813 .into_iter()
814 .filter_map(|start| {
815 let word_end = find_vi_word_end(&state.buffer, start);
816 let end = (word_end + 1).min(state.buffer.len());
817 if end > start {
818 Some(start..end)
819 } else {
820 None
821 }
822 })
823 .collect()
824 };
825
826 if ranges.is_empty() {
827 return;
828 }
829
830 let mut text = String::new();
831 let state = self.active_state_mut();
832 for range in ranges {
833 if !text.is_empty() {
834 text.push('\n');
835 }
836 let range_text = state.get_text_range(range.start, range.end);
837 text.push_str(&range_text);
838 }
839
840 if !text.is_empty() {
841 let len = text.len();
842 self.clipboard.copy(text);
843 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
844 }
845 }
846
847 pub fn yank_word_backward(&mut self) {
849 let cursor_positions: Vec<_> = self
850 .active_cursors()
851 .iter()
852 .map(|(_, c)| c.position)
853 .collect();
854 let ranges: Vec<_> = {
855 let state = self.active_state();
856 cursor_positions
857 .into_iter()
858 .filter_map(|end| {
859 let start = find_word_start_left(&state.buffer, end);
860 if start < end {
861 Some(start..end)
862 } else {
863 None
864 }
865 })
866 .collect()
867 };
868
869 if ranges.is_empty() {
870 return;
871 }
872
873 let mut text = String::new();
874 let state = self.active_state_mut();
875 for range in ranges {
876 if !text.is_empty() {
877 text.push('\n');
878 }
879 let range_text = state.get_text_range(range.start, range.end);
880 text.push_str(&range_text);
881 }
882
883 if !text.is_empty() {
884 let len = text.len();
885 self.clipboard.copy(text);
886 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
887 }
888 }
889
890 pub fn yank_to_line_end(&mut self) {
892 let estimated_line_length = 80;
893
894 let cursor_positions: Vec<_> = self
896 .active_cursors()
897 .iter()
898 .map(|(_, cursor)| cursor.position)
899 .collect();
900
901 let state = self.active_state_mut();
903 let mut ranges = Vec::new();
904 for pos in cursor_positions {
905 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
906 let line_start = iter.current_position();
907 if let Some((_start, content)) = iter.next_line() {
908 let content_len = content.trim_end_matches(&['\n', '\r'][..]).len();
910 let line_end = line_start + content_len;
911 if pos < line_end {
912 ranges.push(pos..line_end);
913 }
914 }
915 }
916
917 if ranges.is_empty() {
918 return;
919 }
920
921 let mut text = String::new();
922 for range in ranges {
923 if !text.is_empty() {
924 text.push('\n');
925 }
926 let range_text = state.get_text_range(range.start, range.end);
927 text.push_str(&range_text);
928 }
929
930 if !text.is_empty() {
931 let len = text.len();
932 self.clipboard.copy(text);
933 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
934 }
935 }
936
937 pub fn yank_to_line_start(&mut self) {
939 let estimated_line_length = 80;
940
941 let cursor_positions: Vec<_> = self
943 .active_cursors()
944 .iter()
945 .map(|(_, cursor)| cursor.position)
946 .collect();
947
948 let state = self.active_state_mut();
950 let mut ranges = Vec::new();
951 for pos in cursor_positions {
952 let iter = state.buffer.line_iterator(pos, estimated_line_length);
953 let line_start = iter.current_position();
954 if pos > line_start {
955 ranges.push(line_start..pos);
956 }
957 }
958
959 if ranges.is_empty() {
960 return;
961 }
962
963 let mut text = String::new();
964 for range in ranges {
965 if !text.is_empty() {
966 text.push('\n');
967 }
968 let range_text = state.get_text_range(range.start, range.end);
969 text.push_str(&range_text);
970 }
971
972 if !text.is_empty() {
973 let len = text.len();
974 self.clipboard.copy(text);
975 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
976 }
977 }
978}