From 8695415fbfe927250f68e93793660e3c4e4a70b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 13 Dec 2020 12:23:50 +0900 Subject: [PATCH] wip: Move to new rendering structure. --- Cargo.lock | 50 ++-- helix-lsp/src/transport.rs | 1 - helix-syntax/languages.toml | 5 + helix-term/src/application.rs | 467 ++++------------------------------ helix-term/src/compositor.rs | 14 +- helix-term/src/editor_view.rs | 311 ++++++++++++++++++++++ helix-term/src/main.rs | 2 + helix-term/src/prompt.rs | 72 +++++- helix-term/src/terminal.rs | 221 ++++++++++++++++ 9 files changed, 680 insertions(+), 463 deletions(-) create mode 100644 helix-syntax/languages.toml create mode 100644 helix-term/src/editor_view.rs create mode 100644 helix-term/src/terminal.rs diff --git a/Cargo.lock b/Cargo.lock index 331934f8d..1c8c86c70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,9 +11,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" +checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4" [[package]] name = "arrayref" @@ -195,9 +195,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" dependencies = [ "jobserver", ] @@ -242,15 +242,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "cloudabi" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" -dependencies = [ - "bitflags", -] - [[package]] name = "concurrent-queue" version = "1.2.2" @@ -615,9 +606,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" [[package]] name = "lock_api" @@ -777,12 +768,11 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0" dependencies = [ - "cfg-if 0.1.10", - "cloudabi", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall", @@ -947,18 +937,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" dependencies = [ "proc-macro2", "quote", @@ -967,9 +957,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" dependencies = [ "itoa", "ryu", @@ -1024,9 +1014,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" +checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" [[package]] name = "smol" @@ -1060,9 +1050,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.53" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" +checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" dependencies = [ "proc-macro2", "quote", @@ -1156,7 +1146,7 @@ dependencies = [ [[package]] name = "tui" version = "0.13.0" -source = "git+https://github.com/fdehau/tui-rs#efdd6bfb193dafcb5e3bdc75e7d2d314065da1d7" +source = "git+https://github.com/fdehau/tui-rs#74243394d90ea1316b6bedac6c9e4f26971c76b6" dependencies = [ "bitflags", "cassowary", diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 4c349a13b..22af1b40f 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -177,7 +177,6 @@ impl Transport { .expect("pending_request with id not found!"); tx.send(Err(error.into())).await?; } - msg => unimplemented!("{:?}", msg), } Ok(()) } diff --git a/helix-syntax/languages.toml b/helix-syntax/languages.toml new file mode 100644 index 000000000..dc4fcf6fd --- /dev/null +++ b/helix-syntax/languages.toml @@ -0,0 +1,5 @@ +[[language]] +name = "rust" +scope = "source.rust" +injection-regex = "rust" +file-types = ["rs"] diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 589aaf6e9..7a74f8ba6 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>; +type Terminal = crate::terminal::Terminal>; -const BASE_WIDTH: u16 = 30; - -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 { +// 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 { 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 = 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 { - let renderer = Renderer::new()?; let mut editor = Editor::new(); + let size = terminal.size()?; if let Some(file) = args.values_of_t::("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 3cf6bf03f..1d94ee633 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), ) -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; @@ -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 000000000..0181623af --- /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 = 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 92ab10c24..63fbe52d7 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 689eac666..4747c9f59 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 000000000..e40343bd7 --- /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 +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 Drop for Terminal +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 Terminal +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> { + 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> { + 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 { + // 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 { + self.backend.size() + } +}