diff options
Diffstat (limited to 'helix-term/src/application.rs')
-rw-r--r-- | helix-term/src/application.rs | 467 |
1 files changed, 48 insertions, 419 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) |