use alloc::{collections::BTreeMap, string::String}; use core::cmp; use modit::{Event, Key, Parser, TextObject, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, FontSystem, Motion, Selection, SyntaxEditor, SyntaxTheme, }; pub use modit::{ViMode, ViParser}; fn undo_2_action<'buffer, E: Edit<'buffer>>( editor: &mut E, action: cosmic_undo_2::Action<&Change>, ) { match action { cosmic_undo_2::Action::Do(change) => { editor.apply_change(change); } cosmic_undo_2::Action::Undo(change) => { //TODO: make this more efficient let mut reversed = change.clone(); reversed.reverse(); editor.apply_change(&reversed); } } } fn finish_change<'buffer, E: Edit<'buffer>>( editor: &mut E, commands: &mut cosmic_undo_2::Commands, changed: &mut bool, ) -> Option { //TODO: join changes together match editor.finish_change() { Some(change) => { if !change.items.is_empty() { commands.push(change.clone()); *changed = true; } Some(change) } None => None, } } fn search<'buffer, E: Edit<'buffer>>(editor: &mut E, value: &str, forwards: bool) -> bool { let mut cursor = editor.cursor(); let start_line = cursor.line; if forwards { while cursor.line < editor.with_buffer(|buffer| buffer.lines.len()) { if let Some(index) = editor.with_buffer(|buffer| { buffer.lines[cursor.line] .text() .match_indices(value) .filter_map(|(i, _)| { if cursor.line != start_line || i > cursor.index { Some(i) } else { None } }) .next() }) { cursor.index = index; editor.set_cursor(cursor); return true; } cursor.line += 1; } } else { cursor.line += 1; while cursor.line > 0 { cursor.line -= 1; if let Some(index) = editor.with_buffer(|buffer| { buffer.lines[cursor.line] .text() .rmatch_indices(value) .filter_map(|(i, _)| { if cursor.line != start_line || i < cursor.index { Some(i) } else { None } }) .next() }) { cursor.index = index; editor.set_cursor(cursor); return true; } } } false } fn select_in<'buffer, E: Edit<'buffer>>(editor: &mut E, start_c: char, end_c: char, include: bool) { // Find the largest encompasing object, or if there is none, find the next one. let cursor = editor.cursor(); let (start, end) = editor.with_buffer(|buffer| { // Search forwards for isolated end character, counting start and end characters found let mut end = cursor; let mut starts = 0; let mut ends = 0; 'find_end: loop { let line = &buffer.lines[end.line]; let text = line.text(); for (i, c) in text[end.index..].char_indices() { if c == end_c { ends += 1; } else if c == start_c { starts += 1; } if ends > starts { end.index += if include { i + c.len_utf8() } else { i }; break 'find_end; } } if end.line + 1 < buffer.lines.len() { end.line += 1; end.index = 0; } else { break 'find_end; } } // Search backwards to resolve starts and ends let mut start = cursor; 'find_start: loop { let line = &buffer.lines[start.line]; let text = line.text(); for (i, c) in text[..start.index].char_indices().rev() { if c == start_c { starts += 1; } else if c == end_c { ends += 1; } if starts >= ends { start.index = if include { i } else { i + c.len_utf8() }; break 'find_start; } } if start.line > 0 { start.line -= 1; start.index = buffer.lines[start.line].text().len(); } else { break 'find_start; } } (start, end) }); editor.set_selection(Selection::Normal(start)); editor.set_cursor(end); } #[derive(Debug)] pub struct ViEditor<'syntax_system, 'buffer> { editor: SyntaxEditor<'syntax_system, 'buffer>, parser: ViParser, passthrough: bool, registers: BTreeMap, search_opt: Option<(String, bool)>, commands: cosmic_undo_2::Commands, changed: bool, } impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { pub fn new(editor: SyntaxEditor<'syntax_system, 'buffer>) -> Self { Self { editor, parser: ViParser::new(), passthrough: false, registers: BTreeMap::new(), search_opt: None, commands: cosmic_undo_2::Commands::new(), changed: false, } } /// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing pub fn update_theme(&mut self, theme_name: &str) -> bool { self.editor.update_theme(theme_name) } /// 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() } /// Get the default cursor color pub fn cursor_color(&self) -> Color { self.editor.cursor_color() } /// Get the default selection color pub fn selection_color(&self) -> Color { self.editor.selection_color() } /// Get the current syntect theme pub fn theme(&self) -> &SyntaxTheme { self.editor.theme() } /// Get changed flag pub fn changed(&self) -> bool { self.changed } /// Set changed flag pub fn set_changed(&mut self, changed: bool) { self.changed = changed; } /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { if passthrough != self.passthrough { self.passthrough = passthrough; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } /// Get current vi parser pub fn parser(&self) -> &ViParser { &self.parser } /// Redo a change pub fn redo(&mut self) { log::debug!("Redo"); for action in self.commands.redo() { undo_2_action(&mut self.editor, action); //TODO: clear changed flag when back to last saved state? self.changed = true; } } /// Undo a change pub fn undo(&mut self) { log::debug!("Undo"); for action in self.commands.undo() { undo_2_action(&mut self.editor, action); //TODO: clear changed flag when back to last saved state? self.changed = true; } } #[cfg(feature = "swash")] pub fn draw(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, mut f: F) where F: FnMut(i32, i32, u32, u32, Color), { let background_color = self.background_color(); let foreground_color = self.foreground_color(); let cursor_color = self.cursor_color(); let selection_color = self.selection_color(); self.with_buffer(|buffer| { let size = buffer.size(); f(0, 0, size.0 as u32, size.1 as u32, background_color); let font_size = buffer.metrics().font_size; let line_height = buffer.metrics().line_height; for run in 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((start, end)) = self.selection_bounds() { 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, selection_color, ); } c_x += c_w; } } if run.glyphs.is_empty() && end.line > line_i { // Highlight all of internal empty lines range_opt = Some((0, 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 = buffer.size().0 as i32; } } f( min, line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, selection_color, ); } } } // Draw cursor if let Some((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) = cursor_glyph_opt(&self.cursor()) { let block_cursor = if self.passthrough { false } else { match self.parser.mode { ViMode::Insert | ViMode::Replace => 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, selection_color, ); } else { f( start_x, line_top as i32, 1, line_height as u32, cursor_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 => foreground_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<'syntax_system, 'buffer> Edit<'buffer> for ViEditor<'syntax_system, 'buffer> { fn buffer_ref(&self) -> &BufferRef<'buffer> { self.editor.buffer_ref() } fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer> { self.editor.buffer_ref_mut() } fn cursor(&self) -> Cursor { self.editor.cursor() } fn set_cursor(&mut self, cursor: Cursor) { self.editor.set_cursor(cursor); } fn selection(&self) -> Selection { self.editor.selection() } fn set_selection(&mut self, selection: Selection) { self.editor.set_selection(selection); } fn auto_indent(&self) -> bool { self.editor.auto_indent() } fn set_auto_indent(&mut self, auto_indent: bool) { self.editor.set_auto_indent(auto_indent); } fn tab_width(&self) -> u16 { self.editor.tab_width() } fn set_tab_width(&mut self, tab_width: u16) { self.editor.set_tab_width(tab_width); } fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { self.editor.shape_as_needed(font_system, prune); } fn delete_range(&mut self, start: Cursor, end: Cursor) { self.editor.delete_range(start, end); } fn insert_at(&mut self, cursor: Cursor, data: &str, attrs_list: Option) -> Cursor { self.editor.insert_at(cursor, data, attrs_list) } fn copy_selection(&self) -> Option { self.editor.copy_selection() } fn delete_selection(&mut self) -> bool { self.editor.delete_selection() } fn apply_change(&mut self, change: &Change) -> bool { self.editor.apply_change(change) } fn start_change(&mut self) { self.editor.start_change(); } fn finish_change(&mut self) -> Option { finish_change(&mut self.editor, &mut self.commands, &mut self.changed) } fn action(&mut self, font_system: &mut FontSystem, action: Action) { log::debug!("Action {:?}", action); let editor = &mut self.editor; // Ensure a change is always started editor.start_change(); if self.passthrough { editor.action(font_system, action); // Always finish change when passing through (TODO: group changes) finish_change(editor, &mut self.commands, &mut self.changed); return; } let key = match action { //TODO: this leaves lots of room for issues in translation, should we directly accept Key? Action::Backspace => Key::Backspace, Action::Delete => Key::Delete, Action::Motion(Motion::Down) => Key::Down, Action::Motion(Motion::End) => Key::End, Action::Enter => Key::Enter, Action::Escape => Key::Escape, Action::Motion(Motion::Home) => Key::Home, Action::Indent => Key::Tab, Action::Insert(c) => Key::Char(c), Action::Motion(Motion::Left) => Key::Left, Action::Motion(Motion::PageDown) => Key::PageDown, Action::Motion(Motion::PageUp) => Key::PageUp, Action::Motion(Motion::Right) => Key::Right, Action::Unindent => Key::Backtab, Action::Motion(Motion::Up) => Key::Up, _ => { log::debug!("Pass through action {:?}", action); editor.action(font_system, action); // Always finish change when passing through (TODO: group changes) finish_change(editor, &mut self.commands, &mut self.changed); return; } }; let has_selection = match editor.selection() { Selection::None => false, _ => true, }; self.parser.parse(key, has_selection, |event| { log::debug!(" Event {:?}", event); let action = match event { Event::AutoIndent => { log::info!("TODO: AutoIndent"); return; } Event::Backspace => Action::Backspace, Event::BackspaceInLine => { let cursor = editor.cursor(); if cursor.index > 0 { Action::Backspace } else { return; } } Event::ChangeStart => { editor.start_change(); return; } Event::ChangeFinish => { finish_change(editor, &mut self.commands, &mut self.changed); return; } Event::Delete => Action::Delete, Event::DeleteInLine => { let cursor = editor.cursor(); if cursor.index < editor.with_buffer(|buffer| buffer.lines[cursor.line].text().len()) { Action::Delete } else { return; } } Event::Escape => Action::Escape, Event::Insert(c) => Action::Insert(c), Event::NewLine => Action::Enter, Event::Put { register, after } => { if let Some((selection, data)) = self.registers.get(®ister) { editor.start_change(); if editor.delete_selection() { editor.insert_string(data, None); } else { match selection { Selection::None | Selection::Normal(_) | Selection::Word(_) => { let mut cursor = editor.cursor(); if after { editor.with_buffer(|buffer| { let text = buffer.lines[cursor.line].text(); if let Some(c) = text[cursor.index..].chars().next() { cursor.index += c.len_utf8(); } }); editor.set_cursor(cursor); } editor.insert_at(cursor, data, None); } Selection::Line(_) => { let mut cursor = editor.cursor(); if after { // Insert at next line cursor.line += 1; } else { // Previous line will be moved down, so set cursor to next line cursor.line += 1; editor.set_cursor(cursor); cursor.line -= 1; } // Insert at start of line cursor.index = 0; // Insert text editor.insert_at(cursor, "\n", None); editor.insert_at(cursor, data, None); //TODO: Hack to allow immediate up/down editor.shape_as_needed(font_system, false); // Move to inserted line, preserving cursor x position if after { editor.action(font_system, Action::Motion(Motion::Down)); } else { editor.action(font_system, Action::Motion(Motion::Up)); } } } } finish_change(editor, &mut self.commands, &mut self.changed); } return; } Event::Redraw => { editor.with_buffer_mut(|buffer| buffer.set_redraw(true)); return; } Event::SelectClear => { editor.set_selection(Selection::None); return; } Event::SelectStart => { let cursor = editor.cursor(); editor.set_selection(Selection::Normal(cursor)); return; } Event::SelectLineStart => { let cursor = editor.cursor(); editor.set_selection(Selection::Line(cursor)); return; } Event::SelectTextObject(text_object, include) => { match text_object { TextObject::AngleBrackets => select_in(editor, '<', '>', include), TextObject::CurlyBrackets => select_in(editor, '{', '}', include), TextObject::DoubleQuotes => select_in(editor, '"', '"', include), TextObject::Parentheses => select_in(editor, '(', ')', include), TextObject::Search { forwards } => { match &self.search_opt { Some((value, _)) => { if search(editor, value, forwards) { let mut cursor = editor.cursor(); editor.set_selection(Selection::Normal(cursor)); //TODO: traverse lines if necessary cursor.index += value.len(); editor.set_cursor(cursor); } } None => {} } } TextObject::SingleQuotes => select_in(editor, '\'', '\'', include), TextObject::SquareBrackets => select_in(editor, '[', ']', include), TextObject::Ticks => select_in(editor, '`', '`', include), TextObject::Word(word) => { let mut cursor = editor.cursor(); let mut selection = editor.selection(); editor.with_buffer(|buffer| { let text = buffer.lines[cursor.line].text(); match WordIter::new(text, word) .find(|&(i, w)| i <= cursor.index && i + w.len() > cursor.index) { Some((i, w)) => { cursor.index = i; selection = Selection::Normal(cursor); cursor.index += w.len(); } None => { //TODO } } }); editor.set_selection(selection); editor.set_cursor(cursor); } _ => { log::info!("TODO: {:?}", text_object); } } return; } Event::SetSearch(value, forwards) => { self.search_opt = Some((value, forwards)); return; } Event::ShiftLeft => Action::Unindent, Event::ShiftRight => Action::Indent, Event::SwapCase => { log::info!("TODO: SwapCase"); return; } Event::Undo => { for action in self.commands.undo() { undo_2_action(editor, action); } return; } Event::Yank { register } => { if let Some(data) = editor.copy_selection() { self.registers.insert(register, (editor.selection(), data)); } return; } Event::Motion(motion) => { match motion { modit::Motion::Around => { //TODO: what to do for this psuedo-motion? return; } modit::Motion::Down => Action::Motion(Motion::Down), modit::Motion::End => Action::Motion(Motion::End), modit::Motion::GotoLine(line) => { Action::Motion(Motion::GotoLine(line.saturating_sub(1))) } modit::Motion::GotoEof => Action::Motion(Motion::GotoLine( editor.with_buffer(|buffer| buffer.lines.len().saturating_sub(1)), )), modit::Motion::Home => Action::Motion(Motion::Home), modit::Motion::Inside => { //TODO: what to do for this psuedo-motion? return; } modit::Motion::Left => Action::Motion(Motion::Left), modit::Motion::LeftInLine => { let cursor = editor.cursor(); if cursor.index > 0 { Action::Motion(Motion::Left) } else { return; } } modit::Motion::Line => { //TODO: what to do for this psuedo-motion? return; } modit::Motion::NextChar(find_c) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { match text[cursor.index..] .char_indices() .filter(|&(i, c)| i > 0 && c == find_c) .next() { Some((i, _)) => { cursor.index += i; } None => {} } } }); editor.set_cursor(cursor); return; } modit::Motion::NextCharTill(find_c) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { let mut last_i = 0; for (i, c) in text[cursor.index..].char_indices() { if last_i > 0 && c == find_c { cursor.index += last_i; break; } else { last_i = i; } } } }); editor.set_cursor(cursor); return; } modit::Motion::NextSearch => match &self.search_opt { Some((value, forwards)) => { search(editor, value, *forwards); return; } None => return, }, modit::Motion::NextWordEnd(word) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { loop { let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { cursor.index = WordIter::new(text, word) .map(|(i, w)| { i + w .char_indices() .last() .map(|(i, _)| i) .unwrap_or(0) }) .find(|&i| i > cursor.index) .unwrap_or(text.len()); if cursor.index == text.len() { // Try again, searching next line continue; } } else if cursor.line + 1 < buffer.lines.len() { // Go to next line and rerun loop cursor.line += 1; cursor.index = 0; continue; } break; } }); editor.set_cursor(cursor); return; } modit::Motion::NextWordStart(word) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { loop { let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { cursor.index = WordIter::new(text, word) .map(|(i, _)| i) .find(|&i| i > cursor.index) .unwrap_or(text.len()); if cursor.index == text.len() { // Try again, searching next line continue; } } else if cursor.line + 1 < buffer.lines.len() { // Go to next line and rerun loop cursor.line += 1; cursor.index = 0; continue; } break; } }); editor.set_cursor(cursor); return; } modit::Motion::PageDown => Action::Motion(Motion::PageDown), modit::Motion::PageUp => Action::Motion(Motion::PageUp), modit::Motion::PreviousChar(find_c) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { match text[..cursor.index] .char_indices() .filter(|&(_, c)| c == find_c) .last() { Some((i, _)) => { cursor.index = i; } None => {} } } }); editor.set_cursor(cursor); return; } modit::Motion::PreviousCharTill(find_c) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { match text[..cursor.index] .char_indices() .filter_map(|(i, c)| { if c == find_c { let end = i + c.len_utf8(); if end < cursor.index { return Some(end); } } None }) .last() { Some(i) => { cursor.index = i; } None => {} } } }); editor.set_cursor(cursor); return; } modit::Motion::PreviousSearch => match &self.search_opt { Some((value, forwards)) => { search(editor, value, !*forwards); return; } None => return, }, modit::Motion::PreviousWordEnd(word) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { loop { let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { cursor.index = WordIter::new(text, word) .map(|(i, w)| { i + w .char_indices() .last() .map(|(i, _)| i) .unwrap_or(0) }) .filter(|&i| i < cursor.index) .last() .unwrap_or(0); if cursor.index == 0 { // Try again, searching previous line continue; } } else if cursor.line > 0 { // Go to previous line and rerun loop cursor.line -= 1; cursor.index = buffer.lines[cursor.line].text().len(); continue; } break; } }); editor.set_cursor(cursor); return; } modit::Motion::PreviousWordStart(word) => { let mut cursor = editor.cursor(); editor.with_buffer(|buffer| { loop { let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { cursor.index = WordIter::new(text, word) .map(|(i, _)| i) .filter(|&i| i < cursor.index) .last() .unwrap_or(0); if cursor.index == 0 { // Try again, searching previous line continue; } } else if cursor.line > 0 { // Go to previous line and rerun loop cursor.line -= 1; cursor.index = buffer.lines[cursor.line].text().len(); continue; } break; } }); editor.set_cursor(cursor); return; } modit::Motion::Right => Action::Motion(Motion::Right), modit::Motion::RightInLine => { let cursor = editor.cursor(); if cursor.index < editor .with_buffer(|buffer| buffer.lines[cursor.line].text().len()) { Action::Motion(Motion::Right) } else { return; } } modit::Motion::ScreenHigh => { //TODO: is this efficient? if let Some(line_i) = editor.with_buffer(|buffer| { buffer.layout_runs().next().map(|first| first.line_i) }) { Action::Motion(Motion::GotoLine(line_i)) } else { return; } } modit::Motion::ScreenLow => { //TODO: is this efficient? if let Some(line_i) = editor.with_buffer(|buffer| { buffer.layout_runs().last().map(|last| last.line_i) }) { Action::Motion(Motion::GotoLine(line_i)) } else { return; } } modit::Motion::ScreenMiddle => { //TODO: is this efficient? let action_opt = editor.with_buffer(|buffer| { let mut layout_runs = buffer.layout_runs(); if let Some(first) = layout_runs.next() { if let Some(last) = layout_runs.last() { Some(Action::Motion(Motion::GotoLine( (last.line_i + first.line_i) / 2, ))) } else { None } } else { None } }); match action_opt { Some(action) => action, None => return, } } modit::Motion::Selection => { //TODO: what to do for this psuedo-motion? return; } modit::Motion::SoftHome => Action::Motion(Motion::SoftHome), modit::Motion::Up => Action::Motion(Motion::Up), } } }; editor.action(font_system, action); }); } } impl<'font_system, 'syntax_system, 'buffer> BorrowedWithFontSystem<'font_system, ViEditor<'syntax_system, 'buffer>> { /// 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) } #[cfg(feature = "swash")] pub fn draw(&mut self, cache: &mut crate::SwashCache, f: F) where F: FnMut(i32, i32, u32, u32, Color), { self.inner.draw(self.font_system, cache, f); } }