Vi-style editor and other editor improvements (#40)

* WIP VI wrapper for editor

* WIP: block cursor

* Create Edit trait, run CI on all feature options

* Add prints describing build steps to ci.sh

* Custom rendering for Vi editor

* Clippy fixes

* More clippy fixes

* Show clippy results in CI

* Fix for Redox

* Fix clippy lint

* Add vi feature to enable vi-style editor

* Add escape to libcosmic text box
This commit is contained in:
Jeremy Soller 2022-11-15 12:26:59 -07:00 committed by GitHub
parent 271ca5cf7a
commit ee54e7626b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 982 additions and 616 deletions

671
src/edit/editor.rs Normal file
View file

@ -0,0 +1,671 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
use core::cmp;
use unicode_segmentation::UnicodeSegmentation;
use crate::{Action, AttrsList, Buffer, BufferLine, Cursor, Edit, LayoutCursor};
#[cfg(feature = "swash")]
use crate::Color;
/// A wrapper of [`Buffer`] for easy editing
pub struct Editor<'a> {
buffer: Buffer<'a>,
cursor: Cursor,
cursor_x_opt: Option<i32>,
select_opt: Option<Cursor>,
cursor_moved: bool,
}
impl<'a> Editor<'a> {
/// Create a new [`Editor`] with the provided [`Buffer`]
pub fn new(buffer: Buffer<'a>) -> Self {
Self {
buffer,
cursor: Cursor::default(),
cursor_x_opt: None,
select_opt: None,
cursor_moved: false,
}
}
fn set_layout_cursor(&mut self, cursor: LayoutCursor) {
let layout = self.buffer.line_layout(cursor.line).expect("layout not found");
let layout_line = match layout.get(cursor.layout) {
Some(some) => some,
None => match layout.last() {
Some(some) => some,
None => todo!("layout cursor in line with no layouts"),
}
};
let new_index = match layout_line.glyphs.get(cursor.glyph) {
Some(glyph) => glyph.start,
None => match layout_line.glyphs.last() {
Some(glyph) => glyph.end,
//TODO: is this correct?
None => 0,
}
};
if self.cursor.line != cursor.line || self.cursor.index != new_index {
self.cursor.line = cursor.line;
self.cursor.index = new_index;
self.buffer.set_redraw(true);
}
}
}
impl<'a> Edit<'a> for Editor<'a> {
fn buffer(&self) -> &Buffer<'a> {
&self.buffer
}
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
&mut self.buffer
}
fn cursor(&self) -> Cursor {
self.cursor
}
fn select_opt(&self) -> Option<Cursor> {
self.select_opt
}
fn shape_as_needed(&mut self) {
if self.cursor_moved {
self.buffer.shape_until_cursor(self.cursor);
self.cursor_moved = false;
} else {
self.buffer.shape_until_scroll();
}
}
fn copy_selection(&mut self) -> Option<String> {
let 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)
}
}
};
let mut selection = String::new();
// Take the selection from the first line
{
// Add selected part of line to string
if start.line == end.line {
selection.push_str(&self.buffer.lines[start.line].text()[start.index..end.index]);
} else {
selection.push_str(&self.buffer.lines[start.line].text()[start.index..]);
selection.push('\n');
}
}
// Take the selection from all interior lines (if they exist)
for line_i in start.line + 1..end.line {
selection.push_str(self.buffer.lines[line_i].text());
selection.push('\n');
}
// Take the selection from the last line
if end.line > start.line {
// Add selected part of line to string
selection.push_str(&self.buffer.lines[end.line].text()[..end.index]);
}
Some(selection)
}
fn delete_selection(&mut self) -> bool {
let select = match self.select_opt.take() {
Some(some) => some,
None => return false,
};
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)
}
}
};
// 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);
}
}
true
}
fn action(&mut self, action: Action) {
let old_cursor = self.cursor;
match action {
Action::Previous => {
let line = &mut self.buffer.lines[self.cursor.line];
if self.cursor.index > 0 {
// Find previous character index
let mut prev_index = 0;
for (i, _) in line.text().grapheme_indices(true) {
if i < self.cursor.index {
prev_index = i;
} else {
break;
}
}
self.cursor.index = prev_index;
self.buffer.set_redraw(true);
} else if self.cursor.line > 0 {
self.cursor.line -= 1;
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
self.buffer.set_redraw(true);
}
self.cursor_x_opt = None;
},
Action::Next => {
let line = &mut 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 {
self.cursor.index += c.len();
self.buffer.set_redraw(true);
break;
}
}
} else if self.cursor.line + 1 < self.buffer.lines.len() {
self.cursor.line += 1;
self.cursor.index = 0;
self.buffer.set_redraw(true);
}
self.cursor_x_opt = None;
},
Action::Left => {
let rtl_opt = self.buffer.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl);
if let Some(rtl) = rtl_opt {
if rtl {
self.action(Action::Next);
} else {
self.action(Action::Previous);
}
}
},
Action::Right => {
let rtl_opt = self.buffer.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl);
if let Some(rtl) = rtl_opt {
if rtl {
self.action(Action::Previous);
} else {
self.action(Action::Next);
}
}
},
Action::Up => {
//TODO: make this preserve X as best as possible!
let mut cursor = self.buffer.layout_cursor(&self.cursor);
if self.cursor_x_opt.is_none() {
self.cursor_x_opt = Some(
cursor.glyph as i32 //TODO: glyph x position
);
}
if cursor.layout > 0 {
cursor.layout -= 1;
} else if cursor.line > 0 {
cursor.line -= 1;
cursor.layout = usize::max_value();
}
if let Some(cursor_x) = self.cursor_x_opt {
cursor.glyph = cursor_x as usize; //TODO: glyph x position
}
self.set_layout_cursor(cursor);
},
Action::Down => {
//TODO: make this preserve X as best as possible!
let mut cursor = self.buffer.layout_cursor(&self.cursor);
let layout_len = self.buffer.line_layout(cursor.line).expect("layout not found").len();
if self.cursor_x_opt.is_none() {
self.cursor_x_opt = Some(
cursor.glyph as i32 //TODO: glyph x position
);
}
if cursor.layout + 1 < layout_len {
cursor.layout += 1;
} else if cursor.line + 1 < self.buffer.lines.len() {
cursor.line += 1;
cursor.layout = 0;
}
if let Some(cursor_x) = self.cursor_x_opt {
cursor.glyph = cursor_x as usize; //TODO: glyph x position
}
self.set_layout_cursor(cursor);
},
Action::Home => {
let mut cursor = self.buffer.layout_cursor(&self.cursor);
cursor.glyph = 0;
self.set_layout_cursor(cursor);
self.cursor_x_opt = None;
},
Action::End => {
let mut cursor = self.buffer.layout_cursor(&self.cursor);
cursor.glyph = usize::max_value();
self.set_layout_cursor(cursor);
self.cursor_x_opt = None;
}
Action::PageUp => {
//TODO: move cursor
let mut scroll = self.buffer.scroll();
scroll -= self.buffer.visible_lines();
self.buffer.set_scroll(scroll);
},
Action::PageDown => {
//TODO: move cursor
let mut scroll = self.buffer.scroll();
scroll += self.buffer.visible_lines();
self.buffer.set_scroll(scroll);
},
Action::Escape => {
if self.select_opt.take().is_some() {
self.buffer.set_redraw(true);
}
},
Action::Insert(character) => {
if character.is_control()
&& !['\t', '\u{92}'].contains(&character)
{
// Filter out special chars (except for tab), use Action instead
log::debug!("Refusing to insert control character {:?}", character);
} else {
self.delete_selection();
let line = &mut self.buffer.lines[self.cursor.line];
// Collect text after insertion as a line
let after = line.split_off(self.cursor.index);
// Append the inserted text
line.append(BufferLine::new(
character.to_string(),
AttrsList::new(line.attrs_list().defaults() /*TODO: provide attrs?*/)
));
// Append the text after insertion
line.append(after);
self.cursor.index += character.len_utf8();
}
},
Action::Enter => {
self.delete_selection();
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);
},
Action::Backspace => {
if self.delete_selection() {
// Deleted selection
} else if self.cursor.index > 0 {
let line = &mut self.buffer.lines[self.cursor.line];
// 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;
}
}
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);
}
},
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];
let range_opt = line
.text()
.grapheme_indices(true)
.take_while(|(i, _)| *i <= self.cursor.index)
.last()
.map(|(i, c)| {
i..(i + c.len())
});
if let Some(range) = range_opt {
self.cursor.index = range.start;
// 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);
}
} 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);
}
},
Action::Click { x, y } => {
self.select_opt = None;
if let Some(new_cursor) = self.buffer.hit(x, y) {
if new_cursor != self.cursor {
self.cursor = new_cursor;
self.buffer.set_redraw(true);
}
}
},
Action::Drag { x, y } => {
if self.select_opt.is_none() {
self.select_opt = Some(self.cursor);
self.buffer.set_redraw(true);
}
if let Some(new_cursor) = self.buffer.hit(x, y) {
if new_cursor != self.cursor {
self.cursor = new_cursor;
self.buffer.set_redraw(true);
}
}
},
Action::Scroll { lines } => {
let mut scroll = self.buffer.scroll();
scroll += lines;
self.buffer.set_scroll(scroll);
}
}
if old_cursor != self.cursor {
self.cursor_moved = true;
/*TODO
if let Some(glyph) = run.glyphs.get(new_cursor_glyph) {
let font_opt = self.buffer.font_system().get_font(glyph.cache_key.font_id);
let text_glyph = &run.text[glyph.start..glyph.end];
log::debug!(
"{}, {}: '{}' ('{}'): '{}' ({:?})",
self.cursor.line,
self.cursor.index,
font_opt.as_ref().map_or("?", |font| font.info.family.as_str()),
font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()),
text_glyph,
text_glyph
);
}
*/
}
}
/// Draw the editor
#[cfg(feature = "swash")]
fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
where F: FnMut(i32, i32, u32, u32, 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 cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32)> {
if cursor.line == line_i {
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
if cursor.index == glyph.start {
return Some((glyph_i, 0.0));
} else 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 offset = glyph.w * (before as f32) / (total as f32);
return Some((glyph_i, offset));
}
}
match run.glyphs.last() {
Some(glyph) => {
if cursor.index == glyph.end {
return Some((run.glyphs.len(), 0.0));
}
},
None => {
return Some((0, 0.0));
}
}
}
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_y - font_size,
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));
}
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;
}
}
f(
min,
line_y - font_size,
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_opt(&self.cursor) {
let x = match run.glyphs.get(cursor_glyph) {
Some(glyph) => {
// Start of detected glyph
if glyph.rtl {
(glyph.x + glyph.w - cursor_glyph_offset) as i32
} else {
(glyph.x + cursor_glyph_offset) as i32
}
},
None => match run.glyphs.last() {
Some(glyph) => {
// End of last glyph
if glyph.rtl {
glyph.x as i32
} else {
(glyph.x + glyph.w) as i32
}
},
None => {
// Start of empty line
0
}
}
};
f(
x,
line_y - font_size,
1,
line_height as u32,
color,
);
}
for glyph in run.glyphs.iter() {
let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
let glyph_color = match glyph.color_opt {
Some(some) => some,
None => color,
};
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
f(x_int + x, line_y + y_int + y, 1, 1, color);
});
}
}
}
}

93
src/edit/mod.rs Normal file
View file

@ -0,0 +1,93 @@
#[cfg(not(feature = "std"))]
use alloc::string::String;
use crate::{Buffer, Cursor};
#[cfg(feature = "swash")]
use crate::Color;
pub use self::editor::*;
mod editor;
#[cfg(feature = "syntect")]
pub use self::syntect::*;
#[cfg(feature = "syntect")]
mod syntect;
#[cfg(feature = "vi")]
pub use self::vi::*;
#[cfg(feature = "vi")]
mod vi;
/// An action to perform on an [`Editor`]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
/// Move cursor to previous character ([Self::Left] in LTR, [Self::Right] in RTL)
Previous,
/// Move cursor to next character ([Self::Right] in LTR, [Self::Left] in RTL)
Next,
/// Move cursor left
Left,
/// Move cursor right
Right,
/// Move cursor up
Up,
/// Move cursor down
Down,
/// Move cursor to start of line
Home,
/// Move cursor to end of line
End,
/// Scroll up one page
PageUp,
/// Scroll down one page
PageDown,
/// Escape, clears selection
Escape,
/// Insert character at cursor
Insert(char),
/// Create new line
Enter,
/// Delete text behind cursor
Backspace,
/// Delete text in front of cursor
Delete,
/// Mouse click at specified position
Click { x: i32, y: i32 },
/// Mouse drag to specified position
Drag { x: i32, y: i32 },
/// Scroll specified number of lines
Scroll { lines: i32 },
}
/// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor`
pub trait Edit<'a> {
/// Get the internal [`Buffer`]
fn buffer(&self) -> &Buffer<'a>;
/// Get the internal [`Buffer`], mutably
fn buffer_mut(&mut self) -> &mut Buffer<'a>;
/// Get the current cursor position
fn cursor(&self) -> Cursor;
/// Get the current selection position
fn select_opt(&self) -> Option<Cursor>;
/// Shape lines until scroll, after adjusting scroll if the cursor moved
fn shape_as_needed(&mut self);
/// Copy selection
fn copy_selection(&mut self) -> Option<String>;
/// Delete selection, adjusting cursor and returning true if there was a selection
// Also used by backspace, delete, insert, and enter when there is a selection
fn delete_selection(&mut self) -> bool;
/// Perform an [Action] on the editor
fn action(&mut self, action: Action);
/// Draw the editor
#[cfg(feature = "swash")]
fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, f: F)
where F: FnMut(i32, i32, u32, u32, Color);
}

272
src/edit/syntect.rs Normal file
View file

@ -0,0 +1,272 @@
#[cfg(not(feature = "std"))]
use alloc::{
string::String,
vec::Vec,
};
#[cfg(feature = "std")]
use std::{
fs,
io,
path::Path,
};
use syntect::highlighting::{
FontStyle,
Highlighter,
HighlightState,
RangedHighlightIterator,
Theme,
ThemeSet,
};
use syntect::parsing::{
ParseState,
ScopeStack,
SyntaxReference,
SyntaxSet,
};
use crate::{
Action,
AttrsList,
Buffer,
Color,
Cursor,
Edit,
Editor,
Style,
Weight,
};
pub struct SyntaxSystem {
pub syntax_set: SyntaxSet,
pub theme_set: ThemeSet,
}
impl SyntaxSystem {
/// Create a new [`SyntaxSystem`]
pub fn new() -> Self {
Self {
//TODO: store newlines in buffer
syntax_set: SyntaxSet::load_defaults_nonewlines(),
theme_set: ThemeSet::load_defaults(),
}
}
}
/// A wrapper of [`Editor`] with syntax highlighting provided by [`SyntaxSystem`]
pub struct SyntaxEditor<'a> {
editor: Editor<'a>,
syntax_system: &'a SyntaxSystem,
syntax: &'a SyntaxReference,
theme: &'a Theme,
highlighter: Highlighter<'a>,
syntax_cache: Vec<(ParseState, HighlightState)>,
}
impl<'a> SyntaxEditor<'a> {
/// Create a new [`SyntaxEditor`] with the provided [`Buffer`], [`SyntaxSystem`], and theme name.
///
/// A good default theme name is "base16-eighties.dark".
///
/// Returns None if theme not found
pub fn new(buffer: Buffer<'a>, syntax_system: &'a SyntaxSystem, theme_name: &str) -> Option<Self> {
let editor = Editor::new(buffer);
let syntax = syntax_system.syntax_set.find_syntax_plain_text();
let theme = syntax_system.theme_set.themes.get(theme_name)?;
let highlighter = Highlighter::new(theme);
Some(Self {
editor,
syntax_system,
syntax,
theme,
highlighter,
syntax_cache: Vec::new(),
})
}
/// Load text from a file, and also set syntax to the best option
///
/// ## Errors
///
/// Returns an [`io::Error`] if reading the file fails
#[cfg(feature = "std")]
pub fn load_text<P: AsRef<Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> io::Result<()> {
let path = path.as_ref();
let text = fs::read_to_string(path)?;
self.editor.buffer_mut().set_text(&text, attrs);
//TODO: re-use text
self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) {
Ok(Some(some)) => some,
Ok(None) => {
log::warn!("no syntax found for {:?}", path);
self.syntax_system.syntax_set.find_syntax_plain_text()
}
Err(err) => {
log::warn!("failed to determine syntax for {:?}: {:?}", path, err);
self.syntax_system.syntax_set.find_syntax_plain_text()
}
};
// Clear syntax cache
self.syntax_cache.clear();
Ok(())
}
/// Get the default background color
pub fn background_color(&self) -> Color {
if let Some(background) = self.theme.settings.background {
Color::rgba(
background.r,
background.g,
background.b,
background.a,
)
} else {
Color::rgb(0, 0, 0)
}
}
/// Get the default foreground (text) color
pub fn foreground_color(&self) -> Color {
if let Some(foreground) = self.theme.settings.foreground {
Color::rgba(
foreground.r,
foreground.g,
foreground.b,
foreground.a,
)
} else {
Color::rgb(0xFF, 0xFF, 0xFF)
}
}
}
impl<'a> Edit<'a> for SyntaxEditor<'a> {
fn buffer(&self) -> &Buffer<'a> {
self.editor.buffer()
}
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
self.editor.buffer_mut()
}
fn cursor(&self) -> Cursor {
self.editor.cursor()
}
fn select_opt(&self) -> Option<Cursor> {
self.editor.select_opt()
}
fn shape_as_needed(&mut self) {
#[cfg(feature = "std")]
let now = std::time::Instant::now();
let buffer = self.editor.buffer_mut();
let mut highlighted = 0;
for line_i in 0..buffer.lines.len() {
let line = &mut buffer.lines[line_i];
if ! line.is_reset() && line_i < self.syntax_cache.len() {
continue;
}
highlighted += 1;
let (mut parse_state, mut highlight_state) = if line_i > 0 && line_i <= self.syntax_cache.len() {
self.syntax_cache[line_i - 1].clone()
} else {
(
ParseState::new(self.syntax),
HighlightState::new(&self.highlighter, ScopeStack::new())
)
};
let ops = parse_state.parse_line(line.text(), &self.syntax_system.syntax_set).expect("failed to parse syntax");
let ranges = RangedHighlightIterator::new(
&mut highlight_state,
&ops,
line.text(),
&self.highlighter,
);
let attrs = line.attrs_list().defaults();
let mut attrs_list = AttrsList::new(attrs);
for (style, _, range) in ranges {
attrs_list.add_span(
range,
attrs
.color(Color::rgba(
style.foreground.r,
style.foreground.g,
style.foreground.b,
style.foreground.a,
))
//TODO: background
.style(if style.font_style.contains(FontStyle::ITALIC) {
Style::Italic
} else {
Style::Normal
})
.weight(if style.font_style.contains(FontStyle::BOLD) {
Weight::BOLD
} else {
Weight::NORMAL
})
//TODO: underline
);
}
// Update line attributes. This operation only resets if the line changes
line.set_attrs_list(attrs_list);
line.set_wrap_simple(true);
//TODO: efficiently do syntax highlighting without having to shape whole buffer
buffer.line_shape(line_i);
let cache_item = (parse_state.clone(), highlight_state.clone());
if line_i < self.syntax_cache.len() {
if self.syntax_cache[line_i] != cache_item {
self.syntax_cache[line_i] = cache_item;
if line_i + 1 < buffer.lines.len() {
buffer.lines[line_i + 1].reset();
}
}
} else {
self.syntax_cache.push(cache_item);
}
}
if highlighted > 0 {
buffer.set_redraw(true);
#[cfg(feature = "std")]
log::debug!("Syntax highlighted {} lines in {:?}", highlighted, now.elapsed());
}
self.editor.shape_as_needed();
}
fn copy_selection(&mut self) -> Option<String> {
self.editor.copy_selection()
}
fn delete_selection(&mut self) -> bool {
self.editor.delete_selection()
}
fn action(&mut self, action: Action) {
self.editor.action(action);
}
/// Draw the editor
#[cfg(feature = "swash")]
fn draw<F>(&self, 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());
self.editor.draw(cache, self.foreground_color(), f);
}
}

393
src/edit/vi.rs Normal file
View file

@ -0,0 +1,393 @@
use alloc::string::String;
use core::cmp;
use unicode_segmentation::UnicodeSegmentation;
use crate::{
Action,
Buffer,
Color,
Cursor,
Edit,
SyntaxEditor,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Mode {
Normal,
Insert,
Command,
Search,
SearchBackwards,
}
pub struct ViEditor<'a> {
editor: SyntaxEditor<'a>,
mode: Mode,
}
impl<'a> ViEditor<'a> {
pub fn new(editor: SyntaxEditor<'a>) -> Self {
Self {
editor,
mode: Mode::Normal,
}
}
/// Load text from a file, and also set syntax to the best option
#[cfg(feature = "std")]
pub fn load_text<P: AsRef<std::path::Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> std::io::Result<()> {
self.editor.load_text(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()
}
}
impl<'a> Edit<'a> for ViEditor<'a> {
fn buffer(&self) -> &Buffer<'a> {
self.editor.buffer()
}
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
self.editor.buffer_mut()
}
fn cursor(&self) -> Cursor {
self.editor.cursor()
}
fn select_opt(&self) -> Option<Cursor> {
self.editor.select_opt()
}
fn shape_as_needed(&mut self) {
self.editor.shape_as_needed()
}
fn copy_selection(&mut self) -> Option<String> {
self.editor.copy_selection()
}
fn delete_selection(&mut self) -> bool {
self.editor.delete_selection()
}
fn action(&mut self, action: Action) {
let old_mode = self.mode;
match self.mode {
Mode::Normal => match action {
Action::Insert(c) => match c {
// Enter insert mode after cursor
'a' => {
self.editor.action(Action::Right);
self.mode = Mode::Insert;
},
// Enter insert mode at end of line
'A' => {
self.editor.action(Action::End);
self.mode = Mode::Insert;
},
// 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(Action::Home);
self.mode = Mode::Insert;
}
// Create line after and enter insert mode
'o' => {
self.editor.action(Action::End);
self.editor.action(Action::Enter);
self.mode = Mode::Insert;
},
// Create line before and enter insert mode
'O' => {
self.editor.action(Action::Home);
self.editor.action(Action::Enter);
self.editor.shape_as_needed(); // TODO: do not require this?
self.editor.action(Action::Up);
self.mode = Mode::Insert;
},
// Left
'h' => self.editor.action(Action::Left),
// Top of screen
//TODO: 'H' => self.editor.action(Action::ScreenHigh),
// Down
'j' => self.editor.action(Action::Down),
// Up
'k' => self.editor.action(Action::Up),
// Right
'l' => self.editor.action(Action::Right),
// Bottom of screen
//TODO: 'L' => self.editor.action(Action::ScreenLow),
// Middle of screen
//TODO: 'M' => self.editor.action(Action::ScreenMiddle),
// Remove character at cursor
'x' => self.editor.action(Action::Delete),
// Remove character before cursor
'X' => self.editor.action(Action::Backspace),
// Go to start of line
'0' => self.editor.action(Action::Home),
// Go to end of line
'$' => self.editor.action(Action::End),
// Go to start of line after whitespace
//TODO: implement this
'^' => self.editor.action(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(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(Action::Left);
}
self.mode = Mode::Normal;
},
_ => self.editor.action(action),
},
_ => {
//TODO: other modes
self.mode = Mode::Normal;
},
}
if self.mode != old_mode {
self.buffer_mut().set_redraw(true);
}
}
#[cfg(feature = "swash")]
fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
where F: FnMut(i32, i32, u32, u32, 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 cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32, f32)> {
//TODO: better calculation of width
let default_width = (font_size as f32) / 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_y - font_size,
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));
}
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;
}
}
f(
min,
line_y - font_size,
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 {
Mode::Normal => true,
Mode::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.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.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_y - font_size,
(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,
1,
line_height as u32,
color,
);
}
}
for glyph in run.glyphs.iter() {
let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
let glyph_color = match glyph.color_opt {
Some(some) => some,
None => color,
};
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
f(x_int + x, line_y + y_int + y, 1, 1, color)
});
}
}
}
}