@@ -6,7 +6,7 @@ use log_viewer_core::io::progressive_reader::{
IndexerMessage , ProgressiveFileReader , VisualHeightIndex , compute_line_visual_height ,
spawn_indexer ,
} ;
use log_viewer_core ::io ::wrap ::{ format_json_line , wrap_line_chars } ;
use log_viewer_core ::io ::wrap ::{ format_json_line , wrap_line_chars , MAX_WRAP_INPUT_LEN };
use log_viewer_core ::types ::LogLevel ;
use log_viewer_core ::watcher ::file_watcher ::{ FileEvent , FileWatcher } ;
@@ -201,12 +201,33 @@ impl App {
/// Compute a single line's viewport entry (wrapped rows + level + height).
fn compute_line_entry ( & self , line : usize , width : usize ) -> ViewportEntry {
let raw = self . get_line ( line ) . unwrap_or_default ( ) ;
// Guard 1: oversized raw input — skip detect_level and JSON formatting
// to avoid O(n) parsing overhead on huge lines.
if raw . len ( ) > MAX_WRAP_INPUT_LEN {
return ViewportEntry {
wrapped_rows : vec ! [ truncate_to_columns ( & raw , width ) ] ,
level : None ,
visual_height : 1 ,
} ;
}
let level = log_viewer_core ::parser ::level ::detect_level ( & raw ) ;
let display_text = if self . json_format {
format_json_line ( & raw )
} else {
raw
} ;
// Guard 2: JSON pretty-printing may expand a line beyond the limit.
if display_text . len ( ) > MAX_WRAP_INPUT_LEN {
return ViewportEntry {
wrapped_rows : vec ! [ truncate_to_columns ( & display_text , width ) ] ,
level ,
visual_height : 1 ,
} ;
}
let mut wrapped = Vec ::new ( ) ;
for sub_line in display_text . split ( '\n' ) {
wrapped . extend ( wrap_line_chars ( sub_line , width ) ) ;
@@ -222,11 +243,23 @@ impl App {
/// Compute visual height for a single line without storing it.
fn compute_visual_height ( & self , line : usize , width : usize ) -> usize {
let raw = self . get_line ( line ) . unwrap_or_default ( ) ;
// Guard 1: oversized raw input.
if raw . len ( ) > MAX_WRAP_INPUT_LEN {
return 1 ;
}
let display_text = if self . json_format {
format_json_line ( & raw )
} else {
raw
} ;
// Guard 2: post-format expansion.
if display_text . len ( ) > MAX_WRAP_INPUT_LEN {
return 1 ;
}
let mut height = 0 ;
for sub_line in display_text . split ( '\n' ) {
height + = wrap_line_chars ( sub_line , width ) . len ( ) ;
@@ -246,7 +279,6 @@ impl App {
/// Returns (start_logical, offset_in_line) for rendering.
pub ( crate ) fn ensure_viewport_cache ( & mut self , width : usize ) -> ( usize , usize ) {
let viewport_height = self . content_height as usize ;
let v_offset = self . v_offset ;
if ! self . is_loaded ( ) | | width = = 0 | | viewport_height = = 0 {
return ( 0 , 0 ) ;
@@ -268,7 +300,7 @@ impl App {
// Find start logical line from v_offset
let ( start_logical , offset_in_line ) = if self . is_loading ( ) {
( v_offset . min ( self . total_lines ( ) . saturating_sub ( 1 ) ) , self . v_sub_offset )
( self . v_offset . min ( self . total_lines ( ) . saturating_sub ( 1 ) ) , self . v_sub_offset )
} else {
self . find_logical_line_at_visual_row ( self . v_offset , width )
} ;
@@ -660,12 +692,15 @@ impl App {
self . json_format = ! self . json_format ;
self . viewport_cache . invalidate ( ) ;
let width = self . viewport_cache . width ;
let ( new_offset , new_sub ) = self . rebase_offset_for_invalidate ( ) ;
if let AppLoadingState ::Ready { reader } = & mut self . loading_state {
reader . invalidate_visual_height_index ( ) ;
if width > 0 {
reader . start_visual_height_rebuild ( width , self . json_format ) ;
}
}
self . v_offset = new_offset ;
self . v_sub_offset = new_sub ;
self . last_g_press = None ;
}
KeyCode ::Char ( 's' ) | KeyCode ::Char ( 'S' )
@@ -824,6 +859,25 @@ impl App {
}
}
/// Compute the rebased offset pair (logical_line, sub_row) from the
/// current visual-row `v_offset`. Returns `(v_offset, v_sub_offset)`
/// suitable for the no-VHI fallback scrolling path.
///
/// MUST be called before borrowing `&mut self.loading_state` for
/// `invalidate_visual_height_index`, because it reads VHI through
/// `&self`.
fn rebase_offset_for_invalidate ( & self ) -> ( usize , usize ) {
if self . get_visual_height_index ( ) . is_some ( ) {
let top_visual = self . v_offset ;
let top_line = self . visual_row_to_logical_row ( top_visual ) ;
let line_first_visual = self . cursor_to_first_visual_row ( top_line ) ;
let sub = top_visual . saturating_sub ( line_first_visual ) ;
( top_line , sub )
} else {
( self . v_offset , self . v_sub_offset )
}
}
fn ensure_visual_height_index ( & mut self , width : usize ) {
let needs_rebuild = match self . get_visual_height_index ( ) {
Some ( idx ) = > ! idx . is_valid_for ( self . json_format , width ) ,
@@ -831,10 +885,13 @@ impl App {
} ;
if needs_rebuild {
let ( new_offset , new_sub ) = self . rebase_offset_for_invalidate ( ) ;
if let AppLoadingState ::Ready { reader } = & mut self . loading_state {
reader . invalidate_visual_height_index ( ) ;
reader . start_visual_height_rebuild ( width , self . json_format ) ;
}
self . v_offset = new_offset ;
self . v_sub_offset = new_sub ;
}
}
@@ -917,6 +974,7 @@ impl App {
fn handle_file_appended ( & mut self ) {
let width = self . get_content_width ( ) ;
let rebased = self . rebase_offset_for_invalidate ( ) ;
match & mut self . loading_state {
AppLoadingState ::Ready { reader } = > {
let old_reader_line_count = reader . line_count ( ) ;
@@ -967,6 +1025,9 @@ impl App {
index . extend_from_heights ( & new_heights ) ;
}
} else {
let ( new_offset , new_sub ) = rebased ;
self . v_offset = new_offset ;
self . v_sub_offset = new_sub ;
reader . invalidate_visual_height_index ( ) ;
reader . start_visual_height_rebuild ( width , self . json_format ) ;
}
@@ -975,9 +1036,11 @@ impl App {
}
Ok ( log_viewer_core ::io ::file_reader ::AppendStatus ::Reloaded ) = > {
let _ = reader . save_cache ( ) ;
let ( new_offset , _new_sub ) = rebased ;
reader . invalidate_visual_height_index ( ) ;
reader . start_visual_height_rebuild ( width , self . json_format ) ;
self . cursor_line = self . cursor_line . min ( self . total_lines ( ) . saturating_sub ( 1 ) ) ;
self . v_offset = new_offset ;
self . v_sub_offset = 0 ;
self . viewport_cache . invalidate ( ) ;
self . clamp_v_offset ( ) ;
@@ -994,12 +1057,14 @@ impl App {
fn reload_ready_reader ( & mut self ) {
let width = self . get_content_width ( ) ;
let ( new_offset , _new_sub ) = self . rebase_offset_for_invalidate ( ) ;
if let AppLoadingState ::Ready { reader } = & mut self . loading_state {
let _ = reader . reload ( ) ;
let _ = reader . save_cache ( ) ;
reader . invalidate_visual_height_index ( ) ;
reader . start_visual_height_rebuild ( width , self . json_format ) ;
self . cursor_line = self . cursor_line . min ( self . total_lines ( ) . saturating_sub ( 1 ) ) ;
self . v_offset = new_offset ;
self . v_sub_offset = 0 ;
self . viewport_cache . invalidate ( ) ;
self . clamp_v_offset ( ) ;
@@ -1031,10 +1096,23 @@ impl App {
} = & mut reader . state
{
* visual_height_index = Some ( index ) ;
}
}
}
// Recalibrate v_offset from logical → visual-row now that VHI is available.
// Must be outside the `reader` borrow above.
if self . get_visual_height_index ( ) . is_some ( ) {
let logical_top = self . v_offset . min ( self . total_lines ( ) . saturating_sub ( 1 ) ) ;
let sub = self . v_sub_offset ;
let first_visual = self . cursor_to_first_visual_row ( logical_top ) ;
let line_height = self
. get_visual_height_index ( )
. map_or ( 1 , | idx | idx . visual_height_of_line ( logical_top ) ) ;
self . v_offset = first_visual . saturating_add ( sub . min ( line_height . saturating_sub ( 1 ) ) ) ;
self . v_sub_offset = 0 ;
self . clamp_v_offset ( ) ;
self . viewport_cache . invalidate ( ) ;
}
}
}
// Poll main indexer (Loading state)
let old_state = std ::mem ::replace ( & mut self . loading_state , AppLoadingState ::Empty ) ;
@@ -1079,9 +1157,12 @@ impl App {
// Gutter width changes (~N → N) shift content_width, so any
// VisualHeightIndex built with the old width is stale.
let ( new_offset , new_sub ) = self . rebase_offset_for_invalidate ( ) ;
if let AppLoadingState ::Ready { reader } = & mut self . loading_state {
reader . invalidate_visual_height_index ( ) ;
}
self . v_offset = new_offset ;
self . v_sub_offset = new_sub ;
if self . reload_after_loading {
self . reload_after_loading = false ;
@@ -1106,6 +1187,13 @@ impl App {
}
}
fn truncate_to_columns ( s : & str , max_cols : usize ) -> String {
if max_cols = = 0 | | s . is_empty ( ) {
return String ::new ( ) ;
}
wrap_line_chars ( s , max_cols ) . into_iter ( ) . next ( ) . unwrap_or_default ( )
}
#[ cfg(test) ]
mod tests {
use super ::* ;
@@ -2538,6 +2626,42 @@ plain text line
assert! ( result . is_ok ( ) ) ;
}
/// Regression test for issue #24:
/// ensure_viewport_cache must use the updated self.v_offset after params_changed
/// recalculates it, not a stale local captured before the change block.
#[ test ]
fn test_loading_viewport_cache_uses_updated_v_offset_on_params_changed ( ) {
let content : String = ( 0 .. 200 ) . map ( | i | format! ( " line {i} \n " ) ) . collect ( ) ;
let path = make_temp_file ( & content ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
app . load_file ( path . to_str ( ) . unwrap ( ) ) . unwrap ( ) ;
assert! ( app . is_loading ( ) , " should be in Loading state " ) ;
app . content_height = 10 ;
app . ensure_viewport_cache ( 80 ) ;
app . v_offset = 90 ;
app . cursor_line = 100 ;
let new_width = 40 ;
app . ensure_viewport_cache ( new_width ) ;
let recomputed_offset = app . v_offset ;
assert_ne! (
recomputed_offset , 90 ,
" v_offset should have been recalculated by params_changed block, still 90 "
) ;
assert_eq! (
app . viewport_cache . logical_start , recomputed_offset . min ( app . total_lines ( ) . saturating_sub ( 1 ) ) ,
" logical_start should match the updated v_offset "
) ;
} ) ;
cleanup ( & path ) ;
assert! ( result . is_ok ( ) ) ;
}
fn install_vhi ( app : & mut App , heights : & [ usize ] ) {
let width = app . get_content_width ( ) ;
let json_format = app . json_format ;
@@ -3091,4 +3215,337 @@ plain text line
} ) ;
assert! ( result . is_ok ( ) ) ;
}
// ── Issue #25 regression tests ──────────────────────────────────
// Ready state without VHI must keep v_offset self-consistent.
#[ test ]
fn test_rebase_offset_converts_visual_to_logical_with_sub ( ) {
let content = " line0 \n line1 \n line2 \n line3 \n line4 \n line5 \n line6 \n line7 \n line8 \n line9 \n " ;
let path = make_temp_file ( content ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
app . content_height = 10 ;
// 10 lines × 3 visual rows each = 30 visual rows
install_vhi ( & mut app , & [ 3 usize ; 10 ] ) ;
// v_offset=7 → visual row 7 is inside line2 (rows 6-8), sub=1
app . v_offset = 7 ;
app . v_sub_offset = 0 ;
let ( logical , sub ) = app . rebase_offset_for_invalidate ( ) ;
assert_eq! ( logical , 2 , " visual row 7 → logical line 2 (rows 6-8) " ) ;
assert_eq! ( sub , 1 , " visual row 7 is sub-row 1 within line 2 " ) ;
// v_offset=0 → line 0, sub=0
app . v_offset = 0 ;
let ( logical , sub ) = app . rebase_offset_for_invalidate ( ) ;
assert_eq! ( logical , 0 ) ;
assert_eq! ( sub , 0 ) ;
// v_offset=5 → line1 (rows 3-5), sub=2
app . v_offset = 5 ;
let ( logical , sub ) = app . rebase_offset_for_invalidate ( ) ;
assert_eq! ( logical , 1 , " visual row 5 → logical line 1 (rows 3-5) " ) ;
assert_eq! ( sub , 2 ) ;
cleanup ( & path ) ;
} ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_rebase_offset_passthrough_when_no_vhi ( ) {
let content = " line0 \n line1 \n line2 \n " ;
let path = make_temp_file ( content ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
// No VHI installed — rebase should return current values unchanged
app . v_offset = 42 ;
app . v_sub_offset = 3 ;
let ( logical , sub ) = app . rebase_offset_for_invalidate ( ) ;
assert_eq! ( logical , 42 ) ;
assert_eq! ( sub , 3 ) ;
cleanup ( & path ) ;
} ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_tab_toggle_rebases_offset_before_invalidate ( ) {
let content = " line0 \n line1 \n line2 \n line3 \n line4 \n line5 \n line6 \n line7 \n line8 \n line9 \n " ;
let path = make_temp_file ( content ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
app . content_height = 10 ;
app . content_width = 20 ;
install_vhi ( & mut app , & [ 3 usize ; 10 ] ) ;
// Scroll to visual row 7 → line2 sub-row 1
app . v_offset = 7 ;
app . cursor_line = 2 ;
app . v_sub_offset = 0 ;
// Simulate Tab toggle: rebase, then invalidate
let rebased = app . rebase_offset_for_invalidate ( ) ;
let ( new_offset , new_sub ) = rebased ;
app . v_offset = new_offset ;
app . v_sub_offset = new_sub ;
assert_eq! ( app . v_offset , 2 , " v_offset should be logical line 2 after rebase " ) ;
assert_eq! ( app . v_sub_offset , 1 , " v_sub_offset should preserve sub-row 1 " ) ;
// Key invariant: v_offset is now a valid logical line number,
// not a stale visual-row offset that would cause a jump.
// Before the fix, v_offset would still be 7 (visual) treated as logical → jump.
assert! (
app . v_offset < app . total_lines ( ) ,
" v_offset ({}) must be a valid logical line index < {} " ,
app . v_offset , app . total_lines ( )
) ;
cleanup ( & path ) ;
} ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_vhi_rebuild_recalibrates_from_viewport_top ( ) {
let content = " line0 \n line1 \n line2 \n line3 \n line4 \n line5 \n line6 \n line7 \n line8 \n line9 \n " ;
let path = make_temp_file ( content ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
app . content_height = 10 ;
app . content_width = 20 ;
// Install VHI with varying heights
install_vhi ( & mut app , & [ 2 , 4 , 3 , 1 , 2 , 5 , 1 , 3 , 2 , 1 ] ) ;
// Scroll to visual row 8 → line2 (rows 6-8), sub=2
app . v_offset = 8 ;
app . cursor_line = 2 ;
app . v_sub_offset = 0 ;
// Rebase (simulates invalidate)
let ( logical_top , sub ) = app . rebase_offset_for_invalidate ( ) ;
assert_eq! ( logical_top , 2 ) ;
assert_eq! ( sub , 2 ) ;
// Simulate what happens: v_offset becomes logical, VHI cleared
app . v_offset = logical_top ;
app . v_sub_offset = sub ;
// Now simulate VHI rebuild completion — the recalibration block
// Install a fresh VHI (same heights, simulates rebuild result)
install_vhi ( & mut app , & [ 2 , 4 , 3 , 1 , 2 , 5 , 1 , 3 , 2 , 1 ] ) ;
// The recalibration logic from poll_background_indexer
let logical_top = app . v_offset . min ( app . total_lines ( ) . saturating_sub ( 1 ) ) ;
let sub = app . v_sub_offset ;
let first_visual = app . cursor_to_first_visual_row ( logical_top ) ;
let line_height = app
. get_visual_height_index ( )
. map_or ( 1 , | idx | idx . visual_height_of_line ( logical_top ) ) ;
app . v_offset = first_visual . saturating_add ( sub . min ( line_height . saturating_sub ( 1 ) ) ) ;
app . v_sub_offset = 0 ;
// line2 starts at visual row 6, sub was 2 → visual row 8
assert_eq! ( app . v_offset , 8 , " v_offset should map back to visual row 8 " ) ;
assert_eq! ( app . v_sub_offset , 0 , " v_sub_offset should be 0 after recalibration " ) ;
// Scrolling should work normally with VHI
app . scroll_down_line ( ) ;
assert_eq! ( app . v_offset , 9 , " scroll down should advance visual row " ) ;
cleanup ( & path ) ;
} ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_vhi_rebuild_clamps_sub_to_new_line_height ( ) {
let content = " line0 \n line1 \n line2 \n " ;
let path = make_temp_file ( content ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
app . content_height = 24 ;
app . content_width = 20 ;
// Line heights: line0=1, line1=5 (wraps), line2=1
install_vhi ( & mut app , & [ 1 , 5 , 1 ] ) ;
// Position at line1 sub-row 4 (last sub-row of 5-row line)
app . v_offset = 5 ;
app . cursor_line = 1 ;
app . v_sub_offset = 0 ;
// Rebase
let ( logical_top , sub ) = app . rebase_offset_for_invalidate ( ) ;
assert_eq! ( logical_top , 1 , " visual row 5 → line 1 (rows 1-5) " ) ;
assert_eq! ( sub , 4 , " sub-row 4 within line 1 " ) ;
app . v_offset = logical_top ;
app . v_sub_offset = sub ;
// Simulate VHI rebuild with SHRUNK line height (e.g., JSON toggle)
// line1 now only 2 visual rows instead of 5
install_vhi ( & mut app , & [ 1 , 2 , 1 ] ) ;
// Recalibration should clamp sub=4 to new height-1=1
let logical_top = app . v_offset . min ( app . total_lines ( ) . saturating_sub ( 1 ) ) ;
let sub = app . v_sub_offset ;
let first_visual = app . cursor_to_first_visual_row ( logical_top ) ;
let line_height = app
. get_visual_height_index ( )
. map_or ( 1 , | idx | idx . visual_height_of_line ( logical_top ) ) ;
let clamped_sub = sub . min ( line_height . saturating_sub ( 1 ) ) ;
app . v_offset = first_visual . saturating_add ( clamped_sub ) ;
app . v_sub_offset = 0 ;
// line1 starts at visual row 1, clamped sub=1 → visual row 2
assert_eq! ( app . v_offset , 2 , " v_offset should be clamped to visual row 2 (line1 row 1 of 2) " ) ;
assert_eq! ( app . v_sub_offset , 0 ) ;
cleanup ( & path ) ;
} ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_loading_to_ready_rebases_visual_offset ( ) {
let content = " line0 \n line1 \n line2 \n line3 \n line4 \n " ;
let path = make_temp_file ( content ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
app . content_height = 10 ;
// Install VHI: 2 rows per line
install_vhi ( & mut app , & [ 2 usize ; 5 ] ) ;
// Scroll to visual row 3 → line1 sub-row 1
app . v_offset = 3 ;
app . cursor_line = 1 ;
app . v_sub_offset = 0 ;
// Simulate Loading→Ready invalidation rebase
let ( new_offset , new_sub ) = app . rebase_offset_for_invalidate ( ) ;
assert_eq! ( new_offset , 1 , " visual row 3 → line 1 " ) ;
assert_eq! ( new_sub , 1 , " sub-row 1 within line 1 " ) ;
// Apply rebased values and clear VHI (simulate invalidate)
app . v_offset = new_offset ;
app . v_sub_offset = new_sub ;
if let AppLoadingState ::Ready { reader } = & mut app . loading_state {
reader . invalidate_visual_height_index ( ) ;
}
// VHI is now None → scroll_down_line uses else branch (v_sub_offset path)
// "line1" at width=80 → compute_visual_height=1, sub=1+1 >= 1 → advance
app . scroll_down_line ( ) ;
assert_eq! ( app . v_offset , 2 , " should advance to line 2 (line1 height=1, sub overflow) " ) ;
assert_eq! ( app . v_sub_offset , 0 ) ;
cleanup ( & path ) ;
} ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_compute_line_entry_oversized_raw_guard ( ) {
let long_line = " x " . repeat ( MAX_WRAP_INPUT_LEN + 1 ) ;
let path = make_temp_file ( & format! ( " short \n {} \n " , long_line ) ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
assert! ( app . is_loaded ( ) ) ;
let entry = app . compute_line_entry ( 1 , 80 ) ;
assert_eq! ( entry . visual_height , 1 , " oversized line should have height 1 " ) ;
assert! ( entry . level . is_none ( ) , " oversized raw line should skip detect_level " ) ;
assert_eq! ( entry . wrapped_rows . len ( ) , 1 ) ;
assert! (
entry . wrapped_rows [ 0 ] . len ( ) < = 80 ,
" preview should be bounded to width "
) ;
let short_entry = app . compute_line_entry ( 0 , 80 ) ;
assert_eq! ( short_entry . visual_height , 1 ) ;
assert! ( short_entry . level . is_none ( ) , " 'short' has no log level " ) ;
} ) ;
cleanup ( & path ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_compute_visual_height_oversized_raw_guard ( ) {
let long_line = " a " . repeat ( MAX_WRAP_INPUT_LEN + 100 ) ;
let path = make_temp_file ( & format! ( " hi \n {} \n " , long_line ) ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
assert! ( app . is_loaded ( ) ) ;
assert_eq! ( app . compute_visual_height ( 0 , 80 ) , 1 , " normal line height=1 " ) ;
assert_eq! ( app . compute_visual_height ( 1 , 80 ) , 1 , " oversized line height=1 " ) ;
} ) ;
cleanup ( & path ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_compute_line_entry_post_format_guard ( ) {
// Build a JSON object whose raw form is just under MAX_WRAP_INPUT_LEN
// but whose pretty-printed form (with added spaces around ':') exceeds it.
let inner = " x " . repeat ( MAX_WRAP_INPUT_LEN - 11 ) ;
let json_line = format! ( r # "{{"msg":"{}"}}"# , inner ) ;
assert! (
json_line . len ( ) < MAX_WRAP_INPUT_LEN ,
" raw JSON should be under limit: got {} " ,
json_line . len ( )
) ;
let path = make_temp_file ( & format! ( " {} \n " , json_line ) ) ;
let result = std ::panic ::catch_unwind ( | | {
let mut app = App ::new ( ) ;
load_file_ready ( & mut app , & path ) ;
app . json_format = true ;
let entry = app . compute_line_entry ( 0 , 80 ) ;
assert_eq! ( entry . visual_height , 1 , " post-format oversized should have height 1 " ) ;
assert_eq! ( entry . wrapped_rows . len ( ) , 1 ) ;
} ) ;
cleanup ( & path ) ;
assert! ( result . is_ok ( ) ) ;
}
#[ test ]
fn test_truncate_to_columns_basic ( ) {
assert_eq! ( truncate_to_columns ( " hello world " , 5 ) , " hello " ) ;
assert_eq! ( truncate_to_columns ( " hi " , 80 ) , " hi " ) ;
assert_eq! ( truncate_to_columns ( " " , 80 ) , " " ) ;
assert_eq! ( truncate_to_columns ( " abc " , 0 ) , " " ) ;
}
#[ test ]
fn test_truncate_to_columns_cjk ( ) {
// Each CJK char is 2 columns wide
assert_eq! ( truncate_to_columns ( " 你好世界 " , 3 ) , " 你 " ) ;
assert_eq! ( truncate_to_columns ( " 你好世界 " , 4 ) , " 你好 " ) ;
}
#[ test ]
fn test_truncate_to_columns_tab ( ) {
// Tab expands to 4 spaces
assert_eq! ( truncate_to_columns ( " a \t b " , 3 ) , " a " ) ;
}
}