From 935cfeae576f734e6cbd455bfa39df014700ae86 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 21 Sep 2020 18:24:16 +0900 Subject: Split parts of helix-term into helix-view. It still largely depends on term for some types but I plan to change that later. --- Cargo.lock | 12 ++- Cargo.toml | 1 + README.md | 9 ++ helix-core/src/lib.rs | 2 + helix-core/src/macros.rs | 17 ++++ helix-core/src/state.rs | 2 +- helix-term/Cargo.toml | 4 +- helix-term/src/commands.rs | 216 --------------------------------------------- helix-term/src/editor.rs | 68 ++++---------- helix-term/src/keymap.rs | 134 ---------------------------- helix-term/src/macros.rs | 16 ---- helix-term/src/main.rs | 5 -- helix-term/src/theme.rs | 98 -------------------- helix-view/Cargo.toml | 18 ++++ helix-view/src/commands.rs | 216 +++++++++++++++++++++++++++++++++++++++++++++ helix-view/src/keymap.rs | 131 +++++++++++++++++++++++++++ helix-view/src/lib.rs | 6 ++ helix-view/src/theme.rs | 179 +++++++++++++++++++++++++++++++++++++ helix-view/src/view.rs | 48 ++++++++++ 19 files changed, 657 insertions(+), 525 deletions(-) create mode 100644 helix-core/src/macros.rs delete mode 100644 helix-term/src/commands.rs delete mode 100644 helix-term/src/keymap.rs delete mode 100644 helix-term/src/macros.rs delete mode 100644 helix-term/src/theme.rs create mode 100644 helix-view/Cargo.toml create mode 100644 helix-view/src/commands.rs create mode 100644 helix-view/src/keymap.rs create mode 100644 helix-view/src/lib.rs create mode 100644 helix-view/src/theme.rs create mode 100644 helix-view/src/view.rs diff --git a/Cargo.lock b/Cargo.lock index f9b52d7d..f37c951c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,12 +410,22 @@ dependencies = [ "argh", "crossterm", "helix-core", - "helix-syntax", + "helix-view", "num_cpus", "smol", "tui", ] +[[package]] +name = "helix-view" +version = "0.1.0" +dependencies = [ + "anyhow", + "crossterm", + "helix-core", + "tui", +] + [[package]] name = "hermit-abi" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index db3530c9..25dbe725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "helix-core", + "helix-view", "helix-term", "helix-syntax", ] diff --git a/README.md b/README.md index 325ff522..3a0ac317 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +# Helix + +| Crate | Description | +| ----------- | ----------- | +| helix-core | Core editing primitives, functional. | +| helix-syntax | Tree-sitter grammars | +| helix-view | UI abstractions for use in backends, imperative shell. | +| helix-term | Terminal UI | + - server-client architecture via gRPC, UI separate from core - multi cursor based editing and slicing - WASM based plugins (builtin LSP & fuzzy file finder) diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index c617fdbf..e443168e 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,5 +1,7 @@ #![allow(unused)] +pub mod config; pub mod graphemes; +pub mod macros; mod position; mod selection; pub mod state; diff --git a/helix-core/src/macros.rs b/helix-core/src/macros.rs new file mode 100644 index 00000000..1321ea5f --- /dev/null +++ b/helix-core/src/macros.rs @@ -0,0 +1,17 @@ +#[macro_export] +macro_rules! hashmap { + (@single $($x:tt)*) => (()); + (@count $($rest:expr),*) => (<[()]>::len(&[$(hashmap!(@single $rest)),*])); + + ($($key:expr => $value:expr,)+) => { hashmap!($($key => $value),+) }; + ($($key:expr => $value:expr),*) => { + { + let _cap = hashmap!(@count $($key),*); + let mut _map = ::std::collections::HashMap::with_capacity(_cap); + $( + let _ = _map.insert($key, $value); + )* + _map + } + }; +} diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs index 5b5f06c0..79e15eff 100644 --- a/helix-core/src/state.rs +++ b/helix-core/src/state.rs @@ -4,7 +4,7 @@ use anyhow::Error; use std::path::PathBuf; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { Normal, Insert, diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index e0b96ead..d32fc0b9 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -12,12 +12,12 @@ path = "src/main.rs" [dependencies] helix-core = { path = "../helix-core" } -helix-syntax = { path = "../helix-syntax" } +helix-view = { path = "../helix-view", features = ["term"]} anyhow = "1" argh = "0.1.3" -crossterm = { version = "0.17", features = ["event-stream"] } smol = "1" num_cpus = "1.13.0" tui = { version = "0.10.0", default-features = false, features = ["crossterm"] } +crossterm = { version = "0.17", features = ["event-stream"] } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs deleted file mode 100644 index 269a7743..00000000 --- a/helix-term/src/commands.rs +++ /dev/null @@ -1,216 +0,0 @@ -use helix_core::{ - graphemes, - state::{Direction, Granularity, Mode, State}, - ChangeSet, Range, Selection, Tendril, Transaction, -}; - -use crate::editor::View; - -/// 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(view: &mut View, count: usize); - -pub fn move_char_left(view: &mut View, count: usize) { - // TODO: use a transaction - let selection = view - .state - .move_selection(Direction::Backward, Granularity::Character, count); - view.state.selection = selection; -} - -pub fn move_char_right(view: &mut View, count: usize) { - // TODO: use a transaction - view.state.selection = - view.state - .move_selection(Direction::Forward, Granularity::Character, count); -} - -pub fn move_line_up(view: &mut View, count: usize) { - // TODO: use a transaction - view.state.selection = view - .state - .move_selection(Direction::Backward, Granularity::Line, count); -} - -pub fn move_line_down(view: &mut View, count: usize) { - // TODO: use a transaction - view.state.selection = view - .state - .move_selection(Direction::Forward, Granularity::Line, count); -} - -// avoid select by default by having a visual mode switch that makes movements into selects - -// insert mode: -// first we calculate the correct cursors/selections -// then we just append at each cursor -// lastly, if it was append mode we shift cursor by 1? - -// inserts at the start of each selection -pub fn insert_mode(view: &mut View, _count: usize) { - view.state.mode = Mode::Insert; - - view.state.selection = view - .state - .selection - .transform(|range| Range::new(range.to(), range.from())) -} - -// inserts at the end of each selection -pub fn append_mode(view: &mut View, _count: usize) { - view.state.mode = Mode::Insert; - - // TODO: as transaction - let text = &view.state.doc.slice(..); - view.state.selection = view.state.selection.transform(|range| { - // TODO: to() + next char - Range::new( - range.from(), - graphemes::next_grapheme_boundary(text, range.to()), - ) - }) -} - -// TODO: I, A, o and O can share a lot of the primitives. - -// 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(); - lines.dedup(); - - lines -} - -// I inserts at the start of each line with a selection -pub fn prepend_to_line(view: &mut View, _count: usize) { - view.state.mode = Mode::Insert; - - let lines = selection_lines(&view.state); - - let positions = lines - .into_iter() - .map(|index| { - // adjust all positions to the start of the line. - view.state.doc.line_to_char(index) - }) - .map(|pos| Range::new(pos, pos)); - - let selection = Selection::new(positions.collect(), 0); - - let transaction = Transaction::new(&mut view.state).with_selection(selection); - - transaction.apply(&mut view.state); - // TODO: need to store into history if successful -} - -// A inserts at the end of each line with a selection -pub fn append_to_line(view: &mut View, _count: usize) { - view.state.mode = Mode::Insert; - - let lines = selection_lines(&view.state); - - let positions = lines - .into_iter() - .map(|index| { - // adjust all positions to the end of the line. - let line = view.state.doc.line(index); - let line_start = view.state.doc.line_to_char(index); - line_start + line.len_chars() - 1 - }) - .map(|pos| Range::new(pos, pos)); - - let selection = Selection::new(positions.collect(), 0); - - let transaction = Transaction::new(&mut view.state).with_selection(selection); - - transaction.apply(&mut view.state); - // TODO: need to store into history if successful -} - -// o inserts a new line after each line with a selection -pub fn open_below(view: &mut View, _count: usize) { - view.state.mode = Mode::Insert; - - let lines = selection_lines(&view.state); - - let positions: Vec<_> = lines - .into_iter() - .map(|index| { - // adjust all positions to the end of the line. - let line = view.state.doc.line(index); - let line_start = view.state.doc.line_to_char(index); - line_start + line.len_chars() - }) - .collect(); - - 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(&view.state, changes).with_selection(selection); - - transaction.apply(&mut view.state); - // TODO: need to store into history if successful -} - -// O inserts a new line before each line with a selection - -pub fn normal_mode(view: &mut View, _count: usize) { - // TODO: if leaving append mode, move cursor back by 1 - view.state.mode = Mode::Normal; -} - -// TODO: insert means add text just before cursor, on exit we should be on the last letter. -pub fn insert_char(view: &mut View, c: char) { - let c = Tendril::from_char(c); - let transaction = Transaction::insert(&view.state, c); - - transaction.apply(&mut view.state); - // TODO: need to store into history if successful -} - -// TODO: handle indent-aware delete -pub fn delete_char_backward(view: &mut View, count: usize) { - let text = &view.state.doc.slice(..); - let transaction = Transaction::change_by_selection(&view.state, |range| { - ( - graphemes::nth_prev_grapheme_boundary(text, range.head, count), - range.head, - None, - ) - }); - transaction.apply(&mut view.state); - // TODO: need to store into history if successful -} - -pub fn delete_char_forward(view: &mut View, count: usize) { - let text = &view.state.doc.slice(..); - let transaction = Transaction::change_by_selection(&view.state, |range| { - ( - graphemes::nth_next_grapheme_boundary(text, range.head, count), - range.head, - None, - ) - }); - transaction.apply(&mut view.state); - // TODO: need to store into history if successful -} diff --git a/helix-term/src/editor.rs b/helix-term/src/editor.rs index 0542d217..e29e7ee8 100644 --- a/helix-term/src/editor.rs +++ b/helix-term/src/editor.rs @@ -1,10 +1,11 @@ -use crate::{commands, keymap, theme::Theme, Args}; +use crate::Args; use helix_core::{ state::coords_at_pos, state::Mode, syntax::{HighlightConfiguration, HighlightEvent, Highlighter}, State, }; +use helix_view::{commands, keymap, View}; use std::{ io::{self, stdout, Write}, @@ -31,19 +32,12 @@ type Terminal = tui::Terminal>; static EX: smol::Executor = smol::Executor::new(); -pub struct View { - pub state: State, - pub first_line: u16, - pub size: (u16, u16), -} - pub struct Editor { terminal: Terminal, view: Option, size: (u16, u16), surface: Surface, cache: Surface, - theme: Theme, } impl Editor { @@ -53,7 +47,6 @@ impl Editor { let mut terminal = Terminal::new(backend)?; let size = terminal::size().unwrap(); let area = Rect::new(0, 0, size.0, size.1); - let theme = Theme::default(); let mut editor = Editor { terminal, @@ -61,7 +54,6 @@ impl Editor { size, surface: Surface::empty(area), cache: Surface::empty(area), - theme, // TODO; move to state }; @@ -73,20 +65,7 @@ impl Editor { } pub fn open(&mut self, path: PathBuf) -> Result<(), Error> { - let mut state = State::load(path)?; - state - .syntax - .as_mut() - .unwrap() - .configure(self.theme.scopes()); - - let view = View { - state, - first_line: 0, - size: self.size, - }; - - self.view = Some(view); + self.view = Some(View::open(path, self.size)?); Ok(()) } @@ -102,7 +81,7 @@ impl Editor { // clear with background color self.surface - .set_style(area, self.theme.get("ui.background")); + .set_style(area, view.theme.get("ui.background").into()); let offset = 5 + 1; // 5 linenr + 1 gutter let viewport = Rect::new(offset, 0, self.size.0, self.size.1 - 1); // - 1 for statusline @@ -161,7 +140,9 @@ impl Editor { use helix_core::graphemes::{grapheme_width, RopeGraphemes}; let style = match spans.first() { - Some(span) => self.theme.get(self.theme.scopes()[span.0].as_str()), + Some(span) => { + view.theme.get(view.theme.scopes()[span.0].as_str()).into() + } None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender }; @@ -202,7 +183,7 @@ impl Editor { } let mut line = 0; - let style = self.theme.get("ui.linenr"); + let style: Style = view.theme.get("ui.linenr").into(); for i in view.first_line..(last_line as u16) { self.surface .set_stringn(0, line, format!("{:>5}", i + 1), 5, style); // lavender @@ -241,7 +222,7 @@ impl Editor { }; self.surface.set_style( Rect::new(0, self.size.1 - 1, self.size.0, 1), - self.theme.get("ui.statusline"), + view.theme.get("ui.statusline").into(), ); // TODO: unfocused one with different color let text_color = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac @@ -296,7 +277,9 @@ impl Editor { self.cache = Surface::empty(area); // TODO: simplistic ensure cursor in view for now - self.ensure_cursor_in_view(); + if let Some(view) = &mut self.view { + view.ensure_cursor_in_view() + }; self.render(); } @@ -333,18 +316,19 @@ impl Editor { _ => (), // skip } // TODO: simplistic ensure cursor in view for now - self.ensure_cursor_in_view(); + view.ensure_cursor_in_view(); self.render(); } Mode::Normal => { // TODO: handle modes and sequences (`gg`) - if let Some(command) = keymap.get(&event) { + let keys = vec![event]; + if let Some(command) = keymap[&Mode::Normal].get(&keys) { // TODO: handle count other than 1 command(view, 1); // TODO: simplistic ensure cursor in view for now - self.ensure_cursor_in_view(); + view.ensure_cursor_in_view(); self.render(); } @@ -361,26 +345,6 @@ impl Editor { } } - fn ensure_cursor_in_view(&mut self) { - if let Some(view) = &mut self.view { - let cursor = view.state.selection().cursor(); - let line = view.state.doc().char_to_line(cursor) as u16; - let document_end = view.first_line + self.size.1.saturating_sub(1) - 1; - - let padding = 5u16; - - // TODO: side scroll - - if line > document_end.saturating_sub(padding) { - // scroll down - view.first_line += line - (document_end.saturating_sub(padding)); - } else if line < view.first_line + padding { - // scroll up - view.first_line = line.saturating_sub(padding); - } - } - } - pub async fn run(&mut self) -> Result<(), Error> { enable_raw_mode()?; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs deleted file mode 100644 index d52ccca4..00000000 --- a/helix-term/src/keymap.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::commands::{self, Command}; -use crossterm::{ - event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers}, - execute, - style::Print, -}; -use std::collections::HashMap; - -// Kakoune-inspired: -// mode = { -// normal = { -// q = record_macro -// w = (next) 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) -// -// 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? -// + = -// } -// } - -type Keymap = HashMap; - -pub fn default() -> Keymap { - hashmap!( - Key { - code: KeyCode::Char('h'), - modifiers: Modifiers::NONE - } => commands::move_char_left as Command, - Key { - code: KeyCode::Char('j'), - modifiers: Modifiers::NONE - } => commands::move_line_down as Command, - Key { - code: KeyCode::Char('k'), - modifiers: Modifiers::NONE - } => commands::move_line_up as Command, - Key { - code: KeyCode::Char('l'), - modifiers: Modifiers::NONE - } => commands::move_char_right as Command, - Key { - code: KeyCode::Char('i'), - modifiers: Modifiers::NONE - } => commands::insert_mode as Command, - Key { - code: KeyCode::Char('I'), - modifiers: Modifiers::SHIFT, - } => commands::prepend_to_line as Command, - Key { - code: KeyCode::Char('a'), - modifiers: Modifiers::NONE - } => commands::append_mode as Command, - Key { - code: KeyCode::Char('A'), - modifiers: Modifiers::SHIFT, - } => commands::append_to_line as Command, - Key { - code: KeyCode::Char('o'), - modifiers: Modifiers::NONE - } => commands::open_below as Command, - Key { - code: KeyCode::Esc, - modifiers: Modifiers::NONE - } => commands::normal_mode as Command, - ) - - // hashmap!( - // Key { - // code: KeyCode::Esc, - // modifiers: Modifiers::NONE - // } => commands::normal_mode as Command, - // ) -} diff --git a/helix-term/src/macros.rs b/helix-term/src/macros.rs deleted file mode 100644 index 3b22a786..00000000 --- a/helix-term/src/macros.rs +++ /dev/null @@ -1,16 +0,0 @@ -macro_rules! hashmap { - (@single $($x:tt)*) => (()); - (@count $($rest:expr),*) => (<[()]>::len(&[$(hashmap!(@single $rest)),*])); - - ($($key:expr => $value:expr,)+) => { hashmap!($($key => $value),+) }; - ($($key:expr => $value:expr),*) => { - { - let _cap = hashmap!(@count $($key),*); - let mut _map = ::std::collections::HashMap::with_capacity(_cap); - $( - let _ = _map.insert($key, $value); - )* - _map - } - }; -} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aca04641..b691eb65 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,11 +1,6 @@ #![allow(unused)] -#[macro_use] -mod macros; -mod commands; mod editor; -mod keymap; -mod theme; use editor::Editor; diff --git a/helix-term/src/theme.rs b/helix-term/src/theme.rs deleted file mode 100644 index 4b2f102e..00000000 --- a/helix-term/src/theme.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::collections::HashMap; -use tui::style::{Color, Style}; - -/// Color theme for syntax highlighting. -pub struct Theme { - scopes: Vec, - mapping: HashMap<&'static str, Style>, -} - -// let highlight_names: Vec = [ -// "attribute", -// "constant.builtin", -// "constant", -// "function.builtin", -// "function.macro", -// "function", -// "keyword", -// "operator", -// "property", -// "punctuation", -// "comment", -// "escape", -// "label", -// // "punctuation.bracket", -// "punctuation.delimiter", -// "string", -// "string.special", -// "tag", -// "type", -// "type.builtin", -// "constructor", -// "variable", -// "variable.builtin", -// "variable.parameter", -// "path", -// ]; - -impl Default for Theme { - fn default() -> Self { - let mapping = hashmap! { - "attribute" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac - "keyword" => Style::default().fg(Color::Rgb(236, 205, 186)), // almond - "punctuation" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender - "punctuation.delimiter" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender - "operator" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac - "property" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender - "variable.parameter" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender - // TODO distinguish type from type.builtin? - "type" => Style::default().fg(Color::Rgb(255, 255, 255)), // white - "type.builtin" => Style::default().fg(Color::Rgb(255, 255, 255)), // white - "constructor" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac - "function" => Style::default().fg(Color::Rgb(255, 255, 255)), // white - "function.macro" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac - "comment" => Style::default().fg(Color::Rgb(105, 124, 129)), // sirocco - "variable.builtin" => Style::default().fg(Color::Rgb(159, 242, 143)), // mint - "constant" => Style::default().fg(Color::Rgb(255, 255, 255)), // white - "constant.builtin" => Style::default().fg(Color::Rgb(255, 255, 255)), // white - "string" => Style::default().fg(Color::Rgb(204, 204, 204)), // silver - "escape" => Style::default().fg(Color::Rgb(239, 186, 93)), // honey - // used for lifetimes - "label" => Style::default().fg(Color::Rgb(239, 186, 93)), // honey - - // TODO: diferentiate number builtin - // TODO: diferentiate doc comment - // TODO: variable as lilac - // TODO: mod/use statements as white - // TODO: mod stuff as chamoise - // TODO: add "(scoped_identifier) @path" for std::mem:: - // - // concat (ERROR) @syntax-error and "MISSING ;" selectors for errors - - "module" => Style::default().fg(Color::Rgb(255, 0, 0)), // white - "variable" => Style::default().fg(Color::Rgb(255, 0, 0)), // white - "function.builtin" => Style::default().fg(Color::Rgb(255, 0, 0)), // white - - "ui.background" => Style::default().bg(Color::Rgb(59, 34, 76)), // midnight - "ui.linenr" => Style::default().fg(Color::Rgb(90, 89, 119)), // comet - "ui.statusline" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver - }; - - let scopes = mapping.keys().map(ToString::to_string).collect(); - - Self { mapping, scopes } - } -} - -impl Theme { - pub fn get(&self, scope: &str) -> Style { - self.mapping - .get(scope) - .copied() - .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255))) - } - - pub fn scopes(&self) -> &[String] { - &self.scopes - } -} diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml new file mode 100644 index 00000000..37957625 --- /dev/null +++ b/helix-view/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "helix-view" +version = "0.1.0" +authors = ["Blaž Hrastnik "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +term = ["tui", "crossterm"] + +[dependencies] +anyhow = "1.0.32" +helix-core = { path = "../helix-core" } + +# Conversion traits +tui = { version = "0.10.0", default-features = false, features = ["crossterm"], optional = true} +crossterm = { version = "0.17", features = ["event-stream"], optional = true} diff --git a/helix-view/src/commands.rs b/helix-view/src/commands.rs new file mode 100644 index 00000000..560167c9 --- /dev/null +++ b/helix-view/src/commands.rs @@ -0,0 +1,216 @@ +use helix_core::{ + graphemes, + state::{Direction, Granularity, Mode, State}, + Range, Selection, Tendril, Transaction, +}; + +use crate::view::View; + +/// 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(view: &mut View, count: usize); + +pub fn move_char_left(view: &mut View, count: usize) { + // TODO: use a transaction + let selection = view + .state + .move_selection(Direction::Backward, Granularity::Character, count); + view.state.selection = selection; +} + +pub fn move_char_right(view: &mut View, count: usize) { + // TODO: use a transaction + view.state.selection = + view.state + .move_selection(Direction::Forward, Granularity::Character, count); +} + +pub fn move_line_up(view: &mut View, count: usize) { + // TODO: use a transaction + view.state.selection = view + .state + .move_selection(Direction::Backward, Granularity::Line, count); +} + +pub fn move_line_down(view: &mut View, count: usize) { + // TODO: use a transaction + view.state.selection = view + .state + .move_selection(Direction::Forward, Granularity::Line, count); +} + +// avoid select by default by having a visual mode switch that makes movements into selects + +// insert mode: +// first we calculate the correct cursors/selections +// then we just append at each cursor +// lastly, if it was append mode we shift cursor by 1? + +// inserts at the start of each selection +pub fn insert_mode(view: &mut View, _count: usize) { + view.state.mode = Mode::Insert; + + view.state.selection = view + .state + .selection + .transform(|range| Range::new(range.to(), range.from())) +} + +// inserts at the end of each selection +pub fn append_mode(view: &mut View, _count: usize) { + view.state.mode = Mode::Insert; + + // TODO: as transaction + let text = &view.state.doc.slice(..); + view.state.selection = view.state.selection.transform(|range| { + // TODO: to() + next char + Range::new( + range.from(), + graphemes::next_grapheme_boundary(text, range.to()), + ) + }) +} + +// TODO: I, A, o and O can share a lot of the primitives. + +// 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(); + lines.dedup(); + + lines +} + +// I inserts at the start of each line with a selection +pub fn prepend_to_line(view: &mut View, _count: usize) { + view.state.mode = Mode::Insert; + + let lines = selection_lines(&view.state); + + let positions = lines + .into_iter() + .map(|index| { + // adjust all positions to the start of the line. + view.state.doc.line_to_char(index) + }) + .map(|pos| Range::new(pos, pos)); + + let selection = Selection::new(positions.collect(), 0); + + let transaction = Transaction::new(&mut view.state).with_selection(selection); + + transaction.apply(&mut view.state); + // TODO: need to store into history if successful +} + +// A inserts at the end of each line with a selection +pub fn append_to_line(view: &mut View, _count: usize) { + view.state.mode = Mode::Insert; + + let lines = selection_lines(&view.state); + + let positions = lines + .into_iter() + .map(|index| { + // adjust all positions to the end of the line. + let line = view.state.doc.line(index); + let line_start = view.state.doc.line_to_char(index); + line_start + line.len_chars() - 1 + }) + .map(|pos| Range::new(pos, pos)); + + let selection = Selection::new(positions.collect(), 0); + + let transaction = Transaction::new(&mut view.state).with_selection(selection); + + transaction.apply(&mut view.state); + // TODO: need to store into history if successful +} + +// o inserts a new line after each line with a selection +pub fn open_below(view: &mut View, _count: usize) { + view.state.mode = Mode::Insert; + + let lines = selection_lines(&view.state); + + let positions: Vec<_> = lines + .into_iter() + .map(|index| { + // adjust all positions to the end of the line. + let line = view.state.doc.line(index); + let line_start = view.state.doc.line_to_char(index); + line_start + line.len_chars() + }) + .collect(); + + 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(&view.state, changes).with_selection(selection); + + transaction.apply(&mut view.state); + // TODO: need to store into history if successful +} + +// O inserts a new line before each line with a selection + +pub fn normal_mode(view: &mut View, _count: usize) { + // TODO: if leaving append mode, move cursor back by 1 + view.state.mode = Mode::Normal; +} + +// TODO: insert means add text just before cursor, on exit we should be on the last letter. +pub fn insert_char(view: &mut View, c: char) { + let c = Tendril::from_char(c); + let transaction = Transaction::insert(&view.state, c); + + transaction.apply(&mut view.state); + // TODO: need to store into history if successful +} + +// TODO: handle indent-aware delete +pub fn delete_char_backward(view: &mut View, count: usize) { + let text = &view.state.doc.slice(..); + let transaction = Transaction::change_by_selection(&view.state, |range| { + ( + graphemes::nth_prev_grapheme_boundary(text, range.head, count), + range.head, + None, + ) + }); + transaction.apply(&mut view.state); + // TODO: need to store into history if successful +} + +pub fn delete_char_forward(view: &mut View, count: usize) { + let text = &view.state.doc.slice(..); + let transaction = Transaction::change_by_selection(&view.state, |range| { + ( + graphemes::nth_next_grapheme_boundary(text, range.head, count), + range.head, + None, + ) + }); + transaction.apply(&mut view.state); + // TODO: need to store into history if successful +} diff --git a/helix-view/src/keymap.rs b/helix-view/src/keymap.rs new file mode 100644 index 00000000..705357a8 --- /dev/null +++ b/helix-view/src/keymap.rs @@ -0,0 +1,131 @@ +use crate::commands::{self, Command}; +use helix_core::{hashmap, state}; +use std::collections::HashMap; + +// Kakoune-inspired: +// mode = { +// normal = { +// q = record_macro +// w = (next) 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) +// +// 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? +// + = +// } +// } + +#[cfg(feature = "term")] +pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers}; + +// TODO: could be trie based +type Keymap = HashMap, Command>; +type Keymaps = HashMap; + +pub fn default() -> Keymaps { + hashmap!( + state::Mode::Normal => + hashmap!( + vec![Key { + code: KeyCode::Char('h'), + modifiers: Modifiers::NONE + }] => commands::move_char_left as Command, + vec![Key { + code: KeyCode::Char('j'), + modifiers: Modifiers::NONE + }] => commands::move_line_down as Command, + vec![Key { + code: KeyCode::Char('k'), + modifiers: Modifiers::NONE + }] => commands::move_line_up as Command, + vec![Key { + code: KeyCode::Char('l'), + modifiers: Modifiers::NONE + }] => commands::move_char_right as Command, + vec![Key { + code: KeyCode::Char('i'), + modifiers: Modifiers::NONE + }] => commands::insert_mode as Command, + vec![Key { + code: KeyCode::Char('I'), + modifiers: Modifiers::SHIFT, + }] => commands::prepend_to_line as Command, + vec![Key { + code: KeyCode::Char('a'), + modifiers: Modifiers::NONE + }] => commands::append_mode as Command, + vec![Key { + code: KeyCode::Char('A'), + modifiers: Modifiers::SHIFT, + }] => commands::append_to_line as Command, + vec![Key { + code: KeyCode::Char('o'), + modifiers: Modifiers::NONE + }] => commands::open_below as Command, + vec![Key { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + }] => commands::normal_mode as Command, + ) + ) +} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs new file mode 100644 index 00000000..2a000f32 --- /dev/null +++ b/helix-view/src/lib.rs @@ -0,0 +1,6 @@ +pub mod commands; +pub mod keymap; +pub mod theme; +pub mod view; + +pub use view::View; diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs new file mode 100644 index 00000000..d61457d7 --- /dev/null +++ b/helix-view/src/theme.rs @@ -0,0 +1,179 @@ +use helix_core::hashmap; +use std::collections::HashMap; + +#[cfg(feature = "term")] +pub use tui::style::{Color, Style}; + +// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)] +// pub struct Color { +// pub r: u8, +// pub g: u8, +// pub b: u8, +// } + +// impl Color { +// pub fn new(r: u8, g: u8, b: u8) -> Self { +// Self { r, g, b } +// } +// } + +// #[cfg(feature = "term")] +// impl Into for Color { +// fn into(self) -> tui::style::Color { +// tui::style::Color::Rgb(self.r, self.g, self.b) +// } +// } + +// impl std::str::FromStr for Color { +// type Err = (); + +// /// Tries to parse a string (`'#FFFFFF'` or `'FFFFFF'`) into RGB. +// fn from_str(input: &str) -> Result { +// let input = input.trim(); +// let input = match (input.chars().next(), input.len()) { +// (Some('#'), 7) => &input[1..], +// (_, 6) => input, +// _ => return Err(()), +// }; + +// u32::from_str_radix(&input, 16) +// .map(|s| Color { +// r: ((s >> 16) & 0xFF) as u8, +// g: ((s >> 8) & 0xFF) as u8, +// b: (s & 0xFF) as u8, +// }) +// .map_err(|_| ()) +// } +// } + +// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)] +// pub struct Style { +// pub fg: Option, +// pub bg: Option, +// // TODO: modifiers (bold, underline, italic, etc) +// } + +// impl Style { +// pub fn fg(mut self, fg: Color) -> Self { +// self.fg = Some(fg); +// self +// } + +// pub fn bg(mut self, bg: Color) -> Self { +// self.bg = Some(bg); +// self +// } +// } + +// #[cfg(feature = "term")] +// impl Into for Style { +// fn into(self) -> tui::style::Style { +// let style = tui::style::Style::default(); + +// if let Some(fg) = self.fg { +// style.fg(fg.into()); +// } + +// if let Some(bg) = self.bg { +// style.bg(bg.into()); +// } + +// style +// } +// } + +/// Color theme for syntax highlighting. +pub struct Theme { + scopes: Vec, + mapping: HashMap<&'static str, Style>, +} + +// let highlight_names: Vec = [ +// "attribute", +// "constant.builtin", +// "constant", +// "function.builtin", +// "function.macro", +// "function", +// "keyword", +// "operator", +// "property", +// "punctuation", +// "comment", +// "escape", +// "label", +// // "punctuation.bracket", +// "punctuation.delimiter", +// "string", +// "string.special", +// "tag", +// "type", +// "type.builtin", +// "constructor", +// "variable", +// "variable.builtin", +// "variable.parameter", +// "path", +// ]; + +impl Default for Theme { + fn default() -> Self { + let mapping = hashmap! { + "attribute" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "keyword" => Style::default().fg(Color::Rgb(236, 205, 186)), // almond + "punctuation" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + "punctuation.delimiter" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + "operator" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "property" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + "variable.parameter" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + // TODO distinguish type from type.builtin? + "type" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "type.builtin" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "constructor" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "function" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "function.macro" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "comment" => Style::default().fg(Color::Rgb(105, 124, 129)), // sirocco + "variable.builtin" => Style::default().fg(Color::Rgb(159, 242, 143)), // mint + "constant" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "constant.builtin" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "string" => Style::default().fg(Color::Rgb(204, 204, 204)), // silver + "escape" => Style::default().fg(Color::Rgb(239, 186, 93)), // honey + // used for lifetimes + "label" => Style::default().fg(Color::Rgb(239, 186, 93)), // honey + + // TODO: diferentiate number builtin + // TODO: diferentiate doc comment + // TODO: variable as lilac + // TODO: mod/use statements as white + // TODO: mod stuff as chamoise + // TODO: add "(scoped_identifier) @path" for std::mem:: + // + // concat (ERROR) @syntax-error and "MISSING ;" selectors for errors + + "module" => Style::default().fg(Color::Rgb(255, 0, 0)), // white + "variable" => Style::default().fg(Color::Rgb(255, 0, 0)), // white + "function.builtin" => Style::default().fg(Color::Rgb(255, 0, 0)), // white + + "ui.background" => Style::default().bg(Color::Rgb(59, 34, 76)), // midnight + "ui.linenr" => Style::default().fg(Color::Rgb(90, 89, 119)), // comet + "ui.statusline" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver + }; + + let scopes = mapping.keys().map(ToString::to_string).collect(); + + Self { mapping, scopes } + } +} + +impl Theme { + pub fn get(&self, scope: &str) -> Style { + self.mapping + .get(scope) + .copied() + .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255))) + } + + pub fn scopes(&self) -> &[String] { + &self.scopes + } +} diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs new file mode 100644 index 00000000..3f7a9974 --- /dev/null +++ b/helix-view/src/view.rs @@ -0,0 +1,48 @@ +use anyhow::Error; + +use std::path::PathBuf; + +use crate::theme::Theme; +use helix_core::State; + +pub struct View { + pub state: State, + pub first_line: u16, + pub size: (u16, u16), + pub theme: Theme, // TODO: share one instance +} + +impl View { + pub fn open(path: PathBuf, size: (u16, u16)) -> Result { + let mut state = State::load(path)?; + let theme = Theme::default(); + state.syntax.as_mut().unwrap().configure(theme.scopes()); + + let view = View { + state, + first_line: 0, + size, // TODO: pass in from term + theme, + }; + + Ok(view) + } + + pub fn ensure_cursor_in_view(&mut self) { + let cursor = self.state.selection().cursor(); + let line = self.state.doc().char_to_line(cursor) as u16; + let document_end = self.first_line + self.size.1.saturating_sub(1) - 1; + + let padding = 5u16; + + // TODO: side scroll + + if line > document_end.saturating_sub(padding) { + // scroll down + self.first_line += line - (document_end.saturating_sub(padding)); + } else if line < self.first_line + padding { + // scroll up + self.first_line = line.saturating_sub(padding); + } + } +} -- cgit v1.2.3-70-g09d2