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