use super::{Buffer, Cursor, Editor, char_to_byte};
impl Editor {
pub fn delete_line(&mut self, buf: &mut Buffer) {
if buf.lines.len() == 1 {
buf.yank = buf.lines[0].clone();
buf.lines[0].clear();
} else {
buf.yank = buf.lines.remove(self.cursor.row);
if self.cursor.row >= buf.lines.len() {
self.cursor.row = buf.lines.len() - 1;
}
}
self.clamp_col(buf, false);
buf.touch();
}
pub fn yank_line(&mut self, buf: &mut Buffer) {
buf.yank = buf.lines[self.cursor.row].clone();
}
pub fn paste_after(&mut self, buf: &mut Buffer) {
if buf.yank.is_empty() {
return;
}
let yanked = buf.yank.clone();
buf.lines.insert(self.cursor.row + 1, yanked);
self.cursor.row += 1;
self.cursor.col = 0;
buf.touch();
}
pub fn delete_range(&mut self, buf: &mut Buffer, from: Cursor, to: Cursor) {
let (from, to) = order(from, to);
if from == to {
return;
}
let lines = &mut buf.lines;
if from.row == to.row {
let line = &mut lines[from.row];
let fb = char_to_byte(line, from.col);
let tb = char_to_byte(line, to.col);
line.replace_range(fb..tb, "");
} else {
let from_byte = char_to_byte(&lines[from.row], from.col);
let to_byte = char_to_byte(&lines[to.row], to.col);
let head: String = lines[from.row][..from_byte].to_string();
let tail: String = lines[to.row][to_byte..].to_string();
lines[from.row] = head + &tail;
let drain_end = (to.row + 1).min(lines.len());
lines.drain((from.row + 1)..drain_end);
}
self.cursor = from;
self.clamp_col(buf, false);
buf.touch();
}
}
impl Buffer {
pub fn range_text(&self, from: Cursor, to: Cursor) -> String {
let (from, to) = order(from, to);
if from == to {
return String::new();
}
if from.row == to.row {
let line = &self.lines[from.row];
let fb = char_to_byte(line, from.col);
let tb = char_to_byte(line, to.col);
line[fb..tb].to_string()
} else {
let mut text = String::new();
let from_byte = char_to_byte(&self.lines[from.row], from.col);
text.push_str(&self.lines[from.row][from_byte..]);
text.push('\n');
for i in (from.row + 1)..to.row {
text.push_str(&self.lines[i]);
text.push('\n');
}
let to_byte = char_to_byte(&self.lines[to.row], to.col);
text.push_str(&self.lines[to.row][..to_byte]);
text
}
}
pub fn yank_range(&mut self, from: Cursor, to: Cursor) {
self.yank = self.range_text(from, to);
}
pub fn lines_text(&self, from_row: usize, to_row: usize) -> String {
let (a, b) = (from_row.min(to_row), from_row.max(to_row));
let b = b.min(self.lines.len().saturating_sub(1));
self.lines[a..=b].join("\n")
}
pub fn yank_lines(&mut self, from_row: usize, to_row: usize) {
self.yank = self.lines_text(from_row, to_row);
}
pub fn block_text(&self, r0: usize, c0: usize, r1: usize, c1: usize) -> String {
let (r0, r1) = (r0.min(r1), r0.max(r1));
let (c0, c1) = (c0.min(c1), c0.max(c1));
let r1 = r1.min(self.lines.len().saturating_sub(1));
let mut text = String::new();
for r in r0..=r1 {
if r > r0 {
text.push('\n');
}
let line = &self.lines[r];
let chars: Vec<char> = line.chars().collect();
let lo = c0.min(chars.len());
let hi = (c1 + 1).min(chars.len());
if lo < hi {
text.extend(&chars[lo..hi]);
}
}
text
}
pub fn yank_block(&mut self, r0: usize, c0: usize, r1: usize, c1: usize) {
self.yank = self.block_text(r0, c0, r1, c1);
}
}
impl Editor {
pub fn delete_lines(&mut self, buf: &mut Buffer, from_row: usize, to_row: usize) {
let (a, b) = (from_row.min(to_row), from_row.max(to_row));
let b = b.min(buf.lines.len().saturating_sub(1));
buf.yank = buf.lines[a..=b].join("\n");
if a == 0 && b + 1 >= buf.lines.len() {
buf.lines.clear();
buf.lines.push(String::new());
self.cursor.row = 0;
} else {
buf.lines.drain(a..=b);
self.cursor.row = a.min(buf.lines.len().saturating_sub(1));
}
self.cursor.col = 0;
self.clamp_col(buf, false);
buf.touch();
}
pub fn delete_block(&mut self, buf: &mut Buffer, r0: usize, c0: usize, r1: usize, c1: usize) {
let (r0, r1) = (r0.min(r1), r0.max(r1));
let (c0, c1) = (c0.min(c1), c0.max(c1));
let r1 = r1.min(buf.lines.len().saturating_sub(1));
buf.yank_block(r0, c0, r1, c1);
for r in r0..=r1 {
let line = buf.lines[r].clone();
let nchars = line.chars().count();
let lo = c0.min(nchars);
let hi = (c1 + 1).min(nchars);
if lo >= hi {
continue;
}
let lo_b = char_to_byte(&line, lo);
let hi_b = char_to_byte(&line, hi);
buf.lines[r].replace_range(lo_b..hi_b, "");
}
self.cursor.row = r0;
self.cursor.col = c0;
self.clamp_col(buf, false);
buf.touch();
}
}
impl Buffer {
pub fn transform_case_range(&mut self, from: Cursor, to: Cursor, f: fn(char) -> char) {
let (from, to) = order(from, to);
if from == to {
return;
}
for row in from.row..=to.row {
let chars: Vec<char> = self.lines[row].chars().collect();
let lo = if row == from.row { from.col } else { 0 };
let hi = if row == to.row {
to.col.min(chars.len())
} else {
chars.len()
};
if lo >= hi {
continue;
}
self.lines[row] = chars
.iter()
.enumerate()
.map(|(i, c)| if i >= lo && i < hi { f(*c) } else { *c })
.collect();
}
self.touch();
}
pub fn transform_case_lines(&mut self, from_row: usize, to_row: usize, f: fn(char) -> char) {
let (a, b) = (from_row.min(to_row), from_row.max(to_row));
let b = b.min(self.lines.len().saturating_sub(1));
for row in a..=b {
self.lines[row] = self.lines[row].chars().map(f).collect();
}
self.touch();
}
pub fn transform_case_block(
&mut self,
r0: usize,
c0: usize,
r1: usize,
c1: usize,
f: fn(char) -> char,
) {
let (r0, r1) = (r0.min(r1), r0.max(r1));
let (c0, c1) = (c0.min(c1), c0.max(c1));
let r1 = r1.min(self.lines.len().saturating_sub(1));
for row in r0..=r1 {
let chars: Vec<char> = self.lines[row].chars().collect();
self.lines[row] = chars
.iter()
.enumerate()
.map(|(i, c)| if i >= c0 && i <= c1 { f(*c) } else { *c })
.collect();
}
self.touch();
}
}
pub fn to_lower_keep_width(c: char) -> char {
if c.is_uppercase() {
let mut it = c.to_lowercase();
let first = it.next().unwrap_or(c);
if it.next().is_some() { c } else { first }
} else {
c
}
}
pub fn to_upper_keep_width(c: char) -> char {
if c.is_lowercase() {
let mut it = c.to_uppercase();
let first = it.next().unwrap_or(c);
if it.next().is_some() { c } else { first }
} else {
c
}
}
pub fn flip_case_char_keep_width(c: char) -> char {
if c.is_uppercase() {
let mut it = c.to_lowercase();
let first = it.next().unwrap_or(c);
if it.next().is_some() { c } else { first }
} else if c.is_lowercase() {
let mut it = c.to_uppercase();
let first = it.next().unwrap_or(c);
if it.next().is_some() { c } else { first }
} else {
c
}
}
fn order(a: Cursor, b: Cursor) -> (Cursor, Cursor) {
if (a.row, a.col) <= (b.row, b.col) {
(a, b)
} else {
(b, a)
}
}
impl Editor {
pub fn join_next_line(&mut self, buf: &mut Buffer) {
if self.cursor.row + 1 >= buf.lines.len() {
return;
}
let next = buf.lines.remove(self.cursor.row + 1);
let next_trimmed = next.trim_start();
let cur = &mut buf.lines[self.cursor.row];
let needs_space = !cur.is_empty()
&& !cur
.chars()
.last()
.map(|c| c.is_whitespace())
.unwrap_or(false)
&& !next_trimmed.is_empty();
let join_col = cur.chars().count();
if needs_space {
cur.push(' ');
}
cur.push_str(next_trimmed);
self.cursor.col = join_col;
buf.touch();
}
pub fn toggle_case_under_cursor(&mut self, buf: &mut Buffer) {
let line = &mut buf.lines[self.cursor.row];
if self.cursor.col >= line.chars().count() {
return;
}
let byte_idx = char_to_byte(line, self.cursor.col);
let ch = line[byte_idx..].chars().next().unwrap();
let replacement: String = if ch.is_uppercase() {
ch.to_lowercase().collect()
} else if ch.is_lowercase() {
ch.to_uppercase().collect()
} else {
return; };
line.replace_range(byte_idx..byte_idx + ch.len_utf8(), &replacement);
buf.touch();
let max = self.current_line_len(buf).saturating_sub(1);
if self.cursor.col < max {
self.cursor.col += 1;
}
}
pub fn delete_to_eol(&mut self, buf: &mut Buffer) {
let line = buf.lines[self.cursor.row].clone();
let byte_idx = char_to_byte(&line, self.cursor.col);
buf.yank = line[byte_idx..].to_string();
buf.lines[self.cursor.row].truncate(byte_idx);
buf.touch();
self.clamp_col(buf, false);
}
pub fn clear_current_line(&mut self, buf: &mut Buffer) {
buf.yank = buf.lines[self.cursor.row].clone();
buf.lines[self.cursor.row].clear();
self.cursor.col = 0;
buf.touch();
}
pub fn toggle_block_comment(&mut self, buf: &mut Buffer, token: &str, rows: &[usize]) {
let mut rows: Vec<usize> = rows
.iter()
.copied()
.filter(|&r| r < buf.lines.len())
.collect();
rows.sort_unstable();
rows.dedup();
let non_blank: Vec<(usize, usize)> = rows
.iter()
.filter_map(|&row| {
let line = &buf.lines[row];
let indent_chars = line.chars().take_while(|c| c.is_whitespace()).count();
if indent_chars == line.chars().count() {
None
} else {
Some((row, indent_chars))
}
})
.collect();
if non_blank.is_empty() {
return;
}
let anchor = non_blank.iter().map(|&(_, i)| i).min().unwrap();
let token_bytes = token.len();
let token_chars = token.chars().count();
let all_commented = non_blank.iter().all(|&(row, _)| {
let line = &buf.lines[row];
let byte = char_to_byte(line, anchor);
line[byte..].starts_with(token)
});
let mut deltas: Vec<(usize, i32)> = Vec::with_capacity(non_blank.len());
if all_commented {
for &(row, _) in &non_blank {
let line = &mut buf.lines[row];
let byte = char_to_byte(line, anchor);
let after_token = &line[byte + token_bytes..];
let trim_bytes = if after_token.starts_with(' ') {
token_bytes + 1
} else {
token_bytes
};
let removed_chars = token_chars + (trim_bytes - token_bytes);
line.replace_range(byte..byte + trim_bytes, "");
deltas.push((row, -(removed_chars as i32)));
}
} else {
let insert = format!("{} ", token);
let added_chars = insert.chars().count();
for &(row, _) in &non_blank {
let line = &mut buf.lines[row];
let byte = char_to_byte(line, anchor);
line.insert_str(byte, &insert);
deltas.push((row, added_chars as i32));
}
}
self.for_each_cursor(|c| shift_cursor_for_block_comment(c, &deltas, anchor));
self.clamp_col(buf, false);
buf.touch();
}
pub fn for_each_cursor(&mut self, mut f: impl FnMut(&mut Cursor)) {
f(&mut self.cursor);
for c in self.extra_cursors.iter_mut() {
f(c);
}
}
pub fn toggle_block_wrap(
&mut self,
buf: &mut Buffer,
open: &str,
close: &str,
from: Cursor,
to: Cursor,
) {
let (lo, hi) = order(from, to);
if lo == hi {
return;
}
let open_chars = open.chars().count();
let close_chars = close.chars().count();
let own_close_row = if hi.col == 0 && hi.row > 0 {
hi.row - 1
} else {
hi.row
};
let own_line_wrapped = lo.col == 0
&& own_close_row > lo.row
&& buf.lines[lo.row] == open
&& buf.lines[own_close_row] == close;
let starts_open = !own_line_wrapped && {
let line = &buf.lines[lo.row];
let byte = char_to_byte(line, lo.col);
line[byte..].starts_with(open)
};
let ends_close = !own_line_wrapped && hi.col >= close_chars && {
let line = &buf.lines[hi.row];
let a = char_to_byte(line, hi.col - close_chars);
let b = char_to_byte(line, hi.col);
&line[a..b] == close
};
if own_line_wrapped {
buf.lines.remove(own_close_row);
self.for_each_cursor(|c| shift_cursor_for_row_delta(c, own_close_row, -1));
buf.lines.remove(lo.row);
self.for_each_cursor(|c| shift_cursor_for_row_delta(c, lo.row, -1));
} else if starts_open && ends_close {
let close_start = hi.col - close_chars;
let a = char_to_byte(&buf.lines[hi.row], close_start);
let b = char_to_byte(&buf.lines[hi.row], hi.col);
buf.lines[hi.row].replace_range(a..b, "");
self.for_each_cursor(|c| shift_cursor_for_edit(c, hi.row, close_start, close_chars, 0));
let a = char_to_byte(&buf.lines[lo.row], lo.col);
let b = char_to_byte(&buf.lines[lo.row], lo.col + open_chars);
buf.lines[lo.row].replace_range(a..b, "");
self.for_each_cursor(|c| shift_cursor_for_edit(c, lo.row, lo.col, open_chars, 0));
} else if should_use_own_line(&buf.lines, lo, hi) {
let close_insert_row = own_close_row + 1;
buf.lines.insert(close_insert_row, close.to_string());
self.for_each_cursor(|c| shift_cursor_for_row_delta(c, close_insert_row, 1));
buf.lines.insert(lo.row, open.to_string());
self.for_each_cursor(|c| shift_cursor_for_row_delta(c, lo.row, 1));
} else {
let hi_byte = char_to_byte(&buf.lines[hi.row], hi.col);
buf.lines[hi.row].insert_str(hi_byte, close);
self.for_each_cursor(|c| shift_cursor_for_edit(c, hi.row, hi.col, 0, close_chars));
let lo_byte = char_to_byte(&buf.lines[lo.row], lo.col);
buf.lines[lo.row].insert_str(lo_byte, open);
self.for_each_cursor(|c| shift_cursor_for_edit(c, lo.row, lo.col, 0, open_chars));
}
self.clamp_col(buf, false);
buf.touch();
}
}
fn should_use_own_line(lines: &[String], lo: Cursor, hi: Cursor) -> bool {
if lo.col != 0 || lo.row == hi.row {
return false;
}
hi.col == 0 || hi.col == lines[hi.row].chars().count()
}
fn shift_cursor_for_row_delta(c: &mut Cursor, pivot: usize, delta: i32) {
if delta > 0 {
if c.row >= pivot {
c.row += delta as usize;
}
} else if delta < 0 {
let d = (-delta) as usize;
if c.row > pivot {
c.row = c.row.saturating_sub(d);
}
}
}
pub(super) fn shift_cursor_for_edit(
c: &mut Cursor,
row: usize,
col_start: usize,
delete: usize,
insert: usize,
) {
if c.row != row {
return;
}
if c.col >= col_start + delete {
c.col = c.col + insert - delete;
} else if c.col > col_start {
c.col = col_start + insert;
}
}
fn shift_cursor_for_block_comment(c: &mut Cursor, deltas: &[(usize, i32)], anchor: usize) {
let Some(&(_, delta)) = deltas.iter().find(|(r, _)| *r == c.row) else {
return;
};
if delta > 0 {
if c.col >= anchor {
c.col = c.col.saturating_add(delta as usize);
}
} else if delta < 0 {
let d = (-delta) as usize;
if c.col >= anchor + d {
c.col -= d;
} else if c.col > anchor {
c.col = anchor;
}
}
}