use reedline::{EditCommand, EditMode, PromptEditMode, ReedlineEvent};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Default)]
pub struct EditorState {
pub cursor_pos: usize,
pub buffer_len: usize,
pub buffer: String,
pub uncertain: bool,
}
impl EditorState {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.cursor_pos = 0;
self.buffer_len = 0;
self.buffer.clear();
self.uncertain = false;
}
pub fn cursor_at_begin(&self) -> bool {
self.cursor_pos == 0
}
pub fn is_empty(&self) -> bool {
self.buffer_len == 0
}
pub fn cursor_at_end(&self) -> bool {
self.cursor_pos == self.buffer_len
}
fn char_to_byte_pos(&self, char_pos: usize) -> usize {
self.buffer
.char_indices()
.nth(char_pos)
.map(|(i, _)| i)
.unwrap_or(self.buffer.len())
}
pub fn update_from_event(&mut self, event: &ReedlineEvent) {
match event {
ReedlineEvent::Edit(commands) => {
for cmd in commands {
self.update_from_edit_command(cmd);
}
}
ReedlineEvent::Multiple(events) => {
for e in events {
self.update_from_event(e);
}
}
ReedlineEvent::UntilFound(events) => {
for e in events {
if matches!(
e,
ReedlineEvent::HistoryHintComplete | ReedlineEvent::HistoryHintWordComplete
) {
if self.cursor_at_end() && self.buffer_len > 0 {
self.uncertain = true;
return;
}
break;
}
}
for e in events {
match e {
ReedlineEvent::MenuLeft | ReedlineEvent::MenuRight => {
self.uncertain = true;
return;
}
ReedlineEvent::Left | ReedlineEvent::Right => {
self.update_from_event(e);
return;
}
ReedlineEvent::Edit(_) => {
self.update_from_event(e);
return;
}
_ => {}
}
}
if let Some(first) = events.first() {
self.update_from_event(first);
}
}
ReedlineEvent::Submit | ReedlineEvent::SubmitOrNewline | ReedlineEvent::Enter => {
self.uncertain = true;
}
ReedlineEvent::Left if self.cursor_pos > 0 => {
self.cursor_pos -= 1;
}
ReedlineEvent::Right if self.cursor_pos < self.buffer_len => {
self.cursor_pos += 1;
}
ReedlineEvent::Up | ReedlineEvent::Down => {
self.uncertain = true;
}
_ => {}
}
}
fn update_from_edit_command(&mut self, cmd: &EditCommand) {
match cmd {
EditCommand::InsertChar(c) => {
let byte_pos = self.char_to_byte_pos(self.cursor_pos);
self.buffer.insert(byte_pos, *c);
self.cursor_pos += 1;
self.buffer_len += 1;
}
EditCommand::InsertString(s) => {
let len = s.chars().count();
let byte_pos = self.char_to_byte_pos(self.cursor_pos);
self.buffer.insert_str(byte_pos, s);
self.cursor_pos += len;
self.buffer_len += len;
}
EditCommand::InsertNewline => {
let byte_pos = self.char_to_byte_pos(self.cursor_pos);
self.buffer.insert(byte_pos, '\n');
self.cursor_pos += 1;
self.buffer_len += 1;
}
EditCommand::Backspace => {
if self.cursor_pos > 0 {
let remove_pos = self.cursor_pos - 1;
let byte_start = self.char_to_byte_pos(remove_pos);
let byte_end = self.char_to_byte_pos(self.cursor_pos);
self.buffer.drain(byte_start..byte_end);
self.cursor_pos -= 1;
self.buffer_len -= 1;
}
}
EditCommand::Delete => {
if self.cursor_pos < self.buffer_len {
let byte_start = self.char_to_byte_pos(self.cursor_pos);
let byte_end = self.char_to_byte_pos(self.cursor_pos + 1);
self.buffer.drain(byte_start..byte_end);
self.buffer_len -= 1;
}
}
EditCommand::MoveLeft { .. } => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
}
EditCommand::MoveRight { .. } => {
if self.cursor_pos < self.buffer_len {
self.cursor_pos += 1;
}
}
EditCommand::MoveToStart { .. } | EditCommand::MoveToLineStart { .. } => {
self.cursor_pos = 0;
}
EditCommand::MoveToEnd { .. } | EditCommand::MoveToLineEnd { .. } => {
self.cursor_pos = self.buffer_len;
}
EditCommand::Clear => {
self.reset();
}
EditCommand::CutFromStart => {
let byte_end = self.char_to_byte_pos(self.cursor_pos);
self.buffer.drain(0..byte_end);
self.buffer_len = self.buffer_len.saturating_sub(self.cursor_pos);
self.cursor_pos = 0;
}
EditCommand::CutToEnd | EditCommand::CutToLineEnd => {
let byte_start = self.char_to_byte_pos(self.cursor_pos);
self.buffer.truncate(byte_start);
self.buffer_len = self.cursor_pos;
}
EditCommand::CutWordLeft | EditCommand::CutWordRight => {
self.uncertain = true;
if matches!(cmd, EditCommand::CutWordLeft) {
let removed = self.cursor_pos.min(5);
self.cursor_pos -= removed;
self.buffer_len = self.buffer_len.saturating_sub(removed);
} else {
let remaining = self.buffer_len - self.cursor_pos;
let removed = remaining.min(5);
self.buffer_len -= removed;
}
}
_ => {
self.uncertain = true;
}
}
}
}
pub type EditorStateRef = Arc<Mutex<EditorState>>;
pub fn new_editor_state_ref() -> EditorStateRef {
Arc::new(Mutex::new(EditorState::new()))
}
pub trait KeyCondition: Send + Sync {
fn check(&self, state: &EditorState) -> bool;
}
#[derive(Debug, Clone, Copy)]
pub struct CursorAtBegin;
impl KeyCondition for CursorAtBegin {
fn check(&self, state: &EditorState) -> bool {
state.cursor_at_begin()
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct BufferEmpty;
impl KeyCondition for BufferEmpty {
fn check(&self, state: &EditorState) -> bool {
state.is_empty()
}
}
#[derive(Debug, Clone, Copy)]
pub struct BufferKnownEmpty;
impl KeyCondition for BufferKnownEmpty {
fn check(&self, state: &EditorState) -> bool {
state.is_empty() && !state.uncertain
}
}
pub type EventMatcher = Box<dyn Fn(&ReedlineEvent) -> bool + Send + Sync>;
pub struct ConditionalRule {
pub match_event: EventMatcher,
pub condition: Box<dyn KeyCondition>,
pub fallback_event: ReedlineEvent,
}
pub struct ConditionalEditMode<E: EditMode> {
inner: E,
state: EditorStateRef,
rules: Vec<ConditionalRule>,
completion_min_chars: Option<usize>,
tree_sitter_word_nav: bool,
}
impl<E: EditMode> ConditionalEditMode<E> {
pub fn new(inner: E, state: EditorStateRef) -> Self {
Self {
inner,
state,
rules: Vec::new(),
completion_min_chars: None,
tree_sitter_word_nav: false,
}
}
pub fn with_completion_min_chars(mut self, min_chars: Option<usize>) -> Self {
self.completion_min_chars = min_chars;
self
}
pub fn with_tree_sitter_word_nav(mut self, enabled: bool) -> Self {
self.tree_sitter_word_nav = enabled;
self
}
pub fn with_rule(mut self, rule: ConditionalRule) -> Self {
self.rules.push(rule);
self
}
fn apply_rules(&self, event: ReedlineEvent) -> ReedlineEvent {
let state = self.state.lock().unwrap();
for rule in &self.rules {
if (rule.match_event)(&event) && !rule.condition.check(&state) {
return rule.fallback_event.clone();
}
}
event
}
fn handle_tree_sitter_word_nav(&self, event: &ReedlineEvent) -> Option<ReedlineEvent> {
use super::word_nav::{token_left_position, token_right_position};
if !self.tree_sitter_word_nav {
return None;
}
let state = self.state.lock().unwrap();
if state.uncertain {
return None;
}
match event {
ReedlineEvent::Edit(commands) if commands.len() == 1 => match &commands[0] {
EditCommand::MoveWordLeft { select } => {
let target = token_left_position(&state.buffer, state.cursor_pos);
Some(Self::create_move_event(state.cursor_pos, target, *select))
}
EditCommand::MoveWordRight { select } => {
let target = token_right_position(&state.buffer, state.cursor_pos);
Some(Self::create_move_event(state.cursor_pos, target, *select))
}
_ => None,
},
ReedlineEvent::UntilFound(events) => {
for (i, e) in events.iter().enumerate() {
if let ReedlineEvent::Edit(commands) = e
&& commands.len() == 1
{
if let EditCommand::MoveWordRight { select } = &commands[0] {
let target = token_right_position(&state.buffer, state.cursor_pos);
let move_event =
Self::create_move_event(state.cursor_pos, target, *select);
let mut new_events = events.clone();
new_events[i] = move_event;
return Some(ReedlineEvent::UntilFound(new_events));
}
if let EditCommand::MoveWordLeft { select } = &commands[0] {
let target = token_left_position(&state.buffer, state.cursor_pos);
let move_event =
Self::create_move_event(state.cursor_pos, target, *select);
let mut new_events = events.clone();
new_events[i] = move_event;
return Some(ReedlineEvent::UntilFound(new_events));
}
}
}
None
}
_ => None,
}
}
fn create_move_event(current: usize, target: usize, select: bool) -> ReedlineEvent {
if current == target {
return ReedlineEvent::None;
}
if target < current {
let diff = current - target;
let commands: Vec<EditCommand> =
std::iter::repeat_n(EditCommand::MoveLeft { select }, diff).collect();
ReedlineEvent::Edit(commands)
} else {
let diff = target - current;
let commands: Vec<EditCommand> =
std::iter::repeat_n(EditCommand::MoveRight { select }, diff).collect();
ReedlineEvent::Edit(commands)
}
}
}
impl<E: EditMode> EditMode for ConditionalEditMode<E> {
fn parse_event(&mut self, event: reedline::ReedlineRawEvent) -> ReedlineEvent {
let inner_event = self.inner.parse_event(event);
let rules_event = self.apply_rules(inner_event);
let final_event = self
.handle_tree_sitter_word_nav(&rules_event)
.unwrap_or(rules_event);
{
let mut state = self.state.lock().unwrap();
state.update_from_event(&final_event);
}
if let Some(min_chars) = self.completion_min_chars {
let state = self.state.lock().unwrap();
if state.buffer_len >= min_chars
&& !state.uncertain
&& is_character_insert(&final_event)
{
return ReedlineEvent::Multiple(vec![
final_event,
ReedlineEvent::Menu("completion_menu".to_string()),
]);
}
}
final_event
}
fn edit_mode(&self) -> PromptEditMode {
self.inner.edit_mode()
}
}
fn is_character_insert(event: &ReedlineEvent) -> bool {
match event {
ReedlineEvent::Edit(commands) => commands.iter().any(|cmd| {
matches!(
cmd,
EditCommand::InsertChar(_) | EditCommand::InsertString(_)
)
}),
ReedlineEvent::Multiple(events) => events.iter().any(is_character_insert),
_ => false,
}
}
#[cfg(test)]
mod tests;