use perl_position_tracking::{Position, Range};
#[derive(Debug, Clone, PartialEq)]
pub struct Edit {
pub start_byte: usize,
pub old_end_byte: usize,
pub new_end_byte: usize,
pub start_position: Position,
pub old_end_position: Position,
pub new_end_position: Position,
}
impl Edit {
pub fn new(
start_byte: usize,
old_end_byte: usize,
new_end_byte: usize,
start_position: Position,
old_end_position: Position,
new_end_position: Position,
) -> Self {
Edit {
start_byte,
old_end_byte,
new_end_byte,
start_position,
old_end_position,
new_end_position,
}
}
pub fn byte_shift(&self) -> isize {
self.new_end_byte as isize - self.old_end_byte as isize
}
pub fn line_shift(&self) -> i32 {
self.new_end_position.line as i32 - self.old_end_position.line as i32
}
pub fn affects_byte(&self, byte: usize) -> bool {
byte >= self.start_byte
}
pub fn overlaps_range(&self, range: &Range) -> bool {
range.start.byte < self.old_end_byte && range.end.byte > self.start_byte
}
pub fn apply_to_position(&self, pos: Position) -> Option<Position> {
if pos.byte < self.start_byte {
Some(pos)
} else if pos.byte >= self.old_end_byte {
Some(Position {
byte: (pos.byte as isize + self.byte_shift()) as usize,
line: (pos.line as i32 + self.line_shift()) as u32,
column: if pos.line == self.old_end_position.line {
let col_shift =
self.new_end_position.column as i32 - self.old_end_position.column as i32;
(pos.column as i32 + col_shift) as u32
} else {
pos.column
},
})
} else {
None
}
}
pub fn apply_to_range(&self, range: &Range) -> Option<Range> {
let new_start = self.apply_to_position(range.start)?;
let new_end = self.apply_to_position(range.end)?;
Some(Range::new(new_start, new_end))
}
}
#[derive(Debug, Clone, Default)]
pub struct EditSet {
pub(crate) edits: Vec<Edit>,
}
impl EditSet {
pub fn new() -> Self {
EditSet { edits: Vec::new() }
}
pub fn add(&mut self, edit: Edit) {
let pos = self
.edits
.iter()
.position(|e| e.start_byte > edit.start_byte)
.unwrap_or(self.edits.len());
self.edits.insert(pos, edit);
}
pub fn apply_to_position(&self, mut pos: Position) -> Option<Position> {
for edit in &self.edits {
pos = edit.apply_to_position(pos)?;
}
Some(pos)
}
pub fn apply_to_range(&self, mut range: Range) -> Option<Range> {
for edit in &self.edits {
range = edit.apply_to_range(&range)?;
}
Some(range)
}
pub fn len(&self) -> usize {
self.edits.len()
}
pub fn is_empty(&self) -> bool {
self.edits.is_empty()
}
pub fn edits(&self) -> &[Edit] {
&self.edits
}
pub fn affects_range(&self, range: &Range) -> bool {
self.edits.iter().any(|edit| edit.overlaps_range(range))
}
pub fn byte_shift_at(&self, byte: usize) -> isize {
self.edits
.iter()
.filter(|edit| edit.old_end_byte <= byte)
.map(|edit| edit.byte_shift())
.sum()
}
pub fn affected_ranges(&self) -> Vec<Range> {
self.edits
.iter()
.map(|edit| Range::new(edit.start_position, edit.old_end_position))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_tdd_support::must_some;
#[test]
fn test_simple_edit() {
let edit = Edit::new(
10,
15,
17,
Position::new(10, 2, 5),
Position::new(15, 2, 10),
Position::new(17, 2, 12),
);
assert_eq!(edit.byte_shift(), 2);
assert_eq!(edit.line_shift(), 0);
let pos = Position::new(5, 1, 5);
assert_eq!(edit.apply_to_position(pos), Some(pos));
let pos = Position::new(20, 2, 15);
let new_pos = must_some(edit.apply_to_position(pos));
assert_eq!(new_pos.byte, 22);
assert_eq!(new_pos.column, 17);
}
#[test]
fn test_multiline_edit() {
let edit = Edit::new(
10,
30,
20,
Position::new(10, 2, 5),
Position::new(30, 4, 10),
Position::new(20, 2, 15),
);
assert_eq!(edit.byte_shift(), -10);
assert_eq!(edit.line_shift(), -2);
let pos = Position::new(50, 6, 5);
let new_pos = must_some(edit.apply_to_position(pos));
assert_eq!(new_pos.byte, 40);
assert_eq!(new_pos.line, 4);
assert_eq!(new_pos.column, 5);
}
#[test]
fn test_edit_set() {
let mut edits = EditSet::new();
edits.add(Edit::new(
10,
15,
17,
Position::new(10, 2, 5),
Position::new(15, 2, 10),
Position::new(17, 2, 12),
));
edits.add(Edit::new(
30,
35,
40,
Position::new(30, 3, 5),
Position::new(35, 3, 10),
Position::new(40, 3, 15),
));
assert_eq!(edits.byte_shift_at(50), 7); }
}