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 {
use super::*;
#[test]
fn test_editor_state_initial() {
let state = EditorState::new();
assert_eq!(state.cursor_pos, 0);
assert_eq!(state.buffer_len, 0);
assert!(state.cursor_at_begin());
assert!(state.is_empty());
}
#[test]
fn test_editor_state_insert_char() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')]));
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 1);
assert!(!state.cursor_at_begin());
assert!(!state.is_empty());
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
assert_eq!(state.cursor_pos, 2);
assert_eq!(state.buffer_len, 2);
}
#[test]
fn test_editor_state_insert_string() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertString(
"hello".to_string(),
)]));
assert_eq!(state.cursor_pos, 5);
assert_eq!(state.buffer_len, 5);
}
#[test]
fn test_editor_state_backspace() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertString(
"abc".to_string(),
)]));
assert_eq!(state.cursor_pos, 3);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Backspace]));
assert_eq!(state.cursor_pos, 2);
assert_eq!(state.buffer_len, 2);
state.cursor_pos = 0;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Backspace]));
assert_eq!(state.cursor_pos, 0);
}
#[test]
fn test_editor_state_move() {
let mut state = EditorState::new();
state.buffer_len = 5;
state.cursor_pos = 2;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
select: false,
}]));
assert_eq!(state.cursor_pos, 1);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::MoveRight {
select: false,
}]));
assert_eq!(state.cursor_pos, 2);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::MoveToStart {
select: false,
}]));
assert_eq!(state.cursor_pos, 0);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::MoveToEnd {
select: false,
}]));
assert_eq!(state.cursor_pos, 5);
}
#[test]
fn test_editor_state_uncertain_on_submit() {
let mut state = EditorState::new();
state.buffer = "hello".to_string();
state.cursor_pos = 5;
state.buffer_len = 5;
assert!(!state.uncertain);
state.update_from_event(&ReedlineEvent::Submit);
assert!(state.uncertain);
assert_eq!(state.cursor_pos, 5);
assert_eq!(state.buffer_len, 5);
}
#[test]
fn test_editor_state_multiple_events() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::InsertChar(':')]),
ReedlineEvent::Menu("completion_menu".to_string()),
]));
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 1);
}
#[test]
fn test_cursor_at_begin_condition() {
let condition = CursorAtBegin;
let mut state = EditorState::new();
assert!(condition.check(&state));
state.cursor_pos = 1;
assert!(!condition.check(&state));
}
#[test]
fn test_buffer_empty_condition() {
let condition = BufferEmpty;
let mut state = EditorState::new();
assert!(condition.check(&state));
state.buffer_len = 1;
assert!(!condition.check(&state));
}
#[test]
fn test_buffer_known_empty_condition() {
let condition = BufferKnownEmpty;
let mut state = EditorState::new();
state.buffer_len = 0;
state.uncertain = false;
assert!(
condition.check(&state),
"certain empty buffer should trigger semicolon shortcut condition"
);
state.uncertain = true;
assert!(
!condition.check(&state),
"uncertain empty buffer should NOT trigger semicolon shortcut condition"
);
state.buffer_len = 1;
state.uncertain = false;
assert!(
!condition.check(&state),
"non-empty buffer should not match"
);
}
#[test]
fn test_cursor_at_end_condition() {
let condition = CursorAtEnd;
let mut state = EditorState::new();
assert!(state.cursor_at_end());
assert!(condition.check(&state));
state.buffer_len = 5;
state.cursor_pos = 5;
assert!(state.cursor_at_end());
assert!(condition.check(&state));
state.cursor_pos = 2;
assert!(!state.cursor_at_end());
assert!(!condition.check(&state));
state.cursor_pos = 0;
assert!(!state.cursor_at_end());
assert!(!condition.check(&state));
}
#[test]
fn test_auto_match_rules_created() {
let rules = create_auto_match_rules();
assert_eq!(rules.len(), 6);
}
#[test]
fn test_auto_match_rule_matches_paren() {
let rules = create_auto_match_rules();
let paren_rule = &rules[0];
let match_event = ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]);
assert!((paren_rule.match_event)(&match_event));
let other_event = ReedlineEvent::Edit(vec![EditCommand::InsertChar('(')]);
assert!(!(paren_rule.match_event)(&other_event));
let bracket_event = ReedlineEvent::Edit(vec![
EditCommand::InsertString("[]".to_string()),
EditCommand::MoveLeft { select: false },
]);
assert!(!(paren_rule.match_event)(&bracket_event));
}
#[test]
fn test_auto_match_condition_cursor_at_end() {
let rules = create_auto_match_rules();
let rule = &rules[0];
let mut state = EditorState::new();
state.buffer_len = 5;
state.cursor_pos = 5;
assert!(rule.condition.check(&state));
state.cursor_pos = 2;
assert!(!rule.condition.check(&state));
}
#[test]
fn test_auto_match_fallback_event() {
let rules = create_auto_match_rules();
let expected_fallbacks = ['(', '[', '{', '"', '\'', '`'];
for (rule, expected_char) in rules.iter().zip(expected_fallbacks.iter()) {
match &rule.fallback_event {
ReedlineEvent::Edit(cmds) => {
assert_eq!(cmds.len(), 1);
match &cmds[0] {
EditCommand::InsertChar(c) => assert_eq!(c, expected_char),
_ => panic!("Expected InsertChar"),
}
}
_ => panic!("Expected Edit event"),
}
}
}
#[test]
fn test_auto_match_full_flow_at_end() {
let state_ref = new_editor_state_ref();
let rules = create_auto_match_rules();
{
let mut state = state_ref.lock().unwrap();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('c')]));
assert_eq!(state.cursor_pos, 3);
assert_eq!(state.buffer_len, 3);
assert!(state.cursor_at_end());
}
let paren_rule = &rules[0];
{
let state = state_ref.lock().unwrap();
assert!(state.cursor_at_end());
assert!(paren_rule.condition.check(&state));
}
}
#[test]
fn test_auto_match_blocked_when_not_at_end() {
let state_ref = new_editor_state_ref();
let rules = create_auto_match_rules();
{
let mut state = state_ref.lock().unwrap();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('c')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
select: false,
}]));
assert_eq!(state.cursor_pos, 2);
assert_eq!(state.buffer_len, 3);
assert!(!state.cursor_at_end());
}
let paren_rule = &rules[0];
{
let state = state_ref.lock().unwrap();
assert!(!state.cursor_at_end());
assert!(!paren_rule.condition.check(&state));
}
}
#[test]
fn test_until_found_right_updates_cursor() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('c')]));
assert_eq!(state.cursor_pos, 3);
assert_eq!(state.buffer_len, 3);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
select: false,
}]));
assert_eq!(state.cursor_pos, 2);
assert!(!state.cursor_at_end());
state.update_from_event(&ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintComplete,
ReedlineEvent::MenuRight,
ReedlineEvent::Right,
]));
assert_eq!(state.cursor_pos, 3);
assert!(state.cursor_at_end());
assert!(!state.uncertain); }
#[test]
fn test_until_found_right_at_end_marks_uncertain() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('c')]));
assert_eq!(state.cursor_pos, 3);
assert!(state.cursor_at_end());
assert!(!state.uncertain);
state.update_from_event(&ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintComplete,
ReedlineEvent::MenuRight,
ReedlineEvent::Right,
]));
assert!(state.uncertain);
}
#[test]
fn test_until_found_right_on_empty_buffer_not_uncertain() {
let mut state = EditorState::new();
assert_eq!(state.cursor_pos, 0);
assert_eq!(state.buffer_len, 0);
assert!(state.cursor_at_end()); assert!(!state.uncertain);
state.update_from_event(&ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintComplete,
ReedlineEvent::MenuRight,
ReedlineEvent::Right,
]));
assert!(!state.uncertain);
assert_eq!(state.cursor_pos, 0);
}
#[test]
fn test_auto_match_inside_braces_regression() {
let rules = create_auto_match_rules();
let paren_rule = &rules[0];
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("{}".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "{}");
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 2);
assert!(!state.cursor_at_end());
assert!(!state.uncertain);
assert_eq!(state.char_after_cursor(), Some('}'));
assert!(paren_rule.condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "{()}");
assert_eq!(state.cursor_pos, 2); }
#[test]
fn test_until_found_left_updates_cursor() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('c')]));
assert_eq!(state.cursor_pos, 3);
assert!(state.cursor_at_end());
state.update_from_event(&ReedlineEvent::UntilFound(vec![
ReedlineEvent::MenuLeft,
ReedlineEvent::Left,
]));
assert_eq!(state.cursor_pos, 2);
assert!(!state.cursor_at_end());
}
#[test]
fn test_right_event_updates_cursor() {
let mut state = EditorState::new();
state.buffer_len = 5;
state.cursor_pos = 2;
state.update_from_event(&ReedlineEvent::Right);
assert_eq!(state.cursor_pos, 3);
state.cursor_pos = 5;
state.update_from_event(&ReedlineEvent::Right);
assert_eq!(state.cursor_pos, 5);
}
#[test]
fn test_left_event_updates_cursor() {
let mut state = EditorState::new();
state.buffer_len = 5;
state.cursor_pos = 2;
state.update_from_event(&ReedlineEvent::Left);
assert_eq!(state.cursor_pos, 1);
state.cursor_pos = 0;
state.update_from_event(&ReedlineEvent::Left);
assert_eq!(state.cursor_pos, 0);
}
#[test]
fn test_char_before_cursor() {
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 2;
assert_eq!(state.char_before_cursor(), Some('b'));
state.cursor_pos = 0;
assert_eq!(state.char_before_cursor(), None);
state.cursor_pos = 3;
assert_eq!(state.char_before_cursor(), Some('c'));
}
#[test]
fn test_char_after_cursor() {
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 1;
assert_eq!(state.char_after_cursor(), Some('b'));
state.cursor_pos = 3;
assert_eq!(state.char_after_cursor(), None);
state.cursor_pos = 0;
assert_eq!(state.char_after_cursor(), Some('a'));
}
#[test]
fn test_char_methods_uncertain() {
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 1;
state.uncertain = true;
assert_eq!(state.char_before_cursor(), None);
assert_eq!(state.char_after_cursor(), None);
}
#[test]
fn test_is_inside_empty_pair_parens() {
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(state.is_inside_empty_pair());
}
#[test]
fn test_is_inside_empty_pair_brackets() {
let mut state = EditorState::new();
state.buffer = "[]".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(state.is_inside_empty_pair());
}
#[test]
fn test_is_inside_empty_pair_braces() {
let mut state = EditorState::new();
state.buffer = "{}".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(state.is_inside_empty_pair());
}
#[test]
fn test_is_inside_empty_pair_quotes() {
let mut state = EditorState::new();
state.buffer = r#""""#.to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(state.is_inside_empty_pair());
state.buffer = "''".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(state.is_inside_empty_pair());
state.buffer = "``".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(state.is_inside_empty_pair());
}
#[test]
fn test_is_inside_empty_pair_not_empty() {
let mut state = EditorState::new();
state.buffer = "(x)".to_string();
state.buffer_len = 3;
state.cursor_pos = 1;
assert!(!state.is_inside_empty_pair());
}
#[test]
fn test_is_inside_empty_pair_mismatched() {
let mut state = EditorState::new();
state.buffer = "(]".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(!state.is_inside_empty_pair());
}
#[test]
fn test_is_inside_empty_pair_uncertain() {
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
state.uncertain = true;
assert!(!state.is_inside_empty_pair());
}
#[test]
fn test_is_inside_empty_pair_edge_positions() {
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 0;
assert!(!state.is_inside_empty_pair());
state.cursor_pos = 2;
assert!(!state.is_inside_empty_pair());
}
#[test]
fn test_reset_clears_buffer() {
let mut state = EditorState::new();
state.buffer = "test".to_string();
state.buffer_len = 4;
state.cursor_pos = 2;
state.uncertain = true;
state.reset();
assert!(state.buffer.is_empty());
assert_eq!(state.buffer_len, 0);
assert_eq!(state.cursor_pos, 0);
assert!(!state.uncertain);
}
#[test]
fn test_insert_char_updates_buffer() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')]));
assert_eq!(state.buffer, "a");
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 1);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
assert_eq!(state.buffer, "ab");
assert_eq!(state.cursor_pos, 2);
assert_eq!(state.buffer_len, 2);
}
#[test]
fn test_insert_string_updates_buffer() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertString(
"hello".to_string(),
)]));
assert_eq!(state.buffer, "hello");
assert_eq!(state.cursor_pos, 5);
assert_eq!(state.buffer_len, 5);
}
#[test]
fn test_insert_char_in_middle() {
let mut state = EditorState::new();
state.buffer = "ac".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('b')]));
assert_eq!(state.buffer, "abc");
assert_eq!(state.cursor_pos, 2);
assert_eq!(state.buffer_len, 3);
}
#[test]
fn test_backspace_updates_buffer() {
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 3;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Backspace]));
assert_eq!(state.buffer, "ab");
assert_eq!(state.cursor_pos, 2);
assert_eq!(state.buffer_len, 2);
}
#[test]
fn test_backspace_in_middle() {
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 2;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Backspace]));
assert_eq!(state.buffer, "ac");
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 2);
}
#[test]
fn test_delete_updates_buffer() {
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 1;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Delete]));
assert_eq!(state.buffer, "ac");
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 2);
}
#[test]
fn test_cut_from_start_updates_buffer() {
let mut state = EditorState::new();
state.buffer = "hello world".to_string();
state.buffer_len = 11;
state.cursor_pos = 6;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::CutFromStart]));
assert_eq!(state.buffer, "world");
assert_eq!(state.cursor_pos, 0);
assert_eq!(state.buffer_len, 5);
}
#[test]
fn test_cut_to_end_updates_buffer() {
let mut state = EditorState::new();
state.buffer = "hello world".to_string();
state.buffer_len = 11;
state.cursor_pos = 5;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::CutToEnd]));
assert_eq!(state.buffer, "hello");
assert_eq!(state.cursor_pos, 5);
assert_eq!(state.buffer_len, 5);
}
#[test]
fn test_history_navigation_sets_uncertain() {
let mut state = EditorState::new();
state.buffer = "test".to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
assert!(!state.uncertain);
state.update_from_event(&ReedlineEvent::Up);
assert!(state.uncertain);
}
#[test]
fn test_unicode_insert_and_delete() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('æ—¥')]));
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertChar('本')]));
assert_eq!(state.buffer, "日本");
assert_eq!(state.cursor_pos, 2);
assert_eq!(state.buffer_len, 2);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Backspace]));
assert_eq!(state.buffer, "æ—¥");
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 1);
}
#[test]
fn test_auto_pair_simulation() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "()");
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 2);
assert!(state.is_inside_empty_pair());
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Backspace]));
assert_eq!(state.buffer, ")");
assert_eq!(state.cursor_pos, 0);
assert_eq!(state.buffer_len, 1);
}
#[test]
fn test_bracket_delete_rules_created() {
let rules = create_bracket_delete_rules();
assert_eq!(rules.len(), 1);
}
#[test]
fn test_bracket_delete_rule_matches_backspace() {
let rules = create_bracket_delete_rules();
let rule = &rules[0];
let backspace_event = ReedlineEvent::Edit(vec![EditCommand::Backspace]);
assert!((rule.match_event)(&backspace_event));
let delete_event = ReedlineEvent::Edit(vec![EditCommand::Delete]);
assert!(!(rule.match_event)(&delete_event));
let multiple_event = ReedlineEvent::Edit(vec![EditCommand::Backspace, EditCommand::Delete]);
assert!(!(rule.match_event)(&multiple_event));
}
#[test]
fn test_bracket_delete_condition_not_inside_pair() {
let rules = create_bracket_delete_rules();
let rule = &rules[0];
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 2;
assert!(rule.condition.check(&state));
}
#[test]
fn test_bracket_delete_condition_inside_pair() {
let rules = create_bracket_delete_rules();
let rule = &rules[0];
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(!rule.condition.check(&state));
}
#[test]
fn test_bracket_delete_fallback_event() {
let rules = create_bracket_delete_rules();
let rule = &rules[0];
match &rule.fallback_event {
ReedlineEvent::Edit(cmds) => {
assert_eq!(cmds.len(), 2);
assert!(matches!(&cmds[0], EditCommand::Backspace));
assert!(matches!(&cmds[1], EditCommand::Delete));
}
_ => panic!("Expected Edit event"),
}
}
#[test]
fn test_bracket_delete_full_flow() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "()");
assert_eq!(state.cursor_pos, 1);
assert!(state.is_inside_empty_pair());
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::Backspace,
EditCommand::Delete,
]));
assert_eq!(state.buffer, "");
assert_eq!(state.cursor_pos, 0);
assert_eq!(state.buffer_len, 0);
}
#[test]
fn test_bracket_delete_all_pair_types() {
let pairs = [
("()", 1),
("[]", 1),
("{}", 1),
(r#""""#, 1),
("''", 1),
("``", 1),
];
for (pair, cursor_pos) in pairs {
let mut state = EditorState::new();
state.buffer = pair.to_string();
state.buffer_len = 2;
state.cursor_pos = cursor_pos;
assert!(
state.is_inside_empty_pair(),
"Expected to be inside empty pair for: {}",
pair
);
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::Backspace,
EditCommand::Delete,
]));
assert_eq!(
state.buffer, "",
"Buffer should be empty after deleting {}",
pair
);
}
}
#[test]
fn test_bracket_delete_uncertain_state() {
let rules = create_bracket_delete_rules();
let rule = &rules[0];
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
state.uncertain = true;
assert!(!state.is_inside_empty_pair());
assert!(rule.condition.check(&state));
}
#[test]
fn test_cursor_at_end_or_before_closing_at_end() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
assert!(condition.check(&state));
state.buffer = "foo".to_string();
state.buffer_len = 3;
state.cursor_pos = 3;
assert!(condition.check(&state));
}
#[test]
fn test_cursor_at_end_or_before_closing_before_closing_chars() {
let condition = CursorAtEndOrBeforeClosing;
let closing_chars = [')', ']', '}', '"', '\'', '`'];
for closing_char in closing_chars {
let mut state = EditorState::new();
state.buffer = format!("x{}", closing_char);
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(
condition.check(&state),
"Expected condition to pass when cursor is before '{}'",
closing_char
);
}
}
#[test]
fn test_cursor_at_end_or_before_closing_before_regular_char() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = "abc".to_string();
state.buffer_len = 3;
state.cursor_pos = 1;
assert!(!condition.check(&state));
}
#[test]
fn test_cursor_at_end_or_before_closing_uncertain() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
state.uncertain = true;
assert!(!condition.check(&state));
}
#[test]
fn test_auto_match_quote_inside_parens() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString(r#""""#.to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, r#"("")"#);
assert_eq!(state.cursor_pos, 2); }
#[test]
fn test_auto_match_parens_inside_quotes() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = r#""""#.to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, r#""()""#);
assert_eq!(state.cursor_pos, 2); }
#[test]
fn test_auto_match_braces_inside_brackets() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = "[]".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("{}".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "[{}]");
assert_eq!(state.cursor_pos, 2);
}
#[test]
fn test_auto_match_parens_inside_raw_string() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = r#"r"""#.to_string();
state.buffer_len = 3;
state.cursor_pos = 2;
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, r#"r"()""#);
assert_eq!(state.cursor_pos, 3);
}
#[test]
fn test_auto_match_at_end_still_works() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = "foo".to_string();
state.buffer_len = 3;
state.cursor_pos = 3;
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "foo()");
assert_eq!(state.cursor_pos, 4);
}
#[test]
fn test_auto_match_blocked_in_middle() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = "foobar".to_string();
state.buffer_len = 6;
state.cursor_pos = 3;
assert!(!condition.check(&state));
}
#[test]
fn test_auto_match_deeply_nested() {
let condition = CursorAtEndOrBeforeClosing;
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("[]".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "([])");
assert_eq!(state.cursor_pos, 2);
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("{}".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "([{}])");
assert_eq!(state.cursor_pos, 3);
assert!(condition.check(&state));
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString(r#""""#.to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, r#"([{""}])"#);
assert_eq!(state.cursor_pos, 4);
}
#[test]
fn test_auto_match_rules_use_new_condition() {
let rules = create_auto_match_rules();
let paren_rule = &rules[0];
let mut state = EditorState::new();
state.buffer = "()".to_string();
state.buffer_len = 2;
state.cursor_pos = 1;
assert!(paren_rule.condition.check(&state));
state.buffer = "ab".to_string();
state.cursor_pos = 1;
assert!(!paren_rule.condition.check(&state));
}
#[test]
fn test_cursor_in_quote_empty() {
let state = EditorState::new();
assert!(!state.cursor_in_quote('"'));
assert!(!state.cursor_in_quote('\''));
}
#[test]
fn test_cursor_in_quote_unclosed() {
let mut state = EditorState::new();
state.buffer = r#""foo"#.to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
assert!(state.cursor_in_quote('"'));
assert!(!state.cursor_in_quote('\''));
state.buffer = "'foo".to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
assert!(state.cursor_in_quote('\''));
assert!(!state.cursor_in_quote('"'));
}
#[test]
fn test_cursor_in_quote_closed() {
let mut state = EditorState::new();
state.buffer = r#""foo""#.to_string();
state.buffer_len = 5;
state.cursor_pos = 5;
assert!(!state.cursor_in_quote('"'));
state.buffer = r#""foo" "#.to_string();
state.buffer_len = 6;
state.cursor_pos = 6;
assert!(!state.cursor_in_quote('"'));
}
#[test]
fn test_cursor_in_quote_escaped() {
let mut state = EditorState::new();
state.buffer = r#""foo\"bar"#.to_string();
state.buffer_len = 9;
state.cursor_pos = 9;
assert!(state.cursor_in_quote('"'));
state.buffer = r#""foo\"bar""#.to_string();
state.buffer_len = 10;
state.cursor_pos = 10;
assert!(!state.cursor_in_quote('"'));
}
#[test]
fn test_cursor_in_quote_uncertain() {
let mut state = EditorState::new();
state.buffer = r#""foo"#.to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
state.uncertain = true;
assert!(!state.cursor_in_quote('"'));
}
#[test]
fn test_quote_auto_match_condition_not_in_string() {
let condition = CursorAtEndOrBeforeClosingAndNotInQuote::new('"');
let mut state = EditorState::new();
state.buffer = "foo".to_string();
state.buffer_len = 3;
state.cursor_pos = 3;
assert!(condition.check(&state));
}
#[test]
fn test_quote_auto_match_condition_in_unclosed_string() {
let condition = CursorAtEndOrBeforeClosingAndNotInQuote::new('"');
let mut state = EditorState::new();
state.buffer = r#""foo"#.to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
assert!(!condition.check(&state));
}
#[test]
fn test_quote_auto_match_condition_after_closed_string() {
let condition = CursorAtEndOrBeforeClosingAndNotInQuote::new('"');
let mut state = EditorState::new();
state.buffer = r#""foo" "#.to_string();
state.buffer_len = 6;
state.cursor_pos = 6;
assert!(condition.check(&state));
}
#[test]
fn test_quote_auto_match_closes_string() {
let rules = create_auto_match_rules();
let quote_rule = &rules[3];
let mut state = EditorState::new();
state.buffer = r#""foo"#.to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
assert!(!quote_rule.condition.check(&state));
match "e_rule.fallback_event {
ReedlineEvent::Edit(cmds) => {
assert_eq!(cmds.len(), 1);
assert!(matches!(&cmds[0], EditCommand::InsertChar('"')));
}
_ => panic!("Expected Edit event"),
}
}
#[test]
fn test_quote_auto_match_works_outside_string() {
let rules = create_auto_match_rules();
let quote_rule = &rules[3];
let mut state = EditorState::new();
state.buffer = "foo".to_string();
state.buffer_len = 3;
state.cursor_pos = 3;
assert!(quote_rule.condition.check(&state));
}
#[test]
fn test_single_quote_auto_match_in_string() {
let rules = create_auto_match_rules();
let quote_rule = &rules[4];
let mut state = EditorState::new();
state.buffer = "'foo".to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
assert!(!quote_rule.condition.check(&state));
}
#[test]
fn test_backtick_auto_match_in_string() {
let rules = create_auto_match_rules();
let quote_rule = &rules[5];
let mut state = EditorState::new();
state.buffer = "`foo".to_string();
state.buffer_len = 4;
state.cursor_pos = 4;
assert!(!quote_rule.condition.check(&state));
}
#[test]
fn test_backspace_after_newline_not_inside_empty_pair() {
let mut state = EditorState::new();
state.update_from_event(&ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]));
assert_eq!(state.buffer, "()");
assert_eq!(state.cursor_pos, 1);
assert!(state.is_inside_empty_pair());
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::InsertNewline]));
assert_eq!(state.buffer, "(\n)");
assert_eq!(state.cursor_pos, 2);
assert!(!state.is_inside_empty_pair());
assert_eq!(state.char_before_cursor(), Some('\n'));
assert_eq!(state.char_after_cursor(), Some(')'));
}
#[test]
fn test_bracket_delete_condition_after_newline() {
let rules = create_bracket_delete_rules();
let rule = &rules[0];
let mut state = EditorState::new();
state.buffer = "(\n)".to_string();
state.buffer_len = 3;
state.cursor_pos = 2;
assert!(!state.is_inside_empty_pair());
assert!(rule.condition.check(&state));
}
#[test]
fn test_backspace_after_newline_deletes_only_newline() {
let mut state = EditorState::new();
state.buffer = "(\n)".to_string();
state.buffer_len = 3;
state.cursor_pos = 2;
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::Backspace]));
assert_eq!(state.buffer, "()");
assert_eq!(state.cursor_pos, 1);
assert_eq!(state.buffer_len, 2);
}
#[test]
fn test_create_move_event_left() {
use super::ConditionalEditMode;
use reedline::Emacs;
let event = ConditionalEditMode::<Emacs>::create_move_event(5, 2, false);
match event {
ReedlineEvent::Edit(commands) => {
assert_eq!(commands.len(), 3);
for cmd in commands {
assert!(matches!(cmd, EditCommand::MoveLeft { select: false }));
}
}
_ => panic!("Expected Edit event with MoveLeft commands"),
}
}
#[test]
fn test_create_move_event_right() {
use super::ConditionalEditMode;
use reedline::Emacs;
let event = ConditionalEditMode::<Emacs>::create_move_event(2, 5, false);
match event {
ReedlineEvent::Edit(commands) => {
assert_eq!(commands.len(), 3);
for cmd in commands {
assert!(matches!(cmd, EditCommand::MoveRight { select: false }));
}
}
_ => panic!("Expected Edit event with MoveRight commands"),
}
}
#[test]
fn test_create_move_event_with_selection() {
use super::ConditionalEditMode;
use reedline::Emacs;
let event = ConditionalEditMode::<Emacs>::create_move_event(2, 5, true);
match event {
ReedlineEvent::Edit(commands) => {
assert_eq!(commands.len(), 3);
for cmd in commands {
assert!(matches!(cmd, EditCommand::MoveRight { select: true }));
}
}
_ => panic!("Expected Edit event with MoveRight commands"),
}
}
#[test]
fn test_create_move_event_same_position() {
use super::ConditionalEditMode;
use reedline::Emacs;
let event = ConditionalEditMode::<Emacs>::create_move_event(5, 5, false);
assert!(matches!(event, ReedlineEvent::None));
}
#[test]
fn test_tree_sitter_word_nav_pipe_operator() {
use super::ConditionalEditMode;
use reedline::Emacs;
let state_ref = new_editor_state_ref();
{
let mut state = state_ref.lock().unwrap();
state.buffer = "x |> filter()".to_string();
state.buffer_len = 13;
state.cursor_pos = 2;
state.uncertain = false;
}
let conditional = ConditionalEditMode::new(Emacs::default(), state_ref.clone())
.with_tree_sitter_word_nav(true);
let event = ReedlineEvent::Edit(vec![EditCommand::MoveWordRight { select: false }]);
let result = conditional.handle_tree_sitter_word_nav(&event);
assert!(result.is_some());
let result_event = result.unwrap();
match result_event {
ReedlineEvent::Edit(commands) => {
assert_eq!(commands.len(), 2); for cmd in commands {
assert!(matches!(cmd, EditCommand::MoveRight { select: false }));
}
}
_ => panic!("Expected Edit event"),
}
}
#[test]
fn test_tree_sitter_word_nav_assignment_left() {
use super::ConditionalEditMode;
use reedline::Emacs;
let state_ref = new_editor_state_ref();
{
let mut state = state_ref.lock().unwrap();
state.buffer = "x <- 42".to_string();
state.buffer_len = 7;
state.cursor_pos = 4;
state.uncertain = false;
}
let conditional = ConditionalEditMode::new(Emacs::default(), state_ref.clone())
.with_tree_sitter_word_nav(true);
let event = ReedlineEvent::Edit(vec![EditCommand::MoveWordLeft { select: false }]);
let result = conditional.handle_tree_sitter_word_nav(&event);
assert!(result.is_some());
let result_event = result.unwrap();
match result_event {
ReedlineEvent::Edit(commands) => {
assert_eq!(commands.len(), 2); for cmd in commands {
assert!(matches!(cmd, EditCommand::MoveLeft { select: false }));
}
}
_ => panic!("Expected Edit event"),
}
}
#[test]
fn test_tree_sitter_word_nav_disabled() {
use super::ConditionalEditMode;
use reedline::Emacs;
let state_ref = new_editor_state_ref();
{
let mut state = state_ref.lock().unwrap();
state.buffer = "x |> filter()".to_string();
state.buffer_len = 13;
state.cursor_pos = 2;
state.uncertain = false;
}
let conditional = ConditionalEditMode::new(Emacs::default(), state_ref.clone());
let event = ReedlineEvent::Edit(vec![EditCommand::MoveWordRight { select: false }]);
let result = conditional.handle_tree_sitter_word_nav(&event);
assert!(result.is_none());
}
#[test]
fn test_tree_sitter_word_nav_uncertain_state() {
use super::ConditionalEditMode;
use reedline::Emacs;
let state_ref = new_editor_state_ref();
{
let mut state = state_ref.lock().unwrap();
state.buffer = "x |> filter()".to_string();
state.buffer_len = 13;
state.cursor_pos = 2;
state.uncertain = true; }
let conditional = ConditionalEditMode::new(Emacs::default(), state_ref.clone())
.with_tree_sitter_word_nav(true);
let event = ReedlineEvent::Edit(vec![EditCommand::MoveWordRight { select: false }]);
let result = conditional.handle_tree_sitter_word_nav(&event);
assert!(result.is_none());
}
#[test]
fn test_tree_sitter_word_nav_until_found() {
use super::ConditionalEditMode;
use reedline::Emacs;
let state_ref = new_editor_state_ref();
{
let mut state = state_ref.lock().unwrap();
state.buffer = "x |> filter()".to_string();
state.buffer_len = 13;
state.cursor_pos = 2;
state.uncertain = false;
}
let conditional = ConditionalEditMode::new(Emacs::default(), state_ref.clone())
.with_tree_sitter_word_nav(true);
let event = ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintWordComplete,
ReedlineEvent::Edit(vec![EditCommand::MoveWordRight { select: false }]),
]);
let result = conditional.handle_tree_sitter_word_nav(&event);
assert!(result.is_some());
let result_event = result.unwrap();
match result_event {
ReedlineEvent::UntilFound(events) => {
assert_eq!(events.len(), 2);
assert!(matches!(events[0], ReedlineEvent::HistoryHintWordComplete));
match &events[1] {
ReedlineEvent::Edit(commands) => {
assert_eq!(commands.len(), 2);
}
_ => panic!("Expected Edit event"),
}
}
_ => panic!("Expected UntilFound event"),
}
}
#[test]
fn test_skip_over_rules_created() {
let rules = create_skip_over_rules();
assert_eq!(rules.len(), 6);
}
#[test]
fn test_skip_over_bracket_rules_match_insert_char() {
let rules = create_skip_over_rules();
for (i, close_char) in [')', ']', '}'].iter().enumerate() {
let rule = &rules[i];
let insert_char_event = ReedlineEvent::Edit(vec![EditCommand::InsertChar(*close_char)]);
assert!(
(rule.match_event)(&insert_char_event),
"Rule {} should match InsertChar('{}')",
i,
close_char
);
let pair = match close_char {
')' => "()",
']' => "[]",
'}' => "{}",
_ => unreachable!(),
};
let insert_string_event = ReedlineEvent::Edit(vec![
EditCommand::InsertString(pair.to_string()),
EditCommand::MoveLeft { select: false },
]);
assert!(
!(rule.match_event)(&insert_string_event),
"Rule {} should NOT match InsertString for pair '{}'",
i,
pair
);
}
}
#[test]
fn test_skip_over_quote_rules_match_insert_string() {
let rules = create_skip_over_rules();
let quote_pairs: [(char, &str); 3] = [('"', r#""""#), ('\'', "''"), ('`', "``")];
for (i, (quote_char, pair)) in quote_pairs.iter().enumerate() {
let rule = &rules[3 + i];
let insert_string_event = ReedlineEvent::Edit(vec![
EditCommand::InsertString(pair.to_string()),
EditCommand::MoveLeft { select: false },
]);
assert!(
(rule.match_event)(&insert_string_event),
"Quote rule {} should match InsertString for pair '{}'",
i,
pair
);
let insert_char_event = ReedlineEvent::Edit(vec![EditCommand::InsertChar(*quote_char)]);
assert!(
!(rule.match_event)(&insert_char_event),
"Quote rule {} should NOT match InsertChar('{}')",
i,
quote_char
);
}
}
#[test]
fn test_opening_bracket_not_matched_by_skip_over_regression() {
let skip_over_rules = create_skip_over_rules();
let auto_match_rules = create_auto_match_rules();
let open_paren_event = ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]);
let close_paren_skip_rule = &skip_over_rules[0]; assert!(
!(close_paren_skip_rule.match_event)(&open_paren_event),
"Skip-over rule for ')' should NOT match opening '(' event"
);
let open_paren_auto_match_rule = &auto_match_rules[0]; assert!(
(open_paren_auto_match_rule.match_event)(&open_paren_event),
"Auto-match rule for '(' should match opening '(' event"
);
}
#[test]
fn test_open_bracket_inside_parens_full_flow() {
let mut state = EditorState::new();
let skip_over_rules = create_skip_over_rules();
let auto_match_rules = create_auto_match_rules();
state.buffer = "foo(c)".to_string();
state.buffer_len = 6;
state.cursor_pos = 5;
assert_eq!(state.char_after_cursor(), Some(')'));
assert!(!state.cursor_at_end());
let open_paren_event = ReedlineEvent::Edit(vec![
EditCommand::InsertString("()".to_string()),
EditCommand::MoveLeft { select: false },
]);
let close_paren_skip_rule = &skip_over_rules[0];
assert!(!(close_paren_skip_rule.match_event)(&open_paren_event));
let open_paren_auto_match_rule = &auto_match_rules[0];
assert!((open_paren_auto_match_rule.match_event)(&open_paren_event));
assert!(
open_paren_auto_match_rule.condition.check(&state),
"Auto-match condition should pass when cursor is before ')'"
);
state.update_from_event(&open_paren_event);
assert_eq!(state.buffer, "foo(c())");
assert_eq!(state.cursor_pos, 6); }
#[test]
fn test_close_bracket_skip_over() {
let mut state = EditorState::new();
let skip_over_rules = create_skip_over_rules();
state.buffer = "foo()".to_string();
state.buffer_len = 5;
state.cursor_pos = 4;
assert_eq!(state.char_after_cursor(), Some(')'));
let close_paren_event = ReedlineEvent::Edit(vec![EditCommand::InsertChar(')')]);
let close_paren_skip_rule = &skip_over_rules[0];
assert!((close_paren_skip_rule.match_event)(&close_paren_event));
assert!(
!close_paren_skip_rule.condition.check(&state),
"Condition should fail when cursor is before ')'"
);
state.update_from_event(&ReedlineEvent::Edit(vec![EditCommand::MoveRight {
select: false,
}]));
assert_eq!(state.cursor_pos, 5); assert_eq!(state.buffer, "foo()"); }
}