#[derive(Debug, Clone)]
pub struct TextOperationResult {
pub new_text: String,
pub new_cursor_position: usize,
pub killed_text: Option<String>,
pub description: String,
}
#[derive(Debug, Clone)]
pub struct CursorMovementResult {
pub new_position: usize,
pub jumped_text: Option<String>,
}
#[must_use]
pub fn kill_line(text: &str, cursor_position: usize) -> TextOperationResult {
let text_len = text.len();
if cursor_position >= text_len {
return TextOperationResult {
new_text: text.to_string(),
new_cursor_position: cursor_position,
killed_text: None,
description: "Nothing to kill".to_string(),
};
}
let line_end = text[cursor_position..]
.find('\n')
.map_or(text_len, |pos| cursor_position + pos);
let killed = text[cursor_position..line_end].to_string();
let mut new_text = String::with_capacity(text_len);
new_text.push_str(&text[..cursor_position]);
if line_end < text_len && text.chars().nth(line_end) == Some('\n') {
new_text.push('\n');
new_text.push_str(&text[line_end + 1..]);
} else {
new_text.push_str(&text[line_end..]);
}
let killed_len = killed.len();
TextOperationResult {
new_text,
new_cursor_position: cursor_position,
killed_text: Some(killed),
description: format!("Killed {killed_len} characters"),
}
}
#[must_use]
pub fn kill_line_backward(text: &str, cursor_position: usize) -> TextOperationResult {
if cursor_position == 0 {
return TextOperationResult {
new_text: text.to_string(),
new_cursor_position: 0,
killed_text: None,
description: "Nothing to kill".to_string(),
};
}
let line_start = text[..cursor_position].rfind('\n').map_or(0, |pos| pos + 1);
let killed = text[line_start..cursor_position].to_string();
let mut new_text = String::with_capacity(text.len());
new_text.push_str(&text[..line_start]);
new_text.push_str(&text[cursor_position..]);
let killed_len = killed.len();
TextOperationResult {
new_text,
new_cursor_position: line_start,
killed_text: Some(killed),
description: format!("Killed {killed_len} characters backward"),
}
}
#[must_use]
pub fn delete_word_backward(text: &str, cursor_position: usize) -> TextOperationResult {
if cursor_position == 0 {
return TextOperationResult {
new_text: text.to_string(),
new_cursor_position: 0,
killed_text: None,
description: "At beginning of text".to_string(),
};
}
let mut pos = cursor_position;
while pos > 0 && text.chars().nth(pos - 1).is_some_and(char::is_whitespace) {
pos -= 1;
}
let word_start = if pos == 0 {
0
} else {
let mut start = pos;
while start > 0 && !text.chars().nth(start - 1).is_some_and(char::is_whitespace) {
start -= 1;
}
start
};
let killed = text[word_start..cursor_position].to_string();
let mut new_text = String::with_capacity(text.len());
new_text.push_str(&text[..word_start]);
new_text.push_str(&text[cursor_position..]);
let killed_trimmed = killed.trim().to_string();
TextOperationResult {
new_text,
new_cursor_position: word_start,
killed_text: Some(killed),
description: format!("Deleted word: '{killed_trimmed}'"),
}
}
#[must_use]
pub fn delete_word_forward(text: &str, cursor_position: usize) -> TextOperationResult {
let text_len = text.len();
if cursor_position >= text_len {
return TextOperationResult {
new_text: text.to_string(),
new_cursor_position: cursor_position,
killed_text: None,
description: "At end of text".to_string(),
};
}
let mut pos = cursor_position;
while pos < text_len && text.chars().nth(pos).is_some_and(char::is_whitespace) {
pos += 1;
}
let word_end = if pos >= text_len {
text_len
} else {
let mut end = pos;
while end < text_len && !text.chars().nth(end).is_some_and(char::is_whitespace) {
end += 1;
}
end
};
let killed = text[cursor_position..word_end].to_string();
let mut new_text = String::with_capacity(text.len());
new_text.push_str(&text[..cursor_position]);
new_text.push_str(&text[word_end..]);
let killed_trimmed = killed.trim().to_string();
TextOperationResult {
new_text,
new_cursor_position: cursor_position,
killed_text: Some(killed),
description: format!("Deleted word: '{killed_trimmed}'"),
}
}
#[must_use]
pub fn move_word_backward(text: &str, cursor_position: usize) -> CursorMovementResult {
if cursor_position == 0 {
return CursorMovementResult {
new_position: 0,
jumped_text: None,
};
}
let mut pos = cursor_position;
while pos > 0 && text.chars().nth(pos - 1).is_some_and(char::is_whitespace) {
pos -= 1;
}
let word_start = if pos == 0 {
0
} else {
let mut start = pos;
while start > 0 && !text.chars().nth(start - 1).is_some_and(char::is_whitespace) {
start -= 1;
}
start
};
let jumped = if word_start < cursor_position {
Some(text[word_start..cursor_position].to_string())
} else {
None
};
CursorMovementResult {
new_position: word_start,
jumped_text: jumped,
}
}
#[must_use]
pub fn move_word_forward(text: &str, cursor_position: usize) -> CursorMovementResult {
let text_len = text.len();
if cursor_position >= text_len {
return CursorMovementResult {
new_position: cursor_position,
jumped_text: None,
};
}
let mut pos = cursor_position;
while pos < text_len && !text.chars().nth(pos).is_some_and(char::is_whitespace) {
pos += 1;
}
while pos < text_len && text.chars().nth(pos).is_some_and(char::is_whitespace) {
pos += 1;
}
let jumped = if pos > cursor_position {
Some(text[cursor_position..pos].to_string())
} else {
None
};
CursorMovementResult {
new_position: pos,
jumped_text: jumped,
}
}
#[must_use]
pub fn jump_to_prev_token(text: &str, cursor_position: usize) -> CursorMovementResult {
if cursor_position == 0 {
return CursorMovementResult {
new_position: 0,
jumped_text: None,
};
}
let mut pos = cursor_position;
while pos > 0 {
let ch = text.chars().nth(pos - 1);
if let Some(c) = ch {
if c.is_whitespace() || "(),;=<>!+-*/".contains(c) {
pos -= 1;
} else {
break;
}
} else {
break;
}
}
let token_start = if pos == 0 {
0
} else {
let mut start = pos;
while start > 0 {
let ch = text.chars().nth(start - 1);
if let Some(c) = ch {
if !c.is_whitespace() && !"(),;=<>!+-*/".contains(c) {
start -= 1;
} else {
break;
}
} else {
break;
}
}
start
};
let jumped = if token_start < cursor_position {
Some(text[token_start..cursor_position].to_string())
} else {
None
};
CursorMovementResult {
new_position: token_start,
jumped_text: jumped,
}
}
#[must_use]
pub fn jump_to_next_token(text: &str, cursor_position: usize) -> CursorMovementResult {
let text_len = text.len();
if cursor_position >= text_len {
return CursorMovementResult {
new_position: cursor_position,
jumped_text: None,
};
}
let mut pos = cursor_position;
while pos < text_len {
let ch = text.chars().nth(pos);
if let Some(c) = ch {
if !c.is_whitespace() && !"(),;=<>!+-*/".contains(c) {
pos += 1;
} else {
break;
}
} else {
break;
}
}
while pos < text_len {
let ch = text.chars().nth(pos);
if let Some(c) = ch {
if c.is_whitespace() || "(),;=<>!+-*/".contains(c) {
pos += 1;
} else {
break;
}
} else {
break;
}
}
let jumped = if pos > cursor_position {
Some(text[cursor_position..pos].to_string())
} else {
None
};
CursorMovementResult {
new_position: pos,
jumped_text: jumped,
}
}
#[must_use]
pub fn clear_text() -> TextOperationResult {
TextOperationResult {
new_text: String::new(),
new_cursor_position: 0,
killed_text: None,
description: "Cleared all text".to_string(),
}
}
#[must_use]
pub fn insert_char(text: &str, cursor_position: usize, ch: char) -> TextOperationResult {
let mut new_text = String::with_capacity(text.len() + 1);
new_text.push_str(&text[..cursor_position.min(text.len())]);
new_text.push(ch);
if cursor_position < text.len() {
new_text.push_str(&text[cursor_position..]);
}
TextOperationResult {
new_text,
new_cursor_position: cursor_position + 1,
killed_text: None,
description: format!("Inserted '{ch}'"),
}
}
#[must_use]
pub fn delete_char(text: &str, cursor_position: usize) -> TextOperationResult {
if cursor_position >= text.len() {
return TextOperationResult {
new_text: text.to_string(),
new_cursor_position: cursor_position,
killed_text: None,
description: "Nothing to delete".to_string(),
};
}
let deleted = text.chars().nth(cursor_position).unwrap();
let mut new_text = String::with_capacity(text.len() - 1);
new_text.push_str(&text[..cursor_position]);
new_text.push_str(&text[cursor_position + 1..]);
TextOperationResult {
new_text,
new_cursor_position: cursor_position,
killed_text: Some(deleted.to_string()),
description: format!("Deleted '{deleted}'"),
}
}
#[must_use]
pub fn backspace(text: &str, cursor_position: usize) -> TextOperationResult {
if cursor_position == 0 {
return TextOperationResult {
new_text: text.to_string(),
new_cursor_position: 0,
killed_text: None,
description: "At beginning".to_string(),
};
}
let deleted = text.chars().nth(cursor_position - 1).unwrap();
let mut new_text = String::with_capacity(text.len() - 1);
new_text.push_str(&text[..cursor_position - 1]);
new_text.push_str(&text[cursor_position..]);
TextOperationResult {
new_text,
new_cursor_position: cursor_position - 1,
killed_text: Some(deleted.to_string()),
description: format!("Deleted '{deleted}'"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kill_line() {
let text = "SELECT * FROM table WHERE id = 1";
let result = kill_line(text, 7);
assert_eq!(result.new_text, "SELECT ");
assert_eq!(
result.killed_text,
Some("* FROM table WHERE id = 1".to_string())
);
assert_eq!(result.new_cursor_position, 7);
}
#[test]
fn test_kill_line_backward() {
let text = "SELECT * FROM table";
let result = kill_line_backward(text, 7);
assert_eq!(result.new_text, "* FROM table");
assert_eq!(result.killed_text, Some("SELECT ".to_string()));
assert_eq!(result.new_cursor_position, 0);
}
#[test]
fn test_delete_word_backward() {
let text = "SELECT * FROM table";
let result = delete_word_backward(text, 13); assert_eq!(result.new_text, "SELECT * table");
assert_eq!(result.killed_text, Some("FROM".to_string()));
assert_eq!(result.new_cursor_position, 9);
}
#[test]
fn test_move_word_forward() {
let text = "SELECT * FROM table";
let result = move_word_forward(text, 0);
assert_eq!(result.new_position, 7);
let result2 = move_word_forward(text, 7);
assert_eq!(result2.new_position, 9); }
#[test]
fn test_move_word_backward() {
let text = "SELECT * FROM table";
let result = move_word_backward(text, 13); assert_eq!(result.new_position, 9);
let result2 = move_word_backward(text, 9);
assert_eq!(result2.new_position, 7); }
#[test]
fn test_jump_to_next_token() {
let text = "SELECT id, name FROM users WHERE id = 1";
let result = jump_to_next_token(text, 0);
assert_eq!(result.new_position, 7);
let result2 = jump_to_next_token(text, 7);
assert_eq!(result2.new_position, 11); }
#[test]
fn test_insert_and_delete() {
let text = "SELECT";
let result = insert_char(text, 6, ' ');
assert_eq!(result.new_text, "SELECT ");
assert_eq!(result.new_cursor_position, 7);
let result2 = delete_char(&result.new_text, 6);
assert_eq!(result2.new_text, "SELECT");
let result3 = backspace(&result.new_text, 7);
assert_eq!(result3.new_text, "SELECT");
assert_eq!(result3.new_cursor_position, 6);
}
}
#[must_use]
pub fn extract_partial_word_at_cursor(query: &str, cursor_pos: usize) -> Option<String> {
if cursor_pos == 0 || cursor_pos > query.len() {
return None;
}
let chars: Vec<char> = query.chars().collect();
let mut start = cursor_pos;
let end = cursor_pos;
let mut in_quote = false;
while start > 0 {
let prev_char = chars[start - 1];
if prev_char == '"' {
start -= 1;
in_quote = true;
break;
} else if prev_char.is_alphanumeric() || prev_char == '_' || (prev_char == ' ' && in_quote)
{
start -= 1;
} else {
break;
}
}
if in_quote && start > 0 {
}
let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum();
let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum();
if start_byte < end_byte {
Some(query[start_byte..end_byte].to_string())
} else {
None
}
}
#[derive(Debug, Clone)]
pub struct CompletionResult {
pub new_text: String,
pub new_cursor_position: usize,
pub description: String,
}
#[must_use]
pub fn apply_completion_to_text(
query: &str,
cursor_pos: usize,
partial_word: &str,
suggestion: &str,
) -> CompletionResult {
let before_partial = &query[..cursor_pos - partial_word.len()];
let after_cursor = &query[cursor_pos..];
let suggestion_to_use = if partial_word.starts_with('"') && suggestion.starts_with('"') {
if suggestion.len() > 1 {
suggestion[1..].to_string()
} else {
suggestion.to_string()
}
} else {
suggestion.to_string()
};
let new_query = format!("{before_partial}{suggestion_to_use}{after_cursor}");
let new_cursor_pos = if suggestion_to_use.ends_with("('')") {
before_partial.len() + suggestion_to_use.len() - 2
} else if suggestion_to_use.ends_with("()") {
before_partial.len() + suggestion_to_use.len()
} else {
before_partial.len() + suggestion_to_use.len()
};
let description = if suggestion_to_use.ends_with("('')") {
format!("Completed '{suggestion}' with cursor positioned for parameter input")
} else if suggestion_to_use.ends_with("()") {
format!("Completed parameterless function '{suggestion}'")
} else {
format!("Completed '{suggestion}'")
};
CompletionResult {
new_text: new_query,
new_cursor_position: new_cursor_pos,
description,
}
}