aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs216
-rw-r--r--helix-term/src/editor.rs92
-rw-r--r--helix-term/src/keymap.rs2
-rw-r--r--helix-term/src/main.rs2
4 files changed, 271 insertions, 41 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
new file mode 100644
index 00000000..269a7743
--- /dev/null
+++ b/helix-term/src/commands.rs
@@ -0,0 +1,216 @@
+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<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-term/src/editor.rs b/helix-term/src/editor.rs
index 76bdffb1..0542d217 100644
--- a/helix-term/src/editor.rs
+++ b/helix-term/src/editor.rs
@@ -1,4 +1,4 @@
-use crate::{keymap, theme::Theme, Args};
+use crate::{commands, keymap, theme::Theme, Args};
use helix_core::{
state::coords_at_pos,
state::Mode,
@@ -31,10 +31,15 @@ type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
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,
- state: Option<State>,
- first_line: u16,
+ view: Option<View>,
size: (u16, u16),
surface: Surface,
cache: Surface,
@@ -52,8 +57,7 @@ impl Editor {
let mut editor = Editor {
terminal,
- state: None,
- first_line: 0,
+ view: None,
size,
surface: Surface::empty(area),
cache: Surface::empty(area),
@@ -75,7 +79,14 @@ impl Editor {
.as_mut()
.unwrap()
.configure(self.theme.scopes());
- self.state = Some(state);
+
+ let view = View {
+ state,
+ first_line: 0,
+ size: self.size,
+ };
+
+ self.view = Some(view);
Ok(())
}
@@ -83,8 +94,8 @@ impl Editor {
use tui::backend::Backend;
use tui::style::Color;
// TODO: ideally not mut but highlights require it because of cursor cache
- match &mut self.state {
- Some(state) => {
+ match &mut self.view {
+ Some(view) => {
let area = Rect::new(0, 0, self.size.0, self.size.1);
let mut stdout = stdout();
self.surface.reset(); // reset is faster than allocating new empty surface
@@ -97,18 +108,18 @@ impl Editor {
let viewport = Rect::new(offset, 0, self.size.0, self.size.1 - 1); // - 1 for statusline
// TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
- let source_code = state.doc().to_string();
+ let source_code = view.state.doc().to_string();
let last_line = std::cmp::min(
- (self.first_line + viewport.height - 1) as usize,
- state.doc().len_lines() - 1,
+ (view.first_line + viewport.height - 1) as usize,
+ view.state.doc().len_lines() - 1,
);
let range = {
// calculate viewport byte ranges
- let start = state.doc().line_to_byte(self.first_line.into());
- let end = state.doc().line_to_byte(last_line)
- + state.doc().line(last_line).len_bytes();
+ let start = view.state.doc().line_to_byte(view.first_line.into());
+ let end = view.state.doc().line_to_byte(last_line)
+ + view.state.doc().line(last_line).len_bytes();
start..end
};
@@ -117,7 +128,8 @@ impl Editor {
// TODO: cache highlight results
// TODO: only recalculate when state.doc is actually modified
- let highlights: Vec<_> = state
+ let highlights: Vec<_> = view
+ .state
.syntax
.as_mut()
.unwrap()
@@ -141,10 +153,10 @@ impl Editor {
HighlightEvent::Source { start, end } => {
// TODO: filter out spans out of viewport for now..
- let start = state.doc().byte_to_char(start);
- let end = state.doc().byte_to_char(end);
+ let start = view.state.doc().byte_to_char(start);
+ let end = view.state.doc().byte_to_char(end);
- let text = state.doc().slice(start..end);
+ let text = view.state.doc().slice(start..end);
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
@@ -191,7 +203,7 @@ impl Editor {
let mut line = 0;
let style = self.theme.get("ui.linenr");
- for i in self.first_line..(last_line as u16) {
+ for i in view.first_line..(last_line as u16) {
self.surface
.set_stringn(0, line, format!("{:>5}", i + 1), 5, style); // lavender
line += 1;
@@ -223,7 +235,7 @@ impl Editor {
// }
// statusline
- let mode = match state.mode() {
+ let mode = match view.state.mode() {
Mode::Insert => "INS",
Mode::Normal => "NOR",
};
@@ -235,7 +247,7 @@ impl Editor {
let text_color = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
self.surface
.set_string(1, self.size.1 - 1, mode, text_color);
- if let Some(path) = state.path() {
+ if let Some(path) = view.state.path() {
self.surface
.set_string(6, self.size.1 - 1, path.to_string_lossy(), text_color);
}
@@ -247,19 +259,19 @@ impl Editor {
std::mem::swap(&mut self.surface, &mut self.cache);
// set cursor shape
- match state.mode() {
+ match view.state.mode() {
Mode::Insert => write!(stdout, "\x1B[6 q"),
Mode::Normal => write!(stdout, "\x1B[2 q"),
};
// render the cursor
- let pos = state.selection().cursor();
- let coords = coords_at_pos(&state.doc().slice(..), pos);
+ let pos = view.state.selection().cursor();
+ let coords = coords_at_pos(&view.state.doc().slice(..), pos);
execute!(
stdout,
cursor::MoveTo(
coords.col as u16 + viewport.x,
- coords.row as u16 - self.first_line + viewport.y,
+ coords.row as u16 - view.first_line + viewport.y,
)
);
}
@@ -295,29 +307,29 @@ impl Editor {
break;
}
Some(Ok(Event::Key(event))) => {
- if let Some(state) = &mut self.state {
- match state.mode() {
+ if let Some(view) = &mut self.view {
+ match view.state.mode() {
Mode::Insert => {
match event {
KeyEvent {
code: KeyCode::Esc, ..
- } => helix_core::commands::normal_mode(state, 1),
+ } => commands::normal_mode(view, 1),
KeyEvent {
code: KeyCode::Backspace,
..
- } => helix_core::commands::delete_char_backward(state, 1),
+ } => commands::delete_char_backward(view, 1),
KeyEvent {
code: KeyCode::Delete,
..
- } => helix_core::commands::delete_char_forward(state, 1),
+ } => commands::delete_char_forward(view, 1),
KeyEvent {
code: KeyCode::Char(c),
..
- } => helix_core::commands::insert_char(state, c),
+ } => commands::insert_char(view, c),
KeyEvent {
code: KeyCode::Enter,
..
- } => helix_core::commands::insert_char(state, '\n'),
+ } => commands::insert_char(view, '\n'),
_ => (), // skip
}
// TODO: simplistic ensure cursor in view for now
@@ -329,7 +341,7 @@ impl Editor {
// TODO: handle modes and sequences (`gg`)
if let Some(command) = keymap.get(&event) {
// TODO: handle count other than 1
- command(state, 1);
+ command(view, 1);
// TODO: simplistic ensure cursor in view for now
self.ensure_cursor_in_view();
@@ -350,10 +362,10 @@ impl Editor {
}
fn ensure_cursor_in_view(&mut self) {
- if let Some(state) = &mut self.state {
- let cursor = state.selection().cursor();
- let line = state.doc().char_to_line(cursor) as u16;
- let document_end = self.first_line + self.size.1.saturating_sub(1) - 1;
+ 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;
@@ -361,10 +373,10 @@ impl Editor {
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 {
+ view.first_line += line - (document_end.saturating_sub(padding));
+ } else if line < view.first_line + padding {
// scroll up
- self.first_line = line.saturating_sub(padding);
+ view.first_line = line.saturating_sub(padding);
}
}
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 5611e104..d52ccca4 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,9 +1,9 @@
+use crate::commands::{self, Command};
use crossterm::{
event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers},
execute,
style::Print,
};
-use helix_core::commands::{self, Command};
use std::collections::HashMap;
// Kakoune-inspired:
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 66ed5d2d..aca04641 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -1,6 +1,8 @@
#![allow(unused)]
#[macro_use]
mod macros;
+
+mod commands;
mod editor;
mod keymap;
mod theme;