use crate::buffer::{Edit, EditBatchSummary, Pos, Selection, TextBuffer};
impl TextBuffer {
pub fn insert(&mut self, pos: Pos, text: &str) -> Pos {
let at = self.pos_to_char(pos);
self.rope.insert(at, text);
let inserted_chars = text.chars().count();
self.char_to_pos(at + inserted_chars)
}
pub fn delete_range(&mut self, a: Pos, b: Pos) -> Pos {
let start = self.pos_to_char(crate::buffer::util::min_pos(self, a, b));
let end = self.pos_to_char(crate::buffer::util::max_pos(self, a, b));
if start < end {
self.rope.remove(start..end);
}
self.char_to_pos(start)
}
pub fn delete_selection(&mut self, sel: Selection) -> (Pos, bool) {
if sel.is_empty() {
return (self.clamp_pos(sel.cursor), false);
}
let (start, end) = sel.ordered();
let new_cursor = self.delete_range(start, end);
(new_cursor, true)
}
pub fn backspace(&mut self, sel: Selection) -> Selection {
if !sel.is_empty() {
let (cursor, _) = self.delete_selection(sel);
return Selection::empty(cursor);
}
let cursor = self.clamp_pos(sel.cursor);
let at = self.pos_to_char(cursor);
if at == 0 {
return Selection::empty(cursor);
}
let start = at - 1;
self.rope.remove(start..at);
let new_cursor = self.char_to_pos(start);
Selection::empty(new_cursor)
}
pub fn delete(&mut self, sel: Selection) -> Selection {
if !sel.is_empty() {
let (cursor, _) = self.delete_selection(sel);
return Selection::empty(cursor);
}
let cursor = self.clamp_pos(sel.cursor);
let at = self.pos_to_char(cursor);
let maxc = self.len_chars();
if at >= maxc {
return Selection::empty(cursor);
}
self.rope.remove(at..at + 1);
let new_cursor = self.char_to_pos(at);
Selection::empty(new_cursor)
}
pub fn insert_newline(&mut self, sel: Selection) -> Selection {
if !sel.is_empty() {
let (start, end) = sel.ordered();
let cursor = self.delete_range(start, end);
let new_cursor = self.insert(cursor, "\n");
return Selection::empty(new_cursor);
}
let cursor = self.clamp_pos(sel.cursor);
let new_cursor = self.insert(cursor, "\n");
Selection::empty(new_cursor)
}
pub fn replace_line_indent(&mut self, line: usize, indent: &str) -> Option<(usize, usize)> {
let line = self.clamp_line(line);
let text = self.line_string(line);
let existing_chars = text
.chars()
.take_while(|ch| *ch == ' ' || *ch == '\t')
.count();
let existing: String = text.chars().take(existing_chars).collect();
if existing == indent {
return None;
}
let _ = self.delete_range(Pos::new(line, 0), Pos::new(line, existing_chars));
let _ = self.insert(Pos::new(line, 0), indent);
Some((existing_chars, indent.chars().count()))
}
pub fn trim_trailing_whitespace(&mut self) -> bool {
let mut changed = false;
for line in (0..self.len_lines()).rev() {
let text = self.line_string(line);
let line_len = text.chars().count();
let trimmed_len = text.trim_end_matches([' ', '\t']).chars().count();
if trimmed_len == line_len {
continue;
}
let _ = self.delete_range(Pos::new(line, trimmed_len), Pos::new(line, line_len));
changed = true;
}
changed
}
pub fn apply_edit(&mut self, edit: Edit) -> Pos {
let maxc = self.len_chars();
let start = edit.range.start.min(maxc);
let end = edit.range.end.min(maxc);
let (start, end) = if start <= end {
(start, end)
} else {
(end, start)
};
if start < end {
self.rope.remove(start..end);
}
if !edit.insert.is_empty() {
self.rope.insert(start, &edit.insert);
let inserted_chars = edit.insert.chars().count();
self.char_to_pos(start + inserted_chars)
} else {
self.char_to_pos(start)
}
}
pub fn apply_edits(&mut self, edits: &[Edit]) -> EditBatchSummary {
let mut changed_start = usize::MAX;
let mut changed_end = 0usize;
let mut cursor = self.char_to_pos(self.len_chars());
for edit in edits {
let maxc = self.len_chars();
let start = edit.range.start.min(maxc);
let end = edit.range.end.min(maxc);
let (start, _) = if start <= end {
(start, end)
} else {
(end, start)
};
cursor = self.apply_edit(edit.clone());
let cursor_char = self.pos_to_char(cursor);
changed_start = changed_start.min(start);
changed_end = changed_end.max(cursor_char.max(start));
}
if edits.is_empty() {
let cursor = self.char_to_pos(self.len_chars());
let at = self.pos_to_char(cursor);
return EditBatchSummary {
changed_range: at..at,
cursor,
edits_applied: 0,
};
}
EditBatchSummary {
changed_range: changed_start..changed_end,
cursor,
edits_applied: edits.len(),
}
}
pub fn replace_selection(&mut self, sel: Selection, text: &str) -> Selection {
if !sel.is_empty() {
let (start, end) = sel.ordered();
let cursor = self.delete_range(start, end);
let cursor = self.insert(cursor, text);
Selection::empty(cursor)
} else {
let cursor = self.insert(sel.cursor, text);
Selection::empty(cursor)
}
}
pub fn paste_before(&mut self, cursor: Pos, text: &str, linewise: bool) -> Pos {
let insert_pos = if linewise {
let line = self.clamp_line(cursor.line);
self.clamp_pos(Pos::new(line, 0))
} else {
self.clamp_pos(cursor)
};
let end_pos = self.insert(insert_pos, text);
if linewise { insert_pos } else { end_pos }
}
pub fn paste_after(&mut self, cursor: Pos, text: &str, linewise: bool) -> Pos {
let insert_pos = if linewise {
let line = self.clamp_line(cursor.line);
let target_line = (line + 1).min(self.len_lines());
self.clamp_pos(Pos::new(target_line, 0))
} else {
let line = self.clamp_line(cursor.line);
let line_len = self.line_len_chars(line);
let col = if cursor.col < line_len {
cursor.col.saturating_add(1)
} else {
line_len
};
Pos::new(line, col)
};
let end_pos = self.insert(insert_pos, text);
if linewise { insert_pos } else { end_pos }
}
pub fn move_line_range_up_once(
&mut self,
start_line: usize,
end_line_inclusive: usize,
) -> Option<(usize, usize)> {
let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
if start == 0 {
return None;
}
let first = start - 1;
let last = end;
let mut entries = self.collect_line_entries(first, last);
entries.rotate_left(1);
let mut replacement = entries.join("\n");
if last + 1 < self.len_lines() {
replacement.push('\n');
}
let replace_start = self.line_to_char(first);
let replace_end = self.line_full_end_char(last);
self.rope.remove(replace_start..replace_end);
self.rope.insert(replace_start, &replacement);
Some((start - 1, end - 1))
}
pub fn move_line_range_up(
&mut self,
start_line: usize,
end_line_inclusive: usize,
count: usize,
) -> Option<(usize, usize)> {
if count == 0 {
return None;
}
let mut current = self.normalized_line_range(start_line, end_line_inclusive);
let mut moved = false;
for _ in 0..count {
let Some(next) = self.move_line_range_up_once(current.0, current.1) else {
break;
};
current = next;
moved = true;
}
if moved { Some(current) } else { None }
}
pub fn move_line_range_down_once(
&mut self,
start_line: usize,
end_line_inclusive: usize,
) -> Option<(usize, usize)> {
let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
if end + 1 >= self.len_lines() {
return None;
}
let first = start;
let last = end + 1;
let mut entries = self.collect_line_entries(first, last);
entries.rotate_right(1);
let mut replacement = entries.join("\n");
if last + 1 < self.len_lines() {
replacement.push('\n');
}
let replace_start = self.line_to_char(first);
let replace_end = self.line_full_end_char(last);
self.rope.remove(replace_start..replace_end);
self.rope.insert(replace_start, &replacement);
Some((start + 1, end + 1))
}
pub fn move_line_range_down(
&mut self,
start_line: usize,
end_line_inclusive: usize,
count: usize,
) -> Option<(usize, usize)> {
if count == 0 {
return None;
}
let mut current = self.normalized_line_range(start_line, end_line_inclusive);
let mut moved = false;
for _ in 0..count {
let Some(next) = self.move_line_range_down_once(current.0, current.1) else {
break;
};
current = next;
moved = true;
}
if moved { Some(current) } else { None }
}
pub fn indent_line_span(
&mut self,
start_line: usize,
end_line_inclusive: usize,
count: usize,
) -> Vec<(usize, usize)> {
if count == 0 {
return Vec::new();
}
let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
let indent = "\t".repeat(count);
let mut added_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
for line in start..=end {
let _ = self.insert(Pos::new(line, 0), &indent);
added_by_line.push((line, count));
}
added_by_line
}
pub fn outdent_line_span(
&mut self,
start_line: usize,
end_line_inclusive: usize,
count: usize,
) -> Vec<(usize, usize)> {
const TAB_STOP: usize = 4;
if count == 0 {
return Vec::new();
}
let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
let mut removed_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
for line in start..=end {
let text = self.line_string(line);
let chars: Vec<char> = text.chars().collect();
let mut idx = 0usize;
let mut levels_left = count;
while levels_left > 0 && idx < chars.len() {
if chars[idx] == '\t' {
idx += 1;
levels_left -= 1;
continue;
}
let mut spaces = 0usize;
while idx + spaces < chars.len() && chars[idx + spaces] == ' ' && spaces < TAB_STOP
{
spaces += 1;
}
if spaces == 0 {
break;
}
idx += spaces;
levels_left -= 1;
}
if idx > 0 {
let _ = self.delete_range(Pos::new(line, 0), Pos::new(line, idx));
}
removed_by_line.push((line, idx));
}
removed_by_line
}
fn normalized_line_range(
&self,
start_line: usize,
end_line_inclusive: usize,
) -> (usize, usize) {
let (start, end) = if start_line <= end_line_inclusive {
(start_line, end_line_inclusive)
} else {
(end_line_inclusive, start_line)
};
let start = self.clamp_line(start);
let end = self.clamp_line(end.max(start));
(start, end)
}
fn collect_line_entries(&self, start_line: usize, end_line_inclusive: usize) -> Vec<String> {
let mut entries = Vec::with_capacity(end_line_inclusive.saturating_sub(start_line) + 1);
for line in start_line..=end_line_inclusive {
entries.push(self.line_string(line));
}
entries
}
}