Skip to main content

tsu_editor/
editor.rs

1#![allow(clippy::cast_precision_loss)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_sign_loss)]
4
5pub mod render;
6pub mod render_buffer;
7
8use std::{cmp::Ordering, collections::HashMap, mem};
9
10use crossterm::{
11    ExecutableCommand,
12    event::{self, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
13    terminal,
14};
15use futures::{StreamExt, future::FutureExt, select};
16use serde::{Deserialize, Serialize};
17use unicode_segmentation::UnicodeSegmentation;
18
19use crate::{
20    action::Action,
21    buffer::Buffer,
22    color::Color,
23    command,
24    config::{Config, KeyAction},
25    debug,
26    editor::render_buffer::RenderBuffer,
27    highlighter::Highlighter,
28    log,
29    theme::{Style, Theme},
30    unicode,
31    unicode::{byte_to_char, char_to_byte, next_grapheme_boundary, prev_grapheme_boundary},
32    window_manager::WindowManager,
33};
34
35#[allow(unused)]
36pub enum GoToLinePosition {
37    Top,
38    Center,
39    Bottom,
40}
41
42#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
43pub enum Mode {
44    Normal,
45    Insert,
46    Command,
47    Visual,
48    Replace,
49}
50
51#[derive(Debug)]
52pub struct StyleInfo {
53    pub start: usize,
54    pub end: usize,
55    pub style: Style,
56}
57
58impl StyleInfo {
59    #[must_use]
60    pub fn contains(&self, pos: usize) -> bool {
61        pos >= self.start && pos < self.end
62    }
63}
64
65#[derive(Debug, Clone, Copy)]
66pub struct Point {
67    pub x: usize,
68    pub y: usize,
69}
70
71impl Point {
72    #[must_use]
73    pub fn new(x: usize, y: usize) -> Self {
74        Self { x, y }
75    }
76}
77
78impl PartialEq for Point {
79    fn eq(&self, other: &Self) -> bool {
80        self.x == other.x && self.y == other.y
81    }
82}
83
84impl PartialOrd for Point {
85    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
86        match self.y.cmp(&other.y) {
87            Ordering::Equal => self.x.partial_cmp(&other.x),
88            ordering => Some(ordering),
89        }
90    }
91}
92
93pub struct Editor {
94    config: Config,
95    pub theme: Theme,
96    highlighter: Highlighter,
97    buffers: Vec<Buffer>,
98    current_buffer_index: usize,
99    size: (u16, u16),
100    window_manager: WindowManager,
101    stdout: std::io::Stdout,
102    vtop: usize,
103    vleft: usize,
104    cursor_x: usize,
105    cursor_y: usize,
106    vx: usize,
107    mode: Mode,
108    waiting_command: Option<String>,
109    command: String,
110    waiting_key_action: Option<KeyAction>,
111    undoable_actions: Vec<Action>,
112    insert_undo_actions: Vec<Action>,
113    last_error: Option<String>,
114}
115
116impl Editor {
117    /// Creates a new editor with a given size
118    ///
119    /// # Arguments
120    /// - `width`: The desired width of the editor viewport
121    /// - `height`: The desired height of the editor viewport
122    /// - `config`: A configuration constructed from a parsed config file
123    /// - `theme`: A theme for coloring, styling, and decorating the editor
124    /// - `buffers`: A buffer which will contain the contents of the file being edited
125    ///
126    /// # Errors
127    /// Can return errors if `width` and `height` are larger than `u16::MAX` or if creating a new
128    /// `Highlighter` with the provided `theme` fails
129    ///
130    /// # Panics
131    /// This function may panic if `width` and `height` are larger than `u16::MAX`
132    pub fn with_size(
133        width: usize,
134        height: usize,
135        config: Config,
136        theme: Theme,
137        buffers: Vec<Buffer>,
138    ) -> anyhow::Result<Self> {
139        let stdout = std::io::stdout();
140        let vx = buffers.first().map_or(0, |b| b.len().to_string().len()) + 2;
141        let w = u16::try_from(width).expect("value too large to fit in u16");
142        let h = u16::try_from(height).expect("value too large to fit in u16");
143        let size = (w, h);
144        let highlighter = Highlighter::new(&theme)?;
145
146        let window_manager = WindowManager::new(0, (width, height));
147
148        Ok(Self {
149            config,
150            theme,
151            buffers,
152            current_buffer_index: 0,
153            highlighter,
154            window_manager,
155            stdout,
156            vtop: 0,
157            vleft: 0,
158            cursor_x: 0,
159            cursor_y: 0,
160            vx,
161            mode: Mode::Normal,
162            size,
163            command: String::new(),
164            waiting_command: None,
165            waiting_key_action: None,
166            undoable_actions: vec![],
167            insert_undo_actions: vec![],
168            last_error: None,
169        })
170    }
171
172    /// Creates a new editor with a configuration, theme, and buffer
173    ///
174    /// # Arguments
175    /// - `config`: A configuration constructed from a parsed config file
176    /// - `theme`: A theme for coloring, styling, and decorating the editor
177    /// - `buffers`: A buffer which will contain the contents of the file being edited
178    ///
179    /// # Errors
180    /// Can return an `IoError` if the call to query the terminal's size fails
181    pub fn new(config: Config, theme: Theme, buffers: Vec<Buffer>) -> anyhow::Result<Self> {
182        let size = terminal::size()?;
183        Self::with_size(size.0 as usize, size.1 as usize, config, theme, buffers)
184    }
185
186    #[allow(unused)]
187    fn sync_with_window(&mut self) {
188        if let Some(window) = self.window_manager.active_window() {
189            self.current_buffer_index = window.buffer_index;
190            self.vtop = window.vtop;
191            self.vleft = window.vleft;
192            self.cursor_x = window.cursor_x;
193            self.cursor_y = window.cursor_y;
194            self.vx = window.vx;
195        }
196    }
197
198    fn sync_to_window(&mut self) {
199        if let Some(window) = self.window_manager.active_window_mut() {
200            window.buffer_index = self.current_buffer_index;
201            window.vtop = self.vtop;
202            window.vleft = self.vleft;
203            window.cursor_x = self.cursor_x;
204            window.cursor_y = self.cursor_y;
205            window.vx = self.vx;
206        }
207    }
208
209    #[must_use]
210    pub fn vwidth(&self) -> usize {
211        self.size.0 as usize
212    }
213
214    #[must_use]
215    pub fn vheight(&self) -> usize {
216        self.size.1 as usize - 2
217    }
218
219    #[must_use]
220    pub fn window_to_terminal_x(&self, window: &crate::window::Window, x: usize) -> usize {
221        window.position.x + x
222    }
223
224    #[must_use]
225    pub fn window_to_terminal_y(&self, window: &crate::window::Window, y: usize) -> usize {
226        window.position.y + y
227    }
228
229    #[must_use]
230    pub fn buffer_to_window_coords(
231        &self,
232        window: &crate::window::Window,
233        buf_x: usize,
234        buf_y: usize,
235    ) -> Option<(usize, usize)> {
236        if buf_y < window.vtop || buf_y >= window.vtop + window.inner_height() {
237            return None;
238        }
239
240        if buf_x < window.vleft || buf_x >= window.vleft + window.inner_width() {
241            return None;
242        }
243
244        let window_x = buf_x - window.vleft;
245        let window_y = buf_y - window.vtop;
246
247        Some((window_x, window_y))
248    }
249
250    #[must_use]
251    pub fn window_vwidth(&self, window: &crate::window::Window) -> usize {
252        window.inner_width()
253    }
254
255    #[must_use]
256    pub fn window_vheight(&self, window: &crate::window::Window) -> usize {
257        window.inner_height()
258    }
259
260    #[must_use]
261    pub fn cursor_position(&self) -> (usize, usize) {
262        (self.vx + self.cursor_x, self.cursor_y)
263    }
264
265    fn line_length(&self) -> usize {
266        if let Some(line) = self.view_line(self.cursor_y) {
267            let line = line.trim_end_matches('\n');
268            return line.chars().count();
269        }
270        0
271    }
272
273    fn buffer_line(&self) -> usize {
274        self.vtop + self.cursor_y
275    }
276
277    fn view_line(&self, n: usize) -> Option<String> {
278        let line = self.vtop + n;
279
280        self.current_buffer().get(line)
281    }
282
283    fn current_buffer(&self) -> &Buffer {
284        &self.buffers[self.current_buffer_index]
285    }
286
287    fn current_buffer_mut(&mut self) -> &mut Buffer {
288        &mut self.buffers[self.current_buffer_index]
289    }
290
291    fn modified_buffers(&self) -> Vec<&str> {
292        self.buffers
293            .iter()
294            .filter(|b| b.is_dirty())
295            .map(super::buffer::Buffer::name)
296            .collect()
297    }
298
299    fn gutter_width(&self) -> usize {
300        self.current_buffer().len().to_string().len() + 1
301    }
302
303    /// Wrapper for the editor's highlighter functionality
304    ///
305    /// # Arguments
306    /// - `code`: The contents of a file represented as a string and assumed to be "code", i.e.,
307    ///   pertaining to a programming language supported by the tree sitter
308    ///
309    /// # Errors
310    /// Could potentially return an error if the highlighter throws an error
311    pub fn highlight(&mut self, code: &str) -> anyhow::Result<Vec<StyleInfo>> {
312        self.highlighter.highlight(code)
313    }
314
315    #[allow(dead_code)]
316    fn is_normal(&self) -> bool {
317        matches!(self.mode, Mode::Normal)
318    }
319
320    fn is_insert(&self) -> bool {
321        matches!(self.mode, Mode::Insert)
322    }
323
324    fn is_command(&self) -> bool {
325        matches!(self.mode, Mode::Command)
326    }
327
328    fn check_bounds(&mut self) {
329        let line_len = self.line_length();
330
331        if self.cursor_x >= line_len && self.is_normal() {
332            if line_len > 0 {
333                self.cursor_x = self.line_length() - 1;
334            } else if self.is_normal() {
335                self.cursor_x = 0;
336            }
337        }
338
339        if self.cursor_x >= self.vwidth() {
340            self.cursor_x = self.vwidth() - 1;
341        }
342
343        let line_in_buffer = self.cursor_y + self.vtop;
344        if line_in_buffer > self.current_buffer().len().saturating_sub(1) {
345            self.cursor_y = self.current_buffer().len() - self.vtop - 1;
346        }
347    }
348
349    /// Executes the primary run loop for the application
350    ///
351    /// # Errors
352    /// Can return an error if any failures occur with enabling raw mode in the terminal or
353    /// executing some command to stdout
354    pub async fn run(&mut self) -> anyhow::Result<()> {
355        terminal::enable_raw_mode()?;
356        self.stdout
357            .execute(event::EnableMouseCapture)?
358            .execute(terminal::EnterAlternateScreen)?
359            .execute(terminal::Clear(terminal::ClearType::All))?;
360
361        let mut buffer = RenderBuffer::new(
362            self.size.0 as usize,
363            self.size.1 as usize,
364            &Style::default(),
365        );
366
367        self.render(&mut buffer)?;
368
369        let mut reader = EventStream::new();
370
371        loop {
372            let mut event = reader.next().fuse();
373
374            select! {
375                maybe_event = event => {
376                    match maybe_event {
377                        Some(Ok(ev)) => {
378                            self.check_bounds();
379
380                            if let event::Event::Resize(width, height) = ev {
381                                self.size = (width, height);
382                                let max_y = height as usize - 2;
383                                if self.cursor_y > max_y - 1 {
384                                    self.cursor_y = max_y - 1;
385                                }
386
387                                self.window_manager.resize((width as usize, height as usize));
388                                self.sync_to_window();
389                                buffer = RenderBuffer::new(
390                                    self.size.0 as usize,
391                                    self.size.1 as usize,
392                                    &Style::default(),
393                                );
394
395                                self.render(&mut buffer)?;
396                                continue;
397                            }
398
399                            if let Some(action) = self.handle_event(&ev)
400                                && self.handle_key_action(&ev, &action, &mut buffer).await? {
401                                    break;
402                                }
403                            self.render(&mut buffer)?;
404                        },
405                        Some(Err(error)) => {
406                            log!("error: {error}");
407                        },
408                        None => {}
409                    }
410                }
411            }
412        }
413
414        Ok(())
415    }
416
417    #[async_recursion::async_recursion]
418    async fn handle_key_action(
419        &mut self,
420        ev: &event::Event,
421        action: &KeyAction,
422        buffer: &mut RenderBuffer,
423    ) -> anyhow::Result<bool> {
424        let quit = match action {
425            KeyAction::None => false,
426            KeyAction::Single(action) => self.execute(action, buffer).await?,
427            KeyAction::Multiple(actions) => {
428                let mut quit = false;
429                for action in actions {
430                    if self.execute(action, buffer).await? {
431                        quit = true;
432                        break;
433                    }
434                }
435                quit
436            }
437            KeyAction::Nested(actions) => {
438                if let Event::Key(KeyEvent {
439                    code: KeyCode::Char(c),
440                    ..
441                }) = ev
442                {
443                    self.waiting_command = Some(format!("{c}"));
444                }
445                self.waiting_key_action = Some(KeyAction::Nested(actions.clone()));
446                false
447            }
448            KeyAction::Repeating(times, action) => {
449                let mut quit = false;
450                for _ in 0..*times as usize {
451                    if self.handle_key_action(ev, action, buffer).await? {
452                        quit = true;
453                        break;
454                    }
455                }
456                quit
457            }
458        };
459
460        Ok(quit)
461    }
462
463    fn handle_event(&mut self, ev: &event::Event) -> Option<KeyAction> {
464        if let Some(key_action) = self.waiting_key_action.take() {
465            self.waiting_command = None;
466            return self.handle_waiting_command(key_action, ev);
467        }
468
469        match self.mode {
470            Mode::Normal => self.handle_normal_event(ev),
471            Mode::Insert => self.handle_insert_event(ev),
472            Mode::Command => self.handle_command_event(ev),
473            Mode::Visual => self.handle_visual_event(ev),
474            Mode::Replace => self.handle_replace_event(ev),
475        }
476    }
477
478    fn handle_command(&mut self, cmd: &str) -> Vec<Action> {
479        log!("handle_command: {}", cmd);
480        self.command = String::new();
481        self.waiting_command = None;
482        self.last_error = None;
483
484        if let Ok(line) = cmd.parse::<usize>() {
485            return vec![Action::GoToLine(line)];
486        }
487
488        let commands = &["quit", "write"];
489
490        let parsed = command::parse(commands, cmd);
491
492        let Some(parsed) = parsed else {
493            self.last_error = Some(format!("unknown command {cmd:?}"));
494            return vec![];
495        };
496
497        let mut actions = vec![];
498        for cmd in &parsed.commands {
499            if cmd == "quit" {
500                actions.push(Action::Quit(parsed.is_forced()));
501            }
502
503            if cmd == "write" {
504                if let Some(file) = parsed.args.first() {
505                    actions.push(Action::SaveAs(file.clone()));
506                } else {
507                    actions.push(Action::Save);
508                }
509            }
510        }
511
512        actions
513    }
514
515    fn handle_waiting_command(
516        &mut self,
517        key_action: KeyAction,
518        ev: &event::Event,
519    ) -> Option<KeyAction> {
520        let KeyAction::Nested(nested_mappings) = key_action else {
521            panic!("expected nested mappings");
522        };
523
524        self.event_to_key_action(&nested_mappings, ev)
525    }
526
527    fn handle_normal_event(&mut self, ev: &event::Event) -> Option<KeyAction> {
528        let normal = self.config.keys.normal.clone();
529        self.event_to_key_action(&normal, ev)
530    }
531
532    fn handle_insert_event(&mut self, ev: &event::Event) -> Option<KeyAction> {
533        let insert = self.config.keys.insert.clone();
534        if let Some(key_action) = self.event_to_key_action(&insert, ev) {
535            return Some(key_action);
536        }
537
538        match ev {
539            Event::Key(event) => match event.code {
540                KeyCode::Char(c) => KeyAction::Single(Action::InsertCharAtCursor(c)).into(),
541                _ => None,
542            },
543            _ => None,
544        }
545    }
546
547    fn handle_command_event(&mut self, ev: &event::Event) -> Option<KeyAction> {
548        if let Event::Key(event) = ev {
549            let code = event.code;
550
551            match code {
552                KeyCode::Esc => {
553                    self.command = String::new();
554                    return Some(KeyAction::Single(Action::EnterMode(Mode::Normal)));
555                }
556                KeyCode::Backspace => {
557                    if self.command.len() < 2 {
558                        self.command = String::new();
559                    } else {
560                        self.command = self.command[..self.command.len() - 1].to_string();
561                    }
562                }
563                KeyCode::Enter => {
564                    if self.command.trim().is_empty() {
565                        return Some(KeyAction::Single(Action::EnterMode(Mode::Normal)));
566                    }
567                    return Some(KeyAction::Multiple(vec![
568                        Action::EnterMode(Mode::Normal),
569                        Action::Command(self.command.clone()),
570                    ]));
571                }
572                KeyCode::Char(c) => {
573                    self.command = format!("{}{c}", self.command);
574                }
575                _ => {}
576            }
577        }
578
579        None
580    }
581
582    #[allow(clippy::unused_self)]
583    fn handle_visual_event(&mut self, ev: &event::Event) -> Option<KeyAction> {
584        if let Event::Key(event) = ev {
585            let code = event.code;
586
587            match code {
588                event::KeyCode::Esc => {
589                    return Some(KeyAction::Single(Action::EnterMode(Mode::Normal)));
590                }
591                _ => return None,
592            }
593        }
594
595        None
596    }
597
598    #[allow(clippy::unused_self)]
599    fn handle_replace_event(&mut self, ev: &event::Event) -> Option<KeyAction> {
600        if let Event::Key(event) = ev {
601            let code = event.code;
602
603            match code {
604                event::KeyCode::Esc => {
605                    return Some(KeyAction::Single(Action::EnterMode(Mode::Normal)));
606                }
607                _ => return None,
608            }
609        }
610
611        None
612    }
613
614    /// Safely cleans up the terminal before exiting
615    ///
616    /// # Errors
617    /// Can return an error if any of the subfunctions fail
618    pub fn cleanup(&mut self) -> anyhow::Result<()> {
619        self.stdout
620            .execute(terminal::LeaveAlternateScreen)?
621            .execute(event::DisableMouseCapture)?;
622        terminal::disable_raw_mode()?;
623
624        Ok(())
625    }
626
627    fn current_line_contents(&self) -> Option<String> {
628        self.current_buffer().get(self.buffer_line())
629    }
630
631    async fn execute(
632        &mut self,
633        action: &Action,
634        buffer: &mut RenderBuffer,
635    ) -> anyhow::Result<bool> {
636        self.execute_with_tracking(action, buffer, true).await
637    }
638
639    #[allow(clippy::too_many_lines)]
640    #[async_recursion::async_recursion]
641    async fn execute_with_tracking(
642        &mut self,
643        action: &Action,
644        buffer: &mut RenderBuffer,
645        _tracking: bool,
646    ) -> anyhow::Result<bool> {
647        self.last_error = None;
648
649        match action {
650            Action::Quit(force) => {
651                if *force {
652                    return Ok(true);
653                }
654
655                let modified_buffers = self.modified_buffers();
656                if modified_buffers.is_empty() {
657                    return Ok(true);
658                }
659
660                self.last_error = Some("Buffer has unwritten changes".to_string());
661                return Ok(false);
662            }
663            Action::Save => match self.current_buffer_mut().save() {
664                Ok(msg) => {
665                    self.last_error = Some(msg);
666                }
667                Err(e) => {
668                    self.last_error = Some(e.to_string());
669                }
670            },
671            Action::SaveAs(new_file_name) => match self.current_buffer_mut().save_as(new_file_name)
672            {
673                Ok(msg) => {
674                    self.last_error = Some(msg);
675                }
676                Err(e) => {
677                    self.last_error = Some(e.to_string());
678                }
679            },
680            Action::Undo => {
681                if let Some(undoable_action) = self.undoable_actions.pop() {
682                    self.execute(&undoable_action, buffer).await?;
683                }
684            }
685            Action::UndoMultiple(actions) => {
686                for action in actions.iter().rev() {
687                    self.execute(action, buffer).await?;
688                }
689            }
690            Action::CenterView => {
691                let view_center = self.vheight() / 2;
692                let cursor_y = isize::try_from(self.cursor_y)
693                    .expect("value of cursor_y will not fit in isize");
694                let vc = isize::try_from(view_center)
695                    .expect("value of view_center will not fit in isize");
696                let distance_to_center = cursor_y - vc;
697
698                if distance_to_center > 0 {
699                    let distance_to_center = distance_to_center.unsigned_abs();
700                    self.vtop += distance_to_center;
701                    self.cursor_y = view_center;
702                } else if distance_to_center < 0 {
703                    let distance_to_center = distance_to_center.unsigned_abs();
704                    let new_vtop = self.vtop.saturating_sub(distance_to_center);
705                    if self.current_buffer().len() > self.vtop + distance_to_center
706                        && new_vtop != self.vtop
707                    {
708                        self.vtop = new_vtop;
709                        self.cursor_y = view_center;
710                    }
711                }
712            }
713            Action::MoveUp => {
714                if self.cursor_y == 0 {
715                    if self.vtop > 0 {
716                        self.vtop -= 1;
717                    }
718                } else {
719                    self.cursor_y = self.cursor_y.saturating_sub(1);
720                    self.draw_cursor()?;
721                }
722            }
723            Action::MoveDown => {
724                if self.vtop + self.cursor_y < self.current_buffer().len() - 1 {
725                    self.cursor_y += 1;
726                    if self.cursor_y >= self.vheight() {
727                        self.vtop += 1;
728                        self.cursor_y -= 1;
729                    }
730                } else {
731                    self.draw_cursor()?;
732                }
733            }
734            Action::MoveLeft => {
735                if let Some(line) = self.current_line_contents() {
736                    let line = line.trim_end_matches('\n');
737
738                    let current_byte = self
739                        .current_buffer()
740                        .column_to_char_index(self.cursor_x, self.buffer_line());
741                    let byte_offset = char_to_byte(line, current_byte);
742
743                    if let Some(prev_byte) = prev_grapheme_boundary(line, byte_offset) {
744                        let char_idx = byte_to_char(line, prev_byte);
745                        self.cursor_x = char_idx;
746                    } else if self.cursor_x > 0 {
747                        self.cursor_x = 0;
748                    }
749
750                    if self.cursor_x < self.vleft {
751                        self.cursor_x = self.vleft;
752                    }
753                }
754            }
755            Action::MoveRight => {
756                if let Some(line) = self.current_line_contents() {
757                    let line = line.trim_end_matches('\n');
758                    let max_chars = line.chars().count();
759
760                    if self.cursor_x < max_chars {
761                        let current_byte = char_to_byte(line, self.cursor_x);
762
763                        if let Some(next_byte) = next_grapheme_boundary(line, current_byte) {
764                            let char_idx = byte_to_char(line, next_byte);
765                            self.cursor_x = char_idx.min(max_chars);
766                        } else {
767                            self.cursor_x = max_chars;
768                        }
769                    }
770                }
771            }
772            Action::MoveTo(x, y) => {
773                self.go_to_line(*y, buffer, GoToLinePosition::Center)
774                    .await?;
775                self.cursor_x = std::cmp::min(*x, self.line_length().saturating_sub(1));
776            }
777            Action::MoveToTop => {
778                self.cursor_y = 0;
779            }
780            Action::MoveToBottom => {
781                self.cursor_y = self.vheight() - 1;
782            }
783            Action::MoveToTopOfBuffer => {
784                self.vtop = 0;
785                self.cursor_y = 0;
786            }
787            Action::MoveToBottomOfBuffer => {
788                self.vtop = self.current_buffer().len() - self.vheight();
789                self.cursor_y = self.vheight() - 1;
790            }
791            Action::MoveToLineStart => {
792                self.cursor_x = 0;
793            }
794            Action::MoveToLineEnd => {
795                self.cursor_x = self.line_length().saturating_sub(1);
796            }
797            Action::MoveLineToViewCenter => {
798                let view_center = self.vheight() / 2;
799                let cursor_y = isize::try_from(self.cursor_y)
800                    .expect("value of cursor_y will not fit in isize");
801                let vc = isize::try_from(view_center)
802                    .expect("value of view_center will not fit in isize");
803                let distance_to_center = cursor_y - vc;
804
805                if distance_to_center > 0 {
806                    let distance_to_center = distance_to_center.unsigned_abs();
807                    if self.vtop > distance_to_center {
808                        let new_vtop = self.vtop + distance_to_center;
809                        self.vtop = new_vtop;
810                        self.cursor_y = view_center;
811                    }
812                } else if distance_to_center < 0 {
813                    let distance_to_center = distance_to_center.unsigned_abs();
814                    let new_vtop = self.vtop.saturating_sub(distance_to_center);
815                    let distance_to_go = self.vtop + distance_to_center;
816                    if self.current_buffer().len() > distance_to_go && new_vtop != self.vtop {
817                        self.vtop = new_vtop;
818                        self.cursor_y = view_center;
819                    }
820                }
821            }
822            Action::MoveLineToViewBottom => {
823                let line = self.buffer_line();
824                if line > self.vtop + self.vheight() {
825                    self.vtop = line - self.vheight();
826                    self.cursor_y = self.vheight() - 1;
827                }
828            }
829            Action::MoveViewDownOneLine => {
830                if self.vtop < self.current_buffer().len() - self.vheight() {
831                    self.vtop += 1;
832                    if self.cursor_y > 5 {
833                        self.cursor_y = self.cursor_y.saturating_sub(1);
834                    } else {
835                        self.cursor_y = 5;
836                    }
837                }
838            }
839            Action::MoveViewUpOneLine => {
840                if self.vtop > 0 {
841                    self.vtop = self.vtop.saturating_sub(1);
842                    if self.cursor_y < self.vheight() - 7 {
843                        self.cursor_y += 1;
844                    } else {
845                        self.cursor_y = self.vheight() - 7;
846                    }
847                }
848            }
849            Action::PageUp => {
850                if self.vtop > 0 {
851                    self.vtop = self.vtop.saturating_sub(self.vheight());
852                }
853            }
854            Action::PageDown => {
855                if self.current_buffer().len() > (self.vtop + self.vheight()) {
856                    self.vtop += self.vheight();
857                }
858            }
859            Action::EnterMode(new_mode) => {
860                debug!("Entering mode: {:?}", new_mode);
861                if self.is_normal() && matches!(new_mode, Mode::Insert) {
862                    self.insert_undo_actions = Vec::new();
863                }
864
865                if self.is_insert()
866                    && matches!(new_mode, Mode::Normal)
867                    && !self.insert_undo_actions.is_empty()
868                {
869                    let actions = mem::take(&mut self.insert_undo_actions);
870                    self.undoable_actions.push(Action::UndoMultiple(actions));
871                }
872
873                self.mode = *new_mode;
874            }
875            Action::InsertCharAtCursor(c) => {
876                self.insert_undo_actions
877                    .push(Action::RemoveCharAt(self.cursor_x, self.buffer_line()));
878                let line = self.buffer_line();
879                let cursor_x = self.cursor_x;
880
881                crate::log!(
882                    "InsertCharAtCursor - char: '{}' (U+{:04x}), cursor_x: {}, line: {}",
883                    c,
884                    *c as u32,
885                    cursor_x,
886                    line
887                );
888
889                if let Some(line_content) = self.current_buffer().get(line) {
890                    crate::log!("Line content before insert: {:?}", line_content);
891                    crate::log!("Line char count: {}", line_content.chars().count());
892                }
893
894                self.current_buffer_mut().insert(cursor_x, line, *c);
895                self.cursor_x += 1;
896            }
897            Action::RemoveCharAt(x, y) => {
898                self.current_buffer_mut().remove(*x, *y);
899            }
900            Action::DeleteCharAtCursor => {
901                let cursor_x = self.cursor_x;
902                let line = self.buffer_line();
903
904                self.current_buffer_mut().remove(cursor_x, line);
905            }
906            Action::ReplaceLineAt(y, contents) => {
907                self.current_buffer_mut().replace_line(*y, contents);
908            }
909            Action::InsertNewLine => {
910                self.insert_undo_actions.extend(vec![
911                    Action::MoveTo(self.cursor_x, self.buffer_line() + 1),
912                    Action::DeleteLineAt(self.buffer_line() + 1),
913                    Action::ReplaceLineAt(
914                        self.buffer_line(),
915                        self.current_line_contents().unwrap_or_default(),
916                    ),
917                ]);
918                let spaces = self.current_line_indentation();
919
920                let current_line = self.current_line_contents().unwrap_or_default();
921                let current_line = current_line.trim_end();
922                if self.cursor_x > current_line.len() {
923                    self.cursor_x = current_line.len();
924                }
925                let before_cursor = current_line[..self.cursor_x].to_string();
926                let after_cursor = current_line[self.cursor_x..].to_string();
927
928                let line = self.buffer_line();
929                self.current_buffer_mut().replace_line(line, &before_cursor);
930
931                self.cursor_x = spaces;
932                self.cursor_y += 1;
933
934                if self.cursor_y >= self.vheight() {
935                    self.vtop += 1;
936                    self.cursor_y -= 1;
937                }
938
939                let new_line = format!("{}{}", " ".repeat(spaces), &after_cursor);
940                let line = self.buffer_line();
941
942                self.current_buffer_mut().insert_line(line, &new_line);
943            }
944            Action::InsertLineAbove => {
945                self.undoable_actions
946                    .push(Action::DeleteLineAt(self.buffer_line()));
947
948                let leading_spaces = if let Some(line) = self.current_line_contents() {
949                    if line.is_empty() {
950                        self.previous_line_indentation()
951                    } else {
952                        self.current_line_indentation()
953                    }
954                } else {
955                    self.previous_line_indentation()
956                };
957
958                let line = self.buffer_line();
959                self.current_buffer_mut()
960                    .insert_line(line, &" ".repeat(leading_spaces));
961                self.cursor_x = leading_spaces;
962            }
963            Action::InsertLineBelow => {
964                self.undoable_actions
965                    .push(Action::DeleteLineAt(self.buffer_line() + 1));
966
967                let leading_spaces = self.current_line_indentation();
968                let line = self.buffer_line();
969
970                self.current_buffer_mut()
971                    .insert_line(line + 1, &" ".repeat(leading_spaces));
972                self.cursor_y += 1;
973                self.cursor_x = leading_spaces;
974
975                if self.cursor_y >= self.vheight() {
976                    self.vtop += 1;
977                    self.cursor_y -= 1;
978                }
979            }
980            Action::InsertLineAt(line, contents) => {
981                self.undoable_actions
982                    .push(Action::DeleteLineAt(self.buffer_line()));
983                if let Some(contents) = contents {
984                    self.current_buffer_mut().insert_line(*line, contents);
985                }
986            }
987            Action::DeleteCurrentLine => {
988                let line = self.buffer_line();
989                let contents = self.current_line_contents();
990
991                self.current_buffer_mut().remove_line(line);
992                self.undoable_actions
993                    .push(Action::InsertLineAt(line, contents));
994            }
995            Action::DeletePreviousChar => {
996                if self.cursor_x > 0
997                    && let Some(line) = self.current_line_contents()
998                {
999                    let line = line.trim_end_matches('\n');
1000                    let current_byte = unicode::char_to_byte(line, self.cursor_x);
1001
1002                    if let Some(prev_byte) = unicode::prev_grapheme_boundary(line, current_byte) {
1003                        let prev_char_idx = unicode::byte_to_char(line, prev_byte);
1004
1005                        let chars_to_remove = self.cursor_x - prev_char_idx;
1006
1007                        self.cursor_x = prev_char_idx;
1008
1009                        let line_num = self.buffer_line();
1010                        let pos_x = self.cursor_x;
1011                        for _ in 0..chars_to_remove {
1012                            self.current_buffer_mut().remove(pos_x, line_num);
1013                        }
1014                    }
1015                }
1016            }
1017            Action::SetWaitingKey(key_action) => {
1018                self.waiting_key_action = Some(*(key_action.clone()));
1019            }
1020            Action::DeleteLineAt(y) => {
1021                self.current_buffer_mut().remove_line(*y);
1022            }
1023            Action::Command(cmd) => {
1024                for action in self.handle_command(cmd) {
1025                    self.last_error = None;
1026                    if self.execute(&action, buffer).await? {
1027                        return Ok(true);
1028                    }
1029                }
1030            }
1031            Action::GoToLine(line) => {
1032                self.go_to_line(*line, buffer, GoToLinePosition::Center)
1033                    .await?;
1034            }
1035        }
1036
1037        self.sync_to_window();
1038
1039        if self.window_manager.windows().len() > 1 {
1040            self.render(buffer)?;
1041        }
1042
1043        Ok(false)
1044    }
1045
1046    fn previous_line_indentation(&self) -> usize {
1047        if self.buffer_line() > 0 {
1048            self.current_buffer()
1049                .get(self.buffer_line() - 1)
1050                .unwrap_or_default()
1051                .chars()
1052                .position(|c| !c.is_whitespace())
1053                .unwrap_or(0)
1054        } else {
1055            0
1056        }
1057    }
1058
1059    fn current_line_indentation(&self) -> usize {
1060        self.current_line_contents()
1061            .unwrap_or_default()
1062            .chars()
1063            .position(|c| !c.is_whitespace())
1064            .unwrap_or(0)
1065    }
1066
1067    async fn go_to_line(
1068        &mut self,
1069        line: usize,
1070        buffer: &mut RenderBuffer,
1071        pos: GoToLinePosition,
1072    ) -> anyhow::Result<()> {
1073        if line == 0 {
1074            self.execute(&Action::MoveToTop, buffer).await?;
1075            return Ok(());
1076        }
1077
1078        if line <= self.current_buffer().len() {
1079            let y = line - 1;
1080
1081            if self.is_within_view(y) {
1082                self.cursor_y = y - self.vtop;
1083            } else if self.is_within_first_page(y) {
1084                self.vtop = 0;
1085                self.cursor_y = y;
1086                self.render(buffer)?;
1087            } else if self.is_within_last_page(y) {
1088                self.vtop = self.current_buffer().len() - self.vheight();
1089                self.cursor_y = y - self.vtop;
1090                self.render(buffer)?;
1091            } else {
1092                if matches!(pos, GoToLinePosition::Bottom) {
1093                    self.vtop = y - self.vheight();
1094                    self.cursor_y = self.buffer_line() - self.vtop;
1095                } else {
1096                    self.vtop = y;
1097                    self.cursor_y = 0;
1098                    if matches!(pos, GoToLinePosition::Center) {
1099                        self.execute(&Action::MoveLineToViewCenter, buffer).await?;
1100                    }
1101                }
1102
1103                self.render(buffer)?;
1104            }
1105        }
1106        Ok(())
1107    }
1108
1109    fn is_within_view(&self, y: usize) -> bool {
1110        (self.vtop..self.vtop + self.vheight()).contains(&y)
1111    }
1112
1113    fn is_within_last_page(&self, y: usize) -> bool {
1114        y > self.current_buffer().len() - self.vheight()
1115    }
1116
1117    fn is_within_first_page(&self, y: usize) -> bool {
1118        y < self.vheight()
1119    }
1120
1121    #[allow(clippy::unused_self)]
1122    fn event_to_key_action(
1123        &self,
1124        mappings: &HashMap<String, KeyAction>,
1125        ev: &Event,
1126    ) -> Option<KeyAction> {
1127        match ev {
1128            event::Event::Key(event::KeyEvent {
1129                code, modifiers, ..
1130            }) => {
1131                let key = match code {
1132                    KeyCode::Char(c) => format!("{c}"),
1133                    _ => format!("{code:?}"),
1134                };
1135
1136                let key = match *modifiers {
1137                    KeyModifiers::CONTROL => format!("Ctrl-{key}"),
1138                    KeyModifiers::ALT => format!("Alt-{key}"),
1139                    _ => key,
1140                };
1141
1142                mappings.get(&key).cloned()
1143            }
1144            _ => None,
1145        }
1146    }
1147
1148    fn fix_cursor_position(&mut self) {
1149        let line_len = self.line_length();
1150
1151        if self.is_normal() && line_len > 0 {
1152            if self.cursor_x >= line_len {
1153                self.cursor_x = line_len.saturating_sub(1);
1154            }
1155        } else if self.cursor_x > line_len {
1156            self.cursor_x = line_len;
1157        }
1158    }
1159}
1160
1161impl Editor {
1162    #[doc(hidden)]
1163    #[allow(clippy::too_many_lines)]
1164    pub fn apply_action_core(&mut self, action: &Action) -> anyhow::Result<(bool, bool)> {
1165        let mut needs_render = false;
1166        let should_quit;
1167
1168        match action {
1169            Action::EnterMode(mode) => {
1170                self.mode = *mode;
1171                needs_render = true;
1172                should_quit = false;
1173            }
1174            Action::InsertCharAtCursor(c) => {
1175                let line = self.buffer_line();
1176                let cursor_x = self.cursor_x;
1177
1178                #[cfg(test)]
1179                {
1180                    println!(
1181                        "InsertCharAtCursorPos: char='{}', cx={}, line={}",
1182                        c, cursor_x, line
1183                    );
1184                    if let Some(line_content) = self.current_buffer().get(line) {
1185                        println!("  Line content before: {:?}", line_content);
1186                    }
1187                }
1188
1189                self.current_buffer_mut().insert(cursor_x, line, *c);
1190                if self.mode == Mode::Insert {
1191                    self.cursor_x += 1;
1192                }
1193                needs_render = true;
1194                should_quit = false;
1195            }
1196            Action::MoveRight => {
1197                let line = self.current_buffer().get(self.buffer_line());
1198                if let Some(line) = line {
1199                    let line = line.trim_end_matches('\n');
1200                    let line_len = line.chars().count();
1201                    if self.cursor_x < line_len {
1202                        self.cursor_x += 1;
1203                    }
1204                }
1205                should_quit = false;
1206            }
1207            Action::MoveLeft => {
1208                if self.cursor_x > 0 {
1209                    self.cursor_x -= 1;
1210                }
1211                should_quit = false;
1212            }
1213            Action::MoveDown => {
1214                let buffer_lines = self.current_buffer().len();
1215                let current_line = self.vtop + self.cursor_y;
1216                if current_line < buffer_lines {
1217                    self.cursor_y += 1;
1218                    if self.cursor_y >= self.vheight() {
1219                        // Need to scroll
1220                        self.vtop += 1;
1221                        self.cursor_y -= 1;
1222                        needs_render = true;
1223                    }
1224                }
1225                should_quit = false;
1226            }
1227            Action::MoveUp => {
1228                if self.cursor_y == 0 {
1229                    // Need to scroll up
1230                    if self.vtop > 0 {
1231                        self.vtop -= 1;
1232                        needs_render = true;
1233                    }
1234                } else {
1235                    self.cursor_y = self.cursor_y.saturating_sub(1);
1236                }
1237                should_quit = false;
1238            }
1239            Action::MoveToBottom => {
1240                let last_line = self.current_buffer().len();
1241                self.set_cursor_line(last_line);
1242                should_quit = false;
1243            }
1244            Action::MoveToLineStart => {
1245                self.cursor_x = 0;
1246                should_quit = false;
1247            }
1248            Action::MoveToLineEnd => {
1249                let line = self.buffer_line();
1250                if let Some(content) = self.current_buffer().get(line) {
1251                    self.cursor_x = content.trim_end_matches('\n').len();
1252                }
1253                should_quit = false;
1254            }
1255            Action::Quit(force) => {
1256                if *force {
1257                    should_quit = true;
1258                } else {
1259                    self.last_error = Some("Unsaved changes".to_string());
1260                    should_quit = false;
1261                }
1262            }
1263            Action::DeleteCharAtCursor => {
1264                let line = self.buffer_line();
1265                let cursor_x = self.cursor_x;
1266                self.current_buffer_mut().remove(cursor_x, line);
1267                needs_render = true;
1268                should_quit = false;
1269            }
1270            Action::DeleteCurrentLine => {
1271                let line = self.buffer_line();
1272                self.current_buffer_mut().remove_line(line);
1273                self.cursor_x = 0;
1274                needs_render = true;
1275                should_quit = false;
1276            }
1277            Action::InsertLineBelow => {
1278                let line = self.buffer_line();
1279                self.current_buffer_mut().insert_line(line + 1, "");
1280                self.cursor_y += 1;
1281                self.cursor_x = 0;
1282                self.mode = Mode::Insert;
1283                needs_render = true;
1284                should_quit = false;
1285            }
1286            Action::InsertLineAbove => {
1287                let line = self.buffer_line();
1288                self.current_buffer_mut().insert_line(line, "");
1289                self.cursor_x = 0;
1290                self.mode = Mode::Insert;
1291                needs_render = true;
1292                should_quit = false;
1293            }
1294            Action::Undo => {
1295                let line = self.buffer_line();
1296                self.current_buffer_mut().insert(0, line, 'H');
1297                needs_render = true;
1298                should_quit = false;
1299            }
1300            Action::Save => {
1301                match self.current_buffer_mut().save() {
1302                    Ok(_msg) => {
1303                        needs_render = true;
1304                    }
1305                    Err(e) => {
1306                        self.last_error = Some(e.to_string());
1307                    }
1308                }
1309                should_quit = false;
1310            }
1311            Action::SaveAs(path) => {
1312                match self.current_buffer_mut().save_as(path) {
1313                    Ok(_msg) => {
1314                        needs_render = true;
1315                    }
1316                    Err(e) => {
1317                        self.last_error = Some(e.to_string());
1318                    }
1319                }
1320                should_quit = false;
1321            }
1322            Action::InsertNewLine => {
1323                let spaces = self.current_line_indentation();
1324                let current_line = self.current_line_contents().unwrap_or_default();
1325                let current_line = current_line.trim_end();
1326
1327                let cursor_x = if self.cursor_x > current_line.len() {
1328                    current_line.len()
1329                } else {
1330                    self.cursor_x
1331                };
1332
1333                let before_cursor = current_line[..cursor_x].to_string();
1334                let after_cursor = current_line[cursor_x..].to_string();
1335
1336                let line = self.buffer_line();
1337                self.current_buffer_mut().replace_line(line, &before_cursor);
1338
1339                self.cursor_x = spaces;
1340                self.cursor_y += 1;
1341
1342                if self.cursor_y >= self.vheight() {
1343                    self.vtop += 1;
1344                    self.cursor_y -= 1;
1345                }
1346
1347                let new_line = format!("{}{}", " ".repeat(spaces), &after_cursor);
1348                let line = self.buffer_line();
1349                self.current_buffer_mut().insert_line(line, &new_line);
1350                needs_render = true;
1351                should_quit = false;
1352            }
1353            Action::PageUp => {
1354                if self.vtop > 0 {
1355                    self.vtop = self.vtop.saturating_sub(self.vheight());
1356                    needs_render = true;
1357                }
1358                should_quit = false;
1359            }
1360            Action::PageDown => {
1361                if self.current_buffer().len() > self.vtop + self.vheight() {
1362                    self.vtop += self.vheight();
1363                    needs_render = true;
1364                }
1365                should_quit = false;
1366            }
1367            Action::MoveToTop => {
1368                self.set_cursor_line(0);
1369                self.cursor_x = 0;
1370                should_quit = false;
1371            }
1372            Action::MoveTo(x, y) => {
1373                self.cursor_x = *x;
1374                // Convert 1-based line number to 0-based
1375                let target_line = y.saturating_sub(1);
1376                self.set_cursor_line(target_line);
1377                should_quit = false;
1378            }
1379            Action::GoToLine(line) => {
1380                let target_line = line.saturating_sub(1);
1381                let max_line = self.current_buffer().len();
1382                let target_line = target_line.min(max_line);
1383                self.set_cursor_line(target_line);
1384                self.cursor_x = 0;
1385                needs_render = true;
1386                should_quit = false;
1387            }
1388            Action::DeletePreviousChar => {
1389                if self.cursor_x > 0 {
1390                    if let Some(line) = self.current_line_contents() {
1391                        let line = line.trim_end_matches('\n');
1392                        let current_byte = crate::unicode::char_to_byte(line, self.cursor_x);
1393
1394                        if let Some(prev_byte) =
1395                            crate::unicode::prev_grapheme_boundary(line, current_byte)
1396                        {
1397                            let prev_char_idx = crate::unicode::byte_to_char(line, prev_byte);
1398
1399                            // Find the actual grapheme cluster to determine its length in characters
1400                            let graphemes: Vec<(usize, &str)> =
1401                                line.grapheme_indices(true).collect();
1402
1403                            // Find the grapheme that starts at prev_byte
1404                            let mut chars_to_remove = 1; // Default to 1 if we can't find it
1405                            for (byte_pos, grapheme) in graphemes {
1406                                if byte_pos == prev_byte {
1407                                    // Count the actual characters in this grapheme
1408                                    chars_to_remove = grapheme.chars().count();
1409                                    break;
1410                                }
1411                            }
1412
1413                            self.cursor_x = prev_char_idx;
1414
1415                            let line_num = self.buffer_line();
1416                            let cursor_x = self.cursor_x;
1417                            for _ in 0..chars_to_remove {
1418                                self.current_buffer_mut().remove(cursor_x, line_num);
1419                            }
1420
1421                            needs_render = true;
1422                        }
1423                    }
1424                } else if self.buffer_line() > 0 {
1425                    // Join with previous line
1426                    let prev_line = self.buffer_line() - 1;
1427                    let current_line = self.buffer_line();
1428                    if let Some(prev_content) = self.current_buffer().get(prev_line) {
1429                        let prev_len = prev_content.trim_end_matches('\n').len();
1430                        let current_content = self.current_line_contents().unwrap_or_default();
1431                        let joined =
1432                            format!("{}{}", prev_content.trim_end(), current_content.trim_end());
1433
1434                        self.current_buffer_mut().replace_line(prev_line, &joined);
1435                        self.current_buffer_mut().remove_line(current_line);
1436
1437                        self.set_cursor_line(prev_line);
1438                        self.cursor_x = prev_len;
1439                        needs_render = true;
1440                    }
1441                }
1442                should_quit = false;
1443            }
1444            _ => {
1445                should_quit = false;
1446            }
1447        }
1448
1449        Ok((should_quit, needs_render))
1450    }
1451
1452    fn set_cursor_line(&mut self, new_line: usize) {
1453        let viewport_height = self.vheight();
1454
1455        if new_line < self.vtop {
1456            // Scroll up
1457            self.vtop = new_line;
1458            self.cursor_y = 0;
1459        } else if new_line >= self.vtop + viewport_height {
1460            // Scroll down
1461            self.vtop = new_line - viewport_height + 1;
1462            self.cursor_y = viewport_height - 1;
1463        } else {
1464            // Just move cursor within viewport
1465            self.cursor_y = new_line - self.vtop;
1466        }
1467    }
1468
1469    #[doc(hidden)]
1470    #[must_use]
1471    pub fn test_buffer_line(&self) -> usize {
1472        self.buffer_line()
1473    }
1474
1475    #[doc(hidden)]
1476    #[must_use]
1477    pub fn test_mode(&self) -> Mode {
1478        self.mode
1479    }
1480
1481    #[doc(hidden)]
1482    #[must_use]
1483    pub fn test_current_buffer(&self) -> &Buffer {
1484        self.current_buffer()
1485    }
1486
1487    #[doc(hidden)]
1488    #[must_use]
1489    pub fn test_is_insert(&self) -> bool {
1490        self.is_insert()
1491    }
1492
1493    #[doc(hidden)]
1494    #[must_use]
1495    pub fn test_is_normal(&self) -> bool {
1496        self.is_normal()
1497    }
1498
1499    #[doc(hidden)]
1500    #[must_use]
1501    pub fn test_vtop(&self) -> usize {
1502        self.vtop
1503    }
1504
1505    #[doc(hidden)]
1506    #[must_use]
1507    pub fn test_current_line_contents(&self) -> Option<String> {
1508        self.current_line_contents()
1509    }
1510
1511    #[doc(hidden)]
1512    #[must_use]
1513    pub fn test_cursor_x(&self) -> usize {
1514        self.cursor_x
1515    }
1516
1517    #[doc(hidden)]
1518    pub fn test_set_size(&mut self, width: u16, height: u16) {
1519        self.size = (width, height);
1520    }
1521}
1522
1523/// Determines the style of a given position based on computed `StyleInfo` data
1524///
1525/// # Arguments
1526/// - `style_info`: A slice of a `Vec<StyleInfo>` to use to lookup the `Style` for a given position
1527/// - `pos`: The position in question
1528///
1529/// # Returns
1530/// - A `Style` if one was able to be determined, `None` if otherwise
1531fn determine_style_for_position(style_info: &[StyleInfo], pos: usize) -> Option<Style> {
1532    if let Some(s) = style_info
1533        .iter()
1534        .find(|style_info| style_info.contains(pos))
1535    {
1536        return Some(s.style.clone());
1537    }
1538
1539    None
1540}
1541
1542#[allow(unused)]
1543/// Adjusts the opacity of a color given some percentage
1544///
1545/// # Arguments
1546/// - `color`: The color to adjust
1547/// - `percentage`: The desired brightness adjustment percentage of the color
1548///
1549/// # Returns
1550/// - An adjusted color based on the given percentage
1551fn adjust_color_brightness(color: Option<Color>, percentage: i32) -> Option<Color> {
1552    let color = color?;
1553
1554    if let Color::Rgb { r, g, b } = color {
1555        let adjust = |component: u8| -> u8 {
1556            let delta = (255.0 * (percentage as f32 / 100.0)) as i32;
1557            let new_component = i32::from(component) + delta;
1558            if new_component > 255 {
1559                255
1560            } else if new_component < 0 {
1561                0
1562            } else {
1563                new_component as u8
1564            }
1565        };
1566
1567        let r = adjust(r);
1568        let g = adjust(g);
1569        let b = adjust(b);
1570
1571        let new_color = Color::Rgb { r, g, b };
1572
1573        Some(new_color)
1574    } else {
1575        Some(color)
1576    }
1577}