//! Carry reading anchors through commits and retain displayed or searched groups across page joins. use std::ops::Range; use super::*; impl TranscriptView { /// Rebuild the layout of an observed revision without counting the reflow as new output. pub(crate) fn rebind_observed_live_revision(&mut self, observed: u64, updated: u64) { if let Some((_, key)) = &mut self.live_key && key.revision == observed { key.revision = updated; key.cacheable = false; } } /// Refresh unseen activity before either footer measurement or transcript painting. pub(crate) fn sync_history_tail(&mut self, cells: &[Arc]) { let tail = cells.last().map(EntryKey::cell); if !self.is_following() && self.last_tail.is_some() && self.last_tail != tail { let previous = cells .iter() .rposition(|cell| Some(EntryKey::cell(cell)) == self.last_tail); self.unseen_activity |= previous.is_none_or(|index| { (index + 1..cells.len()).any(|index| { self.current_layout(cells, index) .is_some_and(|layout| layout.row_count() > 0) }) }); } self.last_tail = tail; if self.is_following() { self.unseen_activity = false; } } /// Replacements restart search and move affected readers; held readers and selections keep their snapshot. pub(crate) fn replace_range( &mut self, cells: &[Arc], range: Range, replacement: &Arc, ) { if range.is_empty() { return; } self.retain_search_origin(cells, range.clone()); if range.end == cells.len() && self.last_tail == cells.last().map(EntryKey::cell) { self.last_tail = Some(EntryKey::cell(replacement)); } if self.snapshot().is_some() { return; } self.restart_search(); let reading = match self.position { Position::Reading(anchor) if range.contains(&self.resolve(cells, anchor)) => { Some(anchor) } _ => None, }; if reading.is_none() { return; } let layouts = range .clone() .filter_map(|index| self.layout(cells, index)) .collect::>(); let replacement_key = EntryKey::cell(replacement); if let Some(anchor) = reading { self.position = Position::Reading(remap( anchor, self.resolve(cells, anchor), range.start, replacement_key, &layouts, )); } } /// Join unseen groups while retaining displayed groups and the search frontier. /// /// Call after inserting the older page and before replacing the old group cells. Unlike /// streamed text finalization, group summaries cannot preserve offsets by concatenation. /// Search keeps both sides of the join so it can finish scanning the newly inserted page. pub(crate) fn replace_group( &mut self, cells: &[Arc], range: Range, replacement: &Arc, ) { if range.is_empty() { return; } self.retain_search_origin(cells, range.clone()); // Older history may extend the final group without introducing new activity. if range.end == cells.len() && self.last_tail == cells.last().map(EntryKey::cell) { self.last_tail = Some(EntryKey::cell(replacement)); } let searching = self.search.has_active_query(); let reading = match self.position { Position::Reading(anchor) => Some(anchor.key), Position::Latest => None, }; let visible = self.visible.iter().any(|row| { cells[range.clone()] .iter() .any(|cell| EntryKey::cell(cell) == row.key) }); if let Some(snapshot) = self.snapshot_mut() { let replaced = &cells[range]; // Pins retain the initial viewport and selected text; later scrolling adds no pins. if searching || visible || replaced.iter().any(|cell| { let key = EntryKey::cell(cell); snapshot.pinned.contains_key(&key) || reading == Some(key) }) { return; } let Some(start) = snapshot.cells.windows(replaced.len()).position(|retained| { retained .iter() .zip(replaced) .all(|(a, b)| Arc::ptr_eq(a, b)) }) else { return; }; let mut joined = snapshot.cells.to_vec(); joined.splice(start..start + replaced.len(), [Arc::clone(replacement)]); snapshot.cells = joined.into(); return; } if searching || matches!(self.position, Position::Reading(anchor) if cells[range].iter().any(|cell| EntryKey::cell(cell) == anchor.key)) { self.held_reading = Some(self.capture_snapshot(cells)); } } /// Retain displayed revisions when an older completed group joins the active group. /// /// Call after `history_loaded` and the live mutation, before removing the canonical tail. /// The widget revisions bracket only hydration so actual pending activity remains visible. pub(crate) fn absorb_tail_into_live( &mut self, cells: &[Arc], previous_revision: u64, hydrated_revision: u64, ) { let Some((tail, preceding)) = cells.split_last() else { return; }; self.retain_search_origin(cells, preceding.len()..cells.len()); let tail_key = EntryKey::cell(tail); if self.last_tail == Some(tail_key) { self.last_tail = preceding.last().map(EntryKey::cell); } if let Some((_, key)) = self.live_key { if !self.is_following() && key.revision != previous_revision { self.unseen_activity = true; } self.rebind_observed_live_revision(key.revision, hydrated_revision); } if self.snapshot().is_some() { return; } if let Position::Reading(anchor) = self.position && (anchor.key == tail_key || anchor.key == EntryKey::Live) { self.held_reading = Some(self.capture_snapshot(cells)); } self.restart_search(); } /// Extend the frozen history only with the explicitly inserted older page. /// /// Current commits and replacements remain outside the snapshot. A retained session header /// identifies the insertion boundary even when it precedes the first history page. pub(super) fn prepend_snapshot_history( &mut self, cells: &[Arc], inserted: Range, ) { let Some(snapshot) = self.snapshot_mut() else { return; }; let existing = snapshot .cells .iter() .map(EntryKey::cell) .collect::>(); let older = cells[inserted.clone()] .iter() .filter(|cell| !existing.contains(&EntryKey::cell(cell))) .cloned() .collect::>(); if older.is_empty() { return; } let index = cells[..inserted.start] .iter() .rev() .find_map(|cell| { snapshot .cells .iter() .position(|retained| Arc::ptr_eq(retained, cell)) }) .map_or(/*default*/ 0, |index| index + 1); let mut extended = snapshot.cells.to_vec(); extended.splice(index..index, older); snapshot.cells = extended.into(); } } #[cfg(test)] #[path = "mutations_tests.rs"] mod tests; fn remap( anchor: Anchor, index: usize, first: usize, key: EntryKey, layouts: &[Arc], ) -> Anchor { let preceding = layouts .iter() .enumerate() .take(index - first) .filter(|(_, layout)| layout.row_count() > 0) .map(|(index, layout)| { layout.text().len() + layouts[index + 1..] .iter() .find(|next| next.row_count() > 0) .map_or(0, |next| layout.separator_after(next).len()) }) .sum::(); Anchor { key, index: first, offset: preceding.saturating_add(anchor.offset), row_bias: anchor.row_bias, } }