1use iced::Task;
4use iced::widget::operation::{focus, select_all};
5
6use super::command::{
7 Command, CompositeCommand, DeleteCharCommand, DeleteForwardCommand,
8 InsertCharCommand, InsertNewlineCommand, ReplaceTextCommand,
9};
10use super::{
11 ArrowDirection, CURSOR_BLINK_INTERVAL, CodeEditor, ImePreedit, Message,
12};
13
14impl CodeEditor {
15 fn finish_edit_operation(&mut self) {
28 self.reset_cursor_blink();
29 self.refresh_search_matches_if_needed();
30 self.buffer_revision = self.buffer_revision.wrapping_add(1);
33 *self.visual_lines_cache.borrow_mut() = None;
34 self.content_cache.clear();
35 self.overlay_cache.clear();
36 }
37
38 fn finish_navigation_operation(&mut self) {
46 self.reset_cursor_blink();
47 self.overlay_cache.clear();
48 }
49
50 fn ensure_grouping_started(&mut self, label: &str) {
59 if !self.is_grouping {
60 self.history.begin_group(label);
61 self.is_grouping = true;
62 }
63 }
64
65 fn end_grouping_if_active(&mut self) {
71 if self.is_grouping {
72 self.history.end_group();
73 self.is_grouping = false;
74 }
75 }
76
77 fn delete_selection_if_present(&mut self) -> bool {
83 if self.selection_start.is_some() && self.selection_end.is_some() {
84 self.delete_selection();
85 self.finish_edit_operation();
86 true
87 } else {
88 false
89 }
90 }
91
92 fn handle_character_input_msg(&mut self, ch: char) -> Task<Message> {
110 if !self.has_focus() {
112 return Task::none();
113 }
114
115 self.ensure_grouping_started("Typing");
117
118 let (line, col) = self.cursor;
119 let mut cmd = InsertCharCommand::new(line, col, ch, self.cursor);
120 cmd.execute(&mut self.buffer, &mut self.cursor);
121 self.history.push(Box::new(cmd));
122
123 self.finish_edit_operation();
124 Task::none()
125 }
126
127 fn handle_tab(&mut self) -> Task<Message> {
133 self.ensure_grouping_started("Tab");
136
137 let (line, col) = self.cursor;
138 for i in 0..4 {
140 let current_col = col + i;
141 let mut cmd = InsertCharCommand::new(
142 line,
143 current_col,
144 ' ',
145 (line, current_col),
146 );
147 cmd.execute(&mut self.buffer, &mut self.cursor);
148 self.history.push(Box::new(cmd));
149 }
150
151 self.finish_navigation_operation();
152 Task::none()
153 }
154
155 fn handle_focus_navigation_tab(&mut self) -> Task<Message> {
161 if !self.search_state.is_open {
163 self.has_canvas_focus = false;
165 self.show_cursor = false;
166
167 Task::none()
171 } else {
172 Task::none()
173 }
174 }
175
176 fn handle_focus_navigation_shift_tab(&mut self) -> Task<Message> {
182 if !self.search_state.is_open {
184 self.has_canvas_focus = false;
186 self.show_cursor = false;
187
188 Task::none()
192 } else {
193 Task::none()
194 }
195 }
196
197 fn handle_enter(&mut self) -> Task<Message> {
203 self.end_grouping_if_active();
205
206 let (line, col) = self.cursor;
207 let mut cmd = InsertNewlineCommand::new(line, col, self.cursor);
208 cmd.execute(&mut self.buffer, &mut self.cursor);
209 self.history.push(Box::new(cmd));
210
211 self.finish_edit_operation();
212 self.scroll_to_cursor()
213 }
214
215 fn handle_backspace(&mut self) -> Task<Message> {
228 self.end_grouping_if_active();
230
231 if self.delete_selection_if_present() {
233 return self.scroll_to_cursor();
234 }
235
236 let (line, col) = self.cursor;
238 let mut cmd =
239 DeleteCharCommand::new(&self.buffer, line, col, self.cursor);
240 cmd.execute(&mut self.buffer, &mut self.cursor);
241 self.history.push(Box::new(cmd));
242
243 self.finish_edit_operation();
244 self.scroll_to_cursor()
245 }
246
247 fn handle_delete(&mut self) -> Task<Message> {
256 self.end_grouping_if_active();
258
259 if self.delete_selection_if_present() {
261 return self.scroll_to_cursor();
262 }
263
264 let (line, col) = self.cursor;
266 let mut cmd =
267 DeleteForwardCommand::new(&self.buffer, line, col, self.cursor);
268 cmd.execute(&mut self.buffer, &mut self.cursor);
269 self.history.push(Box::new(cmd));
270
271 self.finish_edit_operation();
272 Task::none()
273 }
274
275 fn handle_delete_selection(&mut self) -> Task<Message> {
283 self.end_grouping_if_active();
285
286 if self.selection_start.is_some() && self.selection_end.is_some() {
287 self.delete_selection();
288 self.finish_edit_operation();
289 self.scroll_to_cursor()
290 } else {
291 Task::none()
292 }
293 }
294
295 fn handle_arrow_key(
310 &mut self,
311 direction: ArrowDirection,
312 shift_pressed: bool,
313 ) -> Task<Message> {
314 self.end_grouping_if_active();
316
317 if shift_pressed {
318 if self.selection_start.is_none() {
320 self.selection_start = Some(self.cursor);
321 }
322 self.move_cursor(direction);
323 self.selection_end = Some(self.cursor);
324 } else {
325 self.clear_selection();
327 self.move_cursor(direction);
328 }
329 self.finish_navigation_operation();
330 self.scroll_to_cursor()
331 }
332
333 fn handle_home(&mut self, shift_pressed: bool) -> Task<Message> {
345 if shift_pressed {
346 if self.selection_start.is_none() {
348 self.selection_start = Some(self.cursor);
349 }
350 self.cursor.1 = 0; self.selection_end = Some(self.cursor);
352 } else {
353 self.clear_selection();
355 self.cursor.1 = 0;
356 }
357 self.finish_navigation_operation();
358 Task::none()
359 }
360
361 fn handle_end(&mut self, shift_pressed: bool) -> Task<Message> {
373 let line = self.cursor.0;
374 let line_len = self.buffer.line_len(line);
375
376 if shift_pressed {
377 if self.selection_start.is_none() {
379 self.selection_start = Some(self.cursor);
380 }
381 self.cursor.1 = line_len; self.selection_end = Some(self.cursor);
383 } else {
384 self.clear_selection();
386 self.cursor.1 = line_len;
387 }
388 self.finish_navigation_operation();
389 Task::none()
390 }
391
392 fn handle_ctrl_home(&mut self) -> Task<Message> {
400 self.clear_selection();
402 self.cursor = (0, 0);
403 self.finish_navigation_operation();
404 self.scroll_to_cursor()
405 }
406
407 fn handle_ctrl_end(&mut self) -> Task<Message> {
415 self.clear_selection();
417 let last_line = self.buffer.line_count().saturating_sub(1);
418 let last_col = self.buffer.line_len(last_line);
419 self.cursor = (last_line, last_col);
420 self.finish_navigation_operation();
421 self.scroll_to_cursor()
422 }
423
424 fn handle_page_up(&mut self) -> Task<Message> {
432 self.page_up();
433 self.finish_navigation_operation();
434 self.scroll_to_cursor()
435 }
436
437 fn handle_page_down(&mut self) -> Task<Message> {
445 self.page_down();
446 self.finish_navigation_operation();
447 self.scroll_to_cursor()
448 }
449
450 fn handle_mouse_click_msg(&mut self, point: iced::Point) -> Task<Message> {
466 self.request_focus();
468
469 self.has_canvas_focus = true;
471
472 self.end_grouping_if_active();
474
475 self.handle_mouse_click(point);
476 self.reset_cursor_blink();
477 self.clear_selection();
479 self.is_dragging = true;
480 self.selection_start = Some(self.cursor);
481
482 self.show_cursor = true;
484
485 Task::none()
486 }
487
488 fn handle_mouse_drag_msg(&mut self, point: iced::Point) -> Task<Message> {
498 if self.is_dragging {
499 let before_cursor = self.cursor;
500 let before_selection_end = self.selection_end;
501 self.handle_mouse_drag(point);
502 if self.cursor != before_cursor
503 || self.selection_end != before_selection_end
504 {
505 self.overlay_cache.clear();
508 }
509 }
510 Task::none()
511 }
512
513 fn handle_mouse_release_msg(&mut self) -> Task<Message> {
519 self.is_dragging = false;
520 Task::none()
521 }
522
523 fn handle_paste_msg(&mut self, text: &str) -> Task<Message> {
540 self.end_grouping_if_active();
542
543 if text.is_empty() {
545 iced::clipboard::read().and_then(|clipboard_text| {
547 Task::done(Message::Paste(clipboard_text))
548 })
549 } else {
550 self.paste_text(text);
552 self.finish_edit_operation();
553 self.scroll_to_cursor()
554 }
555 }
556
557 fn handle_undo_msg(&mut self) -> Task<Message> {
567 self.end_grouping_if_active();
569
570 if self.history.undo(&mut self.buffer, &mut self.cursor) {
571 self.clear_selection();
572 self.finish_edit_operation();
573 self.scroll_to_cursor()
574 } else {
575 Task::none()
576 }
577 }
578
579 fn handle_redo_msg(&mut self) -> Task<Message> {
585 if self.history.redo(&mut self.buffer, &mut self.cursor) {
586 self.clear_selection();
587 self.finish_edit_operation();
588 self.scroll_to_cursor()
589 } else {
590 Task::none()
591 }
592 }
593
594 fn handle_open_search_msg(&mut self) -> Task<Message> {
604 self.search_state.open_search();
605 self.overlay_cache.clear();
606
607 Task::batch([
609 focus(self.search_state.search_input_id.clone()),
610 select_all(self.search_state.search_input_id.clone()),
611 ])
612 }
613
614 fn handle_open_search_replace_msg(&mut self) -> Task<Message> {
620 self.search_state.open_replace();
621 self.overlay_cache.clear();
622
623 Task::batch([
625 focus(self.search_state.search_input_id.clone()),
626 select_all(self.search_state.search_input_id.clone()),
627 ])
628 }
629
630 fn handle_close_search_msg(&mut self) -> Task<Message> {
636 self.search_state.close();
637 self.overlay_cache.clear();
638 Task::none()
639 }
640
641 fn handle_search_query_changed_msg(
651 &mut self,
652 query: &str,
653 ) -> Task<Message> {
654 self.search_state.set_query(query.to_string(), &self.buffer);
655 self.overlay_cache.clear();
656
657 if let Some(match_pos) = self.search_state.current_match() {
659 self.cursor = (match_pos.line, match_pos.col);
660 self.clear_selection();
661 return self.scroll_to_cursor();
662 }
663 Task::none()
664 }
665
666 fn handle_replace_query_changed_msg(
676 &mut self,
677 replace_text: &str,
678 ) -> Task<Message> {
679 self.search_state.set_replace_with(replace_text.to_string());
680 Task::none()
681 }
682
683 fn handle_toggle_case_sensitive_msg(&mut self) -> Task<Message> {
689 self.search_state.toggle_case_sensitive(&self.buffer);
690 self.overlay_cache.clear();
691
692 if let Some(match_pos) = self.search_state.current_match() {
694 self.cursor = (match_pos.line, match_pos.col);
695 self.clear_selection();
696 return self.scroll_to_cursor();
697 }
698 Task::none()
699 }
700
701 fn handle_find_next_msg(&mut self) -> Task<Message> {
707 if !self.search_state.matches.is_empty() {
708 self.search_state.next_match();
709 if let Some(match_pos) = self.search_state.current_match() {
710 self.cursor = (match_pos.line, match_pos.col);
711 self.clear_selection();
712 self.overlay_cache.clear();
713 return self.scroll_to_cursor();
714 }
715 }
716 Task::none()
717 }
718
719 fn handle_find_previous_msg(&mut self) -> Task<Message> {
725 if !self.search_state.matches.is_empty() {
726 self.search_state.previous_match();
727 if let Some(match_pos) = self.search_state.current_match() {
728 self.cursor = (match_pos.line, match_pos.col);
729 self.clear_selection();
730 self.overlay_cache.clear();
731 return self.scroll_to_cursor();
732 }
733 }
734 Task::none()
735 }
736
737 fn handle_replace_next_msg(&mut self) -> Task<Message> {
743 if let Some(match_pos) = self.search_state.current_match() {
745 let query_len = self.search_state.query.chars().count();
746 let replace_text = self.search_state.replace_with.clone();
747
748 let mut cmd = ReplaceTextCommand::new(
750 &self.buffer,
751 (match_pos.line, match_pos.col),
752 query_len,
753 replace_text,
754 self.cursor,
755 );
756 cmd.execute(&mut self.buffer, &mut self.cursor);
757 self.history.push(Box::new(cmd));
758
759 self.search_state.update_matches(&self.buffer);
761
762 if !self.search_state.matches.is_empty()
764 && let Some(next_match) = self.search_state.current_match()
765 {
766 self.cursor = (next_match.line, next_match.col);
767 }
768
769 self.clear_selection();
770 self.finish_edit_operation();
771 return self.scroll_to_cursor();
772 }
773 Task::none()
774 }
775
776 fn handle_replace_all_msg(&mut self) -> Task<Message> {
782 let all_matches = super::search::find_matches(
784 &self.buffer,
785 &self.search_state.query,
786 self.search_state.case_sensitive,
787 None, );
789
790 if !all_matches.is_empty() {
791 let query_len = self.search_state.query.chars().count();
792 let replace_text = self.search_state.replace_with.clone();
793
794 let mut composite =
796 CompositeCommand::new("Replace All".to_string());
797
798 for match_pos in all_matches.iter().rev() {
800 let cmd = ReplaceTextCommand::new(
801 &self.buffer,
802 (match_pos.line, match_pos.col),
803 query_len,
804 replace_text.clone(),
805 self.cursor,
806 );
807 composite.add(Box::new(cmd));
808 }
809
810 composite.execute(&mut self.buffer, &mut self.cursor);
812 self.history.push(Box::new(composite));
813
814 self.search_state.update_matches(&self.buffer);
816 self.clear_selection();
817 self.finish_edit_operation();
818 self.scroll_to_cursor()
819 } else {
820 Task::none()
821 }
822 }
823
824 fn handle_search_dialog_tab_msg(&mut self) -> Task<Message> {
830 self.search_state.focus_next_field();
832
833 match self.search_state.focused_field {
835 crate::canvas_editor::search::SearchFocusedField::Search => {
836 focus(self.search_state.search_input_id.clone())
837 }
838 crate::canvas_editor::search::SearchFocusedField::Replace => {
839 focus(self.search_state.replace_input_id.clone())
840 }
841 }
842 }
843
844 fn handle_search_dialog_shift_tab_msg(&mut self) -> Task<Message> {
850 self.search_state.focus_previous_field();
852
853 match self.search_state.focused_field {
855 crate::canvas_editor::search::SearchFocusedField::Search => {
856 focus(self.search_state.search_input_id.clone())
857 }
858 crate::canvas_editor::search::SearchFocusedField::Replace => {
859 focus(self.search_state.replace_input_id.clone())
860 }
861 }
862 }
863
864 fn handle_canvas_focus_gained_msg(&mut self) -> Task<Message> {
874 self.has_canvas_focus = true;
875 self.focus_locked = false; self.show_cursor = true;
877 self.reset_cursor_blink();
878 self.overlay_cache.clear();
879 Task::none()
880 }
881
882 fn handle_canvas_focus_lost_msg(&mut self) -> Task<Message> {
888 self.has_canvas_focus = false;
889 self.focus_locked = true; self.show_cursor = false;
891 self.ime_preedit = None;
892 self.overlay_cache.clear();
893 Task::none()
894 }
895
896 fn handle_ime_opened_msg(&mut self) -> Task<Message> {
904 self.ime_preedit = None;
905 self.overlay_cache.clear();
906 Task::none()
907 }
908
909 fn handle_ime_preedit_msg(
922 &mut self,
923 content: &str,
924 selection: &Option<std::ops::Range<usize>>,
925 ) -> Task<Message> {
926 if content.is_empty() {
927 self.ime_preedit = None;
928 } else {
929 self.ime_preedit = Some(ImePreedit {
930 content: content.to_string(),
931 selection: selection.clone(),
932 });
933 }
934
935 self.overlay_cache.clear();
936 Task::none()
937 }
938
939 fn handle_ime_commit_msg(&mut self, text: &str) -> Task<Message> {
951 self.ime_preedit = None;
952
953 if text.is_empty() {
954 self.overlay_cache.clear();
955 return Task::none();
956 }
957
958 self.ensure_grouping_started("Typing");
959
960 self.paste_text(text);
961 self.finish_edit_operation();
962 self.scroll_to_cursor()
963 }
964
965 fn handle_ime_closed_msg(&mut self) -> Task<Message> {
973 self.ime_preedit = None;
974 self.overlay_cache.clear();
975 Task::none()
976 }
977
978 fn handle_tick_msg(&mut self) -> Task<Message> {
990 if self.has_focus()
992 && self.last_blink.elapsed() >= CURSOR_BLINK_INTERVAL
993 {
994 self.cursor_visible = !self.cursor_visible;
995 self.last_blink = super::Instant::now();
996 self.overlay_cache.clear();
997 }
998
999 if !self.has_focus() {
1001 self.show_cursor = false;
1002 }
1003
1004 Task::none()
1005 }
1006
1007 fn handle_scrolled_msg(
1021 &mut self,
1022 viewport: iced::widget::scrollable::Viewport,
1023 ) -> Task<Message> {
1024 let new_scroll = viewport.absolute_offset().y;
1033 let new_height = viewport.bounds().height;
1034 let new_width = viewport.bounds().width;
1035 let scroll_changed = (self.viewport_scroll - new_scroll).abs() > 0.1;
1036 let visible_lines_count =
1037 (new_height / self.line_height).ceil() as usize + 2;
1038 let first_visible_line =
1039 (new_scroll / self.line_height).floor() as usize;
1040 let last_visible_line = first_visible_line + visible_lines_count;
1041 let margin = visible_lines_count
1042 * crate::canvas_editor::CACHE_WINDOW_MARGIN_MULTIPLIER;
1043 let window_start = first_visible_line.saturating_sub(margin);
1044 let window_end = last_visible_line + margin;
1045 let need_rewindow =
1049 if self.cache_window_end_line > self.cache_window_start_line {
1050 let lower_boundary_trigger = self.cache_window_start_line > 0
1051 && first_visible_line
1052 < self
1053 .cache_window_start_line
1054 .saturating_add(visible_lines_count / 2);
1055 let upper_boundary_trigger = last_visible_line
1056 > self
1057 .cache_window_end_line
1058 .saturating_sub(visible_lines_count / 2);
1059 lower_boundary_trigger || upper_boundary_trigger
1060 } else {
1061 true
1062 };
1063 if (self.viewport_height - new_height).abs() > 1.0
1066 || (self.viewport_width - new_width).abs() > 1.0
1067 || (scroll_changed && need_rewindow)
1068 {
1069 self.cache_window_start_line = window_start;
1070 self.cache_window_end_line = window_end;
1071 self.last_first_visible_line = first_visible_line;
1072 self.content_cache.clear();
1073 self.overlay_cache.clear();
1074 }
1075 self.viewport_scroll = new_scroll;
1076 self.viewport_height = new_height;
1077 self.viewport_width = new_width;
1078 Task::none()
1079 }
1080
1081 pub fn update(&mut self, message: &Message) -> Task<Message> {
1094 match message {
1095 Message::CharacterInput(ch) => self.handle_character_input_msg(*ch),
1097 Message::Tab => self.handle_tab(),
1098 Message::Enter => self.handle_enter(),
1099
1100 Message::Backspace => self.handle_backspace(),
1102 Message::Delete => self.handle_delete(),
1103 Message::DeleteSelection => self.handle_delete_selection(),
1104
1105 Message::ArrowKey(direction, shift) => {
1107 self.handle_arrow_key(*direction, *shift)
1108 }
1109 Message::Home(shift) => self.handle_home(*shift),
1110 Message::End(shift) => self.handle_end(*shift),
1111 Message::CtrlHome => self.handle_ctrl_home(),
1112 Message::CtrlEnd => self.handle_ctrl_end(),
1113 Message::PageUp => self.handle_page_up(),
1114 Message::PageDown => self.handle_page_down(),
1115
1116 Message::MouseClick(point) => self.handle_mouse_click_msg(*point),
1118 Message::MouseDrag(point) => self.handle_mouse_drag_msg(*point),
1119 Message::MouseRelease => self.handle_mouse_release_msg(),
1120
1121 Message::Copy => self.copy_selection(),
1123 Message::Paste(text) => self.handle_paste_msg(text),
1124
1125 Message::Undo => self.handle_undo_msg(),
1127 Message::Redo => self.handle_redo_msg(),
1128
1129 Message::OpenSearch => self.handle_open_search_msg(),
1131 Message::OpenSearchReplace => self.handle_open_search_replace_msg(),
1132 Message::CloseSearch => self.handle_close_search_msg(),
1133 Message::SearchQueryChanged(query) => {
1134 self.handle_search_query_changed_msg(query)
1135 }
1136 Message::ReplaceQueryChanged(text) => {
1137 self.handle_replace_query_changed_msg(text)
1138 }
1139 Message::ToggleCaseSensitive => {
1140 self.handle_toggle_case_sensitive_msg()
1141 }
1142 Message::FindNext => self.handle_find_next_msg(),
1143 Message::FindPrevious => self.handle_find_previous_msg(),
1144 Message::ReplaceNext => self.handle_replace_next_msg(),
1145 Message::ReplaceAll => self.handle_replace_all_msg(),
1146 Message::SearchDialogTab => self.handle_search_dialog_tab_msg(),
1147 Message::SearchDialogShiftTab => {
1148 self.handle_search_dialog_shift_tab_msg()
1149 }
1150 Message::FocusNavigationTab => self.handle_focus_navigation_tab(),
1151 Message::FocusNavigationShiftTab => {
1152 self.handle_focus_navigation_shift_tab()
1153 }
1154
1155 Message::CanvasFocusGained => self.handle_canvas_focus_gained_msg(),
1157 Message::CanvasFocusLost => self.handle_canvas_focus_lost_msg(),
1158 Message::ImeOpened => self.handle_ime_opened_msg(),
1159 Message::ImePreedit(content, selection) => {
1160 self.handle_ime_preedit_msg(content, selection)
1161 }
1162 Message::ImeCommit(text) => self.handle_ime_commit_msg(text),
1163 Message::ImeClosed => self.handle_ime_closed_msg(),
1164
1165 Message::Tick => self.handle_tick_msg(),
1167 Message::Scrolled(viewport) => self.handle_scrolled_msg(*viewport),
1168 }
1169 }
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175 use crate::canvas_editor::ArrowDirection;
1176
1177 #[test]
1178 fn test_canvas_focus_lost() {
1179 let mut editor = CodeEditor::new("test", "rs");
1180 editor.has_canvas_focus = true;
1181
1182 let _ = editor.update(&Message::CanvasFocusLost);
1183
1184 assert!(!editor.has_canvas_focus);
1185 assert!(!editor.show_cursor);
1186 assert!(editor.focus_locked, "Focus should be locked when lost");
1187 }
1188
1189 #[test]
1190 fn test_canvas_focus_gained_resets_lock() {
1191 let mut editor = CodeEditor::new("test", "rs");
1192 editor.has_canvas_focus = false;
1193 editor.focus_locked = true;
1194
1195 let _ = editor.update(&Message::CanvasFocusGained);
1196
1197 assert!(editor.has_canvas_focus);
1198 assert!(
1199 !editor.focus_locked,
1200 "Focus lock should be reset when focus is gained"
1201 );
1202 }
1203
1204 #[test]
1205 fn test_focus_lock_state() {
1206 let mut editor = CodeEditor::new("test", "rs");
1207
1208 assert!(!editor.focus_locked);
1210
1211 let _ = editor.update(&Message::CanvasFocusLost);
1213 assert!(editor.focus_locked, "Focus should be locked when lost");
1214
1215 editor.request_focus();
1217 let _ = editor.update(&Message::CanvasFocusGained);
1218 assert!(!editor.focus_locked, "Focus should be unlocked when regained");
1219
1220 editor.focus_locked = true;
1222 editor.reset_focus_lock();
1223 assert!(!editor.focus_locked, "Focus lock should be resetable");
1224 }
1225
1226 #[test]
1227 fn test_reset_focus_lock() {
1228 let mut editor = CodeEditor::new("test", "rs");
1229 editor.focus_locked = true;
1230
1231 editor.reset_focus_lock();
1232
1233 assert!(!editor.focus_locked);
1234 }
1235
1236 #[test]
1237 fn test_home_key() {
1238 let mut editor = CodeEditor::new("hello world", "py");
1239 editor.cursor = (0, 5); let _ = editor.update(&Message::Home(false));
1241 assert_eq!(editor.cursor, (0, 0));
1242 }
1243
1244 #[test]
1245 fn test_end_key() {
1246 let mut editor = CodeEditor::new("hello world", "py");
1247 editor.cursor = (0, 0);
1248 let _ = editor.update(&Message::End(false));
1249 assert_eq!(editor.cursor, (0, 11)); }
1251
1252 #[test]
1253 fn test_arrow_key_with_shift_creates_selection() {
1254 let mut editor = CodeEditor::new("hello world", "py");
1255 editor.cursor = (0, 0);
1256
1257 let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, true));
1259 assert!(editor.selection_start.is_some());
1260 assert!(editor.selection_end.is_some());
1261 }
1262
1263 #[test]
1264 fn test_arrow_key_without_shift_clears_selection() {
1265 let mut editor = CodeEditor::new("hello world", "py");
1266 editor.selection_start = Some((0, 0));
1267 editor.selection_end = Some((0, 5));
1268
1269 let _ = editor.update(&Message::ArrowKey(ArrowDirection::Right, false));
1271 assert_eq!(editor.selection_start, None);
1272 assert_eq!(editor.selection_end, None);
1273 }
1274
1275 #[test]
1276 fn test_typing_with_selection() {
1277 let mut editor = CodeEditor::new("hello world", "py");
1278 editor.request_focus();
1280 editor.has_canvas_focus = true;
1281 editor.focus_locked = false;
1282
1283 editor.selection_start = Some((0, 0));
1284 editor.selection_end = Some((0, 5));
1285
1286 let _ = editor.update(&Message::CharacterInput('X'));
1287 assert_eq!(editor.buffer.line(0), "Xhello world");
1290 }
1291
1292 #[test]
1293 fn test_ctrl_home() {
1294 let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1295 editor.cursor = (2, 5); let _ = editor.update(&Message::CtrlHome);
1297 assert_eq!(editor.cursor, (0, 0)); }
1299
1300 #[test]
1301 fn test_ctrl_end() {
1302 let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1303 editor.cursor = (0, 0); let _ = editor.update(&Message::CtrlEnd);
1305 assert_eq!(editor.cursor, (2, 5)); }
1307
1308 #[test]
1309 fn test_ctrl_home_clears_selection() {
1310 let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1311 editor.cursor = (2, 5);
1312 editor.selection_start = Some((0, 0));
1313 editor.selection_end = Some((2, 5));
1314
1315 let _ = editor.update(&Message::CtrlHome);
1316 assert_eq!(editor.cursor, (0, 0));
1317 assert_eq!(editor.selection_start, None);
1318 assert_eq!(editor.selection_end, None);
1319 }
1320
1321 #[test]
1322 fn test_ctrl_end_clears_selection() {
1323 let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1324 editor.cursor = (0, 0);
1325 editor.selection_start = Some((0, 0));
1326 editor.selection_end = Some((1, 3));
1327
1328 let _ = editor.update(&Message::CtrlEnd);
1329 assert_eq!(editor.cursor, (2, 5));
1330 assert_eq!(editor.selection_start, None);
1331 assert_eq!(editor.selection_end, None);
1332 }
1333
1334 #[test]
1335 fn test_scroll_sets_initial_cache_window() {
1336 let content =
1337 (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1338 let mut editor = CodeEditor::new(&content, "py");
1339
1340 let height = 400.0;
1342 let width = 800.0;
1343 let scroll = 0.0;
1344
1345 let visible_lines_count =
1347 (height / editor.line_height).ceil() as usize + 2;
1348 let first_visible_line = (scroll / editor.line_height).floor() as usize;
1349 let last_visible_line = first_visible_line + visible_lines_count;
1350 let margin = visible_lines_count * 2;
1351 let window_start = first_visible_line.saturating_sub(margin);
1352 let window_end = last_visible_line + margin;
1353
1354 editor.viewport_height = height;
1356 editor.viewport_width = width;
1357 editor.viewport_scroll = -1.0;
1358 let scroll_changed = (editor.viewport_scroll - scroll).abs() > 0.1;
1359 let need_rewindow = true;
1360 if (editor.viewport_height - height).abs() > 1.0
1361 || (editor.viewport_width - width).abs() > 1.0
1362 || (scroll_changed && need_rewindow)
1363 {
1364 editor.cache_window_start_line = window_start;
1365 editor.cache_window_end_line = window_end;
1366 editor.last_first_visible_line = first_visible_line;
1367 }
1368 editor.viewport_scroll = scroll;
1369
1370 assert_eq!(editor.last_first_visible_line, first_visible_line);
1371 assert!(editor.cache_window_end_line > editor.cache_window_start_line);
1372 assert_eq!(editor.cache_window_start_line, window_start);
1373 assert_eq!(editor.cache_window_end_line, window_end);
1374 }
1375
1376 #[test]
1377 fn test_small_scroll_keeps_window() {
1378 let content =
1379 (0..200).map(|i| format!("line{}\n", i)).collect::<String>();
1380 let mut editor = CodeEditor::new(&content, "py");
1381 let height = 400.0;
1382 let width = 800.0;
1383 let initial_scroll = 0.0;
1384 let visible_lines_count =
1385 (height / editor.line_height).ceil() as usize + 2;
1386 let first_visible_line =
1387 (initial_scroll / editor.line_height).floor() as usize;
1388 let last_visible_line = first_visible_line + visible_lines_count;
1389 let margin = visible_lines_count * 2;
1390 let window_start = first_visible_line.saturating_sub(margin);
1391 let window_end = last_visible_line + margin;
1392 editor.cache_window_start_line = window_start;
1393 editor.cache_window_end_line = window_end;
1394 editor.viewport_height = height;
1395 editor.viewport_width = width;
1396 editor.viewport_scroll = initial_scroll;
1397
1398 let small_scroll =
1400 editor.line_height * (visible_lines_count as f32 / 4.0);
1401 let first_visible_line2 =
1402 (small_scroll / editor.line_height).floor() as usize;
1403 let last_visible_line2 = first_visible_line2 + visible_lines_count;
1404 let lower_boundary_trigger = editor.cache_window_start_line > 0
1405 && first_visible_line2
1406 < editor
1407 .cache_window_start_line
1408 .saturating_add(visible_lines_count / 2);
1409 let upper_boundary_trigger = last_visible_line2
1410 > editor
1411 .cache_window_end_line
1412 .saturating_sub(visible_lines_count / 2);
1413 let need_rewindow = lower_boundary_trigger || upper_boundary_trigger;
1414
1415 assert!(!need_rewindow, "Small scroll should be inside the window");
1416 assert_eq!(editor.cache_window_start_line, window_start);
1418 assert_eq!(editor.cache_window_end_line, window_end);
1419 }
1420
1421 #[test]
1422 fn test_large_scroll_rewindows() {
1423 let content =
1424 (0..1000).map(|i| format!("line{}\n", i)).collect::<String>();
1425 let mut editor = CodeEditor::new(&content, "py");
1426 let height = 400.0;
1427 let width = 800.0;
1428 let initial_scroll = 0.0;
1429 let visible_lines_count =
1430 (height / editor.line_height).ceil() as usize + 2;
1431 let first_visible_line =
1432 (initial_scroll / editor.line_height).floor() as usize;
1433 let last_visible_line = first_visible_line + visible_lines_count;
1434 let margin = visible_lines_count * 2;
1435 editor.cache_window_start_line =
1436 first_visible_line.saturating_sub(margin);
1437 editor.cache_window_end_line = last_visible_line + margin;
1438 editor.viewport_height = height;
1439 editor.viewport_width = width;
1440 editor.viewport_scroll = initial_scroll;
1441
1442 let large_scroll =
1444 editor.line_height * ((visible_lines_count * 4) as f32);
1445 let first_visible_line2 =
1446 (large_scroll / editor.line_height).floor() as usize;
1447 let last_visible_line2 = first_visible_line2 + visible_lines_count;
1448 let window_start2 = first_visible_line2.saturating_sub(margin);
1449 let window_end2 = last_visible_line2 + margin;
1450 let need_rewindow = first_visible_line2
1451 < editor
1452 .cache_window_start_line
1453 .saturating_add(visible_lines_count / 2)
1454 || last_visible_line2
1455 > editor
1456 .cache_window_end_line
1457 .saturating_sub(visible_lines_count / 2);
1458 assert!(need_rewindow, "Large scroll should trigger window update");
1459
1460 editor.cache_window_start_line = window_start2;
1462 editor.cache_window_end_line = window_end2;
1463 editor.last_first_visible_line = first_visible_line2;
1464
1465 assert_eq!(editor.cache_window_start_line, window_start2);
1466 assert_eq!(editor.cache_window_end_line, window_end2);
1467 assert_eq!(editor.last_first_visible_line, first_visible_line2);
1468 }
1469
1470 #[test]
1471 fn test_delete_selection_message() {
1472 let mut editor = CodeEditor::new("hello world", "py");
1473 editor.cursor = (0, 0);
1474 editor.selection_start = Some((0, 0));
1475 editor.selection_end = Some((0, 5));
1476
1477 let _ = editor.update(&Message::DeleteSelection);
1478 assert_eq!(editor.buffer.line(0), " world");
1479 assert_eq!(editor.cursor, (0, 0));
1480 assert_eq!(editor.selection_start, None);
1481 assert_eq!(editor.selection_end, None);
1482 }
1483
1484 #[test]
1485 fn test_delete_selection_multiline() {
1486 let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1487 editor.cursor = (0, 2);
1488 editor.selection_start = Some((0, 2));
1489 editor.selection_end = Some((2, 2));
1490
1491 let _ = editor.update(&Message::DeleteSelection);
1492 assert_eq!(editor.buffer.line(0), "line3");
1493 assert_eq!(editor.cursor, (0, 2));
1494 assert_eq!(editor.selection_start, None);
1495 }
1496
1497 #[test]
1498 fn test_delete_selection_no_selection() {
1499 let mut editor = CodeEditor::new("hello world", "py");
1500 editor.cursor = (0, 5);
1501
1502 let _ = editor.update(&Message::DeleteSelection);
1503 assert_eq!(editor.buffer.line(0), "hello world");
1505 assert_eq!(editor.cursor, (0, 5));
1506 }
1507
1508 #[test]
1509 #[allow(clippy::unwrap_used)]
1510 fn test_ime_preedit_and_commit_chinese() {
1511 let mut editor = CodeEditor::new("", "py");
1512 let _ = editor.update(&Message::ImeOpened);
1514 assert!(editor.ime_preedit.is_none());
1515
1516 let content = "安全与合规".to_string();
1518 let selection = Some(0..3); let _ = editor
1520 .update(&Message::ImePreedit(content.clone(), selection.clone()));
1521
1522 assert!(editor.ime_preedit.is_some());
1523 assert_eq!(
1524 editor.ime_preedit.as_ref().unwrap().content.clone(),
1525 content
1526 );
1527 assert_eq!(
1528 editor.ime_preedit.as_ref().unwrap().selection.clone(),
1529 selection
1530 );
1531
1532 let _ = editor.update(&Message::ImeCommit("安全与合规".to_string()));
1534 assert!(editor.ime_preedit.is_none());
1535 assert_eq!(editor.buffer.line(0), "安全与合规");
1536 assert_eq!(editor.cursor, (0, "安全与合规".chars().count()));
1537 }
1538
1539 #[test]
1540 fn test_undo_char_insert() {
1541 let mut editor = CodeEditor::new("hello", "py");
1542 editor.request_focus();
1544 editor.has_canvas_focus = true;
1545 editor.focus_locked = false;
1546
1547 editor.cursor = (0, 5);
1548
1549 let _ = editor.update(&Message::CharacterInput('!'));
1551 assert_eq!(editor.buffer.line(0), "hello!");
1552 assert_eq!(editor.cursor, (0, 6));
1553
1554 editor.history.end_group();
1556 let _ = editor.update(&Message::Undo);
1557 assert_eq!(editor.buffer.line(0), "hello");
1558 assert_eq!(editor.cursor, (0, 5));
1559 }
1560
1561 #[test]
1562 fn test_undo_redo_char_insert() {
1563 let mut editor = CodeEditor::new("hello", "py");
1564 editor.request_focus();
1566 editor.has_canvas_focus = true;
1567 editor.focus_locked = false;
1568
1569 editor.cursor = (0, 5);
1570
1571 let _ = editor.update(&Message::CharacterInput('!'));
1573 editor.history.end_group();
1574
1575 let _ = editor.update(&Message::Undo);
1577 assert_eq!(editor.buffer.line(0), "hello");
1578
1579 let _ = editor.update(&Message::Redo);
1581 assert_eq!(editor.buffer.line(0), "hello!");
1582 assert_eq!(editor.cursor, (0, 6));
1583 }
1584
1585 #[test]
1586 fn test_undo_backspace() {
1587 let mut editor = CodeEditor::new("hello", "py");
1588 editor.cursor = (0, 5);
1589
1590 let _ = editor.update(&Message::Backspace);
1592 assert_eq!(editor.buffer.line(0), "hell");
1593 assert_eq!(editor.cursor, (0, 4));
1594
1595 let _ = editor.update(&Message::Undo);
1597 assert_eq!(editor.buffer.line(0), "hello");
1598 assert_eq!(editor.cursor, (0, 5));
1599 }
1600
1601 #[test]
1602 fn test_undo_newline() {
1603 let mut editor = CodeEditor::new("hello world", "py");
1604 editor.cursor = (0, 5);
1605
1606 let _ = editor.update(&Message::Enter);
1608 assert_eq!(editor.buffer.line(0), "hello");
1609 assert_eq!(editor.buffer.line(1), " world");
1610 assert_eq!(editor.cursor, (1, 0));
1611
1612 let _ = editor.update(&Message::Undo);
1614 assert_eq!(editor.buffer.line(0), "hello world");
1615 assert_eq!(editor.cursor, (0, 5));
1616 }
1617
1618 #[test]
1619 fn test_undo_grouped_typing() {
1620 let mut editor = CodeEditor::new("hello", "py");
1621 editor.request_focus();
1623 editor.has_canvas_focus = true;
1624 editor.focus_locked = false;
1625
1626 editor.cursor = (0, 5);
1627
1628 let _ = editor.update(&Message::CharacterInput(' '));
1630 let _ = editor.update(&Message::CharacterInput('w'));
1631 let _ = editor.update(&Message::CharacterInput('o'));
1632 let _ = editor.update(&Message::CharacterInput('r'));
1633 let _ = editor.update(&Message::CharacterInput('l'));
1634 let _ = editor.update(&Message::CharacterInput('d'));
1635
1636 assert_eq!(editor.buffer.line(0), "hello world");
1637
1638 editor.history.end_group();
1640
1641 let _ = editor.update(&Message::Undo);
1643 assert_eq!(editor.buffer.line(0), "hello");
1644 assert_eq!(editor.cursor, (0, 5));
1645 }
1646
1647 #[test]
1648 fn test_navigation_ends_grouping() {
1649 let mut editor = CodeEditor::new("hello", "py");
1650 editor.request_focus();
1652 editor.has_canvas_focus = true;
1653 editor.focus_locked = false;
1654
1655 editor.cursor = (0, 5);
1656
1657 let _ = editor.update(&Message::CharacterInput('!'));
1659 assert!(editor.is_grouping);
1660
1661 let _ = editor.update(&Message::ArrowKey(ArrowDirection::Left, false));
1663 assert!(!editor.is_grouping);
1664
1665 let _ = editor.update(&Message::CharacterInput('?'));
1667 assert!(editor.is_grouping);
1668
1669 editor.history.end_group();
1670
1671 let _ = editor.update(&Message::Undo);
1673 assert_eq!(editor.buffer.line(0), "hello!");
1674
1675 let _ = editor.update(&Message::Undo);
1676 assert_eq!(editor.buffer.line(0), "hello");
1677 }
1678
1679 #[test]
1680 fn test_edit_increments_revision_and_clears_visual_lines_cache() {
1681 let mut editor = CodeEditor::new("hello", "rs");
1682 editor.request_focus();
1683 editor.has_canvas_focus = true;
1684 editor.focus_locked = false;
1685 editor.cursor = (0, 5);
1686
1687 let _ = editor.visual_lines_cached(800.0);
1688 assert!(
1689 editor.visual_lines_cache.borrow().is_some(),
1690 "visual_lines_cached should populate the cache"
1691 );
1692
1693 let previous_revision = editor.buffer_revision;
1694
1695 let _ = editor.update(&Message::CharacterInput('!'));
1696 assert_eq!(
1697 editor.buffer_revision,
1698 previous_revision.wrapping_add(1),
1699 "buffer_revision should change on buffer edits"
1700 );
1701 assert!(
1702 editor.visual_lines_cache.borrow().is_none(),
1703 "buffer edits should invalidate the visual lines cache"
1704 );
1705 }
1706
1707 #[test]
1708 fn test_multiple_undo_redo() {
1709 let mut editor = CodeEditor::new("a", "py");
1710 editor.request_focus();
1712 editor.has_canvas_focus = true;
1713 editor.focus_locked = false;
1714
1715 editor.cursor = (0, 1);
1716
1717 let _ = editor.update(&Message::CharacterInput('b'));
1719 editor.history.end_group();
1720
1721 let _ = editor.update(&Message::CharacterInput('c'));
1722 editor.history.end_group();
1723
1724 let _ = editor.update(&Message::CharacterInput('d'));
1725 editor.history.end_group();
1726
1727 assert_eq!(editor.buffer.line(0), "abcd");
1728
1729 let _ = editor.update(&Message::Undo);
1731 assert_eq!(editor.buffer.line(0), "abc");
1732
1733 let _ = editor.update(&Message::Undo);
1734 assert_eq!(editor.buffer.line(0), "ab");
1735
1736 let _ = editor.update(&Message::Undo);
1737 assert_eq!(editor.buffer.line(0), "a");
1738
1739 let _ = editor.update(&Message::Redo);
1741 assert_eq!(editor.buffer.line(0), "ab");
1742
1743 let _ = editor.update(&Message::Redo);
1744 assert_eq!(editor.buffer.line(0), "abc");
1745
1746 let _ = editor.update(&Message::Redo);
1747 assert_eq!(editor.buffer.line(0), "abcd");
1748 }
1749
1750 #[test]
1751 fn test_delete_key_with_selection() {
1752 let mut editor = CodeEditor::new("hello world", "py");
1753 editor.selection_start = Some((0, 0));
1754 editor.selection_end = Some((0, 5));
1755 editor.cursor = (0, 5);
1756
1757 let _ = editor.update(&Message::Delete);
1758
1759 assert_eq!(editor.buffer.line(0), " world");
1760 assert_eq!(editor.cursor, (0, 0));
1761 assert_eq!(editor.selection_start, None);
1762 assert_eq!(editor.selection_end, None);
1763 }
1764
1765 #[test]
1766 fn test_delete_key_without_selection() {
1767 let mut editor = CodeEditor::new("hello", "py");
1768 editor.cursor = (0, 0);
1769
1770 let _ = editor.update(&Message::Delete);
1771
1772 assert_eq!(editor.buffer.line(0), "ello");
1774 assert_eq!(editor.cursor, (0, 0));
1775 }
1776
1777 #[test]
1778 fn test_backspace_with_selection() {
1779 let mut editor = CodeEditor::new("hello world", "py");
1780 editor.selection_start = Some((0, 6));
1781 editor.selection_end = Some((0, 11));
1782 editor.cursor = (0, 11);
1783
1784 let _ = editor.update(&Message::Backspace);
1785
1786 assert_eq!(editor.buffer.line(0), "hello ");
1787 assert_eq!(editor.cursor, (0, 6));
1788 assert_eq!(editor.selection_start, None);
1789 assert_eq!(editor.selection_end, None);
1790 }
1791
1792 #[test]
1793 fn test_backspace_without_selection() {
1794 let mut editor = CodeEditor::new("hello", "py");
1795 editor.cursor = (0, 5);
1796
1797 let _ = editor.update(&Message::Backspace);
1798
1799 assert_eq!(editor.buffer.line(0), "hell");
1801 assert_eq!(editor.cursor, (0, 4));
1802 }
1803
1804 #[test]
1805 fn test_delete_multiline_selection() {
1806 let mut editor = CodeEditor::new("line1\nline2\nline3", "py");
1807 editor.selection_start = Some((0, 2));
1808 editor.selection_end = Some((2, 2));
1809 editor.cursor = (2, 2);
1810
1811 let _ = editor.update(&Message::Delete);
1812
1813 assert_eq!(editor.buffer.line(0), "line3");
1814 assert_eq!(editor.cursor, (0, 2));
1815 assert_eq!(editor.selection_start, None);
1816 }
1817
1818 #[test]
1819 fn test_canvas_focus_gained() {
1820 let mut editor = CodeEditor::new("hello world", "py");
1821 assert!(!editor.has_canvas_focus);
1822 assert!(!editor.show_cursor);
1823
1824 let _ = editor.update(&Message::CanvasFocusGained);
1825
1826 assert!(editor.has_canvas_focus);
1827 assert!(editor.show_cursor);
1828 }
1829
1830 #[test]
1831 fn test_mouse_click_gains_focus() {
1832 let mut editor = CodeEditor::new("hello world", "py");
1833 editor.has_canvas_focus = false;
1834 editor.show_cursor = false;
1835
1836 let _ =
1837 editor.update(&Message::MouseClick(iced::Point::new(100.0, 10.0)));
1838
1839 assert!(editor.has_canvas_focus);
1840 assert!(editor.show_cursor);
1841 }
1842}