Skip to content

Commit

Permalink
command mode
Browse files Browse the repository at this point in the history
minor fixes

command modef

minor imporovements

command mode

minor fixes

asdf

asdf
  • Loading branch information
stokhos committed Feb 10, 2021
1 parent 9929cb7 commit a4704de
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 142 deletions.
62 changes: 39 additions & 23 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! Module to hold all user-configurable parameters, until we find a better way to handle
//! configuration

//use std::array::IntoIter;

use crate::ast::display_token::{syntax_category::*, SyntaxCategory};
use crate::core::Direction;
use crate::editor::normal_mode::CmdType;
use crate::editor::{command_mode, normal_mode}; //::CmdType;

use tuikit::prelude::{Color, Key};

Expand Down Expand Up @@ -39,46 +41,60 @@ pub fn default_color_scheme() -> ColorScheme {

/* KEY BINDINGS */

/// Mapping of keys to keystrokes.
/// Mapping of keys to keystrokes in normal mode.
/// Shortcut definition, also allows us to change the type if needed.
pub type KeyMap = std::collections::HashMap<Key, CmdType>;
pub type NormalModeKeyMap = std::collections::HashMap<Key, normal_mode::CmdType>;

/// Generates a 'canonical' [`KeyMap`]. These keybindings will be very similar to those of Vim.
pub fn default_keymap() -> KeyMap {
/// Generates a 'canonical' [`NormalModeKeyMap`]. These keybindings will be very similar to those of Vim.
pub fn normal_mode_default_keymap() -> NormalModeKeyMap {
hmap::hmap! {
Key::Char('q') => CmdType::Quit,
Key::Char('w') => CmdType::Write,
Key::Char('i') => CmdType::InsertBefore,
Key::Char('a') => CmdType::InsertAfter,
Key::Char('o') => CmdType::InsertChild,
Key::Char('r') => CmdType::Replace,
Key::Char('x') => CmdType::Delete,
Key::Char('c') => CmdType::MoveCursor(Direction::Down),
Key::Char('p') => CmdType::MoveCursor(Direction::Up),
Key::Char('h') => CmdType::MoveCursor(Direction::Prev),
Key::Char('j') => CmdType::MoveCursor(Direction::Next),
Key::Char('k') => CmdType::MoveCursor(Direction::Prev),
Key::Char('l') => CmdType::MoveCursor(Direction::Next),
Key::Char('u') => CmdType::Undo,
Key::Char('R') => CmdType::Redo
Key::Char('i') => normal_mode::CmdType::InsertBefore,
Key::Char('a') => normal_mode::CmdType::InsertAfter,
Key::Char('o') => normal_mode::CmdType::InsertChild,
Key::Char('r') => normal_mode::CmdType::Replace,
Key::Char('x') => normal_mode::CmdType::Delete,
Key::Char('c') => normal_mode::CmdType::MoveCursor(Direction::Down),
Key::Char('p') => normal_mode::CmdType::MoveCursor(Direction::Up),
Key::Char('h') => normal_mode::CmdType::MoveCursor(Direction::Prev),
Key::Char('j') => normal_mode::CmdType::MoveCursor(Direction::Next),
Key::Char('k') => normal_mode::CmdType::MoveCursor(Direction::Prev),
Key::Char('l') => normal_mode::CmdType::MoveCursor(Direction::Next),
Key::Char('u') => normal_mode::CmdType::Undo,
Key::Char('R') => normal_mode::CmdType::Redo,
Key::Char(':') => normal_mode::CmdType::CommandMode
}
}

/// /// Mapping of keys to keystrokes in command mode.
pub type CommandModeKeyMap = std::collections::HashMap<Key, command_mode::CmdType>;

/// Generates a 'canonical' [`CommandModeKeyMap`]. These keybindings will be very similar to those of Vim.
pub fn command_mode_default_keymap() -> CommandModeKeyMap {
hmap::hmap! {
Key::Char('q') => command_mode::CmdType::Quit,
Key::Char('w') => command_mode::CmdType::Write,
Key::Char('d') => command_mode::CmdType::DotGraph,
Key::ESC => command_mode::CmdType::NormalMode
}
}
/* COMPLETE CONFIG */

/// A struct to hold the entire run-time configuration of Sapling
#[derive(Debug, Clone)]
pub struct Config {
/// A mapping between [`char`]s and [`CmdType`]s
pub keymap: KeyMap,
/// A mapping between [`char`]s and [`crate::editor::normal_mode::CmdType`]s in normal mode
pub normal_mode_keymap: NormalModeKeyMap,
/// A mapping between [`char`]s and [`crate::editor::command_mode::CmdType`]s in command mode
pub command_mode_keymap: CommandModeKeyMap,
/// The current [`ColorScheme`] of Sapling
pub color_scheme: ColorScheme,
}

impl Default for Config {
fn default() -> Self {
Config {
keymap: default_keymap(),
normal_mode_keymap: normal_mode_default_keymap(),
command_mode_keymap: command_mode_default_keymap(),
color_scheme: default_color_scheme(),
}
}
Expand Down
269 changes: 269 additions & 0 deletions src/editor/command_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
//! The code for 'normal-mode', similar to that of Vim

use super::{keystroke_log::Category, normal_mode, state, Editor};
use crate::ast::Ast;
use crate::config::CommandModeKeyMap;
use crate::core::keystrokes_to_string;

use std::borrow::Cow;
use std::io::prelude::*;
use std::iter::Peekable;

use tuikit::prelude::Key;

/// The struct covering all the [`State`](state::State)s which correspond to Sapling being in
/// command mode.
#[derive(Debug, Clone)]
pub struct State {
name: &'static str,
keystroke_buffer: Vec<Key>,
}

impl Default for State {
fn default() -> Self {
State {
name: "COMMAND",
keystroke_buffer: Vec::new(),
}
}
}

impl<'arena, Node: Ast<'arena>> state::State<'arena, Node> for State {
fn transition(
mut self: Box<Self>,
key: Key,
editor: &mut Editor<'arena, Node>,
) -> (
Box<dyn state::State<'arena, Node>>,
Option<(String, Category)>,
) {
self.keystroke_buffer.push(key);

let tree = &mut editor.tree;

let log_entry =
match parse_command(&editor.config.command_mode_keymap, &self.keystroke_buffer) {
Ok((_, action)) => {
match action {
// If the command was a 'quit', then immediately make a state transition to the
// 'Quitted' state. It doesn't matter what the count is, because quitting is
// idempotent
Action::Quit => {
return (
Box::new(Quit),
Some((action.description(), action.category())),
);
}
Action::Write => {
if let Some(path) = &editor.file_path {
// If the editor was given a file-path, then write to it
let mut file = std::fs::File::create(path).unwrap();
let mut content = tree.to_text(&editor.format_style);
// Force the file to finish with a newline. BTW, <str>.chars().last()
// is O(1), regardless of the length of the string.
if content.chars().last() != Some('\n') {
content.push('\n');
}
file.write_all(content.as_bytes()).unwrap();
} else {
// Otherwise, log a warning and do nothing
log::warn!("No file to write to!");
}
// If we haven't returned yet, then clear the buffer
self.keystroke_buffer.clear();
return (
Box::new(normal_mode::State::default()),
Some((action.description(), action.category())),
);
}
Action::NormalMode => {
return (
Box::new(normal_mode::State::default()),
Some((action.description(), action.category())),
);
}
Action::DotGraph => {
//TODO write dot file to a different file
log::debug!("{}", tree.to_dot_code());
return (
Box::new(normal_mode::State::default()),
Some((action.description(), action.category())),
);
}
}
}

Err(ParseErr::Incomplete) => return (self, None),
Err(ParseErr::Invalid) => (
format!(
"Undefined command '{}'",
keystrokes_to_string(&self.keystroke_buffer)
),
Category::Undefined,
),
};

self.keystroke_buffer.clear();
(self, Some(log_entry))
}

fn keystroke_buffer(&self) -> Cow<'_, str> {
Cow::from(keystrokes_to_string(&self.keystroke_buffer))
}

fn name(&self) -> &'arena str {
return &self.name;
}
}

/// The possible keystroke typed by user without any parameters. Each `KeyStroke` can be mapped to
/// an individual [`char`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum CmdType {
/// Quit Sapling
Quit,
/// Write current buffer to disk
Write,
/// Quit command mode
NormalMode,
/// To dot graph
DotGraph,
}

impl CmdType {
/// Returns a lower-case summary string of the given keystroke
pub fn summary_string(&self) -> &'static str {
match self {
CmdType::Quit => "quit",
CmdType::Write => "write",
CmdType::NormalMode => "switch to normal mode",
CmdType::DotGraph => "write to dot graph",
}
}
}

/// The [`Action`] generated by a single command-mode 'command'.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Action {
/// Quit Sapling
Quit,
/// Write current buffer to disk
Write,
/// Write dot graph to disk
DotGraph,
/// Write current buffer to disk
NormalMode,
}

impl Action {
/// Returns a lower-case summary of the given keystroke, along with the color with which it
/// should be displayed in the log.
pub fn description(&self) -> String {
match self {
Action::Quit => "quit Sapling".to_string(),
Action::Write => "write to disk".to_string(),
Action::DotGraph => "write to .dot".to_string(),
Action::NormalMode => "switch to normal mode".to_string(),
}
}

/// Returns the [`Category`] of this `Action`
pub fn category(&self) -> Category {
match self {
Action::Quit => Category::Quit,
Action::Write => Category::IO,
Action::DotGraph => Category::IO,
Action::NormalMode => Category::NormalMode,
}
}
}

type ParseResult<T> = Result<T, ParseErr>;

/// The possible ways a parsing operation could fail
#[derive(Debug, Clone, Eq, PartialEq)]
enum ParseErr {
Invalid,
Incomplete,
}

/// Attempt to parse an entire command. This is the entry point to the parsing code. This parser
/// is a recursive descent parser, where there is a separate function for [`parse_count`] for count.
///
/// Note that this parser will return as soon as a valid command is reached. Therefore,
/// `"q489flshb"` will be treated like `"q"`, and will return [`Action::Quit`] even though
/// `"q489flshb"` is not technically valid. However, the command buffer is parsed every time the
/// user types a keystroke character, so the user would not be able to input `"q489flshb"` in one
/// go because doing so would require them to first input every possible prefix of `"q489flshb"`,
/// including `"q"`.
fn parse_command(keymap: &CommandModeKeyMap, keys: &[Key]) -> ParseResult<(usize, Action)> {
// Generate an iterator of keystrokes, which are treated similar to tokens by the parser.
let mut key_iter = keys.iter().copied().peekable();

// Parse a count off the front of the command
let count = parse_count(&mut key_iter);
// The first non-count keystroke represents the command name. No keystrokes is an incomplete
// command.
let first_key = key_iter.next().ok_or(ParseErr::Incomplete)?;

Ok((
count,
match keymap.get(&first_key).ok_or(ParseErr::Invalid)? {
// "q" quits Sapling
CmdType::Quit => Action::Quit,
CmdType::Write => Action::Write,
CmdType::DotGraph => Action::DotGraph,
CmdType::NormalMode => Action::NormalMode,
},
))
}

/// Parse a 'count' off the front of an sequence of [`Key`]strokes. This cannot fail, because if
/// the first [`Key`] is not a numeral, this returns `1`.
fn parse_count(keystroke_char_iter: &mut Peekable<impl Iterator<Item = Key>>) -> usize {
// accumulated_count tracks the number that is represented by the keystrokes already consumed
// or None if no numbers have been consumed
let mut accumulated_count: Option<usize> = None;
loop {
let new_digit = match keystroke_char_iter.peek() {
Some(Key::Char('0')) => 0,
Some(Key::Char('1')) => 1,
Some(Key::Char('2')) => 2,
Some(Key::Char('3')) => 3,
Some(Key::Char('4')) => 4,
Some(Key::Char('5')) => 5,
Some(Key::Char('6')) => 6,
Some(Key::Char('7')) => 7,
Some(Key::Char('8')) => 8,
Some(Key::Char('9')) => 9,
_ => break,
};
// Pop the digit. We use lookahead so that we leave the future keystrokes untouched for
// the next parsing.
keystroke_char_iter.next();
// Since we read a new digit, we accumulate it to the count
accumulated_count = Some(accumulated_count.map_or(new_digit, |x| x * 10 + new_digit));
}
accumulated_count.unwrap_or(1)
}

/// The [`State`] that Sapling enters to quit the mainloop and exit
#[derive(Debug, Copy, Clone)]
struct Quit;

impl<'arena, Node: Ast<'arena>> state::State<'arena, Node> for Quit {
fn transition(
self: Box<Self>,
_key: Key,
_editor: &mut Editor<'arena, Node>,
) -> (
Box<dyn state::State<'arena, Node>>,
Option<(String, Category)>,
) {
(self, None)
}

fn is_quit(&self) -> bool {
true
}
}
2 changes: 1 addition & 1 deletion src/editor/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ mod tests {
Action::InsertBefore(c) => self.insert_next_to_cursor(count, c, Side::Prev),
Action::InsertAfter(c) => self.insert_next_to_cursor(count, c, Side::Next),
Action::Delete => self.delete_cursor(count),
Action::Quit | Action::Write => unreachable!(),
Action::CommandMode => unreachable!(),
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/editor/keystroke_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub enum Category {
Quit,
/// An [`Action`] that handles reading and writing from disk
IO,
/// An [`Action`] that swith to command mode
CommandMode,
/// An [`Action`] that swith to normal mode
NormalMode,
/// The keystrokes did not correspond to a well-defined action
Undefined,
}
Expand All @@ -43,6 +47,8 @@ impl Category {
Category::Delete => Color::RED,
Category::Quit => Color::MAGENTA,
Category::IO => Color::GREEN,
Category::CommandMode => Color::LIGHT_MAGENTA,
Category::NormalMode => Color::LIGHT_MAGENTA,
Category::Undefined => Color::LIGHT_RED,
}
}
Expand Down

0 comments on commit a4704de

Please sign in to comment.