Skip to content

Commit 98502a8

Browse files
schlichfdncred
andauthored
helix-mode: add basic mode switching (#1039)
Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
1 parent b69d980 commit 98502a8

12 files changed

Lines changed: 1086 additions & 398 deletions

File tree

Cargo.lock

Lines changed: 622 additions & 330 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ crossbeam = { version = "0.8.2", optional = true }
2424
crossterm = { version = "0.29.0", features = ["serde"] }
2525
fd-lock = "4.0.2"
2626
itertools = "0.13.0"
27+
keybindings = "0.0.2"
2728
nu-ansi-term = "0.50.0"
2829
rusqlite = { version = "0.37.0", optional = true }
2930
serde = { version = "1.0", features = ["derive"] }

examples/helix.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ use reedline::{DefaultPrompt, Helix, Reedline, Signal};
88
use std::io;
99

1010
fn main() -> io::Result<()> {
11-
println!("Helix edit mode demo:\nAbort with Ctrl-C");
11+
println!(
12+
"Helix edit mode demo:
13+
Default mode is insert (`:` prompt), so you can type words.
14+
Press Esc for normal mode.
15+
Press `i` to return to insert mode, or `a` to insert after the current selection.
16+
Only `h`/`l` motions are currently implemented.
17+
Abort with Ctrl-C"
18+
);
1219

1320
let prompt = DefaultPrompt::default();
14-
let mut line_editor = Reedline::create().with_edit_mode(Box::new(Helix));
21+
let mut line_editor = Reedline::create().with_edit_mode(Box::new(Helix::default()));
1522

1623
loop {
1724
let sig = line_editor.read_line(&prompt)?;

src/edit_mode/helix.rs

Lines changed: 0 additions & 64 deletions
This file was deleted.

src/edit_mode/helix/action.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::enums::{EditCommand, ReedlineEvent};
2+
3+
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
4+
pub(super) enum HelixAction {
5+
Type(char),
6+
MoveCharRight,
7+
MoveCharLeft,
8+
#[default]
9+
NoOp,
10+
}
11+
12+
impl HelixAction {
13+
pub(super) fn into_reedline_event(self) -> Option<ReedlineEvent> {
14+
match self {
15+
HelixAction::Type(c) => Some(ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)])),
16+
HelixAction::MoveCharLeft => Some(ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
17+
select: false,
18+
}])),
19+
HelixAction::MoveCharRight => Some(ReedlineEvent::Edit(vec![EditCommand::MoveRight {
20+
select: false,
21+
}])),
22+
HelixAction::NoOp => None,
23+
}
24+
}
25+
}

src/edit_mode/helix/bindings.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use crossterm::event::{KeyCode, KeyModifiers};
2+
use keybindings::{EdgeEvent, EdgePath, EdgeRepeat, EmptyKeyClass, InputBindings};
3+
4+
use super::{
5+
key::HelixKey,
6+
mode::{HelixMachine, HelixMode, HelixStep},
7+
};
8+
9+
#[derive(Default)]
10+
pub(super) struct HelixBindings;
11+
12+
impl HelixBindings {
13+
fn add_single_keypress_mapping(
14+
machine: &mut HelixMachine,
15+
mode: HelixMode,
16+
code: KeyCode,
17+
step: HelixStep,
18+
) {
19+
let path: &EdgePath<HelixKey, EmptyKeyClass> = &[(
20+
EdgeRepeat::Once,
21+
EdgeEvent::Key(HelixKey::new(code, KeyModifiers::NONE)),
22+
)];
23+
machine.add_mapping(mode, path, &step);
24+
}
25+
26+
fn add_bindings(
27+
machine: &mut HelixMachine,
28+
mode: HelixMode,
29+
bindings: &[(KeyCode, HelixStep)],
30+
) {
31+
for (code, step) in bindings {
32+
Self::add_single_keypress_mapping(machine, mode, *code, step.clone());
33+
}
34+
}
35+
}
36+
37+
impl InputBindings<HelixKey, HelixStep> for HelixBindings {
38+
fn setup(&self, machine: &mut HelixMachine) {
39+
let insert_bindings = [(KeyCode::Esc, (None, Some(HelixMode::Normal)))];
40+
let normal_bindings = [
41+
(KeyCode::Char('i'), (None, Some(HelixMode::Insert))),
42+
(
43+
KeyCode::Char('h'),
44+
(Some(super::action::HelixAction::MoveCharLeft), None),
45+
),
46+
(
47+
KeyCode::Left,
48+
(Some(super::action::HelixAction::MoveCharLeft), None),
49+
),
50+
(
51+
KeyCode::Char('l'),
52+
(Some(super::action::HelixAction::MoveCharRight), None),
53+
),
54+
(
55+
KeyCode::Right,
56+
(Some(super::action::HelixAction::MoveCharRight), None),
57+
),
58+
(
59+
KeyCode::Char('a'),
60+
(
61+
Some(super::action::HelixAction::MoveCharRight),
62+
Some(HelixMode::Insert),
63+
),
64+
),
65+
];
66+
67+
Self::add_bindings(machine, HelixMode::Insert, &insert_bindings);
68+
Self::add_bindings(machine, HelixMode::Normal, &normal_bindings);
69+
}
70+
}

src/edit_mode/helix/event.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use crate::enums::{ReedlineEvent, ReedlineRawEvent};
2+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3+
use keybindings::BindingMachine;
4+
5+
use super::{action::HelixAction, mode::HelixMachine};
6+
7+
pub(super) fn parse_event(machine: &mut HelixMachine, event: ReedlineRawEvent) -> ReedlineEvent {
8+
let Some(key_event) = KeyEvent::try_from(event).ok() else {
9+
return ReedlineEvent::None;
10+
};
11+
12+
handle_key_event(machine, key_event)
13+
}
14+
15+
fn is_interrupt_event(key_event: &KeyEvent) -> bool {
16+
matches!(
17+
key_event,
18+
KeyEvent {
19+
code: KeyCode::Char('c'),
20+
modifiers: KeyModifiers::CONTROL,
21+
..
22+
}
23+
)
24+
}
25+
26+
fn handle_key_event(machine: &mut HelixMachine, key_event: KeyEvent) -> ReedlineEvent {
27+
if is_interrupt_event(&key_event) {
28+
return ReedlineEvent::CtrlC;
29+
}
30+
31+
let (action, mode_changed) = apply_key_event(machine, key_event);
32+
33+
action
34+
.and_then(HelixAction::into_reedline_event)
35+
.unwrap_or_else(|| mode_change_event(mode_changed))
36+
}
37+
38+
fn apply_key_event(machine: &mut HelixMachine, key_event: KeyEvent) -> (Option<HelixAction>, bool) {
39+
let previous_mode = machine.mode();
40+
machine.input_key(key_event.into());
41+
42+
let mode_changed = machine.mode() != previous_mode;
43+
let action = machine.pop().map(|(action, _ctx)| action);
44+
45+
(action, mode_changed)
46+
}
47+
48+
fn mode_change_event(mode_changed: bool) -> ReedlineEvent {
49+
if mode_changed {
50+
ReedlineEvent::Repaint
51+
} else {
52+
ReedlineEvent::None
53+
}
54+
}

src/edit_mode/helix/key.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2+
use keybindings::InputKey;
3+
4+
/// A simple `InputKey` implementation around `crossterm` types.
5+
///
6+
/// This avoids pulling in the `crossterm` types used by `modalkit` (which can be a different
7+
/// version than the one used by `reedline`).
8+
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
9+
pub(super) struct HelixKey {
10+
code: KeyCode,
11+
modifiers: KeyModifiers,
12+
}
13+
14+
impl HelixKey {
15+
pub(super) fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
16+
Self { code, modifiers }
17+
}
18+
}
19+
20+
impl From<KeyEvent> for HelixKey {
21+
fn from(event: KeyEvent) -> Self {
22+
Self::new(event.code, event.modifiers)
23+
}
24+
}
25+
26+
impl InputKey for HelixKey {
27+
type Error = std::convert::Infallible;
28+
29+
fn decompose(&mut self) -> Option<Self> {
30+
None
31+
}
32+
33+
fn from_macro_str(mstr: &str) -> Result<Vec<Self>, Self::Error> {
34+
Ok(mstr
35+
.chars()
36+
.map(|c| HelixKey::new(KeyCode::Char(c), KeyModifiers::NONE))
37+
.collect())
38+
}
39+
40+
fn get_char(&self) -> Option<char> {
41+
match self.code {
42+
KeyCode::Char(c) => Some(c),
43+
_ => None,
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)