sql_cli/
cursor_manager.rs1pub struct CursorManager {
4 input_cursor_position: usize,
6
7 _visual_cursor: (usize, usize),
9
10 table_cursor: (usize, usize),
12
13 horizontal_scroll: u16,
15
16 vertical_scroll: usize,
18}
19
20impl Default for CursorManager {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl CursorManager {
27 #[must_use]
28 pub fn new() -> Self {
29 Self {
30 input_cursor_position: 0,
31 _visual_cursor: (0, 0),
32 table_cursor: (0, 0),
33 horizontal_scroll: 0,
34 vertical_scroll: 0,
35 }
36 }
37
38 pub fn move_word_forward(&mut self, text: &str) -> usize {
42 let chars: Vec<char> = text.chars().collect();
43 let mut pos = self.input_cursor_position;
44
45 while pos < chars.len() && !chars[pos].is_whitespace() {
47 pos += 1;
48 }
49
50 while pos < chars.len() && chars[pos].is_whitespace() {
52 pos += 1;
53 }
54
55 self.input_cursor_position = pos;
56 pos
57 }
58
59 pub fn move_word_backward(&mut self, text: &str) -> usize {
61 let chars: Vec<char> = text.chars().collect();
62 let mut pos = self.input_cursor_position;
63
64 if pos == 0 {
65 return 0;
66 }
67
68 pos -= 1;
69
70 while pos > 0 && chars[pos].is_whitespace() {
72 pos -= 1;
73 }
74
75 while pos > 0 && !chars[pos - 1].is_whitespace() {
77 pos -= 1;
78 }
79
80 self.input_cursor_position = pos;
81 pos
82 }
83
84 pub fn move_to_line_start(&mut self) -> usize {
86 self.input_cursor_position = 0;
87 0
88 }
89
90 pub fn move_to_line_end(&mut self, text: &str) -> usize {
92 self.input_cursor_position = text.len();
93 text.len()
94 }
95
96 pub fn move_left(&mut self) -> usize {
98 if self.input_cursor_position > 0 {
99 self.input_cursor_position -= 1;
100 }
101 self.input_cursor_position
102 }
103
104 pub fn move_right(&mut self, text: &str) -> usize {
106 if self.input_cursor_position < text.len() {
107 self.input_cursor_position += 1;
108 }
109 self.input_cursor_position
110 }
111
112 pub fn set_position(&mut self, pos: usize) {
114 self.input_cursor_position = pos;
115 }
116
117 #[must_use]
119 pub fn position(&self) -> usize {
120 self.input_cursor_position
121 }
122
123 pub fn move_table_up(&mut self) -> (usize, usize) {
127 if self.table_cursor.0 > 0 {
128 self.table_cursor.0 -= 1;
129 }
130 self.table_cursor
131 }
132
133 pub fn move_table_down(&mut self, max_rows: usize) -> (usize, usize) {
135 if self.table_cursor.0 < max_rows.saturating_sub(1) {
136 self.table_cursor.0 += 1;
137 }
138 self.table_cursor
139 }
140
141 pub fn move_table_left(&mut self) -> (usize, usize) {
143 if self.table_cursor.1 > 0 {
144 self.table_cursor.1 -= 1;
145 }
146 self.table_cursor
147 }
148
149 pub fn move_table_right(&mut self, max_cols: usize) -> (usize, usize) {
151 if self.table_cursor.1 < max_cols.saturating_sub(1) {
152 self.table_cursor.1 += 1;
153 }
154 self.table_cursor
155 }
156
157 pub fn move_table_home(&mut self) -> (usize, usize) {
159 self.table_cursor.0 = 0;
160 self.table_cursor
161 }
162
163 pub fn move_table_end(&mut self, max_rows: usize) -> (usize, usize) {
165 self.table_cursor.0 = max_rows.saturating_sub(1);
166 self.table_cursor
167 }
168
169 pub fn page_up(&mut self, page_size: usize) -> (usize, usize) {
171 self.table_cursor.0 = self.table_cursor.0.saturating_sub(page_size);
172 self.table_cursor
173 }
174
175 pub fn page_down(&mut self, page_size: usize, max_rows: usize) -> (usize, usize) {
177 self.table_cursor.0 = (self.table_cursor.0 + page_size).min(max_rows.saturating_sub(1));
178 self.table_cursor
179 }
180
181 #[must_use]
183 pub fn table_position(&self) -> (usize, usize) {
184 self.table_cursor
185 }
186
187 pub fn reset_table_cursor(&mut self) {
189 self.table_cursor = (0, 0);
190 }
191
192 pub fn update_horizontal_scroll(&mut self, cursor_col: usize, viewport_width: u16) {
196 let cursor_x = cursor_col as u16;
197
198 if cursor_x >= self.horizontal_scroll + viewport_width {
200 self.horizontal_scroll = cursor_x.saturating_sub(viewport_width - 1);
201 }
202
203 if cursor_x < self.horizontal_scroll {
205 self.horizontal_scroll = cursor_x;
206 }
207 }
208
209 pub fn update_vertical_scroll(&mut self, cursor_row: usize, viewport_height: usize) {
211 if cursor_row >= self.vertical_scroll + viewport_height {
213 self.vertical_scroll = cursor_row.saturating_sub(viewport_height - 1);
214 }
215
216 if cursor_row < self.vertical_scroll {
218 self.vertical_scroll = cursor_row;
219 }
220 }
221
222 #[must_use]
224 pub fn scroll_offsets(&self) -> (u16, usize) {
225 (self.horizontal_scroll, self.vertical_scroll)
226 }
227
228 pub fn set_scroll_offsets(&mut self, horizontal: u16, vertical: usize) {
230 self.horizontal_scroll = horizontal;
231 self.vertical_scroll = vertical;
232 }
233
234 pub fn reset_horizontal_scroll(&mut self) {
236 self.horizontal_scroll = 0;
237 }
238
239 #[must_use]
243 pub fn get_word_at_cursor(&self, text: &str) -> Option<(usize, usize, String)> {
244 if text.is_empty() || self.input_cursor_position > text.len() {
245 return None;
246 }
247
248 let chars: Vec<char> = text.chars().collect();
249 let mut start = self.input_cursor_position;
250 let mut end = self.input_cursor_position;
251
252 if start < chars.len() && chars[start].is_whitespace() {
254 return None;
255 }
256
257 while start > 0 && !chars[start - 1].is_whitespace() {
259 start -= 1;
260 }
261
262 while end < chars.len() && !chars[end].is_whitespace() {
264 end += 1;
265 }
266
267 let word: String = chars[start..end].iter().collect();
268 Some((start, end, word))
269 }
270
271 #[must_use]
273 pub fn get_partial_word_before_cursor(&self, text: &str) -> Option<String> {
274 if self.input_cursor_position == 0 {
275 return None;
276 }
277
278 let before_cursor = &text[..self.input_cursor_position];
279 let last_space = before_cursor.rfind(' ').map_or(0, |i| i + 1);
280
281 if last_space < self.input_cursor_position {
282 Some(before_cursor[last_space..].to_string())
283 } else {
284 None
285 }
286 }
287}
288
289pub trait CursorBuffer {
291 fn cursor_manager(&self) -> &CursorManager;
292 fn cursor_manager_mut(&mut self) -> &mut CursorManager;
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_word_navigation() {
301 let mut cm = CursorManager::new();
302 let text = "SELECT * FROM table WHERE id = 1";
303
304 assert_eq!(cm.position(), 0);
306
307 cm.move_word_forward(text);
309 assert_eq!(cm.position(), 7); cm.move_word_forward(text);
312 assert_eq!(cm.position(), 9); cm.move_word_backward(text);
316 assert_eq!(cm.position(), 7); cm.move_word_backward(text);
319 assert_eq!(cm.position(), 0); }
321
322 #[test]
323 fn test_table_navigation() {
324 let mut cm = CursorManager::new();
325
326 cm.move_table_down(10);
328 assert_eq!(cm.table_position(), (1, 0));
329
330 cm.move_table_right(5);
331 assert_eq!(cm.table_position(), (1, 1));
332
333 cm.move_table_end(10);
335 assert_eq!(cm.table_position(), (9, 1));
336
337 cm.move_table_home();
338 assert_eq!(cm.table_position(), (0, 1));
339 }
340
341 #[test]
342 fn test_scroll_management() {
343 let mut cm = CursorManager::new();
344
345 cm.update_horizontal_scroll(100, 80);
347 assert_eq!(cm.scroll_offsets().0, 21); cm.update_vertical_scroll(50, 20);
351 assert_eq!(cm.scroll_offsets().1, 31); }
353
354 #[test]
355 fn test_word_extraction() {
356 let mut cm = CursorManager::new();
357 let text = "SELECT column FROM table";
358
359 cm.set_position(7);
361 let word = cm.get_word_at_cursor(text);
362 assert_eq!(word, Some((7, 13, "column".to_string())));
363
364 cm.set_position(6);
366 let word = cm.get_word_at_cursor(text);
367 assert_eq!(word, None);
368
369 cm.set_position(10); let partial = cm.get_partial_word_before_cursor(text);
372 assert_eq!(partial, Some("col".to_string()));
373 }
374}