19.2 KiB, 7 runs
created by r2848102244:122, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Saying something about content, at a place in a file the writer has open. |
| 2 | //! |
| 3 | //! An anchored note is the review comment's primitive. The engine has carried |
| 4 | //! one since notes were added: [`Op::Note`] names content rather than a line, so |
| 5 | //! the note narrows when the content is edited around, travels when the content |
| 6 | //! is moved -- into another file included -- and says so plainly when the content |
| 7 | //! it was about has been deleted. What the engine has never had is a way for a |
| 8 | //! person to write one, which is what this module is. |
| 9 | //! |
| 10 | //! # Why a coordinate and not a byte range |
| 11 | //! |
| 12 | //! The engine wants [`ContentRange`]s, which are the history's own names for |
| 13 | //! bytes and no use at all to somebody reading a file. What a person has in front |
| 14 | //! of them is a path and a line, and it is the same path and line `ore flags` |
| 15 | //! prints under a flag, so a coordinate read out of one command can be handed |
| 16 | //! straight to this one. The translation is a render away: the file's bytes, the |
| 17 | //! offsets its lines begin at, and [`Rendered::span`] to turn an offset and a |
| 18 | //! length into the content that occupies it. |
| 19 | //! |
| 20 | //! # The three spellings |
| 21 | //! |
| 22 | //! `path:12` is a line. `path:12-40` is a range of lines. `path:120..480` is a |
| 23 | //! range of bytes, for the files where a line number would be a fiction and for |
| 24 | //! anybody who would rather be exact. The separator alone says which is meant, |
| 25 | //! so nothing has to be flagged. |
| 26 | //! |
| 27 | //! A line's newline belongs to the line it ends, which is the rule |
| 28 | //! [`crate::place`] reads coordinates out by, and this module writes them in by |
| 29 | //! the same rule: a note on line 12 is about every byte of line 12 including the |
| 30 | //! newline that finishes it. Were it otherwise, a note read back would name a |
| 31 | //! line other than the one it was written against. |
| 32 | //! |
| 33 | //! # A file with no lines |
| 34 | //! |
| 35 | //! A file holding NUL bytes has no lines to number, so a line coordinate against |
| 36 | //! one is refused and the byte spelling offered by name. The alternative is |
| 37 | //! counting newlines in a binary and pretending the answer means something. |
| 38 | //! |
| 39 | //! # When a note does not follow content across a file boundary |
| 40 | //! |
| 41 | //! It follows an [`Op::Move`], and what authors one is capture's move detection, |
| 42 | //! which pairs an exact removal with an exact insertion of at least |
| 43 | //! [`crate::capture::MIN_MOVE_LEN`] bytes. Cut fewer than that from one file into |
| 44 | //! another and there is no move in the log to follow: those bytes were deleted |
| 45 | //! and different bytes were written, and a note on them says its content has been |
| 46 | //! deleted, which is what the history records. That is the floor's doing rather |
| 47 | //! than the note's, and it is worth knowing before anybody diagnoses it twice. |
| 48 | //! |
| 49 | //! # Capture first, like every verb |
| 50 | //! |
| 51 | //! The note is anchored to the content the log holds, so the working copy is |
| 52 | //! captured before anything is measured. Without that, a person noting line 12 of |
| 53 | //! a file they have just edited would be noting whatever line 12 was before the |
| 54 | //! edit -- content still in the render, and the wrong content. |
| 55 | |
| 56 | use crate::capture; |
| 57 | use crate::keys::mark_of; |
| 58 | use crate::place::Where; |
| 59 | use crate::repo::Repo; |
| 60 | use crate::tree::{ |
| 61 | self, |
| 62 | Tree, |
| 63 | }; |
| 64 | use crate::verbs::report; |
| 65 | |
| 66 | use oxedyne_fe2o3_core::prelude::*; |
| 67 | use oxedyne_fe2o3_ore::id::ContentRange; |
| 68 | use oxedyne_fe2o3_ore::op::Op; |
| 69 | use oxedyne_fe2o3_ore::seq::render::Rendered; |
| 70 | |
| 71 | |
| 72 | /// How far into a file the test for lines looks. |
| 73 | /// |
| 74 | /// The same distance [`crate::place`] sniffs, and for the same reason: what it |
| 75 | /// decides is whether a line coordinate means anything, and reading all of a |
| 76 | /// large file to decide that is work nobody asked for. |
| 77 | const SNIFF: usize = 8192; |
| 78 | |
| 79 | |
| 80 | /// A place in a file, as the writer of a note spelled it. |
| 81 | #[derive(Debug, PartialEq)] |
| 82 | pub enum Coord { |
| 83 | /// One line, counting from one. |
| 84 | Line(u64), |
| 85 | /// A range of lines, inclusive at both ends. |
| 86 | Lines(u64, u64), |
| 87 | /// A range of bytes, the end exclusive. |
| 88 | Bytes(u64, u64), |
| 89 | } |
| 90 | |
| 91 | |
| 92 | /// What `ore note` was asked to do. |
| 93 | pub enum Asked { |
| 94 | /// Say every note there is, which is what the bare verb means. |
| 95 | List, |
| 96 | /// Write one, at a place in a file. |
| 97 | Write { |
| 98 | /// The path, as the working copy spells it. |
| 99 | path: String, |
| 100 | /// The place in that file. |
| 101 | at: Coord, |
| 102 | /// What the note says. |
| 103 | text: String, |
| 104 | }, |
| 105 | } |
| 106 | |
| 107 | impl Asked { |
| 108 | |
| 109 | /// Reads the arguments `ore note` was given. |
| 110 | /// |
| 111 | /// With none it lists, in the manner of `ore flags`. Otherwise the first |
| 112 | /// argument is the coordinate and everything after it is the note, joined by |
| 113 | /// single spaces. Taking the rest rather than insisting on one quoted |
| 114 | /// argument is the forgiving choice, and the only thing it costs is that runs |
| 115 | /// of spaces inside an unquoted note are not preserved. |
| 116 | pub fn read(rest: &[String]) |
| 117 | -> Outcome<Self> |
| 118 | { |
| 119 | let (first, said) = match rest.split_first() { |
| 120 | Some(pair) => pair, |
| 121 | None => return Ok(Self::List), |
| 122 | }; |
| 123 | let (path, at) = res!(split(first)); |
| 124 | let text = said.join(" "); |
| 125 | if text.trim().is_empty() { |
| 126 | return Err(err!( |
| 127 | "`ore note` was given the place {:?} and nothing to say about it. A \ |
| 128 | note whose text is empty says less than no note at all, since a \ |
| 129 | reader has to open it to find that out. `ore note` on its own says \ |
| 130 | every note there is.", first; |
| 131 | Invalid, Input, Missing)); |
| 132 | } |
| 133 | Ok(Self::Write { path: fmt!("{}", path), at, text }) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | |
| 138 | /// One coordinate, spelled for a person who has just been told they got one |
| 139 | /// wrong. |
| 140 | const EXAMPLE: &str = "src/main.rs:12 \"this branch never fires\""; |
| 141 | |
| 142 | |
| 143 | /// Splits a coordinate into the path and the place in it. |
| 144 | /// |
| 145 | /// The path is everything before the last colon, so that a path holding a colon |
| 146 | /// is not mistaken for a coordinate. A Windows-shaped `C:\...` is therefore read |
| 147 | /// correctly for free, and a file genuinely named with a trailing `:12` is not, |
| 148 | /// which is the trade every tool that prints `path:line` has already made. |
| 149 | fn split(arg: &str) |
| 150 | -> Outcome<(&str, Coord)> |
| 151 | { |
| 152 | let at = match arg.rfind(':') { |
| 153 | Some(i) => i, |
| 154 | None => return Err(err!( |
| 155 | "{:?} names a file but no place in it. A note is about content, so it \ |
| 156 | needs a line, a range of lines, or a range of bytes: `ore note {}` .", |
| 157 | arg, EXAMPLE; |
| 158 | Invalid, Input, Missing)), |
| 159 | }; |
| 160 | let (path, rest) = arg.split_at(at); |
| 161 | // The colon itself belongs to neither half. |
| 162 | let rest = &rest[1..]; |
| 163 | if path.is_empty() { |
| 164 | return Err(err!( |
| 165 | "{:?} names a place but no file to find it in.", arg; |
| 166 | Invalid, Input, Missing)); |
| 167 | } |
| 168 | Ok((path, res!(place(rest, arg)))) |
| 169 | } |
| 170 | |
| 171 | /// Reads the part after the last colon, which is one of the three spellings. |
| 172 | fn place(rest: &str, whole: &str) |
| 173 | -> Outcome<Coord> |
| 174 | { |
| 175 | if let Some((from, to)) = rest.split_once("..") { |
| 176 | let from = res!(number(from, whole)); |
| 177 | let to = res!(number(to, whole)); |
| 178 | if to <= from { |
| 179 | return Err(err!( |
| 180 | "{:?} asks for bytes {}..{}, which is no bytes at all. The first \ |
| 181 | number is the byte the note starts at and the second is the byte it \ |
| 182 | stops before, so the second must be the larger.", whole, from, to; |
| 183 | Invalid, Input, Range)); |
| 184 | } |
| 185 | return Ok(Coord::Bytes(from, to)); |
| 186 | } |
| 187 | if let Some((from, to)) = rest.split_once('-') { |
| 188 | let from = res!(number(from, whole)); |
| 189 | let to = res!(number(to, whole)); |
| 190 | if to < from { |
| 191 | return Err(err!( |
| 192 | "{:?} asks for lines {} to {}, which runs backwards.", |
| 193 | whole, from, to; |
| 194 | Invalid, Input, Range)); |
| 195 | } |
| 196 | return Ok(Coord::Lines(from, to)); |
| 197 | } |
| 198 | Ok(Coord::Line(res!(number(rest, whole)))) |
| 199 | } |
| 200 | |
| 201 | /// Reads one number out of a coordinate, complaining about the coordinate rather |
| 202 | /// than the number, since the coordinate is what was typed. |
| 203 | fn number(said: &str, whole: &str) |
| 204 | -> Outcome<u64> |
| 205 | { |
| 206 | match said.trim().parse::<u64>() { |
| 207 | Ok(n) => Ok(n), |
| 208 | Err(_) => Err(err!( |
| 209 | "{:?} is not a place this reads. The part after the last colon is a \ |
| 210 | line (`12`), a range of lines (`12-40`) or a range of bytes \ |
| 211 | (`120..480`), and {:?} is none of them.", whole, said; |
| 212 | Invalid, Input)), |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | |
| 217 | /// Where the lines of a file begin, or nothing where its bytes are not lines. |
| 218 | /// |
| 219 | /// The offsets kept are of the newlines themselves, matching [`crate::place`], so |
| 220 | /// that the two modules agree byte for byte about which line a byte is on. |
| 221 | fn newlines(bytes: &[u8]) |
| 222 | -> Option<Vec<u64>> |
| 223 | { |
| 224 | if bytes.iter().take(SNIFF).any(|b| *b == 0) { |
| 225 | return None; |
| 226 | } |
| 227 | let mut at: Vec<u64> = Vec::new(); |
| 228 | for (i, b) in bytes.iter().enumerate() { |
| 229 | if *b == b'\n' { |
| 230 | at.push(i as u64); |
| 231 | } |
| 232 | } |
| 233 | Some(at) |
| 234 | } |
| 235 | |
| 236 | /// Returns the bytes a range of lines occupies, the end exclusive. |
| 237 | /// |
| 238 | /// Line `n` runs from the byte after the newline that ended line `n - 1` to the |
| 239 | /// newline that ends line `n`, inclusive of that newline. A last line with no |
| 240 | /// newline of its own ends where the file does. |
| 241 | fn bytes_of_lines(lines: &[u64], len: u64, from: u64, to: u64) |
| 242 | -> Outcome<(u64, u64)> |
| 243 | { |
| 244 | // A file's line count is its newline count, plus one for anything after the |
| 245 | // last newline. An empty file has no lines and no note to hang on one. |
| 246 | let count = if len == 0 { |
| 247 | 0 |
| 248 | } else if lines.last() == Some(&(len - 1)) { |
| 249 | lines.len() as u64 |
| 250 | } else { |
| 251 | lines.len() as u64 + 1 |
| 252 | }; |
| 253 | if from == 0 { |
| 254 | return Err(err!( |
| 255 | "Lines count from one, so there is no line 0."; |
| 256 | Invalid, Input, Range)); |
| 257 | } |
| 258 | if to > count { |
| 259 | return Err(err!( |
| 260 | "The file has {} line{}, so line {} is not in it.", |
| 261 | count, if count == 1 { "" } else { "s" }, to; |
| 262 | Invalid, Input, Range)); |
| 263 | } |
| 264 | let at = if from == 1 { |
| 265 | 0 |
| 266 | } else { |
| 267 | lines[from as usize - 2] + 1 |
| 268 | }; |
| 269 | let end = match lines.get(to as usize - 1) { |
| 270 | Some(n) => n + 1, |
| 271 | None => len, |
| 272 | }; |
| 273 | Ok((at, end)) |
| 274 | } |
| 275 | |
| 276 | /// Returns the bytes a coordinate names in a rendered file. |
| 277 | fn bytes_of(file: &Rendered, at: &Coord, path: &str) |
| 278 | -> Outcome<(u64, u64)> |
| 279 | { |
| 280 | let len = file.bytes().len() as u64; |
| 281 | match at { |
| 282 | Coord::Bytes(from, to) => { |
| 283 | if *to > len { |
| 284 | return Err(err!( |
| 285 | "{} holds {} byte{}, so there is nothing at byte {}.", |
| 286 | path, len, if len == 1 { "" } else { "s" }, to; |
| 287 | Invalid, Input, Range)); |
| 288 | } |
| 289 | Ok((*from, *to)) |
| 290 | }, |
| 291 | Coord::Line(n) => lines_of(file, path, *n, *n), |
| 292 | Coord::Lines(a, b) => lines_of(file, path, *a, *b), |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | /// Returns the bytes a range of lines names, refusing a file that has no lines. |
| 297 | fn lines_of(file: &Rendered, path: &str, from: u64, to: u64) |
| 298 | -> Outcome<(u64, u64)> |
| 299 | { |
| 300 | let bytes = file.bytes(); |
| 301 | let lines = match newlines(bytes) { |
| 302 | Some(l) => l, |
| 303 | None => return Err(err!( |
| 304 | "{} holds bytes that are not text, so a line number in it would mean \ |
| 305 | nothing. Name the bytes instead, as in `{}:0..64` .", path, path; |
| 306 | Invalid, Input)), |
| 307 | }; |
| 308 | let (at, end) = res!(bytes_of_lines(&lines, bytes.len() as u64, from, to)); |
| 309 | Ok((at, end)) |
| 310 | } |
| 311 | |
| 312 | |
| 313 | /// `ore note` -- says something about the content at a place in a file, and goes |
| 314 | /// on saying it about that content wherever the content ends up. |
| 315 | pub fn note(repo: &mut Repo, asked: &Asked) |
| 316 | -> Outcome<()> |
| 317 | { |
| 318 | let mut what = res!(capture::capture(repo)); |
| 319 | report(&what); |
| 320 | println!(); |
| 321 | // The capture hands its render on where that render is still the |
| 322 | // present and holds everything a whole one holds, so the second |
| 323 | // render most verbs used to make is skipped. |
| 324 | let tree = match what.tree.take() { |
| 325 | Some(tree) => tree, |
| 326 | None => res!(tree::whole(&repo.log)), |
| 327 | }; |
| 328 | let (path, at, text) = match asked { |
| 329 | Asked::List => return list(repo, &tree), |
| 330 | Asked::Write { path, at, text } => (path, at, text), |
| 331 | }; |
| 332 | let file = res!(found(&tree, path)); |
| 333 | let (from, end) = res!(bytes_of(file, at, path)); |
| 334 | let on = res!(spanned(file, from, end, path)); |
| 335 | let bytes = end - from; |
| 336 | let replica = repo.cfg.replica; |
| 337 | let id = res!(repo.author(replica, Op::Note { |
| 338 | on, |
| 339 | text: text.as_bytes().to_vec(), |
| 340 | })); |
| 341 | println!("note {} is about {} byte{} at {}", |
| 342 | id, |
| 343 | bytes, if bytes == 1 { "" } else { "s" }, |
| 344 | said(path, at), |
| 345 | ); |
| 346 | println!("it will follow that content through every edit and every move, and \ |
| 347 | say so if the content is deleted"); |
| 348 | Ok(()) |
| 349 | } |
| 350 | |
| 351 | /// Says every note the repository holds, and where each one has ended up. |
| 352 | /// |
| 353 | /// Repository-wide rather than per-file because a note's content may have moved |
| 354 | /// into another file since it was written, or been deleted altogether, and |
| 355 | /// neither of those notes is reachable by asking about the file it was written |
| 356 | /// against. `ore who <file>` is the per-file reading; this is the one that misses |
| 357 | /// nothing. |
| 358 | fn list(repo: &Repo, tree: &Tree) |
| 359 | -> Outcome<()> |
| 360 | { |
| 361 | let notes = tree.repo.notes(); |
| 362 | if notes.is_empty() { |
| 363 | println!("no notes; `ore note {}` writes one", EXAMPLE); |
| 364 | return Ok(()); |
| 365 | } |
| 366 | println!("{} note{}", notes.len(), if notes.len() == 1 { "" } else { "s" }); |
| 367 | let placed = Where::of(tree); |
| 368 | for note in notes { |
| 369 | println!(); |
| 370 | println!("note {} {}", mark_of(&repo.prov, note.note()), note.note()); |
| 371 | if note.on_dead() { |
| 372 | // Not a fault and not a loss: the log holds what it said and what it |
| 373 | // was about, and this is what says no margin will show it. |
| 374 | println!(" every byte it was about has been deleted"); |
| 375 | } else { |
| 376 | // The byte count as well as the coordinate, because a note narrows |
| 377 | // under an edit and the coordinate alone does not show it: a note over |
| 378 | // three lines whose middle line was replaced still reads as three lines |
| 379 | // and is about two of them. |
| 380 | for place in note.files() { |
| 381 | let bytes: u64 = place.spans.iter().map(|s| s.len).sum(); |
| 382 | println!(" {}, {} byte{}", |
| 383 | placed.of_spans(place.file, &place.spans), |
| 384 | bytes, if bytes == 1 { "" } else { "s" }, |
| 385 | ); |
| 386 | } |
| 387 | } |
| 388 | println!(" {}", note.text_lossy()); |
| 389 | } |
| 390 | Ok(()) |
| 391 | } |
| 392 | |
| 393 | /// Finds the file a coordinate names, under the name the working copy gives it. |
| 394 | /// |
| 395 | /// The layout rather than the recorded path, since two files may hold one path |
| 396 | /// and the loser is written to a derived name: what a person can point at is what |
| 397 | /// is on the disk in front of them. |
| 398 | fn found<'a>(tree: &'a Tree, path: &str) |
| 399 | -> Outcome<&'a Rendered> |
| 400 | { |
| 401 | let layout = res!(tree.layout()); |
| 402 | let want = path.as_bytes(); |
| 403 | match layout.file_at(want).and_then(|id| tree.get(id)) { |
| 404 | Some(f) => Ok(f), |
| 405 | None => { |
| 406 | let known: Vec<String> = layout.at.keys().map(|p| tree::shown(p)).collect(); |
| 407 | Err(err!( |
| 408 | "The repository holds no file {:?}. It holds: {}.", |
| 409 | path, |
| 410 | if known.is_empty() { fmt!("nothing") } else { known.join(", ") }; |
| 411 | Invalid, Input, NotFound)) |
| 412 | }, |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | /// Turns an offset and an end into the content that occupies them. |
| 417 | /// |
| 418 | /// The engine refuses a span of nothing, since a note about nothing is a mark |
| 419 | /// with extra spelling, and this says which coordinate asked for nothing rather |
| 420 | /// than passing the engine's wording on. |
| 421 | fn spanned(file: &Rendered, at: u64, end: u64, path: &str) |
| 422 | -> Outcome<Vec<ContentRange>> |
| 423 | { |
| 424 | if end <= at { |
| 425 | return Err(err!( |
| 426 | "That place in {} holds no bytes, so there is nothing for a note to be \ |
| 427 | about. An empty line is bytes -- its newline -- but a line past the end \ |
| 428 | of a file is not.", path; |
| 429 | Invalid, Input, Range)); |
| 430 | } |
| 431 | file.span(at as usize, (end - at) as usize) |
| 432 | } |
| 433 | |
| 434 | /// Writes a coordinate back the way it came, for the line that confirms it. |
| 435 | fn said(path: &str, at: &Coord) -> String { |
| 436 | match at { |
| 437 | Coord::Line(n) => fmt!("{}:{}", path, n), |
| 438 | Coord::Lines(a, b) => fmt!("{}:{}-{}", path, a, b), |
| 439 | Coord::Bytes(a, b) => fmt!("{}:{}..{}", path, a, b), |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | |
| 444 | #[cfg(test)] |
| 445 | mod tests { |
| 446 | use super::*; |
| 447 | |
| 448 | /// The three spellings are told apart by their separator alone. |
| 449 | #[test] |
| 450 | fn coordinate_spellings() |
| 451 | -> Outcome<()> |
| 452 | { |
| 453 | assert_eq!(res!(split("src/main.rs:12")), ("src/main.rs", Coord::Line(12))); |
| 454 | assert_eq!(res!(split("src/main.rs:12-40")), ("src/main.rs", Coord::Lines(12, 40))); |
| 455 | assert_eq!(res!(split("src/main.rs:120..480")), ("src/main.rs", Coord::Bytes(120, 480))); |
| 456 | // The last colon splits, so a path holding one survives. |
| 457 | assert_eq!(res!(split("C:\\src\\main.rs:12")), ("C:\\src\\main.rs", Coord::Line(12))); |
| 458 | Ok(()) |
| 459 | } |
| 460 | |
| 461 | /// A coordinate that names no place, no file, or no sane range is refused. |
| 462 | #[test] |
| 463 | fn coordinate_refusals() |
| 464 | -> Outcome<()> |
| 465 | { |
| 466 | assert!(split("src/main.rs").is_err()); // no place |
| 467 | assert!(split(":12").is_err()); // no file |
| 468 | assert!(split("src/main.rs:").is_err()); // nothing after the colon |
| 469 | assert!(split("src/main.rs:top").is_err()); // not a number |
| 470 | assert!(split("src/main.rs:40-12").is_err()); // backwards |
| 471 | assert!(split("src/main.rs:480..120").is_err()); // backwards |
| 472 | assert!(split("src/main.rs:120..120").is_err()); // no bytes at all |
| 473 | Ok(()) |
| 474 | } |
| 475 | |
| 476 | /// A line's newline belongs to the line it ends, and the last line ends where |
| 477 | /// the file does whether or not it has one. |
| 478 | #[test] |
| 479 | fn lines_to_bytes() |
| 480 | -> Outcome<()> |
| 481 | { |
| 482 | // 0123 4567 89 |
| 483 | let bytes = b"ab\ncd\nef"; |
| 484 | let lines = match newlines(bytes) { |
| 485 | Some(l) => l, |
| 486 | None => return Err(err!("The fixture is text."; Test, Invalid)), |
| 487 | }; |
| 488 | let len = bytes.len() as u64; |
| 489 | // Line 1 is "ab\n": its newline is included. |
| 490 | assert_eq!(res!(bytes_of_lines(&lines, len, 1, 1)), (0, 3)); |
| 491 | // Line 2 is "cd\n". |
| 492 | assert_eq!(res!(bytes_of_lines(&lines, len, 2, 2)), (3, 6)); |
| 493 | // Line 3 has no newline of its own and ends where the file ends. |
| 494 | assert_eq!(res!(bytes_of_lines(&lines, len, 3, 3)), (6, 8)); |
| 495 | // A range spans from the first line's start to the last line's end. |
| 496 | assert_eq!(res!(bytes_of_lines(&lines, len, 1, 2)), (0, 6)); |
| 497 | assert_eq!(res!(bytes_of_lines(&lines, len, 1, 3)), (0, 8)); |
| 498 | // There are three lines and no fourth, and no line zero. |
| 499 | assert!(bytes_of_lines(&lines, len, 4, 4).is_err()); |
| 500 | assert!(bytes_of_lines(&lines, len, 0, 1).is_err()); |
| 501 | Ok(()) |
| 502 | } |
| 503 | |
| 504 | /// A file whose last byte is a newline has no phantom line after it. |
| 505 | #[test] |
| 506 | fn trailing_newline_is_not_a_line() |
| 507 | -> Outcome<()> |
| 508 | { |
| 509 | let bytes = b"ab\n"; |
| 510 | let lines = match newlines(bytes) { |
| 511 | Some(l) => l, |
| 512 | None => return Err(err!("The fixture is text."; Test, Invalid)), |
| 513 | }; |
| 514 | assert_eq!(res!(bytes_of_lines(&lines, 3, 1, 1)), (0, 3)); |
| 515 | assert!(bytes_of_lines(&lines, 3, 2, 2).is_err()); |
| 516 | Ok(()) |
| 517 | } |
| 518 | |
| 519 | /// An empty line is still bytes, because its newline is a byte. |
| 520 | #[test] |
| 521 | fn empty_line_is_still_bytes() |
| 522 | -> Outcome<()> |
| 523 | { |
| 524 | let bytes = b"a\n\nb"; |
| 525 | let lines = match newlines(bytes) { |
| 526 | Some(l) => l, |
| 527 | None => return Err(err!("The fixture is text."; Test, Invalid)), |
| 528 | }; |
| 529 | assert_eq!(res!(bytes_of_lines(&lines, 4, 2, 2)), (2, 3)); |
| 530 | Ok(()) |
| 531 | } |
| 532 | |
| 533 | /// A file holding NUL bytes has no lines to count. |
| 534 | #[test] |
| 535 | fn nul_bytes_have_no_lines() { |
| 536 | assert!(newlines(b"ab\0cd\n").is_none()); |
| 537 | assert!(newlines(b"ab\ncd\n").is_some()); |
| 538 | } |
| 539 | |
| 540 | /// The note is everything after the coordinate, and an empty one is refused. |
| 541 | #[test] |
| 542 | fn text_is_the_rest() |
| 543 | -> Outcome<()> |
| 544 | { |
| 545 | let said = |v: &[&str]| -> Vec<String> { |
| 546 | v.iter().map(|s| fmt!("{}", s)).collect() |
| 547 | }; |
| 548 | // Unquoted words are joined, and the coordinate is not one of them. |
| 549 | match res!(Asked::read(&said(&["src/main.rs:12", "never", "fires"]))) { |
| 550 | Asked::Write { path, at, text } => { |
| 551 | assert_eq!(text, "never fires"); |
| 552 | assert_eq!(path, "src/main.rs"); |
| 553 | assert_eq!(at, Coord::Line(12)); |
| 554 | }, |
| 555 | Asked::List => return Err(err!( |
| 556 | "A coordinate and a note were read as a listing."; Test, Mismatch)), |
| 557 | } |
| 558 | // One quoted argument says the same thing. |
| 559 | match res!(Asked::read(&said(&["src/main.rs:12", "never fires"]))) { |
| 560 | Asked::Write { text, .. } => assert_eq!(text, "never fires"), |
| 561 | Asked::List => return Err(err!( |
| 562 | "A coordinate and a note were read as a listing."; Test, Mismatch)), |
| 563 | } |
| 564 | // A place with nothing to say about it is refused, whitespace included. |
| 565 | assert!(Asked::read(&said(&["src/main.rs:12"])).is_err()); |
| 566 | assert!(Asked::read(&said(&["src/main.rs:12", " "])).is_err()); |
| 567 | // The bare verb lists rather than refusing. |
| 568 | assert!(matches!(res!(Asked::read(&said(&[]))), Asked::List)); |
| 569 | Ok(()) |
| 570 | } |
| 571 | } |