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