mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 17:12:52 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b63f0c34a3 |
+128
-21
@@ -78,45 +78,57 @@ pub fn insert(body: &mut TextBody, pos: Pos, text: &str, props: Option<RunProps>
|
||||
if body.paragraphs.is_empty() {
|
||||
body.paragraphs.push(Paragraph::default());
|
||||
}
|
||||
let (mut pi, mut ch) = clamp(body, pos);
|
||||
let props = props.unwrap_or_else(|| props_at(body, (pi, ch)));
|
||||
let (pi0, mut ch) = clamp(body, pos);
|
||||
let props = props.unwrap_or_else(|| props_at(body, (pi0, ch)));
|
||||
let text = text.replace("\r\n", "\n").replace('\r', "\n");
|
||||
// Work on the paragraph being edited apart from the body, so each new paragraph is pushed to a
|
||||
// list and the body's vector is spliced once instead of shifted for every `\n`.
|
||||
let Some(slot) = body.paragraphs.get_mut(pi0) else { return (pi0, ch) };
|
||||
let mut cur = std::mem::take(slot);
|
||||
let mut done: Vec<Paragraph> = Vec::new();
|
||||
let mut first = true;
|
||||
for part in text.split('\n') {
|
||||
if !first {
|
||||
// Split paragraph pi at ch.
|
||||
let Some(p) = body.paragraphs.get_mut(pi) else { break };
|
||||
let idx = split_at(p, ch);
|
||||
let tail: Vec<Run> = p.runs.drain(idx..).collect();
|
||||
let newp =
|
||||
Paragraph { level: p.level, props: p.props.clone(), runs: tail, def_props: p.def_props.clone(), end_props: p.end_props.clone() };
|
||||
p.end_props = props.clone();
|
||||
body.paragraphs.insert(pi + 1, newp);
|
||||
pi += 1;
|
||||
// Split the current paragraph at ch.
|
||||
let idx = split_at(&mut cur, ch);
|
||||
let tail: Vec<Run> = cur.runs.drain(idx..).collect();
|
||||
let newp = Paragraph {
|
||||
level: cur.level,
|
||||
props: cur.props.clone(),
|
||||
runs: tail,
|
||||
def_props: cur.def_props.clone(),
|
||||
end_props: cur.end_props.clone(),
|
||||
};
|
||||
cur.end_props = props.clone();
|
||||
done.push(std::mem::replace(&mut cur, newp));
|
||||
ch = 0;
|
||||
}
|
||||
first = false;
|
||||
// The run index just after the last insertion: the next one lands there with no rescan.
|
||||
let mut next: Option<usize> = None;
|
||||
for (k, seg) in part.split('\u{b}').enumerate() {
|
||||
let Some(p) = body.paragraphs.get_mut(pi) else { break };
|
||||
if k > 0 {
|
||||
let idx = split_at(p, ch);
|
||||
p.runs.insert(idx, Run { text: String::new(), props: props.clone(), kind: RunKind::Break });
|
||||
let idx = next.unwrap_or_else(|| split_at(&mut cur, ch));
|
||||
cur.runs.insert(idx, Run { text: String::new(), props: props.clone(), kind: RunKind::Break });
|
||||
ch += 1;
|
||||
next = Some(idx + 1);
|
||||
}
|
||||
if seg.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let idx = split_at(p, ch);
|
||||
p.runs.insert(idx, Run::with(seg, props.clone()));
|
||||
let idx = next.unwrap_or_else(|| split_at(&mut cur, ch));
|
||||
cur.runs.insert(idx, Run::with(seg, props.clone()));
|
||||
ch += seg.chars().count();
|
||||
next = Some(idx + 1);
|
||||
}
|
||||
if let Some(p) = body.paragraphs.get_mut(pi) {
|
||||
p.normalize();
|
||||
if p.runs.is_empty() {
|
||||
p.end_props = props.clone();
|
||||
}
|
||||
cur.normalize();
|
||||
if cur.runs.is_empty() {
|
||||
cur.end_props = props.clone();
|
||||
}
|
||||
}
|
||||
let pi = pi0 + done.len();
|
||||
done.push(cur);
|
||||
body.paragraphs.splice(pi0..=pi0, done);
|
||||
(pi, ch)
|
||||
}
|
||||
|
||||
@@ -252,8 +264,16 @@ pub fn slice(body: &TextBody, a: Pos, b: Pos) -> TextBody {
|
||||
/// Insert a formatted body at `pos` (paste): first paragraph merges into the current one.
|
||||
pub fn insert_body(body: &mut TextBody, pos: Pos, src: &TextBody) -> Pos {
|
||||
let mut caret = clamp(body, pos);
|
||||
// Plain text runs go straight in at the run index after the previous one and the paragraph is
|
||||
// normalized once, not rescanned and merged for every source run.
|
||||
let mut next: Option<usize> = None;
|
||||
let mut dirty = false;
|
||||
for (k, sp) in src.paragraphs.iter().enumerate() {
|
||||
if k > 0 {
|
||||
if let (true, Some(p)) = (dirty, body.paragraphs.get_mut(caret.0)) {
|
||||
p.normalize();
|
||||
}
|
||||
(next, dirty) = (None, false);
|
||||
caret = insert(body, caret, "\n", None);
|
||||
if let Some(p) = body.paragraphs.get_mut(caret.0) {
|
||||
p.level = sp.level;
|
||||
@@ -261,6 +281,22 @@ pub fn insert_body(body: &mut TextBody, pos: Pos, src: &TextBody) -> Pos {
|
||||
}
|
||||
}
|
||||
for r in &sp.runs {
|
||||
if r.kind == RunKind::Text
|
||||
&& !r.text.is_empty()
|
||||
&& !r.text.contains(['\n', '\r', '\u{b}'])
|
||||
&& let Some(p) = body.paragraphs.get_mut(caret.0)
|
||||
{
|
||||
let idx = next.unwrap_or_else(|| split_at(p, caret.1));
|
||||
p.runs.insert(idx, Run::with(&r.text, r.props.clone()));
|
||||
caret.1 += r.text.chars().count();
|
||||
(next, dirty) = (Some(idx + 1), true);
|
||||
continue;
|
||||
}
|
||||
// Anything else takes the general path, after the pending runs are merged.
|
||||
if let (true, Some(p)) = (dirty, body.paragraphs.get_mut(caret.0)) {
|
||||
p.normalize();
|
||||
}
|
||||
(next, dirty) = (None, false);
|
||||
match r.kind {
|
||||
RunKind::Text => caret = insert(body, caret, &r.text, Some(r.props.clone())),
|
||||
RunKind::Break => caret = insert(body, caret, "\u{b}", Some(r.props.clone())),
|
||||
@@ -274,6 +310,9 @@ pub fn insert_body(body: &mut TextBody, pos: Pos, src: &TextBody) -> Pos {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (true, Some(p)) = (dirty, body.paragraphs.get_mut(caret.0)) {
|
||||
p.normalize();
|
||||
}
|
||||
caret
|
||||
}
|
||||
|
||||
@@ -568,6 +607,74 @@ mod tests {
|
||||
assert_eq!(caret, (0, 5));
|
||||
}
|
||||
|
||||
/// The paste as it ran before the fast path: one `insert` (and normalize) per source run.
|
||||
fn insert_body_by_runs(body: &mut TextBody, pos: Pos, src: &TextBody) -> Pos {
|
||||
let mut caret = clamp(body, pos);
|
||||
for (k, sp) in src.paragraphs.iter().enumerate() {
|
||||
if k > 0 {
|
||||
caret = insert(body, caret, "\n", None);
|
||||
if let Some(p) = body.paragraphs.get_mut(caret.0) {
|
||||
p.level = sp.level;
|
||||
p.props = sp.props.clone();
|
||||
}
|
||||
}
|
||||
for r in &sp.runs {
|
||||
match r.kind {
|
||||
RunKind::Text => caret = insert(body, caret, &r.text, Some(r.props.clone())),
|
||||
RunKind::Break => caret = insert(body, caret, "\u{b}", Some(r.props.clone())),
|
||||
_ => {
|
||||
if let Some(p) = body.paragraphs.get_mut(caret.0) {
|
||||
let idx = split_at(p, caret.1);
|
||||
p.runs.insert(idx, r.clone());
|
||||
caret.1 += r.char_len();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
caret
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_matches_inserting_run_by_run() {
|
||||
let bold = |t: &str| Run::with(t, RunProps { bold: Some(true), ..Default::default() });
|
||||
let mut src = body("");
|
||||
src.paragraphs = vec![
|
||||
Paragraph {
|
||||
runs: vec![Run::with("ab", RunProps::default()), Run::with("cd", RunProps::default()), bold("ef"), bold("g")],
|
||||
..Default::default()
|
||||
},
|
||||
Paragraph {
|
||||
runs: vec![
|
||||
bold("h"),
|
||||
Run { text: String::new(), props: RunProps::default(), kind: RunKind::Break },
|
||||
Run::with("i", RunProps::default()),
|
||||
],
|
||||
level: 1,
|
||||
..Default::default()
|
||||
},
|
||||
Paragraph { runs: vec![Run::with("jk", RunProps::default())], ..Default::default() },
|
||||
];
|
||||
for at in [0, 1, 3, 6] {
|
||||
let mut dest = body("XYZabc");
|
||||
format(&mut dest, (0, 2), (0, 4), &|p| p.italic = Some(true));
|
||||
let mut want = dest.clone();
|
||||
let want_caret = insert_body_by_runs(&mut want, (0, at), &src);
|
||||
let caret = insert_body(&mut dest, (0, at), &src);
|
||||
assert_eq!(caret, want_caret, "caret at {at}");
|
||||
assert_eq!(format!("{:?}", dest.paragraphs), format!("{:?}", want.paragraphs), "paste at {at}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_insert_splits_into_paragraphs() {
|
||||
let mut b = body("abcd");
|
||||
let c = insert(&mut b, (0, 2), "x\ny\u{b}z\n\nw", None);
|
||||
assert_eq!(c, (3, 1));
|
||||
let texts: Vec<String> = b.paragraphs.iter().map(Paragraph::text).collect();
|
||||
assert_eq!(texts, ["abx", "y\u{b}z", "", "wcd"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn words_and_moves() {
|
||||
let b = body("Hello brave world");
|
||||
|
||||
@@ -415,11 +415,6 @@ impl CaptureScanner {
|
||||
}
|
||||
|
||||
fn read_parts(src: &(impl ByteSource + ?Sized), parts: &[(u64, u32)]) -> Result<Vec<u8>> {
|
||||
// Ranges come from a scan of some source; check them against this one before sizing a buffer.
|
||||
let len = src.len();
|
||||
if parts.iter().any(|&(o, n)| o.checked_add(u64::from(n)).is_none_or(|end| end > len)) {
|
||||
return Err(Error::Invalid("packet range lies outside the source".into()));
|
||||
}
|
||||
let mut v = Vec::with_capacity(parts.iter().map(|p| p.1 as usize).sum());
|
||||
for &(o, n) in parts {
|
||||
let a = v.len();
|
||||
|
||||
@@ -274,13 +274,3 @@ fn a_budget_too_small_to_search_is_spent() {
|
||||
let f = opus_file(6, 312, 0);
|
||||
assert_eq!(scanner.find(&f, 0, f.len() as u64).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_packet_checks_its_ranges_against_the_source_before_allocating() {
|
||||
let f = opus_file(10, 312, 500);
|
||||
let o = open(&f).unwrap();
|
||||
let s = o.stream_of(Codec::Opus).unwrap();
|
||||
// The last packet lies past the end of a source cut short: an error, not a read of missing bytes.
|
||||
let short = &f[..f.len() / 2];
|
||||
assert!(matches!(o.read_packet(&short, s, 9), Err(Error::Invalid(_))));
|
||||
}
|
||||
|
||||
+1
-7
@@ -18,13 +18,7 @@ fn walk(dir: &Path, c: &mut Counts) {
|
||||
let p = e.path();
|
||||
let name = e.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
// `file_type` does not follow links, so a directory link is not walked (and aliases are
|
||||
// not counted twice).
|
||||
let Ok(kind) = e.file_type() else { continue };
|
||||
if kind.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
if kind.is_dir() {
|
||||
if p.is_dir() {
|
||||
// Nested fuzz workspaces and build output are not part of the crate.
|
||||
if name != "target" && name != "fuzz" && !name.starts_with('.') {
|
||||
walk(&p, c);
|
||||
|
||||
Reference in New Issue
Block a user