1use crate::editor::{
2 ApplyEditPlanError, EditBuffer, EditCommand, EditCommandCategory, EditOutcome, EditPlan,
3 WordStyle, classify_key_event,
4};
5use crossterm::event::KeyCode;
6use crossterm::event::KeyEvent;
7use crossterm::event::KeyModifiers;
8use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
9use ratatui::buffer::Buffer;
10use ratatui::layout::Rect;
11use ratatui::style::Color;
12use ratatui::style::Style;
13use ratatui::text::Line;
14use ratatui::widgets::StatefulWidgetRef;
15use ratatui::widgets::WidgetRef;
16use ratatui_core::buffer::Buffer as CoreBuffer;
17use ratatui_core::layout::Rect as CoreRect;
18use ratatui_core::widgets::Widget as _;
19use std::cell::Ref;
20use std::cell::RefCell;
21use std::ops::Range;
22use std::time::Instant;
23use textwrap::Options;
24use tui_scrollbar::{ScrollBar, ScrollLengths};
25use unicode_segmentation::UnicodeSegmentation;
26use unicode_width::UnicodeWidthStr;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub struct ElementId(u64);
34
35impl ElementId {
36 pub fn from_raw(raw: u64) -> Self {
41 Self(raw)
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct ElementKind(pub u16);
50
51pub trait ClipboardProvider: std::fmt::Debug + Send {
59 fn get(&mut self) -> Option<String>;
61 fn set(&mut self, text: &str);
63}
64
65#[derive(Debug, Default)]
67pub struct InternalClipboard {
68 contents: Option<String>,
69}
70
71impl ClipboardProvider for InternalClipboard {
72 fn get(&mut self) -> Option<String> {
73 self.contents.clone()
74 }
75
76 fn set(&mut self, text: &str) {
77 self.contents = Some(text.to_string());
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct TextElementEvent {
86 pub id: ElementId,
88 pub kind: TextElementEventKind,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum TextElementEventKind {
95 Click,
97 HoverEnter,
99 HoverLeave,
101}
102
103#[derive(Debug, Clone)]
109pub struct TextElement {
110 pub id: ElementId,
112 pub range: Range<usize>,
114 pub kind: ElementKind,
116 pub display: Option<Line<'static>>,
120}
121
122#[derive(Debug, Clone, Copy)]
126pub struct Selection {
127 pub anchor: usize,
129 pub head: usize,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum MouseAction {
138 Nothing,
140 CursorPlaced,
142 SelectionUpdated,
144 SelectionFinished,
147 Scrolled,
149}
150
151#[derive(Debug)]
154struct ClickTracker {
155 last_time: Instant,
156 last_pos: (u16, u16),
157 count: u8,
158}
159
160impl Default for ClickTracker {
161 fn default() -> Self {
162 Self {
163 last_time: Instant::now(),
164 last_pos: (u16::MAX, u16::MAX),
165 count: 0,
166 }
167 }
168}
169
170impl ClickTracker {
171 const MULTI_CLICK_MS: u128 = 500;
173
174 fn register(&mut self, col: u16, row: u16) -> u8 {
176 let now = Instant::now();
177 let elapsed = now.duration_since(self.last_time).as_millis();
178 if elapsed < Self::MULTI_CLICK_MS && self.last_pos == (col, row) && self.count < 3 {
179 self.count += 1;
180 } else {
181 self.count = 1;
182 }
183 self.last_time = now;
184 self.last_pos = (col, row);
185 self.count
186 }
187}
188
189#[derive(Debug)]
190pub struct TextArea {
191 text: EditBuffer,
192 wrap_cache: RefCell<Option<WrapCache>>,
193 preferred_col: Option<usize>,
194 elements: Vec<TextElement>,
195 next_element_id: u64,
196 kill_buffer: String,
197 undo: UndoState,
198 selection: Option<Selection>,
200 clipboard_provider: Box<dyn ClipboardProvider + Send>,
203 clipboard: Option<String>,
207 pub keep_selection_after_mouseup: bool,
210 pub selection_style: Style,
220 mouse_down_pos: Option<(u16, u16)>,
222 drag_anchor: Option<usize>,
224 drag_active: bool,
226 last_drag_scroll: Option<Instant>,
228 drag_scroll_steps: u32,
230 pending_drag_scroll: Option<MouseEvent>,
233 click_tracker: ClickTracker,
235 scroll_override: Option<u16>,
240 pub show_scrollbar: bool,
244 pub scrollbar_track_style: Style,
248 pub scrollbar_thumb_style: Style,
251 pub scrollbar_padding: u16,
254 scrollbar_dragging: bool,
256 hovered_element: Option<ElementId>,
258 pending_element_event: Option<TextElementEvent>,
260 tab_width: u8,
264}
265
266#[derive(Debug, Clone)]
267struct WrapCache {
268 width: u16,
269 lines: Vec<Range<usize>>,
270}
271
272#[derive(Debug, Default, Clone, Copy)]
273pub struct TextAreaState {
274 pub scroll: u16,
276}
277
278#[derive(Debug, Clone)]
282struct UndoEntry {
283 text: String,
284 cursor: usize,
285 elements: Vec<TextElement>,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291enum MutationKind {
292 Insert,
294 Delete,
296 Kill,
298 Element,
300 Replace,
302}
303
304#[derive(Debug)]
306struct UndoState {
307 stack: Vec<UndoEntry>,
308 redo: Vec<UndoEntry>,
309 max_depth: usize,
310 last_kind: Option<MutationKind>,
312 last_cursor: usize,
315 last_insert_ws: bool,
318 group_depth: usize,
320 group_checkpoint: Option<UndoEntry>,
324}
325
326impl Default for UndoState {
327 fn default() -> Self {
328 Self {
329 stack: Vec::new(),
330 redo: Vec::new(),
331 max_depth: 100,
332 last_kind: None,
333 last_cursor: 0,
334 last_insert_ws: false,
335 group_depth: 0,
336 group_checkpoint: None,
337 }
338 }
339}
340
341pub fn is_undo_input(key: &KeyEvent) -> bool {
349 matches!(key.code, KeyCode::Char('z'))
350 && (key.modifiers.contains(KeyModifiers::CONTROL)
351 || key.modifiers.contains(KeyModifiers::SUPER))
352}
353
354impl TextArea {
355 fn scroll_lines_for_height(height: u16) -> u16 {
359 match height {
360 0..=5 => 1,
361 6..=15 => 2,
362 _ => 3,
363 }
364 }
365
366 const DRAG_SCROLL_RAMP_MS: &[u128] = &[80, 60, 40];
369
370 fn drag_scroll_interval(step: u32) -> u128 {
372 let ramp = Self::DRAG_SCROLL_RAMP_MS;
373 ramp[ramp.len().min(step as usize + 1) - 1]
374 }
375
376 fn drag_scroll_lines_for_distance(distance: u16) -> usize {
379 match distance {
380 0..=2 => 1,
381 3..=5 => 2,
382 6..=10 => 3,
383 _ => 5,
384 }
385 }
386
387 fn clamp_to_line(&self, pos: usize, line_start: usize, line_end: usize) -> usize {
397 if line_end > line_start {
398 let last_char_start = self.text[line_start..line_end]
400 .char_indices()
401 .next_back()
402 .map(|(i, _)| line_start + i)
403 .unwrap_or(line_start);
404 pos.min(last_char_start)
405 } else {
406 line_start
407 }
408 }
409
410 pub fn new() -> Self {
411 Self {
412 text: EditBuffer::new(),
413 wrap_cache: RefCell::new(None),
414 preferred_col: None,
415 elements: Vec::new(),
416 next_element_id: 0,
417 kill_buffer: String::new(),
418 undo: UndoState::default(),
419 selection: None,
420 clipboard_provider: Box::new(InternalClipboard::default()),
421 clipboard: None,
422 keep_selection_after_mouseup: true,
423 selection_style: Style::default()
424 .bg(Color::Rgb(49, 62, 115))
425 .fg(Color::Rgb(192, 202, 245)),
426 mouse_down_pos: None,
427 drag_anchor: None,
428 drag_active: false,
429 last_drag_scroll: None,
430 drag_scroll_steps: 0,
431 pending_drag_scroll: None,
432 click_tracker: ClickTracker::default(),
433 scroll_override: None,
434 show_scrollbar: true,
435 scrollbar_track_style: Style::default().bg(Color::Rgb(32, 35, 53)),
436 scrollbar_thumb_style: Style::default()
437 .fg(Color::Rgb(42, 46, 65))
438 .bg(Color::Rgb(32, 35, 53)),
439 scrollbar_padding: 0,
440 scrollbar_dragging: false,
441 hovered_element: None,
442 pending_element_event: None,
443 tab_width: 4,
444 }
445 }
446
447 pub fn tab_width(&self) -> u8 {
449 self.tab_width
450 }
451
452 pub fn set_tab_width(&mut self, tab_width: u8) {
454 if self.tab_width != tab_width {
455 self.tab_width = tab_width;
456 self.wrap_cache.replace(None);
457 }
458 }
459
460 pub fn expand_tabs<'a>(&self, text: &'a str) -> std::borrow::Cow<'a, str> {
468 expand_tabs_with_width(text, self.tab_width)
469 }
470
471 fn plain_display_width(&self, text: &str) -> usize {
473 plain_display_width_with_tab(text, self.tab_width)
474 }
475
476 fn grapheme_display_width(&self, grapheme: &str) -> usize {
478 grapheme_display_width_with_tab(grapheme, self.tab_width)
479 }
480
481 fn element_ranges(&self) -> Vec<Range<usize>> {
482 self.elements
483 .iter()
484 .map(|element| element.range.clone())
485 .collect()
486 }
487
488 fn adjust_position_after_edit(
489 position: usize,
490 replaced: &Range<usize>,
491 inserted_len: usize,
492 ) -> usize {
493 if position < replaced.start {
494 position
495 } else if position <= replaced.end {
496 replaced.start + inserted_len
497 } else {
498 position - replaced.len() + inserted_len
499 }
500 }
501
502 fn is_semantic_edit(plan: &EditPlan) -> bool {
503 plan.removed_text() != plan.replacement() || !plan.replaced_byte_range().is_empty()
504 }
505
506 fn assert_valid_edit_plan(&self, plan: &EditPlan) {
507 if let Err(error) = self.text.validate_plan(plan) {
508 panic!("textarea edit invariant failed: {error:?}");
509 }
510 }
511
512 fn apply_validated_edit_plan(
513 &mut self,
514 plan: EditPlan,
515 mutation_kind: Option<MutationKind>,
516 ) -> EditOutcome {
517 let semantic_edit = Self::is_semantic_edit(&plan);
518 let replaced = plan.replaced_byte_range();
519 let inserted_len = plan.replacement().len();
520 let outcome = self.text.apply_validated_plan(&plan);
521 if semantic_edit {
522 self.update_elements_after_replace(replaced.start, replaced.end, inserted_len);
523 if let Some(selection) = &mut self.selection {
524 selection.anchor =
525 Self::adjust_position_after_edit(selection.anchor, &replaced, inserted_len);
526 selection.head =
527 Self::adjust_position_after_edit(selection.head, &replaced, inserted_len);
528 }
529 if self
530 .selection
531 .is_some_and(|selection| selection.anchor == selection.head)
532 {
533 self.selection = None;
534 }
535 self.wrap_cache.replace(None);
536 if mutation_kind == Some(MutationKind::Kill) {
537 self.kill_buffer = plan.into_removed_text();
538 }
539 }
540 if semantic_edit || !matches!(outcome, EditOutcome::Unchanged) {
541 self.preferred_col = None;
542 self.scroll_override = None;
543 }
544 outcome
545 }
546
547 fn try_apply_edit_plan(
548 &mut self,
549 plan: EditPlan,
550 mutation_kind: Option<MutationKind>,
551 ) -> Result<EditOutcome, ApplyEditPlanError> {
552 self.text.validate_plan(&plan)?;
553 let semantic_edit = Self::is_semantic_edit(&plan);
554 if semantic_edit && let Some(kind) = mutation_kind {
555 self.pre_mutate(kind);
556 }
557 let outcome = self.apply_validated_edit_plan(plan, mutation_kind);
558 if semantic_edit && mutation_kind.is_some() {
559 self.post_mutate();
560 }
561 Ok(outcome)
562 }
563
564 fn apply_edit_plan(
565 &mut self,
566 plan: EditPlan,
567 mutation_kind: Option<MutationKind>,
568 ) -> EditOutcome {
569 match self.try_apply_edit_plan(plan, mutation_kind) {
570 Ok(outcome) => outcome,
571 Err(error) => panic!("textarea edit invariant failed: {error:?}"),
572 }
573 }
574
575 fn apply_edit_command(
576 &mut self,
577 command: EditCommand,
578 mutation_kind: Option<MutationKind>,
579 ) -> EditOutcome {
580 let category = command.category();
581 let ranges = self.element_ranges();
582 let plan = self.text.plan_command(command, &ranges);
583 let outcome = self.apply_edit_plan(plan, mutation_kind);
584 if category == EditCommandCategory::Navigation {
585 self.preferred_col = None;
586 self.scroll_override = None;
587 }
588 outcome
589 }
590
591 fn plan_edit_replacement(&self, range: Range<usize>, replacement: &str) -> EditPlan {
592 let replacement = self.expand_tabs(replacement).into_owned();
593 let ranges = self.element_ranges();
594 self.text
595 .plan_replace_byte_range(range, &replacement, &ranges)
596 }
597
598 fn apply_edit_replacement(
599 &mut self,
600 range: Range<usize>,
601 replacement: &str,
602 mutation_kind: Option<MutationKind>,
603 ) {
604 let plan = self.plan_edit_replacement(range, replacement);
605 self.apply_edit_plan(plan, mutation_kind);
606 }
607
608 pub fn set_text(&mut self, text: &str) {
609 let cursor = self.cursor();
610 let plan = self.plan_edit_replacement(0..self.text.len(), text);
611 self.assert_valid_edit_plan(&plan);
612 self.pre_mutate(MutationKind::Replace);
613 let _ = self.text.apply_validated_plan(&plan);
614 self.elements.clear();
615 let len = self.text.len();
616 self.set_cursor_inner(cursor.min(len));
617 self.wrap_cache.replace(None);
618 self.preferred_col = None;
619 self.selection = None;
622 self.mouse_down_pos = None;
623 self.drag_anchor = None;
624 self.drag_active = false;
625 self.last_drag_scroll = None;
626 self.drag_scroll_steps = 0;
627 self.pending_drag_scroll = None;
628 self.click_tracker = ClickTracker::default();
629 self.scroll_override = None;
630 self.scrollbar_dragging = false;
631 self.hovered_element = None;
632 self.pending_element_event = None;
633 self.post_mutate();
634 }
635
636 pub fn text(&self) -> &str {
637 self.text.text()
638 }
639
640 pub fn insert_str(&mut self, text: &str) {
641 if text.is_empty() {
642 return;
643 }
644 self.scroll_override = None;
645 if let Some(first) = text.chars().next() {
647 let first_ws = first.is_whitespace();
648 if self.undo.last_kind == Some(MutationKind::Insert)
649 && self.undo.last_insert_ws != first_ws
650 {
651 self.undo.last_kind = None;
653 }
654 }
655 self.apply_edit_replacement(
656 self.cursor()..self.cursor(),
657 text,
658 Some(MutationKind::Insert),
659 );
660 if let Some(last) = text.chars().last() {
661 self.undo.last_insert_ws = last.is_whitespace();
662 }
663 }
664
665 pub fn insert_str_at(&mut self, pos: usize, text: &str) {
666 if text.is_empty() {
667 return;
668 }
669 self.apply_edit_replacement(pos..pos, text, Some(MutationKind::Insert));
670 if let Some(last) = text.chars().last() {
671 self.undo.last_insert_ws = last.is_whitespace();
672 }
673 }
674
675 pub fn replace_range(&mut self, range: std::ops::Range<usize>, text: &str) {
676 self.apply_edit_replacement(range, text, Some(MutationKind::Replace));
677 }
678
679 pub fn cursor(&self) -> usize {
680 self.text.cursor_byte()
681 }
682
683 pub fn set_cursor(&mut self, pos: usize) {
684 let pos = pos.clamp(0, self.text.len());
685 let pos = self.clamp_pos_to_nearest_boundary(pos);
686 self.set_cursor_inner(pos);
687 self.preferred_col = None;
688 self.scroll_override = None;
689 }
690
691 fn set_cursor_inner(&mut self, pos: usize) {
692 let _ = self.text.set_cursor_byte(pos);
693 }
694
695 pub fn set_scroll_override(&mut self, scroll: Option<u16>) {
706 self.scroll_override = scroll;
707 }
708
709 pub fn scroll_override(&self) -> Option<u16> {
711 self.scroll_override
712 }
713
714 pub fn desired_height(&self, width: u16) -> u16 {
715 self.wrapped_lines(width).len() as u16
716 }
717
718 #[cfg_attr(not(test), allow(dead_code))]
719 pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
720 self.cursor_pos_with_state(area, TextAreaState::default())
721 }
722
723 pub fn cursor_pos_with_state(&self, area: Rect, state: TextAreaState) -> Option<(u16, u16)> {
733 let tw = self.text_width(area);
734 let lines = self.wrapped_lines(tw);
735 let effective_scroll = self.effective_scroll(area.height, &lines, state.scroll);
736 let mut i = Self::wrapped_line_index_by_start(&lines, self.cursor())?;
737 let ls = &lines[i];
738 let mut col = self.display_width_of_range(ls.start, self.cursor()) as u16;
739
740 if col >= tw {
746 i += 1;
747 col = 0;
748 }
749
750 let scroll = effective_scroll as usize;
752 if i < scroll || i >= scroll + area.height as usize {
753 return None;
754 }
755
756 let screen_row = (i - scroll) as u16;
757 Some((area.x + col, area.y + screen_row))
758 }
759
760 pub fn screen_position_of(
766 &self,
767 pos: usize,
768 area: Rect,
769 state: TextAreaState,
770 ) -> Option<(u16, u16)> {
771 let tw = self.text_width(area);
772 let lines = self.wrapped_lines(tw);
773 let effective_scroll = self.effective_scroll(area.height, &lines, state.scroll);
774 let i = Self::wrapped_line_index_by_start(&lines, pos)?;
775 let ls = &lines[i];
776 let col = self.display_width_of_range(ls.start, pos) as u16;
777
778 let scroll = effective_scroll as usize;
779 if i < scroll || i >= scroll + area.height as usize {
780 return None;
781 }
782
783 let screen_row = (i - scroll) as u16;
784 Some((area.x + col, area.y + screen_row))
785 }
786
787 pub fn screen_spans_of_range(
799 &self,
800 range: Range<usize>,
801 area: Rect,
802 state: TextAreaState,
803 ) -> Vec<Rect> {
804 let mut spans = Vec::new();
805 if range.start >= range.end
806 || range.end > self.text.len()
807 || !self.text.is_char_boundary(range.start)
808 || !self.text.is_char_boundary(range.end)
809 {
810 return spans;
811 }
812 let tw = self.text_width(area);
813 let lines = self.wrapped_lines(tw);
814 let scroll = self.effective_scroll(area.height, &lines, state.scroll) as usize;
815 let first = Self::wrapped_line_index_by_start(&lines, range.start).unwrap_or(0);
818 let right_edge = area.x.saturating_add(tw);
822 for (i, ls) in lines.iter().enumerate().skip(first) {
823 if ls.start >= range.end {
824 break;
825 }
826 if i < scroll {
827 continue;
828 }
829 if i >= scroll + area.height as usize {
830 break;
831 }
832 let seg_start = range.start.max(ls.start);
833 let seg_end = range.end.min(ls.end);
834 if seg_start >= seg_end {
835 continue;
836 }
837 let start_x = area
838 .x
839 .saturating_add(self.display_width_of_range(ls.start, seg_start) as u16)
840 .min(right_edge);
841 let end_x = area
842 .x
843 .saturating_add(self.display_width_of_range(ls.start, seg_end) as u16)
844 .min(right_edge);
845 if start_x < end_x {
846 spans.push(Rect {
847 x: start_x,
848 y: area.y + (i - scroll) as u16,
849 width: end_x - start_x,
850 height: 1,
851 });
852 }
853 }
854 spans
855 }
856
857 pub fn buffer_pos_at_screen(
866 &self,
867 col: u16,
868 row: u16,
869 area: Rect,
870 state: TextAreaState,
871 ) -> Option<usize> {
872 if col < area.x || col >= area.x + area.width || row < area.y || row >= area.y + area.height
874 {
875 return None;
876 }
877
878 let tw = self.text_width(area);
879 let lines = self.wrapped_lines(tw);
880 let scroll = self.effective_scroll(area.height, &lines, state.scroll);
881
882 let visual_row = (row - area.y) as usize + scroll as usize;
883
884 if visual_row >= lines.len() {
886 return Some(self.text.len());
887 }
888
889 let line = &lines[visual_row];
890 let target_col = (col - area.x) as usize;
891 let line_end = line.end.min(self.text.len());
893 Some(
894 self.display_col_to_buffer_pos(line.start, line_end, target_col)
895 .0,
896 )
897 }
898
899 fn buffer_pos_at_screen_ex(
902 &self,
903 col: u16,
904 row: u16,
905 area: Rect,
906 state: TextAreaState,
907 ) -> Option<(usize, bool)> {
908 if col < area.x || row < area.y {
909 return None;
910 }
911
912 let tw = self.text_width(area);
913 let lines = self.wrapped_lines(tw);
914 let scroll = self.effective_scroll(area.height, &lines, state.scroll);
915
916 let visual_row = (row - area.y) as usize + scroll as usize;
917
918 if visual_row >= lines.len() {
919 return Some((self.text.len(), false));
920 }
921
922 let line = &lines[visual_row];
923 let target_col = (col - area.x) as usize;
924 let line_end = line.end.min(self.text.len());
925 Some(self.display_col_to_buffer_pos(line.start, line_end, target_col))
926 }
927
928 pub fn element_at_screen(
933 &self,
934 col: u16,
935 row: u16,
936 area: Rect,
937 state: TextAreaState,
938 ) -> Option<&TextElement> {
939 let (pos, hit_element) = self.buffer_pos_at_screen_ex(col, row, area, state)?;
940 if hit_element {
941 self.elements
944 .iter()
945 .find(|e| pos >= e.range.start && pos <= e.range.end && !e.range.is_empty())
946 } else {
947 self.elements
948 .iter()
949 .find(|e| pos >= e.range.start && pos < e.range.end)
950 }
951 }
952
953 pub fn selection_range(&self) -> Option<Range<usize>> {
959 let sel = self.selection?;
960 if sel.anchor == sel.head {
961 return None;
962 }
963 let start = sel.anchor.min(sel.head);
964 let end = sel.anchor.max(sel.head);
965 let expanded = self.expand_range_to_element_boundaries(start..end);
966 let clamped_start = expanded.start.min(self.text.len());
967 let clamped_end = expanded.end.min(self.text.len());
968 if clamped_start >= clamped_end {
969 None
970 } else {
971 Some(clamped_start..clamped_end)
972 }
973 }
974
975 pub fn selected_text(&self) -> Option<String> {
977 let range = self.selection_range()?;
978 Some(self.text[range].to_string())
979 }
980
981 pub fn clear_selection(&mut self) {
983 self.selection = None;
984 }
985
986 pub fn delete_selection(&mut self) -> bool {
991 let Some(range) = self.selection_range() else {
992 return false;
993 };
994 let start = range.start;
995 self.apply_edit_replacement(range, "", Some(MutationKind::Replace));
996 self.set_cursor_inner(start.min(self.text.len()));
997 self.post_mutate();
998 self.selection = None;
999 true
1000 }
1001
1002 pub fn set_selection(&mut self, anchor: usize, head: usize) {
1004 self.selection = Some(Selection { anchor, head });
1005 }
1006
1007 pub fn take_clipboard(&mut self) -> Option<String> {
1012 self.clipboard.take()
1013 }
1014
1015 pub fn clipboard(&self) -> Option<&str> {
1017 self.clipboard.as_deref()
1018 }
1019
1020 pub fn set_clipboard_provider(&mut self, provider: Box<dyn ClipboardProvider + Send>) {
1024 self.clipboard_provider = provider;
1025 }
1026
1027 pub fn poll_element_event(&mut self) -> Option<TextElementEvent> {
1034 self.pending_element_event.take()
1035 }
1036
1037 fn set_clipboard_text(&mut self, text: String) {
1039 if !text.is_empty() {
1040 self.clipboard_provider.set(&text);
1041 self.clipboard = Some(text);
1042 }
1043 }
1044
1045 pub fn poll_timeout_ms(&self) -> Option<u64> {
1058 self.pending_drag_scroll.as_ref()?;
1060 let interval = Self::drag_scroll_interval(self.drag_scroll_steps);
1061 Some(interval as u64)
1062 }
1063
1064 pub fn tick(&mut self, area: Rect, state: TextAreaState) -> MouseAction {
1068 if let Some(event) = self.pending_drag_scroll {
1070 return self.handle_mouse(event, area, state);
1071 }
1072 MouseAction::Nothing
1073 }
1074
1075 fn element_click_snap(&mut self, pos: usize, hit_element: bool) -> Option<MouseAction> {
1083 if !hit_element {
1084 return None;
1085 }
1086 let elem = self
1087 .elements
1088 .iter()
1089 .find(|e| pos >= e.range.start && pos <= e.range.end && !e.range.is_empty())?;
1090 let id = elem.id;
1091 let start = elem.range.start;
1092 self.set_cursor_inner(start);
1093 self.preferred_col = None;
1094 self.drag_anchor = Some(start);
1095 self.pending_element_event = Some(TextElementEvent {
1096 id,
1097 kind: TextElementEventKind::Click,
1098 });
1099 Some(MouseAction::CursorPlaced)
1100 }
1101
1102 pub fn handle_mouse(
1108 &mut self,
1109 event: MouseEvent,
1110 area: Rect,
1111 state: TextAreaState,
1112 ) -> MouseAction {
1113 let tw = self.text_width(area);
1117 let has_scrollbar = self.show_scrollbar && tw < area.width;
1118 let on_scrollbar = has_scrollbar && event.column == area.x + area.width - 1;
1119
1120 if self.scrollbar_dragging {
1122 match event.kind {
1123 MouseEventKind::Drag(MouseButton::Left)
1124 | MouseEventKind::Down(MouseButton::Left) => {
1125 return self.handle_scrollbar_click(event.row, area, tw);
1126 }
1127 MouseEventKind::Up(MouseButton::Left) => {
1128 self.scrollbar_dragging = false;
1129 return MouseAction::Scrolled;
1130 }
1131 _ => {}
1132 }
1133 }
1134
1135 if on_scrollbar && let MouseEventKind::Down(MouseButton::Left) = event.kind {
1136 self.scrollbar_dragging = true;
1137 if self.is_scrollbar_thumb_at(event.row, area, tw) {
1140 return MouseAction::Scrolled;
1141 }
1142 return self.handle_scrollbar_click(event.row, area, tw);
1143 }
1144
1145 match event.kind {
1146 MouseEventKind::Down(MouseButton::Left) => {
1147 if self.drag_active {
1152 return self.handle_mouse(
1153 MouseEvent {
1154 kind: MouseEventKind::Drag(MouseButton::Left),
1155 ..event
1156 },
1157 area,
1158 state,
1159 );
1160 }
1161
1162 let col = event.column;
1163 let row = event.row;
1164
1165 let click_count = self.click_tracker.register(col, row);
1167
1168 self.mouse_down_pos = Some((col, row));
1170 self.drag_active = false;
1171 self.last_drag_scroll = None;
1172 self.drag_scroll_steps = 0;
1173 self.pending_drag_scroll = None;
1174
1175 self.clear_selection();
1177
1178 let Some((pos, hit_element)) = self.buffer_pos_at_screen_ex(col, row, area, state)
1183 else {
1184 self.scroll_override = None;
1185 self.drag_anchor = None;
1186 return MouseAction::Nothing;
1187 };
1188
1189 self.scroll_override = None;
1192
1193 match click_count {
1194 2 => {
1195 if let Some(action) = self.element_click_snap(pos, hit_element) {
1204 return action;
1205 }
1206 let is_ws = pos < self.text.len()
1209 && self.text[pos..]
1210 .chars()
1211 .next()
1212 .is_none_or(|ch| ch.is_whitespace());
1213 let start = self.word_start_at(pos);
1214 let end = self.word_end_at(pos);
1215 if !is_ws && start < end {
1216 self.selection = Some(Selection {
1217 anchor: start,
1218 head: end,
1219 });
1220 let cursor = self.text[start..end]
1223 .char_indices()
1224 .next_back()
1225 .map(|(i, _)| start + i)
1226 .unwrap_or(start);
1227 self.set_cursor_inner(cursor);
1228 self.preferred_col = None;
1229 if let Some(text) = self.selected_text() {
1230 self.set_clipboard_text(text);
1231 }
1232 return MouseAction::SelectionFinished;
1233 }
1234 self.set_cursor_inner(pos);
1236 self.preferred_col = None;
1237 MouseAction::CursorPlaced
1238 }
1239 3 => {
1240 let line_start = self.beginning_of_line(pos);
1242 let line_end_excl = self.end_of_line(pos);
1244 let line_end = if line_end_excl < self.text.len() {
1245 line_end_excl + 1 } else {
1247 line_end_excl
1248 };
1249 self.selection = Some(Selection {
1250 anchor: line_start,
1251 head: line_end,
1252 });
1253 self.set_cursor_inner(pos);
1256 self.preferred_col = None;
1257 if let Some(text) = self.selected_text() {
1258 self.set_clipboard_text(text);
1259 }
1260 MouseAction::SelectionFinished
1261 }
1262 _ => {
1263 if let Some(action) = self.element_click_snap(pos, hit_element) {
1270 return action;
1271 }
1272
1273 self.drag_anchor = Some(pos);
1274 self.set_cursor_inner(pos);
1275 self.preferred_col = None;
1276
1277 MouseAction::CursorPlaced
1278 }
1279 }
1280 }
1281 MouseEventKind::Drag(MouseButton::Left) => {
1282 let Some(anchor) = self.drag_anchor else {
1283 return MouseAction::Nothing;
1284 };
1285
1286 let outside_area = event.row < area.y || event.row >= area.y + area.height;
1294 if outside_area {
1295 self.pending_drag_scroll = Some(event);
1297
1298 let now = Instant::now();
1299 let interval = Self::drag_scroll_interval(self.drag_scroll_steps);
1300 if let Some(last) = self.last_drag_scroll
1301 && now.duration_since(last).as_millis() < interval
1302 {
1303 return MouseAction::Nothing;
1304 }
1305 self.last_drag_scroll = Some(now);
1306 self.drag_scroll_steps = self.drag_scroll_steps.saturating_add(1);
1307 } else {
1308 self.pending_drag_scroll = None;
1310 }
1311
1312 let (head, new_scroll) = {
1313 let tw = self.text_width(area);
1314 let lines = self.wrapped_lines(tw);
1315 let scroll = self.effective_scroll(area.height, &lines, state.scroll) as usize;
1316 let visible_end = scroll + area.height as usize;
1317
1318 if event.row < area.y {
1319 let dist = area.y - event.row;
1321 let n = Self::drag_scroll_lines_for_distance(dist);
1322 let target_line = scroll.saturating_sub(n);
1323 let pos = if target_line < lines.len() {
1324 let col = event.column.saturating_sub(area.x) as usize;
1325 let line = &lines[target_line];
1326 let line_end = line.end.min(self.text.len());
1327 let p = self.display_col_to_buffer_pos(line.start, line_end, col).0;
1328 self.clamp_to_line(p, line.start, line_end)
1329 } else {
1330 0
1331 };
1332 (pos, Some(target_line as u16))
1333 } else if event.row >= area.y + area.height {
1334 let dist = event.row - (area.y + area.height) + 1;
1336 let n = Self::drag_scroll_lines_for_distance(dist);
1337 let target_line = (visible_end + n - 1).min(lines.len().saturating_sub(1));
1338 let max_scroll = lines.len().saturating_sub(area.height as usize);
1339 let new_scroll = (target_line + 1)
1340 .saturating_sub(area.height as usize)
1341 .min(max_scroll);
1342 let pos = if target_line < lines.len() {
1343 let col = event.column.saturating_sub(area.x) as usize;
1344 let line = &lines[target_line];
1345 let line_end = line.end.min(self.text.len());
1346 let pos = self.display_col_to_buffer_pos(line.start, line_end, col).0;
1347 self.clamp_to_line(pos, line.start, line_end)
1348 } else {
1349 self.text.len()
1350 };
1351 (pos, Some(new_scroll as u16))
1352 } else {
1353 let col = event.column.clamp(area.x, area.x + tw.saturating_sub(1));
1355 let row = event.row;
1356 drop(lines); match self.buffer_pos_at_screen(col, row, area, state) {
1358 Some(pos) => (pos, None),
1359 None => return MouseAction::Nothing,
1360 }
1361 }
1362 };
1363
1364 if let Some(s) = new_scroll {
1365 self.scroll_override = Some(s);
1366 }
1367 if head == anchor {
1368 self.drag_active = false;
1369 self.selection = None;
1370 } else {
1371 self.drag_active = true;
1372 self.selection = Some(Selection { anchor, head });
1373 }
1374 self.set_cursor_inner(head);
1375 self.preferred_col = None;
1376
1377 if self.selection.is_some() {
1378 MouseAction::SelectionUpdated
1379 } else {
1380 MouseAction::CursorPlaced
1381 }
1382 }
1383 MouseEventKind::Up(MouseButton::Left) => {
1384 self.mouse_down_pos = None;
1385 let was_drag = self.drag_active;
1386 self.drag_active = false;
1387 self.scrollbar_dragging = false;
1388 self.pending_drag_scroll = None;
1389 self.drag_anchor = None;
1390
1391 if was_drag {
1392 if self.selection_range().is_none() {
1396 self.selection = None;
1397 MouseAction::CursorPlaced
1398 } else {
1399 if let Some(text) = self.selected_text()
1401 && !text.is_empty()
1402 {
1403 self.set_clipboard_text(text);
1404 }
1405
1406 if !self.keep_selection_after_mouseup {
1407 self.selection = None;
1408 }
1409
1410 MouseAction::SelectionFinished
1411 }
1412 } else {
1413 MouseAction::Nothing
1414 }
1415 }
1416 MouseEventKind::ScrollDown => {
1417 let tw = self.text_width(area);
1418 let lines = self.wrapped_lines(tw);
1419 let total = lines.len();
1420 if total <= area.height as usize {
1421 return MouseAction::Nothing;
1422 }
1423 let max_scroll = total.saturating_sub(area.height as usize) as u16;
1424 let current = self
1425 .scroll_override
1426 .unwrap_or_else(|| self.effective_scroll(area.height, &lines, state.scroll));
1427 let scroll_lines = Self::scroll_lines_for_height(area.height);
1428 let new_scroll = (current + scroll_lines).min(max_scroll);
1429 if new_scroll == current {
1430 return MouseAction::Nothing;
1431 }
1432 let drag_new_pos = if self.drag_active {
1434 let target_line =
1435 (new_scroll as usize + area.height as usize - 1).min(lines.len() - 1);
1436 Some(lines[target_line].start)
1437 } else {
1438 None
1439 };
1440 drop(lines);
1441 self.scroll_override = Some(new_scroll);
1442 if let Some(new_pos) = drag_new_pos {
1443 if let Some(sel) = &mut self.selection {
1444 sel.head = new_pos;
1445 }
1446 self.set_cursor_inner(new_pos);
1447 }
1448 MouseAction::Scrolled
1449 }
1450 MouseEventKind::ScrollUp => {
1451 let tw = self.text_width(area);
1452 let lines = self.wrapped_lines(tw);
1453 let total = lines.len();
1454 if total <= area.height as usize {
1455 return MouseAction::Nothing;
1456 }
1457 let current = self
1458 .scroll_override
1459 .unwrap_or_else(|| self.effective_scroll(area.height, &lines, state.scroll));
1460 let scroll_lines = Self::scroll_lines_for_height(area.height);
1461 let new_scroll = current.saturating_sub(scroll_lines);
1462 if new_scroll == current {
1463 return MouseAction::Nothing;
1464 }
1465 let drag_new_pos = if self.drag_active {
1467 let target_line = new_scroll as usize;
1468 Some(if target_line < lines.len() {
1469 lines[target_line].start
1470 } else {
1471 0
1472 })
1473 } else {
1474 None
1475 };
1476 drop(lines);
1477 self.scroll_override = Some(new_scroll);
1478 if let Some(new_pos) = drag_new_pos {
1479 if let Some(sel) = &mut self.selection {
1480 sel.head = new_pos;
1481 }
1482 self.set_cursor_inner(new_pos);
1483 }
1484 MouseAction::Scrolled
1485 }
1486 MouseEventKind::Moved => {
1487 let hovered_id = self
1489 .element_at_screen(event.column, event.row, area, state)
1490 .map(|e| e.id);
1491
1492 let prev = self.hovered_element;
1493 if hovered_id != prev {
1494 if let Some(old_id) = prev {
1498 self.pending_element_event = Some(TextElementEvent {
1499 id: old_id,
1500 kind: TextElementEventKind::HoverLeave,
1501 });
1502 }
1503 if let Some(new_id) = hovered_id {
1504 self.pending_element_event = Some(TextElementEvent {
1505 id: new_id,
1506 kind: TextElementEventKind::HoverEnter,
1507 });
1508 }
1509 self.hovered_element = hovered_id;
1510 }
1511 MouseAction::Nothing
1512 }
1513 _ => MouseAction::Nothing,
1514 }
1515 }
1516
1517 fn handle_scrollbar_click(&mut self, row: u16, area: Rect, tw: u16) -> MouseAction {
1523 if area.height == 0 {
1524 return MouseAction::Nothing;
1525 }
1526 let total = {
1527 let lines = self.wrapped_lines(tw);
1528 lines.len()
1529 };
1530 if total <= area.height as usize {
1531 return MouseAction::Nothing;
1532 }
1533 let max_scroll = total.saturating_sub(area.height as usize) as u16;
1534 let rel_row = row.saturating_sub(area.y);
1535 let scroll = if area.height <= 1 {
1537 0
1538 } else {
1539 ((rel_row as u32 * max_scroll as u32) / (area.height.saturating_sub(1)) as u32) as u16
1540 };
1541 self.scroll_override = Some(scroll.min(max_scroll));
1542 MouseAction::Scrolled
1543 }
1544
1545 fn is_scrollbar_thumb_at(&self, row: u16, area: Rect, tw: u16) -> bool {
1550 if area.height == 0 {
1551 return false;
1552 }
1553 let total = {
1554 let lines = self.wrapped_lines(tw);
1555 lines.len()
1556 };
1557 if total <= area.height as usize {
1558 return false;
1559 }
1560 let current_scroll = self.scroll_override.unwrap_or(0);
1561
1562 let lengths = ScrollLengths {
1563 content_len: total,
1564 viewport_len: area.height as usize,
1565 };
1566 let scrollbar = ScrollBar::vertical(lengths).offset(current_scroll as usize);
1567 let sb_x = area.right().saturating_sub(1);
1568 let core_area = CoreRect {
1569 x: sb_x,
1570 y: area.y,
1571 width: 1,
1572 height: area.height,
1573 };
1574 let mut scratch = CoreBuffer::empty(core_area);
1575 (&scrollbar).render(core_area, &mut scratch);
1576
1577 if row < area.y || row >= area.y + area.height {
1578 return false;
1579 }
1580 scratch[(sb_x, row)].symbol() != " "
1581 }
1582
1583 pub fn is_empty(&self) -> bool {
1584 self.text.is_empty()
1585 }
1586
1587 fn is_word_char(ch: char) -> bool {
1588 ch.is_alphanumeric() || ch == '_'
1589 }
1590
1591 fn char_class(ch: char) -> u8 {
1598 if ch.is_whitespace() {
1599 0
1600 } else if Self::is_word_char(ch) {
1601 1
1602 } else {
1603 2
1604 }
1605 }
1606
1607 fn word_start_at(&self, pos: usize) -> usize {
1615 if let Some(elem) = self
1617 .elements
1618 .iter()
1619 .find(|e| pos >= e.range.start && pos < e.range.end)
1620 {
1621 return elem.range.start;
1622 }
1623
1624 let target_class = if pos < self.text.len() {
1626 Self::char_class(self.text[pos..].chars().next().unwrap())
1627 } else if pos > 0 {
1628 let ch = self.text[..pos].chars().next_back().unwrap();
1629 Self::char_class(ch)
1630 } else {
1631 return 0;
1632 };
1633
1634 let before = &self.text[..pos];
1635 let word_start = before
1636 .char_indices()
1637 .rev()
1638 .find(|&(_, ch)| Self::char_class(ch) != target_class)
1639 .map(|(idx, ch)| idx + ch.len_utf8())
1640 .unwrap_or(0);
1641 self.adjust_pos_out_of_elements(word_start, true)
1642 }
1643
1644 fn word_end_at(&self, pos: usize) -> usize {
1650 if let Some(elem) = self
1652 .elements
1653 .iter()
1654 .find(|e| pos >= e.range.start && pos < e.range.end)
1655 {
1656 return elem.range.end;
1657 }
1658
1659 let target_class = if pos < self.text.len() {
1661 Self::char_class(self.text[pos..].chars().next().unwrap())
1662 } else {
1663 return self.text.len();
1664 };
1665
1666 let after = &self.text[pos..];
1667 let word_end = after
1668 .char_indices()
1669 .find(|&(_, ch)| Self::char_class(ch) != target_class)
1670 .map(|(rel_idx, _)| pos + rel_idx)
1671 .unwrap_or(self.text.len());
1672 self.adjust_pos_out_of_elements(word_end, false)
1673 }
1674
1675 fn current_display_col(&self) -> usize {
1676 let bol = self.beginning_of_current_line();
1677 self.display_width_of_range(bol, self.cursor())
1678 }
1679
1680 fn display_width_of_range(&self, from: usize, to: usize) -> usize {
1687 if from >= to {
1688 return 0;
1689 }
1690 let mut width = 0usize;
1691 let mut pos = from;
1692
1693 for elem in &self.elements {
1694 if elem.range.start >= to {
1695 break; }
1697 if elem.range.end <= pos {
1698 continue; }
1700
1701 if pos < elem.range.start {
1703 let plain_end = elem.range.start.min(to);
1704 width += self.plain_display_width(&self.text[pos..plain_end]);
1705 pos = plain_end;
1706 }
1707 if pos >= to {
1708 break;
1709 }
1710
1711 let elem_start_in_range = elem.range.start.max(pos);
1713 let elem_end_in_range = elem.range.end.min(to);
1714 if elem_start_in_range < elem_end_in_range {
1715 if let Some(display) = &elem.display {
1716 if elem_start_in_range == elem.range.start {
1721 let display_w: usize = display
1722 .spans
1723 .iter()
1724 .map(|s| s.content.as_ref().width())
1725 .sum();
1726 width += display_w;
1727 } else {
1728 width += self.plain_display_width(
1729 &self.text[elem_start_in_range..elem_end_in_range],
1730 );
1731 }
1732 } else {
1733 width += self
1734 .plain_display_width(&self.text[elem_start_in_range..elem_end_in_range]);
1735 }
1736 pos = elem_end_in_range;
1737 }
1738 }
1739
1740 if pos < to {
1742 width += self.plain_display_width(&self.text[pos..to]);
1743 }
1744
1745 width
1746 }
1747
1748 fn wrapped_line_index_by_start(lines: &[Range<usize>], pos: usize) -> Option<usize> {
1749 let idx = lines.partition_point(|r| r.start <= pos);
1752 if idx == 0 { None } else { Some(idx - 1) }
1753 }
1754
1755 fn display_col_to_buffer_pos(
1765 &self,
1766 line_start: usize,
1767 line_end: usize,
1768 target_col: usize,
1769 ) -> (usize, bool) {
1770 let mut width_so_far = 0usize;
1771 let mut pos = line_start;
1772
1773 while pos < line_end {
1774 if let Some(elem_idx) = self
1776 .elements
1777 .iter()
1778 .position(|e| pos >= e.range.start && pos < e.range.end)
1779 {
1780 let elem = &self.elements[elem_idx];
1781 let elem_start = elem.range.start;
1782 let elem_buf_end = elem.range.end;
1783 let elem_line_end = elem_buf_end.min(line_end);
1785
1786 if pos == elem_start {
1787 let elem_display_w = if let Some(display) = &elem.display {
1789 display
1790 .spans
1791 .iter()
1792 .map(|s| s.content.as_ref().width())
1793 .sum()
1794 } else {
1795 self.plain_display_width(&self.text[elem_start..elem_line_end])
1796 };
1797
1798 if width_so_far + elem_display_w > target_col {
1799 let dist_start = target_col.saturating_sub(width_so_far);
1803 let dist_end = elem_display_w.saturating_sub(dist_start);
1804 if dist_start <= dist_end {
1805 return (elem_start, true);
1806 } else {
1807 return (elem_buf_end, true);
1808 }
1809 }
1810 width_so_far += elem_display_w;
1811 pos = elem_buf_end.min(line_end); } else {
1813 let partial_w = self.plain_display_width(&self.text[pos..elem_line_end]);
1816 if width_so_far + partial_w > target_col {
1817 return (elem_buf_end, true);
1819 }
1820 width_so_far += partial_w;
1821 pos = elem_buf_end.min(line_end); }
1823 continue;
1824 }
1825
1826 let slice = &self.text[pos..line_end];
1828 if let Some(grapheme) = slice.graphemes(true).next() {
1829 let grapheme_width = self.grapheme_display_width(grapheme);
1830 width_so_far += grapheme_width;
1831 if width_so_far > target_col {
1832 return (self.clamp_pos_to_nearest_boundary(pos), false);
1833 }
1834 pos += grapheme.len();
1835 } else {
1836 break;
1837 }
1838 }
1839
1840 (self.clamp_pos_to_nearest_boundary(line_end), false)
1841 }
1842
1843 fn move_to_display_col_on_line(
1844 &mut self,
1845 line_start: usize,
1846 line_end: usize,
1847 target_col: usize,
1848 ) {
1849 let cursor = self
1850 .display_col_to_buffer_pos(line_start, line_end, target_col)
1851 .0;
1852 self.set_cursor_inner(cursor);
1853 }
1854
1855 fn beginning_of_line(&self, pos: usize) -> usize {
1856 for i in (0..pos).rev() {
1859 if self.text.as_bytes()[i] == b'\n' && !self.is_inside_element(i) {
1860 return i + 1;
1861 }
1862 }
1863 0
1864 }
1865 fn beginning_of_current_line(&self) -> usize {
1866 self.beginning_of_line(self.cursor())
1867 }
1868
1869 fn end_of_line(&self, pos: usize) -> usize {
1870 for i in pos..self.text.len() {
1872 if self.text.as_bytes()[i] == b'\n' && !self.is_inside_element(i) {
1873 return i;
1874 }
1875 }
1876 self.text.len()
1877 }
1878 fn end_of_current_line(&self) -> usize {
1879 self.end_of_line(self.cursor())
1880 }
1881
1882 fn is_inside_element(&self, pos: usize) -> bool {
1884 self.elements
1885 .iter()
1886 .any(|e| pos >= e.range.start && pos < e.range.end)
1887 }
1888
1889 fn apply_classified_command(&mut self, command: EditCommand) {
1890 if let EditCommand::Insert(character) = command {
1891 self.insert_str(&character.to_string());
1892 return;
1893 }
1894 let mutation_kind = match command.category() {
1895 EditCommandCategory::Insert => unreachable!("insert commands return above"),
1896 EditCommandCategory::Navigation => None,
1897 EditCommandCategory::Delete => Some(MutationKind::Delete),
1898 EditCommandCategory::Kill => Some(MutationKind::Kill),
1899 };
1900 self.apply_edit_command(command, mutation_kind);
1901 }
1902
1903 pub fn input(&mut self, event: KeyEvent) {
1904 if self.selection.is_some() {
1908 if let Some(EditCommand::Insert(character)) = classify_key_event(&event) {
1909 self.begin_undo_group();
1910 if !self.delete_selection() {
1911 self.clear_selection();
1912 }
1913 self.insert_str(&character.to_string());
1914 self.end_undo_group();
1915 return;
1916 }
1917 match event {
1918 KeyEvent {
1920 code: KeyCode::Char('j' | 'm'),
1921 modifiers: KeyModifiers::CONTROL,
1922 ..
1923 }
1924 | KeyEvent {
1925 code: KeyCode::Enter,
1926 ..
1927 } => {
1928 self.begin_undo_group();
1929 if !self.delete_selection() {
1930 self.clear_selection();
1931 }
1932 self.insert_str("\n");
1933 self.end_undo_group();
1934 return;
1935 }
1936 KeyEvent {
1941 code: KeyCode::Backspace | KeyCode::Delete | KeyCode::Char('\x08' | '\x7f'),
1942 ..
1943 }
1944 | KeyEvent {
1945 code: KeyCode::Char('h'),
1946 modifiers: KeyModifiers::CONTROL,
1947 ..
1948 }
1949 | KeyEvent {
1950 code: KeyCode::Char('d'),
1951 modifiers: KeyModifiers::CONTROL,
1952 ..
1953 } => {
1954 if self.delete_selection() {
1955 return;
1956 }
1957 self.clear_selection();
1959 }
1960 KeyEvent {
1962 code: KeyCode::Char('x'),
1963 modifiers: KeyModifiers::CONTROL,
1964 ..
1965 } => {
1966 if let Some(text) = self.selected_text() {
1967 self.set_clipboard_text(text);
1968 }
1969 if self.delete_selection() {
1970 return;
1971 }
1972 self.clear_selection();
1974 }
1975 _ => {
1977 self.clear_selection();
1978 }
1979 }
1980 }
1981
1982 if let Some(command) = classify_key_event(&event) {
1983 self.apply_classified_command(command);
1984 return;
1985 }
1986
1987 match event {
1988 KeyEvent {
1989 code: KeyCode::Char('j' | 'm'),
1990 modifiers: KeyModifiers::CONTROL,
1991 ..
1992 }
1993 | KeyEvent {
1994 code: KeyCode::Enter,
1995 ..
1996 } => self.insert_str("\n"),
1997 KeyEvent {
1998 code: KeyCode::Char('y'),
1999 modifiers: KeyModifiers::CONTROL,
2000 ..
2001 } => {
2002 self.yank();
2003 }
2004
2005 KeyEvent {
2007 code: KeyCode::Char('Z'),
2008 modifiers,
2009 ..
2010 } if modifiers.contains(KeyModifiers::CONTROL)
2011 || modifiers.contains(KeyModifiers::SUPER) =>
2012 {
2013 self.redo();
2015 }
2016 k if is_undo_input(&k) => {
2017 self.undo();
2018 }
2019 KeyEvent {
2020 code: KeyCode::Char('r'),
2021 modifiers: KeyModifiers::CONTROL,
2022 ..
2023 } => {
2024 self.redo();
2025 }
2026
2027 KeyEvent {
2029 code: KeyCode::Char('v'),
2030 modifiers: KeyModifiers::CONTROL,
2031 ..
2032 } => {
2033 if let Some(text) = self.clipboard_provider.get() {
2034 self.insert_str(&text);
2035 }
2036 }
2037
2038 KeyEvent {
2041 code: KeyCode::Left,
2042 modifiers: KeyModifiers::SUPER,
2043 ..
2044 } => {
2045 self.move_cursor_to_beginning_of_line(false);
2046 }
2047 KeyEvent {
2048 code: KeyCode::Right,
2049 modifiers: KeyModifiers::SUPER,
2050 ..
2051 } => {
2052 self.move_cursor_to_end_of_line(false);
2053 }
2054 KeyEvent {
2055 code: KeyCode::Up, ..
2056 }
2057 | KeyEvent {
2058 code: KeyCode::Char('p'),
2059 modifiers: KeyModifiers::CONTROL,
2060 ..
2061 } => {
2062 self.move_cursor_up();
2063 }
2064 KeyEvent {
2065 code: KeyCode::Down,
2066 ..
2067 }
2068 | KeyEvent {
2069 code: KeyCode::Char('n'),
2070 modifiers: KeyModifiers::CONTROL,
2071 ..
2072 } => {
2073 self.move_cursor_down();
2074 }
2075 KeyEvent {
2079 code: KeyCode::Home,
2080 ..
2081 } => {
2082 self.set_cursor(self.beginning_of_current_line());
2083 }
2084
2085 KeyEvent {
2086 code: KeyCode::End, ..
2087 } => {
2088 self.set_cursor(self.end_of_current_line());
2089 }
2090 _o => {
2091 #[cfg(feature = "debug-logs")]
2092 tracing::debug!("Unhandled key event in TextArea: {:?}", _o);
2093 }
2094 }
2095 }
2096
2097 fn snapshot(&self) -> UndoEntry {
2101 UndoEntry {
2102 text: self.text().to_owned(),
2103 cursor: self.cursor(),
2104 elements: self.elements.clone(),
2105 }
2106 }
2107
2108 fn restore(&mut self, entry: UndoEntry) {
2110 self.text = EditBuffer::from_parts(entry.text, entry.cursor);
2111 self.elements = entry.elements;
2112 self.wrap_cache.replace(None);
2113 self.preferred_col = None;
2114 }
2117
2118 fn pre_mutate(&mut self, kind: MutationKind) {
2130 if self.undo.group_depth > 0 {
2132 return;
2133 }
2134
2135 let should_push = match self.undo.last_kind {
2136 None => true,
2137 Some(prev) => {
2138 prev != kind
2139 || self.cursor() != self.undo.last_cursor
2140 || matches!(
2141 kind,
2142 MutationKind::Kill | MutationKind::Element | MutationKind::Replace
2143 )
2144 }
2145 };
2146
2147 if should_push {
2148 let entry = self.snapshot();
2149 self.undo.stack.push(entry);
2150 if self.undo.stack.len() > self.undo.max_depth {
2151 self.undo.stack.remove(0);
2152 }
2153 }
2154 self.undo.redo.clear();
2155 self.undo.last_kind = Some(kind);
2156 }
2157
2158 fn post_mutate(&mut self) {
2161 self.undo.last_cursor = self.cursor();
2162 }
2163
2164 pub fn clear_history(&mut self) {
2174 self.undo.stack.clear();
2175 self.undo.redo.clear();
2176 self.undo.last_kind = None;
2177 self.undo.last_cursor = self.cursor();
2178 }
2179
2180 pub fn undo(&mut self) -> bool {
2182 if let Some(entry) = self.undo.stack.pop() {
2183 self.scroll_override = None;
2184 let current = self.snapshot();
2185 self.undo.redo.push(current);
2186 self.restore(entry);
2187 self.undo.last_kind = None;
2189 self.undo.last_cursor = self.cursor();
2190 true
2191 } else {
2192 false
2193 }
2194 }
2195
2196 pub fn redo(&mut self) -> bool {
2198 if let Some(entry) = self.undo.redo.pop() {
2199 self.scroll_override = None;
2200 let current = self.snapshot();
2201 self.undo.stack.push(current);
2202 self.restore(entry);
2203 self.undo.last_kind = None;
2205 self.undo.last_cursor = self.cursor();
2206 true
2207 } else {
2208 false
2209 }
2210 }
2211
2212 pub fn can_undo(&self) -> bool {
2213 !self.undo.stack.is_empty()
2214 }
2215
2216 pub fn can_redo(&self) -> bool {
2217 !self.undo.redo.is_empty()
2218 }
2219
2220 pub fn begin_undo_group(&mut self) {
2230 if self.undo.group_depth == 0 {
2231 self.undo.group_checkpoint = Some(self.snapshot());
2233 }
2234 self.undo.group_depth += 1;
2235 }
2236
2237 pub fn end_undo_group(&mut self) {
2240 if self.undo.group_depth == 0 {
2241 return; }
2243 self.undo.group_depth -= 1;
2244 if self.undo.group_depth == 0 {
2245 if let Some(checkpoint) = self.undo.group_checkpoint.take() {
2246 let changed = checkpoint.text.as_str() != self.text()
2248 || checkpoint.cursor != self.cursor()
2249 || checkpoint.elements.len() != self.elements.len();
2250 if changed {
2251 self.undo.stack.push(checkpoint);
2252 if self.undo.stack.len() > self.undo.max_depth {
2253 self.undo.stack.remove(0);
2254 }
2255 self.undo.redo.clear();
2256 }
2257 }
2258 self.undo.last_kind = None;
2260 self.undo.last_cursor = self.cursor();
2261 }
2262 }
2263
2264 pub fn cancel_undo_group(&mut self) {
2269 if self.undo.group_depth == 0 {
2270 return; }
2272 self.undo.group_depth = 0;
2274 if let Some(checkpoint) = self.undo.group_checkpoint.take() {
2275 self.restore(checkpoint);
2276 }
2277 self.undo.last_kind = None;
2279 self.undo.last_cursor = self.cursor();
2280 }
2281
2282 pub fn delete_backward(&mut self, n: usize) {
2284 if n == 0 {
2285 return;
2286 }
2287 if n == 1 {
2288 self.apply_edit_command(
2289 EditCommand::DeleteGraphemeBackward,
2290 Some(MutationKind::Delete),
2291 );
2292 return;
2293 }
2294 self.begin_undo_group();
2295 for _ in 0..n {
2296 if matches!(
2297 self.apply_edit_command(
2298 EditCommand::DeleteGraphemeBackward,
2299 Some(MutationKind::Delete),
2300 ),
2301 EditOutcome::Unchanged
2302 ) {
2303 break;
2304 }
2305 }
2306 self.end_undo_group();
2307 }
2308
2309 pub fn delete_forward(&mut self, n: usize) {
2310 if n == 0 {
2311 return;
2312 }
2313 if n == 1 {
2314 self.apply_edit_command(
2315 EditCommand::DeleteGraphemeForward,
2316 Some(MutationKind::Delete),
2317 );
2318 return;
2319 }
2320 self.begin_undo_group();
2321 for _ in 0..n {
2322 if matches!(
2323 self.apply_edit_command(
2324 EditCommand::DeleteGraphemeForward,
2325 Some(MutationKind::Delete),
2326 ),
2327 EditOutcome::Unchanged
2328 ) {
2329 break;
2330 }
2331 }
2332 self.end_undo_group();
2333 }
2334
2335 pub fn delete_backward_word(&mut self) {
2336 self.apply_edit_command(
2337 EditCommand::DeleteWordBackward(WordStyle::Small),
2338 Some(MutationKind::Kill),
2339 );
2340 }
2341
2342 pub fn delete_backward_unix_word(&mut self) {
2345 self.apply_edit_command(
2346 EditCommand::DeleteWordBackward(WordStyle::WhitespaceDelimited),
2347 Some(MutationKind::Kill),
2348 );
2349 }
2350
2351 pub fn delete_forward_word(&mut self) {
2357 self.apply_edit_command(
2358 EditCommand::DeleteWordForward(WordStyle::Small),
2359 Some(MutationKind::Kill),
2360 );
2361 }
2362
2363 pub fn kill_to_end_of_line(&mut self) {
2364 self.apply_edit_command(EditCommand::DeleteToLineEnd, Some(MutationKind::Kill));
2365 }
2366
2367 pub fn kill_to_beginning_of_line(&mut self) {
2368 self.apply_edit_command(EditCommand::DeleteToLineStart, Some(MutationKind::Kill));
2369 }
2370
2371 pub fn kill_current_line(&mut self) {
2374 let bol = self.beginning_of_current_line();
2375 let eol = self.end_of_current_line();
2376
2377 let range = if bol == eol {
2378 if bol > 0 { Some(bol - 1..bol) } else { None }
2379 } else {
2380 Some(bol..eol)
2381 };
2382
2383 if let Some(range) = range {
2384 self.apply_edit_replacement(range, "", Some(MutationKind::Kill));
2385 }
2386 }
2387
2388 pub fn yank(&mut self) {
2389 if self.kill_buffer.is_empty() {
2390 return;
2391 }
2392 let text = self.kill_buffer.clone();
2393 self.apply_edit_replacement(
2394 self.cursor()..self.cursor(),
2395 &text,
2396 Some(MutationKind::Insert),
2397 );
2398 if let Some(last) = text.chars().last() {
2399 self.undo.last_insert_ws = last.is_whitespace();
2400 }
2401 }
2402
2403 pub fn move_cursor_left(&mut self) {
2405 self.apply_edit_command(EditCommand::MoveGraphemeLeft, None);
2406 }
2407
2408 pub fn move_cursor_right(&mut self) {
2410 self.apply_edit_command(EditCommand::MoveGraphemeRight, None);
2411 }
2412
2413 pub fn move_cursor_up(&mut self) {
2414 self.scroll_override = None;
2415 if let Some((target_col, maybe_line)) = {
2417 let cache_ref = self.wrap_cache.borrow();
2418 if let Some(cache) = cache_ref.as_ref() {
2419 let lines = &cache.lines;
2420 if let Some(idx) = Self::wrapped_line_index_by_start(lines, self.cursor()) {
2421 let cur_range = &lines[idx];
2422 let target_col = self.preferred_col.unwrap_or_else(|| {
2423 self.display_width_of_range(cur_range.start, self.cursor())
2424 });
2425 if idx > 0 {
2426 let prev = &lines[idx - 1];
2427 let line_start = prev.start;
2428 let line_end = prev.end;
2429 Some((target_col, Some((line_start, line_end))))
2430 } else {
2431 Some((target_col, None))
2432 }
2433 } else {
2434 None
2435 }
2436 } else {
2437 None
2438 }
2439 } {
2440 match maybe_line {
2442 Some((line_start, line_end)) => {
2443 if self.preferred_col.is_none() {
2444 self.preferred_col = Some(target_col);
2445 }
2446 self.move_to_display_col_on_line(line_start, line_end, target_col);
2447 return;
2448 }
2449 None => {
2450 self.set_cursor_inner(0);
2452 self.preferred_col = None;
2453 return;
2454 }
2455 }
2456 }
2457
2458 if let Some(prev_nl) = self.text[..self.cursor()].rfind('\n') {
2460 let target_col = match self.preferred_col {
2461 Some(c) => c,
2462 None => {
2463 let c = self.current_display_col();
2464 self.preferred_col = Some(c);
2465 c
2466 }
2467 };
2468 let prev_line_start = self.text[..prev_nl].rfind('\n').map(|i| i + 1).unwrap_or(0);
2469 let prev_line_end = prev_nl;
2470 self.move_to_display_col_on_line(prev_line_start, prev_line_end, target_col);
2471 } else {
2472 self.set_cursor_inner(0);
2473 self.preferred_col = None;
2474 }
2475 }
2476
2477 pub fn move_cursor_down(&mut self) {
2478 self.scroll_override = None;
2479 if let Some((target_col, move_to_last)) = {
2481 let cache_ref = self.wrap_cache.borrow();
2482 if let Some(cache) = cache_ref.as_ref() {
2483 let lines = &cache.lines;
2484 if let Some(idx) = Self::wrapped_line_index_by_start(lines, self.cursor()) {
2485 let cur_range = &lines[idx];
2486 let target_col = self.preferred_col.unwrap_or_else(|| {
2487 self.display_width_of_range(cur_range.start, self.cursor())
2488 });
2489 if idx + 1 < lines.len() {
2490 let next = &lines[idx + 1];
2491 let line_start = next.start;
2492 let line_end = next.end;
2493 Some((target_col, Some((line_start, line_end))))
2494 } else {
2495 Some((target_col, None))
2496 }
2497 } else {
2498 None
2499 }
2500 } else {
2501 None
2502 }
2503 } {
2504 match move_to_last {
2505 Some((line_start, line_end)) => {
2506 if self.preferred_col.is_none() {
2507 self.preferred_col = Some(target_col);
2508 }
2509 self.move_to_display_col_on_line(line_start, line_end, target_col);
2510 return;
2511 }
2512 None => {
2513 self.set_cursor_inner(self.text.len());
2515 self.preferred_col = None;
2516 return;
2517 }
2518 }
2519 }
2520
2521 let target_col = match self.preferred_col {
2523 Some(c) => c,
2524 None => {
2525 let c = self.current_display_col();
2526 self.preferred_col = Some(c);
2527 c
2528 }
2529 };
2530 if let Some(next_nl) = self.text[self.cursor()..]
2531 .find('\n')
2532 .map(|i| i + self.cursor())
2533 {
2534 let next_line_start = next_nl + 1;
2535 let next_line_end = self.text[next_line_start..]
2536 .find('\n')
2537 .map(|i| i + next_line_start)
2538 .unwrap_or(self.text.len());
2539 self.move_to_display_col_on_line(next_line_start, next_line_end, target_col);
2540 } else {
2541 self.set_cursor_inner(self.text.len());
2542 self.preferred_col = None;
2543 }
2544 }
2545
2546 pub fn move_cursor_to_beginning_of_line(&mut self, move_up_at_bol: bool) {
2549 if move_up_at_bol {
2550 self.apply_edit_command(EditCommand::MoveLogicalLineStart, None);
2551 return;
2552 }
2553 if let Some(bol) = self.beginning_of_current_visual_line() {
2554 self.set_cursor(bol);
2555 return;
2556 }
2557
2558 let bol = self.beginning_of_current_line();
2559 self.set_cursor(bol);
2560 }
2561
2562 pub fn move_cursor_to_end_of_line(&mut self, move_down_at_eol: bool) {
2565 if move_down_at_eol {
2566 self.apply_edit_command(EditCommand::MoveLogicalLineEnd, None);
2567 return;
2568 }
2569 if let Some(eol) = self.end_of_current_visual_line() {
2570 self.set_cursor(eol);
2571 return;
2572 }
2573
2574 let eol = self.end_of_current_line();
2575 self.set_cursor(eol);
2576 }
2577
2578 fn beginning_of_current_visual_line(&self) -> Option<usize> {
2579 let cache = self.wrap_cache.borrow();
2580 let cache = cache.as_ref()?;
2581 let idx = Self::wrapped_line_index_by_start(&cache.lines, self.cursor())?;
2582 Some(cache.lines[idx].start)
2583 }
2584
2585 fn end_of_current_visual_line(&self) -> Option<usize> {
2588 let cache = self.wrap_cache.borrow();
2589 let cache = cache.as_ref()?;
2590 let idx = Self::wrapped_line_index_by_start(&cache.lines, self.cursor())?;
2591 let line = &cache.lines[idx];
2592 let end = line.end.min(self.text.len());
2593 let soft_continued = cache
2594 .lines
2595 .get(idx + 1)
2596 .is_some_and(|next| next.start == end);
2597 if soft_continued && end > line.start {
2598 Some(self.clamp_to_line(end, line.start, end))
2599 } else {
2600 Some(end)
2601 }
2602 }
2603
2604 pub fn insert_element(
2614 &mut self,
2615 text: &str,
2616 kind: ElementKind,
2617 display: Option<Line<'static>>,
2618 ) -> ElementId {
2619 let plan = self.plan_edit_replacement(self.cursor()..self.cursor(), text);
2620 self.apply_element_transaction(plan, kind, display)
2621 }
2622
2623 pub fn replace_range_with_element(
2631 &mut self,
2632 range: Range<usize>,
2633 text: &str,
2634 kind: ElementKind,
2635 display: Option<Line<'static>>,
2636 ) -> ElementId {
2637 let plan = self.plan_edit_replacement(range, text);
2638 self.apply_element_transaction(plan, kind, display)
2639 }
2640
2641 fn apply_element_transaction(
2642 &mut self,
2643 plan: EditPlan,
2644 kind: ElementKind,
2645 display: Option<Line<'static>>,
2646 ) -> ElementId {
2647 let start = plan.replaced_byte_range().start;
2648 let inserted_len = plan.replacement().len();
2649 self.assert_valid_edit_plan(&plan);
2650 self.pre_mutate(MutationKind::Element);
2651 self.apply_validated_edit_plan(plan, Some(MutationKind::Element));
2652 let end = start + inserted_len;
2653 let id = self.add_element(start..end, kind, display);
2654 self.set_cursor(end);
2655 self.post_mutate();
2656 id
2657 }
2658
2659 fn add_element(
2660 &mut self,
2661 range: Range<usize>,
2662 kind: ElementKind,
2663 display: Option<Line<'static>>,
2664 ) -> ElementId {
2665 let id = ElementId(self.next_element_id);
2666 self.next_element_id += 1;
2667 let elem = TextElement {
2668 id,
2669 range,
2670 kind,
2671 display,
2672 };
2673 self.elements.push(elem);
2674 self.elements.sort_by_key(|e| e.range.start);
2675 self.wrap_cache.replace(None);
2676 id
2677 }
2678
2679 pub fn element_at_cursor(&self) -> Option<&TextElement> {
2685 self.elements
2686 .iter()
2687 .find(|e| self.cursor() >= e.range.start && self.cursor() < e.range.end)
2688 }
2689
2690 pub fn element_text(&self, id: ElementId) -> Option<&str> {
2692 self.elements
2693 .iter()
2694 .find(|e| e.id == id)
2695 .map(|e| &self.text[e.range.clone()])
2696 }
2697
2698 pub fn set_element_display(&mut self, id: ElementId, display: Option<Line<'static>>) {
2700 if let Some(e) = self.elements.iter_mut().find(|e| e.id == id) {
2701 e.display = display;
2702 self.wrap_cache.replace(None);
2703 }
2704 }
2705
2706 pub fn elements(&self) -> &[TextElement] {
2708 &self.elements
2709 }
2710
2711 pub fn restore_elements(
2717 &mut self,
2718 elems: impl IntoIterator<Item = (Range<usize>, ElementKind, Option<Line<'static>>)>,
2719 ) {
2720 for (range, kind, display) in elems {
2721 self.add_element(range, kind, display);
2722 }
2723 self.wrap_cache.replace(None);
2724 }
2725
2726 pub fn inline_element(&mut self, id: ElementId) -> bool {
2734 let Some(idx) = self.elements.iter().position(|e| e.id == id) else {
2735 return false;
2736 };
2737 let end = self.elements[idx].range.end;
2738
2739 self.pre_mutate(MutationKind::Element);
2741
2742 self.elements.remove(idx);
2743 self.set_cursor_inner(end);
2744 self.preferred_col = None;
2745 self.wrap_cache.replace(None);
2746 self.undo.last_kind = None; true
2749 }
2750
2751 pub fn word_at_cursor(&self) -> Option<(Range<usize>, &str)> {
2759 if self.text.is_empty() {
2760 return None;
2761 }
2762 let pos = self.cursor().min(self.text.len());
2763
2764 let start = self.text[..pos]
2766 .rfind(|c: char| c.is_whitespace())
2767 .map(|i| {
2768 i + self.text[i..]
2769 .chars()
2770 .next()
2771 .map(|c| c.len_utf8())
2772 .unwrap_or(1)
2773 })
2774 .unwrap_or(0);
2775
2776 let end = self.text[pos..]
2778 .find(|c: char| c.is_whitespace())
2779 .map(|i| i + pos)
2780 .unwrap_or(self.text.len());
2781
2782 if start >= end {
2786 return None;
2787 }
2788
2789 let word = &self.text[start..end];
2792 if word.chars().all(|c| c.is_whitespace()) {
2793 return None;
2794 }
2795
2796 Some((start..end, word))
2797 }
2798
2799 fn find_element_containing(&self, pos: usize) -> Option<usize> {
2800 self.elements
2801 .iter()
2802 .position(|e| pos > e.range.start && pos < e.range.end)
2803 }
2804
2805 fn clamp_pos_to_nearest_boundary(&self, mut pos: usize) -> usize {
2806 if pos > self.text.len() {
2807 pos = self.text.len();
2808 }
2809 if let Some(idx) = self.find_element_containing(pos) {
2810 let e = &self.elements[idx];
2811 let dist_start = pos.saturating_sub(e.range.start);
2812 let dist_end = e.range.end.saturating_sub(pos);
2813 if dist_start <= dist_end {
2814 e.range.start
2815 } else {
2816 e.range.end
2817 }
2818 } else {
2819 pos
2820 }
2821 }
2822
2823 fn expand_range_to_element_boundaries(&self, mut range: Range<usize>) -> Range<usize> {
2824 loop {
2826 let mut changed = false;
2827 for e in &self.elements {
2828 if e.range.start < range.end && e.range.end > range.start {
2829 let new_start = range.start.min(e.range.start);
2830 let new_end = range.end.max(e.range.end);
2831 if new_start != range.start || new_end != range.end {
2832 range.start = new_start;
2833 range.end = new_end;
2834 changed = true;
2835 }
2836 }
2837 }
2838 if !changed {
2839 break;
2840 }
2841 }
2842 range
2843 }
2844
2845 fn shift_elements(&mut self, at: usize, removed: usize, inserted: usize) {
2846 let end = at + removed;
2848 let diff = inserted as isize - removed as isize;
2849 self.elements
2851 .retain(|e| !(e.range.start >= at && e.range.end <= end));
2852 for e in &mut self.elements {
2853 if e.range.end <= at {
2854 } else if e.range.start >= end {
2856 e.range.start = ((e.range.start as isize) + diff) as usize;
2858 e.range.end = ((e.range.end as isize) + diff) as usize;
2859 } else {
2860 let new_start = at.min(e.range.start);
2863 let new_end = at + inserted.max(e.range.end.saturating_sub(end));
2864 e.range.start = new_start;
2865 e.range.end = new_end;
2866 }
2867 }
2868 }
2869
2870 fn update_elements_after_replace(&mut self, start: usize, end: usize, inserted_len: usize) {
2871 self.shift_elements(start, end.saturating_sub(start), inserted_len);
2872 }
2873
2874 pub fn beginning_of_previous_word(&self) -> usize {
2881 let ranges = self.element_ranges();
2882 self.text
2883 .plan_command(EditCommand::MoveWordLeft(WordStyle::Small), &ranges)
2884 .cursor_byte()
2885 }
2886
2887 pub fn beginning_of_previous_unix_word(&self) -> usize {
2890 let ranges = self.element_ranges();
2891 self.text
2892 .plan_command(
2893 EditCommand::MoveWordLeft(WordStyle::WhitespaceDelimited),
2894 &ranges,
2895 )
2896 .cursor_byte()
2897 }
2898
2899 pub fn end_of_next_word(&self) -> usize {
2906 let ranges = self.element_ranges();
2907 self.text
2908 .plan_command(EditCommand::MoveWordRight(WordStyle::Small), &ranges)
2909 .cursor_byte()
2910 }
2911
2912 fn adjust_pos_out_of_elements(&self, pos: usize, prefer_start: bool) -> usize {
2913 if let Some(idx) = self.find_element_containing(pos) {
2914 let e = &self.elements[idx];
2915 if prefer_start {
2916 e.range.start
2917 } else {
2918 e.range.end
2919 }
2920 } else {
2921 pos
2922 }
2923 }
2924
2925 #[expect(clippy::unwrap_used)]
2926 fn wrapped_lines(&self, width: u16) -> Ref<'_, Vec<Range<usize>>> {
2927 let width = width.max(1);
2931 {
2933 let mut cache = self.wrap_cache.borrow_mut();
2934 let needs_recalc = match cache.as_ref() {
2935 Some(c) => c.width != width,
2936 None => true,
2937 };
2938 if needs_recalc {
2939 let lines = if self.elements.iter().any(|e| e.display.is_some()) {
2940 self.element_aware_wrap_ranges(width as usize)
2941 } else {
2942 crate::wrapping::wrap_ranges(
2943 &self.text,
2944 Options::new(width as usize)
2945 .wrap_algorithm(textwrap::WrapAlgorithm::FirstFit),
2946 )
2947 };
2948 *cache = Some(WrapCache { width, lines });
2949 }
2950 }
2951
2952 let cache = self.wrap_cache.borrow();
2953 Ref::map(cache, |c| &c.as_ref().unwrap().lines)
2954 }
2955
2956 fn element_aware_wrap_ranges(&self, width: usize) -> Vec<Range<usize>> {
2967 let width = width.max(1);
2968 let mut result = Vec::new();
2969
2970 let mut seg_start = 0;
2974 loop {
2975 let seg_end = self.next_logical_newline(seg_start);
2976
2977 self.greedy_wrap_segment(seg_start, seg_end, width, &mut result);
2978
2979 if seg_end >= self.text.len() {
2980 break;
2981 }
2982 seg_start = seg_end + 1; }
2984
2985 if result.is_empty() {
2986 result.push(0..0);
2987 }
2988
2989 result
2990 }
2991
2992 fn next_logical_newline(&self, from: usize) -> usize {
2996 let mut pos = from;
2997 while pos < self.text.len() {
2998 if let Some(elem) = self
3000 .elements
3001 .iter()
3002 .find(|e| pos >= e.range.start && pos < e.range.end)
3003 {
3004 pos = elem.range.end;
3005 continue;
3006 }
3007 if self.text.as_bytes()[pos] == b'\n' {
3008 return pos;
3009 }
3010 pos += 1;
3011 }
3012 self.text.len()
3013 }
3014
3015 fn greedy_wrap_segment(
3017 &self,
3018 start: usize,
3019 end: usize,
3020 width: usize,
3021 result: &mut Vec<Range<usize>>,
3022 ) {
3023 if start >= end {
3024 result.push(start..end);
3026 return;
3027 }
3028
3029 let mut line_start = start;
3030 let mut pos = start;
3031 let mut display_w: usize = 0;
3032 let mut last_break_pos: Option<usize> = None;
3034
3035 while pos < end {
3036 if let Some(elem) = self
3038 .elements
3039 .iter()
3040 .find(|e| pos == e.range.start && e.range.start < e.range.end)
3041 {
3042 let elem_end = elem.range.end.min(end);
3043 let elem_dw: usize = if let Some(display) = &elem.display {
3044 display
3045 .spans
3046 .iter()
3047 .map(|s| s.content.as_ref().width())
3048 .sum()
3049 } else {
3050 self.plain_display_width(&self.text[elem.range.start..elem_end])
3051 };
3052
3053 if display_w > 0 && display_w + elem_dw > width {
3054 let break_at = last_break_pos.unwrap_or(pos);
3056 result.push(line_start..break_at);
3057 line_start = break_at;
3058 while line_start < end
3060 && line_start < pos
3061 && matches!(self.text.as_bytes().get(line_start), Some(b' ' | b'\t'))
3062 {
3063 line_start += 1;
3064 }
3065 pos = line_start;
3066 display_w = 0;
3067 last_break_pos = None;
3068 continue;
3069 }
3070
3071 display_w += elem_dw;
3072 pos = elem_end;
3073 last_break_pos = Some(pos);
3075 continue;
3076 }
3077
3078 let slice = &self.text[pos..end];
3080 let Some(grapheme) = slice.graphemes(true).next() else {
3081 break;
3082 };
3083 let grapheme_width = self.grapheme_display_width(grapheme);
3084
3085 if display_w + grapheme_width > width && display_w > 0 {
3086 let break_at = last_break_pos.unwrap_or(pos);
3088 result.push(line_start..break_at);
3089 line_start = break_at;
3090 while line_start < end
3092 && line_start < pos
3093 && matches!(self.text.as_bytes().get(line_start), Some(b' ' | b'\t'))
3094 {
3095 line_start += 1;
3096 }
3097 display_w = self.display_width_of_range(line_start, pos);
3098 last_break_pos = None;
3099 if line_start == pos {
3100 display_w = grapheme_width;
3102 pos += grapheme.len();
3103 }
3104 continue;
3105 }
3106
3107 if grapheme == " " || grapheme == "\t" {
3108 last_break_pos = Some(pos + grapheme.len());
3110 }
3111
3112 display_w += grapheme_width;
3113 pos += grapheme.len();
3114 }
3115
3116 result.push(line_start..end);
3118 }
3119
3120 fn effective_scroll(
3126 &self,
3127 area_height: u16,
3128 lines: &[Range<usize>],
3129 current_scroll: u16,
3130 ) -> u16 {
3131 let total_lines = lines.len() as u16;
3132 if area_height >= total_lines {
3133 return 0;
3134 }
3135
3136 let max_scroll = total_lines.saturating_sub(area_height);
3137
3138 if let Some(ovr) = self.scroll_override {
3141 return ovr.min(max_scroll);
3142 }
3143
3144 let cursor_line_idx =
3147 Self::wrapped_line_index_by_start(lines, self.cursor()).unwrap_or(0) as u16;
3148
3149 let mut scroll = current_scroll.min(max_scroll);
3150
3151 if cursor_line_idx < scroll {
3153 scroll = cursor_line_idx;
3154 } else if cursor_line_idx >= scroll + area_height {
3155 scroll = cursor_line_idx + 1 - area_height;
3156 }
3157 scroll
3158 }
3159
3160 fn content_width(&self, area_width: u16, area_height: u16) -> (u16, bool) {
3169 if !self.show_scrollbar || area_width <= 1 {
3170 return (area_width, false);
3171 }
3172 let lines = self.wrapped_lines(area_width);
3174 let needs = lines.len() as u16 > area_height;
3175 if needs {
3176 let reserved = 1 + self.scrollbar_padding;
3178 (area_width.saturating_sub(reserved), true)
3179 } else {
3180 (area_width, false)
3181 }
3182 }
3183
3184 fn text_width(&self, area: Rect) -> u16 {
3186 self.content_width(area.width, area.height).0
3187 }
3188}
3189
3190impl WidgetRef for &TextArea {
3191 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
3192 let (cw, needs_sb) = self.content_width(area.width, area.height);
3193 let content_area = Rect { width: cw, ..area };
3194 let lines = self.wrapped_lines(cw);
3195 self.render_lines(content_area, buf, &lines, 0..lines.len());
3196 if needs_sb {
3197 self.render_scrollbar(area, buf, lines.len() as u16, area.height, 0);
3198 }
3199 }
3200}
3201
3202impl StatefulWidgetRef for &TextArea {
3203 type State = TextAreaState;
3204
3205 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
3206 let (cw, needs_sb) = self.content_width(area.width, area.height);
3207 let content_area = Rect { width: cw, ..area };
3208 let lines = self.wrapped_lines(cw);
3209 let scroll = self.effective_scroll(area.height, &lines, state.scroll);
3210 state.scroll = scroll;
3211
3212 let start = scroll as usize;
3213 let end = (scroll + area.height).min(lines.len() as u16) as usize;
3214 self.render_lines(content_area, buf, &lines, start..end);
3215 if needs_sb {
3216 self.render_scrollbar(area, buf, lines.len() as u16, area.height, scroll);
3217 }
3218 }
3219}
3220
3221impl TextArea {
3222 fn render_scrollbar(
3227 &self,
3228 area: Rect,
3229 buf: &mut Buffer,
3230 total_lines: u16,
3231 viewport_lines: u16,
3232 offset: u16,
3233 ) {
3234 if total_lines <= viewport_lines || area.width == 0 || area.height == 0 {
3235 return;
3236 }
3237
3238 let sb_area = Rect {
3239 x: area.right().saturating_sub(1),
3240 y: area.y,
3241 width: 1,
3242 height: area.height,
3243 };
3244
3245 let lengths = ScrollLengths {
3246 content_len: total_lines as usize,
3247 viewport_len: viewport_lines as usize,
3248 };
3249 let scrollbar = ScrollBar::vertical(lengths).offset(offset as usize);
3250
3251 let core_area = CoreRect {
3253 x: sb_area.x,
3254 y: sb_area.y,
3255 width: sb_area.width,
3256 height: sb_area.height,
3257 };
3258 let mut scratch = CoreBuffer::empty(core_area);
3259 (&scrollbar).render(core_area, &mut scratch);
3260
3261 let track_style = self.scrollbar_track_style;
3262 let thumb_style = self.scrollbar_thumb_style;
3263
3264 for row in 0..sb_area.height {
3265 let x = sb_area.x;
3266 let y = sb_area.y + row;
3267 let src = &scratch[(x, y)];
3268 let dst = &mut buf[(x, y)];
3269 let symbol = src.symbol();
3270 dst.set_symbol(symbol);
3271 if symbol == " " {
3272 dst.set_style(track_style);
3273 } else {
3274 dst.set_style(thumb_style);
3275 }
3276 }
3277 }
3278
3279 fn render_lines(
3280 &self,
3281 area: Rect,
3282 buf: &mut Buffer,
3283 lines: &[Range<usize>],
3284 range: std::ops::Range<usize>,
3285 ) {
3286 let area_right = area.x + area.width; let sel_range = self.selection_range();
3288
3289 for (row, idx) in range.enumerate() {
3290 let r = &lines[idx];
3291 let y = area.y + row as u16;
3292 let line_range = r.start..r.end;
3293
3294 let mut display_x: u16 = 0; let mut buf_pos = line_range.start; let overlapping: Vec<&TextElement> = self
3303 .elements
3304 .iter()
3305 .filter(|e| {
3306 let os = e.range.start.max(line_range.start);
3307 let oe = e.range.end.min(line_range.end);
3308 os < oe
3309 })
3310 .collect();
3311
3312 for elem in &overlapping {
3313 let overlap_start = elem.range.start.max(line_range.start);
3314 let overlap_end = elem.range.end.min(line_range.end);
3315
3316 if buf_pos < overlap_start && display_x < area.width {
3318 let plain = &self.text[buf_pos..overlap_start];
3319 let avail = (area.width - display_x) as usize;
3320 let (paint, paint_w) = paint_plain_for_display(plain, avail, self.tab_width);
3321 buf.set_string(area.x + display_x, y, paint.as_ref(), Style::default());
3322 display_x += paint_w as u16;
3323 }
3324
3325 if display_x >= area.width {
3327 buf_pos = overlap_end;
3328 continue;
3329 }
3330
3331 let avail = (area.width - display_x) as usize;
3332
3333 if let Some(display) = &elem.display {
3334 if overlap_start == elem.range.start {
3335 let display = truncate_line_display(display, avail);
3337 for span in &display.spans {
3338 let content = span.content.as_ref();
3339 let w = content.width() as u16;
3340 if display_x >= area.width {
3341 break;
3342 }
3343 buf.set_string(area.x + display_x, y, content, span.style);
3344 display_x += w;
3345 }
3346 }
3347 } else {
3351 let styled = &self.text[overlap_start..overlap_end];
3353 let style = Style::default().fg(Color::Cyan);
3354 let (paint, paint_w) = paint_plain_for_display(styled, avail, self.tab_width);
3355 buf.set_string(area.x + display_x, y, paint.as_ref(), style);
3356 display_x += paint_w as u16;
3357 }
3358
3359 buf_pos = overlap_end;
3360 }
3361
3362 if buf_pos < line_range.end && display_x < area.width {
3364 let plain = &self.text[buf_pos..line_range.end];
3365 let avail = (area.width - display_x) as usize;
3366 let (paint, paint_w) = paint_plain_for_display(plain, avail, self.tab_width);
3367 buf.set_string(area.x + display_x, y, paint.as_ref(), Style::default());
3368 let _painted_end = display_x.saturating_add(paint_w as u16);
3371 let _ = _painted_end;
3372 }
3373
3374 if let Some(sel_range) = &sel_range {
3376 let line_sel_start = sel_range.start.max(line_range.start);
3378 let line_sel_end = sel_range.end.min(line_range.end);
3379 if line_sel_start < line_sel_end {
3380 let col_start =
3382 self.display_width_of_range(line_range.start, line_sel_start) as u16;
3383 let col_end =
3384 self.display_width_of_range(line_range.start, line_sel_end) as u16;
3385 let col_start = col_start.min(area.width);
3386 let col_end = col_end.min(area.width);
3387 for cx in col_start..col_end {
3388 let cell = &mut buf[(area.x + cx, y)];
3389 cell.set_style(self.selection_style);
3390 }
3391 }
3392 }
3393
3394 let _ = area_right; }
3396 }
3397}
3398
3399fn expand_tabs_with_width(text: &str, tab_width: u8) -> std::borrow::Cow<'_, str> {
3401 if tab_width == 0 || !text.contains('\t') {
3402 return std::borrow::Cow::Borrowed(text);
3403 }
3404 std::borrow::Cow::Owned(text.replace('\t', &" ".repeat(tab_width as usize)))
3405}
3406
3407fn grapheme_display_width_with_tab(grapheme: &str, tab_width: u8) -> usize {
3408 if grapheme == "\t" {
3409 if tab_width == 0 {
3410 0
3411 } else {
3412 tab_width as usize
3413 }
3414 } else {
3415 grapheme.width()
3416 }
3417}
3418
3419fn plain_display_width_with_tab(text: &str, tab_width: u8) -> usize {
3420 if tab_width == 0 || !text.contains('\t') {
3421 return text.width();
3422 }
3423 text.graphemes(true)
3424 .map(|g| grapheme_display_width_with_tab(g, tab_width))
3425 .sum()
3426}
3427
3428fn clip_str_to_display_width(s: &str, max_width: usize) -> &str {
3431 clip_str_to_display_width_with_tab(s, max_width, 0)
3432}
3433
3434fn clip_str_to_display_width_with_tab(s: &str, max_width: usize, tab_width: u8) -> &str {
3436 let mut width = 0;
3437 for (i, grapheme) in s.grapheme_indices(true) {
3438 let grapheme_width = grapheme_display_width_with_tab(grapheme, tab_width);
3439 if width + grapheme_width > max_width {
3440 return &s[..i];
3441 }
3442 width += grapheme_width;
3443 }
3444 s
3445}
3446
3447fn paint_plain_for_display(
3450 s: &str,
3451 max_width: usize,
3452 tab_width: u8,
3453) -> (std::borrow::Cow<'_, str>, usize) {
3454 let clipped = clip_str_to_display_width_with_tab(s, max_width, tab_width);
3455 let paint = expand_tabs_with_width(clipped, tab_width);
3456 let w = plain_display_width_with_tab(clipped, tab_width);
3457 (paint, w)
3458}
3459
3460fn truncate_line_display(line: &Line<'static>, max_width: usize) -> Line<'static> {
3469 use ratatui::text::Span;
3470
3471 let total_width: usize = line.spans.iter().map(|s| s.content.as_ref().width()).sum();
3472 if total_width <= max_width {
3473 return line.clone();
3474 }
3475 if max_width == 0 {
3476 return Line::default();
3477 }
3478
3479 let last_char = line
3481 .spans
3482 .iter()
3483 .rev()
3484 .find_map(|s| s.content.as_ref().chars().last());
3485 let (preserve_bracket, bracket_char, bracket_style) = match last_char {
3486 Some(ch @ (']' | ')' | '}' | '>')) => {
3487 let style = line.spans.last().map(|s| s.style).unwrap_or_default();
3489 (true, Some(ch), style)
3490 }
3491 _ => (false, None, Style::default()),
3492 };
3493
3494 let preserve_bracket = preserve_bracket && max_width >= 3;
3497 let content_budget = if preserve_bracket {
3498 max_width.saturating_sub(2) } else {
3500 max_width.saturating_sub(1) };
3502
3503 let mut new_spans: Vec<Span<'static>> = Vec::new();
3504 let mut used = 0usize;
3505
3506 for span in &line.spans {
3507 let content = span.content.as_ref();
3508 let sw = content.width();
3509 if used + sw <= content_budget {
3510 new_spans.push(span.clone());
3511 used += sw;
3512 } else {
3513 let remaining = content_budget - used;
3515 if remaining > 0 {
3516 let partial = clip_str_to_display_width(content, remaining);
3517 if !partial.is_empty() {
3518 new_spans.push(Span::styled(partial.to_string(), span.style));
3519 }
3520 }
3521 break;
3522 }
3523 }
3524
3525 let ellipsis_style = new_spans.last().map(|s| s.style).unwrap_or_default();
3527 new_spans.push(Span::styled("…", ellipsis_style));
3528
3529 if preserve_bracket && let Some(ch) = bracket_char {
3531 new_spans.push(Span::styled(ch.to_string(), bracket_style));
3532 }
3533
3534 Line::from(new_spans)
3535}
3536
3537#[cfg(test)]
3538#[path = "textarea_tests.rs"]
3539mod tests;