From 9dd85dd21e8601a70c00dbebfec3572a3ceac091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=A3=9E?= <1835698775@qq.com> Date: Sun, 20 Sep 2026 18:53:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(terminal=5Fview):=20=E6=97=A0=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E7=9A=84=E8=A1=8C=E4=B8=8D=E5=86=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3=E7=A9=BA=E7=99=BD=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 左边距为每一屏幕行生成时间戳文本,尚未有输出的行(光标下方的空行) 此前会画出 `[ ]` 占位括号,整屏空行看起来全是括号和空白。 改为没有时间戳时不画占位符:只显示行号时补足时间戳列宽的空白以保证 行号对齐,时间戳与行号都不显示时该行留空,由元素侧跳过绘制。列宽与 行号对齐行为不变。 Refs #244 --- .../terminal_view/src/view/terminal_render.rs | 115 ++++++++++++++---- 1 file changed, 93 insertions(+), 22 deletions(-) diff --git a/crates/terminal_view/src/view/terminal_render.rs b/crates/terminal_view/src/view/terminal_render.rs index c86192589..6319afe26 100644 --- a/crates/terminal_view/src/view/terminal_render.rs +++ b/crates/terminal_view/src/view/terminal_render.rs @@ -6,8 +6,6 @@ use terminal::line_timeline::SharedLineTimeline; /// 时间戳列宽(`[HH:MM:SS]`) const TIMESTAMP_COLUMNS: usize = 10; -/// 无时间戳的行用空格占位 -const TIMESTAMP_BLANK: &str = " "; /// 时间戳与行号之间、边距文本与终端内容之间的固定间隔(照 WindTerm 实测 3 格) const MARGIN_GAP: usize = 3; /// 行号列最小宽度(跟随当前最大行号位数) @@ -46,6 +44,45 @@ pub(super) fn line_margin_columns( columns } +/// 单个屏幕行的边距文本:`[HH:MM:SS]` + 3 格 + 行号 + 3 格,长度恒等于 +/// [`line_margin_columns`]。 +/// +/// 尚未有输出的行不画 `[ ]` 占位符:只显示行号时补足时间戳列宽的空白, +/// 保证行号仍然对齐;时间戳与行号都不显示时返回空串(元素侧会跳过绘制)。 +fn line_margin_row_text( + timestamp: Option<&str>, + line_number: usize, + show_timestamps: bool, + show_numbers: bool, + number_digits: usize, +) -> String { + let mut text = String::with_capacity(line_margin_columns( + show_timestamps, + show_numbers, + number_digits, + )); + if show_timestamps { + match timestamp { + // 方括号照 WindTerm,时间线侧只存 `HH:MM:SS`。 + Some(label) => { + text.push('['); + text.push_str(label); + text.push(']'); + text.extend(std::iter::repeat_n(' ', MARGIN_GAP)); + } + None if show_numbers => { + text.extend(std::iter::repeat_n(' ', TIMESTAMP_COLUMNS + MARGIN_GAP)); + } + None => {} + } + } + if show_numbers { + text.push_str(&format!("{:>width$}", line_number, width = number_digits)); + text.extend(std::iter::repeat_n(' ', MARGIN_GAP)); + } + text +} + /// 生成左边距每行文本;时间戳与行号均关闭或处于备用屏幕时返回空。 fn build_line_margin( term: &Term, @@ -71,25 +108,18 @@ fn build_line_margin( continue; } let id = history_size as i64 + row as i64 - display_offset as i64; - let mut text = String::with_capacity(columns); - if show_timestamps { - // 方括号照 WindTerm,时间线侧只存 `HH:MM:SS`。 - text.push('['); - match timeline.label(id) { - Some(label) => text.push_str(&label), - None => text.push_str(TIMESTAMP_BLANK), - } - text.push(']'); - text.extend(std::iter::repeat_n(' ', MARGIN_GAP)); - } - if show_numbers { - text.push_str(&format!( - "{:>width$}", - (id + 1).max(1), - width = number_digits - )); - text.extend(std::iter::repeat_n(' ', MARGIN_GAP)); - } + let label = if show_timestamps { + timeline.label(id) + } else { + None + }; + let text = line_margin_row_text( + label.as_deref(), + (id + 1).max(1) as usize, + show_timestamps, + show_numbers, + number_digits, + ); rows.push(text.into()); } @@ -389,7 +419,9 @@ impl TerminalView { #[cfg(test)] mod tests { - use super::{line_margin_columns, line_number_digits, with_terminal_if_ready}; + use super::{ + line_margin_columns, line_margin_row_text, line_number_digits, with_terminal_if_ready, + }; use alacritty_terminal::sync::FairMutex; #[test] @@ -409,6 +441,45 @@ mod tests { assert_eq!(21, line_margin_columns(true, true, 5)); } + #[test] + fn timestamped_row_keeps_brackets_and_number() { + let text = line_margin_row_text(Some("14:45:38"), 2, true, true, 2); + assert_eq!("[14:45:38] 2 ", text.as_str()); + assert_eq!( + line_margin_columns(true, true, 2), + text.len(), + "边距行文本必须始终填满列宽,否则行号与内容会对不齐" + ); + } + + #[test] + fn row_without_output_renders_no_placeholder_brackets() { + // 有行号:只留时间戳列宽的空白,不画 `[ ]` 占位符。 + let with_number = line_margin_row_text(None, 3, true, true, 2); + assert_eq!(" 3 ", with_number.as_str()); + assert!( + !with_number.contains(['[', ']']), + "没有输出的行不应出现时间戳占位括号:{with_number:?}" + ); + assert_eq!( + line_margin_columns(true, true, 2), + with_number.len(), + "去掉占位符后行号仍须按列宽对齐" + ); + + // 只有时间戳列:整行留空,交给元素侧跳过绘制。 + assert!( + line_margin_row_text(None, 3, true, false, 2).is_empty(), + "仅时间戳模式下的空行不应产生任何可绘制文本" + ); + + // 只有行号列(时间戳关闭):依旧只有行号。 + assert_eq!( + " 3 ", + line_margin_row_text(None, 3, false, true, 2).as_str() + ); + } + #[test] fn terminal_frame_lock_attempt_never_waits_for_parser() { let term = FairMutex::new(1usize); From f1dec7d5a1f7b52a996e78dd1b704e400484d36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=A3=9E?= <1835698775@qq.com> Date: Sun, 20 Sep 2026 19:14:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(terminal=5Fview):=20=E7=A9=BA=E8=A1=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=98=BE=E7=A4=BA=E8=A1=8C=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 左边距改为以“该行是否已有输出”为唯一判定:时间线里没有这一行的时间戳, 说明它还没有任何输出(光标下方的空行),此时整行留空——既不画时间戳 占位括号,也不显示行号。 原来只有显示时间戳时才查时间线,行号列会无条件铺满整屏,空行上挂着 一串孤立的行号。现在两列共用同一个判定。 行时间轴新增一次加锁的按行批量查询 `labels`,视图整帧只加锁一次、不做 逐行查询;原先仅服务视图逐行查询的单行 `label` 已无生产调用方,一并移除, 只保留批量这一条查找路径。 Refs #244 --- crates/terminal/src/line_timeline.rs | 117 ++++++++--- .../terminal_view/src/view/terminal_render.rs | 183 ++++++++++++------ 2 files changed, 210 insertions(+), 90 deletions(-) diff --git a/crates/terminal/src/line_timeline.rs b/crates/terminal/src/line_timeline.rs index 409db3a4d..3e1be0bc1 100644 --- a/crates/terminal/src/line_timeline.rs +++ b/crates/terminal/src/line_timeline.rs @@ -44,10 +44,15 @@ impl SharedLineTimeline { } } - /// 查询某个行 id 的时间戳标签(`HH:MM:SS`)。 - pub fn label(&self, id: i64) -> Option { - 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>> { + match self.0.lock() { + Ok(timeline) => timeline.labels(first_id, count), + Err(_) => vec![None; count], + } } } @@ -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>> { + 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 } } @@ -147,6 +163,12 @@ mod tests { Arc::from(text) } + /// 单行断言的包装:批量查询是唯一查找路径。 + fn label_at(timeline: &LineTimeline, id: i64) -> Option { + 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(); @@ -154,9 +176,9 @@ mod tests { 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] @@ -164,14 +186,14 @@ mod tests { 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] @@ -180,19 +202,19 @@ 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] @@ -200,12 +222,12 @@ mod tests { 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] @@ -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] @@ -228,8 +250,33 @@ 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> = 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> = labels.iter().map(|label| label.as_deref()).collect(); + assert_eq!(vec![Some("10:00:00"), None, None, None], labels); } #[test] @@ -237,9 +284,15 @@ mod tests { 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"); } } diff --git a/crates/terminal_view/src/view/terminal_render.rs b/crates/terminal_view/src/view/terminal_render.rs index 6319afe26..d8be51bdc 100644 --- a/crates/terminal_view/src/view/terminal_render.rs +++ b/crates/terminal_view/src/view/terminal_render.rs @@ -47,34 +47,30 @@ pub(super) fn line_margin_columns( /// 单个屏幕行的边距文本:`[HH:MM:SS]` + 3 格 + 行号 + 3 格,长度恒等于 /// [`line_margin_columns`]。 /// -/// 尚未有输出的行不画 `[ ]` 占位符:只显示行号时补足时间戳列宽的空白, -/// 保证行号仍然对齐;时间戳与行号都不显示时返回空串(元素侧会跳过绘制)。 +/// `label` 是该行的时间戳标签,同时表达“这一行是否已有输出”:`None` 表示还 +/// 没有任何输出(如光标下方的空行),整行留空——既不画 `[ ]` 占位符, +/// 也不显示行号;元素侧会跳过空行。 fn line_margin_row_text( - timestamp: Option<&str>, + label: Option<&str>, line_number: usize, show_timestamps: bool, show_numbers: bool, number_digits: usize, ) -> String { + let Some(label) = label else { + return String::new(); + }; let mut text = String::with_capacity(line_margin_columns( show_timestamps, show_numbers, number_digits, )); if show_timestamps { - match timestamp { - // 方括号照 WindTerm,时间线侧只存 `HH:MM:SS`。 - Some(label) => { - text.push('['); - text.push_str(label); - text.push(']'); - text.extend(std::iter::repeat_n(' ', MARGIN_GAP)); - } - None if show_numbers => { - text.extend(std::iter::repeat_n(' ', TIMESTAMP_COLUMNS + MARGIN_GAP)); - } - None => {} - } + // 方括号照 WindTerm,时间线侧只存 `HH:MM:SS`。 + text.push('['); + text.push_str(label); + text.push(']'); + text.extend(std::iter::repeat_n(' ', MARGIN_GAP)); } if show_numbers { text.push_str(&format!("{:>width$}", line_number, width = number_digits)); @@ -83,7 +79,7 @@ fn line_margin_row_text( text } -/// 生成左边距每行文本;时间戳与行号均关闭或处于备用屏幕时返回空。 +/// 生成左边距每行文本;时间戳与行号均关闭时返回空。 fn build_line_margin( term: &Term, timeline: &SharedLineTimeline, @@ -96,33 +92,35 @@ fn build_line_margin( return LineMargin::default(); } - let history_size = term.history_size(); - let display_offset = term.grid().display_offset(); let screen_lines = term.screen_lines(); // 备用屏幕(vim/less 等 TUI)不展示边距内容。 - let alternate_screen = term.mode().contains(TermMode::ALT_SCREEN); - let mut rows = Vec::with_capacity(screen_lines); - for row in 0..screen_lines { - if alternate_screen { - rows.push(SharedString::default()); - continue; - } - let id = history_size as i64 + row as i64 - display_offset as i64; - let label = if show_timestamps { - timeline.label(id) - } else { - None + if term.mode().contains(TermMode::ALT_SCREEN) { + return LineMargin { + columns, + rows: vec![SharedString::default(); screen_lines], }; - let text = line_margin_row_text( - label.as_deref(), - (id + 1).max(1) as usize, - show_timestamps, - show_numbers, - number_digits, - ); - rows.push(text.into()); } + // 行号与时间戳共用一套坐标:屏幕第 r 行的 id = history_size + r - display_offset, + // 显示行号为 id + 1。时间线在两种列都要用它判定哪些行已经有输出。 + let first_id = term.history_size() as i64 - term.grid().display_offset() as i64; + let rows = timeline + .labels(first_id, screen_lines) + .into_iter() + .enumerate() + .map(|(row, label)| { + let line_number = (first_id + row as i64 + 1).max(1) as usize; + line_margin_row_text( + label.as_deref(), + line_number, + show_timestamps, + show_numbers, + number_digits, + ) + .into() + }) + .collect(); + LineMargin { columns, rows } } @@ -420,9 +418,35 @@ impl TerminalView { #[cfg(test)] mod tests { use super::{ - line_margin_columns, line_margin_row_text, line_number_digits, with_terminal_if_ready, + SharedLineTimeline, build_line_margin, line_margin_columns, line_margin_row_text, + line_number_digits, with_terminal_if_ready, }; + use alacritty_terminal::grid::Dimensions; use alacritty_terminal::sync::FairMutex; + use alacritty_terminal::term::{Config as TermConfig, Term}; + use alacritty_terminal::vte::ansi::{Processor, StdSyncHandler}; + use terminal::line_timeline::LineTimelineSample; + use terminal::pty_backend::GpuiEventProxy; + use tokio::sync::mpsc::unbounded_channel; + + struct MarginTermDimensions { + columns: usize, + screen_lines: usize, + } + + impl Dimensions for MarginTermDimensions { + fn total_lines(&self) -> usize { + self.screen_lines + } + + fn screen_lines(&self) -> usize { + self.screen_lines + } + + fn columns(&self) -> usize { + self.columns + } + } #[test] fn line_number_column_grows_with_line_count() { @@ -453,31 +477,74 @@ mod tests { } #[test] - fn row_without_output_renders_no_placeholder_brackets() { - // 有行号:只留时间戳列宽的空白,不画 `[ ]` 占位符。 - let with_number = line_margin_row_text(None, 3, true, true, 2); - assert_eq!(" 3 ", with_number.as_str()); - assert!( - !with_number.contains(['[', ']']), - "没有输出的行不应出现时间戳占位括号:{with_number:?}" - ); - assert_eq!( - line_margin_columns(true, true, 2), - with_number.len(), - "去掉占位符后行号仍须按列宽对齐" + fn row_without_output_renders_nothing() { + // 还没有输出的行整行留空:既不画 `[ ]` 占位符,也不显示行号。 + for (show_timestamps, show_numbers) in [(true, true), (true, false), (false, true)] { + let text = line_margin_row_text(None, 3, show_timestamps, show_numbers, 2); + assert!( + text.is_empty(), + "无输出行不应渲染任何边距文本(show_timestamps={show_timestamps}, \ + show_numbers={show_numbers}):{text:?}" + ); + } + } + + #[test] + fn line_number_mode_still_renders_for_lines_with_output() { + // 只开行号时,行号仍以“该行是否有输出”为准,而不是无条件铺满屏幕。 + let text = line_margin_row_text(Some("14:45:38"), 3, false, true, 2); + assert_eq!(" 3 ", text.as_str()); + assert_eq!(line_margin_columns(false, true, 2), text.len()); + assert!(!text.contains(['[', ']']), "时间戳关闭时不应出现括号"); + } + + #[test] + fn margin_blanks_rows_below_the_last_output() { + let dimensions = MarginTermDimensions { + columns: 16, + screen_lines: 6, + }; + let (event_tx, _event_rx) = unbounded_channel(); + let mut term = Term::new( + TermConfig::default(), + &dimensions, + GpuiEventProxy::new(event_tx), ); + let mut processor: Processor = Processor::new(); + processor.advance(&mut term, b"$ one\r\ntwo\r\n"); + + // 与 `TerminalModel::sample_line_timeline` 一致地采样:光标停在第 2 行, + // 0..=2 已落时间戳,第 3 行及以后还没有输出。 + let timeline = SharedLineTimeline::default(); + timeline.observe(LineTimelineSample { + history_size: term.history_size(), + screen_lines: term.screen_lines(), + cursor_line: term.grid().cursor.point.line.0, + alternate_screen: false, + scrollback_lines: 1000, + }); - // 只有时间戳列:整行留空,交给元素侧跳过绘制。 + let margin = build_line_margin(&term, &timeline, true, true, 1); + assert_eq!(6, margin.rows.len()); + assert_eq!(line_margin_columns(true, true, 1), margin.columns); + assert_eq!(line_margin_columns(true, true, 1), margin.rows[0].len()); + assert!(margin.rows[0].starts_with('['), "有输出的行仍带时间戳"); + assert!(margin.rows[0].ends_with("1 "), "有输出的行仍带行号"); assert!( - line_margin_row_text(None, 3, true, false, 2).is_empty(), - "仅时间戳模式下的空行不应产生任何可绘制文本" + margin.rows[3..].iter().all(|row| row.is_empty()), + "光标下方的空行整行留空:{:?}", + &margin.rows[3..] ); - // 只有行号列(时间戳关闭):依旧只有行号。 + // 只开行号时同样以“该行是否有输出”为准。 + let numbers_only = build_line_margin(&term, &timeline, false, true, 1); assert_eq!( - " 3 ", - line_margin_row_text(None, 3, false, true, 2).as_str() + line_margin_columns(false, true, 1), + numbers_only.rows[0].len() ); + assert_eq!("1 ", numbers_only.rows[0].as_ref()); + assert_eq!("3 ", numbers_only.rows[2].as_ref()); + assert!(numbers_only.rows[3..].iter().all(|row| row.is_empty())); } #[test]