- 1
//! L5: keeping some of a draft's changes (docs/design/72, P3). - 2
//! - 3
//! A draft is its source plus the ops that produced it. When a person keeps - 4
//! some changes and not others, the kept ops are replayed against the same - 5
//! source by the same engine, so the narrower draft is produced and checked - 6
//! exactly as the original was. Nothing is ever patched out of a finished - 7
//! file. - 8
//! - 9
//! The unit of choice is an atom: one op, except that `set_cells` splits - 10
//! into one atom per cell. An atom that names an anchor an earlier op minted - 11
//! (a new paragraph or slide), or a sheet an earlier op added, requires that - 12
//! op's atom. - 13
- 14
use std::collections::{BTreeMap, HashMap}; - 15
use std::io::Cursor; - 16
- 17
use serde::Serialize; - 18
- 19
use crate::diff::{Change, diff}; - 20
use crate::edit::{self, Applied, EditContext, EditError, OfficeOp}; - 21
use crate::read::{self, Document}; - 22
use crate::{Format, Limits}; - 23
- 24
/// Above this many ops a draft is accepted or rejected as a whole: every - 25
/// choice costs one replay per op. - 26
pub const MAX_OPS: usize = 100; - 27
- 28
/// One change a person can keep or leave out. - 29
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 30
pub struct Choice { - 31
/// `"3"` for op 3, `"3:B4"` for one cell of a `set_cells` op. - 32
pub id: String, - 33
pub label: String, - 34
/// Choices this one cannot be kept without. - 35
pub requires: Vec<String>, - 36
/// What this change does, compared with the state before it. - 37
pub changes: Vec<Change>, - 38
} - 39
- 40
/// The choices a draft offers, each with the changes it makes. - 41
/// - 42
/// Fails, with a reason a person can read, when the recorded ops do not - 43
/// reproduce `draft` (it was changed after it was written, or `source` is - 44
/// not the file it was made from); such a draft can only be taken whole. - 45
pub fn choices( - 46
source: &[u8], - 47
ops: &[OfficeOp], - 48
context: &EditContext, - 49
limits: Limits, - 50
target: Option<Format>, - 51
draft: &Document, - 52
) -> Result<Vec<Choice>, String> { - 53
if ops.len() > MAX_OPS { - 54
return Err(format!( - 55
"the draft was made by {} edits, and choosing among more than {MAX_OPS} is not offered", - 56
ops.len() - 57
)); - 58
} - 59
let mut previous = read::read(Cursor::new(source.to_vec()), limits) - 60
.map_err(|error| format!("the file the draft was made from does not read: {error}"))?; - 61
let mut state = source.to_vec(); - 62
let mut created = Vec::with_capacity(ops.len()); - 63
let mut changes = Vec::with_capacity(ops.len()); - 64
for (index, op) in ops.iter().enumerate() { - 65
let applied = edit::apply(&state, std::slice::from_ref(op), context, limits, target) - 66
.map_err(|error| { - 67
format!( - 68
"replaying edit {} of the draft failed: {}", - 69
index + 1, - 70
error.message - 71
) - 72
})?; - 73
changes.push(diff(Some(&previous), &applied.document).changes); - 74
created.push( - 75
applied - 76
.results - 77
.first() - 78
.map(|result| result.created.clone()) - 79
.unwrap_or_default(), - 80
); - 81
state = applied.bytes; - 82
previous = applied.document; - 83
} - 84
if !diff(Some(&previous), draft).is_empty() { - 85
return Err( - 86
"the draft is not what its recorded edits produce: it changed after it was written, or the file it was made from changed" - 87
.into(), - 88
); - 89
} - 90
let mut choices: Vec<Choice> = Vec::new(); - 91
let mut op_of: Vec<usize> = Vec::new(); - 92
for (index, (op, op_changes)) in ops.iter().zip(changes).enumerate() { - 93
match split_cells(op, &op_changes) { - 94
Some(cells) => { - 95
for (cell, cell_changes) in cells { - 96
choices.push(Choice { - 97
id: format!("{index}:{cell}"), - 98
label: label(op, Some(&cell)), - 99
requires: Vec::new(), - 100
changes: cell_changes, - 101
}); - 102
op_of.push(index); - 103
} - 104
} - 105
None => { - 106
choices.push(Choice { - 107
id: index.to_string(), - 108
label: label(op, None), - 109
requires: Vec::new(), - 110
changes: op_changes, - 111
}); - 112
op_of.push(index); - 113
} - 114
} - 115
} - 116
for position in 0..choices.len() { - 117
let needed = requirements(ops, &created, op_of[position], context); - 118
choices[position].requires = choices - 119
.iter() - 120
.zip(&op_of) - 121
.filter(|(_, op)| needed.contains(op)) - 122
.map(|(choice, _)| choice.id.clone()) - 123
.collect(); - 124
} - 125
Ok(choices) - 126
} - 127
- 128
/// Replays the choices in `keep` (ids from [`choices`]) against `source`. - 129
/// A kept choice whose requirement was left out fails, naming both, and so - 130
/// does a `draft` the ops do not reproduce: narrowing it would silently drop - 131
/// whatever changed it afterwards. - 132
pub fn narrow( - 133
source: &[u8], - 134
ops: &[OfficeOp], - 135
keep: &[String], - 136
context: &EditContext, - 137
limits: Limits, - 138
target: Option<Format>, - 139
draft: &Document, - 140
) -> Result<Applied, EditError> { - 141
if ops.len() > MAX_OPS { - 142
return edit_fail(format!( - 143
"the draft was made by {} edits, and choosing among more than {MAX_OPS} is not offered", - 144
ops.len() - 145
)); - 146
} - 147
let mut whole = vec![false; ops.len()]; - 148
let mut cells: Vec<Vec<String>> = vec![Vec::new(); ops.len()]; - 149
for id in keep { - 150
let (index, cell) = match id.split_once(':') { - 151
Some((index, cell)) => (index, Some(cell)), - 152
None => (id.as_str(), None), - 153
}; - 154
let Some(index) = index - 155
.parse::<usize>() - 156
.ok() - 157
.filter(|index| *index < ops.len()) - 158
else { - 159
return edit_fail(format!("{id:?} is not a change in this draft")); - 160
}; - 161
match cell { - 162
None => whole[index] = true, - 163
Some(cell) => { - 164
let known = matches!(&ops[index], OfficeOp::SetCells { cells, .. } - 165
if cells.keys().any(|key| key.eq_ignore_ascii_case(cell))); - 166
if !known { - 167
return edit_fail(format!("{id:?} is not a change in this draft")); - 168
} - 169
cells[index].push(cell.to_ascii_uppercase()); - 170
} - 171
} - 172
} - 173
let kept: Vec<(usize, OfficeOp)> = ops - 174
.iter() - 175
.enumerate() - 176
.filter_map(|(index, op)| { - 177
if whole[index] { - 178
return Some((index, op.clone())); - 179
} - 180
match op { - 181
OfficeOp::SetCells { sheet, cells: all } if !cells[index].is_empty() => Some(( - 182
index, - 183
OfficeOp::SetCells { - 184
sheet: sheet.clone(), - 185
cells: all - 186
.iter() - 187
.filter(|(key, _)| cells[index].contains(&key.to_ascii_uppercase())) - 188
.map(|(key, value)| (key.clone(), value.clone())) - 189
.collect(), - 190
}, - 191
)), - 192
_ => None, - 193
} - 194
}) - 195
.collect(); - 196
if kept.is_empty() { - 197
return edit_fail("no change was kept; reject the draft instead"); - 198
} - 199
let original = edit::apply(source, ops, context, limits, target)?; - 200
if !diff(Some(&original.document), draft).is_empty() { - 201
return edit_fail( - 202
"the draft is not what its recorded edits produce, so it can only be accepted or rejected whole", - 203
); - 204
} - 205
let created: Vec<Vec<String>> = original - 206
.results - 207
.iter() - 208
.map(|result| result.created.clone()) - 209
.collect(); - 210
let kept_ops: Vec<usize> = kept.iter().map(|(index, _)| *index).collect(); - 211
for (index, _) in &kept { - 212
if let Some(missing) = requirements(ops, &created, *index, context) - 213
.into_iter() - 214
.find(|needed| !kept_ops.contains(needed)) - 215
{ - 216
return Err(EditError { - 217
op: Some((*index, ops[*index].name())), - 218
message: format!( - 219
"it builds on edit {} ({}), which was left out; keep both or neither", - 220
missing + 1, - 221
label(&ops[missing], None) - 222
), - 223
}); - 224
} - 225
} - 226
let mut minted: HashMap<String, String> = HashMap::new(); - 227
let mut state = source.to_vec(); - 228
let mut results = Vec::with_capacity(kept.len()); - 229
let mut notices: Vec<String> = Vec::new(); - 230
let mut last: Option<Applied> = None; - 231
for (index, op) in kept { - 232
let op = remap(op, &minted); - 233
let applied = edit::apply(&state, std::slice::from_ref(&op), context, limits, target) - 234
.map_err(|error| EditError { - 235
op: Some((index, op.name())), - 236
message: error.message, - 237
})?; - 238
if let Some(result) = applied.results.first() { - 239
for (before, now) in created[index].iter().zip(&result.created) { - 240
minted.insert(before.clone(), now.clone()); - 241
} - 242
} - 243
results.extend(applied.results.iter().cloned()); - 244
for notice in &applied.notices { - 245
if !notices.contains(notice) { - 246
notices.push(notice.clone()); - 247
} - 248
} - 249
state.clone_from(&applied.bytes); - 250
last = Some(applied); - 251
} - 252
let Some(last) = last else { - 253
return edit_fail("no change was kept; reject the draft instead"); - 254
}; - 255
Ok(Applied { - 256
bytes: last.bytes, - 257
results, - 258
document: last.document, - 259
notices, - 260
}) - 261
} - 262
- 263
fn edit_fail<T>(message: impl Into<String>) -> Result<T, EditError> { - 264
Err(EditError { - 265
op: None, - 266
message: message.into(), - 267
}) - 268
} - 269
- 270
/// A `set_cells` op of several cells, split per cell, when every change it - 271
/// made belongs to exactly one of its cells. - 272
fn split_cells(op: &OfficeOp, changes: &[Change]) -> Option<Vec<(String, Vec<Change>)>> { - 273
let OfficeOp::SetCells { cells, .. } = op else { - 274
return None; - 275
}; - 276
let keys: Vec<String> = cells.keys().map(|key| key.to_ascii_uppercase()).collect(); - 277
let mut unique = keys.clone(); - 278
unique.sort(); - 279
unique.dedup(); - 280
if keys.len() < 2 || unique.len() != keys.len() { - 281
return None; - 282
} - 283
let mut split: BTreeMap<&str, Vec<Change>> = - 284
keys.iter().map(|key| (key.as_str(), Vec::new())).collect(); - 285
for change in changes { - 286
let address = change.anchor.rsplit_once('!').map(|(_, address)| address)?; - 287
split.get_mut(address)?.push(change.clone()); - 288
} - 289
Some( - 290
keys.iter() - 291
.map(|key| (key.clone(), split.remove(key.as_str()).unwrap_or_default())) - 292
.collect(), - 293
) - 294
} - 295
- 296
/// The earlier ops `index` builds on. - 297
fn requirements( - 298
ops: &[OfficeOp], - 299
created: &[Vec<String>], - 300
index: usize, - 301
context: &EditContext, - 302
) -> Vec<usize> { - 303
let references = references(&ops[index]); - 304
let sheet = match &ops[index] { - 305
OfficeOp::SetCells { sheet, .. } - 306
| OfficeOp::AppendRows { sheet, .. } - 307
| OfficeOp::FormatCells { sheet, .. } - 308
| OfficeOp::SetColumnWidths { sheet, .. } - 309
| OfficeOp::RenameSheet { sheet, .. } => Some(sheet), - 310
_ => None, - 311
}; - 312
(0..index) - 313
.filter(|earlier| { - 314
let minted = created.get(*earlier).is_some_and(|minted| { - 315
minted - 316
.iter() - 317
.any(|minted| references.iter().any(|reference| names(reference, minted))) - 318
}); - 319
// A sheet exists under this name because an earlier op added - 320
// it or gave it the name. - 321
let added = matches!((&ops[*earlier], sheet), - 322
(OfficeOp::AddSheet { name } | OfficeOp::RenameSheet { name, .. }, Some(sheet)) - 323
if name.eq_ignore_ascii_case(sheet)); - 324
// A later op named a paragraph by the numbering an earlier - 325
// removal left; without that removal it would land elsewhere. - 326
let renumbered = edit::renumbered_by(&ops[*earlier], &ops[index], context); - 327
minted || added || renumbered - 328
}) - 329
.collect() - 330
} - 331
- 332
fn names(reference: &str, anchor: &str) -> bool { - 333
reference == anchor - 334
|| reference - 335
.strip_prefix(anchor) - 336
.is_some_and(|rest| rest.starts_with('/')) - 337
} - 338
- 339
fn references(op: &OfficeOp) -> Vec<&str> { - 340
match op { - 341
OfficeOp::ReplaceParagraphText { anchor, .. } - 342
| OfficeOp::DeleteParagraph { anchor } - 343
| OfficeOp::SetPlaceholderText { anchor, .. } - 344
| OfficeOp::SetNotes { anchor, .. } - 345
| OfficeOp::DeleteSlide { anchor } => vec![anchor], - 346
OfficeOp::MoveSlide { anchor, after } => { - 347
let mut all = vec![anchor.as_str()]; - 348
all.extend(after.as_deref()); - 349
all - 350
} - 351
OfficeOp::AddParagraph { after, .. } - 352
| OfficeOp::AddTable { after, .. } - 353
| OfficeOp::AddSlideFromLayout { after, .. } => after.as_deref().into_iter().collect(), - 354
OfficeOp::SetCells { .. } - 355
| OfficeOp::AppendRows { .. } - 356
| OfficeOp::AddSheet { .. } - 357
| OfficeOp::RenameSheet { .. } - 358
| OfficeOp::FormatCells { .. } - 359
| OfficeOp::SetColumnWidths { .. } - 360
| OfficeOp::SetTitle { .. } => Vec::new(), - 361
} - 362
} - 363
- 364
/// Rewrites anchors minted by the original run to the ones this replay - 365
/// minted, so a kept op still names the paragraph or slide it meant. - 366
fn remap(mut op: OfficeOp, minted: &HashMap<String, String>) -> OfficeOp { - 367
let swap = |value: &mut String| { - 368
if let Some((before, now)) = minted.iter().find(|(before, _)| names(value, before)) { - 369
*value = format!("{now}{}", &value[before.len()..]); - 370
} - 371
}; - 372
match &mut op { - 373
OfficeOp::ReplaceParagraphText { anchor, .. } - 374
| OfficeOp::DeleteParagraph { anchor } - 375
| OfficeOp::SetPlaceholderText { anchor, .. } - 376
| OfficeOp::SetNotes { anchor, .. } - 377
| OfficeOp::DeleteSlide { anchor } => swap(anchor), - 378
OfficeOp::MoveSlide { anchor, after } => { - 379
swap(anchor); - 380
if let Some(after) = after { - 381
swap(after); - 382
} - 383
} - 384
OfficeOp::AddSlideFromLayout { - 385
after: Some(after), .. - 386
} - 387
| OfficeOp::AddParagraph { - 388
after: Some(after), .. - 389
} - 390
| OfficeOp::AddTable { - 391
after: Some(after), .. - 392
} => swap(after), - 393
_ => {} - 394
} - 395
op - 396
} - 397
- 398
fn label(op: &OfficeOp, cell: Option<&str>) -> String { - 399
match op { - 400
OfficeOp::ReplaceParagraphText { anchor, .. } => format!("Edit paragraph {anchor}"), - 401
OfficeOp::AddParagraph { after, text, .. } => match after { - 402
Some(after) => format!("New paragraph after {after}: {}", preview(text)), - 403
None => format!("New paragraph: {}", preview(text)), - 404
}, - 405
OfficeOp::AddTable { rows, .. } => { - 406
let columns = rows.iter().map(Vec::len).max().unwrap_or(0); - 407
format!( - 408
"New table, {} {} by {columns} {}", - 409
rows.len(), - 410
if rows.len() == 1 { "row" } else { "rows" }, - 411
if columns == 1 { "column" } else { "columns" } - 412
) - 413
} - 414
OfficeOp::DeleteParagraph { anchor } => format!("Delete paragraph {anchor}"), - 415
OfficeOp::SetCells { sheet, cells } => match cell { - 416
Some(cell) => format!("Set {sheet}!{cell}"), - 417
None if cells.len() == 1 => cells - 418
.keys() - 419
.next() - 420
.map(|cell| format!("Set {sheet}!{}", cell.to_ascii_uppercase())) - 421
.unwrap_or_default(), - 422
None => format!("Set {} cells on {sheet}", cells.len()), - 423
}, - 424
OfficeOp::AppendRows { sheet, rows } => { - 425
format!("Add {} row(s) to {sheet}", rows.len()) - 426
} - 427
OfficeOp::AddSheet { name } => format!("Add sheet {name:?}"), - 428
OfficeOp::RenameSheet { sheet, name } => format!("Rename sheet {sheet:?} to {name:?}"), - 429
OfficeOp::FormatCells { sheet, range, .. } => format!("Format {sheet}!{range}"), - 430
OfficeOp::SetColumnWidths { sheet, widths } => format!( - 431
"Set the width of column(s) {} on {sheet}", - 432
widths.keys().cloned().collect::<Vec<_>>().join(", ") - 433
), - 434
OfficeOp::AddSlideFromLayout { layout, .. } => { - 435
format!("New slide from layout {layout:?}") - 436
} - 437
OfficeOp::SetPlaceholderText { anchor, .. } => format!("Set text of {anchor}"), - 438
OfficeOp::SetNotes { anchor, .. } => format!("Set speaker notes of {anchor}"), - 439
OfficeOp::DeleteSlide { anchor } => format!("Delete {anchor}"), - 440
OfficeOp::MoveSlide { anchor, .. } => format!("Move {anchor}"), - 441
OfficeOp::SetTitle { title } => format!("Set the title to {title:?}"), - 442
} - 443
} - 444
- 445
/// The start of a new paragraph's text, for a change's label. - 446
fn preview(text: &str) -> String { - 447
let text = text.split_whitespace().collect::<Vec<_>>().join(" "); - 448
if text.chars().count() <= 60 { - 449
text - 450
} else { - 451
format!("{}…", text.chars().take(59).collect::<String>()) - 452
} - 453
} - 454
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.