diff --git a/Cargo.lock b/Cargo.lock index 8f523c3ecaf2..2a1dbf223351 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "bumpalo" version = "3.17.0" @@ -41,6 +47,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -101,25 +118,22 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "criterion" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", - "itertools", + "itertools 0.13.0", "num-traits", - "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -132,7 +146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -172,8 +186,11 @@ version = "1.1.0" dependencies = [ "criterion", "libc", + "serde", + "serde_json", "windows-sys", "winresource", + "zstd", ] [[package]] @@ -188,6 +205,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + [[package]] name = "half" version = "2.6.0" @@ -204,12 +233,6 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" -[[package]] -name = "hermit-abi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" - [[package]] name = "indexmap" version = "2.9.0" @@ -221,21 +244,19 @@ dependencies = [ ] [[package]] -name = "is-terminal" -version = "0.4.16" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "hermit-abi", - "libc", - "windows-sys", + "either", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -246,6 +267,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -295,6 +326,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plotters" version = "0.3.7" @@ -341,6 +378,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rayon" version = "1.10.0" @@ -452,6 +495,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "syn" version = "2.0.101" @@ -536,6 +585,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -704,3 +762,40 @@ dependencies = [ "toml", "version_check", ] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 14793a708125..58879051ceec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,4 +54,7 @@ features = [ ] [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } +criterion = { version = "0.6", features = ["html_reports"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +zstd = { version = "0.13", default-features = false } diff --git a/assets/editing-traces/README.md b/assets/editing-traces/README.md new file mode 100644 index 000000000000..d62639c62265 --- /dev/null +++ b/assets/editing-traces/README.md @@ -0,0 +1,5 @@ +# editing-traces + +This directory contains Seph Gentle's ASCII-only `rustcode` editing traces from: https://github.com/josephg/editing-traces + +The trace was provided under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. diff --git a/assets/editing-traces/rustcode.json.zst b/assets/editing-traces/rustcode.json.zst new file mode 100644 index 000000000000..fc29c1623218 Binary files /dev/null and b/assets/editing-traces/rustcode.json.zst differ diff --git a/benches/lib.rs b/benches/lib.rs index 1131f34e62d4..abc9ee1bdc58 100644 --- a/benches/lib.rs +++ b/benches/lib.rs @@ -2,12 +2,109 @@ // Licensed under the MIT License. use std::hint::black_box; +use std::io::Cursor; use std::mem; use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use edit::helpers::*; use edit::simd::MemsetSafe; -use edit::{hash, oklab, simd, unicode}; +use edit::{arena, buffer, hash, oklab, simd, unicode}; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct EditingTracePatch(pub usize, pub usize, pub String); + +#[derive(Deserialize)] +pub struct EditingTraceTransaction { + pub patches: Vec, +} + +#[derive(Deserialize)] +pub struct EditingTraceData { + #[serde(rename = "startContent")] + pub start_content: String, + #[serde(rename = "endContent")] + pub end_content: String, + pub txns: Vec, +} + +fn bench_buffer(c: &mut Criterion) { + let data = include_bytes!("../assets/editing-traces/rustcode.json.zst"); + let data = zstd::decode_all(Cursor::new(data)).unwrap(); + let data: EditingTraceData = serde_json::from_slice(&data).unwrap(); + let mut patches_with_coords = Vec::new(); + + { + let mut tb = buffer::TextBuffer::new(false).unwrap(); + tb.set_crlf(false); + tb.write(data.start_content.as_bytes(), true); + + for t in &data.txns { + for p in &t.patches { + tb.cursor_move_to_offset(p.0); + let beg = tb.cursor_logical_pos(); + + tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType); + + tb.write(p.2.as_bytes(), true); + patches_with_coords.push((beg, p.1 as CoordType, p.2.clone())); + } + } + + let mut actual = String::new(); + tb.save_as_string(&mut actual); + assert_eq!(actual, data.end_content); + } + + let bench_gap_buffer = || { + let mut buf = buffer::GapBuffer::new(false).unwrap(); + buf.replace(0..usize::MAX, data.start_content.as_bytes()); + + for t in &data.txns { + for p in &t.patches { + buf.replace(p.0..p.0 + p.1, p.2.as_bytes()); + } + } + + buf + }; + + let bench_text_buffer = || { + let mut tb = buffer::TextBuffer::new(false).unwrap(); + tb.set_crlf(false); + tb.write(data.start_content.as_bytes(), true); + + for p in &patches_with_coords { + tb.cursor_move_to_logical(p.0); + tb.delete(buffer::CursorMovement::Grapheme, p.1); + tb.write(p.2.as_bytes(), true); + } + + tb + }; + + // Sanity check: If this fails, the implementation is incorrect. + { + let buf = bench_gap_buffer(); + let mut actual = Vec::new(); + buf.extract_raw(0..usize::MAX, &mut actual, 0); + assert_eq!(actual, data.end_content.as_bytes()); + } + { + let mut tb = bench_text_buffer(); + let mut actual = String::new(); + tb.save_as_string(&mut actual); + assert_eq!(actual, data.end_content); + } + + c.benchmark_group("buffer") + .bench_function(BenchmarkId::new("GapBuffer", "rustcode"), |b| { + b.iter(bench_gap_buffer); + }) + .bench_function(BenchmarkId::new("TextBuffer", "rustcode"), |b| { + b.iter(bench_text_buffer); + }); +} fn bench_hash(c: &mut Criterion) { c.benchmark_group("hash") @@ -104,6 +201,9 @@ fn bench_unicode(c: &mut Criterion) { } fn bench(c: &mut Criterion) { + arena::init(128 * MEBI).unwrap(); + + bench_buffer(c); bench_hash(c); bench_oklab(c); bench_simd_memchr2(c); diff --git a/src/buffer/gap_buffer.rs b/src/buffer/gap_buffer.rs index 5ed12f794422..d19da1b0fdef 100644 --- a/src/buffer/gap_buffer.rs +++ b/src/buffer/gap_buffer.rs @@ -245,17 +245,9 @@ impl GapBuffer { self.text_length = 0; } - pub fn extract_raw( - &self, - mut beg: usize, - mut end: usize, - out: &mut Vec, - mut out_off: usize, - ) { - debug_assert!(beg <= end && end <= self.text_length); - - end = end.min(self.text_length); - beg = beg.min(end); + pub fn extract_raw(&self, range: Range, out: &mut Vec, mut out_off: usize) { + let end = range.end.min(self.text_length); + let mut beg = range.start.min(end); out_off = out_off.min(out.len()); if beg >= end { diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index 4676fb945b93..12a61be318a6 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -34,7 +34,7 @@ use std::ops::Range; use std::rc::Rc; use std::str; -use gap_buffer::GapBuffer; +pub use gap_buffer::GapBuffer; use crate::arena::{ArenaString, scratch_arena}; use crate::cell::SemiRefCell; @@ -314,6 +314,11 @@ impl TextBuffer { self.newlines_are_crlf } + /// Changes the newline type without normalizing the document. + pub fn set_crlf(&mut self, crlf: bool) { + self.newlines_are_crlf = crlf; + } + /// Changes the newline type used in the document. /// /// NOTE: Cannot be undone. @@ -920,6 +925,11 @@ impl TextBuffer { self.selection_generation } + /// Moves the cursor by `offset` and updates the selection to contain it. + pub fn selection_update_offset(&mut self, offset: usize) { + self.set_cursor_for_selection(self.cursor_move_to_offset_internal(self.cursor, offset)); + } + /// Moves the cursor to `visual_pos` and updates the selection to contain it. pub fn selection_update_visual(&mut self, visual_pos: Point) { self.set_cursor_for_selection(self.cursor_move_to_visual_internal(self.cursor, visual_pos)); @@ -1935,7 +1945,9 @@ impl TextBuffer { /// The selection is cleared after the call. /// Deletes characters from the buffer based on a delta from the cursor. pub fn delete(&mut self, granularity: CursorMovement, delta: CoordType) { - debug_assert!(delta == -1 || delta == 1); + if delta == 0 { + return; + } let mut beg; let mut end; @@ -1943,8 +1955,8 @@ impl TextBuffer { if let Some(r) = self.selection_range_internal(false) { (beg, end) = r; } else { - if (delta == -1 && self.cursor.offset == 0) - || (delta == 1 && self.cursor.offset >= self.text_length()) + if (delta < 0 && self.cursor.offset == 0) + || (delta > 0 && self.cursor.offset >= self.text_length()) { // Nothing to delete. return; @@ -2014,7 +2026,7 @@ impl TextBuffer { let end = self.cursor_move_to_logical_internal(beg, Point { x: CoordType::MAX, y: end.y }); let mut replacement = Vec::new(); - self.buffer.extract_raw(beg.offset, end.offset, &mut replacement, 0); + self.buffer.extract_raw(beg.offset..end.offset, &mut replacement, 0); let initial_len = replacement.len(); let mut offset = 0; @@ -2078,7 +2090,7 @@ impl TextBuffer { }; let mut out = Vec::new(); - self.buffer.extract_raw(beg.offset, end.offset, &mut out, 0); + self.buffer.extract_raw(beg.offset..end.offset, &mut out, 0); if delete && !out.is_empty() { self.edit_begin(HistoryType::Delete, beg); @@ -2225,7 +2237,7 @@ impl TextBuffer { // Copy the deleted portion into the undo entry. let deleted = &mut undo.deleted; - self.buffer.extract_raw(off, to.offset, deleted, out_off); + self.buffer.extract_raw(off..to.offset, deleted, out_off); // Delete the portion from the buffer by enlarging the gap. let count = to.offset - off;