aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/editor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/editor.rs')
-rw-r--r--helix-term/src/editor.rs442
1 files changed, 214 insertions, 228 deletions
diff --git a/helix-term/src/editor.rs b/helix-term/src/editor.rs
index 81012156..819196f5 100644
--- a/helix-term/src/editor.rs
+++ b/helix-term/src/editor.rs
@@ -4,7 +4,7 @@ use helix_view::{commands, keymap, View};
use std::{
borrow::Cow,
- io::{self, stdout, Write},
+ io::{self, stdout, Stdout, Write},
path::PathBuf,
time::Duration,
};
@@ -24,6 +24,8 @@ use crossterm::{
use tui::{backend::CrosstermBackend, buffer::Buffer as Surface, layout::Rect, style::Style};
+const OFFSET: u16 = 6; // 5 linenr + 1 gutter
+
type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
static EX: smol::Executor = smol::Executor::new();
@@ -68,232 +70,235 @@ impl Editor {
use tui::backend::Backend;
use tui::style::Color;
// TODO: ideally not mut but highlights require it because of cursor cache
- match &mut self.view {
- Some(view) => {
- let area = Rect::new(0, 0, self.size.0, self.size.1);
- let mut stdout = stdout();
- self.surface.reset(); // reset is faster than allocating new empty surface
+ let viewport = Rect::new(OFFSET, 0, self.size.0, self.size.1 - 1); // - 1 for statusline
+ let text_color: Style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
- // clear with background color
- self.surface
- .set_style(area, view.theme.get("ui.background"));
+ self.render_view(viewport);
- let offset = 5 + 1; // 5 linenr + 1 gutter
- let viewport = Rect::new(offset, 0, self.size.0, self.size.1 - 1); // - 1 for statusline
+ self.render_prompt(text_color);
- // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
- let source_code = view.state.doc().to_string();
+ self.render_cursor(viewport, text_color);
+ }
- let last_line = view.last_line();
+ pub fn render_view(&mut self, viewport: Rect) {
+ use tui::style::Color;
+ let area = Rect::new(0, 0, self.size.0, self.size.1);
+ let mut view: &mut View = self.view.as_mut().unwrap();
+ self.surface.reset(); // reset is faster than allocating new empty surface
- let range = {
- // calculate viewport byte ranges
- let start = view.state.doc().line_to_byte(view.first_line);
- let end = view.state.doc().line_to_byte(last_line)
- + view.state.doc().line(last_line).len_bytes();
+ // clear with background color
+ self.surface
+ .set_style(area, view.theme.get("ui.background"));
- start..end
- };
+ // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
+ let source_code = view.state.doc().to_string();
- // TODO: range doesn't actually restrict source, just highlight range
+ let last_line = view.last_line();
- // TODO: cache highlight results
- // TODO: only recalculate when state.doc is actually modified
- let highlights: Vec<_> = match view.state.syntax.as_mut() {
- Some(syntax) => {
- syntax
- .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
- .unwrap()
- .collect() // TODO: we collect here to avoid double borrow, fix later
- }
- None => vec![Ok(HighlightEvent::Source {
- start: range.start,
- end: range.end,
- })],
- };
-
- let mut spans = Vec::new();
-
- let mut visual_x = 0;
- let mut line = 0u16;
-
- let visible_selections: Vec<Range> = view
- .state
- .selection()
- .ranges()
- .iter()
- // TODO: limit selection to one in viewport
- // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
- .copied()
- .collect();
-
- 'outer: for event in highlights {
- match event.unwrap() {
- HighlightEvent::HighlightStart(span) => {
- spans.push(span);
- }
- HighlightEvent::HighlightEnd => {
- spans.pop();
- }
- HighlightEvent::Source { start, end } => {
- // TODO: filter out spans out of viewport for now..
+ let range = {
+ // calculate viewport byte ranges
+ let start = view.state.doc().line_to_byte(view.first_line);
+ let end = view.state.doc().line_to_byte(last_line)
+ + view.state.doc().line(last_line).len_bytes();
- let start = view.state.doc().byte_to_char(start);
- let end = view.state.doc().byte_to_char(end); // <-- index 744, len 743
+ start..end
+ };
- let text = view.state.doc().slice(start..end);
+ // TODO: range doesn't actually restrict source, just highlight range
+ // TODO: cache highlight results
+ // TODO: only recalculate when state.doc is actually modified
+ let highlights: Vec<_> = match view.state.syntax.as_mut() {
+ Some(syntax) => {
+ syntax
+ .highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
+ .unwrap()
+ .collect() // TODO: we collect here to avoid double borrow, fix later
+ }
+ None => vec![Ok(HighlightEvent::Source {
+ start: range.start,
+ end: range.end,
+ })],
+ };
+ let mut spans = Vec::new();
+ let mut visual_x = 0;
+ let mut line = 0u16;
+ let visible_selections: Vec<Range> = view
+ .state
+ .selection()
+ .ranges()
+ .iter()
+ // TODO: limit selection to one in viewport
+ // .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
+ .copied()
+ .collect();
+
+ 'outer: for event in highlights {
+ match event.unwrap() {
+ HighlightEvent::HighlightStart(span) => {
+ spans.push(span);
+ }
+ HighlightEvent::HighlightEnd => {
+ spans.pop();
+ }
+ HighlightEvent::Source { start, end } => {
+ // TODO: filter out spans out of viewport for now..
- use helix_core::graphemes::{grapheme_width, RopeGraphemes};
+ let start = view.state.doc().byte_to_char(start);
+ let end = view.state.doc().byte_to_char(end); // <-- index 744, len 743
- let style = match spans.first() {
- Some(span) => view.theme.get(view.theme.scopes()[span.0].as_str()),
- None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
- };
+ let text = view.state.doc().slice(start..end);
- // TODO: we could render the text to a surface, then cache that, that
- // way if only the selection/cursor changes we can copy from cache
- // and paint the new cursor.
-
- let mut char_index = start;
-
- // iterate over range char by char
- for grapheme in RopeGraphemes::new(&text) {
- // TODO: track current char_index
-
- if grapheme == "\n" {
- visual_x = 0;
- line += 1;
-
- // TODO: with proper iter this shouldn't be necessary
- if line >= viewport.height {
- break 'outer;
- }
- } else if grapheme == "\t" {
- visual_x += (TAB_WIDTH as u16);
- } else {
- // Cow will prevent allocations if span contained in a single slice
- // which should really be the majority case
- let grapheme = Cow::from(grapheme);
- let width = grapheme_width(&grapheme) as u16;
-
- // TODO: this should really happen as an after pass
- let style = if visible_selections
- .iter()
- .any(|range| range.contains(char_index))
- {
- // cedar
- style.clone().bg(Color::Rgb(128, 47, 0))
- } else {
- style
- };
-
- let style = if visible_selections
- .iter()
- .any(|range| range.head == char_index)
- {
- style.clone().bg(Color::Rgb(255, 255, 255))
- } else {
- style
- };
-
- // TODO: paint cursor heads except primary
-
- self.surface.set_string(
- offset + visual_x,
- line,
- grapheme,
- style,
- );
-
- visual_x += width;
- }
- // if grapheme == "\t"
+ use helix_core::graphemes::{grapheme_width, RopeGraphemes};
- char_index += 1;
+ let style = match spans.first() {
+ Some(span) => view.theme.get(view.theme.scopes()[span.0].as_str()),
+ None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
+ };
+
+ // TODO: we could render the text to a surface, then cache that, that
+ // way if only the selection/cursor changes we can copy from cache
+ // and paint the new cursor.
+
+ let mut char_index = start;
+
+ // iterate over range char by char
+ for grapheme in RopeGraphemes::new(&text) {
+ // TODO: track current char_index
+
+ if grapheme == "\n" {
+ visual_x = 0;
+ line += 1;
+
+ // TODO: with proper iter this shouldn't be necessary
+ if line >= viewport.height {
+ break 'outer;
}
- }
- }
- }
+ } else if grapheme == "\t" {
+ visual_x += (TAB_WIDTH as u16);
+ } else {
+ // Cow will prevent allocations if span contained in a single slice
+ // which should really be the majority case
+ let grapheme = Cow::from(grapheme);
+ let width = grapheme_width(&grapheme) as u16;
+
+ // TODO: this should really happen as an after pass
+ let style = if visible_selections
+ .iter()
+ .any(|range| range.contains(char_index))
+ {
+ // cedar
+ style.clone().bg(Color::Rgb(128, 47, 0))
+ } else {
+ style
+ };
- let style: Style = view.theme.get("ui.linenr");
- for (i, line) in (view.first_line..last_line).enumerate() {
- self.surface
- .set_stringn(0, i as u16, format!("{:>5}", line + 1), 5, style);
- // lavender
- }
+ let style = if visible_selections
+ .iter()
+ .any(|range| range.head == char_index)
+ {
+ style.clone().bg(Color::Rgb(255, 255, 255))
+ } else {
+ style
+ };
- // // iterate over selections and render them
- // for range in state.selection.ranges() {
- // // get terminal coords for x,y for each range pos
- // // TODO: this won't work with multiline
- // let (y1, x1) = coords_at_pos(&text, range.from());
- // let (y2, x2) = coords_at_pos(&text, range.to());
- // let area = Rect::new(
- // (x1 + 2) as u16,
- // y1 as u16,
- // (x2 - x1 + 1) as u16,
- // (y2 - y1 + 1) as u16,
- // );
- // self.surface.set_style(area, select);
- // }
-
- // statusline
- let mode = match view.state.mode() {
- Mode::Insert => "INS",
- Mode::Normal => "NOR",
- Mode::Goto => "GOTO",
- Mode::Command => ":",
- };
- self.surface.set_style(
- Rect::new(0, self.size.1 - 1, self.size.0, 1),
- view.theme.get("ui.statusline"),
- );
- // TODO: unfocused one with different color
- let text_color = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
- self.surface
- .set_string(1, self.size.1 - 1, mode, text_color);
-
- // set cursor shape
- match view.state.mode() {
- Mode::Insert => write!(stdout, "\x1B[6 q"),
- Mode::Normal => write!(stdout, "\x1B[2 q"),
- Mode::Goto => write!(stdout, "\x1B[2 q"),
- Mode::Command => write!(stdout, "\x1B[2 q"),
- };
-
- // render the cursor
- let mut pos: Position;
- if view.state.mode() == Mode::Command {
- pos = Position::new(self.size.0 as usize, 2);
- } else {
- if let Some(path) = view.state.path() {
- self.surface.set_string(
- 6,
- self.size.1 - 1,
- path.to_string_lossy(),
- text_color,
- );
- }
+ // TODO: paint cursor heads except primary
+
+ self.surface
+ .set_string(OFFSET + visual_x, line, grapheme, style);
- let cursor = view.state.selection().cursor();
+ visual_x += width;
+ }
+ // if grapheme == "\t"
- pos = view
- .screen_coords_at_pos(&view.state.doc().slice(..), cursor)
- .expect("Cursor is out of bounds.");
- pos.col += viewport.x as usize;
- pos.row += viewport.y as usize;
+ char_index += 1;
+ }
}
+ }
+ }
+ }
- self.terminal
- .backend_mut()
- .draw(self.cache.diff(&self.surface).into_iter());
- // swap the buffer
- std::mem::swap(&mut self.surface, &mut self.cache);
+ pub fn render_prompt(&mut self, text_color: Style) {
+ use tui::backend::Backend;
+ let view = self.view.as_ref().unwrap();
+ let style: Style = view.theme.get("ui.linenr");
+ let last_line = view.last_line();
+ for (i, line) in (view.first_line..last_line).enumerate() {
+ self.surface
+ .set_stringn(0, i as u16, format!("{:>5}", line + 1), 5, style);
+ // lavender
+ }
- execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
+ // let lines = state
+ // .doc
+ // .lines_at(self.first_line as usize)
+ // .take(self.size.1 as usize)
+ // .map(|x| x.as_str().unwrap());
+
+ // // iterate over selections and render them
+ // let select = Style::default().bg(tui::style::Color::LightBlue);
+ // let text = state.doc.slice(..);
+ // for range in state.selection.ranges() {
+ // // get terminal coords for x,y for each range pos
+ // // TODO: this won't work with multiline
+ // let (y1, x1) = coords_at_pos(&text, range.from());
+ // let (y2, x2) = coords_at_pos(&text, range.to());
+ // let area = Rect::new(
+ // (x1 + 2) as u16,
+ // y1 as u16,
+ // (x2 - x1 + 1) as u16,
+ // (y2 - y1 + 1) as u16,
+ // );
+ // self.surface.set_style(area, select);
+
+ // // TODO: don't highlight next char in append mode
+ // }
+
+ // statusline
+ let mode = match view.state.mode() {
+ Mode::Insert => "INS",
+ Mode::Normal => "NOR",
+ Mode::Goto => "GOTO",
+ Mode::Command => ":",
+ };
+ self.surface.set_style(
+ Rect::new(0, self.size.1 - 1, self.size.0, 1),
+ view.theme.get("ui.statusline"),
+ );
+ self.surface
+ .set_string(1, self.size.1 - 1, mode, text_color);
+ self.terminal
+ .backend_mut()
+ .draw(self.cache.diff(&self.surface).into_iter());
+ // swap the buffer
+ std::mem::swap(&mut self.surface, &mut self.cache);
+ }
+
+ pub fn render_cursor(&mut self, viewport: Rect, text_color: Style) {
+ let mut pos: Position;
+ let view = self.view.as_ref().unwrap();
+ let mut stdout = stdout();
+ match view.state.mode() {
+ Mode::Insert => write!(stdout, "\x1B[6 q"),
+ mode => write!(stdout, "\x1B[2 q"),
+ };
+ if view.state.mode() == Mode::Command {
+ pos = Position::new(self.size.0 as usize, 2);
+ } else {
+ if let Some(path) = view.state.path() {
+ self.surface
+ .set_string(6, self.size.1 - 1, path.to_string_lossy(), text_color);
}
- None => (),
+
+ let cursor = view.state.selection().cursor();
+
+ pos = view
+ .screen_coords_at_pos(&view.state.doc().slice(..), cursor)
+ .expect("Cursor is out of bounds.");
+ pos.col += viewport.x as usize;
+ pos.row += viewport.y as usize;
}
+
+ execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
}
pub async fn event_loop(&mut self) {
@@ -331,9 +336,9 @@ impl Editor {
// TODO: sequences (`gg`)
// TODO: handle count other than 1
if let Some(view) = &mut self.view {
+ let keys = vec![event];
match view.state.mode() {
Mode::Insert => {
- let keys = vec![event];
if let Some(command) = keymap[&Mode::Insert].get(&keys) {
command(view, 1);
} else if let KeyEvent {
@@ -344,44 +349,25 @@ impl Editor {
commands::insert::insert_char(view, c);
}
view.ensure_cursor_in_view();
-
self.render();
}
- Mode::Normal => {
- let keys = vec![event];
- if let Some(command) = keymap[&Mode::Normal].get(&keys) {
+ Mode::Command => {
+ if let Some(command) = keymap[&Mode::Command].get(&keys) {
command(view, 1);
-
- // TODO: simplistic ensure cursor in view for now
- view.ensure_cursor_in_view();
-
self.render();
}
}
- Mode::Goto => {
- // TODO: handle modes and sequences (`gg`)
- let keys = vec![event];
- if let Some(command) = keymap[&Mode::Goto].get(&keys) {
- // TODO: handle count other than 1
+ mode => {
+ if let Some(command) = keymap[&mode].get(&keys) {
command(view, 1);
// TODO: simplistic ensure cursor in view for now
view.ensure_cursor_in_view();
-
- self.render();
- }
- }
- Mode::Command => {
- // TODO: handle modes and sequences (`gg`)
- let keys = vec![event];
- if let Some(command) = keymap[&Mode::Goto].get(&keys) {
- // TODO: handle count other than 1
- command(view, 1);
-
self.render();
}
}
}
+ self.render();
}
}
Some(Ok(_)) => {