diff --git a/helix-core/src/commands.rs b/helix-core/src/commands.rs index 62e976860..a6b74449f 100644 --- a/helix-core/src/commands.rs +++ b/helix-core/src/commands.rs @@ -1,4 +1,4 @@ -use crate::state::{Direction, Granularity, State}; +use crate::state::{Direction, Granularity, Mode, State}; /// 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). @@ -48,3 +48,23 @@ pub fn move_line_down(state: &mut State, count: usize) { count, ); } + +pub fn insert_mode(state: &mut State, _count: usize) { + state.mode = Mode::Insert; +} + +pub fn normal_mode(state: &mut State, _count: usize) { + state.mode = Mode::Normal; +} + +// TODO: insert means add text just before cursor, on exit we should be on the last letter. +pub fn insert(state: &mut State, c: char) { + // TODO: needs to work with multiple cursors + use crate::transaction::ChangeSet; + + let pos = state.selection.primary().head; + let changes = ChangeSet::insert(&state.doc, pos, c); + // TODO: need to store history + changes.apply(state.contents_mut()); + state.selection = state.selection.clone().map(&changes); +} diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 3e28c9ceb..1c0b6b748 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -63,8 +63,8 @@ impl Range { /// Map a range through a set of changes. Returns a new range representing the same position /// after the changes are applied. pub fn map(self, changes: &ChangeSet) -> Self { - let anchor = changes.map_pos(self.anchor, Assoc::Before); - let head = changes.map_pos(self.head, Assoc::Before); + let anchor = changes.map_pos(self.anchor, Assoc::After); + let head = changes.map_pos(self.head, Assoc::After); // TODO: possibly unnecessary if self.anchor == anchor && self.head == head { diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs index 9c227a92b..8568c3c3f 100644 --- a/helix-core/src/state.rs +++ b/helix-core/src/state.rs @@ -1,10 +1,16 @@ use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes}; use crate::{Buffer, Rope, RopeSlice, Selection, SelectionRange}; +pub enum Mode { + Normal, + Insert, +} + /// A state represents the current editor state of a single buffer. pub struct State { pub doc: Buffer, pub selection: Selection, + pub mode: Mode, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -26,6 +32,7 @@ impl State { Self { doc, selection: Selection::single(0, 0), + mode: Mode::Normal, } } @@ -119,10 +126,15 @@ impl State { // TODO: update selection in state via transaction } - pub fn file(&self) -> &Rope { + pub fn contents(&self) -> &Rope { // used to access file contents for rendering to screen &self.doc.contents } + + pub fn contents_mut(&mut self) -> &mut Rope { + // used to access file contents for rendering to screen + &mut self.doc.contents + } } /// Coordinates are a 0-indexed line and column pair. diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 1619a4db3..2d8afc9b4 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -47,6 +47,18 @@ impl ChangeSet { } } + pub fn insert(buf: &Buffer, pos: usize, c: char) -> Self { + let len = buf.contents.len_chars(); + Self { + changes: vec![ + Change::Retain(pos), + Change::Insert(Tendril::from_char(c)), + Change::Retain(len - pos), + ], + len, + } + } + // TODO: from iter /// Combine two changesets together. diff --git a/helix-term/src/editor.rs b/helix-term/src/editor.rs index bddc28590..ef4af3dc7 100644 --- a/helix-term/src/editor.rs +++ b/helix-term/src/editor.rs @@ -9,36 +9,11 @@ use crossterm::{ terminal::{self, disable_raw_mode, enable_raw_mode}, }; use futures::{future::FutureExt, select, StreamExt}; -use helix_core::{state::coords_at_pos, Buffer, State}; +use helix_core::{state::coords_at_pos, state::Mode, Buffer, State}; use std::io::{self, stdout, Write}; use std::path::PathBuf; use std::time::Duration; -pub struct BufferComponent<'a> { - x: u16, - y: u16, - contents: Vec<&'a str>, -} - -impl BufferComponent<'_> { - pub fn render(&self) { - for (n, line) in self.contents.iter().enumerate() { - execute!( - stdout(), - SetForegroundColor(Color::DarkCyan), - cursor::MoveTo(self.x, self.y + n as u16), - Print((n + 1).to_string()) - ); - execute!( - stdout(), - SetForegroundColor(Color::Reset), - cursor::MoveTo(self.x + 2, self.y + n as u16), - Print(line) - ); - } - } -} - static EX: smol::Executor = smol::Executor::new(); pub struct Editor { @@ -70,21 +45,56 @@ impl Editor { } fn render(&mut self) { - // TODO: - match &self.state { - Some(s) => { - let view = BufferComponent { - x: 0, - y: self.first_line, - contents: s - .file() - .lines_at(self.first_line as usize) - .take(self.size.1 as usize) - .map(|x| x.as_str().unwrap()) - .collect::>(), + Some(state) => { + let lines = state + .contents() + .lines_at(self.first_line as usize) + .take(self.size.1 as usize) + .map(|x| x.as_str().unwrap()); + + let mut stdout = stdout(); + + for (n, line) in lines.enumerate() { + execute!( + stdout, + SetForegroundColor(Color::DarkCyan), + cursor::MoveTo(0, n as u16), + Print((n + 1).to_string()) + ); + execute!( + stdout, + SetForegroundColor(Color::Reset), + cursor::MoveTo(2, n as u16), + Print(line) + ); + } + + let mode = match state.mode { + Mode::Insert => "INS", + Mode::Normal => "NOR", + }; + + execute!( + stdout, + SetForegroundColor(Color::Reset), + cursor::MoveTo(0, self.size.1), + Print(mode) + ); + + // set cursor shape + match state.mode { + Mode::Insert => write!(stdout, "\x1B[6 q"), + Mode::Normal => write!(stdout, "\x1B[2 q"), }; - view.render(); + + // render the cursor + let pos = state.selection.primary().head; + let coords = coords_at_pos(&state.doc.contents.slice(..), pos); + execute!( + stdout, + cursor::MoveTo((coords.1 + 2) as u16, coords.0 as u16) + ); } None => (), } @@ -108,22 +118,30 @@ impl Editor { break; } Some(Ok(Event::Key(event))) => { - // TODO: handle modes and sequences (`gg`) - if let Some(command) = keymap.get(&event) { - if let Some(state) = &mut self.state { - // TODO: handle count other than 1 - command(state, 1); - self.render(); - // render the cursor - let pos = self.state.as_ref().unwrap().selection.primary().head; - let coords = coords_at_pos( - &self.state.as_ref().unwrap().doc.contents.slice(..), - pos, - ); - execute!( - stdout(), - cursor::MoveTo((coords.1 + 2) as u16, coords.0 as u16) - ); + if let Some(state) = &mut self.state { + match state.mode { + Mode::Insert => { + match event { + KeyEvent { + code: KeyCode::Esc, .. + } => helix_core::commands::normal_mode(state, 1), + KeyEvent { + code: KeyCode::Char(c), + .. + } => helix_core::commands::insert(state, c), + _ => (), // skip + } + self.render(); + } + Mode::Normal => { + // TODO: handle modes and sequences (`gg`) + if let Some(command) = keymap.get(&event) { + // TODO: handle count other than 1 + command(state, 1); + + self.render(); + } + } } } } @@ -145,6 +163,9 @@ impl Editor { self.print_events().await; + // reset cursor shape + write!(stdout, "\x1B[2 q"); + execute!(stdout, terminal::LeaveAlternateScreen)?; disable_raw_mode()?; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index c7da0c892..ddbff3ea4 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -116,5 +116,20 @@ pub fn default() -> Keymap { 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::Esc, + modifiers: Modifiers::NONE + } => commands::normal_mode as Command, ) + + // hashmap!( + // Key { + // code: KeyCode::Esc, + // modifiers: Modifiers::NONE + // } => commands::normal_mode as Command, + // ) }