use super::effects::{VimEffect, VimEffectBatch};
use crate::{
buffer::BufferEdit,
ecs::{
components::{
buffer::{BufferEntity, ViewEntity},
cursor::ByteIndex,
},
events::{
cursor::CursorMoveRequested,
edit::BufferEditRequested,
input::{EditorInputEvent, VimInputRejected, VimInputRejectionReason},
},
},
vim::{
DEFAULT_INSERT_TEXT_BYTES_PER_FRAME, INSERT_NEWLINE, InsertCommand, InsertEditPlanner,
InsertEntry, InsertPlanEdit, InsertState, KeyToken, VimMode, resolve_insert_entry_caret,
},
};
use bevy::prelude::MessageWriter;
pub(super) const MAX_INSERT_TEXT_BYTES_PER_FRAME: usize = DEFAULT_INSERT_TEXT_BYTES_PER_FRAME;
pub(super) struct InsertInputContext<'input, 'writer> {
pub(super) text: &'input str,
pub(super) target: ViewEntity,
pub(super) buffer_target: BufferEntity,
pub(super) cursor_byte_index: usize,
pub(super) insert_state: InsertState,
pub(super) events: &'input [EditorInputEvent],
pub(super) effects: &'input mut VimEffectBatch,
pub(super) rejected: &'input mut MessageWriter<'writer, VimInputRejected>,
}
pub(super) fn handle_insert_input(mode: &mut VimMode, context: InsertInputContext<'_, '_>) -> bool {
let InsertInputContext {
text,
target,
buffer_target,
cursor_byte_index,
insert_state,
events,
effects,
rejected,
} = context;
let mut planner =
InsertEditPlanner::new(text, cursor_byte_index, MAX_INSERT_TEXT_BYTES_PER_FRAME);
let mut exit_requested = false;
for event in events {
let Some(command) = insert_command_from_event(event) else {
continue;
};
match command {
InsertCommand::InsertText(text) => {
if planner.push_insert(&text).is_err() {
reject_insert_payload(target, text.len(), rejected);
}
}
InsertCommand::InsertNewline => {
if planner.push_insert(INSERT_NEWLINE).is_err() {
reject_insert_payload(target, INSERT_NEWLINE.len(), rejected);
}
}
InsertCommand::DeleteBackward => planner.delete_backward(),
InsertCommand::Exit => {
exit_requested = true;
break;
}
}
}
let next_caret = if exit_requested {
*mode = VimMode::Normal;
planner.exit_caret(insert_state.exit_policy())
} else {
planner.caret()
};
emit_insert_plan_edits(buffer_target, planner.into_edits(), effects);
let cursor_move = if exit_requested {
CursorMoveRequested::cursor_cell(target, ByteIndex(next_caret), None)
} else {
CursorMoveRequested::insert_caret(target, ByteIndex(next_caret), None)
};
effects.push(VimEffect::CursorMove(cursor_move));
true
}
pub(super) fn enter_insert_mode(
entry: InsertEntry,
text: &str,
cursor_byte_index: usize,
mode: &mut VimMode,
target: ViewEntity,
buffer_target: BufferEntity,
effects: &mut VimEffectBatch,
) {
let caret = resolve_insert_entry_caret(text, cursor_byte_index, entry);
if entry.opens_line() {
effects.push(VimEffect::BufferEdit(BufferEditRequested {
target: buffer_target,
edit: BufferEdit::insert(caret.edit_byte_index(), INSERT_NEWLINE),
}));
}
*mode = VimMode::Insert(entry.insert_state());
effects.push(VimEffect::CursorMove(CursorMoveRequested::insert_caret(
target,
ByteIndex(caret.caret_byte_index()),
None,
)));
}
fn insert_command_from_event(event: &EditorInputEvent) -> Option<InsertCommand> {
match event {
EditorInputEvent::Text(text) if !text.text.is_empty() => {
Some(InsertCommand::insert_text(text.text.clone()))
}
EditorInputEvent::Text(_) => None,
EditorInputEvent::Key(key) => match key.token {
KeyToken::Escape => Some(InsertCommand::Exit),
KeyToken::Enter => Some(InsertCommand::InsertNewline),
KeyToken::Backspace => Some(InsertCommand::DeleteBackward),
KeyToken::Char(character) if key.repeated && !character.is_control() => {
Some(InsertCommand::insert_text(character.to_string()))
}
KeyToken::Ctrl(_) | KeyToken::Char(_) => None,
},
}
}
fn emit_insert_plan_edits(
buffer_target: BufferEntity,
edits: Vec<InsertPlanEdit>,
effects: &mut VimEffectBatch,
) {
effects.extend(edits.into_iter().map(|edit| {
let edit = match edit {
InsertPlanEdit::Insert { byte_index, text } => BufferEdit::insert(byte_index, text),
InsertPlanEdit::Delete { range } => BufferEdit::delete_unchecked(range),
};
VimEffect::BufferEdit(BufferEditRequested {
target: buffer_target,
edit,
})
}));
}
fn reject_insert_payload(
target: ViewEntity,
rejected_byte_len: usize,
rejected: &mut MessageWriter<VimInputRejected>,
) {
let _sent = rejected.write(VimInputRejected {
target,
reason: VimInputRejectionReason::InsertPayloadTooLarge {
target,
rejected_byte_len,
max_byte_len: MAX_INSERT_TEXT_BYTES_PER_FRAME,
},
});
}
#[cfg(test)]
mod tests {
use super::{VimEffectBatch, emit_insert_plan_edits};
use crate::{
buffer::{BufferEdit, BufferEditShape},
vim::InsertPlanEdit,
};
#[test]
fn adapter_converts_insert_plans_to_buffer_edit_effects() {
let mut effects = VimEffectBatch::default();
emit_insert_plan_edits(
crate::ecs::components::buffer::BufferEntity(
bevy::prelude::Entity::from_raw_u32(0).expect("valid entity index"),
),
vec![InsertPlanEdit::insert(1, "λ")],
&mut effects,
);
let [super::VimEffect::BufferEdit(request)] = effects.as_slice() else {
panic!("expected one buffer edit");
};
assert_eq!(
BufferEditShape::from_edit(&request.edit),
BufferEditShape::from_edit(&BufferEdit::insert(1, "λ"))
);
}
#[test]
fn adapter_preserves_delete_plan_order() {
let mut effects = VimEffectBatch::default();
emit_insert_plan_edits(
crate::ecs::components::buffer::BufferEntity(
bevy::prelude::Entity::from_raw_u32(0).expect("valid entity index"),
),
vec![InsertPlanEdit::delete(2..3), InsertPlanEdit::delete(1..2)],
&mut effects,
);
assert_eq!(effects.as_slice().len(), 2);
}
}