diff --git a/crates/helm-bench/Cargo.toml b/crates/helm-bench/Cargo.toml index aaaec85..77f1622 100644 --- a/crates/helm-bench/Cargo.toml +++ b/crates/helm-bench/Cargo.toml @@ -39,7 +39,6 @@ harness = false name = "encoding" harness = false - [[bench]] name = "snapshots" harness = false @@ -48,7 +47,11 @@ harness = false name = "editor" harness = false - [[bench]] name = "keymap" harness = false + +[[bench]] +name = "amend" +harness = false + diff --git a/crates/helm-bench/benches/amend.rs b/crates/helm-bench/benches/amend.rs new file mode 100644 index 0000000..19a012d --- /dev/null +++ b/crates/helm-bench/benches/amend.rs @@ -0,0 +1,86 @@ +//! Amend benchmarks for `helm-text`, gated by the `bench` CI job against +//! `budgets.toml`. NF-7: no rope edit gets more than 5% slower than its base +//! commit. This one also guards T-110's cost rule: an amend that copied the +//! buffer, or an undo that replayed every amend, would show up here. + +use std::hint::black_box; + +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use helm_text::{Buffer, BufferSet, Edit, Transaction}; + +/// Size of the benchmark text: 10 MiB, cut down to a char boundary. +const TEXT_LEN: usize = 10 << 20; + +/// Number of amends after the first transaction. +const AMENDS: usize = 100; + +/// The characters typed, in turn: 1-, 2-, 3- and 4-byte ones. +const TYPED: &str = "helm 😀中é"; + +/// The lines the text repeats: ASCII code mixed with é (2 bytes), 中 +/// (3 bytes) and 😀 (4 bytes, a surrogate pair in UTF-16), ending in both LF +/// and CRLF. Fixed, so base and head measure the same text. +const LINES: &str = concat!( + "fn main() {\n", + " let café = \"crème brûlée\";\r\n", + " // 中文注释: 编辑器\n", + " println!(\"{café} 😀 done\");\r\n", + " let emoji = ['😀', '中', 'é'];\n", + "}\r\n", + "\n", +); + +/// The largest char boundary of `text` at or below `byte`. +fn floor_boundary(text: &str, mut byte: usize) -> usize { + while !text.is_char_boundary(byte) { + byte -= 1; + } + byte +} + +/// 10 MiB of `LINES` repeated, cut at the last char boundary at or below +/// `TEXT_LEN`. +fn build_text() -> String { + let mut text = LINES.repeat(TEXT_LEN / LINES.len() + 1); + text.truncate(floor_boundary(&text, TEXT_LEN)); + text +} + +fn amend(c: &mut Criterion) { + let text = build_text(); + let mut set = BufferSet::new(); + let id = set.add(Buffer::from(text.as_str())); + + // Character `i` lands right after character `i - 1`, starting at the + // char boundary at or below the middle. + let mut at = floor_boundary(&text, text.len() / 2); + let mut typed = TYPED.chars().cycle().map(|c| { + let edit = Edit::insert(id, at, c.to_string()); + at += c.len_utf8(); + Transaction::from(vec![edit]) + }); + let first = typed.next().expect("cycle never ends"); + let amends: Vec = typed.take(AMENDS).collect(); + + let mut group = c.benchmark_group("amend"); + + group.bench_function("type_100_chars_undo_10mb", |b| { + b.iter_batched( + || (first.clone(), amends.clone()), + |(first, amends)| { + let transaction = set.apply(black_box(first)).expect("valid edit"); + for amend in amends { + set.amend(transaction, black_box(amend)) + .expect("valid amend"); + } + black_box(set.undo(id).expect("known buffer")); + }, + BatchSize::SmallInput, + ) + }); + + group.finish(); +} + +criterion_group!(benches, amend); +criterion_main!(benches); diff --git a/crates/helm-text/src/buffer_set.rs b/crates/helm-text/src/buffer_set.rs index 5cabf4a..7e6a625 100644 --- a/crates/helm-text/src/buffer_set.rs +++ b/crates/helm-text/src/buffer_set.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use crate::{ - AmendError, Buffer, BufferId, HistoryError, Snapshot, TextBuffer, Transaction, + AmendError, Buffer, BufferId, Edit, HistoryError, Snapshot, TextBuffer, Transaction, TransactionError, TransactionId, Version, }; @@ -107,6 +107,10 @@ pub struct BufferSet { next_transaction: u64, } +/// Checked edits per buffer index, as `(start, end, index in the +/// transaction)`. Sorted, that is the order the edits are placed in. +type Placed = BTreeMap>; + /// One buffer and its histories. #[derive(Debug)] struct Entry { @@ -139,6 +143,26 @@ impl Entry { struct Record { /// One change per buffer the transaction touched, lowest buffer first. changes: Vec, + /// Whether the transaction is undone, so it sits in redo histories. + undone: bool, +} + +impl Record { + /// The change to the buffer at `index`, if the transaction touched it. + fn change(&self, index: usize) -> Option<&Change> { + self.changes + .binary_search_by_key(&index, |change| change.buffer) + .ok() + .map(|at| &self.changes[at]) + } + + /// [`change`](Self::change), mutably. + fn change_mut(&mut self, index: usize) -> Option<&mut Change> { + self.changes + .binary_search_by_key(&index, |change| change.buffer) + .ok() + .map(|at| &mut self.changes[at]) + } } /// What one transaction did to one buffer. @@ -161,6 +185,129 @@ struct Splice { text: String, } +impl Change { + /// The change to the buffer at `index` made of `splices`, each paired + /// with the text it replaces. The splices are sorted, don't overlap and + /// refer to the text before the change. + fn new(index: usize, splices: impl IntoIterator) -> Self { + let splices = splices.into_iter(); + let mut forward = Vec::with_capacity(splices.size_hint().0); + let mut inverse = Vec::with_capacity(splices.size_hint().0); + // Bytes added and removed by the splices placed so far, which move + // where this splice's text lands in the new text. + let (mut added, mut removed) = (0, 0); + for (splice, old) in splices { + let new_start = splice.range.start + added - removed; + inverse.push(Splice { + range: new_start..new_start + splice.text.len(), + text: old, + }); + added += splice.text.len(); + removed += splice.range.len(); + forward.push(splice); + } + Self { + buffer: index, + forward, + inverse, + } + } + + /// Folds `edits` into the change, so it turns the text before it into + /// the text after it with `edits` applied. + /// + /// `edits` are sorted the way `apply` places them, don't overlap, and + /// refer to the text after the change, which `buffer` holds. The walk + /// goes through the change's splices, by where their text sits in + /// `buffer`, and `edits` together. A run of them that overlap or touch + /// becomes one splice, so a run of typed characters stays one splice. + /// Splices no edit reaches are kept as they are. The text the new + /// splices replace comes from the old splices and, where the change left + /// the text alone, from `buffer` inside the edits' ranges, so the work grows + /// with the change and the edits, not with the buffer. + fn compose(&mut self, buffer: &Buffer, edits: &[Splice]) { + // Each old splice with its inverse, whose range is where the + // splice's text sits in `buffer`. + let mut olds = mem::take(&mut self.forward) + .into_iter() + .zip(mem::take(&mut self.inverse)) + .peekable(); + let mut edits = edits.iter().peekable(); + let mut composed = Vec::new(); + // Bytes added and removed by the old splices passed so far, which + // map an offset in `buffer` back to the text before the change. + let (mut added, mut removed) = (0, 0); + + loop { + let start = match (olds.peek(), edits.peek()) { + (None, None) => break, + (Some((_, inverse)), Some(edit)) => inverse.range.start.min(edit.range.start), + (Some((_, inverse)), None) => inverse.range.start, + (None, Some(edit)) => edit.range.start, + }; + let before_start = start + removed - added; + + // The run of old splices and edits that starts here: each one + // starts at or before the end of the ones before it. + let mut end = start; + let mut run_olds = Vec::new(); + let mut run_edits = Vec::new(); + loop { + if let Some(old) = olds.next_if(|(_, inverse)| inverse.range.start <= end) { + end = end.max(old.1.range.end); + added += old.0.text.len(); + removed += old.0.range.len(); + run_olds.push(old); + } else if let Some(edit) = edits.next_if(|edit| edit.range.start <= end) { + end = end.max(edit.range.end); + run_edits.push(edit); + } else { + break; + } + } + + if run_edits.is_empty() { + composed.extend( + run_olds + .into_iter() + .map(|(forward, inverse)| (forward, inverse.text)), + ); + continue; + } + + // The run's text before the change: what the old splices + // replaced, and between them, text the change left alone, which + // lies inside an edit's range. + let mut old_text = String::new(); + let mut at = start; + for (_, inverse) in run_olds { + read(buffer, at..inverse.range.start, &mut old_text); + old_text.push_str(&inverse.text); + at = inverse.range.end; + } + read(buffer, at..end, &mut old_text); + + // The run's text after the edits: their texts, and between + // them, text in `buffer`, which lies inside an old splice's. + let mut new_text = String::new(); + let mut at = start; + for edit in run_edits { + read(buffer, at..edit.range.start, &mut new_text); + new_text.push_str(&edit.text); + at = edit.range.end; + } + read(buffer, at..end, &mut new_text); + + let splice = Splice { + range: before_start..end + removed - added, + text: new_text, + }; + composed.push((splice, old_text)); + } + *self = Self::new(self.buffer, composed); + } +} + impl BufferSet { /// An empty set. pub fn new() -> Self { @@ -223,33 +370,7 @@ impl BufferSet { /// describe, and then changes nothing. pub fn apply(&mut self, transaction: Transaction) -> Result { let edits = transaction.into_edits(); - if edits.is_empty() { - return Err(TransactionError::Empty); - } - - // Check every edit, collecting the text each one replaces and, per - // buffer, the edits as `(start, end, index)`. Sorted, that is the - // order the edits are placed in. - let mut olds = Vec::with_capacity(edits.len()); - let mut placed: BTreeMap> = BTreeMap::new(); - for (i, edit) in edits.iter().enumerate() { - let index = self - .index(edit.buffer) - .ok_or(TransactionError::UnknownBuffer { - edit: i, - buffer: edit.buffer, - })?; - let old = self.entries[index] - .buffer - .slice(edit.range.clone()) - .map_err(|error| TransactionError::InvalidRange { edit: i, error })?; - let mine = placed.entry(index).or_default(); - if let Some(other) = earliest_overlap(mine, &edit.range) { - return Err(TransactionError::Overlap { edit: i, other }); - } - mine.insert((edit.range.start, edit.range.end, i)); - olds.push(old); - } + let (mut olds, placed) = self.check(&edits, |_, _, _| Ok(()), |error| error)?; let touched: Vec = placed.keys().copied().collect(); self.drop_redo(&touched); @@ -259,36 +380,29 @@ impl BufferSet { self.next_transaction += 1; let mut changes = Vec::with_capacity(placed.len()); for (index, mine) in placed { - let mut forward = Vec::with_capacity(mine.len()); - let mut inverse = Vec::with_capacity(mine.len()); - // Bytes added and removed by the edits placed so far, which move - // where this edit's text lands in the new text. - let (mut added, mut removed) = (0, 0); - for (start, end, i) in mine { - let text = mem::take(&mut texts[i]); - let new_start = start + added - removed; - inverse.push(Splice { - range: new_start..new_start + text.len(), - text: mem::take(&mut olds[i]), - }); - added += text.len(); - removed += end - start; - forward.push(Splice { - range: start..end, - text, - }); - } + let change = Change::new( + index, + mine.into_iter().map(|(start, end, i)| { + let splice = Splice { + range: start..end, + text: mem::take(&mut texts[i]), + }; + (splice, mem::take(&mut olds[i])) + }), + ); let entry = &mut self.entries[index]; - splice_all(&mut entry.buffer, &forward); + splice_all(&mut entry.buffer, &change.forward); entry.bump(); entry.undo.push(id); - changes.push(Change { - buffer: index, - forward, - inverse, - }); + changes.push(change); } - self.transactions.insert(id, Record { changes }); + self.transactions.insert( + id, + Record { + changes, + undone: false, + }, + ); Ok(TransactionId(id)) } @@ -376,12 +490,58 @@ impl BufferSet { /// buffer, so a run of typed characters undoes like one insert. /// /// [`TextBuffer::slice`]: crate::TextBuffer::slice - #[expect( - unused_variables, - reason = "T-110 interface stub; the implementation deletes this" - )] pub fn amend(&mut self, id: TransactionId, transaction: Transaction) -> Result<(), AmendError> { - todo!() + let Some(record) = self.transactions.get(&id.0) else { + return Err(AmendError::UnknownTransaction { transaction: id }); + }; + if record.undone { + return Err(AmendError::Undone { transaction: id }); + } + + let edits = transaction.into_edits(); + let (_, placed) = self.check( + &edits, + |i, index, buffer| { + if record.change(index).is_none() { + Err(AmendError::Untouched { edit: i, buffer }) + } else if self.entries[index].undo.last() != Some(&id.0) { + Err(AmendError::NotOnTop { edit: i, buffer }) + } else { + Ok(()) + } + }, + AmendError::Transaction, + )?; + + let touched: Vec = placed.keys().copied().collect(); + self.drop_redo(&touched); + + let mut texts: Vec = edits.into_iter().map(|edit| edit.text).collect(); + let Self { + entries, + transactions, + .. + } = self; + let Some(record) = transactions.get_mut(&id.0) else { + unreachable!("`id` is applied, so dropping redo kept it"); + }; + for (index, mine) in placed { + let splices: Vec = mine + .into_iter() + .map(|(start, end, i)| Splice { + range: start..end, + text: mem::take(&mut texts[i]), + }) + .collect(); + let Some(change) = record.change_mut(index) else { + unreachable!("every edit's buffer passed the `Untouched` check"); + }; + let entry = &mut entries[index]; + change.compose(&entry.buffer, &splices); + splice_all(&mut entry.buffer, &splices); + entry.bump(); + } + Ok(()) } /// Reverts the transaction on top of `buffer`'s undo history in every @@ -406,6 +566,44 @@ impl BufferSet { self.step(buffer, Direction::Redo) } + /// Runs `apply`'s checks on `edits`, with `extra` run on each edit + /// right after its buffer is found, and returns the text each edit + /// replaces and, per buffer, the edits as `(start, end, index)`. Sorted, + /// that is the order the edits are placed in. `wrap` turns a failed + /// `apply` check into the caller's error. + fn check( + &self, + edits: &[Edit], + mut extra: impl FnMut(usize, usize, BufferId) -> Result<(), E>, + wrap: impl Fn(TransactionError) -> E, + ) -> Result<(Vec, Placed), E> { + if edits.is_empty() { + return Err(wrap(TransactionError::Empty)); + } + let mut olds = Vec::with_capacity(edits.len()); + let mut placed = Placed::new(); + for (i, edit) in edits.iter().enumerate() { + let index = self.index(edit.buffer).ok_or_else(|| { + wrap(TransactionError::UnknownBuffer { + edit: i, + buffer: edit.buffer, + }) + })?; + extra(i, index, edit.buffer)?; + let old = self.entries[index] + .buffer + .slice(edit.range.clone()) + .map_err(|error| wrap(TransactionError::InvalidRange { edit: i, error }))?; + let mine = placed.entry(index).or_default(); + if let Some(other) = earliest_overlap(mine, &edit.range) { + return Err(wrap(TransactionError::Overlap { edit: i, other })); + } + mine.insert((edit.range.start, edit.range.end, i)); + olds.push(old); + } + Ok((olds, placed)) + } + /// The index in `entries` of buffer `id`, if the set has it. fn index(&self, id: BufferId) -> Option { usize::try_from(id.0) @@ -425,7 +623,9 @@ impl BufferSet { let Some(&top) = direction.from(&self.entries[index]).last() else { return Ok(None); }; - let record = &self.transactions[&top]; + let Some(record) = self.transactions.get_mut(&top) else { + unreachable!("every id in a history has a record"); + }; // Changes are sorted by buffer, so the first blocked one is the // lowest id. @@ -451,6 +651,7 @@ impl BufferSet { to.push(top); entry.bump(); } + record.undone = matches!(direction, Direction::Undo); Ok(Some(TransactionId(top))) } @@ -517,6 +718,14 @@ fn splice_all(buffer: &mut Buffer, splices: &[Splice]) { } } +/// Appends the text in `range` of `buffer` to `into`. `range` comes from +/// splices and edits already checked against `buffer`, so it is valid. +fn read(buffer: &Buffer, range: Range, into: &mut String) { + if !range.is_empty() { + into.push_str(&buffer.slice(range).expect("a checked range")); + } +} + /// The lowest index of an edit in `placed` that overlaps `range`. /// /// The edits in `placed` don't overlap each other, so sorted by start their diff --git a/crates/helm-text/tests/amend.rs b/crates/helm-text/tests/amend.rs index de79124..683189d 100644 --- a/crates/helm-text/tests/amend.rs +++ b/crates/helm-text/tests/amend.rs @@ -61,7 +61,6 @@ fn state(set: &BufferSet, ids: &[BufferId]) -> (Vec, Vec) { // Folding edits into one undo step. #[test] -#[ignore = "T-110: not implemented yet"] fn typing_a_word_undoes_in_one_step() { let (mut set, ids) = set_of(&["say !"]); let a = ids[0]; @@ -82,7 +81,6 @@ fn typing_a_word_undoes_in_one_step() { } #[test] -#[ignore = "T-110: not implemented yet"] fn amend_returns_unit() { let (mut set, ids) = set_of(&["x"]); let a = ids[0]; @@ -95,7 +93,6 @@ fn amend_returns_unit() { } #[test] -#[ignore = "T-110: not implemented yet"] fn the_id_stays_the_same_and_no_id_is_used_up() { let (mut set, ids) = set_of(&["", ""]); let (a, b) = (ids[0], ids[1]); @@ -115,7 +112,6 @@ fn the_id_stays_the_same_and_no_id_is_used_up() { } #[test] -#[ignore = "T-110: not implemented yet"] fn offsets_refer_to_the_text_after_the_transaction() { let (mut set, ids) = set_of(&["abcdef"]); let a = ids[0]; @@ -146,7 +142,6 @@ fn offsets_refer_to_the_text_after_the_transaction() { } #[test] -#[ignore = "T-110: not implemented yet"] fn an_offset_valid_only_before_the_transaction_is_rejected() { let (mut set, ids) = set_of(&["abcdef"]); let a = ids[0]; @@ -165,7 +160,6 @@ fn an_offset_valid_only_before_the_transaction_is_rejected() { } #[test] -#[ignore = "T-110: not implemented yet"] fn amend_can_change_text_the_transaction_put_there() { // Delete part of what the transaction inserted. let (mut set, ids) = set_of(&["ac"]); @@ -205,7 +199,6 @@ fn amend_can_change_text_the_transaction_put_there() { } #[test] -#[ignore = "T-110: not implemented yet"] fn amend_with_edits_far_from_the_transaction() { let (mut set, ids) = set_of(&["one two three"]); let a = ids[0]; @@ -230,7 +223,6 @@ fn amend_with_edits_far_from_the_transaction() { } #[test] -#[ignore = "T-110: not implemented yet"] fn inserts_at_one_offset_keep_their_order() { let (mut set, ids) = set_of(&["ab"]); let a = ids[0]; @@ -251,7 +243,6 @@ fn inserts_at_one_offset_keep_their_order() { } #[test] -#[ignore = "T-110: not implemented yet"] fn wide_characters_and_crlf() { let (mut set, ids) = set_of(&["é中\r\n😀"]); let a = ids[0]; @@ -282,7 +273,6 @@ fn wide_characters_and_crlf() { } #[test] -#[ignore = "T-110: not implemented yet"] fn a_no_op_amend_is_accepted() { let (mut set, ids) = set_of(&["abc"]); let a = ids[0]; @@ -299,7 +289,6 @@ fn a_no_op_amend_is_accepted() { } #[test] -#[ignore = "T-110: not implemented yet"] fn amend_after_redo() { let (mut set, ids) = set_of(&["a"]); let a = ids[0]; @@ -315,7 +304,6 @@ fn amend_after_redo() { } #[test] -#[ignore = "T-110: not implemented yet"] fn amend_an_earlier_transaction_once_the_later_one_is_undone() { let (mut set, ids) = set_of(&["a"]); let a = ids[0]; @@ -333,7 +321,6 @@ fn amend_an_earlier_transaction_once_the_later_one_is_undone() { // Several buffers. #[test] -#[ignore = "T-110: not implemented yet"] fn one_undo_reverts_an_amended_transaction_in_every_buffer() { let (mut set, ids) = set_of(&["fn old() {}", "old();", "other"]); let (a, b, c) = (ids[0], ids[1], ids[2]); @@ -358,7 +345,6 @@ fn one_undo_reverts_an_amended_transaction_in_every_buffer() { } #[test] -#[ignore = "T-110: not implemented yet"] fn amend_may_touch_some_of_the_transactions_buffers() { let (mut set, ids) = set_of(&["a", "b", "c"]); let t = apply( @@ -380,7 +366,6 @@ fn amend_may_touch_some_of_the_transactions_buffers() { } #[test] -#[ignore = "T-110: not implemented yet"] fn a_later_transaction_in_another_buffer_blocks_only_that_buffer() { let (mut set, ids) = set_of(&["a", "b"]); let (a, b) = (ids[0], ids[1]); @@ -410,7 +395,6 @@ fn a_later_transaction_in_another_buffer_blocks_only_that_buffer() { // When an amend is refused. #[test] -#[ignore = "T-110: not implemented yet"] fn a_later_transaction_on_top_refuses_the_amend() { let (mut set, ids) = set_of(&["a", "b"]); let (a, b) = (ids[0], ids[1]); @@ -437,7 +421,6 @@ fn a_later_transaction_on_top_refuses_the_amend() { } #[test] -#[ignore = "T-110: not implemented yet"] fn an_undone_transaction_is_refused() { let (mut set, ids) = set_of(&["a"]); let a = ids[0]; @@ -456,7 +439,6 @@ fn an_undone_transaction_is_refused() { } #[test] -#[ignore = "T-110: not implemented yet"] fn a_dropped_transaction_is_unknown() { let (mut set, ids) = set_of(&["a"]); let a = ids[0]; @@ -474,7 +456,6 @@ fn a_dropped_transaction_is_unknown() { } #[test] -#[ignore = "T-110: not implemented yet"] fn a_transaction_from_another_set_is_unknown() { let (mut other, other_ids) = set_of(&["x"]); let t = apply(&mut other, vec![Edit::insert(other_ids[0], 1, "y")]); @@ -489,7 +470,6 @@ fn a_transaction_from_another_set_is_unknown() { } #[test] -#[ignore = "T-110: not implemented yet"] fn an_untouched_buffer_is_refused() { let (mut set, ids) = set_of(&["a", "b"]); let (a, b) = (ids[0], ids[1]); @@ -507,7 +487,6 @@ fn an_untouched_buffer_is_refused() { } #[test] -#[ignore = "T-110: not implemented yet"] fn apply_errors_come_wrapped() { let (mut set, ids) = set_of(&["héllo"]); let a = ids[0]; @@ -590,7 +569,6 @@ fn apply_errors_come_wrapped() { } #[test] -#[ignore = "T-110: not implemented yet"] fn invalid_ranges_match_slice_on_the_text_after_the_transaction() { let (mut set, ids) = set_of(&["a😀b"]); let a = ids[0]; @@ -615,7 +593,6 @@ fn invalid_ranges_match_slice_on_the_text_after_the_transaction() { } #[test] -#[ignore = "T-110: not implemented yet"] fn the_checks_run_in_order() { let (mut set, ids) = set_of(&["a", "b", "c"]); let (a, b, c) = (ids[0], ids[1], ids[2]); @@ -735,7 +712,6 @@ fn the_checks_run_in_order() { // Redo. #[test] -#[ignore = "T-110: not implemented yet"] fn amend_drops_redo_like_apply() { let (mut set, ids) = set_of(&["a", "b", "c", "d"]); let (a, b, c, d) = (ids[0], ids[1], ids[2], ids[3]); @@ -775,7 +751,6 @@ fn amend_drops_redo_like_apply() { } #[test] -#[ignore = "T-110: not implemented yet"] fn amend_keeps_redo_in_buffers_it_does_not_touch() { // `t1` touched `a` and `b`, but the amend touches only `a`, so `b`'s // redo history stays. @@ -794,7 +769,6 @@ fn amend_keeps_redo_in_buffers_it_does_not_touch() { } #[test] -#[ignore = "T-110: not implemented yet"] fn a_no_op_amend_still_drops_redo() { let (mut set, ids) = set_of(&["a"]); let a = ids[0]; @@ -807,7 +781,6 @@ fn a_no_op_amend_still_drops_redo() { } #[test] -#[ignore = "T-110: not implemented yet"] fn a_failed_amend_keeps_redo() { let (mut set, ids) = set_of(&["a", "b"]); let (a, b) = (ids[0], ids[1]); @@ -834,7 +807,6 @@ fn a_failed_amend_keeps_redo() { // Versions and snapshots. #[test] -#[ignore = "T-110: not implemented yet"] fn amend_bumps_each_touched_buffer_once() { let (mut set, ids) = set_of(&["a", "b", "c", "d"]); let t = apply( @@ -871,7 +843,6 @@ fn amend_bumps_each_touched_buffer_once() { } #[test] -#[ignore = "T-110: not implemented yet"] fn a_failed_amend_keeps_every_version() { let (mut set, ids) = set_of(&["a", "b"]); let (a, b) = (ids[0], ids[1]); @@ -906,7 +877,6 @@ fn a_failed_amend_keeps_every_version() { } #[test] -#[ignore = "T-110: not implemented yet"] fn snapshots_go_stale_on_amend() { let (mut set, ids) = set_of(&["hello", "world"]); let (a, b) = (ids[0], ids[1]); diff --git a/crates/helm-text/tests/amend_properties.rs b/crates/helm-text/tests/amend_properties.rs index 1ce9632..e371e5b 100644 --- a/crates/helm-text/tests/amend_properties.rs +++ b/crates/helm-text/tests/amend_properties.rs @@ -548,7 +548,6 @@ proptest! { /// starting texts, as undoing every separate transaction does, and one /// redo brings back the final texts. #[test] - #[ignore = "T-110: not implemented yet"] fn a_chain_of_amends_matches_separate_transactions( starts in starts(), first in first_specs(), @@ -629,7 +628,6 @@ proptest! { /// Random applies, amends of the latest or any earlier transaction, /// undos and redos: every result, text and version matches the oracle. #[test] - #[ignore = "T-110: not implemented yet"] fn random_histories_with_amends_match_the_oracle(starts in starts(), ops in ops()) { let mut case = Case::new(&starts); for op in &ops { @@ -640,7 +638,6 @@ proptest! { /// After random histories with amends, undoing whatever isn't blocked, /// over and over, returns every buffer to its starting text. #[test] - #[ignore = "T-110: not implemented yet"] fn undoing_everything_after_amends_restores_every_buffer( starts in starts(), ops in ops(),