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();
}
}
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))
}
#[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));
}
}