Skip to main content

revue/widget/input_widgets/textarea/
multi_cursor.rs

1//! Multiple cursor methods for TextArea
2
3use super::cursor::{Cursor, CursorPos};
4
5impl TextArea {
6    /// Add cursor at position (Alt+Click)
7    pub fn add_cursor_at(&mut self, line: usize, col: usize) {
8        let line = line.min(self.lines.len().saturating_sub(1));
9        let col = col.min(self.line_len(line));
10        self.cursors.add_at(CursorPos::new(line, col));
11    }
12
13    /// Add cursor above current (Ctrl+Alt+Up)
14    pub fn add_cursor_above(&mut self) {
15        let primary = self.cursors.primary().pos;
16        if primary.line > 0 {
17            let new_line = primary.line - 1;
18            let new_col = primary.col.min(self.line_len(new_line));
19            self.cursors.add_at(CursorPos::new(new_line, new_col));
20        }
21    }
22
23    /// Add cursor below current (Ctrl+Alt+Down)
24    pub fn add_cursor_below(&mut self) {
25        let primary = self.cursors.primary().pos;
26        if primary.line + 1 < self.lines.len() {
27            let new_line = primary.line + 1;
28            let new_col = primary.col.min(self.line_len(new_line));
29            self.cursors.add_at(CursorPos::new(new_line, new_col));
30        }
31    }
32
33    /// Clear all secondary cursors (Escape)
34    pub fn clear_secondary_cursors(&mut self) {
35        self.cursors.clear_secondary();
36    }
37
38    /// Get word at cursor position
39    fn get_word_at_cursor(&self) -> String {
40        let pos = self.cursors.primary().pos;
41        let Some(line) = self.lines.get(pos.line) else {
42            return String::new();
43        };
44        let chars: Vec<char> = line.chars().collect();
45
46        if chars.is_empty() || pos.col >= chars.len() {
47            return String::new();
48        }
49
50        let mut start = pos.col;
51        let mut end = pos.col;
52
53        // Expand left
54        while start > 0 && chars[start - 1].is_alphanumeric() {
55            start -= 1;
56        }
57
58        // Expand right
59        while end < chars.len() && chars[end].is_alphanumeric() {
60            end += 1;
61        }
62
63        chars[start..end].iter().collect()
64    }
65
66    /// Get current word or selection text
67    fn get_word_or_selection(&self) -> String {
68        // If selection exists, return selected text
69        if let Some(text) = self.get_selection() {
70            return text;
71        }
72        // Otherwise get word under cursor
73        self.get_word_at_cursor()
74    }
75
76    /// Find next occurrence of text from a given position
77    fn find_next_from(&self, text: &str, from: CursorPos) -> Option<CursorPos> {
78        if text.is_empty() {
79            return None;
80        }
81
82        let text_lower = text.to_lowercase();
83
84        // Search from the position after `from`
85        for line_idx in from.line..self.lines.len() {
86            let Some(line) = self.lines.get(line_idx) else {
87                continue;
88            };
89            let line_lower = line.to_lowercase();
90
91            let start_col = if line_idx == from.line {
92                from.col + 1
93            } else {
94                0
95            };
96
97            if start_col < line.len() {
98                if let Some(pos) = line_lower[start_col..].find(&text_lower) {
99                    return Some(CursorPos::new(line_idx, start_col + pos));
100                }
101            }
102        }
103
104        // Wrap around to beginning
105        for line_idx in 0..=from.line.min(self.lines.len().saturating_sub(1)) {
106            let Some(line) = self.lines.get(line_idx) else {
107                continue;
108            };
109            let line_lower = line.to_lowercase();
110
111            let end_col = if line_idx == from.line {
112                from.col + 1
113            } else {
114                line.len()
115            };
116
117            if let Some(pos) = line_lower[..end_col].find(&text_lower) {
118                let found_pos = CursorPos::new(line_idx, pos);
119                // Don't return if it's the same as one of our existing cursors
120                if !self.cursors.iter().any(|c| c.pos == found_pos) {
121                    return Some(found_pos);
122                }
123            }
124        }
125
126        None
127    }
128
129    /// Select next occurrence of current word/selection (Ctrl+D)
130    pub fn select_next_occurrence(&mut self) {
131        let text = self.get_word_or_selection();
132        if text.is_empty() {
133            return;
134        }
135
136        // Find next occurrence after the last cursor
137        let last_pos = self
138            .cursors
139            .iter()
140            .map(|c| c.pos)
141            .max()
142            .unwrap_or(CursorPos::new(0, 0));
143
144        if let Some(match_pos) = self.find_next_from(&text, last_pos) {
145            let end_col = match_pos.col + text.len();
146            let new_cursor =
147                Cursor::with_selection(CursorPos::new(match_pos.line, end_col), match_pos);
148            self.cursors.add(new_cursor);
149        }
150    }
151}
152
153use super::TextArea;