use crate::document::{next_grapheme_offset, EditDelta};
use crate::editor::edit_ops::{apply_byte_delta, cursor_byte};
use crate::editor::list_edit;
use crate::editor::vim_ops::motion::{first_non_blank, line_end_offset};
use crate::editor::{EditorState, Mode};
pub fn paste(editor: &mut EditorState, text: &str, linewise: bool, count: u32, after: bool) {
if text.is_empty() {
return;
}
let repeated = text.repeat(count.max(1) as usize);
if linewise {
paste_linewise(editor, &repeated, after);
} else {
paste_charwise(editor, &repeated, after);
}
}
fn paste_charwise(editor: &mut EditorState, text: &str, after: bool) {
let cursor = editor.cursor.offset;
let line = editor.buffer.char_to_line(cursor);
let line_content_end = line_end_offset(&editor.buffer, line);
let insert_at = if after && cursor < line_content_end {
next_grapheme_offset(&editor.buffer, cursor)
} else {
cursor
};
let inserted_chars = text.chars().count();
editor.apply_delta(EditDelta {
offset: insert_at,
removed: String::new(),
inserted: text.to_owned(),
});
editor.cursor.offset = (insert_at + inserted_chars)
.saturating_sub(1)
.max(insert_at)
.min(editor.buffer.len_chars());
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
fn paste_linewise(editor: &mut EditorState, text: &str, after: bool) {
let cursor = editor.cursor.offset;
let line = editor.buffer.char_to_line(cursor);
let line_count = editor.buffer.line_count();
let len = editor.buffer.len_chars();
let (insert_at, payload, first_line_offset) = if after {
if line + 1 < line_count {
let at = editor.buffer.line_to_char(line + 1);
(at, text.to_owned(), at)
} else {
let body = text.strip_suffix('\n').unwrap_or(text);
(len, format!("\n{body}"), len + 1)
}
} else {
let at = editor.buffer.line_to_char(line);
(at, text.to_owned(), at)
};
editor.apply_delta(EditDelta {
offset: insert_at,
removed: String::new(),
inserted: payload,
});
let landing = first_line_offset.min(editor.buffer.len_chars());
let landing_line = editor.buffer.char_to_line(landing);
editor.cursor.offset = first_non_blank(&editor.buffer, landing_line);
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn replace_char(editor: &mut EditorState, c: char, count: u32) {
let count = count.max(1) as usize;
let cursor = editor.cursor.offset;
let line = editor.buffer.char_to_line(cursor);
let line_end = line_end_offset(&editor.buffer, line);
if cursor + count > line_end {
return;
}
let removed = editor.buffer.slice_to_string(cursor, cursor + count);
let inserted: String = std::iter::repeat_n(c, count).collect();
editor.apply_delta(EditDelta {
offset: cursor,
removed,
inserted,
});
editor.cursor.offset = (cursor + count - 1).min(editor.buffer.len_chars());
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn toggle_case(editor: &mut EditorState, count: u32) {
let count = count.max(1) as usize;
let cursor = editor.cursor.offset;
let line = editor.buffer.char_to_line(cursor);
let line_end = line_end_offset(&editor.buffer, line);
let n = count.min(line_end.saturating_sub(cursor));
if n == 0 {
return;
}
let removed = editor.buffer.slice_to_string(cursor, cursor + n);
let inserted: String = removed.chars().map(toggle_case_char).collect();
editor.apply_delta(EditDelta {
offset: cursor,
removed,
inserted,
});
editor.cursor.offset = (cursor + n).min(line_end);
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn toggle_case_range(editor: &mut EditorState, start: usize, end: usize) {
let len = editor.buffer.len_chars();
let start = start.min(len);
let end = end.min(len);
if start >= end {
return;
}
let removed = editor.buffer.slice_to_string(start, end);
let inserted: String = removed.chars().map(toggle_case_char).collect();
if inserted == removed {
return;
}
editor.apply_delta(EditDelta {
offset: start,
removed,
inserted,
});
editor.cursor.offset = start;
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn set_case_range(editor: &mut EditorState, start: usize, end: usize, upper: bool) {
let len = editor.buffer.len_chars();
let start = start.min(len);
let end = end.min(len);
if start >= end {
return;
}
let removed = editor.buffer.slice_to_string(start, end);
let inserted: String = removed.chars().map(|c| force_case_char(c, upper)).collect();
if inserted == removed {
return;
}
editor.apply_delta(EditDelta {
offset: start,
removed,
inserted,
});
editor.cursor.offset = start;
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn replace_char_range(editor: &mut EditorState, start: usize, end: usize, c: char) {
let len = editor.buffer.len_chars();
let start = start.min(len);
let end = end.min(len);
if start >= end {
return;
}
let removed = editor.buffer.slice_to_string(start, end);
let inserted: String = removed
.chars()
.map(|ch| if ch == '\n' { '\n' } else { c })
.collect();
editor.apply_delta(EditDelta {
offset: start,
removed,
inserted,
});
editor.cursor.offset = start;
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn replace_range_with(editor: &mut EditorState, start: usize, end: usize, text: &str) {
let len = editor.buffer.len_chars();
let start = start.min(len);
let end = end.min(len);
if start >= end && text.is_empty() {
return;
}
let removed = editor.buffer.slice_to_string(start, end);
let inserted_chars = text.chars().count();
editor.apply_delta(EditDelta {
offset: start,
removed,
inserted: text.to_owned(),
});
editor.cursor.offset = (start + inserted_chars)
.saturating_sub(1)
.max(start)
.min(editor.buffer.len_chars());
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
fn toggle_case_char(c: char) -> char {
if c.is_uppercase() {
c.to_lowercase().next().unwrap_or(c)
} else if c.is_lowercase() {
c.to_uppercase().next().unwrap_or(c)
} else {
c
}
}
fn force_case_char(c: char, upper: bool) -> char {
if upper {
c.to_uppercase().next().unwrap_or(c)
} else {
c.to_lowercase().next().unwrap_or(c)
}
}
pub fn join_lines(editor: &mut EditorState, count: u32) {
let joins = count.max(2) as usize - 1; let buf = &editor.buffer;
let start_line = buf.char_to_line(editor.cursor.offset);
let line_count = buf.line_count();
if start_line + 1 >= line_count {
return; }
let region_start = line_end_offset(buf, start_line);
let start_line_start = buf.line_to_char(start_line);
let mut prev_is_ws = region_start == start_line_start
|| (region_start > 0 && buf.rope().char(region_start - 1).is_whitespace());
let mut replacement = String::new();
let mut first_join_offset = None;
let mut last_consumed = start_line;
for step in 1..=joins {
let li = start_line + step;
if li >= line_count {
break;
}
last_consumed = li;
let li_start = buf.line_to_char(li);
let li_end = line_end_offset(buf, li);
let mut content_start = li_start;
while content_start < li_end && matches!(buf.rope().char(content_start), ' ' | '\t') {
content_start += 1;
}
let content = buf.slice_to_string(content_start, li_end);
let is_empty = content_start >= li_end;
let sep = if prev_is_ws || is_empty { "" } else { " " };
if first_join_offset.is_none() {
first_join_offset = Some(region_start + replacement.chars().count());
}
replacement.push_str(sep);
replacement.push_str(&content);
prev_is_ws = content
.chars()
.next_back()
.is_none_or(|c| c.is_whitespace());
}
let region_end = line_end_offset(buf, last_consumed);
let removed = buf.slice_to_string(region_start, region_end);
editor.apply_delta(EditDelta {
offset: region_start,
removed,
inserted: replacement,
});
editor.cursor.offset = first_join_offset
.unwrap_or(region_start)
.min(editor.buffer.len_chars());
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn indent_lines(
editor: &mut EditorState,
first: usize,
last: usize,
right: bool,
indent_width: usize,
) {
let line_count = editor.buffer.line_count();
if line_count == 0 {
return;
}
let last = last.min(line_count - 1);
let first = first.min(last);
let start = editor.buffer.line_to_char(first);
let end = if last + 1 < line_count {
editor.buffer.line_to_char(last + 1)
} else {
editor.buffer.len_chars()
};
let region = editor.buffer.slice_to_string(start, end);
let indent = " ".repeat(indent_width);
let mut out = String::with_capacity(region.len() + indent_width);
for line in region.split_inclusive('\n') {
let (content, nl) = match line.strip_suffix('\n') {
Some(c) => (c, "\n"),
None => (line, ""),
};
if right {
if content.chars().any(|c| !c.is_whitespace()) {
out.push_str(&indent);
}
out.push_str(content);
} else {
out.push_str(strip_indent(content, indent_width));
}
out.push_str(nl);
}
if out == region {
return;
}
editor.apply_delta(EditDelta {
offset: start,
removed: region,
inserted: out,
});
editor.cursor.offset = first_non_blank(&editor.buffer, first);
editor.cursor.preferred_col = editor.cursor.cell_col(&editor.buffer);
editor.update_cursor_block();
}
pub fn open_list_continue(editor: &mut EditorState, below: bool) -> bool {
if editor.mode == Mode::Raw {
return false;
}
let source = editor.buffer.contents();
let byte = cursor_byte(editor);
let Some(info) = list_edit::find_list_at(&source, byte) else {
return false;
};
let Some(item_idx) = list_edit::cursor_item_idx(&info, byte) else {
return false;
};
let at = if below {
info.items[item_idx].line_end
} else if item_idx > 0 {
info.items[item_idx - 1].line_end
} else {
return false;
};
let Some(res) = list_edit::continue_item(&info, &source, at) else {
return false;
};
apply_byte_delta(editor, res.delta, res.cursor_byte);
true
}
pub fn renumber_list_at_cursor(editor: &mut EditorState) {
crate::editor::edit_ops::list_renumber_at_cursor(editor);
}
pub fn indent_list_item(editor: &mut EditorState, right: bool) -> bool {
if editor.mode == Mode::Raw {
return false;
}
let source = editor.buffer.contents();
let byte = cursor_byte(editor);
let Some(info) = list_edit::find_list_at(&source, byte) else {
return false;
};
let res = if right {
list_edit::indent_item(&info, &source, byte, crate::constants::INDENT_WIDTH)
} else {
list_edit::outdent_item(&info, &source, byte, crate::constants::INDENT_WIDTH)
};
let Some(res) = res else {
return false;
};
apply_byte_delta(editor, res.delta, res.cursor_byte);
renumber_list_at_cursor(editor);
true
}
fn strip_indent(content: &str, indent_width: usize) -> &str {
let mut skip = 0;
for (i, c) in content.char_indices() {
if i >= indent_width {
break;
}
match c {
' ' => skip = i + 1,
'\t' => {
skip = i + 1;
break;
}
_ => break,
}
}
&content[skip..]
}