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 = {
47 let state = self.active_state();
48 state
49 .cursors
50 .iter()
51 .any(|(_, cursor)| cursor.has_block_selection())
52 };
53
54 if has_block_selection {
55 let text = self.copy_block_selection_text();
57 if !text.is_empty() {
58 self.clipboard.copy(text);
59 self.status_message = Some(t!("clipboard.copied").to_string());
60 }
61 return;
62 }
63
64 let has_selection = {
66 let state = self.active_state();
67 state
68 .cursors
69 .iter()
70 .any(|(_, cursor)| cursor.selection_range().is_some())
71 };
72
73 if has_selection {
74 let ranges: Vec<_> = {
76 let state = self.active_state();
77 state
78 .cursors
79 .iter()
80 .filter_map(|(_, cursor)| cursor.selection_range())
81 .collect()
82 };
83
84 let mut text = String::new();
85 let state = self.active_state_mut();
86 for range in ranges {
87 if !text.is_empty() {
88 text.push('\n');
89 }
90 let range_text = state.get_text_range(range.start, range.end);
91 text.push_str(&range_text);
92 }
93
94 if !text.is_empty() {
95 self.clipboard.copy(text);
96 self.status_message = Some(t!("clipboard.copied").to_string());
97 }
98 } else {
99 let estimated_line_length = 80;
101 let mut text = String::new();
102 let state = self.active_state_mut();
103
104 let positions: Vec<_> = state.cursors.iter().map(|(_, c)| c.position).collect();
106
107 for pos in positions {
108 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
109 if let Some((_start, content)) = iter.next_line() {
110 if !text.is_empty() {
111 text.push('\n');
112 }
113 text.push_str(&content);
114 }
115 }
116
117 if !text.is_empty() {
118 self.clipboard.copy(text);
119 self.status_message = Some(t!("clipboard.copied_line").to_string());
120 }
121 }
122 }
123
124 fn copy_block_selection_text(&mut self) -> String {
133 let estimated_line_length = 120;
134
135 let block_infos: Vec<_> = {
137 let state = self.active_state();
138 state
139 .cursors
140 .iter()
141 .filter_map(|(_, cursor)| {
142 if !cursor.has_block_selection() {
143 return None;
144 }
145 let block_anchor = cursor.block_anchor?;
146 let anchor_byte = cursor.anchor?; let cursor_byte = cursor.position;
148 Some((block_anchor, anchor_byte, cursor_byte))
149 })
150 .collect()
151 };
152
153 let mut result = String::new();
154
155 for (block_anchor, anchor_byte, cursor_byte) in block_infos {
156 let cursor_2d = {
158 let state = self.active_state();
159 byte_to_2d(&state.buffer, cursor_byte)
160 };
161
162 let min_col = block_anchor.column.min(cursor_2d.column);
164 let max_col = block_anchor.column.max(cursor_2d.column);
165
166 let start_byte = anchor_byte.min(cursor_byte);
168 let end_byte = anchor_byte.max(cursor_byte);
169
170 let state = self.active_state_mut();
172 let mut iter = state
173 .buffer
174 .line_iterator(start_byte, estimated_line_length);
175
176 let mut lines_text = Vec::new();
178 loop {
179 let line_start = iter.current_position();
180
181 if line_start > end_byte {
183 break;
184 }
185
186 if let Some((_offset, line_content)) = iter.next_line() {
187 let content_without_newline = line_content.trim_end_matches(&['\n', '\r'][..]);
190 let chars: Vec<char> = content_without_newline.chars().collect();
191
192 let extracted: String = chars
194 .iter()
195 .skip(min_col)
196 .take(max_col.saturating_sub(min_col))
197 .collect();
198
199 lines_text.push(extracted);
200
201 if line_start + line_content.len() > end_byte {
203 break;
204 }
205 } else {
206 break;
207 }
208 }
209
210 if !result.is_empty() && !lines_text.is_empty() {
212 result.push('\n');
213 }
214 result.push_str(&lines_text.join("\n"));
215 }
216
217 result
218 }
219
220 pub fn copy_selection_with_theme(&mut self, theme_name: &str) {
225 let has_selection = {
227 let state = self.active_state();
228 state
229 .cursors
230 .iter()
231 .any(|(_, cursor)| cursor.selection_range().is_some())
232 };
233
234 if !has_selection {
235 self.status_message = Some(t!("clipboard.no_selection").to_string());
236 return;
237 }
238
239 if theme_name.is_empty() {
241 self.start_copy_with_formatting_prompt();
242 return;
243 }
244 use crate::services::styled_html::render_styled_html;
245
246 let theme = match self.theme_registry.get_cloned(theme_name) {
248 Some(t) => t,
249 None => {
250 self.status_message = Some(format!("Theme '{}' not found", theme_name));
251 return;
252 }
253 };
254
255 let ranges: Vec<_> = {
257 let state = self.active_state();
258 state
259 .cursors
260 .iter()
261 .filter_map(|(_, cursor)| cursor.selection_range())
262 .collect()
263 };
264
265 if ranges.is_empty() {
266 self.status_message = Some(t!("clipboard.no_selection").to_string());
267 return;
268 }
269
270 let min_offset = ranges.iter().map(|r| r.start).min().unwrap_or(0);
272 let max_offset = ranges.iter().map(|r| r.end).max().unwrap_or(0);
273
274 let (text, highlight_spans) = {
276 let state = self.active_state_mut();
277
278 let mut text = String::new();
280 for range in &ranges {
281 if !text.is_empty() {
282 text.push('\n');
283 }
284 let range_text = state.get_text_range(range.start, range.end);
285 text.push_str(&range_text);
286 }
287
288 if text.is_empty() {
289 (text, Vec::new())
290 } else {
291 let highlight_spans = state.highlighter.highlight_viewport(
293 &state.buffer,
294 min_offset,
295 max_offset,
296 &theme,
297 0, );
299 (text, highlight_spans)
300 }
301 };
302
303 if text.is_empty() {
304 self.status_message = Some(t!("clipboard.no_text").to_string());
305 return;
306 }
307
308 let adjusted_spans: Vec<_> = if ranges.len() == 1 {
310 let base_offset = ranges[0].start;
311 highlight_spans
312 .into_iter()
313 .filter_map(|span| {
314 if span.range.end <= base_offset || span.range.start >= ranges[0].end {
315 return None;
316 }
317 let start = span.range.start.saturating_sub(base_offset);
318 let end = (span.range.end - base_offset).min(text.len());
319 if start < end {
320 Some(crate::primitives::highlighter::HighlightSpan {
321 range: start..end,
322 color: span.color,
323 })
324 } else {
325 None
326 }
327 })
328 .collect()
329 } else {
330 Vec::new()
331 };
332
333 let html = render_styled_html(&text, &adjusted_spans, &theme);
335
336 if self.clipboard.copy_html(&html, &text) {
338 self.status_message =
339 Some(t!("clipboard.copied_with_theme", theme = theme_name).to_string());
340 } else {
341 self.clipboard.copy(text);
342 self.status_message = Some(t!("clipboard.copied_plain").to_string());
343 }
344 }
345
346 fn start_copy_with_formatting_prompt(&mut self) {
348 use crate::view::prompt::PromptType;
349
350 let available_themes = self.theme_registry.list();
351 let current_theme_name = &self.theme.name;
352
353 let current_index = available_themes
355 .iter()
356 .position(|info| info.name == *current_theme_name)
357 .unwrap_or(0);
358
359 let suggestions: Vec<crate::input::commands::Suggestion> = available_themes
360 .iter()
361 .map(|info| {
362 let is_current = info.name == *current_theme_name;
363 let description = match (is_current, info.pack.is_empty()) {
364 (true, true) => Some("(current)".to_string()),
365 (true, false) => Some(format!("{} (current)", info.pack)),
366 (false, true) => None,
367 (false, false) => Some(info.pack.clone()),
368 };
369 crate::input::commands::Suggestion {
370 text: info.name.clone(),
371 description,
372 value: Some(info.name.clone()),
373 disabled: false,
374 keybinding: None,
375 source: None,
376 }
377 })
378 .collect();
379
380 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
381 "Copy with theme: ".to_string(),
382 PromptType::CopyWithFormattingTheme,
383 suggestions,
384 ));
385
386 if let Some(prompt) = self.prompt.as_mut() {
387 if !prompt.suggestions.is_empty() {
388 prompt.selected_suggestion = Some(current_index);
389 prompt.input = current_theme_name.to_string();
390 prompt.cursor_pos = prompt.input.len();
391 }
392 }
393 }
394
395 pub fn cut_selection(&mut self) {
399 let has_selection = {
401 let state = self.active_state();
402 state
403 .cursors
404 .iter()
405 .any(|(_, cursor)| cursor.selection_range().is_some())
406 };
407
408 self.copy_selection();
410
411 if has_selection {
412 let mut deletions: Vec<_> = {
415 let state = self.active_state();
416 state
417 .cursors
418 .iter()
419 .filter_map(|(_, c)| c.selection_range())
420 .collect()
421 };
422 deletions.sort_by_key(|r| r.start);
424
425 let state = self.active_state_mut();
426 let primary_id = state.cursors.primary_id();
427 let events: Vec<_> = deletions
428 .iter()
429 .rev()
430 .map(|range| {
431 let deleted_text = state.get_text_range(range.start, range.end);
432 Event::Delete {
433 range: range.clone(),
434 deleted_text,
435 cursor_id: primary_id,
436 }
437 })
438 .collect();
439
440 for event in events {
441 self.active_event_log_mut().append(event.clone());
442 self.apply_event_to_active_buffer(&event);
443 }
444
445 if !deletions.is_empty() {
446 self.status_message = Some(t!("clipboard.cut").to_string());
447 }
448 } else {
449 let estimated_line_length = 80;
451
452 let mut deletions: Vec<_> = {
455 let state = self.active_state_mut();
456 let positions: Vec<_> = state.cursors.iter().map(|(_, c)| c.position).collect();
457
458 positions
459 .into_iter()
460 .filter_map(|pos| {
461 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
462 let line_start = iter.current_position();
463 iter.next_line().map(|(_start, content)| {
464 let line_end = line_start + content.len();
465 line_start..line_end
466 })
467 })
468 .collect()
469 };
470 deletions.sort_by_key(|r| r.start);
472
473 let state = self.active_state_mut();
474 let primary_id = state.cursors.primary_id();
475 let events: Vec<_> = deletions
476 .iter()
477 .rev()
478 .map(|range| {
479 let deleted_text = state.get_text_range(range.start, range.end);
480 Event::Delete {
481 range: range.clone(),
482 deleted_text,
483 cursor_id: primary_id,
484 }
485 })
486 .collect();
487
488 for event in events {
489 self.active_event_log_mut().append(event.clone());
490 self.apply_event_to_active_buffer(&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 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 state = self.active_state();
555 let mut cursor_data: Vec<_> = state
556 .cursors
557 .iter()
558 .map(|(cursor_id, cursor)| {
559 let selection = cursor.selection_range();
560 let insert_position = selection
561 .as_ref()
562 .map(|r| r.start)
563 .unwrap_or(cursor.position);
564 (cursor_id, selection, insert_position)
565 })
566 .collect();
567 cursor_data.sort_by_key(|(_, _, pos)| std::cmp::Reverse(*pos));
568
569 let cursor_data_with_text: Vec<_> = {
571 let state = self.active_state_mut();
572 cursor_data
573 .into_iter()
574 .map(|(cursor_id, selection, insert_position)| {
575 let deleted_text = selection
576 .as_ref()
577 .map(|r| state.get_text_range(r.start, r.end));
578 (cursor_id, selection, insert_position, deleted_text)
579 })
580 .collect()
581 };
582
583 for (cursor_id, selection, insert_position, deleted_text) in cursor_data_with_text {
585 if let (Some(range), Some(text)) = (selection, deleted_text) {
586 events.push(Event::Delete {
587 range,
588 deleted_text: text,
589 cursor_id,
590 });
591 }
592 events.push(Event::Insert {
593 position: insert_position,
594 text: paste_text.clone(),
595 cursor_id,
596 });
597 }
598
599 if events.len() > 1 {
601 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, "Paste".to_string()) {
603 self.active_event_log_mut().append(bulk_edit);
604 }
605 } else if let Some(event) = events.into_iter().next() {
606 self.active_event_log_mut().append(event.clone());
607 self.apply_event_to_active_buffer(&event);
608 }
609
610 self.status_message = Some(t!("clipboard.pasted").to_string());
611 }
612
613 #[doc(hidden)]
617 pub fn set_clipboard_for_test(&mut self, text: String) {
618 self.clipboard.set_internal(text);
619 self.clipboard.set_internal_only(true);
620 }
621
622 #[doc(hidden)]
625 pub fn paste_for_test(&mut self) {
626 let paste_text = match self.clipboard.paste_internal() {
628 Some(text) => text,
629 None => return,
630 };
631
632 self.paste_text(paste_text);
634 }
635
636 #[doc(hidden)]
639 pub fn clipboard_content_for_test(&self) -> String {
640 self.clipboard.get_internal().to_string()
641 }
642
643 pub fn add_cursor_at_next_match(&mut self) {
646 let state = self.active_state_mut();
647 match add_cursor_at_next_match(state) {
648 AddCursorResult::Success {
649 cursor,
650 total_cursors,
651 } => {
652 let next_id = CursorId(self.active_state().cursors.count());
654 let event = Event::AddCursor {
655 cursor_id: next_id,
656 position: cursor.position,
657 anchor: cursor.anchor,
658 };
659
660 self.active_event_log_mut().append(event.clone());
662 self.apply_event_to_active_buffer(&event);
663
664 self.status_message =
665 Some(t!("clipboard.added_cursor_match", count = total_cursors).to_string());
666 }
667 AddCursorResult::WordSelected {
668 word_start,
669 word_end,
670 } => {
671 let primary_id = self.active_state().cursors.primary_id();
673 let primary = self.active_state().cursors.primary();
674 let event = Event::MoveCursor {
675 cursor_id: primary_id,
676 old_position: primary.position,
677 new_position: word_end,
678 old_anchor: primary.anchor,
679 new_anchor: Some(word_start),
680 old_sticky_column: primary.sticky_column,
681 new_sticky_column: 0,
682 };
683
684 self.active_event_log_mut().append(event.clone());
686 self.apply_event_to_active_buffer(&event);
687 }
688 AddCursorResult::Failed { message } => {
689 self.status_message = Some(message);
690 }
691 }
692 }
693
694 pub fn add_cursor_above(&mut self) {
696 let state = self.active_state_mut();
697 match add_cursor_above(state) {
698 AddCursorResult::Success {
699 cursor,
700 total_cursors,
701 } => {
702 let next_id = CursorId(self.active_state().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 state = self.active_state_mut();
727 match add_cursor_below(state) {
728 AddCursorResult::Success {
729 cursor,
730 total_cursors,
731 } => {
732 let next_id = CursorId(self.active_state().cursors.count());
734 let event = Event::AddCursor {
735 cursor_id: next_id,
736 position: cursor.position,
737 anchor: cursor.anchor,
738 };
739
740 self.active_event_log_mut().append(event.clone());
742 self.apply_event_to_active_buffer(&event);
743
744 self.status_message =
745 Some(t!("clipboard.added_cursor_below", count = total_cursors).to_string());
746 }
747 AddCursorResult::Failed { message } => {
748 self.status_message = Some(message);
749 }
750 AddCursorResult::WordSelected { .. } => unreachable!(),
751 }
752 }
753
754 pub fn yank_word_forward(&mut self) {
760 let ranges: Vec<_> = {
761 let state = self.active_state();
762 state
763 .cursors
764 .iter()
765 .filter_map(|(_, cursor)| {
766 let start = cursor.position;
767 let end = find_word_start_right(&state.buffer, start);
768 if end > start {
769 Some(start..end)
770 } else {
771 None
772 }
773 })
774 .collect()
775 };
776
777 if ranges.is_empty() {
778 return;
779 }
780
781 let mut text = String::new();
783 let state = self.active_state_mut();
784 for range in ranges {
785 if !text.is_empty() {
786 text.push('\n');
787 }
788 let range_text = state.get_text_range(range.start, range.end);
789 text.push_str(&range_text);
790 }
791
792 if !text.is_empty() {
793 let len = text.len();
794 self.clipboard.copy(text);
795 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
796 }
797 }
798
799 pub fn yank_word_backward(&mut self) {
801 let ranges: Vec<_> = {
802 let state = self.active_state();
803 state
804 .cursors
805 .iter()
806 .filter_map(|(_, cursor)| {
807 let end = cursor.position;
808 let start = find_word_start_left(&state.buffer, end);
809 if start < end {
810 Some(start..end)
811 } else {
812 None
813 }
814 })
815 .collect()
816 };
817
818 if ranges.is_empty() {
819 return;
820 }
821
822 let mut text = String::new();
823 let state = self.active_state_mut();
824 for range in ranges {
825 if !text.is_empty() {
826 text.push('\n');
827 }
828 let range_text = state.get_text_range(range.start, range.end);
829 text.push_str(&range_text);
830 }
831
832 if !text.is_empty() {
833 let len = text.len();
834 self.clipboard.copy(text);
835 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
836 }
837 }
838
839 pub fn yank_to_line_end(&mut self) {
841 let estimated_line_length = 80;
842
843 let cursor_positions: Vec<_> = {
845 let state = self.active_state();
846 state
847 .cursors
848 .iter()
849 .map(|(_, cursor)| cursor.position)
850 .collect()
851 };
852
853 let state = self.active_state_mut();
855 let mut ranges = Vec::new();
856 for pos in cursor_positions {
857 let mut iter = state.buffer.line_iterator(pos, estimated_line_length);
858 let line_start = iter.current_position();
859 if let Some((_start, content)) = iter.next_line() {
860 let content_len = content.trim_end_matches(&['\n', '\r'][..]).len();
862 let line_end = line_start + content_len;
863 if pos < line_end {
864 ranges.push(pos..line_end);
865 }
866 }
867 }
868
869 if ranges.is_empty() {
870 return;
871 }
872
873 let mut text = String::new();
874 for range in ranges {
875 if !text.is_empty() {
876 text.push('\n');
877 }
878 let range_text = state.get_text_range(range.start, range.end);
879 text.push_str(&range_text);
880 }
881
882 if !text.is_empty() {
883 let len = text.len();
884 self.clipboard.copy(text);
885 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
886 }
887 }
888
889 pub fn yank_to_line_start(&mut self) {
891 let estimated_line_length = 80;
892
893 let cursor_positions: Vec<_> = {
895 let state = self.active_state();
896 state
897 .cursors
898 .iter()
899 .map(|(_, cursor)| cursor.position)
900 .collect()
901 };
902
903 let state = self.active_state_mut();
905 let mut ranges = Vec::new();
906 for pos in cursor_positions {
907 let iter = state.buffer.line_iterator(pos, estimated_line_length);
908 let line_start = iter.current_position();
909 if pos > line_start {
910 ranges.push(line_start..pos);
911 }
912 }
913
914 if ranges.is_empty() {
915 return;
916 }
917
918 let mut text = String::new();
919 for range in ranges {
920 if !text.is_empty() {
921 text.push('\n');
922 }
923 let range_text = state.get_text_range(range.start, range.end);
924 text.push_str(&range_text);
925 }
926
927 if !text.is_empty() {
928 let len = text.len();
929 self.clipboard.copy(text);
930 self.status_message = Some(t!("clipboard.yanked", count = len).to_string());
931 }
932 }
933}