feat(core): extract wrap utilities and extend LineIndex for progressive loading
Move wrap_line_chars and format_json_line from app.rs to core/io/wrap.rs with MAX_WRAP_INPUT_LEN guard. Add serde derives, pub getters, and extend_from_bytes() to LineIndex for incremental index building. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
135
crates/core/src/io/wrap.rs
Normal file
135
crates/core/src/io/wrap.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
/// Maximum input length for wrap/format operations (10 MB).
|
||||
/// Lines exceeding this are returned as-is to avoid pathological cases.
|
||||
pub const MAX_WRAP_INPUT_LEN: usize = 10 * 1024 * 1024;
|
||||
|
||||
/// Split a line into chunks of exactly `width` characters (display columns).
|
||||
/// For a log viewer, we want character-level wrapping, not word-level.
|
||||
pub fn wrap_line_chars(line: &str, width: usize) -> Vec<String> {
|
||||
if width == 0 {
|
||||
return vec![String::new()];
|
||||
}
|
||||
if line.is_empty() {
|
||||
return vec![String::new()];
|
||||
}
|
||||
let mut result = Vec::new();
|
||||
let mut row = String::new();
|
||||
let mut col = 0;
|
||||
for ch in line.chars() {
|
||||
let w = if ch == '\t' { 4 } else { 1 };
|
||||
if col + w > width && !row.is_empty() {
|
||||
result.push(std::mem::take(&mut row));
|
||||
col = 0;
|
||||
}
|
||||
if ch == '\t' {
|
||||
row.push_str(" ");
|
||||
col += 4;
|
||||
} else {
|
||||
row.push(ch);
|
||||
col += w;
|
||||
}
|
||||
if col >= width {
|
||||
result.push(std::mem::take(&mut row));
|
||||
col = 0;
|
||||
}
|
||||
}
|
||||
if !row.is_empty() {
|
||||
result.push(row);
|
||||
}
|
||||
if result.is_empty() {
|
||||
result.push(String::new());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Format a line as pretty-printed JSON if it's a JSON Object.
|
||||
/// Returns the original line unchanged for non-JSON or non-Object content.
|
||||
pub fn format_json_line(line: &str) -> String {
|
||||
if line.trim().is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
// Quick pre-check: only try parsing if it starts with '{'
|
||||
if !line.trim_start().starts_with('{') {
|
||||
return line.to_string();
|
||||
}
|
||||
match serde_json::from_str::<serde_json::Value>(line) {
|
||||
Ok(value) if value.is_object() => {
|
||||
serde_json::to_string_pretty(&value).unwrap_or_else(|_| line.to_string())
|
||||
}
|
||||
_ => line.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_wrap_empty_line() {
|
||||
let result = wrap_line_chars("", 80);
|
||||
assert_eq!(result, vec![""]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_zero_width() {
|
||||
let result = wrap_line_chars("hello", 0);
|
||||
assert_eq!(result, vec![""]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_short_line() {
|
||||
let result = wrap_line_chars("hello", 80);
|
||||
assert_eq!(result, vec!["hello"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_exact_width() {
|
||||
let result = wrap_line_chars("abc", 3);
|
||||
assert_eq!(result, vec!["abc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_multi_row() {
|
||||
let result = wrap_line_chars("abcdef", 3);
|
||||
assert_eq!(result, vec!["abc", "def"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_with_tab() {
|
||||
let result = wrap_line_chars("a\tb", 4);
|
||||
assert_eq!(result, vec!["a", " ", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json_empty() {
|
||||
assert_eq!(format_json_line(""), "");
|
||||
assert_eq!(format_json_line(" "), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json_non_json() {
|
||||
assert_eq!(format_json_line("hello world"), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json_valid_object() {
|
||||
let input = r#"{"key":"value"}"#;
|
||||
let output = format_json_line(input);
|
||||
assert!(
|
||||
output.contains('\n'),
|
||||
"pretty-printed JSON should have newlines"
|
||||
);
|
||||
assert!(output.contains("key"));
|
||||
assert!(output.contains("value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json_array_unchanged() {
|
||||
let input = r#"[1,2,3]"#;
|
||||
assert_eq!(format_json_line(input), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_wrap_input_len_constant() {
|
||||
assert_eq!(MAX_WRAP_INPUT_LEN, 10 * 1024 * 1024);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user