1use crossterm::event::{
2 KeyEvent, KeyModifiers,
3 MouseEvent, MouseEventKind, MouseButton,
4};
5use ratatui::style::Color;
6use ratatui::style::Style;
7use ratatui::prelude::*;
8use std::time::Duration;
9use crate::click::{ClickKind, ClickTracker};
10use crate::code::Code;
11use crate::code::{EditKind, EditBatch};
12use crate::code::{RopeGraphemes, grapheme_width_and_chars_len, grapheme_width};
13use crate::selection::{Selection, SelectionSnap};
14use crate::actions::*;
15use crate::utils;
16use std::collections::HashMap;
17use std::cell::RefCell;
18use std::cmp::Ordering;
19use anyhow::{Result, anyhow};
20
21type Theme = HashMap<String, Style>;
23type Hightlight = (usize, usize, Style);
25type HightlightCache = HashMap<(usize, usize), Vec<Hightlight>>;
27
28pub struct Editor {
31 pub(crate) code: Code,
33 pub(crate) cursor: usize,
35
36 pub(crate) offset_y: usize,
38
39 pub(crate) offset_x: usize,
41
42 pub(crate) theme: Theme,
44
45 pub(crate) selection: Option<Selection>,
47
48 pub(crate) clicks: ClickTracker,
50
51 pub(crate) selection_snap: SelectionSnap,
53
54 pub(crate) clipboard: Option<String>,
56
57 pub(crate) marks: Option<Vec<(usize, usize, Color)>>,
59
60 pub(crate) highlights_cache: RefCell<HightlightCache>,
62}
63
64impl Editor {
65 pub fn new(lang: &str, text: &str, theme: Vec<(&str, &str)>) -> Self {
67 let code = Code::new(text, lang)
68 .or_else(|_| Code::new(text, "text"))
69 .unwrap();
70
71 let theme = Self::build_theme(&theme);
72 let highlights_cache = RefCell::new(HashMap::new());
73
74 Self {
75 code,
76 cursor: 0,
77 offset_y: 0,
78 offset_x: 0,
79 theme,
80 selection: None,
81 clicks: ClickTracker::new(Duration::from_millis(700)),
82 selection_snap: SelectionSnap::None,
83 clipboard: None,
84 marks: None,
85 highlights_cache,
86 }
87 }
88
89 pub fn input(
90 &mut self, key: KeyEvent, area: &Rect,
91 ) -> Result<()> {
92 use crossterm::event::KeyCode;
93
94 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
95 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
96 let _alt = key.modifiers.contains(KeyModifiers::ALT);
97
98 match key.code {
99 KeyCode::Char('รท') => self.apply(ToggleComment { }),
100 KeyCode::Char('z') if ctrl => self.apply(Undo { }),
101 KeyCode::Char('y') if ctrl => self.apply(Redo { }),
102 KeyCode::Char('c') if ctrl => self.apply(Copy { }),
103 KeyCode::Char('v') if ctrl => self.apply(Paste { }),
104 KeyCode::Char('x') if ctrl => self.apply(Cut { }),
105 KeyCode::Char('k') if ctrl => self.apply(DeleteLine { }),
106 KeyCode::Char('d') if ctrl => self.apply(Duplicate { }),
107 KeyCode::Char('a') if ctrl => self.apply(SelectAll { }),
108 KeyCode::Left => self.apply(MoveLeft { shift }),
109 KeyCode::Right => self.apply(MoveRight { shift }),
110 KeyCode::Up => self.apply(MoveUp { shift }),
111 KeyCode::Down => self.apply(MoveDown { shift }),
112 KeyCode::Backspace => self.apply(Delete { }),
113 KeyCode::Enter => self.apply(InsertNewline { }),
114 KeyCode::Char(c) => self.apply(InsertText { text: c.to_string() }),
115 KeyCode::Tab => self.apply(Indent { }),
116 KeyCode::BackTab => self.apply(UnIndent { }),
117 _ => {}
118 }
119 self.focus(&area);
120 Ok(())
121 }
122
123 pub fn focus(&mut self, area: &Rect) {
124 let width = area.width as usize;
125 let height = area.height as usize;
126 let total_lines = self.code.len_lines();
127 let max_line_number = total_lines.max(1);
128 let line_number_digits = max_line_number.to_string().len().max(5);
129 let line_number_width = (line_number_digits + 2) as usize;
130
131 let line = self.code.char_to_line(self.cursor);
132 let col = self.cursor - self.code.line_to_char(line);
133
134 let visible_width = width.saturating_sub(line_number_width);
135 let visible_height = height;
136
137 let step_size = 10;
138 if col < self.offset_x {
139 self.offset_x = col.saturating_sub(step_size);
140 } else if col >= self.offset_x + visible_width {
141 self.offset_x = col.saturating_sub(visible_width - step_size);
142 }
143
144 if line < self.offset_y {
145 self.offset_y = line;
146 } else if line >= self.offset_y + visible_height {
147 self.offset_y = line.saturating_sub(visible_height - 1);
148 }
149 }
150
151 pub fn mouse(
152 &mut self, mouse: MouseEvent, area: &Rect,
153 ) -> Result<()> {
154
155 match mouse.kind {
156 MouseEventKind::ScrollUp => self.scroll_up(),
157 MouseEventKind::ScrollDown => self.scroll_down(area.height as usize),
158 MouseEventKind::Down(MouseButton::Left) => {
159 let pos = self.cursor_from_mouse(mouse.column, mouse.row, area);
160 if let Some(cursor) = pos {
161 self.handle_mouse_down(cursor);
162 }
163 }
164 MouseEventKind::Drag(MouseButton::Left) => {
165 if mouse.row == area.top() {
167 self.scroll_up();
168 }
169 if mouse.row == area.bottom().saturating_sub(1) {
170 self.scroll_down(area.height as usize);
171 }
172 let pos = self.cursor_from_mouse(mouse.column, mouse.row, area);
173 if let Some(cursor) = pos {
174 self.handle_mouse_drag(cursor);
175 }
176 }
177 MouseEventKind::Up(MouseButton::Left) => {
178 self.selection_snap = SelectionSnap::None;
179 }
180 _ => {}
181 }
182 Ok(())
183 }
184
185 fn handle_mouse_down(&mut self, cursor: usize) {
186 let kind = self.clicks.register(cursor);
187 let (start, end, snap) = match kind {
188 ClickKind::Triple => {
189 let (line_start, line_end) = self.code.line_boundaries(cursor);
190 (line_start, line_end, SelectionSnap::Line { anchor: cursor })
191 }
192 ClickKind::Double => {
193 let (word_start, word_end) = self.code.word_boundaries(cursor);
194 (word_start, word_end, SelectionSnap::Word { anchor: cursor })
195 }
196 ClickKind::Single => (cursor, cursor, SelectionSnap::None),
197 };
198
199 self.selection = Some(Selection::from_anchor_and_cursor(start, end));
200 self.cursor = end;
201 self.selection_snap = snap;
202 }
203
204 fn handle_mouse_drag(&mut self, cursor: usize) {
205 match self.selection_snap {
206 SelectionSnap::Line { anchor } => {
207 let (anchor_start, anchor_end) = self.code.line_boundaries(anchor);
208 let (cur_start, cur_end) = self.code.line_boundaries(cursor);
209
210 let (sel_start, sel_end, new_cursor) = match cursor.cmp(&anchor) {
211 Ordering::Greater => (anchor_start, cur_end, cur_end), Ordering::Less => (cur_start, anchor_end, cur_start), Ordering::Equal => (anchor_start, anchor_end, anchor_end),
214 };
215
216 self.selection = Some(Selection::from_anchor_and_cursor(sel_start, sel_end));
217 self.cursor = new_cursor;
218 }
219 SelectionSnap::Word { anchor } => {
220 let (anchor_start, anchor_end) = self.code.word_boundaries(anchor);
221 let (cur_start, cur_end) = self.code.word_boundaries(cursor);
222
223 let (sel_start, sel_end, new_cursor) = match cursor.cmp(&anchor) {
224 Ordering::Greater => (anchor_start, cur_end, cur_end), Ordering::Less => (cur_start, anchor_end, cur_start), Ordering::Equal => (anchor_start, anchor_end, anchor_end),
227 };
228
229 self.selection = Some(Selection::from_anchor_and_cursor(sel_start, sel_end));
230 self.cursor = new_cursor;
231 }
232 SelectionSnap::None => {
233 let anchor = self.selection_anchor();
234 self.selection = Some(Selection::from_anchor_and_cursor(anchor, cursor));
235 self.cursor = cursor;
236 }
237 }
238 }
239
240 fn cursor_from_mouse(
241 &self, mouse_x: u16, mouse_y: u16, area: &Rect
242 ) -> Option<usize> {
243 let total_lines = self.code.len_lines();
244 let max_line_number = total_lines.max(1);
245 let line_number_digits = max_line_number.to_string().len().max(5);
246 let line_number_width = (line_number_digits + 2) as u16;
247
248 if mouse_y < area.top()
249 || mouse_y >= area.bottom()
250 || mouse_x < area.left() + line_number_width
251 {
252 return None;
253 }
254
255 let clicked_row = (mouse_y - area.top()) as usize + self.offset_y;
256 if clicked_row >= self.code.len_lines() {
257 return None;
258 }
259
260 let clicked_col = (mouse_x - area.left() - line_number_width) as usize;
261
262 let line_start_char = self.code.line_to_char(clicked_row);
263 let line_len = self.code.line_len(clicked_row);
264
265 let start_col = self.offset_x.min(line_len);
266 let end_col = line_len;
267
268 let char_start = line_start_char + start_col;
269 let char_end = line_start_char + end_col;
270
271 let mut current_col = 0;
272 let mut char_idx = start_col;
273 let visible_chars = self.code.char_slice(char_start, char_end);
274 for g in RopeGraphemes::new(&visible_chars) {
275 let (g_width, g_chars) = grapheme_width_and_chars_len(g);
276 if current_col + g_width > clicked_col { break; }
277 current_col += g_width;
278 char_idx += g_chars;
279 }
280
281 let line = self.code.char_slice(line_start_char, line_start_char + line_len);
282 let visual_width: usize = RopeGraphemes::new(&line).map(grapheme_width).sum();
283
284 if clicked_col + self.offset_x >= visual_width {
285 let mut end_idx = line.len_chars();
286 if end_idx > 0 && line.char(end_idx - 1) == '\n' {
287 end_idx -= 1;
288 }
289 char_idx = end_idx;
290 }
291
292 Some(line_start_char + char_idx)
293 }
294
295 pub fn clear_selection(&mut self) {
297 self.selection = None;
298 }
299
300 pub fn extend_selection(&mut self, new_cursor: usize) {
302 let anchor = self.selection_anchor();
305 self.selection = Some(Selection::from_anchor_and_cursor(anchor, new_cursor));
306 }
307
308 pub fn selection_anchor(&self) -> usize {
310 self.selection
311 .as_ref()
312 .map(|s| if self.cursor == s.start { s.end } else { s.start })
313 .unwrap_or(self.cursor)
314 }
315
316 pub fn apply<A: Action>(&mut self, mut action: A) {
317 action.apply(self);
318 }
319
320 pub fn set_content(&mut self, content: &str) {
321 self.code.tx();
322 self.code.set_state_before(self.cursor, self.selection);
323 self.code.remove(0, self.code.len());
324 self.code.insert(0, content);
325 self.code.set_state_after(self.cursor, self.selection);
326 self.code.commit();
327 self.reset_highlight_cache();
328 }
329
330 pub fn apply_batch(&mut self, batch: &EditBatch) {
331 self.code.tx();
332
333 if let Some(state) = &batch.state_before {
334 self.code.set_state_before(state.offset, state.selection);
335 }
336 if let Some(state) = &batch.state_after {
337 self.code.set_state_after(state.offset, state.selection);
338 }
339
340 for edit in &batch.edits {
341 match &edit.kind {
342 EditKind::Insert { offset, text } => {
343 self.code.insert(*offset, text);
344 }
345 EditKind::Remove { offset, text } => {
346 self.code.remove(*offset, *offset + text.chars().count());
347 }
348 }
349 }
350 self.code.commit();
351 self.reset_highlight_cache();
352 }
353
354 pub fn set_cursor(&mut self, cursor: usize) {
355 self.cursor = cursor;
356 self.fit_cursor();
357 }
358
359 pub fn fit_cursor(&mut self) {
360 let len = self.code.len_chars();
362 self.cursor = self.cursor.min(len);
363
364 let (row, col) = self.code.point(self.cursor);
366 if col > self.code.line_len(row) {
367 self.cursor = self.code.line_to_char(row) + self.code.line_len(row);
368 }
369 }
370
371 pub fn scroll_up(&mut self) {
372 if self.offset_y > 0 {
373 self.offset_y -= 1;
374 }
375 }
376
377 pub fn scroll_down(&mut self, area_height: usize) {
378 let len_lines = self.code.len_lines();
379 if self.offset_y < len_lines.saturating_sub(area_height) {
380 self.offset_y += 1;
381 }
382 }
383
384 fn build_theme(theme: &Vec<(&str, &str)>) -> Theme {
385 theme.into_iter()
386 .map(|(name, hex)| {
387 let (r, g, b) = utils::rgb(hex);
388 (name.to_string(), Style::default().fg(Color::Rgb(r, g, b)))
389 })
390 .collect()
391 }
392
393 pub fn get_content(&self) -> String {
394 self.code.get_content()
395 }
396
397 pub fn get_content_slice(&self, start: usize, end: usize) -> String {
398 self.code.slice(start, end)
399 }
400
401 pub fn get_cursor(&self) -> usize {
402 self.cursor
403 }
404
405 pub fn set_clipboard(&mut self, text: &str) -> Result<()> {
406 arboard::Clipboard::new()
407 .and_then(|mut c| c.set_text(text.to_string()))
408 .unwrap_or_else(|_| self.clipboard = Some(text.to_string()));
409 Ok(())
410 }
411
412 pub fn get_clipboard(&self) -> Result<String> {
413 arboard::Clipboard::new()
414 .and_then(|mut c| c.get_text())
415 .ok()
416 .or_else(|| self.clipboard.clone())
417 .ok_or_else(|| anyhow!("cant get clipboard"))
418 }
419
420 pub fn set_marks(&mut self, marks: Vec<(usize, usize, &str)>) {
421 self.marks = Some(
422 marks.into_iter()
423 .map(|(start, end, color)| {
424 let (r, g, b) = utils::rgb(color);
425 (start, end, Color::Rgb(r, g, b))
426 })
427 .collect()
428 );
429 }
430
431 pub fn remove_marks(&mut self) {
432 self.marks = None;
433 }
434
435 pub fn has_marks(&self) -> bool {
436 self.marks.is_some()
437 }
438
439 pub fn get_marks(&self) -> Option<&Vec<(usize, usize, Color)>> {
440 self.marks.as_ref()
441 }
442
443 pub fn get_selection_text(&mut self) -> Option<String> {
444 if let Some(selection) = &self.selection && !selection.is_empty() {
445 let text = self.code.slice(selection.start, selection.end);
446 return Some(text);
447 }
448 None
449 }
450
451 pub fn get_selection(&mut self) -> Option<Selection> {
452 return self.selection;
453 }
454
455 pub fn set_selection(&mut self, selection: Option<Selection>) {
456 self.selection = selection;
457 }
458
459 pub fn set_offset_y(&mut self, offset_y: usize) {
460 self.offset_y = offset_y;
461 }
462
463 pub fn set_offset_x(&mut self, offset_x: usize) {
464 self.offset_x = offset_x;
465 }
466
467 pub fn get_offset_y(&self) -> usize {
468 self.offset_y
469 }
470
471 pub fn get_offset_x(&self) -> usize {
472 self.offset_x
473 }
474
475 pub fn code_mut(&mut self) -> &mut Code {
476 &mut self.code
477 }
478
479 pub fn code_ref(&self) -> &Code {
480 &self.code
481 }
482
483 pub fn set_change_callback(
485 &mut self, callback: Box<dyn Fn(Vec<(usize, usize, usize, usize, String)>)>
486 ) {
487 self.code.set_change_callback(callback);
488 }
489
490 pub fn highlight_interval(
491 &self, start: usize, end: usize, theme: &Theme
492 ) -> Vec<(usize, usize, Style)> {
493 let mut cache = self.highlights_cache.borrow_mut();
494 let key = (start, end);
495 if let Some(v) = cache.get(&key) {
496 return v.clone();
497 }
498
499 let highlights = self.code.highlight_interval(start, end, theme);
500 cache.insert(key, highlights.clone());
501 highlights
502 }
503
504 pub fn reset_highlight_cache(&self) {
505 self.highlights_cache.borrow_mut().clear();
506 }
507
508 pub fn get_visible_cursor(
510 &self, area: &Rect
511 ) -> Option<(u16, u16)> {
512 let total_lines = self.code.len_lines();
513 let max_line_number = total_lines.max(1);
514 let line_number_digits = max_line_number.to_string().len().max(5);
515 let line_number_width = line_number_digits + 2;
516
517 let (cursor_line, cursor_char_col) = self.code.point(self.cursor);
518
519 if cursor_line >= self.offset_y && cursor_line < self.offset_y + area.height as usize {
520 let line_start_char = self.code.line_to_char(cursor_line);
521 let line_len = self.code.line_len(cursor_line);
522
523 let max_x = (area.width as usize).saturating_sub(line_number_width);
524 let start_col = self.offset_x;
525
526 let cursor_visual_col: usize = {
527 let slice = self.code.char_slice(line_start_char, line_start_char + cursor_char_col.min(line_len));
528 RopeGraphemes::new(&slice).map(grapheme_width).sum()
529 };
530
531 let offset_visual_col: usize = {
532 let slice = self.code.char_slice(line_start_char, line_start_char + start_col.min(line_len));
533 RopeGraphemes::new(&slice).map(grapheme_width).sum()
534 };
535
536 let relative_visual_col = cursor_visual_col.saturating_sub(offset_visual_col);
537 let visible_x = relative_visual_col.min(max_x);
538
539 let cursor_x = area.left() + (line_number_width + visible_x) as u16;
540 let cursor_y = area.top() + (cursor_line - self.offset_y) as u16;
541
542 if cursor_x < area.right() && cursor_y < area.bottom() {
543 return Some((cursor_x, cursor_y));
544 }
545 }
546
547 return None;
548 }
549}