use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; use anyhow::{Context, Result, bail}; use edtui::{ EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, }; use log::{error, trace}; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::{Alignment, Rect}; use ratatui::prelude::{Color, Style}; use ratatui::widgets::{Block, Borders, Padding, Widget}; use syntect::parsing::SyntaxSet; pub struct Editor { pub state: EditorState, pub event_handler: EditorEventHandler, pub file_path: Option, syntax_set: SyntaxSet, pub(crate) component_state: ComponentState, } impl Editor { pub fn id() -> &'static str { "Editor" } pub fn new(path: &std::path::PathBuf) -> Self { trace!(target:Self::id(), "Building {}", Self::id()); Editor { state: EditorState::default(), event_handler: EditorEventHandler::default(), file_path: Some(path.to_owned()), syntax_set: SyntaxSet::load_defaults_nonewlines(), component_state: ComponentState::default().with_help_text(concat!( "CTRL+S: Save file | ALT+(←/h): Previous tab | ALT+(l/→): Next tab |", " All other input is handled by vim" )), } } pub fn reload_contents(&mut self) -> Result<()> { trace!(target:Self::id(), "Reloading editor file contents {:?}", self.file_path); match self.file_path.clone() { None => { error!(target:Self::id(), "Failed to reload editor contents with None file_path"); bail!("Failed to reload editor contents with None file_path") } Some(path) => self.set_contents(&path), } } pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> { trace!(target:Self::id(), "Setting Editor contents from path {:?}", path); if let Ok(contents) = std::fs::read_to_string(path) { let lines: Vec<_> = contents .lines() .map(|line| line.chars().collect::>()) .collect(); self.file_path = Some(path.clone()); self.state.lines = Lines::new(lines); self.state.cursor.row = 0; self.state.cursor.col = 0; } Ok(()) } pub fn save(&self) -> Result<()> { if let Some(path) = &self.file_path { trace!(target:Self::id(), "Saving Editor contents {:?}", path); return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into()); }; error!(target:Self::id(), "Failed saving Editor contents; file_path was None"); bail!("File not saved. No file path set.") } } impl Widget for &mut Editor { fn render(self, area: Rect, buf: &mut Buffer) { let lang = self .file_path .as_ref() .and_then(|p| p.extension()) .map(|e| e.to_str().unwrap_or("md")) .unwrap_or("md"); let lang_name = self .syntax_set .find_syntax_by_extension(lang) .map(|s| s.name.to_string()) .unwrap_or("Unknown".to_string()); EditorView::new(&mut self.state) .wrap(true) .theme( EditorTheme::default().block( Block::default() .title(lang_name.to_owned()) .title_style(Style::default().fg(Color::Yellow)) .title_alignment(Alignment::Right) .borders(Borders::ALL) .padding(Padding::new(0, 0, 0, 1)) .style(Style::default().fg(self.component_state.get_active_color())), ), ) .syntax_highlighter(SyntaxHighlighter::new("dracula", lang).ok()) .tab_width(2) .line_numbers(LineNumbers::Absolute) .render(area, buf); } } impl Component for Editor { fn handle_event(&mut self, event: Event) -> Result { if let Some(key_event) = event.as_key_event() { // Handle events here that should not be passed on to the vim emulation handler. match self.handle_key_events(key_event)? { Action::Handled => return Ok(Action::Handled), _ => {} } } self.event_handler.on_event(event, &mut self.state); Ok(Action::Pass) } /// The events for the vim emulation should be handled by EditorEventHandler::on_event. /// These events are custom to the clide application. fn handle_key_events(&mut self, key: KeyEvent) -> Result { match key { KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, .. } => { self.save().context("Failed to save file.")?; Ok(Action::Handled) } // For other events not handled here, pass to the vim emulation handler. _ => Ok(Action::Noop), } } fn is_active(&self) -> bool { self.component_state.focus == Focus::Active } }