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. --- 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 ++++++++++ 5 files changed, 580 insertions(+) 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 (limited to 'helix-view/src') 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