From 83f2c24115cc5a3dce90a77440f1ef06f6cf9c78 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 6 Dec 2020 11:53:58 +0900 Subject: wip: Compositor --- helix-term/src/application.rs | 256 ++++++++++++++++++++++++------------------ helix-term/src/compositor.rs | 111 ++++++++++++++++++ helix-term/src/main.rs | 1 + 3 files changed, 261 insertions(+), 107 deletions(-) create mode 100644 helix-term/src/compositor.rs (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 141779ec..30258c1d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -8,6 +8,8 @@ use helix_view::{ Document, Editor, Theme, View, }; +use crate::compositor::{Component, Compositor}; + use log::{debug, info}; use std::{ @@ -35,23 +37,21 @@ use tui::{ style::{Color, Modifier, Style}, }; -const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter - type Terminal = tui::Terminal>; const BASE_WIDTH: u16 = 30; pub struct Application<'a> { - editor: Editor, prompt: Option, - terminal: Renderer, - keymap: Keymaps, + compositor: Compositor, + renderer: Renderer, + executor: &'a smol::Executor<'a>, language_server: helix_lsp::Client, } -struct Renderer { +pub(crate) struct Renderer { size: (u16, u16), terminal: Terminal, surface: Surface, @@ -92,7 +92,6 @@ impl Renderer { // 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); - self.surface.reset(); // reset is faster than allocating new empty surface // clear with background color self.surface.set_style(area, theme.get("ui.background")); @@ -221,8 +220,12 @@ impl Renderer { // TODO: paint cursor heads except primary - self.surface - .set_string(OFFSET + visual_x, line, grapheme, style); + self.surface.set_string( + viewport.x + visual_x, + viewport.y + line, + grapheme, + style, + ); visual_x += width; } @@ -321,7 +324,7 @@ impl Renderer { .set_string(2, self.size.1 - 1, &prompt.line, self.text_color); } - pub fn draw(&mut self) { + pub fn draw_and_swap(&mut self) { use tui::backend::Backend; // TODO: theres probably a better place for this self.terminal @@ -363,112 +366,40 @@ impl Renderer { } } -impl<'a> Application<'a> { - pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result { - let terminal = Renderer::new()?; - let mut editor = Editor::new(); - - if let Some(file) = args.values_of_t::("files").unwrap().pop() { - editor.open(file, terminal.size)?; - } - - let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]); +struct EditorView { + editor: Editor, + prompt: Option, // TODO: this is None for now, make a layer + keymap: Keymaps, +} - let mut app = Self { +impl EditorView { + fn new(editor: Editor) -> Self { + Self { editor, - terminal, - // TODO; move to state prompt: None, - - // keymap: keymap::default(), - executor, - language_server, - }; - - Ok(app) - } - - fn render(&mut self) { - let viewport = Rect::new(OFFSET, 0, self.terminal.size.0, self.terminal.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 { &*(&self.editor.theme as *const Theme) }; - if let Some(view) = self.editor.view_mut() { - self.terminal.render_view(view, viewport, theme_ref); - if let Some(prompt) = &self.prompt { - if prompt.should_close { - self.prompt = None; - } else { - self.terminal.render_prompt(view, prompt, theme_ref); - } - } - } - - self.terminal.draw(); - - // TODO: drop unwrap - self.terminal - .render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport); - } - - pub async fn event_loop(&mut self) { - let mut reader = EventStream::new(); - - // initialize lsp - self.language_server.initialize().await.unwrap(); - self.language_server - .text_document_did_open(&self.editor.view().unwrap().doc) - .await - .unwrap(); - - self.render(); - - loop { - if self.editor.should_close { - break; - } - - use futures_util::{select, FutureExt}; - select! { - event = reader.next().fuse() => { - self.handle_terminal_events(event).await - } - call = self.language_server.incoming.next().fuse() => { - self.handle_language_server_message(call).await - } - } } } +} - pub async fn handle_terminal_events( - &mut self, - event: Option>, - ) { - // Handle key events +impl Component for EditorView { + fn handle_event(&mut self, event: Event, executor: &smol::Executor) -> bool { match event { - Some(Ok(Event::Resize(width, height))) => { - self.terminal.resize(width, height); - + Event::Resize(width, height) => { // TODO: simplistic ensure cursor in view for now // TODO: loop over views if let Some(view) = self.editor.view_mut() { - view.size = self.terminal.size; + view.size = (width, height); view.ensure_cursor_in_view() }; - - self.render(); } - Some(Ok(Event::Key(event))) => { + Event::Key(event) => { // if there's a prompt, it takes priority if let Some(prompt) = &mut self.prompt { self.prompt .as_mut() .unwrap() .handle_input(event, &mut self.editor); - - self.render(); } else if let Some(view) = self.editor.view_mut() { let keys = vec![event]; // TODO: sequences (`gg`) @@ -478,7 +409,7 @@ impl<'a> Application<'a> { if let Some(command) = self.keymap[&Mode::Insert].get(&keys) { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; @@ -490,7 +421,7 @@ impl<'a> Application<'a> { { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; commands::insert::insert_char(&mut cx, c); @@ -557,7 +488,7 @@ impl<'a> Application<'a> { } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; command(&mut cx); @@ -570,7 +501,7 @@ impl<'a> Application<'a> { if let Some(command) = self.keymap[&mode].get(&keys) { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; command(&mut cx); @@ -580,10 +511,119 @@ impl<'a> Application<'a> { } } } - self.render(); } } - Some(Ok(Event::Mouse(_))) => (), // unhandled + Event::Mouse(_) => (), + } + + true + } + fn render(&mut self, renderer: &mut Renderer) { + 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 { &*(&self.editor.theme as *const Theme) }; + if let Some(view) = self.editor.view_mut() { + renderer.render_view(view, viewport, theme_ref); + if let Some(prompt) = &self.prompt { + if prompt.should_close { + self.prompt = None; + } else { + renderer.render_prompt(view, prompt, theme_ref); + } + } + } + + // TODO: drop unwrap + renderer.render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), 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(); + + if let Some(file) = args.values_of_t::("files").unwrap().pop() { + editor.open(file, renderer.size)?; + } + + let mut compositor = Compositor::new(); + compositor.push(Box::new(EditorView::new(editor))); + + let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]); + + let mut app = Self { + renderer, + // TODO; move to state + compositor, + prompt: None, + + executor, + language_server, + }; + + Ok(app) + } + + fn render(&mut self) { + // v2: + self.renderer.surface.reset(); // reset is faster than allocating new empty surface + self.compositor.render(&mut self.renderer); // viewport, + self.renderer.draw_and_swap(); + } + + pub async fn event_loop(&mut self) { + let mut reader = EventStream::new(); + + // initialize lsp + self.language_server.initialize().await.unwrap(); + // TODO: temp + // self.language_server + // .text_document_did_open(&self.editor.view().unwrap().doc) + // .await + // .unwrap(); + + self.render(); + + loop { + // TODO: + // if self.editor.should_close { + // break; + // } + + use futures_util::{select, FutureExt}; + select! { + event = reader.next().fuse() => { + self.handle_terminal_events(event) + } + call = self.language_server.incoming.next().fuse() => { + self.handle_language_server_message(call).await + } + } + } + } + + pub fn handle_terminal_events(&mut self, event: Option>) { + // Handle key events + match event { + Some(Ok(Event::Resize(width, height))) => { + self.renderer.resize(width, height); + + // TODO: use the response + self.compositor + .handle_event(Event::Resize(width, height), self.executor); + + self.render(); + } + Some(Ok(event)) => { + // TODO: use the response + self.compositor.handle_event(event, self.executor); + + self.render(); + } Some(Err(x)) => panic!(x), None => panic!(), }; @@ -599,11 +639,13 @@ impl<'a> Application<'a> { match notification { Notification::PublishDiagnostics(params) => { let path = Some(params.uri.to_file_path().unwrap()); - let view = self - .editor - .views - .iter_mut() - .find(|view| view.doc.path == path); + let view: Option<&mut helix_view::View> = None; + // TODO: + // let view = self + // .editor + // .views + // .iter_mut() + // .find(|view| view.doc.path == path); if let Some(view) = view { let doc = view.doc.text().slice(..); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs new file mode 100644 index 00000000..187c5692 --- /dev/null +++ b/helix-term/src/compositor.rs @@ -0,0 +1,111 @@ +// Features: +// Tracks currently focused component which receives all input +// Event loop is external as opposed to cursive-rs +// Calls render on the component and translates screen coords to local component coords +// +// TODO: +// Q: where is the Application state stored? do we store it into an external static var? +// A: probably makes sense to initialize the editor into a `static Lazy<>` global var. +// +// Q: how do we composit nested structures? There should be sub-components/views +// +// Each component declares it's own size constraints and gets fitted based on it's parent. +// 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; + +pub(crate) trait Component { + /// Process input events, return true if handled. + fn handle_event(&mut self, event: Event, executor: &Executor) -> bool; + // , args: () + + /// Should redraw? Useful for saving redraw cycles if we know component didn't change. + fn should_update(&self) -> bool { + true + } + + fn render(&mut self, renderer: &mut Renderer); +} + +// struct Editor { }; + +// For v1: +// Child views are something each view needs to handle on it's own for now, positioning and sizing +// options, focus tracking. In practice this is simple: we only will need special solving for +// splits etc + +// impl Editor { +// fn render(&mut self, surface: &mut Surface, args: ()) { +// // compute x, y, w, h rects for sub-views! +// // get surface area +// // get constraints for textarea, statusbar +// // -> cassowary-rs + +// // first render textarea +// // then render statusbar +// } +// } + +// usecases to consider: +// - a single view with subviews (textarea + statusbar) +// - a popup panel / dialog with it's own interactions +// - an autocomplete popup that doesn't change focus + +//fn main() { +// let root = Editor::new(); +// let compositor = Compositor::new(); + +// compositor.push(root); + +// // pos: clip to bottom of screen +// compositor.push_at(pos, Prompt::new( +// ":", +// (), +// |input: &str| match input {} +// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent +// // Cursive solves this by allowing to return a special result on process_event +// // that's either Ignore | Consumed(Opt) where C: fn (Compositor) -> () + +// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer +// // but retain the focus where it was. The popup will also need to update as we type into the +// // textarea. It should also capture certain input, such as tab presses etc +// // +// // 1) This could be faked by the top layer pushing down edits into the previous layer. +// // 2) Alternatively, +//} + +pub(crate) struct Compositor { + layers: Vec>, +} + +impl Compositor { + pub fn new() -> Self { + Self { layers: Vec::new() } + } + + pub fn push(&mut self, layer: Box) { + self.layers.push(layer); + } + + pub fn pop(&mut self) { + self.layers.pop(); + } + + pub fn handle_event(&mut self, event: Event, executor: &Executor) -> () { + // TODO: custom focus + if let Some(layer) = self.layers.last_mut() { + layer.handle_event(event, executor); + // return should_update + } + } + + pub fn render(&mut self, renderer: &mut Renderer) { + for layer in &mut self.layers { + layer.render(renderer) + } + } +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 9378d3ee..a43aebd8 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,6 +1,7 @@ #![allow(unused)] mod application; +mod compositor; use application::Application; -- cgit v1.2.3-70-g09d2 From be3c02104600e5bba8e3af7bc9787e62cba30183 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 7 Dec 2020 15:18:37 +0900 Subject: snapshot --- helix-term/src/application.rs | 14 ++++++++------ helix-term/src/compositor.rs | 10 +++++++++- 2 files changed, 17 insertions(+), 7 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 30258c1d..506735e8 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -8,7 +8,7 @@ use helix_view::{ Document, Editor, Theme, View, }; -use crate::compositor::{Component, Compositor}; +use crate::compositor::{Component, Compositor, EventResult}; use log::{debug, info}; @@ -383,7 +383,7 @@ impl EditorView { } impl Component for EditorView { - fn handle_event(&mut self, event: Event, executor: &smol::Executor) -> bool { + fn handle_event(&mut self, event: Event, executor: &smol::Executor) -> EventResult { match event { Event::Resize(width, height) => { // TODO: simplistic ensure cursor in view for now @@ -392,6 +392,7 @@ impl Component for EditorView { view.size = (width, height); view.ensure_cursor_in_view() }; + EventResult::Consumed(None) } Event::Key(event) => { // if there's a prompt, it takes priority @@ -400,6 +401,7 @@ impl Component for EditorView { .as_mut() .unwrap() .handle_input(event, &mut self.editor); + EventResult::Consumed(None) } else if let Some(view) = self.editor.view_mut() { let keys = vec![event]; // TODO: sequences (`gg`) @@ -511,12 +513,13 @@ impl Component for EditorView { } } } + EventResult::Consumed(None) + } else { + EventResult::Ignored } } - Event::Mouse(_) => (), + Event::Mouse(_) => EventResult::Ignored, } - - true } fn render(&mut self, renderer: &mut Renderer) { const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter @@ -569,7 +572,6 @@ impl<'a> Application<'a> { } fn render(&mut self) { - // v2: self.renderer.surface.reset(); // reset is faster than allocating new empty surface self.compositor.render(&mut self.renderer); // viewport, self.renderer.draw_and_swap(); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 187c5692..f859f947 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -18,9 +18,17 @@ use crossterm::event::Event; use smol::Executor; use tui::buffer::Buffer as Surface; +pub(crate) type Callback = Box; + +// Cursive-inspired +pub(crate) enum EventResult { + Ignored, + Consumed(Option), +} + pub(crate) trait Component { /// Process input events, return true if handled. - fn handle_event(&mut self, event: Event, executor: &Executor) -> bool; + fn handle_event(&mut self, event: Event, executor: &Executor) -> EventResult; // , args: () /// Should redraw? Useful for saving redraw cycles if we know component didn't change. -- cgit v1.2.3-70-g09d2 From 5103dc96173afaa1c0793db56f60ec1fef1e0fc3 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 10 Dec 2020 18:13:42 +0900 Subject: move commands and keymap back to terminal. Command needs to be able to deal with UI. We'll separate it again later on. --- Cargo.lock | 1 + helix-term/Cargo.toml | 1 + helix-term/src/application.rs | 45 +-- helix-term/src/commands.rs | 682 ++++++++++++++++++++++++++++++++++++++++++ helix-term/src/compositor.rs | 8 + helix-term/src/keymap.rs | 217 ++++++++++++++ helix-term/src/main.rs | 3 + helix-term/src/prompt.rs | 126 ++++++++ helix-view/src/commands.rs | 680 ----------------------------------------- helix-view/src/keymap.rs | 217 -------------- helix-view/src/lib.rs | 3 - helix-view/src/prompt.rs | 126 -------- 12 files changed, 1053 insertions(+), 1056 deletions(-) create mode 100644 helix-term/src/commands.rs create mode 100644 helix-term/src/keymap.rs create mode 100644 helix-term/src/prompt.rs delete mode 100644 helix-view/src/commands.rs delete mode 100644 helix-view/src/keymap.rs delete mode 100644 helix-view/src/prompt.rs (limited to 'helix-term/src') diff --git a/Cargo.lock b/Cargo.lock index a3e93bd7..331934f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -522,6 +522,7 @@ dependencies = [ "helix-view", "log", "num_cpus", + "once_cell", "smol", "tui", ] diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index c1560ee7..b8eea7c2 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -17,6 +17,7 @@ helix-view = { path = "../helix-view", features = ["term"]} helix-lsp = { path = "../helix-lsp"} anyhow = "1" +once_cell = "1.4" smol = "1" num_cpus = "1" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 506735e8..8c454b5d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,14 +1,14 @@ -use clap::ArgMatches as Args; -use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State}; -use helix_view::{ +use crate::{ commands, - document::Mode, keymap::{self, Keymaps}, - prompt::Prompt, - Document, Editor, Theme, View, }; +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::prompt::Prompt; use log::{debug, info}; @@ -395,23 +395,16 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(event) => { - // if there's a prompt, it takes priority - if let Some(prompt) = &mut self.prompt { - self.prompt - .as_mut() - .unwrap() - .handle_input(event, &mut self.editor); - EventResult::Consumed(None) - } else if let Some(view) = self.editor.view_mut() { + if let Some(view) = self.editor.view_mut() { let keys = vec![event]; // TODO: sequences (`gg`) // TODO: handle count other than 1 match view.doc.mode() { Mode::Insert => { if let Some(command) = self.keymap[&Mode::Insert].get(&keys) { - let mut cx = helix_view::commands::Context { + let mut cx = commands::Context { view, - executor: executor, + executor, count: 1, }; @@ -421,9 +414,9 @@ impl Component for EditorView { .. } = event { - let mut cx = helix_view::commands::Context { + let mut cx = commands::Context { view, - executor: executor, + executor, count: 1, }; commands::insert::insert_char(&mut cx, c); @@ -488,9 +481,9 @@ impl Component for EditorView { // HAXX: special casing for command mode } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) { - let mut cx = helix_view::commands::Context { + let mut cx = commands::Context { view, - executor: executor, + executor, count: 1, }; command(&mut cx); @@ -501,9 +494,9 @@ impl Component for EditorView { } mode => { if let Some(command) = self.keymap[&mode].get(&keys) { - let mut cx = helix_view::commands::Context { + let mut cx = commands::Context { view, - executor: executor, + executor, count: 1, }; command(&mut cx); @@ -530,13 +523,6 @@ impl Component for EditorView { let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) }; if let Some(view) = self.editor.view_mut() { renderer.render_view(view, viewport, theme_ref); - if let Some(prompt) = &self.prompt { - if prompt.should_close { - self.prompt = None; - } else { - renderer.render_prompt(view, prompt, theme_ref); - } - } } // TODO: drop unwrap @@ -562,7 +548,6 @@ impl<'a> Application<'a> { renderer, // TODO; move to state compositor, - prompt: None, executor, language_server, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs new file mode 100644 index 00000000..a791f243 --- /dev/null +++ b/helix-term/src/commands.rs @@ -0,0 +1,682 @@ +use helix_core::{ + graphemes, + indent::TAB_WIDTH, + regex::Regex, + register, selection, + state::{Direction, Granularity, State}, + ChangeSet, Range, Selection, Tendril, Transaction, +}; + +use once_cell::sync::Lazy; + +use crate::prompt::Prompt; + +use helix_view::{ + document::Mode, + view::{View, PADDING}, +}; + +pub struct Context<'a, 'b> { + pub count: usize, + pub view: &'a mut View, + pub executor: &'a smol::Executor<'b>, +} + +/// A command is a function that takes the current state and a count, and does a side-effect on the +/// state (usually by creating and applying a transaction). +pub type Command = fn(cx: &mut Context); + +pub fn move_char_left(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .move_selection(Direction::Backward, Granularity::Character, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn move_char_right(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .move_selection(Direction::Forward, Granularity::Character, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn move_line_up(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .move_selection(Direction::Backward, Granularity::Line, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn move_line_down(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .move_selection(Direction::Forward, Granularity::Line, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn move_line_end(cx: &mut Context) { + let lines = selection_lines(&cx.view.doc.state); + + let positions = lines + .into_iter() + .map(|index| { + // adjust all positions to the end of the line. + + // Line end is pos at the start of next line - 1 + // subtract another 1 because the line ends with \n + cx.view.doc.text().line_to_char(index + 1).saturating_sub(2) + }) + .map(|pos| Range::new(pos, pos)); + + let selection = Selection::new(positions.collect(), 0); + + cx.view.doc.set_selection(selection); +} + +pub fn move_line_start(cx: &mut Context) { + let lines = selection_lines(&cx.view.doc.state); + + let positions = lines + .into_iter() + .map(|index| { + // adjust all positions to the start of the line. + cx.view.doc.text().line_to_char(index) + }) + .map(|pos| Range::new(pos, pos)); + + let selection = Selection::new(positions.collect(), 0); + + cx.view.doc.set_selection(selection); +} + +pub fn move_next_word_start(cx: &mut Context) { + let pos = cx.view.doc.state.move_pos( + cx.view.doc.selection().cursor(), + Direction::Forward, + Granularity::Word, + cx.count, + ); + + cx.view.doc.set_selection(Selection::point(pos)); +} + +pub fn move_prev_word_start(cx: &mut Context) { + let pos = cx.view.doc.state.move_pos( + cx.view.doc.selection().cursor(), + Direction::Backward, + Granularity::Word, + cx.count, + ); + + cx.view.doc.set_selection(Selection::point(pos)); +} + +pub fn move_next_word_end(cx: &mut Context) { + let pos = State::move_next_word_end( + &cx.view.doc.text().slice(..), + cx.view.doc.selection().cursor(), + cx.count, + ); + + cx.view.doc.set_selection(Selection::point(pos)); +} + +pub fn move_file_start(cx: &mut Context) { + cx.view.doc.set_selection(Selection::point(0)); + + cx.view.doc.mode = Mode::Normal; +} + +pub fn move_file_end(cx: &mut Context) { + let text = &cx.view.doc.text(); + let last_line = text.line_to_char(text.len_lines().saturating_sub(2)); + cx.view.doc.set_selection(Selection::point(last_line)); + + cx.view.doc.mode = Mode::Normal; +} + +pub fn check_cursor_in_view(view: &View) -> bool { + let cursor = view.doc.selection().cursor(); + let line = view.doc.text().char_to_line(cursor); + let document_end = view.first_line + view.size.1.saturating_sub(1) as usize; + + if (line > document_end.saturating_sub(PADDING)) | (line < view.first_line + PADDING) { + return false; + } + true +} + +pub fn page_up(cx: &mut Context) { + if cx.view.first_line < PADDING { + return; + } + + cx.view.first_line = cx.view.first_line.saturating_sub(cx.view.size.1 as usize); + + if !check_cursor_in_view(cx.view) { + let text = cx.view.doc.text(); + let pos = text.line_to_char(cx.view.last_line().saturating_sub(PADDING)); + cx.view.doc.set_selection(Selection::point(pos)); + } +} + +pub fn page_down(cx: &mut Context) { + cx.view.first_line += cx.view.size.1 as usize + PADDING; + + if cx.view.first_line < cx.view.doc.text().len_lines() { + let text = cx.view.doc.text(); + let pos = text.line_to_char(cx.view.first_line as usize); + cx.view.doc.set_selection(Selection::point(pos)); + } +} + +pub fn half_page_up(cx: &mut Context) { + if cx.view.first_line < PADDING { + return; + } + + cx.view.first_line = cx + .view + .first_line + .saturating_sub(cx.view.size.1 as usize / 2); + + if !check_cursor_in_view(cx.view) { + let text = &cx.view.doc.text(); + let pos = text.line_to_char(cx.view.last_line() - PADDING); + cx.view.doc.set_selection(Selection::point(pos)); + } +} + +pub fn half_page_down(cx: &mut Context) { + let lines = cx.view.doc.text().len_lines(); + if cx.view.first_line < lines.saturating_sub(cx.view.size.1 as usize) { + cx.view.first_line += cx.view.size.1 as usize / 2; + } + if !check_cursor_in_view(cx.view) { + let text = cx.view.doc.text(); + let pos = text.line_to_char(cx.view.first_line as usize); + cx.view.doc.set_selection(Selection::point(pos)); + } +} +// avoid select by default by having a visual mode switch that makes movements into selects + +pub fn extend_char_left(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .extend_selection(Direction::Backward, Granularity::Character, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn extend_char_right(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .extend_selection(Direction::Forward, Granularity::Character, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn extend_line_up(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .extend_selection(Direction::Backward, Granularity::Line, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn extend_line_down(cx: &mut Context) { + let selection = + cx.view + .doc + .state + .extend_selection(Direction::Forward, Granularity::Line, cx.count); + cx.view.doc.set_selection(selection); +} + +pub fn split_selection_on_newline(cx: &mut Context) { + let text = &cx.view.doc.text().slice(..); + // only compile the regex once + #[allow(clippy::trivial_regex)] + static REGEX: Lazy = Lazy::new(|| Regex::new(r"\n").unwrap()); + let selection = selection::split_on_matches(text, cx.view.doc.selection(), ®EX); + cx.view.doc.set_selection(selection); +} + +pub fn select_line(cx: &mut Context) { + // TODO: count + let pos = cx.view.doc.selection().primary(); + let text = cx.view.doc.text(); + let line = text.char_to_line(pos.head); + let start = text.line_to_char(line); + let end = text.line_to_char(line + 1).saturating_sub(1); + + cx.view.doc.set_selection(Selection::single(start, end)); +} + +pub fn delete_selection(cx: &mut Context) { + let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { + (range.from(), range.to() + 1, None) + }); + cx.view.doc.apply(&transaction); + + append_changes_to_history(cx); +} + +pub fn change_selection(cx: &mut Context) { + delete_selection(cx); + insert_mode(cx); +} + +pub fn collapse_selection(cx: &mut Context) { + let selection = cx + .view + .doc + .selection() + .transform(|range| Range::new(range.head, range.head)); + + cx.view.doc.set_selection(selection); +} + +pub fn flip_selections(cx: &mut Context) { + let selection = cx + .view + .doc + .selection() + .transform(|range| Range::new(range.head, range.anchor)); + + cx.view.doc.set_selection(selection); +} + +fn enter_insert_mode(cx: &mut Context) { + cx.view.doc.mode = Mode::Insert; + + append_changes_to_history(cx); +} +// inserts at the start of each selection +pub fn insert_mode(cx: &mut Context) { + enter_insert_mode(cx); + + let selection = cx + .view + .doc + .selection() + .transform(|range| Range::new(range.to(), range.from())); + cx.view.doc.set_selection(selection); +} + +// inserts at the end of each selection +pub fn append_mode(cx: &mut Context) { + enter_insert_mode(cx); + cx.view.doc.restore_cursor = true; + + // TODO: as transaction + let text = &cx.view.doc.text().slice(..); + let selection = cx.view.doc.selection().transform(|range| { + // TODO: to() + next char + Range::new( + range.from(), + graphemes::next_grapheme_boundary(text, range.to()), + ) + }); + cx.view.doc.set_selection(selection); +} + +// TODO: I, A, o and O can share a lot of the primitives. +pub fn command_mode(_cx: &mut Context) { + unimplemented!() +} + +// calculate line numbers for each selection range +fn selection_lines(state: &State) -> Vec { + let mut lines = state + .selection + .ranges() + .iter() + .map(|range| state.doc.char_to_line(range.head)) + .collect::>(); + + lines.sort_unstable(); // sorting by usize so _unstable is preferred + lines.dedup(); + + lines +} + +// I inserts at the start of each line with a selection +pub fn prepend_to_line(cx: &mut Context) { + enter_insert_mode(cx); + + move_line_start(cx); +} + +// A inserts at the end of each line with a selection +pub fn append_to_line(cx: &mut Context) { + enter_insert_mode(cx); + + move_line_end(cx); +} + +// o inserts a new line after each line with a selection +pub fn open_below(cx: &mut Context) { + enter_insert_mode(cx); + + let lines = selection_lines(&cx.view.doc.state); + + let positions: Vec<_> = lines + .into_iter() + .map(|index| { + // adjust all positions to the end of the line/start of the next one. + cx.view.doc.text().line_to_char(index + 1) + }) + .collect(); + + // TODO: use same logic as insert_newline for indentation + let changes = positions.iter().copied().map(|index| + // generate changes + (index, index, Some(Tendril::from_char('\n')))); + + // TODO: count actually inserts "n" new lines and starts editing on all of them. + // TODO: append "count" newlines and modify cursors to those lines + + let selection = Selection::new( + positions + .iter() + .copied() + .map(|pos| Range::new(pos, pos)) + .collect(), + 0, + ); + + let transaction = Transaction::change(&cx.view.doc.state, changes).with_selection(selection); + + cx.view.doc.apply(&transaction); +} + +// O inserts a new line before each line with a selection + +fn append_changes_to_history(cx: &mut Context) { + if cx.view.doc.changes.is_empty() { + return; + } + + let new_changeset = ChangeSet::new(cx.view.doc.text()); + let changes = std::mem::replace(&mut cx.view.doc.changes, new_changeset); + // Instead of doing this messy merge we could always commit, and based on transaction + // annotations either add a new layer or compose into the previous one. + let transaction = Transaction::from(changes).with_selection(cx.view.doc.selection().clone()); + + // increment document version + // TODO: needs to happen on undo/redo too + cx.view.doc.version += 1; + + // TODO: trigger lsp/documentDidChange with changes + + // HAXX: we need to reconstruct the state as it was before the changes.. + let old_state = std::mem::replace(&mut cx.view.doc.old_state, cx.view.doc.state.clone()); + // TODO: take transaction by value? + cx.view + .doc + .history + .commit_revision(&transaction, &old_state); + + // TODO: notify LSP of changes +} + +pub fn normal_mode(cx: &mut Context) { + cx.view.doc.mode = Mode::Normal; + + append_changes_to_history(cx); + + // if leaving append mode, move cursor back by 1 + if cx.view.doc.restore_cursor { + let text = &cx.view.doc.text().slice(..); + let selection = cx.view.doc.selection().transform(|range| { + Range::new( + range.from(), + graphemes::prev_grapheme_boundary(text, range.to()), + ) + }); + cx.view.doc.set_selection(selection); + + cx.view.doc.restore_cursor = false; + } +} + +pub fn goto_mode(cx: &mut Context) { + cx.view.doc.mode = Mode::Goto; +} + +// NOTE: Transactions in this module get appended to history when we switch back to normal mode. +pub mod insert { + use super::*; + // TODO: insert means add text just before cursor, on exit we should be on the last letter. + pub fn insert_char(cx: &mut Context, c: char) { + let c = Tendril::from_char(c); + let transaction = Transaction::insert(&cx.view.doc.state, c); + + cx.view.doc.apply(&transaction); + } + + pub fn insert_tab(cx: &mut Context) { + insert_char(cx, '\t'); + } + + pub fn insert_newline(cx: &mut Context) { + let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { + let indent_level = helix_core::indent::suggested_indent_for_pos( + cx.view.doc.syntax.as_ref(), + &cx.view.doc.state, + range.head, + ); + let indent = " ".repeat(TAB_WIDTH).repeat(indent_level); + let mut text = String::with_capacity(1 + indent.len()); + text.push('\n'); + text.push_str(&indent); + (range.head, range.head, Some(text.into())) + }); + cx.view.doc.apply(&transaction); + } + + // TODO: handle indent-aware delete + pub fn delete_char_backward(cx: &mut Context) { + let text = &cx.view.doc.text().slice(..); + let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { + ( + graphemes::nth_prev_grapheme_boundary(text, range.head, cx.count), + range.head, + None, + ) + }); + cx.view.doc.apply(&transaction); + } + + pub fn delete_char_forward(cx: &mut Context) { + let text = &cx.view.doc.text().slice(..); + let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { + ( + range.head, + graphemes::nth_next_grapheme_boundary(text, range.head, cx.count), + None, + ) + }); + cx.view.doc.apply(&transaction); + } +} + +pub fn insert_char_prompt(prompt: &mut Prompt, c: char) { + prompt.insert_char(c); +} + +// Undo / Redo + +pub fn undo(cx: &mut Context) { + if let Some(revert) = cx.view.doc.history.undo() { + cx.view.doc.version += 1; + cx.view.doc.apply(&revert); + } + + // TODO: each command could simply return a Option, then the higher level handles storing it? +} + +pub fn redo(cx: &mut Context) { + if let Some(transaction) = cx.view.doc.history.redo() { + cx.view.doc.version += 1; + cx.view.doc.apply(&transaction); + } +} + +// Yank / Paste + +pub fn yank(cx: &mut Context) { + // TODO: should selections be made end inclusive? + let values = cx + .view + .doc + .state + .selection() + .fragments(&cx.view.doc.text().slice(..)) + .map(|cow| cow.into_owned()) + .collect(); + + // TODO: allow specifying reg + let reg = '"'; + register::set(reg, values); +} + +pub fn paste(cx: &mut Context) { + // TODO: allow specifying reg + let reg = '"'; + if let Some(values) = register::get(reg) { + let repeat = std::iter::repeat( + values + .last() + .map(|value| Tendril::from_slice(value)) + .unwrap(), + ); + + // TODO: if any of values ends \n it's linewise paste + // + // p => paste after + // P => paste before + // alt-p => paste every yanked selection after selected text + // alt-P => paste every yanked selection before selected text + // R => replace selected text with yanked text + // alt-R => replace selected text with every yanked text + // + // append => insert at next line + // insert => insert at start of line + // replace => replace + // default insert + + let linewise = values.iter().any(|value| value.ends_with('\n')); + + let mut values = values.into_iter().map(Tendril::from).chain(repeat); + + let transaction = if linewise { + // paste on the next line + // TODO: can simply take a range + modifier and compute the right pos without ifs + let text = cx.view.doc.text(); + Transaction::change_by_selection(&cx.view.doc.state, |range| { + let line_end = text.line_to_char(text.char_to_line(range.head) + 1); + (line_end, line_end, Some(values.next().unwrap())) + }) + } else { + Transaction::change_by_selection(&cx.view.doc.state, |range| { + (range.head + 1, range.head + 1, Some(values.next().unwrap())) + }) + }; + + cx.view.doc.apply(&transaction); + append_changes_to_history(cx); + } +} + +fn get_lines(view: &View) -> Vec { + let mut lines = Vec::new(); + + // Get all line numbers + for range in view.doc.selection().ranges() { + let start = view.doc.text().char_to_line(range.from()); + let end = view.doc.text().char_to_line(range.to()); + + for line in start..=end { + lines.push(line) + } + } + lines.sort_unstable(); // sorting by usize so _unstable is preferred + lines.dedup(); + lines +} + +pub fn indent(cx: &mut Context) { + let lines = get_lines(cx.view); + + // Indent by one level + let indent = Tendril::from(" ".repeat(TAB_WIDTH)); + + let transaction = Transaction::change( + &cx.view.doc.state, + lines.into_iter().map(|line| { + let pos = cx.view.doc.text().line_to_char(line); + (pos, pos, Some(indent.clone())) + }), + ); + cx.view.doc.apply(&transaction); + append_changes_to_history(cx); +} + +pub fn unindent(cx: &mut Context) { + let lines = get_lines(cx.view); + let mut changes = Vec::with_capacity(lines.len()); + + for line_idx in lines { + let line = cx.view.doc.text().line(line_idx); + let mut width = 0; + + for ch in line.chars() { + match ch { + ' ' => width += 1, + '\t' => width = (width / TAB_WIDTH + 1) * TAB_WIDTH, + _ => break, + } + + if width >= TAB_WIDTH { + break; + } + } + + if width > 0 { + let start = cx.view.doc.text().line_to_char(line_idx); + changes.push((start, start + width, None)) + } + } + + let transaction = Transaction::change(&cx.view.doc.state, changes.into_iter()); + + cx.view.doc.apply(&transaction); + append_changes_to_history(cx); +} + +pub fn indent_selection(_cx: &mut Context) { + // loop over each line and recompute proper indentation + unimplemented!() +} + +// + +pub fn save(cx: &mut Context) { + // Spawns an async task to actually do the saving. This way we prevent blocking. + + // TODO: handle save errors somehow? + cx.executor.spawn(cx.view.doc.save()).detach(); +} diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index f859f947..158a8b28 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -20,6 +20,14 @@ use tui::buffer::Buffer as Surface; pub(crate) type Callback = Box; +// --> EventResult should have a callback that takes a context with methods like .popup(), +// .prompt() etc. That way we can abstract it from the renderer. +// Q: How does this interact with popups where we need to be able to specify the rendering of the +// popup? +// A: It could just take a textarea. +// +// If Compositor was specified in the callback that's then problematic because of + // Cursive-inspired pub(crate) enum EventResult { Ignored, diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs new file mode 100644 index 00000000..af46f7a4 --- /dev/null +++ b/helix-term/src/keymap.rs @@ -0,0 +1,217 @@ +use crate::commands::{self, Command}; +use helix_core::hashmap; +use helix_view::document::Mode; +use std::collections::HashMap; + +// Kakoune-inspired: +// mode = { +// normal = { +// q = record_macro +// w = (next) word +// W = next WORD +// e = end of word +// E = end of WORD +// r = +// t = 'till char +// y = yank +// u = undo +// U = redo +// i = insert +// I = INSERT (start of line) +// o = open below (insert on new line below) +// O = open above (insert on new line above) +// p = paste (before cursor) +// P = PASTE (after cursor) +// ` = +// [ = select to text object start (alt = select whole object) +// ] = select to text object end +// { = extend to inner object start +// } = extend to inner object end +// a = append +// A = APPEND (end of line) +// s = split +// S = select +// d = delete() +// f = find_char() +// g = goto (gg, G, gc, gd, etc) +// +// h = move_char_left(n) +// j = move_line_down(n) +// k = move_line_up(n) +// l = move_char_right(n) +// : = command line +// ; = collapse selection to cursor +// " = use register +// ` = convert case? (to lower) (alt = swap case) +// ~ = convert to upper case +// . = repeat last command +// \ = disable hook? +// / = search +// > = indent +// < = deindent +// % = select whole buffer (in vim = jump to matching bracket) +// * = search pattern in selection +// ( = rotate main selection backward +// ) = rotate main selection forward +// - = trim selections? (alt = merge contiguous sel together) +// @ = convert tabs to spaces +// & = align cursor +// ? = extend to next given regex match (alt = to prev) +// +// in kakoune these are alt-h alt-l / gh gl +// select from curs to begin end / move curs to begin end +// 0 = start of line +// ^ = start of line (first non blank char) +// $ = end of line +// +// z = save selections +// Z = restore selections +// x = select line +// X = extend line +// c = change selected text +// C = copy selection? +// v = view menu (viewport manipulation) +// b = select to previous word start +// B = select to previous WORD start +// +// +// +// +// +// +// = = align? +// + = +// } +// +// gd = goto definition +// gr = goto reference +// } + +// #[cfg(feature = "term")] +pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers}; + +// TODO: could be trie based +pub type Keymap = HashMap, Command>; +pub type Keymaps = HashMap; + +macro_rules! key { + ($ch:expr) => { + Key { + code: KeyCode::Char($ch), + modifiers: Modifiers::NONE, + } + }; +} + +macro_rules! shift { + ($ch:expr) => { + Key { + code: KeyCode::Char($ch), + modifiers: Modifiers::SHIFT, + } + }; +} + +macro_rules! ctrl { + ($ch:expr) => { + Key { + code: KeyCode::Char($ch), + modifiers: Modifiers::CONTROL, + } + }; +} + +// macro_rules! alt { +// ($ch:expr) => { +// Key { +// code: KeyCode::Char($ch), +// modifiers: Modifiers::ALT, +// } +// }; +// } + +pub fn default() -> Keymaps { + hashmap!( + Mode::Normal => + // as long as you cast the first item, rust is able to infer the other cases + hashmap!( + vec![key!('h')] => commands::move_char_left as Command, + vec![key!('j')] => commands::move_line_down, + vec![key!('k')] => commands::move_line_up, + vec![key!('0')] => commands::move_line_start, + vec![key!('$')] => commands::move_line_end, + vec![key!('l')] => commands::move_char_right, + vec![shift!('H')] => commands::extend_char_left, + vec![shift!('J')] => commands::extend_line_down, + vec![shift!('K')] => commands::extend_line_up, + vec![shift!('L')] => commands::extend_char_right, + vec![key!('w')] => commands::move_next_word_start, + vec![key!('b')] => commands::move_prev_word_start, + vec![key!('e')] => commands::move_next_word_end, + vec![key!('g')] => commands::goto_mode, + vec![key!('i')] => commands::insert_mode, + vec![shift!('I')] => commands::prepend_to_line, + vec![key!('a')] => commands::append_mode, + vec![shift!('A')] => commands::append_to_line, + vec![key!('o')] => commands::open_below, + vec![key!('d')] => commands::delete_selection, + vec![key!('c')] => commands::change_selection, + vec![key!('s')] => commands::split_selection_on_newline, + vec![key!(';')] => commands::collapse_selection, + // TODO should be alt(;) + vec![key!('%')] => commands::flip_selections, + vec![key!('x')] => commands::select_line, + vec![key!('u')] => commands::undo, + vec![shift!('U')] => commands::redo, + vec![key!('y')] => commands::yank, + vec![key!('p')] => commands::paste, + vec![key!('>')] => commands::indent, + vec![key!('<')] => commands::unindent, + vec![key!(':')] => commands::command_mode, + vec![Key { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + }] => commands::normal_mode, + vec![Key { + code: KeyCode::PageUp, + modifiers: Modifiers::NONE + }] => commands::page_up, + vec![Key { + code: KeyCode::PageDown, + modifiers: Modifiers::NONE + }] => commands::page_down, + vec![ctrl!('u')] => commands::half_page_up, + vec![ctrl!('d')] => commands::half_page_down, + ), + Mode::Insert => hashmap!( + vec![Key { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + }] => commands::normal_mode as Command, + vec![Key { + code: KeyCode::Backspace, + modifiers: Modifiers::NONE + }] => commands::insert::delete_char_backward, + vec![Key { + code: KeyCode::Delete, + modifiers: Modifiers::NONE + }] => commands::insert::delete_char_forward, + vec![Key { + code: KeyCode::Enter, + modifiers: Modifiers::NONE + }] => commands::insert::insert_newline, + vec![Key { + code: KeyCode::Tab, + modifiers: Modifiers::NONE + }] => commands::insert::insert_tab, + ), + Mode::Goto => hashmap!( + vec![Key { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + }] => commands::normal_mode as Command, + vec![key!('g')] => commands::move_file_start as Command, + vec![key!('e')] => commands::move_file_end as Command, + ), + ) +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index a43aebd8..92ab10c2 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,7 +1,10 @@ #![allow(unused)] mod application; +mod commands; mod compositor; +mod keymap; +mod prompt; use application::Application; diff --git a/helix-term/src/prompt.rs b/helix-term/src/prompt.rs new file mode 100644 index 00000000..4a39f2ec --- /dev/null +++ b/helix-term/src/prompt.rs @@ -0,0 +1,126 @@ +use crate::Editor; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +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; + } + + pub fn handle_event(&mut self, key_event: KeyEvent, editor: &mut Editor) { + match key_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)(editor, &self.line), + KeyEvent { + code: KeyCode::Tab, .. + } => self.change_completion_selection(), + KeyEvent { + code: KeyCode::Char('q'), + modifiers: KeyModifiers::CONTROL, + } => self.exit_selection(), + _ => (), + } + } +} diff --git a/helix-view/src/commands.rs b/helix-view/src/commands.rs deleted file mode 100644 index c135a3da..00000000 --- a/helix-view/src/commands.rs +++ /dev/null @@ -1,680 +0,0 @@ -use helix_core::{ - graphemes, - indent::TAB_WIDTH, - regex::Regex, - register, selection, - state::{Direction, Granularity, State}, - ChangeSet, Range, Selection, Tendril, Transaction, -}; -use once_cell::sync::Lazy; - -use crate::{ - document::Mode, - prompt::Prompt, - view::{View, PADDING}, -}; - -pub struct Context<'a, 'b> { - pub count: usize, - pub view: &'a mut View, - pub executor: &'a smol::Executor<'b>, -} - -/// A command is a function that takes the current state and a count, and does a side-effect on the -/// state (usually by creating and applying a transaction). -pub type Command = fn(cx: &mut Context); - -pub fn move_char_left(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .move_selection(Direction::Backward, Granularity::Character, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn move_char_right(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .move_selection(Direction::Forward, Granularity::Character, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn move_line_up(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .move_selection(Direction::Backward, Granularity::Line, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn move_line_down(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .move_selection(Direction::Forward, Granularity::Line, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn move_line_end(cx: &mut Context) { - let lines = selection_lines(&cx.view.doc.state); - - let positions = lines - .into_iter() - .map(|index| { - // adjust all positions to the end of the line. - - // Line end is pos at the start of next line - 1 - // subtract another 1 because the line ends with \n - cx.view.doc.text().line_to_char(index + 1).saturating_sub(2) - }) - .map(|pos| Range::new(pos, pos)); - - let selection = Selection::new(positions.collect(), 0); - - cx.view.doc.set_selection(selection); -} - -pub fn move_line_start(cx: &mut Context) { - let lines = selection_lines(&cx.view.doc.state); - - let positions = lines - .into_iter() - .map(|index| { - // adjust all positions to the start of the line. - cx.view.doc.text().line_to_char(index) - }) - .map(|pos| Range::new(pos, pos)); - - let selection = Selection::new(positions.collect(), 0); - - cx.view.doc.set_selection(selection); -} - -pub fn move_next_word_start(cx: &mut Context) { - let pos = cx.view.doc.state.move_pos( - cx.view.doc.selection().cursor(), - Direction::Forward, - Granularity::Word, - cx.count, - ); - - cx.view.doc.set_selection(Selection::point(pos)); -} - -pub fn move_prev_word_start(cx: &mut Context) { - let pos = cx.view.doc.state.move_pos( - cx.view.doc.selection().cursor(), - Direction::Backward, - Granularity::Word, - cx.count, - ); - - cx.view.doc.set_selection(Selection::point(pos)); -} - -pub fn move_next_word_end(cx: &mut Context) { - let pos = State::move_next_word_end( - &cx.view.doc.text().slice(..), - cx.view.doc.selection().cursor(), - cx.count, - ); - - cx.view.doc.set_selection(Selection::point(pos)); -} - -pub fn move_file_start(cx: &mut Context) { - cx.view.doc.set_selection(Selection::point(0)); - - cx.view.doc.mode = Mode::Normal; -} - -pub fn move_file_end(cx: &mut Context) { - let text = &cx.view.doc.text(); - let last_line = text.line_to_char(text.len_lines().saturating_sub(2)); - cx.view.doc.set_selection(Selection::point(last_line)); - - cx.view.doc.mode = Mode::Normal; -} - -pub fn check_cursor_in_view(view: &View) -> bool { - let cursor = view.doc.selection().cursor(); - let line = view.doc.text().char_to_line(cursor); - let document_end = view.first_line + view.size.1.saturating_sub(1) as usize; - - if (line > document_end.saturating_sub(PADDING)) | (line < view.first_line + PADDING) { - return false; - } - true -} - -pub fn page_up(cx: &mut Context) { - if cx.view.first_line < PADDING { - return; - } - - cx.view.first_line = cx.view.first_line.saturating_sub(cx.view.size.1 as usize); - - if !check_cursor_in_view(cx.view) { - let text = cx.view.doc.text(); - let pos = text.line_to_char(cx.view.last_line().saturating_sub(PADDING)); - cx.view.doc.set_selection(Selection::point(pos)); - } -} - -pub fn page_down(cx: &mut Context) { - cx.view.first_line += cx.view.size.1 as usize + PADDING; - - if cx.view.first_line < cx.view.doc.text().len_lines() { - let text = cx.view.doc.text(); - let pos = text.line_to_char(cx.view.first_line as usize); - cx.view.doc.set_selection(Selection::point(pos)); - } -} - -pub fn half_page_up(cx: &mut Context) { - if cx.view.first_line < PADDING { - return; - } - - cx.view.first_line = cx - .view - .first_line - .saturating_sub(cx.view.size.1 as usize / 2); - - if !check_cursor_in_view(cx.view) { - let text = &cx.view.doc.text(); - let pos = text.line_to_char(cx.view.last_line() - PADDING); - cx.view.doc.set_selection(Selection::point(pos)); - } -} - -pub fn half_page_down(cx: &mut Context) { - let lines = cx.view.doc.text().len_lines(); - if cx.view.first_line < lines.saturating_sub(cx.view.size.1 as usize) { - cx.view.first_line += cx.view.size.1 as usize / 2; - } - if !check_cursor_in_view(cx.view) { - let text = cx.view.doc.text(); - let pos = text.line_to_char(cx.view.first_line as usize); - cx.view.doc.set_selection(Selection::point(pos)); - } -} -// avoid select by default by having a visual mode switch that makes movements into selects - -pub fn extend_char_left(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .extend_selection(Direction::Backward, Granularity::Character, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn extend_char_right(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .extend_selection(Direction::Forward, Granularity::Character, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn extend_line_up(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .extend_selection(Direction::Backward, Granularity::Line, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn extend_line_down(cx: &mut Context) { - let selection = - cx.view - .doc - .state - .extend_selection(Direction::Forward, Granularity::Line, cx.count); - cx.view.doc.set_selection(selection); -} - -pub fn split_selection_on_newline(cx: &mut Context) { - let text = &cx.view.doc.text().slice(..); - // only compile the regex once - #[allow(clippy::trivial_regex)] - static REGEX: Lazy = Lazy::new(|| Regex::new(r"\n").unwrap()); - let selection = selection::split_on_matches(text, cx.view.doc.selection(), ®EX); - cx.view.doc.set_selection(selection); -} - -pub fn select_line(cx: &mut Context) { - // TODO: count - let pos = cx.view.doc.selection().primary(); - let text = cx.view.doc.text(); - let line = text.char_to_line(pos.head); - let start = text.line_to_char(line); - let end = text.line_to_char(line + 1).saturating_sub(1); - - cx.view.doc.set_selection(Selection::single(start, end)); -} - -pub fn delete_selection(cx: &mut Context) { - let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { - (range.from(), range.to() + 1, None) - }); - cx.view.doc.apply(&transaction); - - append_changes_to_history(cx); -} - -pub fn change_selection(cx: &mut Context) { - delete_selection(cx); - insert_mode(cx); -} - -pub fn collapse_selection(cx: &mut Context) { - let selection = cx - .view - .doc - .selection() - .transform(|range| Range::new(range.head, range.head)); - - cx.view.doc.set_selection(selection); -} - -pub fn flip_selections(cx: &mut Context) { - let selection = cx - .view - .doc - .selection() - .transform(|range| Range::new(range.head, range.anchor)); - - cx.view.doc.set_selection(selection); -} - -fn enter_insert_mode(cx: &mut Context) { - cx.view.doc.mode = Mode::Insert; - - append_changes_to_history(cx); -} -// inserts at the start of each selection -pub fn insert_mode(cx: &mut Context) { - enter_insert_mode(cx); - - let selection = cx - .view - .doc - .selection() - .transform(|range| Range::new(range.to(), range.from())); - cx.view.doc.set_selection(selection); -} - -// inserts at the end of each selection -pub fn append_mode(cx: &mut Context) { - enter_insert_mode(cx); - cx.view.doc.restore_cursor = true; - - // TODO: as transaction - let text = &cx.view.doc.text().slice(..); - let selection = cx.view.doc.selection().transform(|range| { - // TODO: to() + next char - Range::new( - range.from(), - graphemes::next_grapheme_boundary(text, range.to()), - ) - }); - cx.view.doc.set_selection(selection); -} - -// TODO: I, A, o and O can share a lot of the primitives. -pub fn command_mode(_cx: &mut Context) { - unimplemented!() -} - -// calculate line numbers for each selection range -fn selection_lines(state: &State) -> Vec { - let mut lines = state - .selection - .ranges() - .iter() - .map(|range| state.doc.char_to_line(range.head)) - .collect::>(); - - lines.sort_unstable(); // sorting by usize so _unstable is preferred - lines.dedup(); - - lines -} - -// I inserts at the start of each line with a selection -pub fn prepend_to_line(cx: &mut Context) { - enter_insert_mode(cx); - - move_line_start(cx); -} - -// A inserts at the end of each line with a selection -pub fn append_to_line(cx: &mut Context) { - enter_insert_mode(cx); - - move_line_end(cx); -} - -// o inserts a new line after each line with a selection -pub fn open_below(cx: &mut Context) { - enter_insert_mode(cx); - - let lines = selection_lines(&cx.view.doc.state); - - let positions: Vec<_> = lines - .into_iter() - .map(|index| { - // adjust all positions to the end of the line/start of the next one. - cx.view.doc.text().line_to_char(index + 1) - }) - .collect(); - - // TODO: use same logic as insert_newline for indentation - let changes = positions.iter().copied().map(|index| - // generate changes - (index, index, Some(Tendril::from_char('\n')))); - - // TODO: count actually inserts "n" new lines and starts editing on all of them. - // TODO: append "count" newlines and modify cursors to those lines - - let selection = Selection::new( - positions - .iter() - .copied() - .map(|pos| Range::new(pos, pos)) - .collect(), - 0, - ); - - let transaction = Transaction::change(&cx.view.doc.state, changes).with_selection(selection); - - cx.view.doc.apply(&transaction); -} - -// O inserts a new line before each line with a selection - -fn append_changes_to_history(cx: &mut Context) { - if cx.view.doc.changes.is_empty() { - return; - } - - let new_changeset = ChangeSet::new(cx.view.doc.text()); - let changes = std::mem::replace(&mut cx.view.doc.changes, new_changeset); - // Instead of doing this messy merge we could always commit, and based on transaction - // annotations either add a new layer or compose into the previous one. - let transaction = Transaction::from(changes).with_selection(cx.view.doc.selection().clone()); - - // increment document version - // TODO: needs to happen on undo/redo too - cx.view.doc.version += 1; - - // TODO: trigger lsp/documentDidChange with changes - - // HAXX: we need to reconstruct the state as it was before the changes.. - let old_state = std::mem::replace(&mut cx.view.doc.old_state, cx.view.doc.state.clone()); - // TODO: take transaction by value? - cx.view - .doc - .history - .commit_revision(&transaction, &old_state); - - // TODO: notify LSP of changes -} - -pub fn normal_mode(cx: &mut Context) { - cx.view.doc.mode = Mode::Normal; - - append_changes_to_history(cx); - - // if leaving append mode, move cursor back by 1 - if cx.view.doc.restore_cursor { - let text = &cx.view.doc.text().slice(..); - let selection = cx.view.doc.selection().transform(|range| { - Range::new( - range.from(), - graphemes::prev_grapheme_boundary(text, range.to()), - ) - }); - cx.view.doc.set_selection(selection); - - cx.view.doc.restore_cursor = false; - } -} - -pub fn goto_mode(cx: &mut Context) { - cx.view.doc.mode = Mode::Goto; -} - -// NOTE: Transactions in this module get appended to history when we switch back to normal mode. -pub mod insert { - use super::*; - // TODO: insert means add text just before cursor, on exit we should be on the last letter. - pub fn insert_char(cx: &mut Context, c: char) { - let c = Tendril::from_char(c); - let transaction = Transaction::insert(&cx.view.doc.state, c); - - cx.view.doc.apply(&transaction); - } - - pub fn insert_tab(cx: &mut Context) { - insert_char(cx, '\t'); - } - - pub fn insert_newline(cx: &mut Context) { - let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { - let indent_level = helix_core::indent::suggested_indent_for_pos( - cx.view.doc.syntax.as_ref(), - &cx.view.doc.state, - range.head, - ); - let indent = " ".repeat(TAB_WIDTH).repeat(indent_level); - let mut text = String::with_capacity(1 + indent.len()); - text.push('\n'); - text.push_str(&indent); - (range.head, range.head, Some(text.into())) - }); - cx.view.doc.apply(&transaction); - } - - // TODO: handle indent-aware delete - pub fn delete_char_backward(cx: &mut Context) { - let text = &cx.view.doc.text().slice(..); - let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { - ( - graphemes::nth_prev_grapheme_boundary(text, range.head, cx.count), - range.head, - None, - ) - }); - cx.view.doc.apply(&transaction); - } - - pub fn delete_char_forward(cx: &mut Context) { - let text = &cx.view.doc.text().slice(..); - let transaction = Transaction::change_by_selection(&cx.view.doc.state, |range| { - ( - range.head, - graphemes::nth_next_grapheme_boundary(text, range.head, cx.count), - None, - ) - }); - cx.view.doc.apply(&transaction); - } -} - -pub fn insert_char_prompt(prompt: &mut Prompt, c: char) { - prompt.insert_char(c); -} - -// Undo / Redo - -pub fn undo(cx: &mut Context) { - if let Some(revert) = cx.view.doc.history.undo() { - cx.view.doc.version += 1; - cx.view.doc.apply(&revert); - } - - // TODO: each command could simply return a Option, then the higher level handles storing it? -} - -pub fn redo(cx: &mut Context) { - if let Some(transaction) = cx.view.doc.history.redo() { - cx.view.doc.version += 1; - cx.view.doc.apply(&transaction); - } -} - -// Yank / Paste - -pub fn yank(cx: &mut Context) { - // TODO: should selections be made end inclusive? - let values = cx - .view - .doc - .state - .selection() - .fragments(&cx.view.doc.text().slice(..)) - .map(|cow| cow.into_owned()) - .collect(); - - // TODO: allow specifying reg - let reg = '"'; - register::set(reg, values); -} - -pub fn paste(cx: &mut Context) { - // TODO: allow specifying reg - let reg = '"'; - if let Some(values) = register::get(reg) { - let repeat = std::iter::repeat( - values - .last() - .map(|value| Tendril::from_slice(value)) - .unwrap(), - ); - - // TODO: if any of values ends \n it's linewise paste - // - // p => paste after - // P => paste before - // alt-p => paste every yanked selection after selected text - // alt-P => paste every yanked selection before selected text - // R => replace selected text with yanked text - // alt-R => replace selected text with every yanked text - // - // append => insert at next line - // insert => insert at start of line - // replace => replace - // default insert - - let linewise = values.iter().any(|value| value.ends_with('\n')); - - let mut values = values.into_iter().map(Tendril::from).chain(repeat); - - let transaction = if linewise { - // paste on the next line - // TODO: can simply take a range + modifier and compute the right pos without ifs - let text = cx.view.doc.text(); - Transaction::change_by_selection(&cx.view.doc.state, |range| { - let line_end = text.line_to_char(text.char_to_line(range.head) + 1); - (line_end, line_end, Some(values.next().unwrap())) - }) - } else { - Transaction::change_by_selection(&cx.view.doc.state, |range| { - (range.head + 1, range.head + 1, Some(values.next().unwrap())) - }) - }; - - cx.view.doc.apply(&transaction); - append_changes_to_history(cx); - } -} - -fn get_lines(view: &View) -> Vec { - let mut lines = Vec::new(); - - // Get all line numbers - for range in view.doc.selection().ranges() { - let start = view.doc.text().char_to_line(range.from()); - let end = view.doc.text().char_to_line(range.to()); - - for line in start..=end { - lines.push(line) - } - } - lines.sort_unstable(); // sorting by usize so _unstable is preferred - lines.dedup(); - lines -} - -pub fn indent(cx: &mut Context) { - let lines = get_lines(cx.view); - - // Indent by one level - let indent = Tendril::from(" ".repeat(TAB_WIDTH)); - - let transaction = Transaction::change( - &cx.view.doc.state, - lines.into_iter().map(|line| { - let pos = cx.view.doc.text().line_to_char(line); - (pos, pos, Some(indent.clone())) - }), - ); - cx.view.doc.apply(&transaction); - append_changes_to_history(cx); -} - -pub fn unindent(cx: &mut Context) { - let lines = get_lines(cx.view); - let mut changes = Vec::with_capacity(lines.len()); - - for line_idx in lines { - let line = cx.view.doc.text().line(line_idx); - let mut width = 0; - - for ch in line.chars() { - match ch { - ' ' => width += 1, - '\t' => width = (width / TAB_WIDTH + 1) * TAB_WIDTH, - _ => break, - } - - if width >= TAB_WIDTH { - break; - } - } - - if width > 0 { - let start = cx.view.doc.text().line_to_char(line_idx); - changes.push((start, start + width, None)) - } - } - - let transaction = Transaction::change(&cx.view.doc.state, changes.into_iter()); - - cx.view.doc.apply(&transaction); - append_changes_to_history(cx); -} - -pub fn indent_selection(_cx: &mut Context) { - // loop over each line and recompute proper indentation - unimplemented!() -} - -// - -pub fn save(cx: &mut Context) { - // Spawns an async task to actually do the saving. This way we prevent blocking. - - // TODO: handle save errors somehow? - cx.executor.spawn(cx.view.doc.save()).detach(); -} diff --git a/helix-view/src/keymap.rs b/helix-view/src/keymap.rs deleted file mode 100644 index c815911e..00000000 --- a/helix-view/src/keymap.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::commands::{self, Command}; -use crate::document::Mode; -use helix_core::hashmap; -use std::collections::HashMap; - -// Kakoune-inspired: -// mode = { -// normal = { -// q = record_macro -// w = (next) word -// W = next WORD -// e = end of word -// E = end of WORD -// r = -// t = 'till char -// y = yank -// u = undo -// U = redo -// i = insert -// I = INSERT (start of line) -// o = open below (insert on new line below) -// O = open above (insert on new line above) -// p = paste (before cursor) -// P = PASTE (after cursor) -// ` = -// [ = select to text object start (alt = select whole object) -// ] = select to text object end -// { = extend to inner object start -// } = extend to inner object end -// a = append -// A = APPEND (end of line) -// s = split -// S = select -// d = delete() -// f = find_char() -// g = goto (gg, G, gc, gd, etc) -// -// h = move_char_left(n) -// j = move_line_down(n) -// k = move_line_up(n) -// l = move_char_right(n) -// : = command line -// ; = collapse selection to cursor -// " = use register -// ` = convert case? (to lower) (alt = swap case) -// ~ = convert to upper case -// . = repeat last command -// \ = disable hook? -// / = search -// > = indent -// < = deindent -// % = select whole buffer (in vim = jump to matching bracket) -// * = search pattern in selection -// ( = rotate main selection backward -// ) = rotate main selection forward -// - = trim selections? (alt = merge contiguous sel together) -// @ = convert tabs to spaces -// & = align cursor -// ? = extend to next given regex match (alt = to prev) -// -// in kakoune these are alt-h alt-l / gh gl -// select from curs to begin end / move curs to begin end -// 0 = start of line -// ^ = start of line (first non blank char) -// $ = end of line -// -// z = save selections -// Z = restore selections -// x = select line -// X = extend line -// c = change selected text -// C = copy selection? -// v = view menu (viewport manipulation) -// b = select to previous word start -// B = select to previous WORD start -// -// -// -// -// -// -// = = align? -// + = -// } -// -// gd = goto definition -// gr = goto reference -// } - -#[cfg(feature = "term")] -pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers}; - -// TODO: could be trie based -pub type Keymap = HashMap, Command>; -pub type Keymaps = HashMap; - -macro_rules! key { - ($ch:expr) => { - Key { - code: KeyCode::Char($ch), - modifiers: Modifiers::NONE, - } - }; -} - -macro_rules! shift { - ($ch:expr) => { - Key { - code: KeyCode::Char($ch), - modifiers: Modifiers::SHIFT, - } - }; -} - -macro_rules! ctrl { - ($ch:expr) => { - Key { - code: KeyCode::Char($ch), - modifiers: Modifiers::CONTROL, - } - }; -} - -// macro_rules! alt { -// ($ch:expr) => { -// Key { -// code: KeyCode::Char($ch), -// modifiers: Modifiers::ALT, -// } -// }; -// } - -pub fn default() -> Keymaps { - hashmap!( - Mode::Normal => - // as long as you cast the first item, rust is able to infer the other cases - hashmap!( - vec![key!('h')] => commands::move_char_left as Command, - vec![key!('j')] => commands::move_line_down, - vec![key!('k')] => commands::move_line_up, - vec![key!('0')] => commands::move_line_start, - vec![key!('$')] => commands::move_line_end, - vec![key!('l')] => commands::move_char_right, - vec![shift!('H')] => commands::extend_char_left, - vec![shift!('J')] => commands::extend_line_down, - vec![shift!('K')] => commands::extend_line_up, - vec![shift!('L')] => commands::extend_char_right, - vec![key!('w')] => commands::move_next_word_start, - vec![key!('b')] => commands::move_prev_word_start, - vec![key!('e')] => commands::move_next_word_end, - vec![key!('g')] => commands::goto_mode, - vec![key!('i')] => commands::insert_mode, - vec![shift!('I')] => commands::prepend_to_line, - vec![key!('a')] => commands::append_mode, - vec![shift!('A')] => commands::append_to_line, - vec![key!('o')] => commands::open_below, - vec![key!('d')] => commands::delete_selection, - vec![key!('c')] => commands::change_selection, - vec![key!('s')] => commands::split_selection_on_newline, - vec![key!(';')] => commands::collapse_selection, - // TODO should be alt(;) - vec![key!('%')] => commands::flip_selections, - vec![key!('x')] => commands::select_line, - vec![key!('u')] => commands::undo, - vec![shift!('U')] => commands::redo, - vec![key!('y')] => commands::yank, - vec![key!('p')] => commands::paste, - vec![key!('>')] => commands::indent, - vec![key!('<')] => commands::unindent, - vec![key!(':')] => commands::command_mode, - vec![Key { - code: KeyCode::Esc, - modifiers: Modifiers::NONE - }] => commands::normal_mode, - vec![Key { - code: KeyCode::PageUp, - modifiers: Modifiers::NONE - }] => commands::page_up, - vec![Key { - code: KeyCode::PageDown, - modifiers: Modifiers::NONE - }] => commands::page_down, - vec![ctrl!('u')] => commands::half_page_up, - vec![ctrl!('d')] => commands::half_page_down, - ), - Mode::Insert => hashmap!( - vec![Key { - code: KeyCode::Esc, - modifiers: Modifiers::NONE - }] => commands::normal_mode as Command, - vec![Key { - code: KeyCode::Backspace, - modifiers: Modifiers::NONE - }] => commands::insert::delete_char_backward, - vec![Key { - code: KeyCode::Delete, - modifiers: Modifiers::NONE - }] => commands::insert::delete_char_forward, - vec![Key { - code: KeyCode::Enter, - modifiers: Modifiers::NONE - }] => commands::insert::insert_newline, - vec![Key { - code: KeyCode::Tab, - modifiers: Modifiers::NONE - }] => commands::insert::insert_tab, - ), - Mode::Goto => hashmap!( - vec![Key { - code: KeyCode::Esc, - modifiers: Modifiers::NONE - }] => commands::normal_mode as Command, - vec![key!('g')] => commands::move_file_start as Command, - vec![key!('e')] => commands::move_file_end as Command, - ), - ) -} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 3b923744..f28c8116 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -1,8 +1,5 @@ -pub mod commands; pub mod document; pub mod editor; -pub mod keymap; -pub mod prompt; pub mod theme; pub mod view; diff --git a/helix-view/src/prompt.rs b/helix-view/src/prompt.rs deleted file mode 100644 index e2a9c80d..00000000 --- a/helix-view/src/prompt.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::Editor; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -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; - } - - pub fn handle_input(&mut self, key_event: KeyEvent, editor: &mut Editor) { - match key_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)(editor, &self.line), - KeyEvent { - code: KeyCode::Tab, .. - } => self.change_completion_selection(), - KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::CONTROL, - } => self.exit_selection(), - _ => (), - } - } -} -- cgit v1.2.3-70-g09d2 From ada3f92c5b96e4c66f5647c4ac2487f3903692b3 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 11 Dec 2020 18:25:09 +0900 Subject: wip: Getting the new prompt to render in a new layer. --- helix-term/src/application.rs | 182 +++++++++++++----------------------------- helix-term/src/commands.rs | 57 ++++++++++++- helix-term/src/compositor.rs | 36 ++++++--- helix-term/src/prompt.rs | 29 +++++-- 4 files changed, 159 insertions(+), 145 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8c454b5d..35d698d1 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -42,16 +42,15 @@ type Terminal = tui::Terminal>; const BASE_WIDTH: u16 = 30; pub struct Application<'a> { - prompt: Option, - compositor: Compositor, + editor: Editor, renderer: Renderer, executor: &'a smol::Executor<'a>, language_server: helix_lsp::Client, } -pub(crate) struct Renderer { +pub struct Renderer { size: (u16, u16), terminal: Terminal, surface: Surface, @@ -263,6 +262,11 @@ impl Renderer { 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, @@ -271,7 +275,7 @@ impl Renderer { ); } - pub fn render_prompt(&mut self, view: &View, prompt: &Prompt, theme: &Theme) { + 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 @@ -343,15 +347,6 @@ impl Renderer { let pos = if let Some(prompt) = prompt { Position::new(self.size.0 as usize, 2 + prompt.cursor) } else { - if let Some(path) = view.doc.path() { - self.surface.set_string( - 6, - self.size.1 - 1, - path.to_string_lossy(), - self.text_color, - ); - } - let cursor = view.doc.state.selection().cursor(); let mut pos = view @@ -367,146 +362,77 @@ impl Renderer { } struct EditorView { - editor: Editor, - prompt: Option, // TODO: this is None for now, make a layer keymap: Keymaps, } impl EditorView { - fn new(editor: Editor) -> Self { + fn new() -> Self { Self { - editor, - prompt: None, keymap: keymap::default(), } } } +use crate::compositor::Context; + impl Component for EditorView { - fn handle_event(&mut self, event: Event, executor: &smol::Executor) -> EventResult { + 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) = self.editor.view_mut() { + 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) = self.editor.view_mut() { + 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 - match view.doc.mode() { + 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) { - let mut cx = commands::Context { - view, - executor, - count: 1, - }; - command(&mut cx); } else if let KeyEvent { code: KeyCode::Char(c), .. } = event { - let mut cx = commands::Context { - view, - executor, - count: 1, - }; commands::insert::insert_char(&mut cx, c); } - view.ensure_cursor_in_view(); } Mode::Normal => { - if let &[KeyEvent { - code: KeyCode::Char(':'), - .. - }] = keys.as_slice() - { - let prompt = Prompt::new( - ":".to_owned(), - |_input: &str| { - // TODO: i need this duplicate list right now to avoid borrow checker issues - let command_list = vec![ - String::from("q"), - String::from("aaa"), - String::from("bbb"), - String::from("ccc"), - String::from("ddd"), - String::from("eee"), - String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"), - String::from("q"), - String::from("aaa"), - String::from("bbb"), - String::from("ccc"), - String::from("ddd"), - String::from("eee"), - String::from("q"), - String::from("aaa"), - String::from("bbb"), - String::from("ccc"), - String::from("ddd"), - String::from("eee"), - String::from("q"), - String::from("aaa"), - String::from("bbb"), - String::from("ccc"), - String::from("ddd"), - String::from("eee"), - String::from("q"), - String::from("aaa"), - String::from("bbb"), - String::from("ccc"), - String::from("ddd"), - String::from("eee"), - ]; - command_list - .into_iter() - .filter(|command| command.contains(_input)) - .collect() - }, // completion - |editor: &mut Editor, input: &str| match input { - "q" => editor.should_close = true, - _ => (), - }, - ); - - self.prompt = Some(prompt); - - // HAXX: special casing for command mode - } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) { - let mut cx = commands::Context { - view, - executor, - count: 1, - }; + if let Some(command) = self.keymap[&Mode::Normal].get(&keys) { command(&mut cx); // TODO: simplistic ensure cursor in view for now - view.ensure_cursor_in_view(); } } mode => { if let Some(command) = self.keymap[&mode].get(&keys) { - let mut cx = commands::Context { - view, - executor, - count: 1, - }; command(&mut cx); // TODO: simplistic ensure cursor in view for now - view.ensure_cursor_in_view(); } } } - EventResult::Consumed(None) + // appease borrowck + let callback = cx.callback.take(); + + view.ensure_cursor_in_view(); + + EventResult::Consumed(callback) } else { EventResult::Ignored } @@ -514,19 +440,19 @@ impl Component for EditorView { Event::Mouse(_) => EventResult::Ignored, } } - fn render(&mut self, renderer: &mut Renderer) { + 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 { &*(&self.editor.theme as *const Theme) }; - if let Some(view) = self.editor.view_mut() { + 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(self.editor.view().unwrap(), self.prompt.as_ref(), viewport); + renderer.render_cursor(cx.editor.view().unwrap(), None, viewport); } } @@ -540,11 +466,12 @@ impl<'a> Application<'a> { } let mut compositor = Compositor::new(); - compositor.push(Box::new(EditorView::new(editor))); + compositor.push(Box::new(EditorView::new())); let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]); let mut app = Self { + editor, renderer, // TODO; move to state compositor, @@ -558,7 +485,11 @@ impl<'a> Application<'a> { fn render(&mut self) { self.renderer.surface.reset(); // reset is faster than allocating new empty surface - self.compositor.render(&mut self.renderer); // viewport, + 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(); } @@ -569,17 +500,16 @@ impl<'a> Application<'a> { self.language_server.initialize().await.unwrap(); // TODO: temp // self.language_server - // .text_document_did_open(&self.editor.view().unwrap().doc) + // .text_document_did_open(&cx.editor.view().unwrap().doc) // .await // .unwrap(); self.render(); loop { - // TODO: - // if self.editor.should_close { - // break; - // } + if self.editor.should_close { + break; + } use futures_util::{select, FutureExt}; select! { @@ -594,26 +524,26 @@ impl<'a> Application<'a> { } pub fn handle_terminal_events(&mut self, event: Option>) { + let mut cx = crate::compositor::Context { + editor: &mut self.editor, + executor: &self.executor, + }; // Handle key events - match event { + let should_redraw = match event { Some(Ok(Event::Resize(width, height))) => { self.renderer.resize(width, height); - // TODO: use the response self.compositor - .handle_event(Event::Resize(width, height), self.executor); - - self.render(); - } - Some(Ok(event)) => { - // TODO: use the response - self.compositor.handle_event(event, self.executor); - - self.render(); + .handle_event(Event::Resize(width, height), &mut cx) } + Some(Ok(event)) => self.compositor.handle_event(event, &mut cx), Some(Err(x)) => panic!(x), None => panic!(), }; + + if should_redraw { + self.render(); + } } pub async fn handle_language_server_message(&mut self, call: Option) { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a791f243..04482ef7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -9,17 +9,21 @@ use helix_core::{ use once_cell::sync::Lazy; +use crate::compositor::Compositor; use crate::prompt::Prompt; use helix_view::{ document::Mode, view::{View, PADDING}, + Editor, }; pub struct Context<'a, 'b> { pub count: usize, pub view: &'a mut View, pub executor: &'a smol::Executor<'b>, + + pub callback: Option, } /// A command is a function that takes the current state and a count, and does a side-effect on the @@ -333,8 +337,57 @@ pub fn append_mode(cx: &mut Context) { } // TODO: I, A, o and O can share a lot of the primitives. -pub fn command_mode(_cx: &mut Context) { - unimplemented!() +pub fn command_mode(cx: &mut Context) { + cx.callback = Some(Box::new(|compositor: &mut Compositor| { + let prompt = Prompt::new( + ":".to_owned(), + |_input: &str| { + // TODO: i need this duplicate list right now to avoid borrow checker issues + let command_list = vec![ + String::from("q"), + String::from("aaa"), + String::from("bbb"), + String::from("ccc"), + String::from("ddd"), + String::from("eee"), + String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"), + String::from("q"), + String::from("aaa"), + String::from("bbb"), + String::from("ccc"), + String::from("ddd"), + String::from("eee"), + String::from("q"), + String::from("aaa"), + String::from("bbb"), + String::from("ccc"), + String::from("ddd"), + String::from("eee"), + String::from("q"), + String::from("aaa"), + String::from("bbb"), + String::from("ccc"), + String::from("ddd"), + String::from("eee"), + String::from("q"), + String::from("aaa"), + String::from("bbb"), + String::from("ccc"), + String::from("ddd"), + String::from("eee"), + ]; + command_list + .into_iter() + .filter(|command| command.contains(_input)) + .collect() + }, // completion + |editor: &mut Editor, input: &str| match input { + "q" => editor.should_close = true, + _ => (), + }, + ); + compositor.push(Box::new(prompt)); + })); } // calculate line numbers for each selection range diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 158a8b28..3cf6bf03 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -18,7 +18,7 @@ use crossterm::event::Event; use smol::Executor; use tui::buffer::Buffer as Surface; -pub(crate) type Callback = Box; +pub type Callback = Box; // --> EventResult should have a callback that takes a context with methods like .popup(), // .prompt() etc. That way we can abstract it from the renderer. @@ -29,14 +29,21 @@ pub(crate) type Callback = Box; // If Compositor was specified in the callback that's then problematic because of // Cursive-inspired -pub(crate) enum EventResult { +pub enum EventResult { Ignored, Consumed(Option), } -pub(crate) trait Component { +use helix_view::{Editor, View}; +// shared with commands.rs +pub struct Context<'a, 'b> { + pub editor: &'a mut Editor, + pub executor: &'a smol::Executor<'b>, +} + +pub trait Component { /// Process input events, return true if handled. - fn handle_event(&mut self, event: Event, executor: &Executor) -> EventResult; + fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult; // , args: () /// Should redraw? Useful for saving redraw cycles if we know component didn't change. @@ -44,7 +51,7 @@ pub(crate) trait Component { true } - fn render(&mut self, renderer: &mut Renderer); + fn render(&mut self, renderer: &mut Renderer, ctx: &mut Context); } // struct Editor { }; @@ -94,7 +101,7 @@ pub(crate) trait Component { // // 2) Alternatively, //} -pub(crate) struct Compositor { +pub struct Compositor { layers: Vec>, } @@ -111,17 +118,24 @@ impl Compositor { self.layers.pop(); } - pub fn handle_event(&mut self, event: Event, executor: &Executor) -> () { + pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { // TODO: custom focus if let Some(layer) = self.layers.last_mut() { - layer.handle_event(event, executor); - // return should_update + return match layer.handle_event(event, cx) { + EventResult::Consumed(Some(callback)) => { + callback(self); + true + } + EventResult::Consumed(None) => true, + EventResult::Ignored => false, + }; } + false } - pub fn render(&mut self, renderer: &mut Renderer) { + pub fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) { for layer in &mut self.layers { - layer.render(renderer) + layer.render(renderer, cx) } } } diff --git a/helix-term/src/prompt.rs b/helix-term/src/prompt.rs index 4a39f2ec..689eac66 100644 --- a/helix-term/src/prompt.rs +++ b/helix-term/src/prompt.rs @@ -1,5 +1,9 @@ -use crate::Editor; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crate::{ + application::Renderer, + compositor::{Component, Context, EventResult}, +}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use helix_view::Editor; use std::string::String; pub struct Prompt { @@ -79,9 +83,16 @@ impl Prompt { pub fn exit_selection(&mut self) { self.completion_selection_index = None; } +} + +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, + }; - pub fn handle_event(&mut self, key_event: KeyEvent, editor: &mut Editor) { - match key_event { + match event { KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE, @@ -112,7 +123,7 @@ impl Prompt { KeyEvent { code: KeyCode::Enter, .. - } => (self.callback_fn)(editor, &self.line), + } => (self.callback_fn)(cx.editor, &self.line), KeyEvent { code: KeyCode::Tab, .. } => self.change_completion_selection(), @@ -121,6 +132,12 @@ impl Prompt { modifiers: KeyModifiers::CONTROL, } => self.exit_selection(), _ => (), - } + }; + + EventResult::Consumed(None) + } + + fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) { + renderer.render_prompt(self, &cx.editor.theme) } } -- cgit v1.2.3-70-g09d2 From 29cb33300b1486c778e9318e87e60c26695c2520 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 12 Dec 2020 20:18:44 +0900 Subject: wip --- helix-term/src/application.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 35d698d1..589aaf6e 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -336,6 +336,7 @@ impl Renderer { .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) { @@ -412,13 +413,6 @@ impl Component for EditorView { commands::insert::insert_char(&mut cx, c); } } - Mode::Normal => { - if let Some(command) = self.keymap[&Mode::Normal].get(&keys) { - command(&mut cx); - - // TODO: simplistic ensure cursor in view for now - } - } mode => { if let Some(command) = self.keymap[&mode].get(&keys) { command(&mut cx); @@ -484,7 +478,6 @@ impl<'a> Application<'a> { } fn render(&mut self) { - self.renderer.surface.reset(); // reset is faster than allocating new empty surface let mut cx = crate::compositor::Context { editor: &mut self.editor, executor: &self.executor, @@ -543,6 +536,7 @@ impl<'a> Application<'a> { if should_redraw { self.render(); + // calling render twice here fixes it for some reason } } -- cgit v1.2.3-70-g09d2 From 8695415fbfe927250f68e93793660e3c4e4a70b4 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 13 Dec 2020 12:23:50 +0900 Subject: 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 (limited to 'helix-term/src') diff --git a/Cargo.lock b/Cargo.lock index 331934f8..1c8c86c7 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 4c349a13..22af1b40 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 00000000..dc4fcf6f --- /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 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>; - -const BASE_WIDTH: u16 = 30; +type Terminal = crate::terminal::Terminal>; -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 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), ) -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 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 = 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 +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() + } +} -- cgit v1.2.3-70-g09d2 From ef0d062b1fd202fe89bc4bbd33826c46f660ef70 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 13 Dec 2020 13:29:34 +0900 Subject: Fix cursor positioning. --- helix-term/src/application.rs | 30 ++++-------------------------- helix-term/src/component.rs | 20 -------------------- helix-term/src/compositor.rs | 14 ++++++++++++++ helix-term/src/editor_view.rs | 24 ++++++++++++++++++++---- helix-term/src/prompt.rs | 8 ++++++++ 5 files changed, 46 insertions(+), 50 deletions(-) delete mode 100644 helix-term/src/component.rs (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 7a74f8ba..c25871c7 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -9,7 +9,6 @@ use crate::prompt::Prompt; use log::{debug, info}; use std::{ - borrow::Cow, io::{self, stdout, Stdout, Write}, path::PathBuf, time::Duration, @@ -47,31 +46,9 @@ pub struct Application { // TODO: temp #[inline(always)] pub fn text_color() -> Style { - return Style::default().fg(Color::Rgb(219, 191, 239)); // lilac + Style::default().fg(Color::Rgb(219, 191, 239)) // lilac } -// 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()); @@ -106,13 +83,14 @@ impl Application { 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); + let pos = compositor.cursor_position(area, &mut cx); self.terminal.draw(); + self.terminal.set_cursor(pos.col as u16, pos.row as u16); } pub async fn event_loop(&mut self) { diff --git a/helix-term/src/component.rs b/helix-term/src/component.rs deleted file mode 100644 index 08d6c620..00000000 --- a/helix-term/src/component.rs +++ /dev/null @@ -1,20 +0,0 @@ -// IDEA: render to a cache buffer, then if not changed, copy the buf into the parent -type Surface = (); -pub trait Component { - /// Process input events, return true if handled. - fn process_event(&mut self, event: crossterm::event::Event, args: ()) -> bool; - /// Should redraw? Useful for saving redraw cycles if we know component didn't change. - fn should_update(&self) -> bool { - true - } - - fn render(&mut self, surface: &mut Surface, args: ()); -} - -// HStack / VStack -// focus by component id: each View/Editor gets it's own incremental id at create -// Component: View(Arc) -> multiple views can point to same state -// id 0 = prompt? -// when entering to prompt, it needs to direct Commands to last focus window -// -> prompt.trigger(focus_id), on_leave -> focus(focus_id) -// popups on another layer diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 1d94ee63..2e65f02a 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -14,6 +14,7 @@ // cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), ) use crossterm::event::Event; +use helix_core::Position; use smol::Executor; use tui::buffer::Buffer as Surface; use tui::layout::Rect; @@ -52,6 +53,10 @@ pub trait Component { } fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context); + + fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option { + None + } } // struct Editor { }; @@ -138,4 +143,13 @@ impl Compositor { layer.render(area, surface, cx) } } + + pub fn cursor_position(&self, area: Rect, cx: &mut Context) -> Position { + for layer in self.layers.iter().rev() { + if let Some(pos) = layer.cursor_position(area, cx) { + return pos; + } + } + panic!("No layer returned a position!"); + } } diff --git a/helix-term/src/editor_view.rs b/helix-term/src/editor_view.rs index 0181623a..b778e79b 100644 --- a/helix-term/src/editor_view.rs +++ b/helix-term/src/editor_view.rs @@ -21,6 +21,8 @@ pub struct EditorView { keymap: Keymaps, } +const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter + impl EditorView { pub fn new() -> Self { Self { @@ -34,11 +36,10 @@ impl EditorView { 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); + self.render_statusline(view, area, surface, theme); } // TODO: ideally not &mut View but highlights require it because of cursor cache @@ -218,7 +219,7 @@ impl EditorView { }; // statusline surface.set_style( - Rect::new(0, viewport.y, viewport.height, 1), + Rect::new(0, viewport.y, viewport.width, 1), theme.get("ui.statusline"), ); surface.set_string(1, viewport.y, mode, text_color()); @@ -306,6 +307,21 @@ impl Component for EditorView { } // TODO: drop unwrap - // TODO: !!! self.render_cursor(cx.editor.view().unwrap(), None, viewport); + } + + 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/prompt.rs b/helix-term/src/prompt.rs index 4747c9f5..7f473ebc 100644 --- a/helix-term/src/prompt.rs +++ b/helix-term/src/prompt.rs @@ -1,5 +1,6 @@ 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; @@ -200,4 +201,11 @@ impl Component for Prompt { 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 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 (limited to 'helix-term/src') 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 From 07801b60bccd0f084eae925e0290c24322de575f Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 13 Dec 2020 13:57:28 +0900 Subject: Remove the prompt on ESC. --- helix-term/src/ui/prompt.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 071cac90..ce00a129 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -1,4 +1,4 @@ -use crate::compositor::{Component, Context, EventResult}; +use crate::compositor::{Component, Compositor, Context, EventResult}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use helix_core::Position; use helix_view::Editor; @@ -161,7 +161,12 @@ impl Component for Prompt { } => self.insert_char(c), KeyEvent { code: KeyCode::Esc, .. - } => self.should_close = true, + } => { + return EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + // remove the layer + compositor.pop(); + }))); + } KeyEvent { code: KeyCode::Right, .. -- cgit v1.2.3-70-g09d2