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
}
#[allow(dead_code)] pub fn char_before_cursor(&self) -> Option<char> {
if self.uncertain || self.cursor_pos == 0 {
return None;
}
self.buffer.chars().nth(self.cursor_pos - 1)
}
pub fn char_after_cursor(&self) -> Option<char> {
if self.uncertain || self.cursor_pos >= self.buffer_len {
return None;
}
self.buffer.chars().nth(self.cursor_pos)
}
pub fn is_inside_empty_pair(&self) -> bool {
if self.uncertain {
return false;
}
if self.buffer.contains('\n') {
return false;
}
let Some(before) = self.char_before_cursor() else {
return false;
};
let Some(after) = self.char_after_cursor() else {
return false;
};
matches!(
(before, after),
('(', ')') | ('[', ']') | ('{', '}') | ('"', '"') | ('\'', '\'') | ('`', '`')
)
}
pub fn cursor_in_quote(&self, quote_char: char) -> bool {
if self.uncertain {
return false;
}
let text_before: String = self.buffer.chars().take(self.cursor_pos).collect();
let mut count = 0;
let mut chars = text_before.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
chars.next();
} else if c == quote_char {
count += 1;
}
}
count % 2 == 1
}
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::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
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct CursorAtEnd;
impl KeyCondition for CursorAtEnd {
fn check(&self, state: &EditorState) -> bool {
state.cursor_at_end()
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct InsideEmptyPair;
impl KeyCondition for InsideEmptyPair {
fn check(&self, state: &EditorState) -> bool {
state.is_inside_empty_pair()
}
}
#[derive(Debug, Clone, Copy)]
pub struct NotInsideEmptyPair;
impl KeyCondition for NotInsideEmptyPair {
fn check(&self, state: &EditorState) -> bool {
!state.is_inside_empty_pair()
}
}
#[derive(Debug, Clone, Copy)]
pub struct CursorAtEndOrBeforeClosing;
impl CursorAtEndOrBeforeClosing {
const CLOSING_CHARS: [char; 6] = [')', ']', '}', '"', '\'', '`'];
}
impl KeyCondition for CursorAtEndOrBeforeClosing {
fn check(&self, state: &EditorState) -> bool {
if state.cursor_at_end() {
return true;
}
if let Some(char_after) = state.char_after_cursor() {
return Self::CLOSING_CHARS.contains(&char_after);
}
false
}
}
pub struct CursorAtEndOrBeforeClosingAndNotInQuote {
quote_char: char,
}
impl CursorAtEndOrBeforeClosingAndNotInQuote {
pub fn new(quote_char: char) -> Self {
Self { quote_char }
}
}
impl KeyCondition for CursorAtEndOrBeforeClosingAndNotInQuote {
fn check(&self, state: &EditorState) -> bool {
if !CursorAtEndOrBeforeClosing.check(state) {
return false;
}
!state.cursor_in_quote(self.quote_char)
}
}
pub struct CursorNotBeforeChar {
target_char: char,
}
impl CursorNotBeforeChar {
pub fn new(target_char: char) -> Self {
Self { target_char }
}
}
impl KeyCondition for CursorNotBeforeChar {
fn check(&self, state: &EditorState) -> bool {
if let Some(char_after) = state.char_after_cursor() {
char_after != self.target_char
} else {
true
}
}
}
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
}
pub fn with_rules(mut self, rules: impl IntoIterator<Item = ConditionalRule>) -> Self {
self.rules.extend(rules);
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,
}
}
pub fn create_auto_match_rules() -> Vec<ConditionalRule> {
let pairs: [(char, &str, bool); 6] = [
('(', "()", false),
('[', "[]", false),
('{', "{}", false),
('"', r#""""#, true),
('\'', "''", true),
('`', "``", true),
];
pairs
.into_iter()
.map(|(open_char, pair, is_quote)| {
let pair_string = pair.to_string();
let condition: Box<dyn KeyCondition> = if is_quote {
Box::new(CursorAtEndOrBeforeClosingAndNotInQuote::new(open_char))
} else {
Box::new(CursorAtEndOrBeforeClosing)
};
ConditionalRule {
match_event: Box::new(move |event| {
matches!(
event,
ReedlineEvent::Edit(cmds)
if cmds.len() == 2
&& matches!(&cmds[0], EditCommand::InsertString(s) if s == &pair_string)
&& matches!(&cmds[1], EditCommand::MoveLeft { select: false })
)
}),
condition,
fallback_event: ReedlineEvent::Edit(vec![EditCommand::InsertChar(open_char)]),
}
})
.collect()
}
pub fn create_skip_over_rules() -> Vec<ConditionalRule> {
let mut rules = Vec::new();
let bracket_closing_chars = [')', ']', '}'];
for close_char in bracket_closing_chars {
rules.push(ConditionalRule {
match_event: Box::new(move |event| {
matches!(
event,
ReedlineEvent::Edit(cmds)
if cmds.len() == 1
&& matches!(&cmds[0], EditCommand::InsertChar(c) if *c == close_char)
)
}),
condition: Box::new(CursorNotBeforeChar::new(close_char)),
fallback_event: ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]),
});
}
let quote_chars: [(char, &str); 3] = [('"', r#""""#), ('\'', "''"), ('`', "``")];
for (quote_char, pair) in quote_chars {
let pair_string = pair.to_string();
rules.push(ConditionalRule {
match_event: Box::new(move |event| {
matches!(
event,
ReedlineEvent::Edit(cmds)
if cmds.len() == 2
&& matches!(&cmds[0], EditCommand::InsertString(s) if s == &pair_string)
&& matches!(&cmds[1], EditCommand::MoveLeft { select: false })
)
}),
condition: Box::new(CursorNotBeforeChar::new(quote_char)),
fallback_event: ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]),
});
}
rules
}
pub fn create_bracket_delete_rules() -> Vec<ConditionalRule> {
vec![ConditionalRule {
match_event: Box::new(|event| {
matches!(
event,
ReedlineEvent::Edit(cmds)
if cmds.len() == 1 && matches!(&cmds[0], EditCommand::Backspace)
)
}),
condition: Box::new(NotInsideEmptyPair),
fallback_event: ReedlineEvent::Edit(vec![EditCommand::Backspace, EditCommand::Delete]),
}]
}
#[cfg(test)]
mod tests;