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::{find_word_start_left, find_word_start_right};
17
18use super::Editor;
19
20fn byte_to_2d(buffer: &Buffer, byte_pos: usize) -> Position2D {
22 let line = buffer.get_line_number(byte_pos);
23 let line_start = buffer.line_start_offset(line).unwrap_or(0);
24 let column = byte_pos.saturating_sub(line_start);
25 Position2D { line, column }
26}
27
28impl Editor {
40 pub fn copy_selection(&mut self) {
45 let has_block_selection = self
47 .active_cursors()
48 .iter()
49 .any(|(_, cursor)| cursor.has_block_selection());
50
51 if has_block_selection {
52 let text = self.copy_block_selection_text();
54 if !text.is_empty() {
55 self.clipboard.copy(text);
56 self.status_message = Some(t!("clipboard.copied").to_string());
57 }
58 return;
59 }
60
61 let has_selection = self
63 .active_cursors()
64 .iter()
65 .any(|(_, cursor)| cursor.selection_range().is_some());
66
67 if has_selection {
68 let ranges: Vec<_> = self
70 .active_cursors()
71 .iter()
72 .filter_map(|(_, cursor)| cursor.selection_range())
73 .collect();
74
75 let mut text = String::new();
76 let state = self.active_state_mut();
77 for range in ranges {
78 if !text.is_empty() {
79 text.push('\n');
80 }
81 let range_text = state.get_text_range(range.start, range.end);
82 text.push_str(&range_text);
83 }
84
85 if !text.is_empty() {
86 self.clipboard.copy(text);
87 self.status_message = Some(t!("clipboard.copied").to_string());
88 }
89 } else {
90 let estimated_line_length = 80;
92 let mut text = String::new();
93
94 let positions: Vec<_> = self
96 .active_cursors()
97 .iter()
98 .map(|(_, c)| c.position)
99 .collect();
100 let state = self.active_state_mut();
101
102 for pos in positions {
103 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
104 if let Some((_start, content)) = iter.next_line() {
105 if !text.is_empty() {
106 text.push('\n');
107 }
108 text.push_str(&content);
109 }
110 }
111
112 if !text.is_empty() {
113 self.clipboard.copy(text);
114 self.status_message = Some(t!("clipboard.copied_line").to_string());
115 }
116 }
117 }
118
119 fn copy_block_selection_text(&mut self) -> String {
128 let estimated_line_length = 120;
129
130 let block_infos: Vec<_> = self
132 .active_cursors()
133 .iter()
134 .filter_map(|(_, cursor)| {
135 if !cursor.has_block_selection() {
136 return None;
137 }
138 let block_anchor = cursor.block_anchor?;
139 let anchor_byte = cursor.anchor?; let cursor_byte = cursor.position;
141 Some((block_anchor, anchor_byte, cursor_byte))
142 })
143 .collect();
144
145 let mut result = String::new();
146
147 for (block_anchor, anchor_byte, cursor_byte) in block_infos {
148 let cursor_2d = {
150 let state = self.active_state();
151 byte_to_2d(&state.buffer, cursor_byte)
152 };
153
154 let min_col = block_anchor.column.min(cursor_2d.column);
156 let max_col = block_anchor.column.max(cursor_2d.column);
157
158 let start_byte = anchor_byte.min(cursor_byte);
160 let end_byte = anchor_byte.max(cursor_byte);
161
162 let state = self.active_state_mut();
164 let mut iter = state
165 .buffer
166 .line_iterator(start_byte, estimated_line_length);
167
168 let mut lines_text = Vec::new();
170 loop {
171 let line_start = iter.current_position();
172
173 if line_start > end_byte {
175 break;
176 }
177
178 if let Some((_offset, line_content)) = iter.next_line() {
179 let content_without_newline = line_content.trim_end_matches(&['\n', '\r'][..]);
182 let chars: Vec<char> = content_without_newline.chars().collect();
183
184 let extracted: String = chars
186 .iter()
187 .skip(min_col)
188 .take(max_col.saturating_sub(min_col))
189 .collect();
190
191 lines_text.push(extracted);
192
193 if line_start + line_content.len() > end_byte {
195 break;
196 }
197 } else {
198 break;
199 }
200 }
201
202 if !result.is_empty() && !lines_text.is_empty() {
204 result.push('\n');
205 }
206 result.push_str(&lines_text.join("\n"));
207 }
208
209 result
210 }
211
212 pub fn copy_selection_with_theme(&mut self, theme_name: &str) {
217 let has_selection = self
219 .active_cursors()
220 .iter()
221 .any(|(_, cursor)| cursor.selection_range().is_some());
222
223 if !has_selection {
224 self.status_message = Some(t!("clipboard.no_selection").to_string());
225 return;
226 }
227
228 if theme_name.is_empty() {
230 self.start_copy_with_formatting_prompt();
231 return;
232 }
233 use crate::services::styled_html::render_styled_html;
234
235 let theme = match self.theme_registry.get_cloned(theme_name) {
237 Some(t) => t,
238 None => {
239 self.status_message = Some(format!("Theme '{}' not found", theme_name));
240 return;
241 }
242 };
243
244 let ranges: Vec<_> = self
246 .active_cursors()
247 .iter()
248 .filter_map(|(_, cursor)| cursor.selection_range())
249 .collect();
250
251 if ranges.is_empty() {
252 self.status_message = Some(t!("clipboard.no_selection").to_string());
253 return;
254 }
255
256 let min_offset = ranges.iter().map(|r| r.start).min().unwrap_or(0);
258 let max_offset = ranges.iter().map(|r| r.end).max().unwrap_or(0);
259
260 let (text, highlight_spans) = {
262 let state = self.active_state_mut();
263
264 let mut text = String::new();
266 for range in &ranges {
267 if !text.is_empty() {
268 text.push('\n');
269 }
270 let range_text = state.get_text_range(range.start, range.end);
271 text.push_str(&range_text);
272 }
273
274 if text.is_empty() {
275 (text, Vec::new())
276 } else {
277 let highlight_spans = state.highlighter.highlight_viewport(
279 &state.buffer,
280 min_offset,
281 max_offset,
282 &theme,
283 0, );
285 (text, highlight_spans)
286 }
287 };
288
289 if text.is_empty() {
290 self.status_message = Some(t!("clipboard.no_text").to_string());
291 return;
292 }
293
294 let adjusted_spans: Vec<_> = if ranges.len() == 1 {
296 let base_offset = ranges[0].start;
297 highlight_spans
298 .into_iter()
299 .filter_map(|span| {
300 if span.range.end <= base_offset || span.range.start >= ranges[0].end {
301 return None;
302 }
303 let start = span.range.start.saturating_sub(base_offset);
304 let end = (span.range.end - base_offset).min(text.len());
305 if start < end {
306 Some(crate::primitives::highlighter::HighlightSpan {
307 range: start..end,
308 color: span.color,
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.status_message =
326 Some(t!("clipboard.copied_with_theme", theme = theme_name).to_string());
327 } else {
328 self.clipboard.copy(text);
329 self.status_message = Some(t!("clipboard.copied_plain").to_string());
330 }
331 }
332
333 fn start_copy_with_formatting_prompt(&mut self) {
335 use crate::view::prompt::PromptType;
336
337 let available_themes = self.theme_registry.list();
338 let current_theme_name = &self.theme.name;
339
340 let current_index = available_themes
342 .iter()
343 .position(|info| info.name == *current_theme_name)
344 .unwrap_or(0);
345
346 let suggestions: Vec<crate::input::commands::Suggestion> = available_themes
347 .iter()
348 .map(|info| {
349 let is_current = info.name == *current_theme_name;
350 let description = match (is_current, info.pack.is_empty()) {
351 (true, true) => Some("(current)".to_string()),
352 (true, false) => Some(format!("{} (current)", info.pack)),
353 (false, true) => None,
354 (false, false) => Some(info.pack.clone()),
355 };
356 crate::input::commands::Suggestion {
357 text: info.name.clone(),
358 description,
359 value: Some(info.name.clone()),
360 disabled: false,
361 keybinding: None,
362 source: None,
363 }
364 })
365 .collect();
366
367 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
368 "Copy with theme: ".to_string(),
369 PromptType::CopyWithFormattingTheme,
370 suggestions,
371 ));
372
373 if let Some(prompt) = self.prompt.as_mut() {
374 if !prompt.suggestions.is_empty() {
375 prompt.selected_suggestion = Some(current_index);
376 prompt.input = current_theme_name.to_string();
377 prompt.cursor_pos = prompt.input.len();
378 }
379 }
380 }
381
382 pub fn cut_selection(&mut self) {
386 let has_selection = self
388 .active_cursors()
389 .iter()
390 .any(|(_, cursor)| cursor.selection_range().is_some());
391
392 self.copy_selection();
394
395 if has_selection {
396 let mut deletions: Vec<_> = self
399 .active_cursors()
400 .iter()
401 .filter_map(|(_, c)| c.selection_range())
402 .collect();
403 deletions.sort_by_key(|r| r.start);
405
406 let primary_id = self.active_cursors().primary_id();
407 let state = self.active_state_mut();
408 let events: Vec<_> = deletions
409 .iter()
410 .rev()
411 .map(|range| {
412 let deleted_text = state.get_text_range(range.start, range.end);
413 Event::Delete {
414 range: range.clone(),
415 deleted_text,
416 cursor_id: primary_id,
417 }
418 })
419 .collect();
420
421 if events.len() > 1 {
423 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Cut".to_string()) {
425 self.active_event_log_mut().append(bulk_edit);
426 }
427 } else if let Some(event) = events.into_iter().next() {
428 self.active_event_log_mut().append(event.clone());
429 self.apply_event_to_active_buffer(&event);
430 }
431
432 if !deletions.is_empty() {
433 self.status_message = Some(t!("clipboard.cut").to_string());
434 }
435 } else {
436 let estimated_line_length = 80;
438
439 let positions: Vec<_> = self
442 .active_cursors()
443 .iter()
444 .map(|(_, c)| c.position)
445 .collect();
446 let mut deletions: Vec<_> = {
447 let state = self.active_state_mut();
448 positions
449 .into_iter()
450 .filter_map(|pos| {
451 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
452 let line_start = iter.current_position();
453 iter.next_line().map(|(_start, content)| {
454 let line_end = line_start + content.len();
455 line_start..line_end
456 })
457 })
458 .collect()
459 };
460 deletions.sort_by_key(|r| r.start);
462
463 let primary_id = self.active_cursors().primary_id();
464 let state = self.active_state_mut();
465 let events: Vec<_> = deletions
466 .iter()
467 .rev()
468 .map(|range| {
469 let deleted_text = state.get_text_range(range.start, range.end);
470 Event::Delete {
471 range: range.clone(),
472 deleted_text,
473 cursor_id: primary_id,
474 }
475 })
476 .collect();
477
478 if events.len() > 1 {
480 if let Some(bulk_edit) =
482 self.apply_events_as_bulk_edit(events, "Cut line".to_string())
483 {
484 self.active_event_log_mut().append(bulk_edit);
485 }
486 } else if let Some(event) = events.into_iter().next() {
487 self.active_event_log_mut().append(event.clone());
488 self.apply_event_to_active_buffer(&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.active_event_log_mut().append(event.clone());
610 self.apply_event_to_active_buffer(&event);
611 }
612
613 self.status_message = Some(t!("clipboard.pasted").to_string());
614 }
615
616 #[doc(hidden)]
620 pub fn set_clipboard_for_test(&mut self, text: String) {
621 self.clipboard.set_internal(text);
622 self.clipboard.set_internal_only(true);
623 }
624
625 #[doc(hidden)]
628 pub fn paste_for_test(&mut self) {
629 let paste_text = match self.clipboard.paste_internal() {
631 Some(text) => text,
632 None => return,
633 };
634
635 self.paste_text(paste_text);
637 }
638
639 #[doc(hidden)]
642 pub fn clipboard_content_for_test(&self) -> String {
643 self.clipboard.get_internal().to_string()
644 }
645
646 pub fn add_cursor_at_next_match(&mut self) {
649 let cursors = self.active_cursors().clone();
650 let state = self.active_state_mut();
651 match add_cursor_at_next_match(state, &cursors) {
652 AddCursorResult::Success {
653 cursor,
654 total_cursors,
655 } => {
656 let next_id = CursorId(self.active_cursors().count());
658 let event = Event::AddCursor {
659 cursor_id: next_id,
660 position: cursor.position,
661 anchor: cursor.anchor,
662 };
663
664 self.active_event_log_mut().append(event.clone());
666 self.apply_event_to_active_buffer(&event);
667
668 self.status_message =
669 Some(t!("clipboard.added_cursor_match", count = total_cursors).to_string());
670 }
671 AddCursorResult::WordSelected {
672 word_start,
673 word_end,
674 } => {
675 let primary_id = self.active_cursors().primary_id();
677 let primary = self.active_cursors().primary();
678 let event = Event::MoveCursor {
679 cursor_id: primary_id,
680 old_position: primary.position,
681 new_position: word_end,
682 old_anchor: primary.anchor,
683 new_anchor: Some(word_start),
684 old_sticky_column: primary.sticky_column,
685 new_sticky_column: 0,
686 };
687
688 self.active_event_log_mut().append(event.clone());
690 self.apply_event_to_active_buffer(&event);
691 }
692 AddCursorResult::Failed { message } => {
693 self.status_message = Some(message);
694 }
695 }
696 }
697
698 pub fn add_cursor_above(&mut self) {
700 let cursors = self.active_cursors().clone();
701 let state = self.active_state_mut();
702 match add_cursor_above(state, &cursors) {
703 AddCursorResult::Success {
704 cursor,
705 total_cursors,
706 } => {
707 let next_id = CursorId(self.active_cursors().count());
709 let event = Event::AddCursor {
710 cursor_id: next_id,
711 position: cursor.position,
712 anchor: cursor.anchor,
713 };
714
715 self.active_event_log_mut().append(event.clone());
717 self.apply_event_to_active_buffer(&event);
718
719 self.status_message =
720 Some(t!("clipboard.added_cursor_above", count = total_cursors).to_string());
721 }
722 AddCursorResult::Failed { message } => {
723 self.status_message = Some(message);
724 }
725 AddCursorResult::WordSelected { .. } => unreachable!(),
726 }
727 }
728
729 pub fn add_cursor_below(&mut self) {
731 let cursors = self.active_cursors().clone();
732 let state = self.active_state_mut();
733 match add_cursor_below(state, &cursors) {
734 AddCursorResult::Success {
735 cursor,
736 total_cursors,
737 } => {
738 let next_id = CursorId(self.active_cursors().count());
740 let event = Event::AddCursor {
741 cursor_id: next_id,
742 position: cursor.position,
743 anchor: cursor.anchor,
744 };
745
746 self.active_event_log_mut().append(event.clone());
748 self.apply_event_to_active_buffer(&event);
749
750 self.status_message =
751 Some(t!("clipboard.added_cursor_below", count = total_cursors).to_string());
752 }
753 AddCursorResult::Failed { message } => {
754 self.status_message = Some(message);
755 }
756 AddCursorResult::WordSelected { .. } => unreachable!(),
757 }
758 }
759
760 pub fn yank_word_forward(&mut self) {
766 let cursor_positions: Vec<_> = self
767 .active_cursors()
768 .iter()
769 .map(|(_, c)| c.position)
770 .collect();
771 let ranges: Vec<_> = {
772 let state = self.active_state();
773 cursor_positions
774 .into_iter()
775 .filter_map(|start| {
776 let end = find_word_start_right(&state.buffer, start);
777 if end > start {
778 Some(start..end)
779 } else {
780 None
781 }
782 })
783 .collect()
784 };
785
786 if ranges.is_empty() {
787 return;
788 }
789
790 let mut text = String::new();
792 let state = self.active_state_mut();
793 for range in ranges {
794 if !text.is_empty() {
795 text.push('\n');
796 }
797 let range_text = state.get_text_range(range.start, range.end);
798 text.push_str(&range_text);
799 }
800
801 if !text.is_empty() {
802 let len = text.len();
803 self.clipboard.copy(text);
804 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
805 }
806 }
807
808 pub fn yank_word_backward(&mut self) {
810 let cursor_positions: Vec<_> = self
811 .active_cursors()
812 .iter()
813 .map(|(_, c)| c.position)
814 .collect();
815 let ranges: Vec<_> = {
816 let state = self.active_state();
817 cursor_positions
818 .into_iter()
819 .filter_map(|end| {
820 let start = find_word_start_left(&state.buffer, end);
821 if start < end {
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_to_line_end(&mut self) {
853 let estimated_line_length = 80;
854
855 let cursor_positions: Vec<_> = self
857 .active_cursors()
858 .iter()
859 .map(|(_, cursor)| cursor.position)
860 .collect();
861
862 let state = self.active_state_mut();
864 let mut ranges = Vec::new();
865 for pos in cursor_positions {
866 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
867 let line_start = iter.current_position();
868 if let Some((_start, content)) = iter.next_line() {
869 let content_len = content.trim_end_matches(&['\n', '\r'][..]).len();
871 let line_end = line_start + content_len;
872 if pos < line_end {
873 ranges.push(pos..line_end);
874 }
875 }
876 }
877
878 if ranges.is_empty() {
879 return;
880 }
881
882 let mut text = String::new();
883 for range in ranges {
884 if !text.is_empty() {
885 text.push('\n');
886 }
887 let range_text = state.get_text_range(range.start, range.end);
888 text.push_str(&range_text);
889 }
890
891 if !text.is_empty() {
892 let len = text.len();
893 self.clipboard.copy(text);
894 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
895 }
896 }
897
898 pub fn yank_to_line_start(&mut self) {
900 let estimated_line_length = 80;
901
902 let cursor_positions: Vec<_> = self
904 .active_cursors()
905 .iter()
906 .map(|(_, cursor)| cursor.position)
907 .collect();
908
909 let state = self.active_state_mut();
911 let mut ranges = Vec::new();
912 for pos in cursor_positions {
913 let iter = state.buffer.line_iterator(pos, estimated_line_length);
914 let line_start = iter.current_position();
915 if pos > line_start {
916 ranges.push(line_start..pos);
917 }
918 }
919
920 if ranges.is_empty() {
921 return;
922 }
923
924 let mut text = String::new();
925 for range in ranges {
926 if !text.is_empty() {
927 text.push('\n');
928 }
929 let range_text = state.get_text_range(range.start, range.end);
930 text.push_str(&range_text);
931 }
932
933 if !text.is_empty() {
934 let len = text.len();
935 self.clipboard.copy(text);
936 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
937 }
938 }
939}