use crate::action::{Action, ActionError};
use crate::changeset::{ChangeSet, Edit};
use crate::history::EditKind;
use crate::resolve::Resolved;
use crate::selection::SelectionSet;
use crate::text::Document;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Bookmarks {
lines: Vec<usize>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct EditLines {
pub start: usize,
pub old_end: usize,
pub new_lines: usize,
pub at_line_start: bool,
pub insertion: bool,
}
impl Bookmarks {
pub fn lines(&self) -> &[usize] {
&self.lines
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn contains(&self, line: usize) -> bool {
self.lines.binary_search(&line).is_ok()
}
pub fn set(&mut self, lines: impl IntoIterator<Item = usize>, line_count: usize) {
let mut lines: Vec<usize> = lines.into_iter().filter(|&l| l < line_count).collect();
lines.sort_unstable();
lines.dedup();
self.lines = lines;
}
pub fn insert(&mut self, line: usize) -> bool {
match self.lines.binary_search(&line) {
Ok(_) => false,
Err(i) => {
self.lines.insert(i, line);
true
}
}
}
pub fn remove(&mut self, line: usize) -> bool {
match self.lines.binary_search(&line) {
Ok(i) => {
self.lines.remove(i);
true
}
Err(_) => false,
}
}
pub fn toggle(&mut self, line: usize) -> bool {
!self.remove(line) && self.insert(line)
}
pub fn clear(&mut self) {
self.lines.clear();
}
pub fn next_after(&self, line: usize) -> Option<usize> {
let i = self.lines.partition_point(|&l| l <= line);
self.lines.get(i).or(self.lines.first()).copied()
}
pub fn prev_before(&self, line: usize) -> Option<usize> {
let i = self.lines.partition_point(|&l| l < line);
i.checked_sub(1)
.and_then(|i| self.lines.get(i))
.or(self.lines.last())
.copied()
}
pub fn invert(&mut self, line_count: usize) {
let mut marked = self.lines.iter().copied().peekable();
let mut inverted = Vec::with_capacity(line_count.saturating_sub(self.lines.len()));
for line in 0..line_count {
if marked.peek() == Some(&line) {
marked.next();
} else {
inverted.push(line);
}
}
self.lines = inverted;
}
pub(crate) fn map(&mut self, edits: &[EditLines], line_count: usize) {
if self.lines.is_empty() || edits.is_empty() {
return;
}
let mut out = Vec::with_capacity(self.lines.len());
let mut delta: isize = 0;
let mut i = 0;
for &line in &self.lines {
while let Some(e) = edits.get(i).filter(|e| e.old_end < line) {
delta += e.new_lines as isize - (e.old_end - e.start) as isize;
i += 1;
}
let new = match edits.get(i) {
Some(e) if e.start < line => {
e.start as isize + delta + (line - e.start).min(e.new_lines) as isize
}
Some(e) if e.start == line && e.at_line_start && e.insertion => {
(line + e.new_lines) as isize + delta
}
_ => line as isize + delta,
};
out.push(new.clamp(0, line_count.saturating_sub(1) as isize) as usize);
}
out.sort_unstable();
out.dedup();
self.lines = out;
}
}
pub fn marked_text(doc: &Document, marks: &Bookmarks) -> String {
let last = doc.line_count() - 1;
let mut text = String::new();
for &line in marks.lines() {
text.push_str(&doc.line_text(line));
if line < last {
text.push('\n');
}
}
text
}
pub(crate) fn caret_lines(doc: &Document, sels: &SelectionSet) -> Vec<usize> {
let mut lines: Vec<usize> = sels.ranges().iter().map(|s| doc.line_of(s.head)).collect();
lines.dedup();
lines
}
pub(crate) fn resolve(
action: &Action,
doc: &Document,
sels: &SelectionSet,
marks: &Bookmarks,
) -> Result<Resolved, ActionError> {
if marks.is_empty() {
return Err(ActionError::NoBookmarks);
}
let caret = doc.line_of(sels.primary().head);
let go = |line: Option<usize>| {
let line = line.ok_or(ActionError::NoBookmarks)?;
Ok(Resolved::Selections(SelectionSet::caret(
doc.line_start(line),
)))
};
match action {
Action::NextBookmark => go(marks.next_after(caret)),
Action::PrevBookmark => go(marks.prev_before(caret)),
Action::RemoveBookmarkedLines => {
Ok(crate::transform::remove_lines(doc, sels, marks.lines()))
}
Action::RemoveUnbookmarkedLines => {
let last = doc.line_count() - 1;
let count = if last > 0 && doc.line_start(last) == doc.len() {
last
} else {
last + 1
};
let mut unmarked = marks.clone();
unmarked.invert(count);
Ok(crate::transform::remove_lines(doc, sels, unmarked.lines()))
}
Action::ReplaceBookmarkedLines(text) => {
let edits: Vec<Edit> = marks
.lines()
.iter()
.map(|&l| Edit::new(doc.line_start(l), doc.line_end(l), text.as_str()))
.collect();
let changes = ChangeSet::new(doc.len(), edits);
let after = SelectionSet::caret(
changes.map_position(sels.primary().head, crate::changeset::Bias::Before),
);
Ok(Resolved::Edit {
changes,
after,
kind: EditKind::Other,
})
}
_ => Ok(Resolved::Selections(sels.clone())),
}
}
pub(crate) fn after_replace(before: &[usize], text: &str) -> Vec<usize> {
let breaks = text.bytes().filter(|&b| b == b'\n').count();
before
.iter()
.enumerate()
.map(|(i, &l)| l + i * breaks)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn marks(lines: &[usize]) -> Bookmarks {
let mut b = Bookmarks::default();
b.set(lines.iter().copied(), usize::MAX);
b
}
#[test]
fn toggle_insert_remove_and_set() {
let mut b = Bookmarks::default();
assert!(b.toggle(3));
assert!(b.toggle(1));
assert_eq!(b.lines(), &[1, 3]);
assert!(!b.toggle(3));
assert_eq!(b.lines(), &[1]);
assert!(!b.insert(1));
assert!(b.remove(1));
assert!(b.is_empty());
b.set([5, 2, 2, 9], 6);
assert_eq!(b.lines(), &[2, 5]);
}
#[test]
fn next_and_previous_wrap_around() {
let b = marks(&[2, 5, 9]);
assert_eq!(b.next_after(0), Some(2));
assert_eq!(b.next_after(2), Some(5));
assert_eq!(b.next_after(9), Some(2));
assert_eq!(b.prev_before(5), Some(2));
assert_eq!(b.prev_before(2), Some(9));
assert_eq!(b.prev_before(100), Some(9));
assert_eq!(Bookmarks::default().next_after(0), None);
}
#[test]
fn invert_marks_the_other_lines() {
let mut b = marks(&[0, 2]);
b.invert(5);
assert_eq!(b.lines(), &[1, 3, 4]);
b.invert(5);
assert_eq!(b.lines(), &[0, 2]);
}
#[test]
fn marked_text_keeps_line_breaks_except_at_the_end() {
let doc = Document::new("a\nb\nc");
assert_eq!(marked_text(&doc, &marks(&[0, 2])), "a\nc");
assert_eq!(marked_text(&doc, &marks(&[1])), "b\n");
}
#[test]
fn after_replace_counts_the_inserted_lines() {
assert_eq!(after_replace(&[1, 4], "x\ny"), vec![1, 5]);
assert_eq!(after_replace(&[1, 4], "x"), vec![1, 4]);
}
}