aboutsummaryrefslogtreecommitdiff
path: root/helix-view/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view/src')
-rw-r--r--helix-view/src/commands.rs216
-rw-r--r--helix-view/src/keymap.rs131
-rw-r--r--helix-view/src/lib.rs6
-rw-r--r--helix-view/src/theme.rs179
-rw-r--r--helix-view/src/view.rs48
5 files changed, 580 insertions, 0 deletions
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<usize> {
+ let mut lines = state
+ .selection
+ .ranges()
+ .iter()
+ .map(|range| state.doc.char_to_line(range.head))
+ .collect::<Vec<_>>();
+
+ 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<Vec<Key>, Command>;
+type Keymaps = HashMap<state::Mode, Keymap>;
+
+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<tui::style::Color> 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<Self, Self::Err> {
+// 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<Color>,
+// pub bg: Option<Color>,
+// // 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<tui::style::Style> 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<String>,
+ mapping: HashMap<&'static str, Style>,
+}
+
+// let highlight_names: Vec<String> = [
+// "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<View, Error> {
+ 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);
+ }
+ }
+}