1use std::{collections::BTreeMap, num::NonZeroU32, ops::Range};
2
3use anyhow::{Context, Result};
4
5use crate::{
6 model::{Row, RowKind},
7 source::{CheckedPair, Gap},
8 storage::{Document, FileIndex},
9};
10
11mod reading;
12pub(crate) use reading::SourceOrder;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[cfg_attr(feature = "delivery-trace", derive(serde::Serialize))]
17#[cfg_attr(feature = "delivery-trace", serde(untagged))]
18pub enum LineId {
19 Patch(usize),
20 Context {
21 file: usize,
22 old: NonZeroU32,
23 new: NonZeroU32,
24 },
25}
26
27impl From<usize> for LineId {
28 fn from(id: usize) -> Self {
29 Self::Patch(id)
30 }
31}
32
33impl LineId {
34 pub fn context(file: usize, old: u32, new: u32) -> Option<Self> {
36 Some(Self::Context {
37 file,
38 old: NonZeroU32::new(old)?,
39 new: NonZeroU32::new(new)?,
40 })
41 }
42
43 pub(crate) fn patch(self) -> Option<usize> {
44 match self {
45 Self::Patch(id) => Some(id),
46 Self::Context { .. } => None,
47 }
48 }
49
50 pub(crate) fn belongs_to(self, file: &crate::storage::FileIndex) -> bool {
51 match self {
52 Self::Patch(id) => (file.source_base..file.source_end()).contains(&id),
53 Self::Context { file: id, .. } => id == file.id,
54 }
55 }
56}
57
58#[derive(Clone, Copy)]
59enum Part {
60 Patch,
61 Hidden(usize),
62 Context { gap: usize, offset: u32 },
63 NoNewline,
64}
65
66struct Segment {
67 start: usize,
68 patch: usize,
69 part: Part,
70}
71
72#[derive(Default)]
73struct Rows {
74 segments: Vec<Segment>,
75 len: usize,
76 hunks: Vec<usize>,
77}
78
79impl Rows {
80 fn push(&mut self, patch: usize, len: usize, part: Part) -> Result<()> {
81 if len != 0 {
82 self.segments.push(Segment {
83 start: self.len,
84 patch,
85 part,
86 });
87 self.len = self
88 .len
89 .checked_add(len)
90 .context("Expanded row count overflow")?;
91 }
92 Ok(())
93 }
94
95 fn segment(&self, row: usize) -> Option<&Segment> {
96 if row >= self.len {
97 return None;
98 }
99 self.segments.get(
100 self.segments
101 .partition_point(|segment| segment.start <= row)
102 .checked_sub(1)?,
103 )
104 }
105
106 fn patch_row(&self, row: usize) -> usize {
107 let index = self
108 .segments
109 .partition_point(|segment| segment.patch <= row)
110 .saturating_sub(1);
111 let segment = &self.segments[index];
112 segment.start
113 + if matches!(segment.part, Part::Patch) {
114 row - segment.patch
115 } else {
116 0
117 }
118 }
119}
120
121struct Expansion {
122 source: Gap,
123 revealed: u32,
124}
125
126impl Expansion {
127 fn leading(&self) -> bool {
128 self.source.before == Some(0)
129 }
130 fn exposed(&self) -> Range<u32> {
131 if self.leading() {
132 self.source.len - self.revealed..self.source.len
133 } else {
134 0..self.revealed
135 }
136 }
137 fn line(&self, file: usize, offset: u32) -> LineId {
138 LineId::context(
139 file,
140 self.source.old + offset + 1,
141 self.source.new + offset + 1,
142 )
143 .expect("Validated source line is positive")
144 }
145}
146
147struct FileView {
148 gaps: Vec<Expansion>,
149 rows: [Rows; 2],
150}
151
152impl FileView {
153 fn insertion(file: &FileIndex, gap: &Gap, split: bool) -> (usize, usize) {
154 let end = gap
155 .before
156 .map_or_else(|| file.rows(split).len(), |h| file.hunk_row(h, split));
157 let replaces = end.checked_sub(1).is_some_and(|row| {
158 file.rows(split)
159 .get(row)
160 .is_some_and(|row| row.kind == RowKind::Gap)
161 });
162 (end - usize::from(replaces), end)
163 }
164
165 fn build(&self, file: &FileIndex) -> Result<[Rows; 2]> {
166 let mut layouts = [Rows::default(), Rows::default()];
167 for (layout, split) in [false, true].into_iter().enumerate() {
168 let rows = &mut layouts[layout];
169 let mut next = 0;
170 for (index, gap) in self.gaps.iter().enumerate() {
171 let (start, end) = Self::insertion(file, &gap.source, split);
172 rows.push(
173 next,
174 start.checked_sub(next).context("Context gaps overlap")?,
175 Part::Patch,
176 )?;
177 let hidden = gap.source.len - gap.revealed;
178 if gap.leading() && hidden != 0 {
179 rows.push(start, 1, Part::Hidden(index))?;
180 }
181 rows.push(
182 start,
183 gap.revealed as usize,
184 Part::Context {
185 gap: index,
186 offset: gap.exposed().start,
187 },
188 )?;
189 if !gap.leading() && hidden != 0 {
190 rows.push(start, 1, Part::Hidden(index))?;
191 }
192 if gap.source.no_newline && hidden == 0 {
193 rows.push(start, 1, Part::NoNewline)?;
194 }
195 next = end;
196 }
197 rows.push(next, file.rows(split).len() - next, Part::Patch)?;
198 rows.hunks = (0..file.hunks.len())
199 .map(|h| rows.patch_row(file.hunk_row(h, split)))
200 .collect();
201 }
202 Ok(layouts)
203 }
204}
205
206#[derive(Default)]
207pub(crate) struct View {
208 files: BTreeMap<usize, FileView>,
209 extra: Vec<[usize; 2]>,
210 source_revision: reading::Revision,
211}
212
213impl View {
214 #[cfg(feature = "owner-census")]
215 pub(crate) fn census(&self) -> serde_json::Value {
216 let mut ranges = 0;
217 let mut exposed = 0u64;
218 let mut segments = 0;
219 let mut allocations = self.extra.capacity() * size_of::<[usize; 2]>();
220 for file in self.files.values() {
221 ranges += file.gaps.len();
222 exposed += file
223 .gaps
224 .iter()
225 .map(|gap| u64::from(gap.revealed))
226 .sum::<u64>();
227 allocations += file.gaps.capacity() * size_of::<Expansion>();
228 for rows in &file.rows {
229 segments += rows.segments.len();
230 allocations += rows.segments.capacity() * size_of::<Segment>()
231 + rows.hunks.capacity() * size_of::<usize>();
232 }
233 }
234 serde_json::json!({
235 "files": self.files.len(), "gap_ranges": ranges, "exposed_lines": exposed,
236 "layout_segments": segments, "vector_capacity_bytes": allocations,
237 "inline_file_values_bytes": self.files.len() * size_of::<FileView>(),
238 "scope": "View vectors and occupied file values only; excludes map-node overhead, source descriptors, caches, and allocator overhead",
239 })
240 }
241
242 pub(crate) fn row(&self, doc: &Document, row: usize, split: bool) -> Option<Row<LineId>> {
243 let file = self.file_at(doc, row, split)?;
244 let local = row - self.file_row(doc, file.id, split);
245 let Some(view) = self.files.get(&file.id) else {
246 return file
247 .rows(split)
248 .get(local)
249 .map(|row| row.map(LineId::Patch));
250 };
251 let segment = view.rows[usize::from(split)].segment(local)?;
252 let kind = match segment.part {
253 Part::Patch => {
254 return file
255 .rows(split)
256 .get(segment.patch + local - segment.start)
257 .map(|row| row.map(LineId::Patch));
258 }
259 Part::Hidden(_) => RowKind::Gap,
260 Part::Context { gap, offset } => {
261 let id = view.gaps[gap].line(file.id, offset + (local - segment.start) as u32);
262 RowKind::Code {
263 left: Some(id),
264 right: Some(id),
265 }
266 }
267 Part::NoNewline => RowKind::NoNewline {
268 left: true,
269 right: true,
270 },
271 };
272 Some(Row {
273 file: file.id,
274 kind,
275 })
276 }
277
278 pub(crate) fn len(&self, doc: &Document, split: bool) -> usize {
279 doc.row_len(split)
280 + self
281 .extra
282 .last()
283 .map_or(0, |extra| extra[usize::from(split)])
284 }
285
286 pub(crate) fn source_row(&self, doc: &Document, id: LineId, split: bool) -> Option<usize> {
287 match id {
288 LineId::Patch(id) => Some(self.patch_row(doc, doc.row_for_source(id, split), split)),
289 LineId::Context { file, old, new } => {
290 let (old, new) = (old.get(), new.get());
291 let view = self.files.get(&file)?;
292 let gap = view
293 .gaps
294 .partition_point(|gap| gap.source.old < old)
295 .checked_sub(1)?;
296 let source = &view.gaps[gap];
297 let offset = old - source.source.old - 1;
298 if !source.exposed().contains(&offset)
299 || new.checked_sub(source.source.new) != Some(offset + 1)
300 {
301 return None;
302 }
303 let (slot, _) = FileView::insertion(&doc.files[file], &source.source, split);
304 let segments = &view.rows[usize::from(split)].segments;
305 let first = segments.partition_point(|segment| segment.patch < slot);
306 let segment = segments[first..].iter().take_while(|segment| segment.patch == slot).find(|segment|
307 matches!(segment.part, Part::Context { gap: index, .. } if index == gap))?;
308 Some(
309 self.file_row(doc, file, split)
310 + segment.start
311 + (offset - source.exposed().start) as usize,
312 )
313 }
314 }
315 }
316
317 pub(crate) fn file_row(&self, doc: &Document, file: usize, split: bool) -> usize {
318 doc.files[file].row_base(split)
319 + file
320 .checked_sub(1)
321 .map_or(0, |index| self.extra_at(index, split))
322 }
323
324 pub(crate) fn boundary(
325 &self,
326 doc: &Document,
327 row: usize,
328 split: bool,
329 forward: bool,
330 ) -> Option<usize> {
331 if self.files.is_empty() {
332 return doc.boundary(row, split, forward, false);
333 }
334 let first = self.file_at(doc, row, split)?.id;
335 let mut index = first;
336 loop {
337 let file = &doc.files[index];
338 let base = self.file_row(doc, index, split);
339 let found = if let Some(view) = self.files.get(&index) {
340 let hunks = &view.rows[usize::from(split)].hunks;
341 if index != first {
342 if forward {
343 hunks.first().copied()
344 } else {
345 hunks.last().copied()
346 }
347 } else if forward {
348 hunks
349 .get(hunks.partition_point(|h| base + *h <= row))
350 .copied()
351 } else {
352 hunks
353 .partition_point(|h| base + *h < row)
354 .checked_sub(1)
355 .map(|i| hunks[i])
356 }
357 } else if index == first {
358 file.rows(split).boundary(row - base, forward, false)
359 } else {
360 file.rows(split).first_boundary(forward, false)
361 };
362 if let Some(local) = found {
363 return Some(base + local);
364 }
365 index = if forward {
366 index.checked_add(1).filter(|i| *i < doc.files.len())?
367 } else {
368 index.checked_sub(1)?
369 };
370 }
371 }
372
373 fn extra_at(&self, file: usize, split: bool) -> usize {
374 self.extra
375 .get(file)
376 .or_else(|| self.extra.last())
377 .map_or(0, |extra| extra[usize::from(split)])
378 }
379
380 fn file_at<'a>(&self, doc: &'a Document, row: usize, split: bool) -> Option<&'a FileIndex> {
381 doc.files
382 .get(
383 doc.files.partition_point(|file| {
384 file.row_end(split) + self.extra_at(file.id, split) <= row
385 }),
386 )
387 .map(|f| f.as_ref())
388 }
389
390 pub(crate) fn patch_row(&self, doc: &Document, row: usize, split: bool) -> usize {
391 let Some(file) = doc
392 .files
393 .get(doc.files.partition_point(|f| f.row_end(split) <= row))
394 else {
395 return row;
396 };
397 let local = row - file.row_base(split);
398 self.file_row(doc, file.id, split)
399 + self
400 .files
401 .get(&file.id)
402 .map_or(local, |view| view.rows[usize::from(split)].patch_row(local))
403 }
404
405 pub(crate) fn original_row(&self, doc: &Document, row: usize, split: bool) -> usize {
406 let Some(file) = self.file_at(doc, row, split) else {
407 return row;
408 };
409 let local = row - self.file_row(doc, file.id, split);
410 let original = self
411 .files
412 .get(&file.id)
413 .and_then(|view| view.rows[usize::from(split)].segment(local))
414 .map_or(local, |segment| {
415 segment.patch
416 + if matches!(segment.part, Part::Patch) {
417 local - segment.start
418 } else {
419 0
420 }
421 });
422 file.row_base(split) + original.min(file.rows(split).len().saturating_sub(1))
423 }
424
425 pub(crate) fn set(
426 &mut self,
427 doc: &Document,
428 file: usize,
429 pair: Option<&CheckedPair>,
430 ) -> Result<()> {
431 let view = pair
432 .filter(|pair| !pair.gaps.is_empty())
433 .map(|pair| {
434 let mut view = FileView {
435 gaps: pair
436 .gaps
437 .iter()
438 .map(|gap| Expansion {
439 source: *gap,
440 revealed: 0,
441 })
442 .collect(),
443 rows: [Rows::default(), Rows::default()],
444 };
445 view.rows = view.build(&doc.files[file])?;
446 Ok::<_, anyhow::Error>(view)
447 })
448 .transpose()?;
449 self.install(doc, file, view)?;
450 self.source_revision.invalidate();
451 Ok(())
452 }
453
454 fn install(&mut self, doc: &Document, file: usize, view: Option<FileView>) -> Result<()> {
455 let old: [usize; 2] = std::array::from_fn(|layout| {
456 self.files.get(&file).map_or(0, |view| {
457 view.rows[layout].len - doc.files[file].rows(layout == 1).len()
458 })
459 });
460 let new: [usize; 2] = std::array::from_fn(|layout| {
461 view.as_ref().map_or(0, |view| {
462 view.rows[layout].len - doc.files[file].rows(layout == 1).len()
463 })
464 });
465 if self.extra.is_empty() && view.is_none() {
466 return Ok(());
467 }
468 self.extra.resize(
469 doc.files.len(),
470 self.extra.last().copied().unwrap_or([0; 2]),
471 );
472 for layout in 0..2 {
473 doc.row_len(layout == 1)
474 .checked_add(self.extra.last().map_or(0, |extra| extra[layout]) - old[layout])
475 .and_then(|n| n.checked_add(new[layout]))
476 .context("Expanded document row count overflow")?;
477 }
478 for extra in &mut self.extra[file..] {
479 for layout in 0..2 {
480 extra[layout] = extra[layout] - old[layout] + new[layout];
481 }
482 }
483 if let Some(view) = view {
484 self.files.insert(file, view);
485 } else {
486 self.files.remove(&file);
487 }
488 Ok(())
489 }
490
491 pub(crate) fn gap(
492 &self,
493 doc: &Document,
494 row: usize,
495 split: bool,
496 ) -> Option<(usize, usize, u32)> {
497 let file = self.file_at(doc, row, split)?;
498 let view = self.files.get(&file.id)?;
499 let segment =
500 view.rows[usize::from(split)].segment(row - self.file_row(doc, file.id, split))?;
501 let Part::Hidden(gap) = segment.part else {
502 return None;
503 };
504 Some((
505 file.id,
506 gap,
507 view.gaps[gap].source.len - view.gaps[gap].revealed,
508 ))
509 }
510
511 pub(crate) fn expand(&mut self, doc: &Document, file: usize, gap: usize) -> Result<LineId> {
512 let view = self
513 .files
514 .get_mut(&file)
515 .context("Context gap is unavailable")?;
516 let item = view
517 .gaps
518 .get_mut(gap)
519 .context("Context gap is outside the file")?;
520 let before = item.revealed;
521 item.revealed += 20.min(item.source.len - item.revealed);
522 let offset = if item.leading() {
523 item.exposed().start
524 } else {
525 item.revealed - 1
526 };
527 let selected = item.line(file, offset);
528 let built = view.build(&doc.files[file]);
529 let rows = match built {
530 Ok(rows) => rows,
531 Err(error) => {
532 view.gaps[gap].revealed = before;
533 return Err(error);
534 }
535 };
536 let old: [usize; 2] = std::array::from_fn(|layout| view.rows[layout].len);
537 let growth: [usize; 2] = std::array::from_fn(|layout| rows[layout].len - old[layout]);
538 for layout in 0..2 {
539 if doc
540 .row_len(layout == 1)
541 .checked_add(self.extra.last().map_or(0, |e| e[layout]))
542 .and_then(|n| n.checked_add(growth[layout]))
543 .is_none()
544 {
545 view.gaps[gap].revealed = before;
546 anyhow::bail!("Expanded document row count overflow");
547 }
548 }
549 view.rows = rows;
550 self.extra.resize(
551 doc.files.len(),
552 self.extra.last().copied().unwrap_or([0; 2]),
553 );
554 for extra in &mut self.extra[file..] {
555 for layout in 0..2 {
556 extra[layout] += growth[layout];
557 }
558 }
559 self.source_revision.invalidate();
560 Ok(selected)
561 }
562}