1use super::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub(super) enum TextBoundary {
7 Grapheme,
8 Word,
9}
10
11#[derive(Debug, Clone)]
19pub struct WordBoundaryConfig {
20 ascii_is_boundary: [bool; 128],
21}
22
23impl Default for WordBoundaryConfig {
24 fn default() -> Self {
25 let mut ascii_is_boundary = [true; 128];
26 for b in 0u8..=127 {
27 let ch = b as char;
28 if ch.is_ascii_alphanumeric() || ch == '_' {
29 ascii_is_boundary[b as usize] = false;
30 }
31 }
32 ascii_is_boundary[b' ' as usize] = true;
34 ascii_is_boundary[b'\t' as usize] = true;
35 ascii_is_boundary[b'\n' as usize] = true;
36 ascii_is_boundary[b'\r' as usize] = true;
37 Self { ascii_is_boundary }
38 }
39}
40
41impl WordBoundaryConfig {
42 pub fn set_ascii_boundary_chars(&mut self, boundary_chars: &str) {
49 self.ascii_is_boundary = [false; 128];
50 self.ascii_is_boundary[b' ' as usize] = true;
52 self.ascii_is_boundary[b'\t' as usize] = true;
53 self.ascii_is_boundary[b'\n' as usize] = true;
54 self.ascii_is_boundary[b'\r' as usize] = true;
55
56 for ch in boundary_chars.chars() {
57 if ch.is_ascii() {
58 self.ascii_is_boundary[ch as usize] = true;
59 }
60 }
61 }
62
63 pub(super) fn is_ascii_word_char(&self, ch: char) -> bool {
64 if !ch.is_ascii() {
65 return false;
66 }
67 !self.ascii_is_boundary[ch as usize]
68 }
69
70 pub(super) fn is_word_token_char(&self, ch: char) -> bool {
71 if ch.is_whitespace() {
72 return false;
73 }
74 if ch.is_ascii() {
75 self.is_ascii_word_char(ch)
76 } else {
77 true
79 }
80 }
81}
82
83pub(super) fn byte_offset_for_char_column(text: &str, column: usize) -> usize {
84 if column == 0 {
85 return 0;
86 }
87
88 text.char_indices()
89 .nth(column)
90 .map(|(byte, _)| byte)
91 .unwrap_or_else(|| text.len())
92}
93
94pub(super) fn char_column_for_byte_offset(text: &str, byte_offset: usize) -> usize {
95 text.get(..byte_offset).unwrap_or(text).chars().count()
96}
97
98pub(super) fn leading_horizontal_whitespace(text: &str) -> (usize, usize) {
99 let mut column = 0usize;
100 for (byte, ch) in text.char_indices() {
101 if ch != ' ' && ch != '\t' {
102 return (column, byte);
103 }
104 column += 1;
105 }
106
107 (column, text.len())
108}
109
110pub(super) fn prev_boundary_column(text: &str, column: usize, boundary: TextBoundary) -> usize {
111 let byte_pos = byte_offset_for_char_column(text, column);
112
113 let mut prev = 0usize;
114 match boundary {
115 TextBoundary::Grapheme => {
116 for (b, _) in text.grapheme_indices(true) {
117 if b >= byte_pos {
118 break;
119 }
120 prev = b;
121 }
122 }
123 TextBoundary::Word => {
124 for (b, _) in text.split_word_bound_indices() {
125 if b >= byte_pos {
126 break;
127 }
128 prev = b;
129 }
130 }
131 }
132
133 char_column_for_byte_offset(text, prev)
134}
135
136pub(super) fn next_boundary_column(text: &str, column: usize, boundary: TextBoundary) -> usize {
137 let byte_pos = byte_offset_for_char_column(text, column);
138
139 let mut next = text.len();
140 match boundary {
141 TextBoundary::Grapheme => {
142 for (b, _) in text.grapheme_indices(true) {
143 if b > byte_pos {
144 next = b;
145 break;
146 }
147 }
148 }
149 TextBoundary::Word => {
150 for (b, _) in text.split_word_bound_indices() {
151 if b > byte_pos {
152 next = b;
153 break;
154 }
155 }
156 }
157 }
158
159 char_column_for_byte_offset(text, next)
160}
161
162impl CommandExecutor {
163 pub(super) fn execute_select_line_command(&mut self) -> Result<CommandResult, CommandError> {
164 let snapshot = self.snapshot_selection_set();
165 let selections = snapshot.selections;
166 let primary_index = snapshot.primary_index;
167
168 let line_count = self.editor.line_index.line_count();
169 if line_count == 0 {
170 return Ok(CommandResult::Success);
171 }
172
173 let mut next: Vec<Selection> = Vec::with_capacity(selections.len());
174 for sel in selections {
175 let (min_pos, max_pos) = crate::selection_set::selection_min_max(&sel);
176 let start_line = min_pos.line.min(line_count.saturating_sub(1));
177 let end_line = max_pos.line.min(line_count.saturating_sub(1));
178
179 let start = Position::new(start_line, 0);
180 let end = if end_line + 1 < line_count {
181 Position::new(end_line + 1, 0)
182 } else {
183 let line_text = self
184 .editor
185 .line_index
186 .get_line_text(end_line)
187 .unwrap_or_default();
188 Position::new(end_line, line_text.chars().count())
189 };
190
191 next.push(Selection {
192 start,
193 end,
194 direction: SelectionDirection::Forward,
195 });
196 }
197
198 self.execute_cursor(CursorCommand::SetSelections {
199 selections: next,
200 primary_index,
201 })?;
202 Ok(CommandResult::Success)
203 }
204
205 pub(super) fn execute_select_word_command(&mut self) -> Result<CommandResult, CommandError> {
206 let snapshot = self.snapshot_selection_set();
207 let selections = snapshot.selections;
208 let primary_index = snapshot.primary_index;
209
210 let line_count = self.editor.line_index.line_count();
211 if line_count == 0 {
212 return Ok(CommandResult::Success);
213 }
214
215 let mut next: Vec<Selection> = Vec::with_capacity(selections.len());
216
217 for sel in selections {
218 if sel.start != sel.end {
220 next.push(sel);
221 continue;
222 }
223
224 let caret = sel.end;
225 let line = caret.line.min(line_count.saturating_sub(1));
226 let line_text = self
227 .editor
228 .line_index
229 .get_line_text(line)
230 .unwrap_or_default();
231 let col = caret.column.min(line_text.chars().count());
232
233 let Some((start_col, end_col)) = self.word_token_range_in_line(&line_text, col) else {
234 next.push(sel);
235 continue;
236 };
237
238 let start = Position::new(line, start_col);
239 let end = Position::new(line, end_col);
240
241 next.push(Selection {
242 start,
243 end,
244 direction: SelectionDirection::Forward,
245 });
246 }
247
248 self.execute_cursor(CursorCommand::SetSelections {
249 selections: next,
250 primary_index,
251 })?;
252 Ok(CommandResult::Success)
253 }
254
255 pub(super) fn execute_expand_selection_command(
256 &mut self,
257 ) -> Result<CommandResult, CommandError> {
258 let snapshot = self.snapshot_selection_set();
262 if snapshot.selections.iter().any(|s| s.start != s.end) {
263 self.execute_select_line_command()
264 } else {
265 self.execute_select_word_command()
266 }
267 }
268
269 pub(super) fn execute_expand_selection_by_command(
270 &mut self,
271 unit: ExpandSelectionUnit,
272 count: usize,
273 direction: ExpandSelectionDirection,
274 ) -> Result<CommandResult, CommandError> {
275 if count == 0 {
276 return Ok(CommandResult::Success);
277 }
278
279 let snapshot = self.snapshot_selection_set();
280 let selections = snapshot.selections;
281 let primary_index = snapshot.primary_index;
282
283 let line_count = self.editor.line_index.line_count();
284 if line_count == 0 {
285 return Ok(CommandResult::Success);
286 }
287
288 let mut next: Vec<Selection> = Vec::with_capacity(selections.len());
289 for sel in selections {
290 let (min_pos, max_pos) = crate::selection_set::selection_min_max(&sel);
291 let mut start = min_pos;
292 let mut end = max_pos;
293
294 match direction {
295 ExpandSelectionDirection::Backward => {
296 start = self.expand_position_by_unit(start, unit, count, direction);
297 }
298 ExpandSelectionDirection::Forward => {
299 end = self.expand_position_by_unit(end, unit, count, direction);
300 }
301 }
302
303 next.push(Selection {
304 start,
305 end,
306 direction: SelectionDirection::Forward,
307 });
308 }
309
310 self.execute_cursor(CursorCommand::SetSelections {
312 selections: next,
313 primary_index,
314 })?;
315 Ok(CommandResult::Success)
316 }
317
318 pub(super) fn expand_position_by_unit(
319 &self,
320 pos: Position,
321 unit: ExpandSelectionUnit,
322 count: usize,
323 direction: ExpandSelectionDirection,
324 ) -> Position {
325 match unit {
326 ExpandSelectionUnit::Character => self.expand_position_by_char(pos, count, direction),
327 ExpandSelectionUnit::Word => self.expand_position_by_word(pos, count, direction),
328 ExpandSelectionUnit::Line => self.expand_position_by_line(pos, count, direction),
329 }
330 }
331
332 pub(super) fn expand_position_by_char(
333 &self,
334 pos: Position,
335 count: usize,
336 direction: ExpandSelectionDirection,
337 ) -> Position {
338 let line_index = &self.editor.line_index;
339 let mut offset = line_index.position_to_char_offset(pos.line, pos.column);
340 let char_count = self.editor.char_count();
341
342 offset = match direction {
343 ExpandSelectionDirection::Backward => offset.saturating_sub(count),
344 ExpandSelectionDirection::Forward => offset.saturating_add(count).min(char_count),
345 };
346
347 let (line, col) = line_index.char_offset_to_position(offset);
348 Position::new(line, col)
349 }
350
351 pub(super) fn expand_position_by_line(
352 &self,
353 pos: Position,
354 count: usize,
355 direction: ExpandSelectionDirection,
356 ) -> Position {
357 let line_index = &self.editor.line_index;
358 let line_count = line_index.line_count();
359 if line_count == 0 {
360 return Position::new(0, 0);
361 }
362
363 let mut line = pos.line.min(line_count.saturating_sub(1));
364 line = match direction {
365 ExpandSelectionDirection::Backward => line.saturating_sub(count),
366 ExpandSelectionDirection::Forward => line.saturating_add(count),
367 };
368
369 if line >= line_count {
370 let line_text = line_index
371 .get_line_text(line_count.saturating_sub(1))
372 .unwrap_or_default();
373 return Position::new(line_count.saturating_sub(1), line_text.chars().count());
374 }
375
376 Position::new(line, 0)
377 }
378
379 pub(super) fn expand_position_by_word(
380 &self,
381 mut pos: Position,
382 count: usize,
383 direction: ExpandSelectionDirection,
384 ) -> Position {
385 for _ in 0..count {
386 let next = match direction {
387 ExpandSelectionDirection::Backward => self.prev_word_boundary_position(pos),
388 ExpandSelectionDirection::Forward => self.next_word_boundary_position(pos),
389 };
390 if next == pos {
391 break;
392 }
393 pos = next;
394 }
395 pos
396 }
397
398 pub(super) fn next_word_boundary_position(&self, pos: Position) -> Position {
399 let line_index = &self.editor.line_index;
400 let line_count = line_index.line_count();
401 if line_count == 0 {
402 return Position::new(0, 0);
403 }
404
405 let mut line = pos.line.min(line_count.saturating_sub(1));
406 let mut col = pos.column;
407
408 loop {
409 let line_text = line_index.get_line_text(line).unwrap_or_default();
410 let chars: Vec<char> = line_text.chars().collect();
411 let len = chars.len();
412 col = col.min(len);
413
414 let mut i = col;
416 while i < len && !self.editor.word_boundary.is_word_token_char(chars[i]) {
417 i += 1;
418 }
419
420 if i < len {
421 if chars[i].is_ascii() && self.editor.word_boundary.is_ascii_word_char(chars[i]) {
423 let mut end = i + 1;
424 while end < len
425 && chars[end].is_ascii()
426 && self.editor.word_boundary.is_ascii_word_char(chars[end])
427 {
428 end += 1;
429 }
430 return Position::new(line, end);
431 }
432 return Position::new(line, i + 1);
433 }
434
435 if line + 1 >= line_count {
437 return Position::new(line, len);
438 }
439 line += 1;
440 col = 0;
441 }
442 }
443
444 pub(super) fn prev_word_boundary_position(&self, pos: Position) -> Position {
445 let line_index = &self.editor.line_index;
446 let line_count = line_index.line_count();
447 if line_count == 0 {
448 return Position::new(0, 0);
449 }
450
451 let mut line = pos.line.min(line_count.saturating_sub(1));
452 let mut col = pos.column;
453
454 loop {
455 let line_text = line_index.get_line_text(line).unwrap_or_default();
456 let chars: Vec<char> = line_text.chars().collect();
457 let len = chars.len();
458 col = col.min(len);
459
460 if col == 0 {
461 if line == 0 {
462 return Position::new(0, 0);
463 }
464 line -= 1;
465 col = usize::MAX;
466 continue;
467 }
468
469 let mut i = col.saturating_sub(1).min(len.saturating_sub(1));
470 while i < len && !self.editor.word_boundary.is_word_token_char(chars[i]) {
471 if i == 0 {
472 break;
473 }
474 i -= 1;
475 }
476
477 if self.editor.word_boundary.is_word_token_char(chars[i]) {
478 if chars[i].is_ascii() && self.editor.word_boundary.is_ascii_word_char(chars[i]) {
480 while i > 0
481 && chars[i - 1].is_ascii()
482 && self.editor.word_boundary.is_ascii_word_char(chars[i - 1])
483 {
484 i -= 1;
485 }
486 return Position::new(line, i);
487 }
488 return Position::new(line, i);
489 }
490
491 if line == 0 {
493 return Position::new(0, 0);
494 }
495 line -= 1;
496 col = usize::MAX;
497 }
498 }
499
500 pub(super) fn execute_add_cursor_vertical_command(
501 &mut self,
502 above: bool,
503 ) -> Result<CommandResult, CommandError> {
504 let snapshot = self.snapshot_selection_set();
505 let mut selections = snapshot.selections;
506 let primary_index = snapshot.primary_index;
507
508 let line_count = self.editor.line_index.line_count();
509 if line_count == 0 {
510 return Ok(CommandResult::Success);
511 }
512
513 let mut extra: Vec<Selection> = Vec::new();
514 for sel in &selections {
515 let caret = sel.end;
516 let target_line = if above {
517 if caret.line == 0 {
518 continue;
519 }
520 caret.line - 1
521 } else {
522 let next = caret.line + 1;
523 if next >= line_count {
524 continue;
525 }
526 next
527 };
528
529 let col = self.clamp_column_for_line(target_line, caret.column);
530 let pos = Position::new(target_line, col);
531 extra.push(Selection {
532 start: pos,
533 end: pos,
534 direction: SelectionDirection::Forward,
535 });
536 }
537
538 if extra.is_empty() {
539 return Ok(CommandResult::Success);
540 }
541
542 selections.extend(extra);
543
544 self.execute_cursor(CursorCommand::SetSelections {
545 selections,
546 primary_index,
547 })?;
548 Ok(CommandResult::Success)
549 }
550
551 pub(super) fn selection_query(
552 &self,
553 selections: &[Selection],
554 primary_index: usize,
555 ) -> Option<(String, Option<SearchMatch>)> {
556 let primary = selections.get(primary_index)?;
557 let range = self.selection_char_range(primary);
558
559 if range.start != range.end {
560 let len = range.end - range.start;
561 return Some((self.editor.text_range(range.start, len), Some(range)));
562 }
563
564 let caret = primary.end;
565 let line_text = self
566 .editor
567 .line_index
568 .get_line_text(caret.line)
569 .unwrap_or_default();
570 let col = caret.column.min(line_text.chars().count());
571 let (start_col, end_col) = self.word_token_range_in_line(&line_text, col)?;
572 if start_col == end_col {
573 return None;
574 }
575
576 let start = self
577 .editor
578 .line_index
579 .position_to_char_offset(caret.line, start_col);
580 let end = self
581 .editor
582 .line_index
583 .position_to_char_offset(caret.line, end_col);
584 let range = SearchMatch {
585 start,
586 end: end.max(start),
587 };
588 Some((
589 self.editor
590 .text_range(range.start, range.end.saturating_sub(range.start)),
591 Some(range),
592 ))
593 }
594
595 pub(super) fn execute_add_next_occurrence_command(
596 &mut self,
597 options: SearchOptions,
598 ) -> Result<CommandResult, CommandError> {
599 let snapshot = self.snapshot_selection_set();
600 let mut selections = snapshot.selections;
601 let primary_index = snapshot.primary_index;
602
603 let Some((query, primary_range)) = self.selection_query(&selections, primary_index) else {
604 return Ok(CommandResult::Success);
605 };
606 if query.is_empty() {
607 return Ok(CommandResult::Success);
608 }
609
610 if let Some(primary_range) = primary_range
612 && primary_range.start != primary_range.end
613 {
614 let current = selections
615 .get(primary_index)
616 .map(|s| self.selection_char_range(s))
617 .unwrap_or(SearchMatch { start: 0, end: 0 });
618 if current.start == current.end {
619 let (start_line, start_col) = self
620 .editor
621 .line_index
622 .char_offset_to_position(primary_range.start);
623 let (end_line, end_col) = self
624 .editor
625 .line_index
626 .char_offset_to_position(primary_range.end);
627 if let Some(sel) = selections.get_mut(primary_index) {
628 *sel = Selection {
629 start: Position::new(start_line, start_col),
630 end: Position::new(end_line, end_col),
631 direction: SelectionDirection::Forward,
632 };
633 }
634 }
635 }
636
637 let text = self.editor.get_text();
638
639 let mut ranges: Vec<SearchMatch> = selections
640 .iter()
641 .map(|s| self.selection_char_range(s))
642 .filter(|r| r.start != r.end)
643 .collect();
644
645 if let Some(primary_range) = primary_range
646 && primary_range.start != primary_range.end
647 && !ranges
648 .iter()
649 .any(|r| r.start == primary_range.start && r.end == primary_range.end)
650 {
651 ranges.push(primary_range);
652 }
653
654 let mut existing: Vec<(usize, usize)> = ranges
655 .iter()
656 .map(|r| (r.start.min(r.end), r.end.max(r.start)))
657 .collect();
658 existing.sort_unstable();
659
660 let from = existing.iter().map(|(_, end)| *end).max().unwrap_or(0);
661
662 let mut search_from = from;
663 let mut wrapped = false;
664 let mut found: Option<SearchMatch> = None;
665
666 loop {
667 let next = find_next(&text, &query, options, search_from)
668 .map_err(|err| CommandError::Other(err.to_string()))?;
669
670 let Some(m) = next else {
671 if wrapped {
672 break;
673 }
674 wrapped = true;
675 search_from = 0;
676 continue;
677 };
678
679 let overlaps = existing.iter().any(|(s, e)| m.start < *e && m.end > *s);
680
681 if overlaps {
682 if m.end >= text.chars().count() {
683 break;
684 }
685 search_from = m.end + 1;
686 continue;
687 }
688
689 found = Some(m);
690 break;
691 }
692
693 let Some(m) = found else {
694 return Ok(CommandResult::Success);
695 };
696
697 let (start_line, start_col) = self.editor.line_index.char_offset_to_position(m.start);
698 let (end_line, end_col) = self.editor.line_index.char_offset_to_position(m.end);
699
700 selections.push(Selection {
701 start: Position::new(start_line, start_col),
702 end: Position::new(end_line, end_col),
703 direction: SelectionDirection::Forward,
704 });
705
706 let new_primary_index = selections.len().saturating_sub(1);
707 self.execute_cursor(CursorCommand::SetSelections {
708 selections,
709 primary_index: new_primary_index,
710 })?;
711
712 Ok(CommandResult::Success)
713 }
714
715 pub(super) fn execute_add_all_occurrences_command(
716 &mut self,
717 options: SearchOptions,
718 ) -> Result<CommandResult, CommandError> {
719 let snapshot = self.snapshot_selection_set();
720 let selections = snapshot.selections;
721 let primary_index = snapshot.primary_index;
722
723 let Some((query, primary_range)) = self.selection_query(&selections, primary_index) else {
724 return Ok(CommandResult::Success);
725 };
726 if query.is_empty() {
727 return Ok(CommandResult::Success);
728 }
729
730 let text = self.editor.get_text();
731 let matches =
732 find_all(&text, &query, options).map_err(|err| CommandError::Other(err.to_string()))?;
733
734 if matches.is_empty() {
735 return Ok(CommandResult::Success);
736 }
737
738 let mut out: Vec<Selection> = Vec::with_capacity(matches.len());
739 let mut next_primary = 0usize;
740 let primary_range = primary_range.filter(|r| r.start != r.end);
741
742 for (idx, m) in matches.iter().enumerate() {
743 let (start_line, start_col) = self.editor.line_index.char_offset_to_position(m.start);
744 let (end_line, end_col) = self.editor.line_index.char_offset_to_position(m.end);
745 out.push(Selection {
746 start: Position::new(start_line, start_col),
747 end: Position::new(end_line, end_col),
748 direction: SelectionDirection::Forward,
749 });
750
751 if let Some(pr) = primary_range
752 && pr.start == m.start
753 && pr.end == m.end
754 {
755 next_primary = idx;
756 }
757 }
758
759 self.execute_cursor(CursorCommand::SetSelections {
760 selections: out,
761 primary_index: next_primary,
762 })?;
763
764 Ok(CommandResult::Success)
765 }
766}
767
768impl CommandExecutor {
769 pub(super) fn execute_cursor(
770 &mut self,
771 command: CursorCommand,
772 ) -> Result<CommandResult, CommandError> {
773 match command {
774 CursorCommand::MoveTo { line, column } => {
775 if line >= self.editor.line_index.line_count() {
776 return Err(CommandError::InvalidPosition { line, column });
777 }
778
779 let clamped_column = self.clamp_column_for_line(line, column);
780 self.editor.cursor_position = Position::new(line, clamped_column);
781 self.preferred_x_cells = self
782 .editor
783 .logical_position_to_visual(line, clamped_column)
784 .map(|(_, x)| x);
785 self.editor.secondary_selections.clear();
787 Ok(CommandResult::Success)
788 }
789 CursorCommand::MoveBy {
790 delta_line,
791 delta_column,
792 } => {
793 let new_line = if delta_line >= 0 {
794 self.editor.cursor_position.line + delta_line as usize
795 } else {
796 self.editor
797 .cursor_position
798 .line
799 .saturating_sub((-delta_line) as usize)
800 };
801
802 let new_column = if delta_column >= 0 {
803 self.editor.cursor_position.column + delta_column as usize
804 } else {
805 self.editor
806 .cursor_position
807 .column
808 .saturating_sub((-delta_column) as usize)
809 };
810
811 if new_line >= self.editor.line_index.line_count() {
812 return Err(CommandError::InvalidPosition {
813 line: new_line,
814 column: new_column,
815 });
816 }
817
818 let clamped_column = self.clamp_column_for_line(new_line, new_column);
819 self.editor.cursor_position = Position::new(new_line, clamped_column);
820 self.preferred_x_cells = self
821 .editor
822 .logical_position_to_visual(new_line, clamped_column)
823 .map(|(_, x)| x);
824 Ok(CommandResult::Success)
825 }
826 CursorCommand::MoveGraphemeLeft => {
827 let line_count = self.editor.line_index.line_count();
828 if line_count == 0 {
829 return Ok(CommandResult::Success);
830 }
831
832 let mut line = self
833 .editor
834 .cursor_position
835 .line
836 .min(line_count.saturating_sub(1));
837 let mut line_text = self
838 .editor
839 .line_index
840 .get_line_text(line)
841 .unwrap_or_default();
842 let mut line_char_len = line_text.chars().count();
843 let mut col = self.editor.cursor_position.column.min(line_char_len);
844
845 if col == 0 {
846 if line == 0 {
847 return Ok(CommandResult::Success);
848 }
849 line = line.saturating_sub(1);
850 line_text = self
851 .editor
852 .line_index
853 .get_line_text(line)
854 .unwrap_or_default();
855 line_char_len = line_text.chars().count();
856 col = line_char_len;
857 } else {
858 col = prev_boundary_column(&line_text, col, TextBoundary::Grapheme);
859 }
860
861 self.editor.cursor_position = Position::new(line, col);
862 self.preferred_x_cells = self
863 .editor
864 .logical_position_to_visual(line, col)
865 .map(|(_, x)| x);
866 Ok(CommandResult::Success)
867 }
868 CursorCommand::MoveGraphemeRight => {
869 let line_count = self.editor.line_index.line_count();
870 if line_count == 0 {
871 return Ok(CommandResult::Success);
872 }
873
874 let line = self
875 .editor
876 .cursor_position
877 .line
878 .min(line_count.saturating_sub(1));
879 let line_text = self
880 .editor
881 .line_index
882 .get_line_text(line)
883 .unwrap_or_default();
884 let line_char_len = line_text.chars().count();
885 let col = self.editor.cursor_position.column.min(line_char_len);
886
887 let (line, col) = if col >= line_char_len {
888 if line + 1 >= line_count {
889 return Ok(CommandResult::Success);
890 }
891 (line + 1, 0)
892 } else {
893 (
894 line,
895 next_boundary_column(&line_text, col, TextBoundary::Grapheme),
896 )
897 };
898
899 self.editor.cursor_position = Position::new(line, col);
900 self.preferred_x_cells = self
901 .editor
902 .logical_position_to_visual(line, col)
903 .map(|(_, x)| x);
904 Ok(CommandResult::Success)
905 }
906 CursorCommand::MoveWordLeft => {
907 let line_count = self.editor.line_index.line_count();
908 if line_count == 0 {
909 return Ok(CommandResult::Success);
910 }
911
912 let mut line = self
913 .editor
914 .cursor_position
915 .line
916 .min(line_count.saturating_sub(1));
917 let mut line_text = self
918 .editor
919 .line_index
920 .get_line_text(line)
921 .unwrap_or_default();
922 let mut line_char_len = line_text.chars().count();
923 let mut col = self.editor.cursor_position.column.min(line_char_len);
924
925 if col == 0 {
926 if line == 0 {
927 return Ok(CommandResult::Success);
928 }
929 line = line.saturating_sub(1);
930 line_text = self
931 .editor
932 .line_index
933 .get_line_text(line)
934 .unwrap_or_default();
935 line_char_len = line_text.chars().count();
936 col = line_char_len;
937 } else {
938 col = prev_boundary_column(&line_text, col, TextBoundary::Word);
939 }
940
941 self.editor.cursor_position = Position::new(line, col);
942 self.preferred_x_cells = self
943 .editor
944 .logical_position_to_visual(line, col)
945 .map(|(_, x)| x);
946 Ok(CommandResult::Success)
947 }
948 CursorCommand::MoveWordRight => {
949 let line_count = self.editor.line_index.line_count();
950 if line_count == 0 {
951 return Ok(CommandResult::Success);
952 }
953
954 let line = self
955 .editor
956 .cursor_position
957 .line
958 .min(line_count.saturating_sub(1));
959 let line_text = self
960 .editor
961 .line_index
962 .get_line_text(line)
963 .unwrap_or_default();
964 let line_char_len = line_text.chars().count();
965 let col = self.editor.cursor_position.column.min(line_char_len);
966
967 let (line, col) = if col >= line_char_len {
968 if line + 1 >= line_count {
969 return Ok(CommandResult::Success);
970 }
971 (line + 1, 0)
972 } else {
973 (
974 line,
975 next_boundary_column(&line_text, col, TextBoundary::Word),
976 )
977 };
978
979 self.editor.cursor_position = Position::new(line, col);
980 self.preferred_x_cells = self
981 .editor
982 .logical_position_to_visual(line, col)
983 .map(|(_, x)| x);
984 Ok(CommandResult::Success)
985 }
986 CursorCommand::MoveVisualBy { delta_rows } => {
987 let Some((current_row, current_x)) = self.editor.logical_position_to_visual(
988 self.editor.cursor_position.line,
989 self.editor.cursor_position.column,
990 ) else {
991 return Ok(CommandResult::Success);
992 };
993
994 let preferred_x = self.preferred_x_cells.unwrap_or(current_x);
995 self.preferred_x_cells = Some(preferred_x);
996
997 let total_visual = self.editor.visual_line_count();
998 if total_visual == 0 {
999 return Ok(CommandResult::Success);
1000 }
1001
1002 let target_row = if delta_rows >= 0 {
1003 current_row.saturating_add(delta_rows as usize)
1004 } else {
1005 current_row.saturating_sub((-delta_rows) as usize)
1006 }
1007 .min(total_visual.saturating_sub(1));
1008
1009 let Some(pos) = self
1010 .editor
1011 .visual_position_to_logical(target_row, preferred_x)
1012 else {
1013 return Ok(CommandResult::Success);
1014 };
1015
1016 self.editor.cursor_position = pos;
1017 Ok(CommandResult::Success)
1018 }
1019 CursorCommand::MoveToVisual { row, x_cells } => {
1020 let Some(pos) = self.editor.visual_position_to_logical(row, x_cells) else {
1021 return Ok(CommandResult::Success);
1022 };
1023
1024 self.editor.cursor_position = pos;
1025 self.preferred_x_cells = Some(x_cells);
1026 self.editor.secondary_selections.clear();
1028 Ok(CommandResult::Success)
1029 }
1030 CursorCommand::MoveToLineStart => {
1031 let line = self.editor.cursor_position.line;
1032 self.editor.cursor_position = Position::new(line, 0);
1033 self.preferred_x_cells = Some(0);
1034 self.editor.secondary_selections.clear();
1035 Ok(CommandResult::Success)
1036 }
1037 CursorCommand::MoveToLineEnd => {
1038 let line = self.editor.cursor_position.line;
1039 let end_col = self.clamp_column_for_line(line, usize::MAX);
1040 self.editor.cursor_position = Position::new(line, end_col);
1041 self.preferred_x_cells = self
1042 .editor
1043 .logical_position_to_visual(line, end_col)
1044 .map(|(_, x)| x);
1045 self.editor.secondary_selections.clear();
1046 Ok(CommandResult::Success)
1047 }
1048 CursorCommand::MoveToVisualLineStart => {
1049 let line = self.editor.cursor_position.line;
1050 let Some(layout) = self.editor.layout_engine.get_line_layout(line) else {
1051 return Ok(CommandResult::Success);
1052 };
1053
1054 let line_text = self
1055 .editor
1056 .line_index
1057 .get_line_text(line)
1058 .unwrap_or_default();
1059 let line_char_len = line_text.chars().count();
1060 let column = self.editor.cursor_position.column.min(line_char_len);
1061
1062 let mut seg_start = 0usize;
1063 for wp in &layout.wrap_points {
1064 if column >= wp.char_index {
1065 seg_start = wp.char_index;
1066 } else {
1067 break;
1068 }
1069 }
1070
1071 self.editor.cursor_position = Position::new(line, seg_start);
1072 self.preferred_x_cells = self
1073 .editor
1074 .logical_position_to_visual(line, seg_start)
1075 .map(|(_, x)| x);
1076 self.editor.secondary_selections.clear();
1077 Ok(CommandResult::Success)
1078 }
1079 CursorCommand::MoveToVisualLineEnd => {
1080 let line = self.editor.cursor_position.line;
1081 let Some(layout) = self.editor.layout_engine.get_line_layout(line) else {
1082 return Ok(CommandResult::Success);
1083 };
1084
1085 let line_text = self
1086 .editor
1087 .line_index
1088 .get_line_text(line)
1089 .unwrap_or_default();
1090 let line_char_len = line_text.chars().count();
1091 let column = self.editor.cursor_position.column.min(line_char_len);
1092
1093 let mut seg_end = line_char_len;
1094 for wp in &layout.wrap_points {
1095 if column < wp.char_index {
1096 seg_end = wp.char_index;
1097 break;
1098 }
1099 }
1100
1101 self.editor.cursor_position = Position::new(line, seg_end);
1102 self.preferred_x_cells = self
1103 .editor
1104 .logical_position_to_visual(line, seg_end)
1105 .map(|(_, x)| x);
1106 self.editor.secondary_selections.clear();
1107 Ok(CommandResult::Success)
1108 }
1109 CursorCommand::SetSelection { start, end } => {
1110 if start.line >= self.editor.line_index.line_count()
1111 || end.line >= self.editor.line_index.line_count()
1112 {
1113 return Err(CommandError::InvalidPosition {
1114 line: start.line.max(end.line),
1115 column: start.column.max(end.column),
1116 });
1117 }
1118
1119 let start = Position::new(
1120 start.line,
1121 self.clamp_column_for_line(start.line, start.column),
1122 );
1123 let end = Position::new(end.line, self.clamp_column_for_line(end.line, end.column));
1124
1125 let direction = if start.line < end.line
1126 || (start.line == end.line && start.column <= end.column)
1127 {
1128 SelectionDirection::Forward
1129 } else {
1130 SelectionDirection::Backward
1131 };
1132
1133 self.editor.selection = Some(Selection {
1134 start,
1135 end,
1136 direction,
1137 });
1138 Ok(CommandResult::Success)
1139 }
1140 CursorCommand::ExtendSelection { to } => {
1141 if to.line >= self.editor.line_index.line_count() {
1142 return Err(CommandError::InvalidPosition {
1143 line: to.line,
1144 column: to.column,
1145 });
1146 }
1147
1148 let to = Position::new(to.line, self.clamp_column_for_line(to.line, to.column));
1149
1150 if let Some(ref mut selection) = self.editor.selection {
1151 selection.end = to;
1152 selection.direction = if selection.start.line < to.line
1153 || (selection.start.line == to.line && selection.start.column <= to.column)
1154 {
1155 SelectionDirection::Forward
1156 } else {
1157 SelectionDirection::Backward
1158 };
1159 } else {
1160 self.editor.selection = Some(Selection {
1162 start: self.editor.cursor_position,
1163 end: to,
1164 direction: if self.editor.cursor_position.line < to.line
1165 || (self.editor.cursor_position.line == to.line
1166 && self.editor.cursor_position.column <= to.column)
1167 {
1168 SelectionDirection::Forward
1169 } else {
1170 SelectionDirection::Backward
1171 },
1172 });
1173 }
1174 Ok(CommandResult::Success)
1175 }
1176 CursorCommand::ClearSelection => {
1177 self.editor.selection = None;
1178 Ok(CommandResult::Success)
1179 }
1180 CursorCommand::SetSelections {
1181 selections,
1182 primary_index,
1183 } => {
1184 let line_count = self.editor.line_index.line_count();
1185 if selections.is_empty() {
1186 return Err(CommandError::Other(
1187 "SetSelections requires a non-empty selection list".to_string(),
1188 ));
1189 }
1190 if primary_index >= selections.len() {
1191 return Err(CommandError::Other(format!(
1192 "Invalid primary_index {} for {} selections",
1193 primary_index,
1194 selections.len()
1195 )));
1196 }
1197
1198 for sel in &selections {
1199 if sel.start.line >= line_count || sel.end.line >= line_count {
1200 return Err(CommandError::InvalidPosition {
1201 line: sel.start.line.max(sel.end.line),
1202 column: sel.start.column.max(sel.end.column),
1203 });
1204 }
1205 }
1206
1207 let (selections, primary_index) =
1208 crate::selection_set::normalize_selections(selections, primary_index);
1209
1210 let primary = selections
1211 .get(primary_index)
1212 .cloned()
1213 .ok_or_else(|| CommandError::Other("Invalid primary selection".to_string()))?;
1214
1215 self.editor.cursor_position = primary.end;
1216 self.editor.selection = if primary.start == primary.end {
1217 None
1218 } else {
1219 Some(primary.clone())
1220 };
1221
1222 self.editor.secondary_selections = selections
1223 .into_iter()
1224 .enumerate()
1225 .filter_map(|(idx, sel)| {
1226 if idx == primary_index {
1227 None
1228 } else {
1229 Some(sel)
1230 }
1231 })
1232 .collect();
1233
1234 Ok(CommandResult::Success)
1235 }
1236 CursorCommand::ClearSecondarySelections => {
1237 self.editor.secondary_selections.clear();
1238 Ok(CommandResult::Success)
1239 }
1240 CursorCommand::SetRectSelection { anchor, active } => {
1241 let line_count = self.editor.line_index.line_count();
1242 if anchor.line >= line_count || active.line >= line_count {
1243 return Err(CommandError::InvalidPosition {
1244 line: anchor.line.max(active.line),
1245 column: anchor.column.max(active.column),
1246 });
1247 }
1248
1249 let (selections, primary_index) =
1250 crate::selection_set::rect_selections(anchor, active);
1251
1252 self.execute_cursor(CursorCommand::SetSelections {
1254 selections,
1255 primary_index,
1256 })?;
1257 Ok(CommandResult::Success)
1258 }
1259 CursorCommand::SelectLine => self.execute_select_line_command(),
1260 CursorCommand::SelectWord => self.execute_select_word_command(),
1261 CursorCommand::ExpandSelection => self.execute_expand_selection_command(),
1262 CursorCommand::ExpandSelectionBy {
1263 unit,
1264 count,
1265 direction,
1266 } => self.execute_expand_selection_by_command(unit, count, direction),
1267 CursorCommand::AddCursorAbove => self.execute_add_cursor_vertical_command(true),
1268 CursorCommand::AddCursorBelow => self.execute_add_cursor_vertical_command(false),
1269 CursorCommand::AddNextOccurrence { options } => {
1270 self.execute_add_next_occurrence_command(options)
1271 }
1272 CursorCommand::AddAllOccurrences { options } => {
1273 self.execute_add_all_occurrences_command(options)
1274 }
1275 CursorCommand::MoveToMatchingBracket => self.execute_move_to_matching_bracket_command(),
1276 CursorCommand::SnippetNextPlaceholder => self.execute_snippet_navigation_command(true),
1277 CursorCommand::SnippetPrevPlaceholder => self.execute_snippet_navigation_command(false),
1278 CursorCommand::FindNext { query, options } => {
1279 self.execute_find_command(query, options, true)
1280 }
1281 CursorCommand::FindPrev { query, options } => {
1282 self.execute_find_command(query, options, false)
1283 }
1284 }
1285 }
1286
1287 }
1289
1290impl CommandExecutor {
1291 pub(super) fn position_to_char_offset_clamped(&self, pos: Position) -> usize {
1292 let line_count = self.editor.line_index.line_count();
1293 if line_count == 0 {
1294 return 0;
1295 }
1296
1297 let line = pos.line.min(line_count.saturating_sub(1));
1298 let line_text = self
1299 .editor
1300 .line_index
1301 .get_line_text(line)
1302 .unwrap_or_default();
1303 let line_char_len = line_text.chars().count();
1304 let column = pos.column.min(line_char_len);
1305 self.editor.line_index.position_to_char_offset(line, column)
1306 }
1307
1308 pub(super) fn position_to_char_offset_and_virtual_pad(&self, pos: Position) -> (usize, usize) {
1309 let line_count = self.editor.line_index.line_count();
1310 if line_count == 0 {
1311 return (0, 0);
1312 }
1313
1314 let line = pos.line.min(line_count.saturating_sub(1));
1315 let line_text = self
1316 .editor
1317 .line_index
1318 .get_line_text(line)
1319 .unwrap_or_default();
1320 let line_char_len = line_text.chars().count();
1321 let clamped_col = pos.column.min(line_char_len);
1322 let offset = self
1323 .editor
1324 .line_index
1325 .position_to_char_offset(line, clamped_col);
1326 let pad = pos.column.saturating_sub(clamped_col);
1327 (offset, pad)
1328 }
1329
1330 pub(super) fn normalize_cursor_and_selection(&mut self) {
1331 let line_index = &self.editor.line_index;
1332 let line_count = line_index.line_count();
1333 if line_count == 0 {
1334 self.editor.cursor_position = Position::new(0, 0);
1335 self.editor.selection = None;
1336 self.editor.secondary_selections.clear();
1337 return;
1338 }
1339
1340 self.editor.cursor_position =
1341 Self::clamp_position_lenient_with_index(line_index, self.editor.cursor_position);
1342
1343 if let Some(ref mut selection) = self.editor.selection {
1344 selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
1345 selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
1346 selection.direction = if selection.start.line < selection.end.line
1347 || (selection.start.line == selection.end.line
1348 && selection.start.column <= selection.end.column)
1349 {
1350 SelectionDirection::Forward
1351 } else {
1352 SelectionDirection::Backward
1353 };
1354 }
1355
1356 for selection in &mut self.editor.secondary_selections {
1357 selection.start = Self::clamp_position_lenient_with_index(line_index, selection.start);
1358 selection.end = Self::clamp_position_lenient_with_index(line_index, selection.end);
1359 selection.direction = if selection.start.line < selection.end.line
1360 || (selection.start.line == selection.end.line
1361 && selection.start.column <= selection.end.column)
1362 {
1363 SelectionDirection::Forward
1364 } else {
1365 SelectionDirection::Backward
1366 };
1367 }
1368 }
1369
1370 pub(super) fn clamp_column_for_line(&self, line: usize, column: usize) -> usize {
1371 Self::clamp_column_for_line_with_index(&self.editor.line_index, line, column)
1372 }
1373
1374 pub(super) fn clamp_position_lenient_with_index(
1375 line_index: &LineIndex,
1376 pos: Position,
1377 ) -> Position {
1378 let line_count = line_index.line_count();
1379 if line_count == 0 {
1380 return Position::new(0, 0);
1381 }
1382
1383 let clamped_line = pos.line.min(line_count.saturating_sub(1));
1384 Position::new(clamped_line, pos.column)
1386 }
1387
1388 pub(super) fn clamp_column_for_line_with_index(
1389 line_index: &LineIndex,
1390 line: usize,
1391 column: usize,
1392 ) -> usize {
1393 let line_start = line_index.position_to_char_offset(line, 0);
1394 let line_end = line_index.position_to_char_offset(line, usize::MAX);
1395 let line_len = line_end.saturating_sub(line_start);
1396 column.min(line_len)
1397 }
1398}