use super::{Count, RegisterKind, RegisterText, motion};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PastePlacement {
After,
Before,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PastePlan {
byte_index: usize,
text: String,
cursor_byte_index: usize,
}
impl PastePlan {
#[must_use]
const fn new(byte_index: usize, text: String, cursor_byte_index: usize) -> Self {
Self {
byte_index,
text,
cursor_byte_index,
}
}
#[must_use]
pub const fn byte_index(&self) -> usize {
self.byte_index
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn into_text(self) -> String {
self.text
}
#[must_use]
pub const fn cursor_byte_index(&self) -> usize {
self.cursor_byte_index
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PastePlanError {
EmptyRegister,
PayloadTooLarge {
requested_byte_len: usize,
max_byte_len: usize,
},
}
pub fn plan_paste(
text: &str,
cursor_byte_index: usize,
register: &RegisterText,
count: Count,
placement: PastePlacement,
max_byte_len: usize,
) -> Result<PastePlan, PastePlanError> {
if register.is_empty() {
return Err(PastePlanError::EmptyRegister);
}
match register.kind() {
RegisterKind::Characterwise => plan_characterwise_paste(
text,
cursor_byte_index,
register,
count,
placement,
max_byte_len,
),
RegisterKind::Linewise => plan_linewise_paste(
text,
cursor_byte_index,
register,
count,
placement,
max_byte_len,
),
}
}
fn plan_characterwise_paste(
text: &str,
cursor_byte_index: usize,
register: &RegisterText,
count: Count,
placement: PastePlacement,
max_byte_len: usize,
) -> Result<PastePlan, PastePlanError> {
let insert_at = match placement {
PastePlacement::After => next_char_boundary_for_paste(text, cursor_byte_index),
PastePlacement::Before => motion::clamp_to_boundary(text, cursor_byte_index),
};
let inserted = counted_text(register.as_str(), count, max_byte_len)?;
let cursor = inserted.chars().last().map_or(insert_at, |character| {
insert_at + inserted.len().saturating_sub(character.len_utf8())
});
Ok(PastePlan::new(insert_at, inserted, cursor))
}
fn plan_linewise_paste(
text: &str,
cursor_byte_index: usize,
register: &RegisterText,
count: Count,
placement: PastePlacement,
max_byte_len: usize,
) -> Result<PastePlan, PastePlanError> {
let cursor = motion::clamp_to_boundary(text, cursor_byte_index);
let line_start = line_start(text, cursor);
let line_end = line_end_including_newline(text, cursor);
let mut inserted = counted_linewise_text(register.as_str(), count, max_byte_len)?;
let mut pasted_start_offset = 0;
let insert_at = match placement {
PastePlacement::Before => line_start,
PastePlacement::After if text.is_empty() => 0,
PastePlacement::After if line_end == text.len() && !text.ends_with('\n') => {
inserted.insert(0, '\n');
pasted_start_offset = '\n'.len_utf8();
text.len()
}
PastePlacement::After => line_end,
};
if inserted.len() > max_byte_len {
return Err(PastePlanError::PayloadTooLarge {
requested_byte_len: inserted.len(),
max_byte_len,
});
}
let pasted_start = insert_at + pasted_start_offset;
let cursor = pasted_start + first_non_blank_or_line_start(&inserted[pasted_start_offset..]);
Ok(PastePlan::new(insert_at, inserted, cursor))
}
fn counted_text(text: &str, count: Count, max_byte_len: usize) -> Result<String, PastePlanError> {
let requested_byte_len =
text.len()
.checked_mul(count.get())
.ok_or(PastePlanError::PayloadTooLarge {
requested_byte_len: usize::MAX,
max_byte_len,
})?;
if requested_byte_len > max_byte_len {
return Err(PastePlanError::PayloadTooLarge {
requested_byte_len,
max_byte_len,
});
}
Ok(text.repeat(count.get()))
}
fn counted_linewise_text(
text: &str,
count: Count,
max_byte_len: usize,
) -> Result<String, PastePlanError> {
let unit_len = text.len() + usize::from(!text.ends_with('\n'));
let requested_byte_len =
unit_len
.checked_mul(count.get())
.ok_or(PastePlanError::PayloadTooLarge {
requested_byte_len: usize::MAX,
max_byte_len,
})?;
if requested_byte_len > max_byte_len {
return Err(PastePlanError::PayloadTooLarge {
requested_byte_len,
max_byte_len,
});
}
let mut repeated = String::with_capacity(requested_byte_len);
for _ in 0..count.get() {
repeated.push_str(text);
if !text.ends_with('\n') {
repeated.push('\n');
}
}
Ok(repeated)
}
fn next_char_boundary_for_paste(text: &str, index: usize) -> usize {
let index = motion::clamp_to_boundary(text, index);
let Some(character) = text[index..].chars().next() else {
return index;
};
if character == '\n' {
index
} else {
index + character.len_utf8()
}
}
fn line_start(text: &str, index: usize) -> usize {
let index = motion::clamp_to_boundary(text, index);
text[..index]
.rfind('\n')
.map_or(0, |newline_index| newline_index + '\n'.len_utf8())
}
fn line_end_including_newline(text: &str, index: usize) -> usize {
let index = motion::clamp_to_boundary(text, index);
text[index..]
.find('\n')
.map_or(text.len(), |offset| index + offset + '\n'.len_utf8())
}
fn first_non_blank_or_line_start(line: &str) -> usize {
line.char_indices()
.find_map(|(offset, character)| {
(character != '\n' && !character.is_whitespace()).then_some(offset)
})
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::{PastePlacement, PastePlan, PastePlanError, plan_paste};
use crate::vim::{Count, RegisterKind, RegisterText};
const MAX: usize = 1024;
#[test]
fn plans_characterwise_paste_after_utf8_cursor_cell() {
let register = RegisterText::new("β", RegisterKind::Characterwise);
let plan = plan_paste(
"aλc",
"a".len(),
®ister,
Count::default(),
PastePlacement::After,
MAX,
)
.unwrap();
assert_eq!(
plan,
PastePlan::new("aλ".len(), String::from("β"), "aλ".len())
);
}
#[test]
fn plans_characterwise_paste_before_cursor_cell() {
let register = RegisterText::new("x", RegisterKind::Characterwise);
let plan = plan_paste(
"abc",
1,
®ister,
Count::default(),
PastePlacement::Before,
MAX,
)
.unwrap();
assert_eq!(plan, PastePlan::new(1, String::from("x"), 1));
}
#[test]
fn plans_linewise_paste_below_final_line_without_newline() {
let register = RegisterText::new("two", RegisterKind::Linewise);
let plan = plan_paste(
"one",
0,
®ister,
Count::default(),
PastePlacement::After,
MAX,
)
.unwrap();
assert_eq!(
plan,
PastePlan::new("one".len(), String::from("\ntwo\n"), 4)
);
}
#[test]
fn plans_linewise_paste_above_current_line() {
let register = RegisterText::new(" one\n", RegisterKind::Linewise);
let plan = plan_paste(
"two\n",
0,
®ister,
Count::default(),
PastePlacement::Before,
MAX,
)
.unwrap();
assert_eq!(plan, PastePlan::new(0, String::from(" one\n"), 2));
}
#[test]
fn counted_paste_is_budgeted_without_allocating() {
let register = RegisterText::new("abcd", RegisterKind::Characterwise);
let error = plan_paste(
"x",
0,
®ister,
std::num::NonZeroUsize::new(4).unwrap().into(),
PastePlacement::After,
15,
)
.unwrap_err();
assert_eq!(
error,
PastePlanError::PayloadTooLarge {
requested_byte_len: 16,
max_byte_len: 15,
}
);
}
}