summaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs467
-rw-r--r--helix-term/src/compositor.rs14
-rw-r--r--helix-term/src/editor_view.rs311
-rw-r--r--helix-term/src/main.rs2
-rw-r--r--helix-term/src/prompt.rs72
-rw-r--r--helix-term/src/terminal.rs221
6 files changed, 655 insertions, 432 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 589aaf6e..7a74f8ba 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,13 +1,9 @@
-use crate::{
- commands,
- keymap::{self, Keymaps},
-};
use clap::ArgMatches as Args;
-use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
use helix_view::{document::Mode, Document, Editor, Theme, View};
use crate::compositor::{Component, Compositor, EventResult};
+use crate::editor_view::EditorView;
use crate::prompt::Prompt;
use log::{debug, info};
@@ -37,426 +33,54 @@ use tui::{
style::{Color, Modifier, Style},
};
-type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
-
-const BASE_WIDTH: u16 = 30;
+type Terminal = crate::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
-pub struct Application<'a> {
+pub struct Application {
compositor: Compositor,
editor: Editor,
- renderer: Renderer,
+ terminal: Terminal,
- executor: &'a smol::Executor<'a>,
+ executor: &'static smol::Executor<'static>,
language_server: helix_lsp::Client,
}
-pub struct Renderer {
- size: (u16, u16),
- terminal: Terminal,
- surface: Surface,
- cache: Surface,
- text_color: Style,
+// TODO: temp
+#[inline(always)]
+pub fn text_color() -> Style {
+ return Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
}
-impl Renderer {
- pub fn new() -> Result<Self, Error> {
+// pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
+// let mut stdout = stdout();
+// match view.doc.mode() {
+// Mode::Insert => write!(stdout, "\x1B[6 q"),
+// mode => write!(stdout, "\x1B[2 q"),
+// };
+// let pos = if let Some(prompt) = prompt {
+// Position::new(self.size.0 as usize, 2 + prompt.cursor)
+// } else {
+// let cursor = view.doc.state.selection().cursor();
+
+// let mut pos = view
+// .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
+// .expect("Cursor is out of bounds.");
+// pos.col += viewport.x as usize;
+// pos.row += viewport.y as usize;
+// pos
+// };
+
+// execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
+// }
+
+impl Application {
+ pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result<Self, Error> {
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
- let size = terminal::size().unwrap();
- let text_color: Style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
-
- let area = Rect::new(0, 0, size.0, size.1);
-
- Ok(Self {
- size,
- terminal,
- surface: Surface::empty(area),
- cache: Surface::empty(area),
- text_color,
- })
- }
-
- pub fn resize(&mut self, width: u16, height: u16) {
- self.size = (width, height);
- let area = Rect::new(0, 0, width, height);
- self.surface = Surface::empty(area);
- self.cache = Surface::empty(area);
- }
-
- pub fn render_view(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
- self.render_buffer(view, viewport, theme);
- self.render_statusline(view, theme);
- }
-
- // TODO: ideally not &mut View but highlights require it because of cursor cache
- pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
- let area = Rect::new(0, 0, self.size.0, self.size.1);
-
- // clear with background color
- self.surface.set_style(area, theme.get("ui.background"));
-
- // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
- let source_code = view.doc.text().to_string();
-
- let last_line = view.last_line();
-
- let range = {
- // calculate viewport byte ranges
- let start = view.doc.text().line_to_byte(view.first_line);
- let end = view.doc.text().line_to_byte(last_line)
- + view.doc.text().line(last_line).len_bytes();
-
- 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.doc.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
- .doc
- .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 start = view.doc.text().byte_to_char(start);
- let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
-
- let text = view.doc.text().slice(start..end);
-
- use helix_core::graphemes::{grapheme_width, RopeGraphemes};
-
- let style = match spans.first() {
- Some(span) => theme.get(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 = if visible_selections
- .iter()
- .any(|range| range.head == char_index)
- {
- style.clone().bg(Color::Rgb(255, 255, 255))
- } else {
- style
- };
-
- // ugh, improve with a traverse method
- // or interleave highlight spans with selection and diagnostic spans
- let style = if view.doc.diagnostics.iter().any(|diagnostic| {
- diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
- }) {
- style.clone().add_modifier(Modifier::UNDERLINED)
- } else {
- style
- };
-
- // TODO: paint cursor heads except primary
-
- self.surface.set_string(
- viewport.x + visual_x,
- viewport.y + line,
- grapheme,
- style,
- );
-
- visual_x += width;
- }
-
- char_index += 1;
- }
- }
- }
- }
-
- let style: Style = theme.get("ui.linenr");
- let warning: Style = theme.get("warning");
- let last_line = view.last_line();
- for (i, line) in (view.first_line..last_line).enumerate() {
- if view.doc.diagnostics.iter().any(|d| d.line == line) {
- self.surface.set_stringn(0, i as u16, "●", 1, warning);
- }
-
- self.surface
- .set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
- }
- }
-
- pub fn render_statusline(&mut self, view: &View, theme: &Theme) {
- let mode = match view.doc.mode() {
- Mode::Insert => "INS",
- Mode::Normal => "NOR",
- Mode::Goto => "GOTO",
- };
- // statusline
- self.surface.set_style(
- Rect::new(0, self.size.1 - 2, self.size.0, 1),
- theme.get("ui.statusline"),
- );
- self.surface
- .set_string(1, self.size.1 - 2, mode, self.text_color);
-
- if let Some(path) = view.doc.path() {
- self.surface
- .set_string(6, self.size.1 - 2, path.to_string_lossy(), self.text_color);
- }
-
- self.surface.set_string(
- self.size.0 - 10,
- self.size.1 - 2,
- format!("{}", view.doc.diagnostics.len()),
- self.text_color,
- );
- }
-
- pub fn render_prompt(&mut self, prompt: &Prompt, theme: &Theme) {
- // completion
- if !prompt.completion.is_empty() {
- // TODO: find out better way of clearing individual lines of the screen
- let mut row = 0;
- let mut col = 0;
- let max_col = self.size.0 / BASE_WIDTH;
- let col_height = ((prompt.completion.len() as u16 + max_col - 1) / max_col);
-
- for i in (3..col_height + 3) {
- self.surface.set_string(
- 0,
- self.size.1 - i as u16,
- " ".repeat(self.size.0 as usize),
- self.text_color,
- );
- }
- self.surface.set_style(
- Rect::new(0, self.size.1 - col_height - 2, self.size.0, col_height),
- theme.get("ui.statusline"),
- );
- for (i, command) in prompt.completion.iter().enumerate() {
- let color = if prompt.completion_selection_index.is_some()
- && i == prompt.completion_selection_index.unwrap()
- {
- Style::default().bg(Color::Rgb(104, 060, 232))
- } else {
- self.text_color
- };
- self.surface.set_stringn(
- 1 + col * BASE_WIDTH,
- self.size.1 - col_height - 2 + row,
- &command,
- BASE_WIDTH as usize - 1,
- color,
- );
- row += 1;
- if row > col_height - 1 {
- row = 0;
- col += 1;
- }
- if col > max_col {
- break;
- }
- }
- }
- // render buffer text
- self.surface
- .set_string(1, self.size.1 - 1, &prompt.prompt, self.text_color);
- self.surface
- .set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
- }
-
- pub fn draw_and_swap(&mut self) {
- use tui::backend::Backend;
- // TODO: theres probably a better place for this
- self.terminal
- .backend_mut()
- .draw(self.cache.diff(&self.surface).into_iter());
- // swap the buffer
- std::mem::swap(&mut self.surface, &mut self.cache);
- self.surface.reset(); // reset is faster than allocating new empty surface
- }
-
- pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
- let mut stdout = stdout();
- match view.doc.mode() {
- Mode::Insert => write!(stdout, "\x1B[6 q"),
- mode => write!(stdout, "\x1B[2 q"),
- };
- let pos = if let Some(prompt) = prompt {
- Position::new(self.size.0 as usize, 2 + prompt.cursor)
- } else {
- let cursor = view.doc.state.selection().cursor();
-
- let mut pos = view
- .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
- .expect("Cursor is out of bounds.");
- pos.col += viewport.x as usize;
- pos.row += viewport.y as usize;
- pos
- };
-
- execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
- }
-}
-
-struct EditorView {
- keymap: Keymaps,
-}
-
-impl EditorView {
- fn new() -> Self {
- Self {
- keymap: keymap::default(),
- }
- }
-}
-
-use crate::compositor::Context;
-
-impl Component for EditorView {
- fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
- match event {
- Event::Resize(width, height) => {
- // TODO: simplistic ensure cursor in view for now
- // TODO: loop over views
- if let Some(view) = cx.editor.view_mut() {
- view.size = (width, height);
- view.ensure_cursor_in_view()
- };
- EventResult::Consumed(None)
- }
- Event::Key(event) => {
- if let Some(view) = cx.editor.view_mut() {
- let keys = vec![event];
- // TODO: sequences (`gg`)
- let mode = view.doc.mode();
- // TODO: handle count other than 1
- let mut cx = commands::Context {
- view,
- executor: cx.executor,
- count: 1,
- callback: None,
- };
-
- match mode {
- Mode::Insert => {
- if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
- command(&mut cx);
- } else if let KeyEvent {
- code: KeyCode::Char(c),
- ..
- } = event
- {
- commands::insert::insert_char(&mut cx, c);
- }
- }
- mode => {
- if let Some(command) = self.keymap[&mode].get(&keys) {
- command(&mut cx);
-
- // TODO: simplistic ensure cursor in view for now
- }
- }
- }
- // appease borrowck
- let callback = cx.callback.take();
-
- view.ensure_cursor_in_view();
-
- EventResult::Consumed(callback)
- } else {
- EventResult::Ignored
- }
- }
- Event::Mouse(_) => EventResult::Ignored,
- }
- }
- fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) {
- const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
- let viewport = Rect::new(OFFSET, 0, renderer.size.0, renderer.size.1 - 2); // - 2 for statusline and prompt
-
- // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
- // theme. Theme is immutable mutating view won't disrupt theme_ref.
- let theme_ref = unsafe { &*(&cx.editor.theme as *const Theme) };
- if let Some(view) = cx.editor.view_mut() {
- renderer.render_view(view, viewport, theme_ref);
- }
-
- // TODO: drop unwrap
- renderer.render_cursor(cx.editor.view().unwrap(), None, viewport);
- }
-}
-
-impl<'a> Application<'a> {
- pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
- let renderer = Renderer::new()?;
let mut editor = Editor::new();
+ let size = terminal.size()?;
if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
- editor.open(file, renderer.size)?;
+ editor.open(file, (size.width, size.height))?;
}
let mut compositor = Compositor::new();
@@ -466,7 +90,7 @@ impl<'a> Application<'a> {
let mut app = Self {
editor,
- renderer,
+ terminal,
// TODO; move to state
compositor,
@@ -478,12 +102,17 @@ impl<'a> Application<'a> {
}
fn render(&mut self) {
- let mut cx = crate::compositor::Context {
- editor: &mut self.editor,
- executor: &self.executor,
- };
- self.compositor.render(&mut self.renderer, &mut cx); // viewport,
- self.renderer.draw_and_swap();
+ let executor = &self.executor;
+ let editor = &mut self.editor;
+ let compositor = &self.compositor;
+
+ // TODO: should be unnecessary
+ // self.terminal.autoresize();
+ let mut cx = crate::compositor::Context { editor, executor };
+ let area = self.terminal.size().unwrap();
+ compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
+
+ self.terminal.draw();
}
pub async fn event_loop(&mut self) {
@@ -524,7 +153,7 @@ impl<'a> Application<'a> {
// Handle key events
let should_redraw = match event {
Some(Ok(Event::Resize(width, height))) => {
- self.renderer.resize(width, height);
+ self.terminal.resize(Rect::new(0, 0, width, height));
self.compositor
.handle_event(Event::Resize(width, height), &mut cx)
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 3cf6bf03..1d94ee63 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -13,10 +13,10 @@
// Q: how does this work with popups?
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
-use crate::application::Renderer;
use crossterm::event::Event;
use smol::Executor;
use tui::buffer::Buffer as Surface;
+use tui::layout::Rect;
pub type Callback = Box<dyn Fn(&mut Compositor)>;
@@ -36,9 +36,9 @@ pub enum EventResult {
use helix_view::{Editor, View};
// shared with commands.rs
-pub struct Context<'a, 'b> {
+pub struct Context<'a> {
pub editor: &'a mut Editor,
- pub executor: &'a smol::Executor<'b>,
+ pub executor: &'static smol::Executor<'static>,
}
pub trait Component {
@@ -51,7 +51,7 @@ pub trait Component {
true
}
- fn render(&mut self, renderer: &mut Renderer, ctx: &mut Context);
+ fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
}
// struct Editor { };
@@ -133,9 +133,9 @@ impl Compositor {
false
}
- pub fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) {
- for layer in &mut self.layers {
- layer.render(renderer, cx)
+ pub fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ for layer in &self.layers {
+ layer.render(area, surface, cx)
}
}
}
diff --git a/helix-term/src/editor_view.rs b/helix-term/src/editor_view.rs
new file mode 100644
index 00000000..0181623a
--- /dev/null
+++ b/helix-term/src/editor_view.rs
@@ -0,0 +1,311 @@
+use crate::application::text_color;
+use crate::commands;
+use crate::compositor::{Component, Compositor, EventResult};
+use crate::keymap::{self, Keymaps};
+use crossterm::{
+ cursor,
+ event::{read, Event, EventStream, KeyCode, KeyEvent},
+};
+use helix_view::{document::Mode, Document, Editor, Theme, View};
+use std::borrow::Cow;
+use tui::{
+ backend::CrosstermBackend,
+ buffer::Buffer as Surface,
+ layout::Rect,
+ style::{Color, Modifier, Style},
+};
+
+use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
+
+pub struct EditorView {
+ keymap: Keymaps,
+}
+
+impl EditorView {
+ pub fn new() -> Self {
+ Self {
+ keymap: keymap::default(),
+ }
+ }
+ pub fn render_view(
+ &self,
+ view: &mut View,
+ viewport: Rect,
+ surface: &mut Surface,
+ theme: &Theme,
+ ) {
+ const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
+ let area = Rect::new(OFFSET, 0, viewport.width - OFFSET, viewport.height - 2); // - 2 for statusline and prompt
+ self.render_buffer(view, area, surface, theme);
+ let area = Rect::new(0, viewport.height - 2, viewport.width, 1);
+ self.render_statusline(view, viewport, surface, theme);
+ }
+
+ // TODO: ideally not &mut View but highlights require it because of cursor cache
+ pub fn render_buffer(
+ &self,
+ view: &mut View,
+ viewport: Rect,
+ surface: &mut Surface,
+ theme: &Theme,
+ ) {
+ // clear with background color
+ surface.set_style(viewport, theme.get("ui.background"));
+
+ // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
+ let source_code = view.doc.text().to_string();
+
+ let last_line = view.last_line();
+
+ let range = {
+ // calculate viewport byte ranges
+ let start = view.doc.text().line_to_byte(view.first_line);
+ let end = view.doc.text().line_to_byte(last_line)
+ + view.doc.text().line(last_line).len_bytes();
+
+ 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.doc.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
+ .doc
+ .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 start = view.doc.text().byte_to_char(start);
+ let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
+
+ let text = view.doc.text().slice(start..end);
+
+ use helix_core::graphemes::{grapheme_width, RopeGraphemes};
+
+ let style = match spans.first() {
+ Some(span) => theme.get(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 = if visible_selections
+ .iter()
+ .any(|range| range.head == char_index)
+ {
+ style.clone().bg(Color::Rgb(255, 255, 255))
+ } else {
+ style
+ };
+
+ // ugh, improve with a traverse method
+ // or interleave highlight spans with selection and diagnostic spans
+ let style = if view.doc.diagnostics.iter().any(|diagnostic| {
+ diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
+ }) {
+ style.clone().add_modifier(Modifier::UNDERLINED)
+ } else {
+ style
+ };
+
+ // TODO: paint cursor heads except primary
+
+ surface.set_string(
+ viewport.x + visual_x,
+ viewport.y + line,
+ grapheme,
+ style,
+ );
+
+ visual_x += width;
+ }
+
+ char_index += 1;
+ }
+ }
+ }
+ }
+
+ let style: Style = theme.get("ui.linenr");
+ let warning: Style = theme.get("warning");
+ let last_line = view.last_line();
+ for (i, line) in (view.first_line..last_line).enumerate() {
+ if view.doc.diagnostics.iter().any(|d| d.line == line) {
+ surface.set_stringn(0, i as u16, "●", 1, warning);
+ }
+
+ surface.set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
+ }
+ }
+
+ pub fn render_statusline(
+ &self,
+ view: &View,
+ viewport: Rect,
+ surface: &mut Surface,
+ theme: &Theme,
+ ) {
+ let mode = match view.doc.mode() {
+ Mode::Insert => "INS",
+ Mode::Normal => "NOR",
+ Mode::Goto => "GOTO",
+ };
+ // statusline
+ surface.set_style(
+ Rect::new(0, viewport.y, viewport.height, 1),
+ theme.get("ui.statusline"),
+ );
+ surface.set_string(1, viewport.y, mode, text_color());
+
+ if let Some(path) = view.doc.path() {
+ surface.set_string(6, viewport.y, path.to_string_lossy(), text_color());
+ }
+
+ surface.set_string(
+ viewport.width - 10,
+ viewport.y,
+ format!("{}", view.doc.diagnostics.len()),
+ text_color(),
+ );
+ }
+}
+
+use crate::compositor::Context;
+
+impl Component for EditorView {
+ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
+ match event {
+ Event::Resize(width, height) => {
+ // TODO: simplistic ensure cursor in view for now
+ // TODO: loop over views
+ if let Some(view) = cx.editor.view_mut() {
+ view.size = (width, height);
+ view.ensure_cursor_in_view()
+ };
+ EventResult::Consumed(None)
+ }
+ Event::Key(event) => {
+ if let Some(view) = cx.editor.view_mut() {
+ let keys = vec![event];
+ // TODO: sequences (`gg`)
+ let mode = view.doc.mode();
+ // TODO: handle count other than 1
+ let mut cx = commands::Context {
+ view,
+ executor: cx.executor,
+ count: 1,
+ callback: None,
+ };
+
+ match mode {
+ Mode::Insert => {
+ if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
+ command(&mut cx);
+ } else if let KeyEvent {
+ code: KeyCode::Char(c),
+ ..
+ } = event
+ {
+ commands::insert::insert_char(&mut cx, c);
+ }
+ }
+ mode => {
+ if let Some(command) = self.keymap[&mode].get(&keys) {
+ command(&mut cx);
+
+ // TODO: simplistic ensure cursor in view for now
+ }
+ }
+ }
+ // appease borrowck
+ let callback = cx.callback.take();
+
+ view.ensure_cursor_in_view();
+
+ EventResult::Consumed(callback)
+ } else {
+ EventResult::Ignored
+ }
+ }
+ Event::Mouse(_) => EventResult::Ignored,
+ }
+ }
+
+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
+ // theme. Theme is immutable mutating view won't disrupt theme_ref.
+ let theme_ref = unsafe { &*(&cx.editor.theme as *const Theme) };
+ if let Some(view) = cx.editor.view_mut() {
+ self.render_view(view, area, surface, theme_ref);
+ }
+
+ // TODO: drop unwrap
+ // TODO: !!! self.render_cursor(cx.editor.view().unwrap(), None, viewport);
+ }
+}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 92ab10c2..63fbe52d 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -3,8 +3,10 @@
mod application;
mod commands;
mod compositor;
+mod editor_view;
mod keymap;
mod prompt;
+mod terminal;
use application::Application;
diff --git a/helix-term/src/prompt.rs b/helix-term/src/prompt.rs
index 689eac66..4747c9f5 100644
--- a/helix-term/src/prompt.rs
+++ b/helix-term/src/prompt.rs
@@ -1,9 +1,7 @@
-use crate::{
- application::Renderer,
- compositor::{Component, Context, EventResult},
-};
+use crate::compositor::{Component, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use helix_view::Editor;
+use helix_view::Theme;
use std::string::String;
pub struct Prompt {
@@ -85,6 +83,68 @@ impl Prompt {
}
}
+use tui::{
+ buffer::Buffer as Surface,
+ layout::Rect,
+ style::{Color, Modifier, Style},
+};
+
+const BASE_WIDTH: u16 = 30;
+use crate::application::text_color;
+
+impl Prompt {
+ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, theme: &Theme) {
+ // completion
+ if !self.completion.is_empty() {
+ // TODO: find out better way of clearing individual lines of the screen
+ let mut row = 0;
+ let mut col = 0;
+ let max_col = area.width / BASE_WIDTH;
+ let col_height = ((self.completion.len() as u16 + max_col - 1) / max_col);
+
+ for i in (3..col_height + 3) {
+ surface.set_string(
+ 0,
+ area.height - i as u16,
+ " ".repeat(area.width as usize),
+ text_color(),
+ );
+ }
+ surface.set_style(
+ Rect::new(0, area.height - col_height - 2, area.width, col_height),
+ theme.get("ui.statusline"),
+ );
+ for (i, command) in self.completion.iter().enumerate() {
+ let color = if self.completion_selection_index.is_some()
+ && i == self.completion_selection_index.unwrap()
+ {
+ Style::default().bg(Color::Rgb(104, 060, 232))
+ } else {
+ text_color()
+ };
+ surface.set_stringn(
+ 1 + col * BASE_WIDTH,
+ area.height - col_height - 2 + row,
+ &command,
+ BASE_WIDTH as usize - 1,
+ color,
+ );
+ row += 1;
+ if row > col_height - 1 {
+ row = 0;
+ col += 1;
+ }
+ if col > max_col {
+ break;
+ }
+ }
+ }
+ // render buffer text
+ surface.set_string(1, area.height - 1, &self.prompt, text_color());
+ surface.set_string(2, area.height - 1, &self.line, text_color());
+ }
+}
+
impl Component for Prompt {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let event = match event {
@@ -137,7 +197,7 @@ impl Component for Prompt {
EventResult::Consumed(None)
}
- fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) {
- renderer.render_prompt(self, &cx.editor.theme)
+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ self.render_prompt(area, surface, &cx.editor.theme)
}
}
diff --git a/helix-term/src/terminal.rs b/helix-term/src/terminal.rs
new file mode 100644
index 00000000..e40343bd
--- /dev/null
+++ b/helix-term/src/terminal.rs
@@ -0,0 +1,221 @@
+use std::io;
+use tui::{
+ backend::Backend,
+ buffer::Buffer,
+ layout::Rect,
+ widgets::{StatefulWidget, Widget},
+};
+
+#[derive(Debug, Clone, PartialEq)]
+/// UNSTABLE
+enum ResizeBehavior {
+ Fixed,
+ Auto,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+/// UNSTABLE
+pub struct Viewport {
+ area: Rect,
+ resize_behavior: ResizeBehavior,
+}
+
+impl Viewport {
+ /// UNSTABLE
+ pub fn fixed(area: Rect) -> Viewport {
+ Viewport {
+ area,
+ resize_behavior: ResizeBehavior::Fixed,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+/// Options to pass to [`Terminal::with_options`]
+pub struct TerminalOptions {
+ /// Viewport used to draw to the terminal
+ pub viewport: Viewport,
+}
+
+/// Interface to the terminal backed by Termion
+#[derive(Debug)]
+pub struct Terminal<B>
+where
+ B: Backend,
+{
+ backend: B,
+ /// Holds the results of the current and previous draw calls. The two are compared at the end
+ /// of each draw pass to output the necessary updates to the terminal
+ buffers: [Buffer; 2],
+ /// Index of the current buffer in the previous array
+ current: usize,
+ /// Whether the cursor is currently hidden
+ hidden_cursor: bool,
+ /// Viewport
+ viewport: Viewport,
+}
+
+impl<B> Drop for Terminal<B>
+where
+ B: Backend,
+{
+ fn drop(&mut self) {
+ // Attempt to restore the cursor state
+ if self.hidden_cursor {
+ if let Err(err) = self.show_cursor() {
+ eprintln!("Failed to show the cursor: {}", err);
+ }
+ }
+ }
+}
+
+impl<B> Terminal<B>
+where
+ B: Backend,
+{
+ /// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
+ /// default colors for the foreground and the background
+ pub fn new(backend: B) -> io::Result<Terminal<B>> {
+ let size = backend.size()?;
+ Terminal::with_options(
+ backend,
+ TerminalOptions {
+ viewport: Viewport {
+ area: size,
+ resize_behavior: ResizeBehavior::Auto,
+ },
+ },
+ )
+ }
+
+ /// UNSTABLE
+ pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
+ Ok(Terminal {
+ backend,
+ buffers: [
+ Buffer::empty(options.viewport.area),
+ Buffer::empty(options.viewport.area),
+ ],
+ current: 0,
+ hidden_cursor: false,
+ viewport: options.viewport,
+ })
+ }
+
+ // /// Get a Frame object which provides a consistent view into the terminal state for rendering.
+ // pub fn get_frame(&mut self) -> Frame<B> {
+ // Frame {
+ // terminal: self,
+ // cursor_position: None,
+ // }
+ // }
+
+ pub fn current_buffer_mut(&mut self) -> &mut Buffer {
+ &mut self.buffers[self.current]
+ }
+
+ pub fn backend(&self) -> &B {
+ &self.backend
+ }
+
+ pub fn backend_mut(&mut self) -> &mut B {
+ &mut self.backend
+ }
+
+ /// Obtains a difference between the previous and the current buffer and passes it to the
+ /// current backend for drawing.
+ pub fn flush(&mut self) -> io::Result<()> {
+ let previous_buffer = &self.buffers[1 - self.current];
+ let current_buffer = &self.buffers[self.current];
+ let updates = previous_buffer.diff(current_buffer);
+ self.backend.draw(updates.into_iter())
+ }
+
+ /// Updates the Terminal so that internal buffers match the requested size. Requested size will
+ /// be saved so the size can remain consistent when rendering.
+ /// This leads to a full clear of the screen.
+ pub fn resize(&mut self, area: Rect) -> io::Result<()> {
+ self.buffers[self.current].resize(area);
+ self.buffers[1 - self.current].resize(area);
+ self.viewport.area = area;
+ self.clear()
+ }
+
+ /// Queries the backend for size and resizes if it doesn't match the previous size.
+ pub fn autoresize(&mut self) -> io::Result<()> {
+ if self.viewport.resize_behavior == ResizeBehavior::Auto {
+ let size = self.size()?;
+ if size != self.viewport.area {
+ self.resize(size)?;
+ }
+ };
+ Ok(())
+ }
+
+ /// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
+ /// and prepares for the next draw call.
+ pub fn draw(&mut self) -> io::Result<()> {
+ // // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
+ // // and the terminal (if growing), which may OOB.
+ // self.autoresize()?;
+
+ // let mut frame = self.get_frame();
+ // f(&mut frame);
+ // // We can't change the cursor position right away because we have to flush the frame to
+ // // stdout first. But we also can't keep the frame around, since it holds a &mut to
+ // // Terminal. Thus, we're taking the important data out of the Frame and dropping it.
+ // let cursor_position = frame.cursor_position;
+
+ // Draw to stdout
+ self.flush()?;
+
+ // match cursor_position {
+ // None => self.hide_cursor()?,
+ // Some((x, y)) => {
+ // self.show_cursor()?;
+ // self.set_cursor(x, y)?;
+ // }
+ // }
+
+ // Swap buffers
+ self.buffers[1 - self.current].reset();
+ self.current = 1 - self.current;
+
+ // Flush
+ self.backend.flush()?;
+ Ok(())
+ }
+
+ pub fn hide_cursor(&mut self) -> io::Result<()> {
+ self.backend.hide_cursor()?;
+ self.hidden_cursor = true;
+ Ok(())
+ }
+
+ pub fn show_cursor(&mut self) -> io::Result<()> {
+ self.backend.show_cursor()?;
+ self.hidden_cursor = false;
+ Ok(())
+ }
+
+ pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
+ self.backend.get_cursor()
+ }
+
+ pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
+ self.backend.set_cursor(x, y)
+ }
+
+ /// Clear the terminal and force a full redraw on the next draw call.
+ pub fn clear(&mut self) -> io::Result<()> {
+ self.backend.clear()?;
+ // Reset the back buffer to make sure the next update will redraw everything.
+ self.buffers[1 - self.current].reset();
+ Ok(())
+ }
+
+ /// Queries the real size of the backend.
+ pub fn size(&self) -> io::Result<Rect> {
+ self.backend.size()
+ }
+}