pub(super) fn move_left(cursor: usize, text: &str) -> usize {
if cursor > 0 {
let mut new_pos = cursor - 1;
while new_pos > 0 && !text.is_char_boundary(new_pos) {
new_pos -= 1;
}
new_pos
} else {
cursor
}
}
pub(super) fn move_right(cursor: usize, text: &str) -> usize {
if cursor < text.len() {
let mut new_pos = cursor + 1;
while new_pos < text.len() && !text.is_char_boundary(new_pos) {
new_pos += 1;
}
new_pos
} else {
cursor
}
}
pub(super) fn move_line_start(cursor: usize, text: &str) -> usize {
text[..cursor].rfind('\n').map(|i| i + 1).unwrap_or(0)
}
pub(super) fn move_first_non_blank(cursor: usize, text: &str) -> usize {
let line_start = move_line_start(cursor, text);
for (i, c) in text[line_start..].char_indices() {
if c == '\n' || !c.is_whitespace() {
return line_start + i;
}
}
line_start
}
fn line_end(cursor: usize, text: &str, past_end: bool) -> usize {
let line_end = text[cursor..]
.find('\n')
.map(|i| cursor + i)
.unwrap_or(text.len());
if past_end || line_end == 0 {
line_end
} else {
let mut last_char_start = line_end.saturating_sub(1);
while last_char_start > 0 && !text.is_char_boundary(last_char_start) {
last_char_start -= 1;
}
last_char_start
}
}
pub(super) fn move_line_end(cursor: usize, text: &str) -> usize {
line_end(cursor, text, false)
}
pub(super) fn move_line_end_insert(cursor: usize, text: &str) -> usize {
line_end(cursor, text, true)
}
pub(super) fn move_word_forward(cursor: usize, text: &str) -> usize {
let bytes = text.as_bytes();
let mut pos = cursor;
while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
pos += 1;
}
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
pos
}
pub(super) fn move_word_backward(cursor: usize, text: &str) -> usize {
let bytes = text.as_bytes();
let mut pos = cursor;
while pos > 0 && bytes[pos - 1].is_ascii_whitespace() {
pos -= 1;
}
while pos > 0 && !bytes[pos - 1].is_ascii_whitespace() {
pos -= 1;
}
pos
}
pub(super) fn move_word_end(cursor: usize, text: &str) -> usize {
let bytes = text.as_bytes();
let mut pos = cursor;
if pos < bytes.len() {
pos += 1;
}
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos > cursor + 1 {
pos -= 1;
}
pos
}
pub(super) fn move_up(cursor: usize, text: &str) -> usize {
let line_start = text[..cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
if line_start == 0 {
return cursor;
}
let col = cursor - line_start;
let prev_line_start = text[..line_start - 1]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
let prev_line_end = line_start - 1; let prev_line_len = prev_line_end - prev_line_start;
prev_line_start + col.min(prev_line_len)
}
pub(super) fn move_down(cursor: usize, text: &str) -> usize {
let line_start = text[..cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = cursor - line_start;
let Some(newline_pos) = text[cursor..].find('\n') else {
return cursor;
};
let next_line_start = cursor + newline_pos + 1;
if next_line_start >= text.len() {
return text.len();
}
let next_line_end = text[next_line_start..]
.find('\n')
.map(|i| next_line_start + i)
.unwrap_or(text.len());
let next_line_len = next_line_end - next_line_start;
next_line_start + col.min(next_line_len)
}
pub(super) fn move_to_matching_bracket(cursor: usize, text: &str) -> usize {
if cursor >= text.len() {
return cursor;
}
let Some(c) = text[cursor..].chars().next() else {
return cursor;
};
let pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
for (open, close) in pairs.iter() {
if c == *open {
if let Some(pos) = find_matching_forward(cursor, text, *open, *close) {
return pos;
}
return cursor;
}
if c == *close {
if let Some(pos) = find_matching_backward(cursor, text, *open, *close) {
return pos;
}
return cursor;
}
}
cursor
}
pub(super) fn find_matching_forward(
cursor: usize,
text: &str,
open: char,
close: char,
) -> Option<usize> {
let mut depth = 1;
let mut pos = cursor;
pos += open.len_utf8();
for (i, c) in text[pos..].char_indices() {
if c == open {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
return Some(pos + i);
}
}
}
None
}
pub(super) fn find_matching_backward(
cursor: usize,
text: &str,
open: char,
close: char,
) -> Option<usize> {
let mut depth = 1;
let search_text = &text[..cursor];
for (i, c) in search_text.char_indices().rev() {
if c == close {
depth += 1;
} else if c == open {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
None
}