Skip to content

Commit

Permalink
handle line numbers in "copy" operations
Browse files Browse the repository at this point in the history
Copy operations include an "ln" field that is used to update the line
number of the lines being copied. If we ignore it, we end up with
inconsistent line numbers in the cache.

For instance, consider the following file (`|` indicates the cursor
position):

```
a|
b
```

On pressing "Enter" to insert a newline, the following update is being
sent:

{"update":{"annotations":[{"n":1,"payloads":null,"ranges":[[1,0,1,0]],"type":"selection"}],"ops":[{"lines":[{"ln":1,"styles":[0,2,8],"text":"a\n"},{"cursor":[0],"ln":2,"styles":[0,1,8],"text":"\n"}],"n":2,"op":"ins"},{"n":1,"op":"skip"},{"ln":3,"n":1,"op":"copy"}],"pristine":false},"view_id":"view-id-2"}

The intersting operation is {"ln":3,"n":1,"op":"copy"}.
Before the update we have the following line cache:

LineCache { invalid_before: 0, lines: [Line { text: "a", cursor: [1], styles: [StyleDef { offset: 0, length: 2, style_id: 8 }], line_num: Some(1) }, Line { text: "b", cursor: [], styles: [StyleDef { offset: 0, length: 1, style_id: 8 }], line_num: Some(2) }], invalid_after: 0 }

If we ignore the "ln" field, we end up with the following cache where
we have to lines number "2" ("" and "b"):

LineCache { invalid_before: 0, lines: [Line { text: "a", cursor: [], styles: [StyleDef { offset: 0, length: 2, style_id: 8 }], line_num: Some(1) }, Line { text: "", cursor: [0], styles: [StyleDef { offset: 0, length: 1, style_id: 8 }], line_num: Some(2) }, Line { text: "b", cursor: [], styles: [StyleDef { offset: 0, length: 1, style_id: 8 }], line_num: Some(2) }], invalid_after: 0 }

because the "b" line is copied as is. With this commit, we end up with
the following line cache:

LineCache { invalid_before: 0, lines: [Line { text: "a", cursor: [], styles: [StyleDef { offset: 0, length: 2, style_id: 8 }], line_num: Some(1) }, Line { text: "", cursor: [0], styles: [StyleDef { offset: 0, length: 1, style_id: 8 }], line_num: Some(2) }, Line { text: "b", cursor: [], styles: [StyleDef { offset: 0, length: 1, style_id: 8 }], line_num: Some(3) }], invalid_after: 0 }
  • Loading branch information
little-dude committed Jun 8, 2019
1 parent 0628c9c commit cd57fec
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 18 deletions.
94 changes: 77 additions & 17 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct UpdateHelper<'a, 'b, 'c> {
}

impl<'a, 'b, 'c> UpdateHelper<'a, 'b, 'c> {
fn apply_copy(&mut self, nb_lines: u64) {
fn apply_copy(&mut self, nb_lines: u64, first_line_num: Option<u64>) {
debug!("copying {} lines", nb_lines);
let UpdateHelper {
ref mut old_lines,
Expand All @@ -79,41 +79,101 @@ impl<'a, 'b, 'c> UpdateHelper<'a, 'b, 'c> {
ref mut new_invalid_after,
..
} = *self;

// The number of lines left to copy
let mut nb_lines = nb_lines;

// Copy invalid lines that precede the valid ones
if **old_invalid_before > nb_lines {
// STEP 1: Handle the invalid lines that precede the valid ones
// ------------------------------------------------------------

if **old_invalid_before >= nb_lines {
// case 1: there are more (or equal) invalid lines than lines to copy

// decrement old_invalid_lines by nb_lines
**old_invalid_before -= nb_lines;

// and increment new_invalid_lines by the same amount
*new_invalid_before += nb_lines;

// there is no more line to copy so we're done
return;

} else if **old_invalid_after > 0 {
// case 2: there are more lines to copy than invalid lines

// decrement the nb of lines to copy by the number of invalid lines
nb_lines -= **old_invalid_before;

// increment new_invalid_lines by the same amount
*new_invalid_before += **old_invalid_before;

// we don't have any invalid lines left
**old_invalid_before = 0;
}

// Copy the valid lines
// STEP 2: Handle the valid lines
// ------------------------------------------------------------

let nb_valid_lines = old_lines.len();
if nb_lines < nb_valid_lines as u64 {
new_lines.extend(old_lines.drain(0..nb_lines as usize));
return;
let range;

if nb_lines <= (nb_valid_lines as u64) {
// case 1: the are more (or equal) valid lines than lines to copy

// the range of lines to copy: from the start to nb_lines - 1;
range = 0..nb_lines as usize;

// after the copy, we won't have any line remaining to copy
nb_lines = 0;
} else {
new_lines.extend(old_lines.drain(..));
// case 2: there are more lines to copy than valid lines

// we copy all the valid lines
range = 0..nb_valid_lines;

// after the operation we'll have (nb_lines - nb_valid_lines) left to copy
nb_lines -= nb_valid_lines as u64;
}

// Copy the remaining invalid lines
// we'll only apply the copy if there actually are valid lines to copy
if nb_valid_lines > 0 {
let old_first_line_num = old_lines[0].line_num.unwrap(); // valid lines always have a line number, so it's fine to unwrap.

// the line_num_updater is the function that will update the line numbers if necessary
let diff = if let Some(new_first_line_num) = first_line_num {
new_first_line_num as i64 - old_first_line_num as i64
} else {
0
};

let copied_lines = old_lines.drain(range).map(|mut line| {
line.line_num = line.line_num.map(|line_num| (line_num as i64 + diff) as u64);
line
});
new_lines.extend(copied_lines);
}

// if there are no more lines to copy we're done
if nb_lines == 0 {
return;
}

// STEP 3: Handle the remaining invalid lines
// ------------------------------------------------------------


// We should have at least enought invalid lines to copy, otherwise it indicates there's a
// problem, and we panic.
if **old_invalid_after >= nb_lines {
**old_invalid_after -= nb_lines;
*new_invalid_after += nb_lines;
return;
} else {
error!(
"{} lines left to copy, but only {} lines in the old cache",
nb_lines, **old_invalid_after
);
panic!("cache update failed");
}

error!(
"{} lines left to copy, but only {} lines in the old cache",
nb_lines, **old_invalid_after
);
panic!("cache update failed");
}

fn apply_skip(&mut self, nb_lines: u64) {
Expand Down Expand Up @@ -212,7 +272,7 @@ impl<'a, 'b, 'c> UpdateHelper<'a, 'b, 'c> {
debug!("operation: {:?}", &op);
debug!("cache helper before operation {:?}", &self);
match op.operation_type {
OperationType::Copy_ => (&mut self).apply_copy(op.nb_lines),
OperationType::Copy_ => (&mut self).apply_copy(op.nb_lines, op.line_num),
OperationType::Skip => (&mut self).apply_skip(op.nb_lines),
OperationType::Invalidate => (&mut self).apply_invalidate(op.nb_lines),
OperationType::Insert => (&mut self).apply_insert(op.lines),
Expand Down
21 changes: 21 additions & 0 deletions src/structs/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub struct Operation {
pub operation_type: OperationType,
#[serde(rename = "n")]
pub nb_lines: u64,
#[serde(rename = "ln")]
pub line_num: Option<u64>,
#[serde(default)]
pub lines: Vec<Line>,
}
Expand Down Expand Up @@ -48,6 +50,7 @@ fn deserialize_operation_from_value() {
let operation = Operation {
operation_type: OperationType::Insert,
nb_lines: 12,
line_num: None,
lines: vec![],
};
let deserialized: Result<Operation, _> = serde_json::from_value(value);
Expand All @@ -58,6 +61,7 @@ fn deserialize_operation_from_value() {
let operation = Operation {
operation_type: OperationType::Invalidate,
nb_lines: 60,
line_num: None,
lines: vec![
Line {
cursor: vec![0],
Expand Down Expand Up @@ -85,6 +89,7 @@ fn deserialize_operation() {
let operation = Operation {
operation_type: OperationType::Insert,
nb_lines: 12,
line_num: None,
lines: vec![],
};
let deserialized: Result<Operation, _> = serde_json::from_str(s);
Expand All @@ -96,6 +101,7 @@ fn deserialize_operation() {
let operation = Operation {
operation_type: OperationType::Invalidate,
nb_lines: 60,
line_num: None,
lines: vec![
Line {
cursor: vec![0],
Expand All @@ -114,3 +120,18 @@ fn deserialize_operation() {
let deserialized: Result<Operation, _> = serde_json::from_str(s);
assert_eq!(deserialized.unwrap(), operation);
}

#[test]
fn deserialize_copy() {
use serde_json;
let s = r#"{"ln":3,"n":1,"op":"copy"}"#;
let operation = Operation {
operation_type: OperationType::Copy_,
line_num: Some(3),
nb_lines: 1,
lines: Vec::new(),
};

let deserialized: Result<Operation, _> = serde_json::from_str(s);
assert_eq!(deserialized.unwrap(), operation);
}
4 changes: 3 additions & 1 deletion src/structs/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl<'de> Deserialize<'de> for Update {
fn deserialize_update() {
use serde_json;
use std::str::FromStr;

use super::Line;
use super::operation::{Operation, OperationType};

Expand All @@ -57,11 +57,13 @@ fn deserialize_update() {
Operation {
operation_type: OperationType::Invalidate,
nb_lines: 60,
line_num: None,
lines: vec![],
},
Operation {
operation_type: OperationType::Insert,
nb_lines: 12,
line_num: None,
lines: vec![
Line {
cursor: vec![0],
Expand Down

0 comments on commit cd57fec

Please sign in to comment.