Skip to content

Commit 8c304af

Browse files
authored
fix: draw_tip_box to account for long words (#1451)
1 parent 5e364fb commit 8c304af

File tree

2 files changed

+128
-4
lines changed

2 files changed

+128
-4
lines changed

crates/q_chat/src/lib.rs

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -689,8 +689,21 @@ impl ChatContext {
689689
}
690690
line.push_str(word);
691691
} else {
692-
wrapped_lines.push(line);
693-
line = word.to_string();
692+
// Here we need to account for words that are too long as well
693+
if word.len() >= inner_width {
694+
let mut start = 0_usize;
695+
for (i, _) in word.chars().enumerate() {
696+
if i - start >= inner_width {
697+
wrapped_lines.push(word[start..i].to_string());
698+
start = i;
699+
}
700+
}
701+
wrapped_lines.push(word[start..].to_string());
702+
line = String::new();
703+
} else {
704+
wrapped_lines.push(line);
705+
line = word.to_string();
706+
}
694707
}
695708
}
696709

@@ -725,15 +738,18 @@ impl ChatContext {
725738
// Centered wrapped content
726739
for line in wrapped_lines {
727740
let visible_line_len = strip_ansi_escapes::strip(&line).len();
728-
let left_pad = (box_width - 4 - visible_line_len) / 2;
741+
let left_pad = box_width.saturating_sub(4).saturating_sub(visible_line_len) / 2;
729742

730743
let content = format!(
731744
"│ {: <pad$}{}{: <rem$} │",
732745
"",
733746
line,
734747
"",
735748
pad = left_pad,
736-
rem = box_width - 4 - left_pad - visible_line_len
749+
rem = box_width
750+
.saturating_sub(4)
751+
.saturating_sub(left_pad)
752+
.saturating_sub(visible_line_len),
737753
);
738754
execute!(self.output, style::Print(format!("{}\n", content)))?;
739755
}
@@ -3449,6 +3465,9 @@ fn create_stream(model_responses: serde_json::Value) -> StreamingClient {
34493465

34503466
#[cfg(test)]
34513467
mod tests {
3468+
use bstr::ByteSlice;
3469+
use shared_writer::TestWriterWithSink;
3470+
34523471
use super::*;
34533472

34543473
#[tokio::test]
@@ -3831,4 +3850,86 @@ mod tests {
38313850
assert_eq!(processed, expected.trim().to_string(), "Failed for input: {}", input);
38323851
}
38333852
}
3853+
3854+
#[tokio::test]
3855+
async fn test_draw_tip_box() {
3856+
let ctx = Context::builder().with_test_home().await.unwrap().build_fake();
3857+
let buf = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
3858+
let test_writer = TestWriterWithSink { sink: buf.clone() };
3859+
let output = SharedWriter::new(test_writer.clone());
3860+
let tool_manager = ToolManager::default();
3861+
let tool_config = serde_json::from_str::<HashMap<String, ToolSpec>>(include_str!("tools/tool_index.json"))
3862+
.expect("Tools failed to load");
3863+
let test_client = create_stream(serde_json::json!([]));
3864+
3865+
let mut chat_context = ChatContext::new(
3866+
Arc::clone(&ctx),
3867+
"fake_conv_id",
3868+
Settings::new_fake(),
3869+
State::new_fake(),
3870+
output,
3871+
None,
3872+
InputSource::new_mock(vec![]),
3873+
true,
3874+
test_client,
3875+
|| Some(80),
3876+
tool_manager,
3877+
None,
3878+
tool_config,
3879+
ToolPermissions::new(0),
3880+
)
3881+
.await
3882+
.unwrap();
3883+
3884+
// Test with a short tip
3885+
let short_tip = "This is a short tip";
3886+
chat_context.draw_tip_box(short_tip).expect("Failed to draw tip box");
3887+
3888+
// Test with a longer tip that should wrap
3889+
let long_tip = "This is a much longer tip that should wrap to multiple lines because it exceeds the inner width of the tip box which is calculated based on the GREETING_BREAK_POINT constant";
3890+
chat_context.draw_tip_box(long_tip).expect("Failed to draw tip box");
3891+
3892+
// Test with a long tip with two long words that should wrap
3893+
let long_tip_with_one_long_word = {
3894+
let mut s = "a".repeat(200);
3895+
s.push(' ');
3896+
s.push_str(&"a".repeat(200));
3897+
s
3898+
};
3899+
chat_context
3900+
.draw_tip_box(long_tip_with_one_long_word.as_str())
3901+
.expect("Failed to draw tip box");
3902+
3903+
// Test with a long tip with two long words that should wrap
3904+
let long_tip_with_two_long_words = "a".repeat(200);
3905+
chat_context
3906+
.draw_tip_box(long_tip_with_two_long_words.as_str())
3907+
.expect("Failed to draw tip box");
3908+
3909+
// Get the output and verify it contains expected formatting elements
3910+
let content = test_writer.get_content();
3911+
let output_str = content.to_str_lossy();
3912+
3913+
// Check for box drawing characters
3914+
assert!(output_str.contains("╭"), "Output should contain top-left corner");
3915+
assert!(output_str.contains("╮"), "Output should contain top-right corner");
3916+
assert!(output_str.contains("│"), "Output should contain vertical lines");
3917+
assert!(output_str.contains("╰"), "Output should contain bottom-left corner");
3918+
assert!(output_str.contains("╯"), "Output should contain bottom-right corner");
3919+
3920+
// Check for the label
3921+
assert!(
3922+
output_str.contains("Did you know?"),
3923+
"Output should contain the 'Did you know?' label"
3924+
);
3925+
3926+
// Check that both tips are present
3927+
assert!(output_str.contains(short_tip), "Output should contain the short tip");
3928+
3929+
// For the long tip, we check for substrings since it will be wrapped
3930+
let long_tip_parts: Vec<&str> = long_tip.split_whitespace().collect();
3931+
for part in long_tip_parts.iter().take(3) {
3932+
assert!(output_str.contains(part), "Output should contain parts of the long tip");
3933+
}
3934+
}
38343935
}

crates/q_chat/src/shared_writer.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,26 @@ impl Write for NullWriter {
6464
Ok(())
6565
}
6666
}
67+
68+
#[derive(Debug, Clone)]
69+
pub struct TestWriterWithSink {
70+
pub sink: Arc<Mutex<Vec<u8>>>,
71+
}
72+
73+
impl TestWriterWithSink {
74+
#[allow(dead_code)]
75+
pub fn get_content(&self) -> Vec<u8> {
76+
self.sink.lock().unwrap().clone()
77+
}
78+
}
79+
80+
impl Write for TestWriterWithSink {
81+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
82+
self.sink.lock().unwrap().append(&mut buf.to_vec());
83+
Ok(buf.len())
84+
}
85+
86+
fn flush(&mut self) -> io::Result<()> {
87+
Ok(())
88+
}
89+
}

0 commit comments

Comments
 (0)