use super::types::{Cursor, TextPosition};
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiCursor {
carets: Vec<Cursor>,
primary: Cursor,
}
impl Default for MultiCursor {
fn default() -> Self {
Self::new(Cursor::default())
}
}
impl MultiCursor {
pub fn new(primary: Cursor) -> Self {
Self { carets: vec![primary], primary }
}
pub fn from_parts(primary: Cursor, extras: &[Cursor]) -> Self {
let mut set = Self { carets: Vec::new(), primary };
set.carets.push(primary);
for caret in extras {
set.carets.push(*caret);
}
set.normalize();
if !set.carets.contains(&primary) {
set.carets.push(primary);
set.normalize();
}
set
}
pub fn carets(&self) -> &[Cursor] {
&self.carets
}
pub fn primary(&self) -> Cursor {
self.primary
}
pub fn secondaries(&self) -> Vec<Cursor> {
self.carets.iter().copied().filter(|caret| *caret != self.primary).collect()
}
pub fn len(&self) -> usize {
self.carets.len()
}
pub fn is_empty(&self) -> bool {
self.carets.is_empty()
}
pub fn has_multiple(&self) -> bool {
self.carets.len() > 1
}
pub fn iter(&self) -> core::slice::Iter<'_, Cursor> {
self.carets.iter()
}
pub fn push(&mut self, caret: Cursor) -> bool {
if self.carets.contains(&caret) {
return false;
}
self.carets.push(caret);
self.normalize();
true
}
pub fn clear_secondaries(&mut self) {
self.carets.retain(|caret| *caret == self.primary);
}
pub fn set_primary(&mut self, caret: Cursor) {
self.carets.retain(|existing| *existing != self.primary);
self.carets.push(caret);
self.primary = caret;
self.normalize();
}
fn normalize(&mut self) {
self.carets.sort_by(|a, b| {
a.head
.line
.cmp(&b.head.line)
.then_with(|| a.head.column.cmp(&b.head.column))
.then_with(|| a.anchor.line.cmp(&b.anchor.line))
.then_with(|| a.anchor.column.cmp(&b.anchor.column))
});
self.carets.dedup();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OffsetEdit {
pub start: usize,
pub end: usize,
pub insert: String,
}
pub(crate) fn char_offset(lines: &[String], position: TextPosition) -> usize {
let mut offset = 0usize;
for line in 0..position.line.min(lines.len()) {
offset += lines.get(line).map(|text| text.chars().count()).unwrap_or(0) + 1;
}
let line_len = lines.get(position.line).map(|text| text.chars().count()).unwrap_or(0);
offset + position.column.min(line_len)
}
pub(crate) fn position_at_offset(lines: &[String], offset: usize) -> TextPosition {
let mut remaining = offset;
for (index, line) in lines.iter().enumerate() {
let length = line.chars().count();
if remaining <= length {
return TextPosition::new(index, remaining);
}
remaining -= length + 1;
}
let last = lines.len().saturating_sub(1);
TextPosition::new(last, lines.get(last).map(|line| line.chars().count()).unwrap_or(0))
}
pub(crate) fn project(text: &str, edits: &[OffsetEdit]) -> (String, Vec<Option<usize>>) {
let chars: Vec<char> = text.chars().collect();
let mut order: Vec<usize> = (0..edits.len()).collect();
order.sort_by_key(|&index| (edits[index].start, edits[index].end));
let mut result = String::with_capacity(text.len());
let mut carets: Vec<Option<usize>> = vec![None; edits.len()];
let mut cursor = 0usize;
let mut delta: isize = 0;
for &index in &order {
let edit = &edits[index];
let start = edit.start.min(chars.len());
let end = edit.end.min(chars.len()).max(start);
if start < cursor {
continue;
}
result.extend(chars[cursor..start].iter());
let new_start = (start as isize + delta).max(0) as usize;
let insert_len = edit.insert.chars().count();
result.push_str(&edit.insert);
carets[index] = Some(new_start + insert_len);
delta += insert_len as isize - (end - start) as isize;
cursor = end;
}
result.extend(chars[cursor.min(chars.len())..].iter());
(result, carets)
}
#[cfg(test)]
mod tests {
use super::*;
fn caret(line: usize, column: usize) -> Cursor {
Cursor { head: TextPosition::new(line, column), anchor: TextPosition::new(line, column) }
}
#[test]
fn a_fresh_set_holds_only_the_primary() {
let set = MultiCursor::new(caret(1, 2));
assert_eq!(set.len(), 1);
assert!(!set.has_multiple());
assert_eq!(set.primary(), caret(1, 2));
}
#[test]
fn push_keeps_carets_sorted_and_unique() {
let mut set = MultiCursor::new(caret(2, 0));
assert!(set.push(caret(0, 4)));
assert!(!set.push(caret(0, 4)), "duplicates are rejected");
assert!(set.push(caret(1, 1)));
let heads: Vec<(usize, usize)> =
set.carets().iter().map(|c| (c.head.line, c.head.column)).collect();
assert_eq!(heads, vec![(0, 4), (1, 1), (2, 0)]);
assert_eq!(set.len(), 3);
assert!(set.has_multiple());
assert_eq!(set.primary().head.line, 2);
}
#[test]
fn clear_secondaries_keeps_the_primary() {
let mut set = MultiCursor::from_parts(caret(1, 0), &[caret(0, 0), caret(2, 0)]);
assert_eq!(set.len(), 3);
set.clear_secondaries();
assert_eq!(set.len(), 1);
assert_eq!(set.primary(), caret(1, 0));
}
#[test]
fn char_offsets_round_trip_across_lines() {
let lines: Vec<String> = vec![String::from("abc"), String::from("de"), String::from("")];
assert_eq!(char_offset(&lines, TextPosition::new(0, 1)), 1);
assert_eq!(char_offset(&lines, TextPosition::new(1, 0)), 4);
assert_eq!(char_offset(&lines, TextPosition::new(2, 0)), 7);
assert_eq!(position_at_offset(&lines, 4), TextPosition::new(1, 0));
assert_eq!(position_at_offset(&lines, 5), TextPosition::new(1, 1));
assert_eq!(position_at_offset(&lines, 7), TextPosition::new(2, 0));
}
#[test]
fn project_inserts_at_every_caret_and_reports_new_offsets() {
let edits = vec![
OffsetEdit { start: 0, end: 0, insert: String::from("X") },
OffsetEdit { start: 2, end: 2, insert: String::from("Y") },
];
let (text, carets) = project("abc", &edits);
assert_eq!(text, "XabYc");
assert_eq!(carets, vec![Some(1), Some(4)]);
}
#[test]
fn project_deletes_ranges_from_the_right() {
let edits = vec![
OffsetEdit { start: 0, end: 1, insert: String::new() },
OffsetEdit { start: 4, end: 5, insert: String::new() },
];
let (text, carets) = project("aXbYc", &edits);
assert_eq!(text, "XbY");
assert_eq!(carets, vec![Some(0), Some(3)]);
}
#[test]
fn project_drops_overlapping_edits() {
let edits = vec![
OffsetEdit { start: 0, end: 3, insert: String::from("Z") },
OffsetEdit { start: 1, end: 2, insert: String::from("!") },
];
let (text, carets) = project("abcd", &edits);
assert_eq!(text, "Zd");
assert_eq!(carets[0], Some(1));
assert_eq!(carets[1], None, "the overlapping edit is dropped");
}
#[test]
fn project_clamps_out_of_range_offsets() {
let edits = vec![OffsetEdit { start: 99, end: 99, insert: String::from("!") }];
let (text, carets) = project("ab", &edits);
assert_eq!(text, "ab!");
assert_eq!(carets, vec![Some(3)]);
}
}