Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/src/place.rs

10.1 KiB, 5 runs

created by r2848102244:124, 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//! Where a flag's content is, said as a place in a file the reader has open.
2//!
3//! A flag names operations and content, and both are the history's own names: an
4//! identity like `r2554025284:8` and an offset into what that operation wrote.
5//! Neither is a position in a file, and the self-hosting trial found that
6//! connecting the two meant reading `ore who` beside `ore flags` by hand
7//! (`self_hosting_trial.md` §2.2). This module does that reading.
8//!
9//! The lookup is the engine's [`Placement`], which is the render turned round:
10//! content in, the file showing it and the spans it occupies out. What is added
11//! here is the part a person reads -- a path, a line number, and the honest
12//! answer where there is no line to give.
13//!
14//! # Content that is in no file
15//!
16//! Some flags name bytes that render nowhere, and that is not a failure of the
17//! lookup. Two authors who rewrite one region both delete the old bytes, so the
18//! content their `Overlap` reports is dead and shows in no file at all; a splice
19//! whose insertion was buried by an arbitration is in the log and not in the
20//! tree; an operation whose placement fell out of every file owns bytes nobody
21//! can see. Each of those is said as **names bytes no file shows**, because the
22//! alternative -- omitting the flag, or naming the file the operation was
23//! written into -- is either hiding it or making a location up.
24//!
25//! # Which content a flag is about
26//!
27//! Mostly its own: what the operation removed, moved or wrote. The exception is
28//! a yielded splice, whose own bytes are buried by construction. A reader of that
29//! flag wants the contended region, so what is located is the region as the file
30//! now holds it -- the work of the operation that prevailed -- and the wording
31//! says as much rather than pretending the yielded bytes are there.
32//!
33//! # A file with no lines
34//!
35//! A line number in a file holding NUL bytes is a fiction, so such a file is
36//! given a byte range instead. The test is the crude one every tool uses, and it
37//! is deliberately crude: what it decides is how a coordinate is printed, and
38//! nothing else.
39
40use crate::tree::{
41 self,
42 Tree,
43};
44
45use oxedyne_fe2o3_core::prelude::*;
46use oxedyne_fe2o3_ore::id::{
47 ContentRange,
48 OpId,
49};
50use oxedyne_fe2o3_ore::log::OpLog;
51use oxedyne_fe2o3_ore::op::Op;
52use oxedyne_fe2o3_ore::seq::render::{
53 Flag,
54 Place,
55 Placement,
56 Span,
57};
58
59use std::collections::BTreeMap;
60
61
62/// How many places one flag names before the rest are counted rather than
63/// listed.
64const PLACE_LIMIT: usize = 3;
65
66/// How far into a file the test for lines looks.
67///
68/// A NUL byte anywhere makes a file's line numbers a fiction, but a file that is
69/// text for its first few kilobytes is text as far as a flag's coordinate is
70/// concerned, and reading all of a large file to decide how to print one number
71/// is work nobody asked for.
72const SNIFF: usize = 8192;
73
74
75/// What content a flag is about, and whose work it is.
76enum About {
77 /// The content the flag's own operation named or wrote.
78 Own(Vec<ContentRange>),
79 /// The contended region as the file now holds it, which is the work of the
80 /// operation that prevailed rather than the flag's own.
81 Region(Vec<ContentRange>),
82}
83
84impl About {
85 /// Returns the ranges, whosever they are.
86 fn ranges(&self) -> &[ContentRange] {
87 match self {
88 Self::Own(r) => r,
89 Self::Region(r) => r,
90 }
91 }
92}
93
94
95/// One file as a coordinate needs it: where it sits, whether it is still there,
96/// and where its lines begin.
97struct Sheet {
98 /// The file's path, as bytes.
99 path: Vec<u8>,
100 /// Whether the file still exists.
101 live: bool,
102 /// Offsets of the newlines, ascending, or `None` where the file holds bytes
103 /// no line number would mean anything about.
104 lines: Option<Vec<u64>>,
105}
106
107/// Returns the line a byte offset falls on, counting from one.
108///
109/// A newline belongs to the line it ends, so an offset landing on one is on that
110/// line and the byte after it begins the next.
111fn line_of(lines: &[u64], at: u64) -> u64 {
112 lines.partition_point(|n| *n < at) as u64 + 1
113}
114
115impl Sheet {
116 /// Writes one file's spans as a coordinate: a line, a range of lines, or a
117 /// range of bytes where the file has no lines to speak of.
118 fn coord(&self, spans: &[Span]) -> String {
119 let (from, to) = match (spans.first(), spans.last()) {
120 (Some(a), Some(b)) => (a.at, b.end()),
121 _ => return fmt!("{}", tree::shown(&self.path)),
122 };
123 let at = match &self.lines {
124 // A span of no bytes cannot be pointed at, and an end of zero would
125 // underflow the line of the last byte, so the empty case is the start.
126 Some(lines) => {
127 let first = line_of(lines, from);
128 let last = line_of(lines, to.saturating_sub(1).max(from));
129 if first == last {
130 fmt!("{}:{}", tree::shown(&self.path), first)
131 } else {
132 fmt!("{}:{}-{}", tree::shown(&self.path), first, last)
133 }
134 },
135 None => fmt!("{} bytes {}..{}", tree::shown(&self.path), from, to),
136 };
137 if self.live {
138 at
139 } else {
140 fmt!("{}, in a file that has been deleted", at)
141 }
142 }
143}
144
145
146/// The render read backwards, with everything a coordinate needs beside it.
147///
148/// Built once per verb and asked once per flag, since building it walks every
149/// run of every file.
150pub struct Where {
151 /// Content in, file and spans out.
152 placed: Placement,
153 /// Every file the render holds, by identity.
154 files: BTreeMap<OpId, Sheet>,
155}
156
157impl Where {
158
159 /// Reads a rendered tree into the lookup.
160 pub fn of(tree: &Tree) -> Self {
161 let mut files: BTreeMap<OpId, Sheet> = BTreeMap::new();
162 for file in tree.repo.files() {
163 let bytes = file.bytes();
164 let lines = if bytes.iter().take(SNIFF).any(|b| *b == 0) {
165 None
166 } else {
167 let mut at: Vec<u64> = Vec::new();
168 for (i, b) in bytes.iter().enumerate() {
169 if *b == b'\n' {
170 at.push(i as u64);
171 }
172 }
173 Some(at)
174 };
175 files.insert(file.file(), Sheet {
176 path: file.path().to_vec(),
177 live: file.is_live(),
178 lines,
179 });
180 }
181 Self { placed: tree.repo.placement(), files }
182 }
183
184 /// Returns where a flag's content is, as a sentence to put under the flag.
185 ///
186 /// It is never empty: content that renders nowhere is said to render nowhere,
187 /// which is the whole point of asking.
188 pub fn of_flag(&self, flag: &Flag, log: &OpLog) -> String {
189 let about = named(flag, log);
190 let found = self.placed.find(about.ranges());
191 if found.is_empty() {
192 return fmt!("names bytes no file shows");
193 }
194 let shown = self.coords(&found);
195 match about {
196 About::Own(_) => fmt!("at {}", shown),
197 About::Region(_) => fmt!("the region as the file now holds it is at {}", shown),
198 }
199 }
200
201 /// Returns where some content is, as a coordinate a reader can act on.
202 ///
203 /// The same question [`Where::of_flag`] asks, without a flag to ask it about:
204 /// what `ore revert` needs when it accounts for an operation whose work is
205 /// about to go, and what anything else wanting to point at content will need
206 /// too. Content that renders nowhere is said to render nowhere, for the reason
207 /// [`Where::of_flag`] says it.
208 pub fn of_content(&self, ranges: &[ContentRange]) -> String {
209 let found = self.placed.find(ranges);
210 if found.is_empty() {
211 return fmt!("in no file");
212 }
213 fmt!("at {}", self.coords(&found))
214 }
215
216 /// Returns where some spans of one file are, as a coordinate a reader can act
217 /// on.
218 ///
219 /// What a note needs, its content having already been resolved into the file
220 /// showing it: the coordinate alone, with none of the wording a flag's
221 /// placement carries.
222 pub fn of_spans(&self, file: OpId, spans: &[Span]) -> String {
223 match self.files.get(&file) {
224 Some(sheet) => sheet.coord(spans),
225 // A note's place names a file the render produced, so this is
226 // unreachable; the identity is printed rather than the coordinate
227 // dropped.
228 None => fmt!("{}", file),
229 }
230 }
231
232 /// Writes a list of places as one phrase.
233 fn coords(&self, found: &[Place]) -> String {
234 let mut said: Vec<String> = Vec::new();
235 for place in found.iter().take(PLACE_LIMIT) {
236 said.push(match self.files.get(&place.file) {
237 Some(sheet) => sheet.coord(&place.spans),
238 // A place names a file the render produced, so this is unreachable;
239 // the identity is printed rather than the coordinate dropped.
240 None => fmt!("{}", place.file),
241 });
242 }
243 if found.len() > said.len() {
244 said.push(fmt!("{} more", found.len() - said.len()));
245 }
246 said.join(" and ")
247 }
248}
249
250
251/// Returns the content a flag is about.
252///
253/// The rule is one sentence: the content the flag's own operation named, except
254/// for a yielded splice, whose bytes are buried by construction and whose reader
255/// wants the region rather than the burial.
256fn named(flag: &Flag, log: &OpLog) -> About {
257 match flag {
258 // What the move no longer shows, which is where a concurrent move took it.
259 Flag::Torn { lost, .. } => About::Own(lost.clone()),
260 // The content both operations named, which is dead wherever both of them
261 // deleted it.
262 Flag::Overlap { region, .. } => About::Own(vec![*region]),
263 // The region as it stands, which is what prevailed: this flag's own bytes
264 // are in the spill and in no file, and the flag says so in words.
265 Flag::Yielded { to, .. } => About::Region(wrote(*to, log)),
266 // Everything else is about its own operation's work: what a splice wrote,
267 // or what a move carried.
268 other => match other.op() {
269 Some(op) => About::Own(wrote(op, log)),
270 None => About::Own(Vec::new()),
271 },
272 }
273}
274
275/// Returns the content an operation put somewhere: the bytes a splice inserted,
276/// or the runs a move carried.
277///
278/// A splice's insertion is content of the splice's own identity, offsets zero
279/// upwards, which is what the sequence mints for it; a move takes content that is
280/// already somebody's and keeps its names.
281///
282/// Reachable from the rest of the tool because `ore revert` asks the same
283/// question of an operation whose work is about to go dark, and asking it twice
284/// is how the two answers come to differ over a move.
285pub fn wrote(op: OpId, log: &OpLog) -> Vec<ContentRange> {
286 match log.op(&op) {
287 Some(Op::Splice { insert, .. }) if !insert.is_empty() => {
288 match ContentRange::new(op, 0, insert.len() as u64) {
289 Ok(r) => vec![r],
290 Err(_) => Vec::new(),
291 }
292 },
293 Some(Op::Move { src, .. }) => src.clone(),
294 _ => Vec::new(),
295 }
296}