1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use std::cell::RefCell;
3use std::collections::HashMap;
4use sveltui::prelude::FocusId;
5
6thread_local! {
7 static CODE_BUFFER_MAP: RefCell<HashMap<FocusId, CodeBuffer>> = RefCell::new(HashMap::new());
8}
9
10pub fn with_code_buffer<R>(id: FocusId, f: impl FnOnce(&CodeBuffer) -> R) -> R {
11 CODE_BUFFER_MAP.with(|m| {
12 let mut map = m.borrow_mut();
13 let buf = map.entry(id).or_default();
14 f(buf)
15 })
16}
17
18pub fn with_code_buffer_mut<R>(id: FocusId, f: impl FnOnce(&mut CodeBuffer) -> R) -> R {
19 CODE_BUFFER_MAP.with(|m| {
20 let mut map = m.borrow_mut();
21 let buf = map.entry(id).or_default();
22 f(buf)
23 })
24}
25
26pub fn get_code_buffer(id: FocusId) -> CodeBuffer {
27 with_code_buffer(id, |b| b.clone())
28}
29
30pub fn set_code_buffer(id: FocusId, buf: CodeBuffer) {
31 CODE_BUFFER_MAP.with(|m| {
32 m.borrow_mut().insert(id, buf);
33 });
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37struct HistoryEntry {
38 lines: Vec<String>,
39 cursor_row: usize,
40 cursor_col: usize,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct CodeBuffer {
45 lines: Vec<String>,
46 pub cursor_row: usize,
47 pub cursor_col: usize,
48 pub scroll_row: usize,
49 pub scroll_col: usize,
50 preferred_col: Option<usize>,
51 pub tab_size: usize,
52 undo_stack: Vec<HistoryEntry>,
53 redo_stack: Vec<HistoryEntry>,
54}
55
56impl Default for CodeBuffer {
57 fn default() -> Self {
58 Self::new("")
59 }
60}
61
62pub fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
63 s.char_indices()
64 .nth(char_idx)
65 .map(|(b, _)| b)
66 .unwrap_or(s.len())
67}
68
69impl CodeBuffer {
70 pub fn new(content: impl Into<String>) -> Self {
71 let s = content.into();
72 let lines: Vec<String> = if s.is_empty() {
73 vec![String::new()]
74 } else {
75 let mut l: Vec<String> = s.lines().map(|line| line.to_string()).collect();
76 if s.ends_with('\n') {
77 l.push(String::new());
78 }
79 if l.is_empty() { vec![String::new()] } else { l }
80 };
81
82 let cursor_row = lines.len().saturating_sub(1);
83 let cursor_col = lines[cursor_row].chars().count();
84
85 Self {
86 lines,
87 cursor_row,
88 cursor_col,
89 scroll_row: 0,
90 scroll_col: 0,
91 preferred_col: None,
92 tab_size: 4,
93 undo_stack: Vec::new(),
94 redo_stack: Vec::new(),
95 }
96 }
97
98 fn push_undo(&mut self) {
99 if self.undo_stack.len() >= 100 {
100 self.undo_stack.remove(0);
101 }
102 self.undo_stack.push(HistoryEntry {
103 lines: self.lines.clone(),
104 cursor_row: self.cursor_row,
105 cursor_col: self.cursor_col,
106 });
107 self.redo_stack.clear();
108 }
109
110 pub fn undo(&mut self) -> bool {
111 let Some(prev) = self.undo_stack.pop() else {
112 return false;
113 };
114 self.redo_stack.push(HistoryEntry {
115 lines: self.lines.clone(),
116 cursor_row: self.cursor_row,
117 cursor_col: self.cursor_col,
118 });
119 self.lines = prev.lines;
120 self.cursor_row = prev.cursor_row.min(self.lines.len().saturating_sub(1));
121 self.cursor_col = prev.cursor_col.min(self.line_len(self.cursor_row));
122 self.preferred_col = None;
123 true
124 }
125
126 pub fn redo(&mut self) -> bool {
127 let Some(next) = self.redo_stack.pop() else {
128 return false;
129 };
130 self.undo_stack.push(HistoryEntry {
131 lines: self.lines.clone(),
132 cursor_row: self.cursor_row,
133 cursor_col: self.cursor_col,
134 });
135 self.lines = next.lines;
136 self.cursor_row = next.cursor_row.min(self.lines.len().saturating_sub(1));
137 self.cursor_col = next.cursor_col.min(self.line_len(self.cursor_row));
138 self.preferred_col = None;
139 true
140 }
141
142 pub fn content(&self) -> String {
143 self.lines.join("\n")
144 }
145
146 pub fn set_content(&mut self, text: impl Into<String>) {
147 let s = text.into();
148 self.lines = if s.is_empty() {
149 vec![String::new()]
150 } else {
151 let mut l: Vec<String> = s.lines().map(|line| line.to_string()).collect();
152 if s.ends_with('\n') {
153 l.push(String::new());
154 }
155 if l.is_empty() { vec![String::new()] } else { l }
156 };
157 let max_row = self.lines.len().saturating_sub(1);
158 self.cursor_row = self.cursor_row.min(max_row);
159 self.cursor_col = self.cursor_col.min(self.line_len(self.cursor_row));
160 self.preferred_col = None;
161 }
162
163 pub fn lines(&self) -> &[String] {
164 &self.lines
165 }
166
167 pub fn line(&self, row: usize) -> Option<&str> {
168 self.lines.get(row).map(|s| s.as_str())
169 }
170
171 pub fn line_count(&self) -> usize {
172 self.lines.len()
173 }
174
175 pub fn line_len(&self, row: usize) -> usize {
176 self.lines.get(row).map(|l| l.chars().count()).unwrap_or(0)
177 }
178
179 pub fn is_empty(&self) -> bool {
180 self.lines.is_empty() || (self.lines.len() == 1 && self.lines[0].is_empty())
181 }
182
183 pub fn cursor_pos(&self) -> (usize, usize) {
184 (self.cursor_row, self.cursor_col)
185 }
186
187 pub fn set_cursor_pos(&mut self, row: usize, col: usize) {
188 let max_row = self.lines.len().saturating_sub(1);
189 self.cursor_row = row.min(max_row);
190 self.cursor_col = col.min(self.line_len(self.cursor_row));
191 self.preferred_col = None;
192 }
193
194 pub fn move_left(&mut self) {
197 if self.cursor_col > 0 {
198 self.cursor_col -= 1;
199 self.preferred_col = None;
200 } else if self.cursor_row > 0 {
201 self.cursor_row -= 1;
202 self.cursor_col = self.line_len(self.cursor_row);
203 self.preferred_col = None;
204 }
205 }
206
207 pub fn move_right(&mut self) {
208 let cur_line_len = self.line_len(self.cursor_row);
209 if self.cursor_col < cur_line_len {
210 self.cursor_col += 1;
211 self.preferred_col = None;
212 } else if self.cursor_row + 1 < self.lines.len() {
213 self.cursor_row += 1;
214 self.cursor_col = 0;
215 self.preferred_col = None;
216 }
217 }
218
219 pub fn move_up(&mut self) {
220 if self.cursor_row > 0 {
221 let target_col = self.preferred_col.unwrap_or(self.cursor_col);
222 self.preferred_col = Some(target_col);
223 self.cursor_row -= 1;
224 self.cursor_col = target_col.min(self.line_len(self.cursor_row));
225 }
226 }
227
228 pub fn move_down(&mut self) {
229 if self.cursor_row + 1 < self.lines.len() {
230 let target_col = self.preferred_col.unwrap_or(self.cursor_col);
231 self.preferred_col = Some(target_col);
232 self.cursor_row += 1;
233 self.cursor_col = target_col.min(self.line_len(self.cursor_row));
234 }
235 }
236
237 pub fn move_home(&mut self) {
238 let cur_line = &self.lines[self.cursor_row];
240 let first_non_ws = cur_line.chars().take_while(|c| c.is_whitespace()).count();
241
242 if self.cursor_col == first_non_ws {
243 self.cursor_col = 0;
244 } else {
245 self.cursor_col = first_non_ws;
246 }
247 self.preferred_col = None;
248 }
249
250 pub fn move_end(&mut self) {
251 self.cursor_col = self.line_len(self.cursor_row);
252 self.preferred_col = None;
253 }
254
255 pub fn move_top(&mut self) {
256 self.cursor_row = 0;
257 self.cursor_col = 0;
258 self.preferred_col = None;
259 }
260
261 pub fn move_bottom(&mut self) {
262 self.cursor_row = self.lines.len().saturating_sub(1);
263 self.cursor_col = self.line_len(self.cursor_row);
264 self.preferred_col = None;
265 }
266
267 pub fn page_up(&mut self, page_size: usize) {
268 let target_col = self.preferred_col.unwrap_or(self.cursor_col);
269 self.preferred_col = Some(target_col);
270 self.cursor_row = self.cursor_row.saturating_sub(page_size);
271 self.cursor_col = target_col.min(self.line_len(self.cursor_row));
272 }
273
274 pub fn page_down(&mut self, page_size: usize) {
275 let target_col = self.preferred_col.unwrap_or(self.cursor_col);
276 self.preferred_col = Some(target_col);
277 let max_row = self.lines.len().saturating_sub(1);
278 self.cursor_row = (self.cursor_row + page_size).min(max_row);
279 self.cursor_col = target_col.min(self.line_len(self.cursor_row));
280 }
281
282 pub fn move_word_left(&mut self) {
283 if self.cursor_col == 0 {
284 if self.cursor_row > 0 {
285 self.move_left();
286 }
287 return;
288 }
289 let line = &self.lines[self.cursor_row];
290 let chars: Vec<char> = line.chars().collect();
291 let mut idx = self.cursor_col;
292
293 while idx > 0 && chars[idx - 1].is_whitespace() {
294 idx -= 1;
295 }
296 while idx > 0 && !chars[idx - 1].is_whitespace() {
297 idx -= 1;
298 }
299 self.cursor_col = idx;
300 self.preferred_col = None;
301 }
302
303 pub fn move_word_right(&mut self) {
304 let cur_len = self.line_len(self.cursor_row);
305 if self.cursor_col >= cur_len {
306 if self.cursor_row + 1 < self.lines.len() {
307 self.move_right();
308 }
309 return;
310 }
311 let line = &self.lines[self.cursor_row];
312 let chars: Vec<char> = line.chars().collect();
313 let mut idx = self.cursor_col;
314
315 while idx < cur_len && !chars[idx].is_whitespace() {
316 idx += 1;
317 }
318 while idx < cur_len && chars[idx].is_whitespace() {
319 idx += 1;
320 }
321 self.cursor_col = idx;
322 self.preferred_col = None;
323 }
324
325 pub fn insert_char(&mut self, c: char) {
328 self.push_undo();
329 if c == '\n' {
330 self.insert_newline();
331 } else {
332 let byte_pos = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
333 self.lines[self.cursor_row].insert(byte_pos, c);
334 self.cursor_col += 1;
335 self.preferred_col = None;
336 }
337 }
338
339 pub fn insert_str(&mut self, s: &str) {
340 self.push_undo();
341 for c in s.chars() {
342 if c == '\n' {
343 let byte_pos = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
344 let rest = self.lines[self.cursor_row][byte_pos..].to_string();
345 self.lines[self.cursor_row].truncate(byte_pos);
346 self.cursor_row += 1;
347 self.lines.insert(self.cursor_row, rest);
348 self.cursor_col = 0;
349 } else {
350 let byte_pos = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
351 self.lines[self.cursor_row].insert(byte_pos, c);
352 self.cursor_col += 1;
353 }
354 }
355 self.preferred_col = None;
356 }
357
358 pub fn insert_tab(&mut self) {
359 let spaces = " ".repeat(self.tab_size);
360 self.insert_str(&spaces);
361 }
362
363 pub fn insert_newline(&mut self) {
364 let curr_line = &self.lines[self.cursor_row];
365 let byte_pos = char_to_byte_index(curr_line, self.cursor_col);
366
367 let before_cursor = &curr_line[..byte_pos];
368 let rest = curr_line[byte_pos..].to_string();
369
370 let mut indent: String = before_cursor
372 .chars()
373 .take_while(|c| *c == ' ' || *c == '\t')
374 .collect();
375
376 let trimmed_before = before_cursor.trim_end();
378 if trimmed_before.ends_with('{')
379 || trimmed_before.ends_with('(')
380 || trimmed_before.ends_with('[')
381 || trimmed_before.ends_with(':')
382 {
383 indent.push_str(&" ".repeat(self.tab_size));
384 }
385
386 self.lines[self.cursor_row].truncate(byte_pos);
387 self.cursor_row += 1;
388 let mut new_line = indent;
389 let new_col = new_line.chars().count();
390 new_line.push_str(&rest);
391 self.lines.insert(self.cursor_row, new_line);
392 self.cursor_col = new_col;
393 self.preferred_col = None;
394 }
395
396 pub fn backspace(&mut self) -> bool {
397 self.push_undo();
398 if self.cursor_col > 0 {
399 let line = &self.lines[self.cursor_row];
400 let is_in_leading_ws = line[..char_to_byte_index(line, self.cursor_col)]
401 .chars()
402 .all(|c| c == ' ');
403
404 let del_chars = if is_in_leading_ws
406 && self.cursor_col >= self.tab_size
407 && (self.cursor_col % self.tab_size == 0)
408 {
409 self.tab_size
410 } else {
411 1
412 };
413
414 let del_idx = self.cursor_col.saturating_sub(del_chars);
415 let start_byte = char_to_byte_index(&self.lines[self.cursor_row], del_idx);
416 let end_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
417 self.lines[self.cursor_row].drain(start_byte..end_byte);
418 self.cursor_col = del_idx;
419 self.preferred_col = None;
420 true
421 } else if self.cursor_row > 0 {
422 let curr_line = self.lines.remove(self.cursor_row);
423 self.cursor_row -= 1;
424 self.cursor_col = self.line_len(self.cursor_row);
425 self.lines[self.cursor_row].push_str(&curr_line);
426 self.preferred_col = None;
427 true
428 } else {
429 false
430 }
431 }
432
433 pub fn delete(&mut self) -> bool {
434 self.push_undo();
435 let cur_len = self.line_len(self.cursor_row);
436 if self.cursor_col < cur_len {
437 let start_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
438 let end_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col + 1);
439 self.lines[self.cursor_row].drain(start_byte..end_byte);
440 self.preferred_col = None;
441 true
442 } else if self.cursor_row + 1 < self.lines.len() {
443 let next_line = self.lines.remove(self.cursor_row + 1);
444 self.lines[self.cursor_row].push_str(&next_line);
445 self.preferred_col = None;
446 true
447 } else {
448 false
449 }
450 }
451
452 pub fn delete_word_left(&mut self) -> bool {
453 if self.cursor_col > 0 {
454 self.push_undo();
455 let old_col = self.cursor_col;
456 self.move_word_left();
457 let new_col = self.cursor_col;
458 let start_byte = char_to_byte_index(&self.lines[self.cursor_row], new_col);
459 let end_byte = char_to_byte_index(&self.lines[self.cursor_row], old_col);
460 self.lines[self.cursor_row].drain(start_byte..end_byte);
461 self.preferred_col = None;
462 true
463 } else if self.cursor_row > 0 {
464 self.backspace()
465 } else {
466 false
467 }
468 }
469
470 pub fn delete_to_end(&mut self) -> bool {
471 let cur_len = self.line_len(self.cursor_row);
472 if self.cursor_col < cur_len {
473 self.push_undo();
474 let start_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
475 self.lines[self.cursor_row].truncate(start_byte);
476 self.preferred_col = None;
477 true
478 } else if self.cursor_row + 1 < self.lines.len() {
479 self.delete()
480 } else {
481 false
482 }
483 }
484
485 pub fn delete_to_start(&mut self) -> bool {
486 if self.cursor_col > 0 {
487 self.push_undo();
488 let end_byte = char_to_byte_index(&self.lines[self.cursor_row], self.cursor_col);
489 self.lines[self.cursor_row].drain(0..end_byte);
490 self.cursor_col = 0;
491 self.preferred_col = None;
492 true
493 } else {
494 false
495 }
496 }
497
498 pub fn duplicate_line(&mut self) {
499 self.push_undo();
500 let current_line = self.lines[self.cursor_row].clone();
501 self.cursor_row += 1;
502 self.lines.insert(self.cursor_row, current_line);
503 self.preferred_col = None;
504 }
505
506 pub fn clear(&mut self) {
507 self.push_undo();
508 self.lines = vec![String::new()];
509 self.cursor_row = 0;
510 self.cursor_col = 0;
511 self.scroll_row = 0;
512 self.scroll_col = 0;
513 self.preferred_col = None;
514 }
515
516 pub fn update_viewport(&mut self, visible_width: usize, visible_height: usize) {
517 if visible_height > 0 {
518 if self.cursor_row < self.scroll_row {
519 self.scroll_row = self.cursor_row;
520 } else if self.cursor_row >= self.scroll_row + visible_height {
521 self.scroll_row = self.cursor_row.saturating_sub(visible_height) + 1;
522 }
523 }
524 if visible_width > 0 {
525 if self.cursor_col < self.scroll_col {
526 self.scroll_col = self.cursor_col;
527 } else if self.cursor_col >= self.scroll_col + visible_width {
528 self.scroll_col = self.cursor_col.saturating_sub(visible_width) + 1;
529 }
530 }
531 }
532
533 pub fn accepts_key(&self, key: &KeyEvent) -> bool {
536 let control = key.modifiers.contains(KeyModifiers::CONTROL);
537 let alt = key.modifiers.contains(KeyModifiers::ALT);
538 match key.code {
539 KeyCode::Char(c) if control => {
540 matches!(c, 'w' | 'k' | 'u' | 'a' | 'e' | 'h' | 'd' | 'z' | 'y')
541 }
542 KeyCode::Char(_) => !alt,
543 KeyCode::Enter
544 | KeyCode::Tab
545 | KeyCode::BackTab
546 | KeyCode::Backspace
547 | KeyCode::Delete
548 | KeyCode::Left
549 | KeyCode::Right
550 | KeyCode::Up
551 | KeyCode::Down
552 | KeyCode::PageUp
553 | KeyCode::PageDown
554 | KeyCode::Home
555 | KeyCode::End => true,
556 _ => false,
557 }
558 }
559
560 pub fn handle_key(&mut self, key: &KeyEvent) -> bool {
561 let is_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
562 let is_alt = key.modifiers.contains(KeyModifiers::ALT);
563 let is_shift = key.modifiers.contains(KeyModifiers::SHIFT);
564
565 match key.code {
566 KeyCode::Char('z') if is_ctrl => {
567 if is_shift {
568 self.redo()
569 } else {
570 self.undo()
571 }
572 }
573 KeyCode::Char('y') if is_ctrl => self.redo(),
574 KeyCode::Char('w') if is_ctrl => self.delete_word_left(),
575 KeyCode::Char('k') if is_ctrl => self.delete_to_end(),
576 KeyCode::Char('u') if is_ctrl => self.delete_to_start(),
577 KeyCode::Char('d') if is_ctrl => {
578 self.duplicate_line();
579 true
580 }
581 KeyCode::Char('a') if is_ctrl => {
582 self.move_home();
583 true
584 }
585 KeyCode::Char('e') if is_ctrl => {
586 self.move_end();
587 true
588 }
589 KeyCode::Char('h') if is_ctrl => self.backspace(),
590 KeyCode::Char(c) if !is_ctrl && !is_alt => {
591 self.insert_char(c);
592 true
593 }
594 KeyCode::Enter => {
595 self.push_undo();
596 self.insert_newline();
597 true
598 }
599 KeyCode::Tab => {
600 self.push_undo();
601 self.insert_tab();
602 true
603 }
604 KeyCode::BackTab => {
605 let line = &self.lines[self.cursor_row];
607 let leading_spaces = line.chars().take_while(|c| *c == ' ').count();
608 if leading_spaces > 0 {
609 let remove_count = leading_spaces.min(self.tab_size);
610 self.lines[self.cursor_row].drain(0..remove_count);
611 self.cursor_col = self.cursor_col.saturating_sub(remove_count);
612 true
613 } else {
614 false
615 }
616 }
617 KeyCode::Backspace => {
618 if is_alt {
619 self.delete_word_left()
620 } else {
621 self.backspace()
622 }
623 }
624 KeyCode::Delete => self.delete(),
625 KeyCode::Left => {
626 if is_ctrl || is_alt {
627 self.move_word_left();
628 } else {
629 self.move_left();
630 }
631 true
632 }
633 KeyCode::Right => {
634 if is_ctrl || is_alt {
635 self.move_word_right();
636 } else {
637 self.move_right();
638 }
639 true
640 }
641 KeyCode::Up => {
642 if is_ctrl {
643 self.move_top();
644 } else {
645 self.move_up();
646 }
647 true
648 }
649 KeyCode::Down => {
650 if is_ctrl {
651 self.move_bottom();
652 } else {
653 self.move_down();
654 }
655 true
656 }
657 KeyCode::PageUp => {
658 self.page_up(10);
659 true
660 }
661 KeyCode::PageDown => {
662 self.page_down(10);
663 true
664 }
665 KeyCode::Home => {
666 if is_ctrl {
667 self.move_top();
668 } else {
669 self.move_home();
670 }
671 true
672 }
673 KeyCode::End => {
674 if is_ctrl {
675 self.move_bottom();
676 } else {
677 self.move_end();
678 }
679 true
680 }
681 _ => false,
682 }
683 }
684}