Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vim normal mode hjkl navigation support to editor #15

Merged
merged 1 commit into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions core/src/state/notebook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use {
Error, Event, Glues, NotebookTransition, Result,
},
consume::{directory, note, traverse},
inner_state::InnerState::{self, *},
inner_state::{
InnerState::{self, *},
VimState,
},
};

pub use directory_item::{DirectoryItem, DirectoryItemChildren, TreeItem};
Expand Down Expand Up @@ -89,11 +92,16 @@ impl NotebookState {
NoteTreeNumber(n) => {
format!("Steps: '{n}' selected")
}
EditingNormalMode => {
EditingNormalMode(VimState::Idle) => {
let name = &self.get_selected_note()?.name;

format!("Note '{name}' normal mode")
}
EditingNormalMode(VimState::Numbering(n)) => {
let name = &self.get_selected_note()?.name;

format!("Note '{name}' normal mode, steps: '{n}'")
}
EditingInsertMode => {
let name = &self.get_selected_note()?.name;

Expand Down Expand Up @@ -134,15 +142,23 @@ impl NotebookState {
"[Esc] Cancel".to_owned(),
]
}
EditingNormalMode => {
EditingNormalMode(VimState::Idle) => {
vec![
"[i] Insert mode".to_owned(),
"[h|j|k|l] Move cursor".to_owned(),
"[1-9] Set steps".to_owned(),
"[b] Browse note tree".to_owned(),
"[n] Toggle line number".to_owned(),
"[h] Show editor keymap".to_owned(),
"[Esc] Quit".to_owned(),
]
}
EditingNormalMode(VimState::Numbering(n)) => {
vec![
format!("[h|j|k|l] Move cursor {n} steps"),
"[0-9] Append steps".to_owned(),
"[Esc] Cancel".to_owned(),
]
}
EditingInsertMode => {
vec![
"[Esc] Save note & Normal mode".to_owned(),
Expand Down
6 changes: 3 additions & 3 deletions core/src/state/notebook/consume/note.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
data::{Directory, Note},
db::Db,
state::notebook::{DirectoryItem, InnerState, NotebookState, SelectedItem},
state::notebook::{DirectoryItem, InnerState, NotebookState, SelectedItem, VimState},
Error, NotebookTransition, Result,
};

Expand Down Expand Up @@ -93,7 +93,7 @@ pub async fn open(
let content = db.fetch_note_content(note.id.clone()).await?;

state.editing = Some(note.clone());
state.inner_state = InnerState::EditingNormalMode;
state.inner_state = InnerState::EditingNormalMode(VimState::Idle);

Ok(NotebookTransition::OpenNote { note, content })
}
Expand All @@ -107,7 +107,7 @@ pub async fn edit(state: &mut NotebookState) -> Result<NotebookTransition> {
pub async fn view(state: &mut NotebookState) -> Result<NotebookTransition> {
let note = state.get_editing()?.clone();

state.inner_state = InnerState::EditingNormalMode;
state.inner_state = InnerState::EditingNormalMode(VimState::Idle);

Ok(NotebookTransition::ViewMode(note))
}
Expand Down
7 changes: 5 additions & 2 deletions core/src/state/notebook/inner_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod note_selected;
mod note_tree_number;

use crate::{db::Db, state::notebook::NotebookState, Event, NotebookTransition, Result};
pub use editing_normal_mode::VimState;

#[derive(Clone)]
pub enum InnerState {
Expand All @@ -15,7 +16,7 @@ pub enum InnerState {
DirectorySelected,
DirectoryMoreActions,
NoteTreeNumber(usize),
EditingNormalMode,
EditingNormalMode(VimState),
EditingInsertMode,
}

Expand All @@ -32,7 +33,9 @@ pub async fn consume(
NoteMoreActions => note_more_actions::consume(db, state, event).await,
DirectoryMoreActions => directory_more_actions::consume(db, state, event).await,
NoteTreeNumber(n) => note_tree_number::consume(db, state, *n, event).await,
EditingNormalMode => editing_normal_mode::consume(db, state, event).await,
EditingNormalMode(vim_state) => {
editing_normal_mode::consume(db, state, *vim_state, event).await
}
EditingInsertMode => editing_insert_mode::consume(db, state, event).await,
}
}
95 changes: 93 additions & 2 deletions core/src/state/notebook/inner_state/editing_normal_mode.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
use crate::{
db::Db,
state::notebook::{directory, note, NotebookState},
Error, Event, KeyEvent, NotebookEvent, NotebookTransition, Result,
state::notebook::{directory, note, InnerState, NotebookState},
transition::{NormalModeTransition, NotebookTransition},
Error, Event, KeyEvent, NotebookEvent, Result,
};

#[derive(Clone, Copy)]
pub enum VimState {
Idle,
Numbering(usize),
}

pub async fn consume(
db: &mut Db,
state: &mut NotebookState,
vim_state: VimState,
event: Event,
) -> Result<NotebookTransition> {
match vim_state {
VimState::Idle => consume_idle(db, state, event).await,
VimState::Numbering(n) => consume_numbering(state, n, event).await,
}
}

async fn consume_idle(
db: &mut Db,
state: &mut NotebookState,
event: Event,
Expand All @@ -18,6 +37,78 @@ pub async fn consume(
Notebook(UpdateNoteContent(content)) => note::update_content(db, state, content).await,
Key(KeyEvent::E) | Notebook(EditNote) => note::edit(state).await,
Key(KeyEvent::B) | Notebook(BrowseNoteTree) => note::browse(state).await,
Key(KeyEvent::Num(n)) => {
state.inner_state = InnerState::EditingNormalMode(VimState::Numbering(n.into()));

Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::NumberingMode,
))
}
Key(KeyEvent::J) => Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorDown(1),
)),
Key(KeyEvent::K) => Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorUp(1),
)),
Key(KeyEvent::H) => Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorBack(1),
)),
Key(KeyEvent::L) => Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorForward(1),
)),
event @ Key(_) => Ok(NotebookTransition::Inedible(event)),
_ => Err(Error::Wip("todo: Notebook::consume".to_owned())),
}
}

async fn consume_numbering(
state: &mut NotebookState,
n: usize,
event: Event,
) -> Result<NotebookTransition> {
use Event::*;

match event {
Key(KeyEvent::Num(n2)) => {
state.inner_state = InnerState::EditingNormalMode(VimState::Numbering(n2 + n * 10));

Ok(NotebookTransition::None)
}
Key(KeyEvent::J) => {
state.inner_state = InnerState::EditingNormalMode(VimState::Idle);

Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorDown(n),
))
}
Key(KeyEvent::K) => {
state.inner_state = InnerState::EditingNormalMode(VimState::Idle);

Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorUp(n),
))
}
Key(KeyEvent::H) => {
state.inner_state = InnerState::EditingNormalMode(VimState::Idle);

Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorBack(n),
))
}
Key(KeyEvent::L) => {
state.inner_state = InnerState::EditingNormalMode(VimState::Idle);

Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::MoveCursorForward(n),
))
}
Key(KeyEvent::Esc) => {
state.inner_state = InnerState::EditingNormalMode(VimState::Idle);

Ok(NotebookTransition::EditingNormalMode(
NormalModeTransition::IdleMode,
))
}
event @ Key(_) => Ok(NotebookTransition::Inedible(event)),
_ => Err(Error::Wip("todo: Notebook::consume".to_owned())),
}
Expand Down
11 changes: 11 additions & 0 deletions core/src/transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ pub enum NotebookTransition {
// Additional frontend action required
SelectNext(usize),
SelectPrev(usize),
EditingNormalMode(NormalModeTransition),
}

#[derive(Display)]
pub enum NormalModeTransition {
IdleMode,
NumberingMode,
MoveCursorDown(usize),
MoveCursorUp(usize),
MoveCursorBack(usize),
MoveCursorForward(usize),
}

impl From<EntryTransition> for Transition {
Expand Down
72 changes: 45 additions & 27 deletions tui/src/context/notebook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub enum ContextState {
NoteTreeNumbering,
NoteActionsDialog,
DirectoryActionsDialog,
EditorNormalMode,
EditorNormalMode { idle: bool },
EditorInsertMode,
}

Expand Down Expand Up @@ -128,7 +128,7 @@ impl NotebookContext {
}

pub fn open_note(&mut self, note: Note, content: String) {
self.state = ContextState::EditorNormalMode;
self.state = ContextState::EditorNormalMode { idle: true };
self.opened_note = Some(note);
self.editor = TextArea::from(content.lines());
}
Expand All @@ -142,9 +142,8 @@ impl NotebookContext {
match self.state {
ContextState::NoteTreeBrowsing => self.consume_on_note_tree_browsing(code),
ContextState::NoteTreeNumbering => self.consume_on_note_tree_numbering(code),
ContextState::EditorNormalMode | ContextState::EditorInsertMode => {
self.consume_on_editor(input)
}
ContextState::EditorNormalMode { idle } => self.consume_on_editor_normal(input, idle),
ContextState::EditorInsertMode => self.consume_on_editor_insert(input),
ContextState::NoteActionsDialog => self.consume_on_note_actions(code),
ContextState::DirectoryActionsDialog => self.consume_on_directory_actions(code),
}
Expand Down Expand Up @@ -264,31 +263,12 @@ impl NotebookContext {
}
}

fn consume_on_editor(&mut self, input: &Input) -> Action {
fn consume_on_editor_normal(&mut self, input: &Input, idle: bool) -> Action {
let code = match input {
Input::Key(key) => key.code,
_ => return Action::None,
};

if self.state == ContextState::EditorInsertMode {
if code == KeyCode::Esc {
self.state = ContextState::EditorNormalMode;
return Action::Dispatch(NotebookEvent::ViewNote.into());
} else if matches!(
input,
Input::Key(KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL,
..
})
) {
return TuiAction::ShowEditorKeymap.into();
} else {
self.editor.input(input.clone());
return Action::None;
}
}

match code {
KeyCode::Char('b') => {
self.state = ContextState::NoteTreeBrowsing;
Expand All @@ -305,16 +285,54 @@ impl NotebookContext {

Action::None
}
KeyCode::Char('h') => TuiAction::ShowEditorKeymap.into(),
KeyCode::Esc => TuiAction::Confirm {
KeyCode::Esc if idle => TuiAction::Confirm {
message: "Do you want to quit?".to_owned(),
action: Box::new(TuiAction::Quit.into()),
}
.into(),
KeyCode::Esc
| KeyCode::Char('h')
| KeyCode::Char('j')
| KeyCode::Char('k')
| KeyCode::Char('l')
| KeyCode::Char('0')
| KeyCode::Char('1')
| KeyCode::Char('2')
| KeyCode::Char('3')
| KeyCode::Char('4')
| KeyCode::Char('5')
| KeyCode::Char('6')
| KeyCode::Char('7')
| KeyCode::Char('8')
| KeyCode::Char('9') => Action::PassThrough,
_ => Action::None,
}
}

fn consume_on_editor_insert(&mut self, input: &Input) -> Action {
let code = match input {
Input::Key(key) => key.code,
_ => return Action::None,
};

if code == KeyCode::Esc {
self.state = ContextState::EditorNormalMode { idle: true };
Action::Dispatch(NotebookEvent::ViewNote.into())
} else if matches!(
input,
Input::Key(KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL,
..
})
) {
TuiAction::ShowEditorKeymap.into()
} else {
self.editor.input(input.clone());
Action::None
}
}

fn consume_on_note_actions(&mut self, code: KeyCode) -> Action {
match code {
KeyCode::Char('j') => {
Expand Down
Loading