use alloc::string::String; use core::cmp; use unicode_segmentation::UnicodeSegmentation; use crate::{ Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, FontSystem, SyntaxEditor, }; #[derive(Clone, Debug, Eq, PartialEq)] pub enum ViMode { /// Passthrough mode, disables vi features Passthrough, /// Normal mode Normal, /// Insert mode Insert, /// Command mode Command { value: String }, /// Search mode Search { value: String, forwards: bool }, } #[derive(Debug)] pub struct ViEditor<'a> { editor: SyntaxEditor<'a>, mode: ViMode, search_opt: Option<(String, bool)>, } impl<'a> ViEditor<'a> { pub fn new(editor: SyntaxEditor<'a>) -> Self { Self { editor, mode: ViMode::Normal, search_opt: None, } } /// Load text from a file, and also set syntax to the best option #[cfg(feature = "std")] pub fn load_text>( &mut self, font_system: &mut FontSystem, path: P, attrs: crate::Attrs, ) -> std::io::Result<()> { self.editor.load_text(font_system, path, attrs) } /// Get the default background color pub fn background_color(&self) -> Color { self.editor.background_color() } /// Get the default foreground (text) color pub fn foreground_color(&self) -> Color { self.editor.foreground_color() } /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { if passthrough { self.mode = ViMode::Passthrough; } else { self.mode = ViMode::Normal; } } /// Get current vi editing mode pub fn mode(&self) -> &ViMode { &self.mode } fn search(&mut self, inverted: bool) { let (search, mut forwards) = match &self.search_opt { Some(some) => some, None => return, }; if inverted { forwards = !forwards; } let mut cursor = self.cursor(); let start_line = cursor.line; if forwards { while cursor.line < self.buffer().lines.len() { if let Some(index) = self.buffer().lines[cursor.line] .text() .match_indices(search.as_str()) .filter_map(|(i, _)| { if cursor.line != start_line || i > cursor.index { Some(i) } else { None } }) .next() { cursor.index = index; self.set_cursor(cursor); return; } cursor.line += 1; } } else { cursor.line += 1; while cursor.line > 0 { cursor.line -= 1; if let Some(index) = self.buffer().lines[cursor.line] .text() .rmatch_indices(search.as_str()) .filter_map(|(i, _)| { if cursor.line != start_line || i < cursor.index { Some(i) } else { None } }) .next() { cursor.index = index; self.set_cursor(cursor); return; } } } } } impl<'a> Edit for ViEditor<'a> { fn buffer(&self) -> &Buffer { self.editor.buffer() } fn buffer_mut(&mut self) -> &mut Buffer { self.editor.buffer_mut() } fn cursor(&self) -> Cursor { self.editor.cursor() } fn set_cursor(&mut self, cursor: Cursor) { self.editor.set_cursor(cursor); } fn select_opt(&self) -> Option { self.editor.select_opt() } fn set_select_opt(&mut self, select_opt: Option) { self.editor.set_select_opt(select_opt); } fn shape_as_needed(&mut self, font_system: &mut FontSystem) { self.editor.shape_as_needed(font_system); } fn copy_selection(&self) -> Option { self.editor.copy_selection() } fn delete_selection(&mut self) -> bool { self.editor.delete_selection() } fn insert_string(&mut self, data: &str, attrs_list: Option) { self.editor.insert_string(data, attrs_list); } fn action(&mut self, font_system: &mut FontSystem, action: Action) { let old_mode = self.mode.clone(); match self.mode { ViMode::Passthrough => self.editor.action(font_system, action), ViMode::Normal => match action { Action::Insert(c) => match c { // Enter insert mode after cursor 'a' => { self.editor.action(font_system, Action::Right); self.mode = ViMode::Insert; } // Enter insert mode at end of line 'A' => { self.editor.action(font_system, Action::End); self.mode = ViMode::Insert; } // Previous word 'b' => { //TODO: WORD vs word, iterate by vi word rules self.editor.action(font_system, Action::PreviousWord); } // Previous WORD 'B' => { //TODO: WORD vs word, iterate by vi word rules self.editor.action(font_system, Action::PreviousWord); } // Change mode 'c' => { if self.editor.select_opt().is_some() { self.editor.action(font_system, Action::Delete); self.mode = ViMode::Insert; } else { //TODO: change to next cursor movement } } // Delete mode 'd' => { if self.editor.select_opt().is_some() { self.editor.action(font_system, Action::Delete); } else { //TODO: delete to next cursor movement } } // End of word 'e' => { //TODO: WORD vs word, iterate by vi word rules self.editor.action(font_system, Action::NextWord); } // End of WORD 'E' => { //TODO: WORD vs word, iterate by vi word rules self.editor.action(font_system, Action::NextWord); } // Enter insert mode at cursor 'i' => { self.mode = ViMode::Insert; } // Enter insert mode at start of line 'I' => { self.editor.action(font_system, Action::SoftHome); self.mode = ViMode::Insert; } // Create line after and enter insert mode 'o' => { self.editor.action(font_system, Action::End); self.editor.action(font_system, Action::Enter); self.mode = ViMode::Insert; } // Create line before and enter insert mode 'O' => { self.editor.action(font_system, Action::Home); self.editor.action(font_system, Action::Enter); self.editor.shape_as_needed(font_system); // TODO: do not require this? self.editor.action(font_system, Action::Up); self.mode = ViMode::Insert; } // Left 'h' => self.editor.action(font_system, Action::Left), // Top of screen //TODO: 'H' => self.editor.action(Action::ScreenHigh), // Down 'j' => self.editor.action(font_system, Action::Down), // Up 'k' => self.editor.action(font_system, Action::Up), // Right 'l' | ' ' => self.editor.action(font_system, Action::Right), // Bottom of screen //TODO: 'L' => self.editor.action(Action::ScreenLow), // Middle of screen //TODO: 'M' => self.editor.action(Action::ScreenMiddle), // Next search item 'n' => self.search(false), // Previous search item 'N' => self.search(true), // Enter visual mode 'v' => { if self.editor.select_opt().is_some() { self.editor.set_select_opt(None); } else { self.editor.set_select_opt(Some(self.editor.cursor())); } } // Enter line visual mode 'V' => { if self.editor.select_opt().is_some() { self.editor.set_select_opt(None); } else { self.editor.action(font_system, Action::Home); self.editor.set_select_opt(Some(self.editor.cursor())); //TODO: set cursor_x_opt to max self.editor.action(font_system, Action::End); } } // Next word 'w' => { //TODO: WORD vs word, iterate by vi word rules self.editor.action(font_system, Action::NextWord); } // Next WORD 'W' => { //TODO: WORD vs word, iterate by vi word rules self.editor.action(font_system, Action::NextWord); } // Remove character at cursor 'x' => self.editor.action(font_system, Action::Delete), // Remove character before cursor 'X' => self.editor.action(font_system, Action::Backspace), // Go to start of line '0' => self.editor.action(font_system, Action::Home), // Go to end of line '$' => self.editor.action(font_system, Action::End), // Go to start of line after whitespace '^' => self.editor.action(font_system, Action::SoftHome), // Enter command mode ':' => { self.mode = ViMode::Command { value: String::new(), }; } // Enter search mode '/' => { self.mode = ViMode::Search { value: String::new(), forwards: true, }; } // Enter search backwards mode '?' => { self.mode = ViMode::Search { value: String::new(), forwards: false, }; } _ => (), }, // Go to start of next line Action::Enter => { self.editor.action(font_system, Action::Down); self.editor.action(font_system, Action::SoftHome); } // Left Action::Backspace => { self.editor.action(font_system, Action::Left); } _ => self.editor.action(font_system, action), }, ViMode::Insert => match action { Action::Escape => { let cursor = self.cursor(); let layout_cursor = self.buffer().layout_cursor(&cursor); if layout_cursor.glyph > 0 { self.editor.action(font_system, Action::Left); } self.mode = ViMode::Normal; } _ => self.editor.action(font_system, action), }, ViMode::Command { ref mut value } => match action { Action::Escape => { self.mode = ViMode::Normal; } Action::Insert(c) => match c { _ => { value.push(c); } }, Action::Enter => { //TODO: run command self.mode = ViMode::Normal; } Action::Backspace => { if value.pop().is_none() { self.mode = ViMode::Normal; } } _ => self.editor.action(font_system, action), }, ViMode::Search { ref mut value, forwards, } => match action { Action::Escape => { self.mode = ViMode::Normal; } Action::Insert(c) => { value.push(c); } Action::Enter => { //TODO: do not require clone? self.search_opt = Some((value.clone(), forwards)); self.mode = ViMode::Normal; self.search(false); } Action::Backspace => { if value.pop().is_none() { self.mode = ViMode::Normal; } } _ => self.editor.action(font_system, action), }, } if self.mode != old_mode { self.buffer_mut().set_redraw(true); } } #[cfg(feature = "swash")] fn draw( &self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, color: Color, mut f: F, ) where F: FnMut(i32, i32, u32, u32, Color), { let size = self.buffer().size(); f(0, 0, size.0 as u32, size.1 as u32, self.background_color()); let font_size = self.buffer().metrics().font_size; let line_height = self.buffer().metrics().line_height; for run in self.buffer().layout_runs() { let line_i = run.line_i; let line_y = run.line_y; let line_top = run.line_top; let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32, f32)> { //TODO: better calculation of width let default_width = font_size / 2.0; if cursor.line == line_i { for (glyph_i, glyph) in run.glyphs.iter().enumerate() { if cursor.index >= glyph.start && cursor.index < glyph.end { // Guess x offset based on characters let mut before = 0; let mut total = 0; let cluster = &run.text[glyph.start..glyph.end]; for (i, _) in cluster.grapheme_indices(true) { if glyph.start + i < cursor.index { before += 1; } total += 1; } let width = glyph.w / (total as f32); let offset = (before as f32) * width; return Some((glyph_i, offset, width)); } } match run.glyphs.last() { Some(glyph) => { if cursor.index == glyph.end { return Some((run.glyphs.len(), 0.0, default_width)); } } None => { return Some((0, 0.0, default_width)); } } } None }; // Highlight selection (TODO: HIGHLIGHT COLOR!) if let Some(select) = self.select_opt() { let (start, end) = match select.line.cmp(&self.cursor().line) { cmp::Ordering::Greater => (self.cursor(), select), cmp::Ordering::Less => (select, self.cursor()), cmp::Ordering::Equal => { /* select.line == self.cursor.line */ if select.index < self.cursor().index { (select, self.cursor()) } else { /* select.index >= self.cursor.index */ (self.cursor(), select) } } }; if line_i >= start.line && line_i <= end.line { let mut range_opt = None; for glyph in run.glyphs.iter() { // Guess x offset based on characters let cluster = &run.text[glyph.start..glyph.end]; let total = cluster.grapheme_indices(true).count(); let mut c_x = glyph.x; let c_w = glyph.w / total as f32; for (i, c) in cluster.grapheme_indices(true) { let c_start = glyph.start + i; let c_end = glyph.start + i + c.len(); if (start.line != line_i || c_end > start.index) && (end.line != line_i || c_start < end.index) { range_opt = match range_opt.take() { Some((min, max)) => Some(( cmp::min(min, c_x as i32), cmp::max(max, (c_x + c_w) as i32), )), None => Some((c_x as i32, (c_x + c_w) as i32)), }; } else if let Some((min, max)) = range_opt.take() { f( min, line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), ); } c_x += c_w; } } if run.glyphs.is_empty() && end.line > line_i { // Highlight all of internal empty lines range_opt = Some((0, self.buffer().size().0 as i32)); } if let Some((mut min, mut max)) = range_opt.take() { if end.line > line_i { // Draw to end of line if run.rtl { min = 0; } else { max = self.buffer().size().0 as i32; } } f( min, line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), ); } } } // Draw cursor if let Some((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) = cursor_glyph_opt(&self.cursor()) { let block_cursor = match self.mode { ViMode::Passthrough => false, ViMode::Normal => true, ViMode::Insert => false, _ => true, /*TODO: determine block cursor in other modes*/ }; let (start_x, end_x) = match run.glyphs.get(cursor_glyph) { Some(glyph) => { // Start of detected glyph if glyph.level.is_rtl() { ( (glyph.x + glyph.w - cursor_glyph_offset) as i32, (glyph.x + glyph.w - cursor_glyph_offset - cursor_glyph_width) as i32, ) } else { ( (glyph.x + cursor_glyph_offset) as i32, (glyph.x + cursor_glyph_offset + cursor_glyph_width) as i32, ) } } None => match run.glyphs.last() { Some(glyph) => { // End of last glyph if glyph.level.is_rtl() { (glyph.x as i32, (glyph.x - cursor_glyph_width) as i32) } else { ( (glyph.x + glyph.w) as i32, (glyph.x + glyph.w + cursor_glyph_width) as i32, ) } } None => { // Start of empty line (0, cursor_glyph_width as i32) } }, }; if block_cursor { let left_x = cmp::min(start_x, end_x); let right_x = cmp::max(start_x, end_x); f( left_x, line_top as i32, (right_x - left_x) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), ); } else { f(start_x, line_top as i32, 1, line_height as u32, color); } } for glyph in run.glyphs.iter() { let physical_glyph = glyph.physical((0., 0.), 1.0); let glyph_color = match glyph.color_opt { Some(some) => some, None => color, }; cache.with_pixels( font_system, physical_glyph.cache_key, glyph_color, |x, y, color| { f( physical_glyph.x + x, line_y as i32 + physical_glyph.y + y, 1, 1, color, ); }, ); } } } } impl<'a, 'b> BorrowedWithFontSystem<'b, ViEditor<'a>> { /// Load text from a file, and also set syntax to the best option #[cfg(feature = "std")] pub fn load_text>( &mut self, path: P, attrs: crate::Attrs, ) -> std::io::Result<()> { self.inner.load_text(self.font_system, path, attrs) } }