From 7dc24a25ba148a9cd7c936e02cc03873ed6a467b Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 13 Dec 2020 13:35:30 +0900 Subject: Move ui modules under a ui:: namespace. --- helix-term/src/application.rs | 33 ++--- helix-term/src/commands.rs | 2 +- helix-term/src/editor_view.rs | 327 ------------------------------------------ helix-term/src/helix.log | 0 helix-term/src/main.rs | 3 +- helix-term/src/prompt.rs | 211 --------------------------- helix-term/src/ui/editor.rs | 327 ++++++++++++++++++++++++++++++++++++++++++ helix-term/src/ui/mod.rs | 14 ++ helix-term/src/ui/prompt.rs | 212 +++++++++++++++++++++++++++ 9 files changed, 564 insertions(+), 565 deletions(-) delete mode 100644 helix-term/src/editor_view.rs create mode 100644 helix-term/src/helix.log delete mode 100644 helix-term/src/prompt.rs create mode 100644 helix-term/src/ui/editor.rs create mode 100644 helix-term/src/ui/mod.rs create mode 100644 helix-term/src/ui/prompt.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c25871c7..dc37612a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -2,9 +2,8 @@ use clap::ArgMatches as Args; 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 crate::compositor::Compositor; +use crate::ui; use log::{debug, info}; @@ -19,18 +18,11 @@ use smol::prelude::*; use anyhow::Error; use crossterm::{ - cursor, - event::{read, Event, EventStream, KeyCode, KeyEvent}, - execute, queue, - terminal::{self, disable_raw_mode, enable_raw_mode}, + event::{Event, EventStream}, + execute, terminal, }; -use tui::{ - backend::CrosstermBackend, - buffer::Buffer as Surface, - layout::Rect, - style::{Color, Modifier, Style}, -}; +use tui::{backend::CrosstermBackend, layout::Rect}; type Terminal = crate::terminal::Terminal>; @@ -43,12 +35,6 @@ pub struct Application { language_server: helix_lsp::Client, } -// TODO: temp -#[inline(always)] -pub fn text_color() -> Style { - Style::default().fg(Color::Rgb(219, 191, 239)) // lilac -} - impl Application { pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result { let backend = CrosstermBackend::new(stdout()); @@ -61,14 +47,13 @@ impl Application { } let mut compositor = Compositor::new(); - compositor.push(Box::new(EditorView::new())); + compositor.push(Box::new(ui::EditorView::new())); let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]); let mut app = Self { editor, terminal, - // TODO; move to state compositor, executor, @@ -213,7 +198,7 @@ impl Application { } pub async fn run(&mut self) -> Result<(), Error> { - enable_raw_mode()?; + terminal::enable_raw_mode()?; let mut stdout = stdout(); @@ -223,7 +208,7 @@ impl Application { let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { execute!(std::io::stdout(), terminal::LeaveAlternateScreen); - disable_raw_mode(); + terminal::disable_raw_mode(); hook(info); })); @@ -234,7 +219,7 @@ impl Application { execute!(stdout, terminal::LeaveAlternateScreen)?; - disable_raw_mode()?; + terminal::disable_raw_mode()?; Ok(()) } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 04482ef7..b345d2e8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,7 +10,7 @@ use helix_core::{ use once_cell::sync::Lazy; use crate::compositor::Compositor; -use crate::prompt::Prompt; +use crate::ui::Prompt; use helix_view::{ document::Mode, diff --git a/helix-term/src/editor_view.rs b/helix-term/src/editor_view.rs deleted file mode 100644 index b778e79b..00000000 --- a/helix-term/src/editor_view.rs +++ /dev/null @@ -1,327 +0,0 @@ -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, -} - -const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter - -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, - ) { - 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, area, 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 = 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.width, 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 - } - - fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option { - // match view.doc.mode() { - // Mode::Insert => write!(stdout, "\x1B[6 q"), - // mode => write!(stdout, "\x1B[2 q"), - // }; - let view = ctx.editor.view().unwrap(); - 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 += area.x as usize + OFFSET as usize; - pos.row += area.y as usize; - Some(pos) - } -} diff --git a/helix-term/src/helix.log b/helix-term/src/helix.log new file mode 100644 index 00000000..e69de29b diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 63fbe52d..f350b4c1 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -3,10 +3,9 @@ mod application; mod commands; mod compositor; -mod editor_view; mod keymap; -mod prompt; mod terminal; +mod ui; use application::Application; diff --git a/helix-term/src/prompt.rs b/helix-term/src/prompt.rs deleted file mode 100644 index 7f473ebc..00000000 --- a/helix-term/src/prompt.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::compositor::{Component, Context, EventResult}; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; -use helix_core::Position; -use helix_view::Editor; -use helix_view::Theme; -use std::string::String; - -pub struct Prompt { - pub prompt: String, - pub line: String, - pub cursor: usize, - pub completion: Vec, - pub should_close: bool, - pub completion_selection_index: Option, - completion_fn: Box Vec>, - callback_fn: Box, -} - -impl Prompt { - pub fn new( - prompt: String, - mut completion_fn: impl FnMut(&str) -> Vec + 'static, - callback_fn: impl FnMut(&mut Editor, &str) + 'static, - ) -> Prompt { - Prompt { - prompt, - line: String::new(), - cursor: 0, - completion: completion_fn(""), - should_close: false, - completion_selection_index: None, - completion_fn: Box::new(completion_fn), - callback_fn: Box::new(callback_fn), - } - } - - pub fn insert_char(&mut self, c: char) { - self.line.insert(self.cursor, c); - self.cursor += 1; - self.completion = (self.completion_fn)(&self.line); - self.exit_selection(); - } - - pub fn move_char_left(&mut self) { - if self.cursor > 0 { - self.cursor -= 1; - } - } - - pub fn move_char_right(&mut self) { - if self.cursor < self.line.len() { - self.cursor += 1; - } - } - - pub fn move_start(&mut self) { - self.cursor = 0; - } - - pub fn move_end(&mut self) { - self.cursor = self.line.len(); - } - - pub fn delete_char_backwards(&mut self) { - if self.cursor > 0 { - self.line.remove(self.cursor - 1); - self.cursor -= 1; - self.completion = (self.completion_fn)(&self.line); - } - self.exit_selection(); - } - - pub fn change_completion_selection(&mut self) { - if self.completion.is_empty() { - return; - } - let index = - self.completion_selection_index.map(|i| i + 1).unwrap_or(0) % self.completion.len(); - self.completion_selection_index = Some(index); - self.line = self.completion[index].clone(); - } - pub fn exit_selection(&mut self) { - self.completion_selection_index = None; - } -} - -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 { - Event::Key(event) => event, - _ => return EventResult::Ignored, - }; - - match event { - KeyEvent { - code: KeyCode::Char(c), - modifiers: KeyModifiers::NONE, - } => self.insert_char(c), - KeyEvent { - code: KeyCode::Esc, .. - } => self.should_close = true, - KeyEvent { - code: KeyCode::Right, - .. - } => self.move_char_right(), - KeyEvent { - code: KeyCode::Left, - .. - } => self.move_char_left(), - KeyEvent { - code: KeyCode::Char('e'), - modifiers: KeyModifiers::CONTROL, - } => self.move_end(), - KeyEvent { - code: KeyCode::Char('a'), - modifiers: KeyModifiers::CONTROL, - } => self.move_start(), - KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::NONE, - } => self.delete_char_backwards(), - KeyEvent { - code: KeyCode::Enter, - .. - } => (self.callback_fn)(cx.editor, &self.line), - KeyEvent { - code: KeyCode::Tab, .. - } => self.change_completion_selection(), - KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::CONTROL, - } => self.exit_selection(), - _ => (), - }; - - EventResult::Consumed(None) - } - - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { - self.render_prompt(area, surface, &cx.editor.theme) - } - - fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option { - Some(Position::new( - area.height as usize - 1, - area.x as usize + 2 + self.cursor, - )) - } -} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs new file mode 100644 index 00000000..ceb5a442 --- /dev/null +++ b/helix-term/src/ui/editor.rs @@ -0,0 +1,327 @@ +use crate::commands; +use crate::compositor::{Component, Compositor, Context, EventResult}; +use crate::keymap::{self, Keymaps}; +use crate::ui::text_color; + +use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State}; +use helix_view::{document::Mode, Document, Editor, Theme, View}; +use std::borrow::Cow; + +use crossterm::{ + cursor, + event::{read, Event, EventStream, KeyCode, KeyEvent}, +}; +use tui::{ + backend::CrosstermBackend, + buffer::Buffer as Surface, + layout::Rect, + style::{Color, Modifier, Style}, +}; + +pub struct EditorView { + keymap: Keymaps, +} + +const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter + +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, + ) { + 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, area, 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 = 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 text_color = text_color(); + 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.width, 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, + ); + } +} + +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 + } + + fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option { + // match view.doc.mode() { + // Mode::Insert => write!(stdout, "\x1B[6 q"), + // mode => write!(stdout, "\x1B[2 q"), + // }; + let view = ctx.editor.view().unwrap(); + 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 += area.x as usize + OFFSET as usize; + pos.row += area.y as usize; + Some(pos) + } +} diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs new file mode 100644 index 00000000..bc79e09c --- /dev/null +++ b/helix-term/src/ui/mod.rs @@ -0,0 +1,14 @@ +mod editor; +mod prompt; + +pub use editor::EditorView; +pub use prompt::Prompt; + +pub use tui::layout::Rect; +pub use tui::style::{Color, Modifier, Style}; + +// TODO: temp +#[inline(always)] +pub fn text_color() -> Style { + Style::default().fg(Color::Rgb(219, 191, 239)) // lilac +} diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs new file mode 100644 index 00000000..071cac90 --- /dev/null +++ b/helix-term/src/ui/prompt.rs @@ -0,0 +1,212 @@ +use crate::compositor::{Component, Context, EventResult}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use helix_core::Position; +use helix_view::Editor; +use helix_view::Theme; +use std::string::String; + +pub struct Prompt { + pub prompt: String, + pub line: String, + pub cursor: usize, + pub completion: Vec, + pub should_close: bool, + pub completion_selection_index: Option, + completion_fn: Box Vec>, + callback_fn: Box, +} + +impl Prompt { + pub fn new( + prompt: String, + mut completion_fn: impl FnMut(&str) -> Vec + 'static, + callback_fn: impl FnMut(&mut Editor, &str) + 'static, + ) -> Prompt { + Prompt { + prompt, + line: String::new(), + cursor: 0, + completion: completion_fn(""), + should_close: false, + completion_selection_index: None, + completion_fn: Box::new(completion_fn), + callback_fn: Box::new(callback_fn), + } + } + + pub fn insert_char(&mut self, c: char) { + self.line.insert(self.cursor, c); + self.cursor += 1; + self.completion = (self.completion_fn)(&self.line); + self.exit_selection(); + } + + pub fn move_char_left(&mut self) { + if self.cursor > 0 { + self.cursor -= 1; + } + } + + pub fn move_char_right(&mut self) { + if self.cursor < self.line.len() { + self.cursor += 1; + } + } + + pub fn move_start(&mut self) { + self.cursor = 0; + } + + pub fn move_end(&mut self) { + self.cursor = self.line.len(); + } + + pub fn delete_char_backwards(&mut self) { + if self.cursor > 0 { + self.line.remove(self.cursor - 1); + self.cursor -= 1; + self.completion = (self.completion_fn)(&self.line); + } + self.exit_selection(); + } + + pub fn change_completion_selection(&mut self) { + if self.completion.is_empty() { + return; + } + let index = + self.completion_selection_index.map(|i| i + 1).unwrap_or(0) % self.completion.len(); + self.completion_selection_index = Some(index); + self.line = self.completion[index].clone(); + } + pub fn exit_selection(&mut self) { + self.completion_selection_index = None; + } +} + +use tui::{ + buffer::Buffer as Surface, + layout::Rect, + style::{Color, Modifier, Style}, +}; + +const BASE_WIDTH: u16 = 30; +use crate::ui::text_color; + +impl Prompt { + pub fn render_prompt(&self, area: Rect, surface: &mut Surface, theme: &Theme) { + let text_color = text_color(); + // 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 { + Event::Key(event) => event, + _ => return EventResult::Ignored, + }; + + match event { + KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::NONE, + } => self.insert_char(c), + KeyEvent { + code: KeyCode::Esc, .. + } => self.should_close = true, + KeyEvent { + code: KeyCode::Right, + .. + } => self.move_char_right(), + KeyEvent { + code: KeyCode::Left, + .. + } => self.move_char_left(), + KeyEvent { + code: KeyCode::Char('e'), + modifiers: KeyModifiers::CONTROL, + } => self.move_end(), + KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::CONTROL, + } => self.move_start(), + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, + } => self.delete_char_backwards(), + KeyEvent { + code: KeyCode::Enter, + .. + } => (self.callback_fn)(cx.editor, &self.line), + KeyEvent { + code: KeyCode::Tab, .. + } => self.change_completion_selection(), + KeyEvent { + code: KeyCode::Char('q'), + modifiers: KeyModifiers::CONTROL, + } => self.exit_selection(), + _ => (), + }; + + EventResult::Consumed(None) + } + + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + self.render_prompt(area, surface, &cx.editor.theme) + } + + fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option { + Some(Position::new( + area.height as usize - 1, + area.x as usize + 2 + self.cursor, + )) + } +} -- cgit v1.2.3-70-g09d2