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