diff --git a/Cargo.toml b/Cargo.toml index ed57a57..ddb78a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,20 +10,22 @@ repository = "https://github.com/pop-os/cosmic-text" rust-version = "1.65" [dependencies] +cosmic_undo_2 = { version = "0.2.0", optional = true } fontdb = { version = "0.16.0", default-features = false } +hashbrown = { version = "0.14.1", optional = true, default-features = false } libm = "0.2.8" log = "0.4.20" +modit = { version = "0.1.0", optional = true } +rangemap = "1.4.0" +rustc-hash = { version = "1.1.0", default-features = false } rustybuzz = { version = "0.11.0", default-features = false, features = ["libm"] } +self_cell = "1.0.1" swash = { version = "0.1.8", optional = true } syntect = { version = "5.1.0", optional = true } sys-locale = { version = "0.3.1", optional = true } unicode-linebreak = "0.1.5" unicode-script = "0.5.5" unicode-segmentation = "1.10.1" -rangemap = "1.4.0" -hashbrown = { version = "0.14.1", optional = true, default-features = false } -rustc-hash = { version = "1.1.0", default-features = false } -self_cell = "1.0.1" [dependencies.unicode-bidi] version = "0.3.13" @@ -40,7 +42,7 @@ std = [ "sys-locale", "unicode-bidi/std", ] -vi = ["syntect"] +vi = ["modit", "syntect", "cosmic_undo_2"] wasm-web = ["sys-locale?/js"] warn_on_missing_glyphs = [] fontconfig = ["fontdb/fontconfig", "std"] diff --git a/examples/editor-orbclient/src/main.rs b/examples/editor-orbclient/src/main.rs index ca82313..14caf46 100644 --- a/examples/editor-orbclient/src/main.rs +++ b/examples/editor-orbclient/src/main.rs @@ -22,7 +22,7 @@ fn main() { let display_scale = match orbclient::get_display_size() { Ok((w, h)) => { log::info!("Display size: {}, {}", w, h); - (h as f32 / 1600.0) + 1.0 + (h / 1600) as f32 + 1.0 } Err(err) => { log::warn!("Failed to get display size: {}", err); diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 66092db..a3d8c41 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -119,7 +119,7 @@ fn main() { editor .buffer_mut() - .set_rich_text(spans.iter().copied(), Shaping::Advanced); + .set_rich_text(spans.iter().copied(), attrs, Shaping::Advanced); let mut swash_cache = SwashCache::new(); diff --git a/redoxer.sh b/redoxer.sh index ba39b4c..bc7310c 100755 --- a/redoxer.sh +++ b/redoxer.sh @@ -10,12 +10,12 @@ redoxer install \ --path examples/editor-orbclient \ --root "target/redoxer" -cmd="env RUST_LOG=cosmic_text=debug,editor_orbclient=debug ./bin/editor-orbclient" +args=(env RUST_LOG=cosmic_text=debug,editor_orbclient=debug /root/bin/editor-orbclient) if [ -f "$1" ] then filename="$(basename "$1")" cp "$1" "target/redoxer/${filename}" - cmd="${cmd} '${filename}'" + args+=("${filename}") fi cd target/redoxer @@ -24,5 +24,4 @@ cd target/redoxer redoxer exec \ --gui \ --folder . \ - /bin/sh -c \ - "${cmd}" + "${args[@]}" diff --git a/src/buffer.rs b/src/buffer.rs index a04675d..c67547c 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; +use alloc::{string::String, vec::Vec}; use core::{cmp, fmt}; use unicode_segmentation::UnicodeSegmentation; @@ -54,8 +51,9 @@ impl Cursor { } /// Whether to associate cursors placed at a boundary between runs with the run before or after it. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] pub enum Affinity { + #[default] Before, After, } @@ -86,12 +84,6 @@ impl Affinity { } } -impl Default for Affinity { - fn default() -> Self { - Affinity::Before - } -} - /// The position of a cursor within a [`Buffer`]. #[derive(Debug)] pub struct LayoutCursor { @@ -552,12 +544,7 @@ impl Buffer { /// /// Will panic if `metrics.font_size` is zero. pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) { - if metrics != self.metrics { - assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); - self.metrics = metrics; - self.relayout(font_system); - self.shape_until_scroll(font_system); - } + self.set_metrics_and_size(font_system, metrics, self.width, self.height); } /// Get the current [`Wrap`] @@ -581,10 +568,27 @@ impl Buffer { /// Set the current buffer dimensions pub fn set_size(&mut self, font_system: &mut FontSystem, width: f32, height: f32) { + self.set_metrics_and_size(font_system, self.metrics, width, height); + } + + /// Set the current [`Metrics`] and buffer dimensions at the same time + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics_and_size( + &mut self, + font_system: &mut FontSystem, + metrics: Metrics, + width: f32, + height: f32, + ) { let clamped_width = width.max(0.0); let clamped_height = height.max(0.0); - if clamped_width != self.width || clamped_height != self.height { + if metrics != self.metrics || clamped_width != self.width || clamped_height != self.height { + assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); + self.metrics = metrics; self.width = clamped_width; self.height = clamped_height; self.relayout(font_system); @@ -618,7 +622,7 @@ impl Buffer { attrs: Attrs, shaping: Shaping, ) { - self.set_rich_text(font_system, [(text, attrs)], shaping); + self.set_rich_text(font_system, [(text, attrs)], attrs, shaping); } /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) @@ -634,6 +638,7 @@ impl Buffer { /// ("hello, ", attrs), /// ("cosmic\ntext", attrs.family(Family::Monospace)), /// ], + /// attrs, /// Shaping::Advanced, /// ); /// ``` @@ -641,13 +646,14 @@ impl Buffer { &mut self, font_system: &mut FontSystem, spans: I, + default_attrs: Attrs, shaping: Shaping, ) where I: IntoIterator)>, { self.lines.clear(); - let mut attrs_list = AttrsList::new(Attrs::new()); + let mut attrs_list = AttrsList::new(default_attrs); let mut line_string = String::new(); let mut end = 0; let (string, spans_data): (String, Vec<_>) = spans @@ -676,7 +682,7 @@ impl Buffer { // this is reached only if this text is empty self.lines.push(BufferLine::new( String::new(), - AttrsList::new(Attrs::new()), + AttrsList::new(default_attrs), shaping, )); break; @@ -690,7 +696,10 @@ impl Buffer { let text_start = line_string.len(); line_string.push_str(text); let text_end = line_string.len(); - attrs_list.add_span(text_start..text_end, *attrs); + // Only add attrs if they don't match the defaults + if *attrs != attrs_list.defaults() { + attrs_list.add_span(text_start..text_end, *attrs); + } } // we know that at the end of a line, @@ -705,7 +714,7 @@ impl Buffer { if maybe_line.is_some() { // finalize this line and start a new line let prev_attrs_list = - core::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); + core::mem::replace(&mut attrs_list, AttrsList::new(default_attrs)); let prev_line_string = core::mem::take(&mut line_string); let buffer_line = BufferLine::new(prev_line_string, prev_attrs_list, shaping); self.lines.push(buffer_line); @@ -923,6 +932,16 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { self.inner.set_size(self.font_system, width, height); } + /// Set the current [`Metrics`] and buffer dimensions at the same time + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics_and_size(&mut self, metrics: Metrics, width: f32, height: f32) { + self.inner + .set_metrics_and_size(self.font_system, metrics, width, height); + } + /// Set text of buffer, using provided attributes for each line by default pub fn set_text(&mut self, text: &str, attrs: Attrs, shaping: Shaping) { self.inner.set_text(self.font_system, text, attrs, shaping); @@ -941,14 +960,16 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { /// ("hello, ", attrs), /// ("cosmic\ntext", attrs.family(Family::Monospace)), /// ], + /// attrs, /// Shaping::Advanced, /// ); /// ``` - pub fn set_rich_text<'r, 's, I>(&mut self, spans: I, shaping: Shaping) + pub fn set_rich_text<'r, 's, I>(&mut self, spans: I, default_attrs: Attrs, shaping: Shaping) where I: IntoIterator)>, { - self.inner.set_rich_text(self.font_system, spans, shaping); + self.inner + .set_rich_text(self.font_system, spans, default_attrs, shaping); } /// Draw the buffer diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 274e05b..fe026ed 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(not(feature = "std"))] -use alloc::string::String; +use alloc::string::{String, ToString}; use core::{ cmp::{self, Ordering}, iter::once, @@ -11,8 +11,8 @@ use unicode_segmentation::UnicodeSegmentation; #[cfg(feature = "swash")] use crate::Color; use crate::{ - Action, Affinity, AttrsList, Buffer, BufferLine, Cursor, Edit, FontSystem, LayoutCursor, - Shaping, + Action, Affinity, AttrsList, Buffer, BufferLine, Change, ChangeItem, Cursor, Edit, FontSystem, + LayoutCursor, Shaping, }; /// A wrapper of [`Buffer`] for easy editing @@ -23,6 +23,9 @@ pub struct Editor { cursor_x_opt: Option, select_opt: Option, cursor_moved: bool, + auto_indent: bool, + tab_width: u16, + change: Option, } impl Editor { @@ -34,6 +37,9 @@ impl Editor { cursor_x_opt: None, select_opt: None, cursor_moved: false, + auto_indent: false, + tab_width: 4, + change: None, } } @@ -70,6 +76,155 @@ impl Editor { self.buffer.set_redraw(true); } } + + fn delete_range(&mut self, start: Cursor, end: Cursor) { + // Collect removed data for change tracking + let mut change_text = String::new(); + + // Delete the selection from the last line + let end_line_opt = if end.line > start.line { + // Get part of line after selection + let after = self.buffer.lines[end.line].split_off(end.index); + + // Remove end line + let removed = self.buffer.lines.remove(end.line); + change_text.insert_str(0, removed.text()); + + Some(after) + } else { + None + }; + + // Delete interior lines (in reverse for safety) + for line_i in (start.line + 1..end.line).rev() { + let removed = self.buffer.lines.remove(line_i); + change_text.insert_str(0, removed.text()); + } + + // Delete the selection from the first line + { + // Get part after selection if start line is also end line + let after_opt = if start.line == end.line { + Some(self.buffer.lines[start.line].split_off(end.index)) + } else { + None + }; + + // Delete selected part of line + let removed = self.buffer.lines[start.line].split_off(start.index); + change_text.insert_str(0, removed.text()); + + // Re-add part of line after selection + if let Some(after) = after_opt { + self.buffer.lines[start.line].append(after); + } + + // Re-add valid parts of end line + if let Some(end_line) = end_line_opt { + self.buffer.lines[start.line].append(end_line); + } + } + + if let Some(ref mut change) = self.change { + let item = ChangeItem { + start, + end, + text: change_text, + insert: false, + }; + change.items.push(item); + } + } + + fn insert_at( + &mut self, + mut cursor: Cursor, + data: &str, + attrs_list: Option, + ) -> Cursor { + let mut remaining_split_len = data.len(); + if remaining_split_len == 0 { + return cursor; + } + + // Save cursor for change tracking + let start = cursor; + + let line: &mut BufferLine = &mut self.buffer.lines[cursor.line]; + let insert_line = cursor.line + 1; + + // Collect text after insertion as a line + let after: BufferLine = line.split_off(cursor.index); + let after_len = after.text().len(); + + // Collect attributes + let mut final_attrs = attrs_list.unwrap_or_else(|| { + AttrsList::new(line.attrs_list().get_span(cursor.index.saturating_sub(1))) + }); + + // Append the inserted text, line by line + // we want to see a blank entry if the string ends with a newline + let addendum = once("").filter(|_| data.ends_with('\n')); + let mut lines_iter = data.split_inclusive('\n').chain(addendum); + if let Some(data_line) = lines_iter.next() { + let mut these_attrs = final_attrs.split_off(data_line.len()); + remaining_split_len -= data_line.len(); + core::mem::swap(&mut these_attrs, &mut final_attrs); + line.append(BufferLine::new( + data_line + .strip_suffix(char::is_control) + .unwrap_or(data_line), + these_attrs, + Shaping::Advanced, + )); + } else { + panic!("str::lines() did not yield any elements"); + } + if let Some(data_line) = lines_iter.next_back() { + remaining_split_len -= data_line.len(); + let mut tmp = BufferLine::new( + data_line + .strip_suffix(char::is_control) + .unwrap_or(data_line), + final_attrs.split_off(remaining_split_len), + Shaping::Advanced, + ); + tmp.append(after); + self.buffer.lines.insert(insert_line, tmp); + cursor.line += 1; + } else { + line.append(after); + } + for data_line in lines_iter.rev() { + remaining_split_len -= data_line.len(); + let tmp = BufferLine::new( + data_line + .strip_suffix(char::is_control) + .unwrap_or(data_line), + final_attrs.split_off(remaining_split_len), + Shaping::Advanced, + ); + self.buffer.lines.insert(insert_line, tmp); + cursor.line += 1; + } + + assert_eq!(remaining_split_len, 0); + + // Append the text after insertion + cursor.index = self.buffer.lines[cursor.line].text().len() - after_len; + + if let Some(ref mut change) = self.change { + let item = ChangeItem { + start, + end: cursor, + text: data.to_string(), + insert: true, + }; + change.items.push(item); + } + + cursor + } } impl Edit for Editor { @@ -86,7 +241,12 @@ impl Edit for Editor { } fn set_cursor(&mut self, cursor: Cursor) { - self.cursor = cursor; + if self.cursor != cursor { + self.cursor = cursor; + self.cursor_moved = true; + self.cursor_x_opt = None; + self.buffer.set_redraw(true); + } } fn select_opt(&self) -> Option { @@ -100,6 +260,29 @@ impl Edit for Editor { } } + fn auto_indent(&self) -> bool { + self.auto_indent + } + + fn set_auto_indent(&mut self, auto_indent: bool) { + self.auto_indent = auto_indent; + } + + fn tab_width(&self) -> u16 { + self.tab_width + } + + fn set_tab_width(&mut self, tab_width: u16) { + // A tab width of 0 is not allowed + if tab_width == 0 { + return; + } + if self.tab_width != tab_width { + self.tab_width = tab_width; + self.buffer.set_redraw(true); + } + } + fn shape_as_needed(&mut self, font_system: &mut FontSystem) { if self.cursor_moved { self.buffer.shape_until_cursor(font_system, self.cursor); @@ -176,123 +359,52 @@ impl Edit for Editor { // Reset cursor to start of selection self.cursor = start; - // Delete the selection from the last line - let end_line_opt = if end.line > start.line { - // Get part of line after selection - let after = self.buffer.lines[end.line].split_off(end.index); - - // Remove end line - self.buffer.lines.remove(end.line); - - Some(after) - } else { - None - }; - - // Delete interior lines (in reverse for safety) - for line_i in (start.line + 1..end.line).rev() { - self.buffer.lines.remove(line_i); - } - - // Delete the selection from the first line - { - // Get part after selection if start line is also end line - let after_opt = if start.line == end.line { - Some(self.buffer.lines[start.line].split_off(end.index)) - } else { - None - }; - - // Delete selected part of line - self.buffer.lines[start.line].split_off(start.index); - - // Re-add part of line after selection - if let Some(after) = after_opt { - self.buffer.lines[start.line].append(after); - } - - // Re-add valid parts of end line - if let Some(end_line) = end_line_opt { - self.buffer.lines[start.line].append(end_line); - } - } + // Delete from start to end of selection + self.delete_range(start, end); true } fn insert_string(&mut self, data: &str, attrs_list: Option) { self.delete_selection(); - let mut remaining_split_len = data.len(); - if remaining_split_len == 0 { - return; + let new_cursor = self.insert_at(self.cursor, data, attrs_list); + self.set_cursor(new_cursor); + } + + fn apply_change(&mut self, change: &Change) -> bool { + // Cannot apply changes if there is a pending change + match self.change.take() { + Some(pending) => { + if !pending.items.is_empty() { + //TODO: is this a good idea? + log::warn!("pending change caused apply_change to be ignored!"); + self.change = Some(pending); + return false; + } + } + None => {} } - let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; - let insert_line = self.cursor.line + 1; - - // Collect text after insertion as a line - let after: BufferLine = line.split_off(self.cursor.index); - let after_len = after.text().len(); - - // Collect attributes - let mut final_attrs = attrs_list.unwrap_or_else(|| { - AttrsList::new( - line.attrs_list() - .get_span(self.cursor.index.saturating_sub(1)), - ) - }); - - // Append the inserted text, line by line - // we want to see a blank entry if the string ends with a newline - let addendum = once("").filter(|_| data.ends_with('\n')); - let mut lines_iter = data.split_inclusive('\n').chain(addendum); - if let Some(data_line) = lines_iter.next() { - let mut these_attrs = final_attrs.split_off(data_line.len()); - remaining_split_len -= data_line.len(); - core::mem::swap(&mut these_attrs, &mut final_attrs); - line.append(BufferLine::new( - data_line - .strip_suffix(char::is_control) - .unwrap_or(data_line), - these_attrs, - Shaping::Advanced, - )); - } else { - panic!("str::lines() did not yield any elements"); - } - if let Some(data_line) = lines_iter.next_back() { - remaining_split_len -= data_line.len(); - let mut tmp = BufferLine::new( - data_line - .strip_suffix(char::is_control) - .unwrap_or(data_line), - final_attrs.split_off(remaining_split_len), - Shaping::Advanced, - ); - tmp.append(after); - self.buffer.lines.insert(insert_line, tmp); - self.cursor.line += 1; - } else { - line.append(after); - } - for data_line in lines_iter.rev() { - remaining_split_len -= data_line.len(); - let tmp = BufferLine::new( - data_line - .strip_suffix(char::is_control) - .unwrap_or(data_line), - final_attrs.split_off(remaining_split_len), - Shaping::Advanced, - ); - self.buffer.lines.insert(insert_line, tmp); - self.cursor.line += 1; + for item in change.items.iter() { + //TODO: edit cursor if needed? + if item.insert { + self.cursor = self.insert_at(item.start, &item.text, None); + } else { + self.cursor = item.start; + self.delete_range(item.start, item.end); + } } + true + } - assert_eq!(remaining_split_len, 0); + fn start_change(&mut self) { + if self.change.is_none() { + self.change = Some(Change::default()); + } + } - // Append the text after insertion - self.cursor.index = self.buffer.lines[self.cursor.line].text().len() - after_len; - self.cursor_moved = true; + fn finish_change(&mut self) -> Option { + self.change.take() } fn action(&mut self, font_system: &mut FontSystem, action: Action) { @@ -300,7 +412,7 @@ impl Edit for Editor { match action { Action::Previous => { - let line = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index > 0 { // Find previous character index let mut prev_index = 0; @@ -324,7 +436,7 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::Next => { - let line = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index < line.text().len() { for (i, c) in line.text().grapheme_indices(true) { if i == self.cursor.index { @@ -343,9 +455,9 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::Left => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { @@ -356,9 +468,9 @@ impl Edit for Editor { } } Action::Right => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { @@ -426,6 +538,17 @@ impl Edit for Editor { self.set_layout_cursor(font_system, cursor); self.cursor_x_opt = None; } + Action::SoftHome => { + let line = &self.buffer.lines[self.cursor.line]; + self.cursor.index = line + .text() + .char_indices() + .filter_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) }) + .next() + .unwrap_or(0); + self.buffer.set_redraw(true); + self.cursor_x_opt = None; + } Action::End => { let mut cursor = self.buffer.layout_cursor(&self.cursor); cursor.glyph = usize::max_value(); @@ -483,82 +606,222 @@ impl Edit for Editor { } } Action::Enter => { - self.delete_selection(); + //TODO: what about indenting more after opening brackets or parentheses? + if self.auto_indent { + let mut string = String::from("\n"); + { + let line = &self.buffer.lines[self.cursor.line]; + let text = line.text(); + for c in text.chars() { + if c.is_whitespace() { + string.push(c); + } else { + break; + } + } + } + self.insert_string(&string, None); + } else { + self.insert_string("\n", None); + } - let new_line = self.buffer.lines[self.cursor.line].split_off(self.cursor.index); - - self.cursor.line += 1; - self.cursor.index = 0; - - self.buffer.lines.insert(self.cursor.line, new_line); + // Ensure line is properly shaped and laid out (for potential immediate commands) + self.buffer.line_layout(font_system, self.cursor.line); } Action::Backspace => { if self.delete_selection() { // Deleted selection - } else if self.cursor.index > 0 { - let line = &mut self.buffer.lines[self.cursor.line]; + } else { + // Save current cursor as end + let end = self.cursor; - // Get text line after cursor - let after = line.split_off(self.cursor.index); - - // Find previous character index - let mut prev_index = 0; - for (i, _) in line.text().char_indices() { - if i < self.cursor.index { - prev_index = i; - } else { - break; - } + if self.cursor.index > 0 { + // Move cursor to previous character index + let line = &self.buffer.lines[self.cursor.line]; + self.cursor.index = line.text()[..self.cursor.index] + .char_indices() + .next_back() + .map_or(0, |(i, _)| i); + } else if self.cursor.line > 0 { + // Move cursor to previous line + self.cursor.line -= 1; + let line = &self.buffer.lines[self.cursor.line]; + self.cursor.index = line.text().len(); } - self.cursor.index = prev_index; - - // Remove character - line.split_off(self.cursor.index); - - // Add text after cursor - line.append(after); - } else if self.cursor.line > 0 { - let mut line_index = self.cursor.line; - let old_line = self.buffer.lines.remove(line_index); - line_index -= 1; - - let line = &mut self.buffer.lines[line_index]; - - self.cursor.line = line_index; - self.cursor.index = line.text().len(); - - line.append(old_line); + if self.cursor != end { + // Delete range + self.delete_range(self.cursor, end); + } } } Action::Delete => { if self.delete_selection() { // Deleted selection - } else if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() { - let line = &mut self.buffer.lines[self.cursor.line]; + } else { + // Save current cursor as end + let mut end = self.cursor; - let range_opt = line - .text() - .grapheme_indices(true) - .take_while(|(i, _)| *i <= self.cursor.index) - .last() - .map(|(i, c)| i..(i + c.len())); + if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() { + let line = &self.buffer.lines[self.cursor.line]; - if let Some(range) = range_opt { - self.cursor.index = range.start; + let range_opt = line + .text() + .grapheme_indices(true) + .take_while(|(i, _)| *i <= self.cursor.index) + .last() + .map(|(i, c)| i..(i + c.len())); - // Get text after deleted EGC - let after = line.split_off(range.end); - - // Delete EGC - line.split_off(range.start); - - // Add text after deleted EGC - line.append(after); + if let Some(range) = range_opt { + self.cursor.index = range.start; + end.index = range.end; + } + } else if self.cursor.line + 1 < self.buffer.lines.len() { + end.line += 1; + end.index = 0; } - } else if self.cursor.line + 1 < self.buffer.lines.len() { - let old_line = self.buffer.lines.remove(self.cursor.line + 1); - self.buffer.lines[self.cursor.line].append(old_line); + + if self.cursor != end { + self.delete_range(self.cursor, end); + } + } + } + Action::Indent => { + // Get start and end of selection + let (start, end) = match self.select_opt { + Some(select) => 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) + } + } + }, + None => (self.cursor, self.cursor), + }; + + // For every line in selection + let tab_width: usize = self.tab_width.into(); + for line_i in start.line..=end.line { + // Determine indexes of last indent and first character after whitespace + let mut after_whitespace; + let mut required_indent = 0; + { + let line = &self.buffer.lines[line_i]; + let text = line.text(); + // Default to end of line if no non-whitespace found + after_whitespace = text.len(); + for (count, (index, c)) in text.char_indices().enumerate() { + if !c.is_whitespace() { + after_whitespace = index; + required_indent = tab_width - (count % tab_width); + break; + } + } + } + + // No indent required (not possible?) + if required_indent == 0 { + required_indent = tab_width; + } + + self.insert_at( + Cursor::new(line_i, after_whitespace), + &" ".repeat(required_indent), + None, + ); + + // Adjust cursor + if self.cursor.line == line_i { + //TODO: should we be forcing cursor index to current indent location? + if self.cursor.index < after_whitespace { + self.cursor.index = after_whitespace; + } + self.cursor.index += required_indent; + } + + // Adjust selection + if let Some(ref mut select) = self.select_opt { + if select.line == line_i && select.index >= after_whitespace { + select.index += required_indent; + } + } + + // Request redraw + self.buffer.set_redraw(true); + } + } + Action::Unindent => { + // Get start and end of selection + let (start, end) = match self.select_opt { + Some(select) => 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) + } + } + }, + None => (self.cursor, self.cursor), + }; + + // For every line in selection + let tab_width: usize = self.tab_width.into(); + for line_i in start.line..=end.line { + // Determine indexes of last indent and first character after whitespace + let mut last_indent = 0; + let mut after_whitespace; + { + let line = &self.buffer.lines[line_i]; + let text = line.text(); + // Default to end of line if no non-whitespace found + after_whitespace = text.len(); + for (count, (index, c)) in text.char_indices().enumerate() { + if !c.is_whitespace() { + after_whitespace = index; + break; + } + if count % tab_width == 0 { + last_indent = index; + } + } + } + + // No de-indent required + if last_indent == after_whitespace { + continue; + } + + // Delete one indent + self.delete_range( + Cursor::new(line_i, last_indent), + Cursor::new(line_i, after_whitespace), + ); + + // Adjust cursor + if self.cursor.line == line_i && self.cursor.index > last_indent { + self.cursor.index -= after_whitespace - last_indent; + } + + // Adjust selection + if let Some(ref mut select) = self.select_opt { + if select.line == line_i && select.index > last_indent { + select.index -= after_whitespace - last_indent; + } + } + + // Request redraw + self.buffer.set_redraw(true); } } Action::Click { x, y } => { @@ -594,7 +857,7 @@ impl Edit for Editor { self.buffer.set_scroll(scroll); } Action::PreviousWord => { - let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index > 0 { self.cursor.index = line .text() @@ -613,7 +876,7 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::NextWord => { - let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index < line.text().len() { self.cursor.index = line .text() @@ -631,9 +894,9 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::LeftWord => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { @@ -644,9 +907,9 @@ impl Edit for Editor { } } Action::RightWord => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { @@ -666,6 +929,11 @@ impl Edit for Editor { self.cursor.index = self.buffer.lines[self.cursor.line].text().len(); self.cursor_x_opt = None; } + Action::GotoLine(line) => { + let mut cursor = self.buffer.layout_cursor(&self.cursor); + cursor.line = line; + self.set_layout_cursor(font_system, cursor); + } } if old_cursor != self.cursor { diff --git a/src/edit/mod.rs b/src/edit/mod.rs index 1c16746..7976537 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -1,5 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::string::String; +use alloc::{string::String, vec::Vec}; #[cfg(feature = "swash")] use crate::Color; @@ -35,6 +35,8 @@ pub enum Action { Down, /// Move cursor to start of line Home, + /// Move cursor to start of line, skipping whitespace + SoftHome, /// Move cursor to end of line End, /// Move cursor to start of paragraph @@ -57,12 +59,24 @@ pub enum Action { Backspace, /// Delete text in front of cursor Delete, + // Indent text (typically Tab) + Indent, + // Unindent text (typically Shift+Tab) + Unindent, /// Mouse click at specified position - Click { x: i32, y: i32 }, + Click { + x: i32, + y: i32, + }, /// Mouse drag to specified position - Drag { x: i32, y: i32 }, + Drag { + x: i32, + y: i32, + }, /// Scroll specified number of lines - Scroll { lines: i32 }, + Scroll { + lines: i32, + }, /// Move cursor to previous word boundary PreviousWord, /// Move cursor to next word boundary @@ -75,6 +89,45 @@ pub enum Action { BufferStart, /// Move cursor to the end of the document BufferEnd, + /// Move cursor to specific line + GotoLine(usize), +} + +/// A unique change to an editor +#[derive(Clone, Debug)] +pub struct ChangeItem { + /// Cursor indicating start of change + pub start: Cursor, + /// Cursor indicating end of change + pub end: Cursor, + /// Text to be inserted or deleted + pub text: String, + /// Insert if true, delete if false + pub insert: bool, +} + +impl ChangeItem { + // Reverse change item (in place) + pub fn reverse(&mut self) { + self.insert = !self.insert; + } +} + +/// A set of change items grouped into one logical change +#[derive(Clone, Debug, Default)] +pub struct Change { + /// Change items grouped into one change + pub items: Vec, +} + +impl Change { + // Reverse change (in place) + pub fn reverse(&mut self) { + self.items.reverse(); + for item in self.items.iter_mut() { + item.reverse(); + } + } } /// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor` @@ -111,6 +164,18 @@ pub trait Edit { /// Set the current selection position fn set_select_opt(&mut self, select_opt: Option); + /// Get the current automatic indentation setting + fn auto_indent(&self) -> bool; + + /// Enable or disable automatic indentation + fn set_auto_indent(&mut self, auto_indent: bool); + + /// Get the current tab width + fn tab_width(&self) -> u16; + + /// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored + fn set_tab_width(&mut self, tab_width: u16); + /// Shape lines until scroll, after adjusting scroll if the cursor moved fn shape_as_needed(&mut self, font_system: &mut FontSystem); @@ -125,6 +190,15 @@ pub trait Edit { /// attributes, or with the previous character's attributes if None is given. fn insert_string(&mut self, data: &str, attrs_list: Option); + /// Apply a change + fn apply_change(&mut self, change: &Change) -> bool; + + /// Start collecting change + fn start_change(&mut self); + + /// Get completed change + fn finish_change(&mut self) -> Option; + /// Perform an [Action] on the editor fn action(&mut self, font_system: &mut FontSystem, action: Action); diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index 8eda391..ae91963 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -3,15 +3,17 @@ use alloc::{string::String, vec::Vec}; #[cfg(feature = "std")] use std::{fs, io, path::Path}; use syntect::highlighting::{ - FontStyle, HighlightState, Highlighter, RangedHighlightIterator, Theme, ThemeSet, + FontStyle, HighlightState, Highlighter, RangedHighlightIterator, ThemeSet, }; use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}; use crate::{ - Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, Editor, FontSystem, - Shaping, Style, Weight, Wrap, + Action, AttrsList, BorrowedWithFontSystem, Buffer, Change, Color, Cursor, Edit, Editor, + FontSystem, Shaping, Style, Weight, Wrap, }; +pub use syntect::highlighting::Theme as SyntaxTheme; + #[derive(Debug)] pub struct SyntaxSystem { pub syntax_set: SyntaxSet, @@ -35,7 +37,7 @@ pub struct SyntaxEditor<'a> { editor: Editor, syntax_system: &'a SyntaxSystem, syntax: &'a SyntaxReference, - theme: &'a Theme, + theme: &'a SyntaxTheme, highlighter: Highlighter<'a>, syntax_cache: Vec<(ParseState, HighlightState)>, } @@ -65,9 +67,11 @@ impl<'a> SyntaxEditor<'a> { /// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing pub fn update_theme(&mut self, theme_name: &str) -> bool { if let Some(theme) = self.syntax_system.theme_set.themes.get(theme_name) { - self.theme = theme; - self.highlighter = Highlighter::new(theme); - self.syntax_cache.clear(); + if self.theme != theme { + self.theme = theme; + self.highlighter = Highlighter::new(theme); + self.syntax_cache.clear(); + } true } else { @@ -130,6 +134,11 @@ impl<'a> SyntaxEditor<'a> { Color::rgb(0xFF, 0xFF, 0xFF) } } + + /// Get the current syntect theme + pub fn theme(&self) -> &SyntaxTheme { + self.theme + } } impl<'a> Edit for SyntaxEditor<'a> { @@ -157,16 +166,50 @@ impl<'a> Edit for SyntaxEditor<'a> { self.editor.set_select_opt(select_opt); } + 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) { #[cfg(feature = "std")] let now = std::time::Instant::now(); + let cursor = self.cursor(); let buffer = self.editor.buffer_mut(); - + let lines = buffer.visible_lines(); + let scroll_end = buffer.scroll() + lines; + let mut total_layout = 0; let mut highlighted = 0; for line_i in 0..buffer.lines.len() { + // Break out if we have reached the end of scroll and are past the cursor + if total_layout >= scroll_end && line_i > cursor.line { + break; + } + let line = &mut buffer.lines[line_i]; if !line.is_reset() && line_i < self.syntax_cache.len() { + //TODO: duplicated code! + // Perform shaping and layout of this line in order to count if we have reached scroll + match buffer.line_layout(font_system, line_i) { + Some(layout_lines) => { + total_layout += layout_lines.len() as i32; + } + None => { + //TODO: should this be possible? + } + } continue; } highlighted += 1; @@ -221,8 +264,15 @@ impl<'a> Edit for SyntaxEditor<'a> { line.set_attrs_list(attrs_list); line.set_wrap(Wrap::Word); - //TODO: efficiently do syntax highlighting without having to shape whole buffer - buffer.line_shape(font_system, line_i); + // Perform shaping and layout of this line in order to count if we have reached scroll + match buffer.line_layout(font_system, line_i) { + Some(layout_lines) => { + total_layout += layout_lines.len() as i32; + } + None => { + //TODO: should this be possible? + } + } let cache_item = (parse_state.clone(), highlight_state.clone()); if line_i < self.syntax_cache.len() { @@ -262,6 +312,18 @@ impl<'a> Edit for SyntaxEditor<'a> { self.editor.insert_string(data, attrs_list); } + 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 { + self.editor.finish_change() + } + fn action(&mut self, font_system: &mut FontSystem, action: Action) { self.editor.action(font_system, action); } diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 4d5fd89..db88372 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,35 +1,182 @@ use alloc::string::String; use core::cmp; +use modit::{Event, Key, Motion, Parser, TextObject, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ - Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, FontSystem, - SyntaxEditor, + Action, AttrsList, BorrowedWithFontSystem, Buffer, Change, Color, Cursor, Edit, FontSystem, + SyntaxEditor, SyntaxTheme, }; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum Mode { - Normal, - Insert, - Command, - Search, - SearchBackwards, +pub use modit::{ViMode, ViParser}; + +fn undo_2_action(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( + editor: &mut E, + commands: &mut cosmic_undo_2::Commands, + changed: &mut bool, +) { + //TODO: join changes together + match editor.finish_change() { + Some(change) => { + if !change.items.is_empty() { + commands.push(change); + *changed = true; + } + } + None => {} + } +} + +fn search(editor: &mut E, value: &str, forwards: bool) -> bool { + let mut cursor = editor.cursor(); + let start_line = cursor.line; + if forwards { + while cursor.line < editor.buffer().lines.len() { + if let Some(index) = editor.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.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(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 buffer = editor.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; + } + } + + editor.set_select_opt(Some(start)); + editor.set_cursor(end); } #[derive(Debug)] pub struct ViEditor<'a> { editor: SyntaxEditor<'a>, - mode: Mode, + parser: ViParser, + passthrough: bool, + search_opt: Option<(String, bool)>, + commands: cosmic_undo_2::Commands, + changed: bool, } impl<'a> ViEditor<'a> { pub fn new(editor: SyntaxEditor<'a>) -> Self { Self { editor, - mode: Mode::Normal, + parser: ViParser::new(), + passthrough: false, + 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>( @@ -50,6 +197,54 @@ impl<'a> ViEditor<'a> { pub fn foreground_color(&self) -> Color { self.editor.foreground_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.buffer_mut().set_redraw(true); + } + } + + /// Get current vi parser + pub fn parser(&self) -> &ViParser { + &self.parser + } + + /// Redo a change + pub fn redo(&mut self) { + log::info!("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::info!("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; + } + } } impl<'a> Edit for ViEditor<'a> { @@ -77,6 +272,22 @@ impl<'a> Edit for ViEditor<'a> { self.editor.set_select_opt(select_opt); } + 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) { self.editor.shape_as_needed(font_system); } @@ -93,143 +304,450 @@ impl<'a> Edit for ViEditor<'a> { self.editor.insert_string(data, attrs_list); } + 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 { + self.editor.finish_change() + } + fn action(&mut self, font_system: &mut FontSystem, action: Action) { - let old_mode = self.mode; + log::info!("Action {:?}", action); - match self.mode { - Mode::Normal => match action { - Action::Insert(c) => match c { - // Enter insert mode after cursor - 'a' => { - self.editor.action(font_system, Action::Right); - self.mode = Mode::Insert; - } - // Enter insert mode at end of line - 'A' => { - self.editor.action(font_system, Action::End); - self.mode = Mode::Insert; - } - // Change mode - 'c' => { - if self.editor.select_opt().is_some() { - self.editor.action(font_system, Action::Delete); - self.mode = Mode::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 - } - } - // Enter insert mode at cursor - 'i' => { - self.mode = Mode::Insert; - } - // Enter insert mode at start of line - 'I' => { - //TODO: soft home, skip whitespace - self.editor.action(font_system, Action::Home); - self.mode = Mode::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 = Mode::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 = Mode::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), - // 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); - } - } - // 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 - //TODO: implement this - '^' => self.editor.action(font_system, Action::Home), - // Enter command mode - ':' => { - self.mode = Mode::Command; - } - // Enter search mode - '/' => { - self.mode = Mode::Search; - } - // Enter search backwards mode - '?' => { - self.mode = Mode::SearchBackwards; - } - _ => (), - }, - _ => self.editor.action(font_system, action), - }, - Mode::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 = Mode::Normal; - } - _ => self.editor.action(font_system, 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::Down => Key::Down, + Action::End => Key::End, + Action::Enter => Key::Enter, + Action::Escape => Key::Escape, + Action::Home => Key::Home, + Action::Indent => Key::Tab, + Action::Insert(c) => Key::Char(c), + Action::Left => Key::Left, + Action::PageDown => Key::PageDown, + Action::PageUp => Key::PageUp, + Action::Right => Key::Right, + Action::Unindent => Key::Backtab, + Action::Up => Key::Up, _ => { - //TODO: other modes - self.mode = Mode::Normal; + log::info!("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; } - } + }; - if self.mode != old_mode { - self.buffer_mut().set_redraw(true); - } + self.parser.parse(key, false, |event| { + log::info!(" Event {:?}", event); + let action = match event { + Event::AutoIndent => { + log::info!("TODO"); + return; + } + Event::Backspace => Action::Backspace, + Event::ChangeStart => { + editor.start_change(); + return; + } + Event::ChangeFinish => { + finish_change(editor, &mut self.commands, &mut self.changed); + return; + } + Event::Copy => { + log::info!("TODO"); + return; + } + Event::Delete => Action::Delete, + Event::Escape => Action::Escape, + Event::Insert(c) => Action::Insert(c), + Event::NewLine => Action::Enter, + Event::Paste => { + log::info!("TODO"); + return; + } + Event::Redraw => { + editor.buffer_mut().set_redraw(true); + return; + } + Event::SelectClear => { + editor.set_select_opt(None); + return; + } + Event::SelectStart => { + let cursor = editor.cursor(); + editor.set_select_opt(Some(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_select_opt(Some(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 select_opt = editor.select_opt(); + let buffer = editor.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; + select_opt = Some(cursor); + cursor.index += w.len(); + } + None => { + //TODO + } + } + editor.set_select_opt(select_opt); + 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"); + return; + } + Event::Undo => { + for action in self.commands.undo() { + undo_2_action(editor, action); + } + return; + } + Event::Motion(motion) => { + match motion { + Motion::Around => { + //TODO: what to do for this psuedo-motion? + return; + } + Motion::Down => Action::Down, + Motion::End => Action::End, + Motion::GotoLine(line) => Action::GotoLine(line.saturating_sub(1)), + Motion::GotoEof => { + Action::GotoLine(editor.buffer().lines.len().saturating_sub(1)) + } + Motion::Home => Action::Home, + Motion::Inside => { + //TODO: what to do for this psuedo-motion? + return; + } + Motion::Left => Action::Left, + Motion::LeftInLine => { + let cursor = editor.cursor(); + if cursor.index > 0 { + Action::Left + } else { + return; + } + } + Motion::Line => { + //TODO: what to do for this psuedo-motion? + return; + } + Motion::NextChar(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + editor.set_cursor(cursor); + } + None => {} + } + } + return; + } + Motion::NextCharTill(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + editor.set_cursor(cursor); + break; + } else { + last_i = i; + } + } + } + return; + } + Motion::NextSearch => match &self.search_opt { + Some((value, forwards)) => { + search(editor, value, *forwards); + return; + } + None => return, + }, + Motion::NextWordEnd(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + } + Motion::NextWordStart(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + } + Motion::PageDown => Action::PageDown, + Motion::PageUp => Action::PageUp, + Motion::PreviousChar(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + editor.set_cursor(cursor); + } + None => {} + } + } + return; + } + Motion::PreviousCharTill(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + editor.set_cursor(cursor); + } + None => {} + } + } + return; + } + Motion::PreviousSearch => match &self.search_opt { + Some((value, forwards)) => { + search(editor, value, !*forwards); + return; + } + None => return, + }, + Motion::PreviousWordEnd(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + } + Motion::PreviousWordStart(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.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; + } + Motion::Right => Action::Right, + Motion::RightInLine => { + let cursor = editor.cursor(); + let buffer = editor.buffer(); + if cursor.index < buffer.lines[cursor.line].text().len() { + Action::Right + } else { + return; + } + } + Motion::ScreenHigh => { + //TODO: is this efficient? + if let Some(first) = editor.buffer().layout_runs().next() { + Action::GotoLine(first.line_i) + } else { + return; + } + } + Motion::ScreenLow => { + //TODO: is this efficient? + if let Some(last) = editor.buffer().layout_runs().last() { + Action::GotoLine(last.line_i) + } else { + return; + } + } + Motion::ScreenMiddle => { + //TODO: is this efficient? + let mut layout_runs = editor.buffer().layout_runs(); + if let Some(first) = layout_runs.next() { + if let Some(last) = layout_runs.last() { + Action::GotoLine((last.line_i + first.line_i) / 2) + } else { + return; + } + } else { + return; + } + } + Motion::Selection => { + //TODO: what to do for this psuedo-motion? + return; + } + Motion::SoftHome => Action::SoftHome, + Motion::Up => Action::Up, + } + } + }; + editor.action(font_system, action); + }); } #[cfg(feature = "swash")] @@ -242,12 +760,16 @@ impl<'a> Edit for ViEditor<'a> { ) 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 @@ -326,7 +848,7 @@ impl<'a> Edit for ViEditor<'a> { } else if let Some((min, max)) = range_opt.take() { f( min, - (line_y - font_size) as i32, + line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), @@ -352,7 +874,7 @@ impl<'a> Edit for ViEditor<'a> { } f( min, - (line_y - font_size) as i32, + line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), @@ -365,10 +887,13 @@ impl<'a> Edit for ViEditor<'a> { if let Some((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) = cursor_glyph_opt(&self.cursor()) { - let block_cursor = match self.mode { - Mode::Normal => true, - Mode::Insert => false, - _ => true, /*TODO: determine block cursor in other modes*/ + 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) { @@ -411,19 +936,13 @@ impl<'a> Edit for ViEditor<'a> { let right_x = cmp::max(start_x, end_x); f( left_x, - (line_y - font_size) as i32, + 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_y - font_size) as i32, - 1, - line_height as u32, - color, - ); + f(start_x, line_top as i32, 1, line_height as u32, color); } } diff --git a/src/lib.rs b/src/lib.rs index ac684b9..52b48f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,10 +61,10 @@ #![allow(clippy::new_without_default)] // TODO: address occurrences and then deny // +// Overflows can produce unpredictable results and are only checked in debug builds +#![allow(clippy::arithmetic_side_effects)] // Indexing a slice can cause panics and that is something we always want to avoid #![allow(clippy::indexing_slicing)] -// Overflows can produce unpredictable results and are only checked in debug builds -#![allow(clippy::integer_arithmetic)] // Soundness issues // // Dereferencing unaligned pointers may be undefined behavior diff --git a/src/shape.rs b/src/shape.rs index a1a16b8..cfc9da0 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -1252,12 +1252,10 @@ impl ShapeLine { layout_lines.push(LayoutLine { w: if align != Align::Justified { visual_line.w + } else if self.rtl { + start_x - x } else { - if self.rtl { - start_x - x - } else { - x - } + x }, max_ascent: max_ascent * font_size, max_descent: max_descent * font_size,