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