From 3962be29524353707fa4f13afeac638e3e8f14cd Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 3 May 2026 08:59:25 +0000 Subject: [PATCH] =?UTF-8?q?review(p9-fb-10):=20=ED=9A=8C=EC=B0=A8=202=20?= =?UTF-8?q?=EC=A7=80=EC=A0=81=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TAGS_COL_W const 추출 (truncate + pad 동시 사용 — drift 방지). - format_doc_row 직접 unit test 2개 (Hangul title / Hangul tag) 가 display column 정렬을 정확히 pin. `` 으로 되돌리는 회귀가 unit-level 에서 catch 됨. --- crates/kebab-tui/src/library.rs | 62 +++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/crates/kebab-tui/src/library.rs b/crates/kebab-tui/src/library.rs index 5e82d83..2a7f711 100644 --- a/crates/kebab-tui/src/library.rs +++ b/crates/kebab-tui/src/library.rs @@ -17,6 +17,11 @@ use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; use crate::app::{App, KeyOutcome, Pane}; use crate::input::{display_width, truncate_to_display_width}; +/// Width (in display columns) of the `tags` column in the doc-list +/// row. Used twice — truncate input + pad calculation — so a const +/// keeps them in sync. +const TAGS_COL_W: usize = 12; + /// Internal state owned by `LibraryState`. Public-by-crate so /// `handle_key_library` can mutate it without crossing the /// `pub`-visibility boundary `LibraryState` exposes. @@ -193,7 +198,7 @@ pub(crate) fn format_doc_row(d: &DocSummary, title_w: usize) -> String { } else { d.tags.join(",") }; - let tags = truncate_to_display_width(&tags, 12); + let tags = truncate_to_display_width(&tags, TAGS_COL_W); let updated = d .updated_at .format(&time::format_description::well_known::Rfc3339) @@ -206,7 +211,7 @@ pub(crate) fn format_doc_row(d: &DocSummary, title_w: usize) -> String { // from `display_width`, then concatenate — the truncate above // already guarantees `display_width(title) <= title_w`. let title_pad = title_w.saturating_sub(display_width(&title)); - let tags_pad = 12usize.saturating_sub(display_width(&tags)); + let tags_pad = TAGS_COL_W.saturating_sub(display_width(&tags)); format!( "{title}{:title_pad$} {tags}{:tags_pad$} {updated_short:<10} {chunk_count}", "", @@ -404,3 +409,56 @@ pub(crate) fn refresh_docs(state: &mut App) -> anyhow::Result<()> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use kebab_core::{ + ChunkerVersion, DocSummary, DocumentId, Lang, ParserVersion, SourceType, TrustLevel, + WorkspacePath, + }; + use time::OffsetDateTime; + + fn doc(title: &str, tags: &[&str]) -> DocSummary { + DocSummary { + doc_id: DocumentId("a".repeat(32)), + doc_path: WorkspacePath::new("x.md".into()).unwrap(), + title: title.into(), + lang: Lang("en".into()), + tags: tags.iter().map(|s| (*s).into()).collect(), + trust_level: TrustLevel::Primary, + source_type: SourceType::Note, + byte_len: 1, + chunk_count: 1, + created_at: OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(), + updated_at: OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(), + parser_version: ParserVersion("p".into()), + chunker_version: ChunkerVersion("c".into()), + } + } + + /// p9-fb-10: format_doc_row pads by display width (not char + /// count) so wide-char titles don't shift downstream columns. + /// Regression pin — `` (std::fmt char-count form) + /// would fail this for any Hangul title. + #[test] + fn format_doc_row_pads_by_display_width_for_hangul_title() { + let row = format_doc_row(&doc("러스트로 만드는 KB", &["rust"]), 30); + // Expected layout (display cols): + // title 30 + " "(2) + tags 12 + " "(2) + date 10 + " "(2) + chunk + // chunk = "1" → 1 col. Total = 30+2+12+2+10+2+1 = 59. + assert_eq!( + display_width(&row), + 59, + "row must align to display columns, not char count: {row:?}" + ); + } + + /// p9-fb-10: Hangul tag also pads by display width. + #[test] + fn format_doc_row_pads_by_display_width_for_hangul_tag() { + let row = format_doc_row(&doc("ascii", &["한글"]), 20); + // title 20 + " " + tags 12 + " " + date 10 + " " + "1" = 49 + assert_eq!(display_width(&row), 49, "row: {row:?}"); + } +}