From 70d9f7475837dff2cbc0e8de3b271ce6f46192f1 Mon Sep 17 00:00:00 2001 From: shubhook Date: Thu, 15 Oct 2026 18:35:11 +0530 Subject: [PATCH] text: implement BufferSet::amend by composing splices amend folds new edits into an applied transaction's stored change for each buffer they touch. It walks the change's splices and the new edits together by position and merges any run that overlaps or touches into one splice, so a typing run stays one splice and undoes in one step. apply and amend share one check helper, and a record now knows whether it is undone. Adds the amend/type_100_chars_undo_10mb benchmark. Refs: T-110, ED-2 --- crates/helm-bench/Cargo.toml | 4 + crates/helm-bench/benches/amend.rs | 86 ++++++ crates/helm-text/src/buffer_set.rs | 329 +++++++++++++++++---- crates/helm-text/tests/amend.rs | 30 -- crates/helm-text/tests/amend_properties.rs | 3 - 5 files changed, 359 insertions(+), 93 deletions(-) create mode 100644 crates/helm-bench/benches/amend.rs diff --git a/crates/helm-bench/Cargo.toml b/crates/helm-bench/Cargo.toml index 63947f0..46c53f3 100644 --- a/crates/helm-bench/Cargo.toml +++ b/crates/helm-bench/Cargo.toml @@ -48,3 +48,7 @@ harness = false name = "editor" 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(),