use crate::state::EditorState;
use crate::view::ui::split_rendering::base_tokens::build_base_tokens;
use crate::view::ui::split_rendering::transforms::{
apply_conceal_ranges, apply_soft_breaks, apply_wrapping_transform,
};
use crate::view::ui::view_pipeline::{ViewLine, ViewLineIterator};
use fresh_core::api::ViewTokenWireKind;
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
pub const DEFAULT_BYTE_BUDGET: usize = 8 * 1024 * 1024;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum CacheViewMode {
Source,
Compose,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct LineWrapKey {
pub pipeline_inputs_version: u64,
pub view_mode: CacheViewMode,
pub line_start: usize,
pub effective_width: u32,
pub gutter_width: u16,
pub wrap_column: Option<u32>,
pub hanging_indent: bool,
pub line_wrap_enabled: bool,
}
#[inline]
pub fn pipeline_inputs_version(
buffer_version: u64,
soft_breaks_version: u32,
conceal_version: u32,
virtual_text_version: u32,
) -> u64 {
(buffer_version & 0xFFFF_FFFF)
^ ((soft_breaks_version as u64) << 32)
^ ((conceal_version as u64) << 48)
^ ((virtual_text_version as u64) << 16)
}
fn estimate_view_lines_bytes(lines: &[ViewLine]) -> usize {
let mut total = 48; for line in lines {
let chars = line.char_source_bytes.len();
let visual = line.visual_to_char.len();
total += line.text.len() + chars * 56 + visual * 8 + 96;
}
total
}
#[derive(Debug, Clone)]
pub struct LineWrapCache {
map: HashMap<LineWrapKey, Arc<Vec<ViewLine>>>,
order: VecDeque<LineWrapKey>,
byte_budget: usize,
current_bytes: usize,
}
impl Default for LineWrapCache {
fn default() -> Self {
Self::with_byte_budget(DEFAULT_BYTE_BUDGET)
}
}
impl LineWrapCache {
pub fn with_byte_budget(byte_budget: usize) -> Self {
assert!(byte_budget > 0, "LineWrapCache byte_budget must be > 0");
Self {
map: HashMap::new(),
order: VecDeque::new(),
byte_budget,
current_bytes: 0,
}
}
pub fn len(&self) -> usize {
debug_assert_eq!(
self.map.len(),
self.order.len(),
"LineWrapCache invariant: map.len() == order.len()"
);
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn byte_budget(&self) -> usize {
self.byte_budget
}
pub fn current_bytes(&self) -> usize {
self.current_bytes
}
pub fn get(&self, key: &LineWrapKey) -> Option<Arc<Vec<ViewLine>>> {
self.map.get(key).cloned()
}
pub fn get_or_insert_with<F>(&mut self, key: LineWrapKey, compute: F) -> Arc<Vec<ViewLine>>
where
F: FnOnce() -> Vec<ViewLine>,
{
if let Some(v) = self.map.get(&key) {
return v.clone();
}
let value = Arc::new(compute());
self.insert_fresh(key, value.clone());
value
}
pub fn put(&mut self, key: LineWrapKey, value: Arc<Vec<ViewLine>>) {
if let Some(existing) = self.map.get_mut(&key) {
let old_bytes = estimate_view_lines_bytes(existing);
let new_bytes = estimate_view_lines_bytes(&value);
*existing = value;
self.current_bytes = self.current_bytes + new_bytes - old_bytes.min(self.current_bytes);
return;
}
self.insert_fresh(key, value);
}
pub fn clear(&mut self) {
self.map.clear();
self.order.clear();
self.current_bytes = 0;
}
fn insert_fresh(&mut self, key: LineWrapKey, value: Arc<Vec<ViewLine>>) {
debug_assert!(!self.map.contains_key(&key));
let new_bytes = estimate_view_lines_bytes(&value);
while self.current_bytes + new_bytes > self.byte_budget && !self.order.is_empty() {
if let Some(oldest_key) = self.order.pop_front() {
if let Some(oldest_val) = self.map.remove(&oldest_key) {
let shed = estimate_view_lines_bytes(&oldest_val);
self.current_bytes = self.current_bytes.saturating_sub(shed);
}
}
}
self.map.insert(key, value);
self.order.push_back(key);
self.current_bytes += new_bytes;
debug_assert_eq!(self.map.len(), self.order.len());
}
}
pub fn layout_for_plain_text(
line_text: &str,
effective_width: usize,
gutter_width: usize,
hanging_indent: bool,
tab_size: usize,
) -> Vec<ViewLine> {
use crate::view::ui::view_pipeline::LineStart;
use fresh_core::api::ViewTokenWire;
let tokens = vec![ViewTokenWire {
source_offset: Some(0),
kind: ViewTokenWireKind::Text(line_text.to_string()),
style: None,
}];
let wrapped = apply_wrapping_transform(tokens, effective_width, gutter_width, hanging_indent);
let mut lines: Vec<ViewLine> =
ViewLineIterator::new(&wrapped, false, true, tab_size, false).collect();
if lines.is_empty() {
lines.push(ViewLine {
text: String::new(),
source_start_byte: Some(0),
char_source_bytes: Vec::new(),
char_styles: Vec::new(),
char_visual_cols: Vec::new(),
visual_to_char: Vec::new(),
tab_starts: std::collections::HashSet::new(),
line_start: LineStart::Beginning,
ends_with_newline: false,
});
}
lines
}
pub fn layout_for_line(
state: &mut EditorState,
line_start: usize,
line_end: usize,
geom: &WrapGeometry,
) -> Arc<Vec<ViewLine>> {
let version = pipeline_inputs_version(
state.buffer.version(),
state.soft_breaks.version(),
state.conceals.version(),
state.virtual_texts.version(),
);
let key = geom.key(line_start, version);
if let Some(cached) = state.line_wrap_cache.get(&key) {
return cached;
}
let layout = compute_line_layout(state, line_start, line_end, geom);
let arc = Arc::new(layout);
state.line_wrap_cache.put(key, arc.clone());
arc
}
pub fn char_position_in_layout(layout: &[ViewLine], char_pos_in_line: usize) -> (usize, usize) {
if layout.is_empty() {
return (0, 0);
}
let mut source_chars_consumed = 0usize;
for (i, line) in layout.iter().enumerate() {
let source_chars_in_row = line
.char_source_bytes
.iter()
.filter(|b| b.is_some())
.count();
if char_pos_in_line < source_chars_consumed + source_chars_in_row {
let within_row = char_pos_in_line - source_chars_consumed;
let mut source_count = 0usize;
for (char_idx, byte) in line.char_source_bytes.iter().enumerate() {
if byte.is_some() {
if source_count == within_row {
return (i, line.visual_col_at_char(char_idx));
}
source_count += 1;
}
}
return (i, line.visual_width().saturating_sub(1));
}
source_chars_consumed += source_chars_in_row;
}
let last_idx = layout.len() - 1;
let last = &layout[last_idx];
let last_col = last.visual_width().saturating_sub(1);
(last_idx, last_col)
}
#[derive(Debug, Clone, Copy)]
pub struct WrapGeometry {
pub effective_width: usize,
pub gutter_width: usize,
pub hanging_indent: bool,
pub wrap_column: Option<u32>,
pub line_wrap_enabled: bool,
pub view_mode: CacheViewMode,
}
impl WrapGeometry {
pub fn key(&self, line_start: usize, pipeline_inputs_version: u64) -> LineWrapKey {
LineWrapKey {
pipeline_inputs_version,
view_mode: self.view_mode,
line_start,
effective_width: self.effective_width as u32,
gutter_width: self.gutter_width as u16,
wrap_column: self.wrap_column,
hanging_indent: self.hanging_indent,
line_wrap_enabled: self.line_wrap_enabled,
}
}
}
pub fn compute_line_layout(
state: &mut EditorState,
line_start: usize,
line_end: usize,
geom: &WrapGeometry,
) -> Vec<ViewLine> {
let is_binary = state.buffer.is_binary();
let line_ending = state.buffer.line_ending();
let estimated_line_length = state.buffer.estimated_line_length();
let tab_size = state.buffer_settings.tab_size;
let mut tokens = build_base_tokens(
&mut state.buffer,
line_start,
estimated_line_length,
1, is_binary,
line_ending,
&[], );
let is_compose = matches!(geom.view_mode, CacheViewMode::Compose);
if is_compose && !state.soft_breaks.is_empty() {
let sb = state
.soft_breaks
.query_viewport(line_start, line_end, &state.marker_list);
if !sb.is_empty() {
tokens = apply_soft_breaks(tokens, &sb);
}
}
if is_compose && !state.conceals.is_empty() {
let cr = state
.conceals
.query_viewport(line_start, line_end, &state.marker_list);
if !cr.is_empty() {
tokens = apply_conceal_ranges(tokens, &cr);
}
}
if geom.line_wrap_enabled {
tokens = apply_wrapping_transform(
tokens,
geom.effective_width,
geom.gutter_width,
geom.hanging_indent,
);
}
let all_lines: Vec<ViewLine> =
ViewLineIterator::new(&tokens, is_binary, !is_binary, tab_size, false).collect();
let mut result = Vec::with_capacity(all_lines.len().min(8));
for (i, line) in all_lines.into_iter().enumerate() {
use crate::view::ui::view_pipeline::LineStart;
if i > 0 && matches!(line.line_start, LineStart::AfterSourceNewline) {
break;
}
result.push(line);
}
if result.is_empty() {
result.push(ViewLine {
text: String::new(),
source_start_byte: Some(line_start),
char_source_bytes: Vec::new(),
char_styles: Vec::new(),
char_visual_cols: Vec::new(),
visual_to_char: Vec::new(),
tab_starts: std::collections::HashSet::new(),
line_start: crate::view::ui::view_pipeline::LineStart::Beginning,
ends_with_newline: false,
});
}
result
}
pub fn count_visual_rows_via_pipeline(
state: &mut EditorState,
line_start: usize,
line_end: usize,
geom: &WrapGeometry,
) -> u32 {
compute_line_layout(state, line_start, line_end, geom).len() as u32
}
#[inline]
pub fn state_pipeline_inputs_version(state: &EditorState) -> u64 {
pipeline_inputs_version(
state.buffer.version(),
state.soft_breaks.version(),
state.conceals.version(),
state.virtual_texts.version(),
)
}
pub fn placeholder_layout_for_row_count(n: u32) -> Vec<ViewLine> {
use crate::view::ui::view_pipeline::LineStart;
(0..n)
.map(|_| ViewLine {
text: String::new(),
source_start_byte: None,
char_source_bytes: Vec::new(),
char_styles: Vec::new(),
char_visual_cols: Vec::new(),
visual_to_char: Vec::new(),
tab_starts: std::collections::HashSet::new(),
line_start: LineStart::Beginning,
ends_with_newline: false,
})
.collect()
}
pub fn count_visual_rows_for_text_with_soft_breaks(
line_text: &str,
line_start: usize,
soft_breaks_in_line: &[(usize, u16)],
effective_width: usize,
gutter_width: usize,
hanging_indent: bool,
) -> u32 {
if soft_breaks_in_line.is_empty() {
return count_visual_rows_for_text(
line_text,
effective_width,
gutter_width,
hanging_indent,
);
}
let mut total: u32 = 0;
let mut prev_end: usize = 0; let mut prev_indent: u16 = 0;
for &(pos, indent) in soft_breaks_in_line {
if pos < line_start {
continue;
}
let rel = pos - line_start;
if rel >= line_text.len() {
continue;
}
if rel < prev_end {
continue;
}
let segment = &line_text[prev_end..rel];
total = total.saturating_add(count_segment_rows_with_indent(
segment,
prev_indent,
effective_width,
gutter_width,
hanging_indent,
));
let consumed = line_text[rel..]
.chars()
.next()
.map(|c| c.len_utf8())
.unwrap_or(0);
prev_end = (rel + consumed).min(line_text.len());
prev_indent = indent;
}
let segment = &line_text[prev_end..];
total = total.saturating_add(count_segment_rows_with_indent(
segment,
prev_indent,
effective_width,
gutter_width,
hanging_indent,
));
total.max(1)
}
fn count_segment_rows_with_indent(
segment: &str,
leading_indent: u16,
effective_width: usize,
gutter_width: usize,
hanging_indent: bool,
) -> u32 {
if segment.is_empty() && leading_indent == 0 {
return 1;
}
if leading_indent == 0 {
return count_visual_rows_for_text(segment, effective_width, gutter_width, hanging_indent);
}
let mut prefixed = String::with_capacity(leading_indent as usize + segment.len());
for _ in 0..leading_indent {
prefixed.push(' ');
}
prefixed.push_str(segment);
count_visual_rows_for_text(&prefixed, effective_width, gutter_width, hanging_indent)
}
pub fn count_visual_rows_for_text(
line_text: &str,
effective_width: usize,
gutter_width: usize,
hanging_indent: bool,
) -> u32 {
use crate::view::ui::split_rendering::transforms::apply_wrapping_transform;
use fresh_core::api::ViewTokenWire;
let tokens = vec![ViewTokenWire {
source_offset: Some(0),
kind: ViewTokenWireKind::Text(line_text.to_string()),
style: None,
}];
let wrapped = apply_wrapping_transform(tokens, effective_width, gutter_width, hanging_indent);
let mut rows: u32 = 0;
let mut row_has_content = false;
for t in &wrapped {
match &t.kind {
ViewTokenWireKind::Newline => break,
ViewTokenWireKind::Break => {
if row_has_content {
rows += 1;
}
row_has_content = false;
}
ViewTokenWireKind::Text(s) => {
if !s.is_empty() {
row_has_content = true;
}
}
ViewTokenWireKind::Space | ViewTokenWireKind::BinaryByte(_) => {
row_has_content = true;
}
}
}
if row_has_content {
rows += 1;
}
rows.max(1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::view::ui::view_pipeline::LineStart;
fn key(line_start: usize, version: u64) -> LineWrapKey {
LineWrapKey {
pipeline_inputs_version: version,
view_mode: CacheViewMode::Source,
line_start,
effective_width: 80,
gutter_width: 6,
wrap_column: None,
hanging_indent: false,
line_wrap_enabled: true,
}
}
fn dummy_lines(n: u32) -> Vec<ViewLine> {
(0..n)
.map(|_| ViewLine {
text: String::new(),
source_start_byte: Some(0),
char_source_bytes: Vec::new(),
char_styles: Vec::new(),
char_visual_cols: Vec::new(),
visual_to_char: Vec::new(),
tab_starts: std::collections::HashSet::new(),
line_start: LineStart::Beginning,
ends_with_newline: false,
})
.collect()
}
const ROOMY: usize = 1024 * 1024;
const TIGHT: usize = 500;
#[test]
fn empty_cache_is_empty() {
let cache = LineWrapCache::default();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert_eq!(cache.current_bytes(), 0);
}
#[test]
fn get_or_insert_caches_on_miss() {
let mut cache = LineWrapCache::with_byte_budget(ROOMY);
let mut compute_calls = 0;
let v = cache.get_or_insert_with(key(100, 1), || {
compute_calls += 1;
dummy_lines(7)
});
assert_eq!(v.len(), 7);
assert_eq!(compute_calls, 1);
assert_eq!(cache.len(), 1);
}
#[test]
fn repeat_lookup_is_a_hit() {
let mut cache = LineWrapCache::with_byte_budget(ROOMY);
let mut compute_calls = 0;
cache.get_or_insert_with(key(100, 1), || {
compute_calls += 1;
dummy_lines(7)
});
let v = cache.get_or_insert_with(key(100, 1), || {
compute_calls += 1;
dummy_lines(99) });
assert_eq!(v.len(), 7);
assert_eq!(compute_calls, 1, "second lookup should be a hit");
}
#[test]
fn different_versions_are_separate_entries() {
let mut cache = LineWrapCache::with_byte_budget(ROOMY);
cache.get_or_insert_with(key(100, 1), || dummy_lines(3));
cache.get_or_insert_with(key(100, 2), || dummy_lines(5));
assert_eq!(cache.get(&key(100, 1)).map(|v| v.len()), Some(3));
assert_eq!(cache.get(&key(100, 2)).map(|v| v.len()), Some(5));
assert_eq!(cache.len(), 2);
}
#[test]
fn evicts_oldest_when_byte_budget_reached() {
let mut cache = LineWrapCache::with_byte_budget(TIGHT);
cache.get_or_insert_with(key(100, 1), || dummy_lines(1));
cache.get_or_insert_with(key(200, 1), || dummy_lines(1));
cache.get_or_insert_with(key(300, 1), || dummy_lines(1));
cache.get_or_insert_with(key(400, 1), || dummy_lines(1));
assert!(cache.current_bytes() <= TIGHT);
assert_eq!(cache.get(&key(100, 1)).is_none(), true, "oldest evicted");
assert!(cache.get(&key(400, 1)).is_some());
}
#[test]
fn structural_invariant_holds_under_many_inserts() {
let mut cache = LineWrapCache::with_byte_budget(TIGHT);
for i in 0..200u64 {
cache.get_or_insert_with(key(i as usize, i), || dummy_lines(1));
assert_eq!(cache.len(), cache.map.len());
assert_eq!(cache.len(), cache.order.len());
assert_eq!(cache.current_bytes <= cache.byte_budget, true);
}
}
#[test]
fn put_overwrites_existing_value_without_reordering() {
let mut cache = LineWrapCache::with_byte_budget(ROOMY);
cache.get_or_insert_with(key(100, 1), || dummy_lines(1));
cache.get_or_insert_with(key(200, 1), || dummy_lines(1));
cache.get_or_insert_with(key(300, 1), || dummy_lines(1));
cache.put(key(200, 1), Arc::new(dummy_lines(42)));
assert_eq!(cache.get(&key(200, 1)).map(|v| v.len()), Some(42));
cache.get_or_insert_with(key(400, 1), || dummy_lines(1));
for k in [100usize, 200, 300, 400] {
assert!(cache.get(&key(k, 1)).is_some(), "k={k} should be present");
}
}
#[test]
fn clear_empties_cache() {
let mut cache = LineWrapCache::with_byte_budget(ROOMY);
cache.get_or_insert_with(key(100, 1), || dummy_lines(1));
cache.get_or_insert_with(key(200, 1), || dummy_lines(1));
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.current_bytes(), 0);
assert!(cache.get(&key(100, 1)).is_none());
}
#[test]
fn pipeline_inputs_version_changes_when_any_source_changes() {
let a = pipeline_inputs_version(100, 5, 3, 7);
assert_ne!(
a,
pipeline_inputs_version(101, 5, 3, 7),
"buffer bump changes version"
);
assert_ne!(
a,
pipeline_inputs_version(100, 6, 3, 7),
"soft-break bump changes version"
);
assert_ne!(
a,
pipeline_inputs_version(100, 5, 4, 7),
"conceal bump changes version"
);
assert_ne!(
a,
pipeline_inputs_version(100, 5, 3, 8),
"virtual-text bump changes version"
);
}
#[test]
#[should_panic]
fn zero_byte_budget_rejected() {
LineWrapCache::with_byte_budget(0);
}
#[test]
fn oversize_entry_is_accepted_then_agable() {
let mut cache = LineWrapCache::with_byte_budget(TIGHT);
cache.get_or_insert_with(key(1, 1), || dummy_lines(50));
assert!(cache.get(&key(1, 1)).is_some());
cache.get_or_insert_with(key(2, 1), || dummy_lines(1));
assert!(cache.get(&key(1, 1)).is_none());
assert!(cache.get(&key(2, 1)).is_some());
}
#[test]
fn empty_line_is_one_row() {
for width in [5usize, 10, 42, 80, 120] {
assert_eq!(count_visual_rows_for_text("", width, 0, false), 1);
assert_eq!(count_visual_rows_for_text("", width, 6, false), 1);
}
}
#[test]
fn line_that_fits_is_one_row() {
for text in ["hello", "hello world", "a b c d"] {
assert_eq!(count_visual_rows_for_text(text, 80, 6, false), 1);
}
}
#[test]
fn width_monotonicity() {
let texts = [
"",
"short",
"a b c d e f g h i j k l m n o",
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"word00 word01 word02 word03 word04 word05 word06 word07",
];
let gutter = 2usize;
for text in &texts {
let mut prev_rows: Option<u32> = None;
for w in [10usize, 15, 20, 30, 50, 80, 120, 200] {
let rows = count_visual_rows_for_text(text, w, gutter, false);
if let Some(prev) = prev_rows {
assert!(
rows <= prev,
"width monotonicity violated: rows({} chars, w={}) = {} > rows at prev w = {}. \
text={:?}",
text.len(),
w,
rows,
prev,
text,
);
}
prev_rows = Some(rows);
}
}
}
#[test]
fn row_count_is_always_at_least_one() {
let cases = [
("", 80usize),
("x", 80),
("", 2), ("abc", 3),
(
"a very long line with lots of words that will definitely wrap",
20,
),
];
for (text, w) in cases {
assert!(
count_visual_rows_for_text(text, w, 0, false) >= 1,
"row count < 1 for text={:?}, width={}",
text,
w,
);
}
}
#[test]
fn prefix_never_has_more_rows() {
let base = "aaaaaaaaaa bbbbbbbbbb cccccccccc dddddddddd eeeeeeeeee";
let width = 20usize;
let gutter = 2usize;
let mut prev_rows: u32 = 0;
for len in (0..=base.len()).step_by(5) {
let prefix = &base[..len];
let rows = count_visual_rows_for_text(prefix, width, gutter, false);
assert!(
rows >= prev_rows,
"prefix property violated: len={}, rows={}, prev_rows={}",
len,
rows,
prev_rows,
);
prev_rows = rows;
}
}
#[test]
fn count_is_deterministic() {
let text = "word00 word01 word02 word03 word04 word05 word06 word07 word08 word09";
let w = 30usize;
let g = 4usize;
let r1 = count_visual_rows_for_text(text, w, g, false);
for _ in 0..16 {
let r = count_visual_rows_for_text(text, w, g, false);
assert_eq!(r, r1, "non-deterministic row count");
}
}
#[test]
fn shadow_agreement_pure_primitive() {
let texts: Vec<String> = (0..30)
.map(|i| {
let n = (i * 7 + 3) % 120 + 5;
let seed = [b'a', b'b', b'c', b' ', b'd', b'e', b'f', b' ', b'1', b'2'];
(0..n).map(|k| seed[k % seed.len()] as char).collect()
})
.collect();
let widths: [usize; 5] = [12, 20, 42, 80, 120];
let mut real = LineWrapCache::with_byte_budget(TIGHT);
for step in 0..400usize {
let t_idx = (step * 37 + 11) % texts.len();
let w_idx = (step * 5 + 3) % widths.len();
let text = &texts[t_idx];
let width = widths[w_idx];
let shadow_rows = count_visual_rows_for_text(text, width, 2, false);
let key = LineWrapKey {
pipeline_inputs_version: 0,
view_mode: CacheViewMode::Source,
line_start: t_idx, effective_width: width as u32,
gutter_width: 2,
wrap_column: None,
hanging_indent: false,
line_wrap_enabled: true,
};
let real_val = real.get_or_insert_with(key, || dummy_lines(shadow_rows));
assert_eq!(
real_val.len() as u32,
shadow_rows,
"shadow disagreement at step {step}: text_idx={t_idx}, width={width}, \
real={}, shadow={shadow_rows}",
real_val.len(),
);
assert!(
real.current_bytes() <= real.byte_budget(),
"cache exceeded byte budget"
);
}
}
#[test]
fn version_bump_makes_old_entry_unreachable() {
let mut cache = LineWrapCache::with_byte_budget(ROOMY);
let key_v0 = LineWrapKey {
pipeline_inputs_version: 100,
view_mode: CacheViewMode::Source,
line_start: 42,
effective_width: 80,
gutter_width: 6,
wrap_column: None,
hanging_indent: false,
line_wrap_enabled: true,
};
cache.get_or_insert_with(key_v0, || dummy_lines(5));
assert_eq!(cache.get(&key_v0).map(|v| v.len()), Some(5));
let key_v1 = LineWrapKey {
pipeline_inputs_version: 101,
..key_v0
};
assert!(
cache.get(&key_v1).is_none(),
"v1 lookup must miss even though v0 entry is still present"
);
let mut miss_called = 0;
let v = cache.get_or_insert_with(key_v1, || {
miss_called += 1;
dummy_lines(7)
});
assert_eq!(v.len(), 7);
assert_eq!(miss_called, 1);
assert_eq!(cache.get(&key_v1).map(|v| v.len()), Some(7));
assert_eq!(
cache.get(&key_v0).map(|v| v.len()),
Some(5),
"v0 entry preserved until evicted"
);
}
#[test]
fn every_key_dimension_separates_entries() {
let base = LineWrapKey {
pipeline_inputs_version: 1,
view_mode: CacheViewMode::Source,
line_start: 10,
effective_width: 80,
gutter_width: 6,
wrap_column: None,
hanging_indent: false,
line_wrap_enabled: true,
};
let variations: [LineWrapKey; 8] = [
LineWrapKey {
pipeline_inputs_version: 2,
..base
},
LineWrapKey {
view_mode: CacheViewMode::Compose,
..base
},
LineWrapKey {
line_start: 11,
..base
},
LineWrapKey {
effective_width: 81,
..base
},
LineWrapKey {
gutter_width: 7,
..base
},
LineWrapKey {
wrap_column: Some(70),
..base
},
LineWrapKey {
hanging_indent: true,
..base
},
LineWrapKey {
line_wrap_enabled: false,
..base
},
];
let mut cache = LineWrapCache::with_byte_budget(ROOMY);
cache.get_or_insert_with(base, || dummy_lines(1));
for (i, v) in variations.iter().enumerate() {
assert_ne!(*v, base, "variation {i} shouldn't equal base");
assert!(
cache.get(v).is_none(),
"variation {i} unexpectedly hit base entry"
);
cache.get_or_insert_with(*v, || dummy_lines(2 + i as u32));
}
assert_eq!(cache.get(&base).map(|v| v.len()), Some(1));
for (i, v) in variations.iter().enumerate() {
assert_eq!(cache.get(v).map(|v| v.len()), Some(2 + i));
}
}
}