Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 85 additions & 32 deletions crates/terminal/src/line_timeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ impl SharedLineTimeline {
}
}

/// 查询某个行 id 的时间戳标签(`HH:MM:SS`)。
pub fn label(&self, id: i64) -> Option<String> {
let timeline = self.0.lock().ok()?;
timeline.label(id).map(str::to_string)
/// 整帧渲染用:一次取出从 `first_id` 开始 `count` 行的标签,只加锁一次。
///
/// 返回长度恒为 `count`,第 `i` 项对应行 id `first_id + i`;还没有任何输出的
/// 行(以及已滚出保留窗口的行)为 `None`。左边距据此判定该行是否留空。
pub fn labels(&self, first_id: i64, count: usize) -> Vec<Option<Arc<str>>> {
match self.0.lock() {
Ok(timeline) => timeline.labels(first_id, count),
Err(_) => vec![None; count],
}
}
}

Expand Down Expand Up @@ -119,12 +124,23 @@ impl LineTimeline {
}
}

fn label(&self, id: i64) -> Option<&str> {
let index = self
/// 连续行 id 的批量查询:先二分定位起点,再顺序推进,避免逐行查找。
fn labels(&self, first_id: i64, count: usize) -> Vec<Option<Arc<str>>> {
let mut labels = Vec::with_capacity(count);
let mut index = self
.stamps
.binary_search_by_key(&id, |(stamp_id, _)| *stamp_id)
.ok()?;
self.stamps.get(index).map(|(_, label)| label.as_ref())
.binary_search_by_key(&first_id, |(stamp_id, _)| *stamp_id)
.unwrap_or_else(|insert| insert);
for offset in 0..count as i64 {
match self.stamps.get(index) {
Some((stamp_id, label)) if *stamp_id == first_id + offset => {
labels.push(Some(label.clone()));
index += 1;
}
_ => labels.push(None),
}
}
labels
}
}

Expand All @@ -147,31 +163,37 @@ mod tests {
Arc::from(text)
}

/// 单行断言的包装:批量查询是唯一查找路径。
fn label_at(timeline: &LineTimeline, id: i64) -> Option<String> {
let labels = timeline.labels(id, 1);
labels.into_iter().next().flatten().map(|l| l.to_string())
}

#[test]
fn stamps_cursor_advance_without_renumbering_earlier_lines() {
let mut timeline = LineTimeline::default();

timeline.observe_labelled(sample(0, 24, 0), label("10:00:00"));
timeline.observe_labelled(sample(0, 24, 3), label("10:00:01"));

assert_eq!(Some("10:00:00"), timeline.label(0));
assert_eq!(Some("10:00:01"), timeline.label(3));
assert_eq!(None, timeline.label(5));
assert_eq!(Some("10:00:00"), label_at(&timeline, 0).as_deref());
assert_eq!(Some("10:00:01"), label_at(&timeline, 3).as_deref());
assert_eq!(None, label_at(&timeline, 5).as_deref());
}

#[test]
fn scrolling_stamps_new_bottom_lines_only() {
let mut timeline = LineTimeline::default();

timeline.observe_labelled(sample(0, 24, 23), label("10:00:00"));
assert_eq!(Some("10:00:00"), timeline.label(23));
assert_eq!(Some("10:00:00"), label_at(&timeline, 23).as_deref());

// 滚屏一行:缓冲区底部前移,只有新行拿到新时间戳。
timeline.observe_labelled(sample(5, 24, 23), label("10:05:00"));

assert_eq!(Some("10:00:00"), timeline.label(23));
assert_eq!(Some("10:05:00"), timeline.label(28));
assert_eq!(None, timeline.label(29));
assert_eq!(Some("10:00:00"), label_at(&timeline, 23).as_deref());
assert_eq!(Some("10:05:00"), label_at(&timeline, 28).as_deref());
assert_eq!(None, label_at(&timeline, 29).as_deref());
}

#[test]
Expand All @@ -180,32 +202,32 @@ mod tests {

// 首屏:光标走到第 10 行,0..=10 均已落时间戳。
timeline.observe_labelled(sample(0, 24, 10), label("10:00:00"));
assert_eq!(Some("10:00:00"), timeline.label(5));
assert_eq!(Some("10:00:00"), timeline.label(10));
assert_eq!(Some("10:00:00"), label_at(&timeline, 5).as_deref());
assert_eq!(Some("10:00:00"), label_at(&timeline, 10).as_deref());

// 窗口行数变化(reflow):只把游标对齐,不给重排出的行补时间戳。
timeline.observe_labelled(sample(2, 23, 10), label("10:00:05"));
assert_eq!(Some("10:00:00"), timeline.label(5));
assert_eq!(None, timeline.label(11));
assert_eq!(None, timeline.label(12));
assert_eq!(Some("10:00:00"), label_at(&timeline, 5).as_deref());
assert_eq!(None, label_at(&timeline, 11).as_deref());
assert_eq!(None, label_at(&timeline, 12).as_deref());

// reflow 之后新到达的行仍然准确。
timeline.observe_labelled(sample(2, 23, 14), label("10:00:06"));
assert_eq!(Some("10:00:06"), timeline.label(14));
assert_eq!(None, timeline.label(17));
assert_eq!(Some("10:00:06"), label_at(&timeline, 14).as_deref());
assert_eq!(None, label_at(&timeline, 17).as_deref());
}

#[test]
fn clearing_history_restarts_numbering() {
let mut timeline = LineTimeline::default();

timeline.observe_labelled(sample(500, 24, 23), label("10:00:00"));
assert_eq!(Some("10:00:00"), timeline.label(523));
assert_eq!(Some("10:00:00"), label_at(&timeline, 523).as_deref());

timeline.observe_labelled(sample(0, 24, 2), label("10:01:00"));

assert_eq!(None, timeline.label(523));
assert_eq!(Some("10:01:00"), timeline.label(2));
assert_eq!(None, label_at(&timeline, 523).as_deref());
assert_eq!(Some("10:01:00"), label_at(&timeline, 2).as_deref());
}

#[test]
Expand All @@ -217,8 +239,8 @@ mod tests {
alt.alternate_screen = true;
timeline.observe_labelled(alt, label("10:00:10"));

assert_eq!(Some("10:00:00"), timeline.label(4));
assert_eq!(None, timeline.label(9));
assert_eq!(Some("10:00:00"), label_at(&timeline, 4).as_deref());
assert_eq!(None, label_at(&timeline, 9).as_deref());
}

#[test]
Expand All @@ -228,18 +250,49 @@ mod tests {
timeline.observe_labelled(sample(0, 24, 0), label("10:00:00"));
timeline.observe_labelled(sample(5000, 24, 23), label("10:10:00"));

assert_eq!(None, timeline.label(0));
assert_eq!(Some("10:10:00"), timeline.label(5023));
assert_eq!(None, label_at(&timeline, 0).as_deref());
assert_eq!(Some("10:10:00"), label_at(&timeline, 5023).as_deref());
}

#[test]
fn batch_labels_align_by_line_id_across_a_screen_span() {
let mut timeline = LineTimeline::default();
// 光标停在第 2 行:0..=2 已落时间戳,其余屏幕行还没有输出。
timeline.observe_labelled(sample(0, 24, 2), label("10:00:00"));

let labels = timeline.labels(0, 5);
let labels: Vec<Option<&str>> = labels.iter().map(|label| label.as_deref()).collect();
assert_eq!(
vec![
Some("10:00:00"),
Some("10:00:00"),
Some("10:00:00"),
None,
None
],
labels
);

// 窗口起点不在 0 时仍按绝对行 id 对齐(左侧行已滚出保留窗口 / 越界同样为 None)。
let labels = timeline.labels(2, 4);
let labels: Vec<Option<&str>> = labels.iter().map(|label| label.as_deref()).collect();
assert_eq!(vec![Some("10:00:00"), None, None, None], labels);
}

#[test]
fn shared_timeline_exposes_labels() {
let shared = SharedLineTimeline::default();
shared.observe(sample(0, 24, 1));

let label = shared.label(1).expect("sampled line should have a label");
let labels = shared.labels(0, 3);
assert_eq!(3, labels.len(), "返回值长度与请求的屏幕行数一致");

let label = labels[0]
.as_deref()
.expect("sampled line should have a label");
assert_eq!(8, label.len());
assert_eq!(Some(':'), label.chars().nth(2));
assert_eq!(None, shared.label(9));
assert!(labels[1].is_some(), "第 1 行已有输出");
assert!(labels[2].is_none(), "光标下方还没有输出的行为 None");
}
}
Loading
Loading