Skip to content
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
52 changes: 34 additions & 18 deletions crates/keybindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ pub trait InputState {
/// The output context type returned along with actions.
type Output: Clone + Default;

/// Implementation-specific type for indicating how to render the cursor.
type CursorHint: Default;

/// Return a hint for how to render the cursor.
fn get_cursor_hint(&self) -> Self::CursorHint {
Self::CursorHint::default()
}

/// Reset any action-specific state.
fn reset(&mut self);

Expand Down Expand Up @@ -225,11 +233,6 @@ impl InputKey for char {
pub trait InputKeyState<Key, C: InputKeyClass<Key>>: InputState {
/// Update the context as needed after a `Key` has matched an [EdgeEvent].
fn event(&mut self, event: &EdgeEvent<Key, C>, key: &Key) {}

/// Return a character to show at the current cursor position.
fn get_cursor_indicator(&self) -> Option<char> {
None
}
}

/// Trait for the input modes specific to a consumer.
Expand Down Expand Up @@ -358,7 +361,7 @@ pub trait InputBindings<Key: InputKey, S: Step<Key>> {
}

/// Trait for objects that can process input keys using previously mapped bindings.
pub trait BindingMachine<K, A, S, C>
pub trait BindingMachine<K, A, S, C, H>
where
K: InputKey,
{
Expand All @@ -384,7 +387,7 @@ where
fn reset_mode(&mut self);

/// Returns a character to show for the cursor.
fn get_cursor_indicator(&self) -> Option<char>;
fn get_cursor_hint(&self) -> H;

/// Repeat a recent sequence of tracked actions, and optionally override their original
/// contexts using [InputState::merge]. The repeated sequence will be inserted at
Expand Down Expand Up @@ -427,6 +430,7 @@ pub struct EmptyKeyState {}

impl InputState for EmptyKeyState {
type Output = Self;
type CursorHint = ();

fn merge(original: Self, _: &Self) -> Self {
original
Expand Down Expand Up @@ -1254,6 +1258,11 @@ where
self.actions.push_back(pair);
}

/// Returns an implementation-defined hint for how to render the input cursor.
pub fn get_cursor_hint(&self) -> <S::State as InputState>::CursorHint {
self.ctx.get_cursor_hint()
}

/// Returns the mode we've most recently entered.
///
/// Modes reached via [Fallthrough](EdgeEvent::Fallthrough) will not change what this returns.
Expand All @@ -1267,8 +1276,14 @@ where
}
}

impl<Key, S> BindingMachine<Key, S::A, S::Sequence, <S::State as InputState>::Output>
for ModalMachine<Key, S>
impl<Key, S>
BindingMachine<
Key,
S::A,
S::Sequence,
<S::State as InputState>::Output,
<S::State as InputState>::CursorHint,
> for ModalMachine<Key, S>
where
Key: InputKey,
S: Step<Key>,
Expand Down Expand Up @@ -1378,8 +1393,8 @@ where
self.state.show(&self.ctx)
}

fn get_cursor_indicator(&self) -> Option<char> {
self.ctx.get_cursor_indicator()
fn get_cursor_hint(&self) -> <S::State as InputState>::CursorHint {
self.ctx.get_cursor_hint()
}

fn repeat(&mut self, seq: S::Sequence, ctx: Option<<S::State as InputState>::Output>) {
Expand Down Expand Up @@ -1710,6 +1725,11 @@ mod tests {

impl InputState for TestContext {
type Output = Self;
type CursorHint = Option<char>;

fn get_cursor_hint(&self) -> Option<char> {
self.temp.cursor
}

fn merge(mut original: Self, other: &Self) -> Self {
if other.temp.count.is_some() {
Expand Down Expand Up @@ -1768,10 +1788,6 @@ mod tests {
},
}
}

fn get_cursor_indicator(&self) -> Option<char> {
self.temp.cursor
}
}

impl Step<TestKey> for TestStep {
Expand Down Expand Up @@ -3113,20 +3129,20 @@ mod tests {

// No cursor indicator yet.
assert_eq!(tm.mode(), TestMode::Insert);
assert_eq!(tm.get_cursor_indicator(), None);
assert_eq!(tm.get_cursor_hint(), None);

// Set cursor indicator while we wait for the register.
tm.input_key(ctl!('r'));
assert_eq!(tm.pop(), None);
assert_eq!(tm.mode(), TestMode::Insert);
assert_eq!(tm.get_cursor_indicator(), Some('^'));
assert_eq!(tm.get_cursor_hint(), Some('^'));

// Set cursor indicator while we wait for the register.
ctx.temp.register = Some('a');
ctx.temp.cursor = Some('^');
tm.input_key(key!('a'));
assert_pop2!(tm, TestAction::Paste, ctx);
assert_eq!(tm.mode(), TestMode::Insert);
assert_eq!(tm.get_cursor_indicator(), None);
assert_eq!(tm.get_cursor_hint(), None);
}
}
4 changes: 2 additions & 2 deletions crates/modalkit-ratatui/examples/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,13 +738,13 @@ impl Editor {
let area = f.area();

let modestr = bindings.show_mode();
let cursor = bindings.get_cursor_indicator();
let cursor = bindings.get_cursor_hint();
let dialogstr = bindings.show_dialog(area.width as usize, area.height as usize);

let screen = Screen::new(store).show_dialog(dialogstr).show_mode(modestr).borders(true);
f.render_stateful_widget(screen, area, sstate);

render_cursor(f, sstate, cursor);
render_cursor(f, sstate, &cursor);
})?;
if sstate.hide_term_cursor() {
term.hide_cursor()?;
Expand Down
9 changes: 6 additions & 3 deletions crates/modalkit-ratatui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ use crossterm::{
};

use modalkit::actions::Action;
use modalkit::editing::{application::ApplicationInfo, completion::CompletionList, store::Store};
use modalkit::editing::application::ApplicationInfo;
use modalkit::editing::completion::CompletionList;
use modalkit::editing::cursor::CursorStyle;
use modalkit::editing::store::Store;
use modalkit::errors::{EditResult, UIResult};
use modalkit::prelude::*;

Expand Down Expand Up @@ -258,9 +261,9 @@ pub trait Window<I: ApplicationInfo>: WindowOps<I> + Sized {
}

/// Position and draw a terminal cursor.
pub fn render_cursor<T: TerminalCursor>(f: &mut Frame, widget: &T, cursor: Option<char>) {
pub fn render_cursor<T: TerminalCursor>(f: &mut Frame, widget: &T, cursor: &CursorStyle) {
if let Some((cx, cy)) = widget.get_term_cursor() {
if let Some(c) = cursor {
if let Some(c) = cursor.get_indicator() {
let style = Style::default().fg(Color::Green);
let span = Span::styled(c.to_string(), style);
let para = Paragraph::new(span);
Expand Down
25 changes: 25 additions & 0 deletions crates/modalkit/src/editing/cursor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,31 @@ pub(crate) fn block_cursors(a: &Cursor, b: &Cursor) -> (Cursor, Cursor) {
(Cursor::new(lstart, lcol), Cursor::new(lend, rcol).goal(rgoal))
}

/// The style in which to render the editor's cursor.
#[derive(Clone, Default)]
#[non_exhaustive]
pub struct CursorStyle {
pub(crate) indicator: Option<char>,
pub(crate) insert: Option<editor_types::prelude::InsertStyle>,
}

impl CursorStyle {
/// Returns whether a specific character should be shown for the cursor.
///
/// For example, when pressing `^V` in Vim, a caret (`^`) is used for the cursor.
pub fn get_indicator(&self) -> Option<char> {
self.indicator
}

/// Returns whether the cursor is being used for inserting text.
///
/// This allows consumers to show the cursor differently in their UI if
/// they wish, such as using a blinking cursor when in Insert mode.
pub fn get_insert_style(&self) -> Option<editor_types::prelude::InsertStyle> {
self.insert
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
16 changes: 10 additions & 6 deletions crates/modalkit/src/editing/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ use std::borrow::Cow;
use std::collections::VecDeque;

use crate::actions::MacroAction;
use crate::editing::cursor::CursorStyle;
use crate::errors::{EditError, EditResult};
use crate::key::MacroError;
use crate::keybindings::{dialog::Dialog, BindingMachine, InputKey};
use crate::keybindings::dialog::Dialog;
use crate::keybindings::{BindingMachine, InputKey};
use crate::prelude::*;

use super::{
Expand All @@ -47,7 +49,7 @@ pub struct KeyManager<K, A, S>
where
K: InputKey,
{
bindings: Box<dyn BindingMachine<K, A, S, EditContext>>,
bindings: Box<dyn BindingMachine<K, A, S, EditContext, CursorStyle>>,
keystack: VecDeque<K>,

recording: Option<(Register, bool)>,
Expand All @@ -62,7 +64,9 @@ where
K: InputKey,
{
/// Create a new instance.
pub fn new<B: BindingMachine<K, A, S, EditContext> + 'static>(bindings: B) -> Self {
pub fn new<B: BindingMachine<K, A, S, EditContext, CursorStyle> + 'static>(
bindings: B,
) -> Self {
let bindings = Box::new(bindings);

Self {
Expand Down Expand Up @@ -147,7 +151,7 @@ where
}
}

impl<K, A, S> BindingMachine<K, A, S, EditContext> for KeyManager<K, A, S>
impl<K, A, S> BindingMachine<K, A, S, EditContext, CursorStyle> for KeyManager<K, A, S>
where
K: InputKey + ToString,
{
Expand Down Expand Up @@ -201,8 +205,8 @@ where
self.bindings.reset_mode()
}

fn get_cursor_indicator(&self) -> Option<char> {
self.bindings.get_cursor_indicator()
fn get_cursor_hint(&self) -> CursorStyle {
self.bindings.get_cursor_hint()
}

fn repeat(&mut self, seq: S, other: Option<EditContext>) {
Expand Down
6 changes: 6 additions & 0 deletions crates/modalkit/src/env/emacs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::marker::PhantomData;

use crate::{
actions::{Action, InsertTextAction, PromptAction},
editing::cursor::CursorStyle,
key::TerminalKey,
keybindings::{
EdgeEvent,
Expand Down Expand Up @@ -208,6 +209,11 @@ impl<I: ApplicationInfo> Clone for EmacsState<I> {

impl<I: ApplicationInfo> InputState for EmacsState<I> {
type Output = EditContext;
type CursorHint = CursorStyle;

fn get_cursor_hint(&self) -> Self::CursorHint {
CursorStyle { indicator: None, insert: Some(self.persist.insert) }
}

fn merge(original: EditContext, overrides: &EditContext) -> EditContext {
let mut builder = EditContextBuilder::from(original);
Expand Down
8 changes: 5 additions & 3 deletions crates/modalkit/src/env/mixed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
actions::Action,
editing::application::{ApplicationInfo, EmptyInfo},
editing::context::EditContext,
editing::cursor::CursorStyle,
key::TerminalKey,
keybindings::{dialog::Dialog, BindingMachine, InputKey, Step},
prelude::RepeatType,
Expand Down Expand Up @@ -86,7 +87,8 @@ where
}
}

impl<K, I> BindingMachine<K, Action<I>, RepeatType, EditContext> for MixedBindings<K, I>
impl<K, I> BindingMachine<K, Action<I>, RepeatType, EditContext, CursorStyle>
for MixedBindings<K, I>
where
K: InputKey,
I: ApplicationInfo,
Expand Down Expand Up @@ -117,8 +119,8 @@ where
delegate_bindings!(self, BindingMachine::reset_mode)
}

fn get_cursor_indicator(&self) -> Option<char> {
delegate_bindings!(self, BindingMachine::get_cursor_indicator)
fn get_cursor_hint(&self) -> CursorStyle {
delegate_bindings!(self, BindingMachine::get_cursor_hint)
}

fn repeat(&mut self, rt: RepeatType, other: Option<EditContext>) {
Expand Down
18 changes: 9 additions & 9 deletions crates/modalkit/src/env/vim/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3901,10 +3901,10 @@ mod tests {

// Type a digraph.
vm.input_key(ctl!('k'));
assert_eq!(vm.get_cursor_indicator(), Some('?'));
assert_eq!(vm.get_cursor_hint().get_indicator(), Some('?'));

vm.input_key(key!('L'));
assert_eq!(vm.get_cursor_indicator(), Some('L'));
assert_eq!(vm.get_cursor_hint().get_indicator(), Some('L'));

ctx.ch.digraph1 = Some('L');
ctx.ch.digraph2 = Some('i');
Expand All @@ -3914,7 +3914,7 @@ mod tests {

// Type a literal.
vm.input_key(ctl!('v'));
assert_eq!(vm.get_cursor_indicator(), Some('^'));
assert_eq!(vm.get_cursor_hint().get_indicator(), Some('^'));

ctx.ch.digraph1 = None;
ctx.ch.digraph2 = None;
Expand Down Expand Up @@ -3964,20 +3964,20 @@ mod tests {
ctx.action.register_append = false;
vm.input_key(ctl!('r'));
assert_eq!(vm.pop(), None);
assert_eq!(vm.get_cursor_indicator(), Some('"'));
assert_eq!(vm.get_cursor_hint().get_indicator(), Some('"'));
vm.input_key(key!('z'));
assert_pop1!(vm, Action::from(it), ctx);
assert_eq!(vm.mode(), VimMode::Insert);
assert_eq!(vm.get_cursor_indicator(), None);
assert_eq!(vm.get_cursor_hint().get_indicator(), None);

// Pressing ^R^C should go back to Normal mode.
ctx.action.register = None;
vm.input_key(ctl!('r'));
assert_eq!(vm.pop(), None);
assert_eq!(vm.get_cursor_indicator(), Some('"'));
assert_eq!(vm.get_cursor_hint().get_indicator(), Some('"'));
vm.input_key(ctl!('c'));
assert_insert_exit!(vm, ctx);
assert_eq!(vm.get_cursor_indicator(), None);
assert_eq!(vm.get_cursor_hint().get_indicator(), None);
}

#[test]
Expand Down Expand Up @@ -4253,13 +4253,13 @@ mod tests {
vm.input_key(ctl!('v'));
assert_eq!(vm.pop(), None);
assert_eq!(vm.mode(), VimMode::Insert);
assert_eq!(vm.get_cursor_indicator(), Some('^'));
assert_eq!(vm.get_cursor_hint().get_indicator(), Some('^'));

ctx.ch.hex = Some(0x1B);
vm.input_key(key!(KeyCode::Esc));
assert_pop2!(vm, TYPE_CONTEXTUAL, ctx);
assert_eq!(vm.mode(), VimMode::Insert);
assert_eq!(vm.get_cursor_indicator(), None);
assert_eq!(vm.get_cursor_hint().get_indicator(), None);

// Test that typing in a full octal sequence works.
ctx.ch.hex = Some(0x7F);
Expand Down
Loading
Loading