1use iced::widget::operation::scroll_to;
4use iced::widget::scrollable;
5use iced::{Point, Task};
6#[cfg(not(target_arch = "wasm32"))]
7use std::time::Instant;
8
9#[cfg(target_arch = "wasm32")]
10use web_time::Instant;
11
12use super::measure_text_width;
13
14use super::vim::VimMotion;
15use super::wrapping::{VisualLine, WrappingCalculator};
16use super::{ArrowDirection, CodeEditor, Message, cursor_set};
17use crate::text_buffer::TextBuffer;
18
19fn compute_next_position(
23 pos: (usize, usize),
24 direction: ArrowDirection,
25 buffer: &TextBuffer,
26 visual_lines: &[VisualLine],
27) -> Option<(usize, usize)> {
28 let (line, col) = pos;
29 match direction {
30 ArrowDirection::Up | ArrowDirection::Down => {
31 let current_visual =
32 WrappingCalculator::logical_to_visual(visual_lines, line, col)?;
33
34 let target_visual = match direction {
35 ArrowDirection::Up => current_visual.checked_sub(1)?,
36 ArrowDirection::Down => {
37 let next = current_visual + 1;
38 if next < visual_lines.len() {
39 next
40 } else {
41 return None;
42 }
43 }
44 _ => return None,
45 };
46
47 let target_vl = &visual_lines[target_visual];
48 let current_vl = &visual_lines[current_visual];
49
50 let new_col = if target_vl.logical_line == line {
51 let offset_in_current =
52 col.saturating_sub(current_vl.start_col);
53 let target_col = target_vl.start_col + offset_in_current;
54 if target_col >= target_vl.end_col {
55 target_vl.end_col.saturating_sub(1).max(target_vl.start_col)
56 } else {
57 target_col
58 }
59 } else {
60 let target_line_len = buffer.line_len(target_vl.logical_line);
61 (target_vl.start_col + col.min(target_vl.len()))
62 .min(target_line_len)
63 };
64
65 Some((target_vl.logical_line, new_col))
66 }
67 ArrowDirection::Left => {
68 if col > 0 {
69 Some((line, col - 1))
70 } else if line > 0 {
71 Some((line - 1, buffer.line_len(line - 1)))
72 } else {
73 None
74 }
75 }
76 ArrowDirection::Right => {
77 let line_len = buffer.line_len(line);
78 if col < line_len {
79 Some((line, col + 1))
80 } else if line + 1 < buffer.line_count() {
81 Some((line + 1, 0))
82 } else {
83 None
84 }
85 }
86 }
87}
88
89impl CodeEditor {
90 pub(crate) fn vim_normal_position(
94 &self,
95 position: (usize, usize),
96 ) -> (usize, usize) {
97 let line = position.0.min(self.buffer.line_count().saturating_sub(1));
98 let line_len = self.buffer.line_len(line);
99 let max_col = line_len.saturating_sub(1);
100 (line, position.1.min(max_col))
101 }
102
103 fn vim_position_after(&self, position: (usize, usize)) -> (usize, usize) {
104 let line_len = self.buffer.line_len(position.0);
105 if line_len == 0 {
106 if position.0 + 1 < self.buffer.line_count() {
107 (position.0 + 1, 0)
108 } else {
109 position
110 }
111 } else {
112 (position.0, (position.1 + 1).min(line_len))
113 }
114 }
115
116 pub(crate) fn apply_vim_visual_selection(
119 &mut self,
120 anchor: (usize, usize),
121 active: (usize, usize),
122 linewise: bool,
123 ) {
124 let anchor = self.vim_normal_position(anchor);
125 let active = self.vim_normal_position(active);
126 let cursor = if linewise {
127 let start_line = anchor.0.min(active.0);
128 let end_line = anchor.0.max(active.0);
129 let end = if end_line + 1 < self.buffer.line_count() {
130 (end_line + 1, 0)
131 } else {
132 (end_line, self.buffer.line_len(end_line))
133 };
134 cursor_set::Cursor { position: end, anchor: Some((start_line, 0)) }
135 } else if active >= anchor {
136 cursor_set::Cursor {
137 position: self.vim_position_after(active),
138 anchor: Some(anchor),
139 }
140 } else {
141 cursor_set::Cursor {
142 position: active,
143 anchor: Some(self.vim_position_after(anchor)),
144 }
145 };
146 self.cursors.set_single(cursor.position);
147 self.cursors.primary_mut().anchor = cursor.anchor;
148 self.overlay_cache.clear();
149 }
150
151 pub(crate) fn vim_motion_target(
154 &self,
155 start: (usize, usize),
156 motion: VimMotion,
157 count: usize,
158 explicit_count: bool,
159 ) -> (usize, usize) {
160 let mut position = self.vim_normal_position(start);
161 let count = count.max(1);
162
163 match motion {
164 VimMotion::Left => {
165 position.1 = position.1.saturating_sub(count);
166 }
167 VimMotion::Right => {
168 let max_col =
169 self.buffer.line_len(position.0).saturating_sub(1);
170 position.1 = position.1.saturating_add(count).min(max_col);
171 }
172 VimMotion::Up | VimMotion::Down => {
173 let direction = if motion == VimMotion::Up {
174 ArrowDirection::Up
175 } else {
176 ArrowDirection::Down
177 };
178 let visual_lines =
179 self.visual_lines_cached(self.viewport_width);
180 for _ in 0..count {
181 let Some(next) = compute_next_position(
182 position,
183 direction,
184 &self.buffer,
185 &visual_lines,
186 ) else {
187 break;
188 };
189 position = self.vim_normal_position(next);
190 }
191 }
192 VimMotion::WordForward
193 | VimMotion::WordBackward
194 | VimMotion::WordEnd => {
195 let chars = self.vim_char_index();
196 for _ in 0..count {
197 position = Self::vim_word_motion(&chars, position, motion);
198 }
199 }
200 VimMotion::LineStart => position.1 = 0,
201 VimMotion::FirstNonBlank => {
202 position.1 = self
203 .buffer
204 .line(position.0)
205 .chars()
206 .position(|ch| !ch.is_whitespace())
207 .unwrap_or(0);
208 }
209 VimMotion::LineEnd => {
210 position.1 = self.buffer.line_len(position.0).saturating_sub(1);
211 }
212 VimMotion::DocumentStart => {
213 let line = count
214 .saturating_sub(1)
215 .min(self.buffer.line_count().saturating_sub(1));
216 position = (line, 0);
217 }
218 VimMotion::DocumentEnd => {
219 let line = if explicit_count {
220 count
221 .saturating_sub(1)
222 .min(self.buffer.line_count().saturating_sub(1))
223 } else {
224 self.buffer.line_count().saturating_sub(1)
225 };
226 position = (line, 0);
227 }
228 }
229
230 self.vim_normal_position(position)
231 }
232
233 fn vim_char_index(&self) -> Vec<((usize, usize), char)> {
239 let mut chars = Vec::new();
240 for line in 0..self.buffer.line_count() {
241 chars.extend(
242 self.buffer
243 .line(line)
244 .chars()
245 .enumerate()
246 .map(|(col, ch)| ((line, col), ch)),
247 );
248 if line + 1 < self.buffer.line_count() {
249 chars.push(((line, self.buffer.line_len(line)), '\n'));
250 }
251 }
252 chars
253 }
254
255 fn vim_word_motion(
258 chars: &[((usize, usize), char)],
259 start: (usize, usize),
260 motion: VimMotion,
261 ) -> (usize, usize) {
262 #[derive(Clone, Copy, PartialEq, Eq)]
263 enum Class {
264 Space,
265 Word,
266 Punctuation,
267 }
268
269 fn class(ch: char) -> Class {
270 if ch.is_whitespace() {
271 Class::Space
272 } else if ch.is_alphanumeric() || ch == '_' {
273 Class::Word
274 } else {
275 Class::Punctuation
276 }
277 }
278
279 if chars.is_empty() {
280 return (0, 0);
281 }
282
283 let insertion =
284 chars.partition_point(|(position, _)| *position < start);
285 let exact = insertion < chars.len() && chars[insertion].0 == start;
286
287 match motion {
288 VimMotion::WordForward => {
289 let mut index = insertion;
290 if exact {
291 let current_class = class(chars[index].1);
292 if current_class == Class::Space {
293 while index < chars.len()
294 && class(chars[index].1) == Class::Space
295 {
296 index += 1;
297 }
298 } else {
299 while index < chars.len()
300 && class(chars[index].1) == current_class
301 {
302 index += 1;
303 }
304 while index < chars.len()
305 && class(chars[index].1) == Class::Space
306 {
307 index += 1;
308 }
309 }
310 }
311 chars[index.min(chars.len() - 1)].0
312 }
313 VimMotion::WordBackward => {
314 let mut index = insertion.saturating_sub(1);
315 while index > 0 && class(chars[index].1) == Class::Space {
316 index -= 1;
317 }
318 let target_class = class(chars[index].1);
319 while index > 0 && class(chars[index - 1].1) == target_class {
320 index -= 1;
321 }
322 chars[index].0
323 }
324 VimMotion::WordEnd => {
325 let mut index = insertion.min(chars.len() - 1);
326 if exact {
327 let current_class = class(chars[index].1);
328 if current_class != Class::Space
329 && index + 1 < chars.len()
330 && class(chars[index + 1].1) == current_class
331 {
332 while index + 1 < chars.len()
333 && class(chars[index + 1].1) == current_class
334 {
335 index += 1;
336 }
337 return chars[index].0;
338 }
339 index = (index + 1).min(chars.len() - 1);
340 }
341 while index + 1 < chars.len()
342 && class(chars[index].1) == Class::Space
343 {
344 index += 1;
345 }
346 let target_class = class(chars[index].1);
347 while index + 1 < chars.len()
348 && class(chars[index + 1].1) == target_class
349 {
350 index += 1;
351 }
352 chars[index].0
353 }
354 _ => start,
355 }
356 }
357
358 pub fn set_cursor(&mut self, line: usize, col: usize) -> Task<Message> {
373 let line = line.min(self.buffer.line_count().saturating_sub(1));
374 let line_len = self.buffer.line(line).chars().count();
375 let col = col.min(line_len);
376
377 self.cursors.set_single((line, col));
378 self.is_dragging = false;
381
382 self.last_blink = Instant::now();
384
385 self.overlay_cache.clear();
386 self.scroll_to_cursor()
387 }
388
389 pub(crate) fn move_cursor(&mut self, direction: ArrowDirection) {
394 let visual_lines = self.visual_lines_cached(self.viewport_width);
398
399 for cursor in self.cursors.as_mut_slice() {
400 if let Some(new_pos) = compute_next_position(
401 cursor.position,
402 direction,
403 &self.buffer,
404 &visual_lines,
405 ) {
406 cursor.position = new_pos;
407 }
408 }
409
410 self.cursors.sort_and_merge();
412
413 self.overlay_cache.clear();
416 }
417
418 pub(crate) fn calculate_cursor_from_point(
425 &self,
426 point: Point,
427 ) -> Option<(usize, usize)> {
428 if point.x < self.gutter_width() {
430 return None; }
432
433 let visual_line_idx = (point.y / self.line_height) as usize;
435
436 let visual_lines = self.visual_lines_cached(self.viewport_width);
439
440 if visual_line_idx >= visual_lines.len() {
441 let last_line = self.buffer.line_count().saturating_sub(1);
443 let last_col = self.buffer.line_len(last_line);
444 return Some((last_line, last_col));
445 }
446
447 let visual_line = &visual_lines[visual_line_idx];
448
449 let x_in_text =
451 point.x - self.gutter_width() - 5.0 + self.horizontal_scroll_offset;
452
453 let line_content = self.buffer.line(visual_line.logical_line);
455
456 let mut current_width = 0.0;
457 let mut col_offset = 0;
458
459 for c in line_content
461 .chars()
462 .skip(visual_line.start_col)
463 .take(visual_line.end_col - visual_line.start_col)
464 {
465 let char_width = super::measure_char_width(
466 c,
467 self.full_char_width,
468 self.char_width,
469 );
470
471 if current_width + char_width / 2.0 > x_in_text {
472 break;
473 }
474 current_width += char_width;
475 col_offset += 1;
476 }
477
478 let col = visual_line.start_col + col_offset;
479 Some((visual_line.logical_line, col))
480 }
481
482 pub(crate) fn handle_mouse_click(&mut self, point: Point) {
486 let before = self.cursors.primary_position();
487 if let Some(pos) = self.calculate_cursor_from_point(point) {
488 self.cursors.primary_mut().position = pos;
489 if self.cursors.primary_position() != before {
490 self.overlay_cache.clear();
492 }
493 }
494 }
495
496 pub(crate) fn classify_click(&self, position: Point) -> u8 {
503 let now = Instant::now();
504 let count = match self.last_click.get() {
505 Some((time, pos, count))
506 if now.duration_since(time)
507 < std::time::Duration::from_millis(400)
508 && pos.distance(position) < 6.0 =>
509 {
510 if count >= 3 {
511 1
512 } else {
513 count + 1
514 }
515 }
516 _ => 1,
517 };
518 self.last_click.set(Some((now, position, count)));
519 count
520 }
521
522 pub(crate) fn scroll_to_cursor(&self) -> Task<Message> {
524 let visual_lines = self.visual_lines_cached(self.viewport_width);
527
528 let pos = self.cursors.primary_position();
529 let cursor_visual =
530 WrappingCalculator::logical_to_visual(&visual_lines, pos.0, pos.1);
531
532 let cursor_y = if let Some(visual_idx) = cursor_visual {
533 visual_idx as f32 * self.line_height
534 } else {
535 pos.0 as f32 * self.line_height
537 };
538
539 let viewport_top = self.viewport_scroll;
540 let viewport_bottom = self.viewport_scroll + self.viewport_height;
541
542 let top_margin = self.line_height * 2.0;
544 let bottom_margin = self.line_height * 2.0;
545
546 let new_v_scroll = if cursor_y < viewport_top + top_margin {
548 Some((cursor_y - top_margin).max(0.0))
550 } else if cursor_y + self.line_height > viewport_bottom - bottom_margin
551 {
552 Some(
554 cursor_y + self.line_height + bottom_margin
555 - self.viewport_height,
556 )
557 } else {
558 None
559 };
560
561 let vertical_task = if let Some(new_scroll) = new_v_scroll {
562 scroll_to(
563 self.scrollable_id.clone(),
564 scrollable::AbsoluteOffset { x: 0.0, y: new_scroll },
565 )
566 } else {
567 Task::none()
568 };
569
570 let h_task = if !self.wrap_enabled {
572 let cursor_content_x = if let Some(visual_idx) = cursor_visual {
574 let vl = &visual_lines[visual_idx];
575 let line_content = self.buffer.line(vl.logical_line);
576 let prefix: String = line_content
577 .chars()
578 .skip(vl.start_col)
579 .take(pos.1.saturating_sub(vl.start_col))
580 .collect();
581 self.gutter_width()
582 + 5.0
583 + measure_text_width(
584 &prefix,
585 self.full_char_width,
586 self.char_width,
587 )
588 } else {
589 self.gutter_width() + 5.0
590 };
591
592 let left_boundary = self.gutter_width() + self.char_width;
593 let right_boundary = self.viewport_width - self.char_width * 2.0;
594 let cursor_viewport_x =
595 cursor_content_x - self.horizontal_scroll_offset;
596
597 let new_h_offset = if cursor_viewport_x < left_boundary {
598 (cursor_content_x - left_boundary).max(0.0)
599 } else if cursor_viewport_x > right_boundary {
600 cursor_content_x - right_boundary
601 } else {
602 self.horizontal_scroll_offset };
604
605 if (new_h_offset - self.horizontal_scroll_offset).abs() > 0.5 {
606 scroll_to(
607 self.horizontal_scrollable_id.clone(),
608 scrollable::AbsoluteOffset { x: new_h_offset, y: 0.0 },
609 )
610 } else {
611 Task::none()
612 }
613 } else {
614 Task::none()
615 };
616
617 Task::batch([vertical_task, h_task])
618 }
619
620 fn move_cursors_by_line(&mut self, map_line: impl Fn(usize) -> usize) {
630 for cursor in self.cursors.as_mut_slice() {
631 let new_line = map_line(cursor.position.0);
632 let line_len = self.buffer.line_len(new_line);
633 cursor.position = (new_line, cursor.position.1.min(line_len));
634 }
635 self.cursors.sort_and_merge();
636 self.overlay_cache.clear();
637 }
638
639 pub(crate) fn page_up(&mut self) {
641 let lines_per_page = (self.viewport_height / self.line_height) as usize;
642 self.move_cursors_by_line(|line| line.saturating_sub(lines_per_page));
643 }
644
645 pub(crate) fn page_down(&mut self) {
647 let lines_per_page = (self.viewport_height / self.line_height) as usize;
648 let max_line = self.buffer.line_count().saturating_sub(1);
649 self.move_cursors_by_line(|line| (line + lines_per_page).min(max_line));
650 }
651
652 pub(crate) fn handle_mouse_drag(&mut self, point: Point) {
656 if let Some(pos) = self.calculate_cursor_from_point(point) {
657 self.cursors.primary_mut().position = pos;
658 }
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn vim_word_motion_crosses_line_boundary_via_prebuilt_index() {
668 let editor = CodeEditor::new("one two\nthree four", "txt");
669 let chars = editor.vim_char_index();
670
671 assert_eq!(
672 CodeEditor::vim_word_motion(&chars, (0, 4), VimMotion::WordForward),
673 (1, 0)
674 );
675 assert_eq!(
676 CodeEditor::vim_word_motion(
677 &chars,
678 (1, 0),
679 VimMotion::WordBackward
680 ),
681 (0, 4)
682 );
683 assert_eq!(
684 CodeEditor::vim_word_motion(&chars, (0, 4), VimMotion::WordEnd),
685 (0, 6)
686 );
687 }
688
689 #[test]
690 fn test_cursor_movement() {
691 let mut editor = CodeEditor::new("line1\nline2", "py");
692 editor.move_cursor(ArrowDirection::Down);
693 assert_eq!(editor.cursors.primary_position().0, 1);
694 editor.move_cursor(ArrowDirection::Right);
695 assert_eq!(editor.cursors.primary_position().1, 1);
696 }
697
698 #[test]
699 fn test_page_down() {
700 let content = (0..100)
702 .map(|i| format!("line {i}"))
703 .collect::<Vec<_>>()
704 .join("\n");
705 let mut editor = CodeEditor::new(&content, "py");
706
707 editor.page_down();
708 assert!(editor.cursors.primary_position().0 >= 25);
710 assert!(editor.cursors.primary_position().0 <= 35);
711 }
712
713 #[test]
714 fn test_page_up() {
715 let content = (0..100)
717 .map(|i| format!("line {i}"))
718 .collect::<Vec<_>>()
719 .join("\n");
720 let mut editor = CodeEditor::new(&content, "py");
721
722 editor.cursors.primary_mut().position = (50, 0);
724 editor.page_up();
725
726 assert!(editor.cursors.primary_position().0 >= 15);
728 assert!(editor.cursors.primary_position().0 <= 25);
729 }
730
731 #[test]
732 fn test_page_down_at_end() {
733 let content =
734 (0..10).map(|i| format!("line {i}")).collect::<Vec<_>>().join("\n");
735 let mut editor = CodeEditor::new(&content, "py");
736
737 editor.page_down();
738 assert_eq!(editor.cursors.primary_position().0, 9);
740 }
741
742 #[test]
743 fn test_page_up_at_start() {
744 let content = (0..100)
745 .map(|i| format!("line {i}"))
746 .collect::<Vec<_>>()
747 .join("\n");
748 let mut editor = CodeEditor::new(&content, "py");
749
750 editor.cursors.primary_mut().position = (0, 0);
752 editor.page_up();
753 assert_eq!(editor.cursors.primary_position().0, 0);
754 }
755
756 #[test]
757 fn test_cursor_click_cjk() {
758 use iced::Point;
759 let mut editor = CodeEditor::new("你好", "txt");
760 editor.set_line_numbers_enabled(false);
761 editor.set_folding_enabled(false);
764
765 let full_char_width = editor.full_char_width();
766 let half_width = full_char_width / 2.0;
767 let padding = 5.0;
768
769 editor
775 .handle_mouse_click(Point::new((half_width - 2.0) + padding, 10.0));
776
777 assert_eq!(editor.cursors.primary_position(), (0, 0));
778
779 editor
782 .handle_mouse_click(Point::new((half_width + 2.0) + padding, 10.0));
783 assert_eq!(editor.cursors.primary_position(), (0, 1));
784
785 editor.handle_mouse_click(Point::new(
789 (full_char_width + half_width - 2.0) + padding,
790 10.0,
791 ));
792 assert_eq!(editor.cursors.primary_position(), (0, 1));
793
794 editor.handle_mouse_click(Point::new(
798 (full_char_width + half_width + 2.0) + padding,
799 10.0,
800 ));
801 assert_eq!(editor.cursors.primary_position(), (0, 2));
802 }
803
804 #[test]
805 fn test_multi_cursor_move_left() {
806 let mut editor = CodeEditor::new("abc\ndef", "rs");
807 editor.cursors.primary_mut().position = (0, 2);
808 editor.cursors.add_cursor((1, 2));
809
810 editor.move_cursor(ArrowDirection::Left);
811
812 let positions: Vec<(usize, usize)> =
814 editor.cursors.iter().map(|c| c.position).collect();
815 assert!(positions.contains(&(0, 1)));
816 assert!(positions.contains(&(1, 1)));
817 }
818
819 #[test]
820 fn test_multi_cursor_move_right() {
821 let mut editor = CodeEditor::new("abc\ndef", "rs");
822 editor.cursors.primary_mut().position = (0, 1);
823 editor.cursors.add_cursor((1, 1));
824
825 editor.move_cursor(ArrowDirection::Right);
826
827 let positions: Vec<(usize, usize)> =
828 editor.cursors.iter().map(|c| c.position).collect();
829 assert!(positions.contains(&(0, 2)));
830 assert!(positions.contains(&(1, 2)));
831 }
832
833 #[test]
834 fn test_multi_cursor_move_deduplicates() {
835 let mut editor = CodeEditor::new("abc", "rs");
836 editor.cursors.primary_mut().position = (0, 0);
838 editor.cursors.add_cursor((0, 1));
839 assert_eq!(editor.cursors.len(), 2);
840
841 editor.move_cursor(ArrowDirection::Right);
842
843 assert_eq!(editor.cursors.len(), 2);
845 }
846}