Skip to content

Commit faf7ab5

Browse files
committed
♻️ Consolidate string truncation utilities and simplify reducer modules
Extract duplicated truncation functions into centralized utils module with truncate_chars (character-based) and truncate_width (unicode display width) functions. Components now import from utils instead of maintaining their own implementations. Simplify reducer architecture by inlining agent event handling into the main reducer and reducing modal/navigation submodules to helper functions only. This eliminates an unnecessary abstraction layer while keeping the code organized. - Add src/studio/utils.rs with comprehensive tests - Remove duplicate truncation functions from 5 components - Delete reducer/agent.rs, consolidate into mod.rs - Reduce modal.rs and navigation.rs to helper functions only
1 parent fda964a commit faf7ab5

File tree

17 files changed

+414
-1113
lines changed

17 files changed

+414
-1113
lines changed

src/studio/components/diff_view.rs

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//!
33
//! Displays git diffs with syntax highlighting for added/removed lines.
44
5+
use crate::studio::theme;
6+
use crate::studio::utils::truncate_width;
57
use ratatui::Frame;
68
use ratatui::layout::Rect;
79
use ratatui::style::{Modifier, Style};
@@ -10,9 +12,6 @@ use ratatui::widgets::{
1012
Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
1113
};
1214
use std::path::PathBuf;
13-
use unicode_width::UnicodeWidthStr;
14-
15-
use crate::studio::theme;
1615

1716
// ═══════════════════════════════════════════════════════════════════════════════
1817
// Diff Types
@@ -534,36 +533,6 @@ pub fn parse_diff(diff_text: &str) -> Vec<FileDiff> {
534533
// Rendering
535534
// ═══════════════════════════════════════════════════════════════════════════════
536535

537-
/// Truncate a line to fit within the given display width (accounting for unicode)
538-
fn truncate_line(content: &str, max_width: usize) -> String {
539-
if max_width == 0 {
540-
return String::new();
541-
}
542-
543-
let content_width = content.width();
544-
if content_width <= max_width {
545-
content.to_string()
546-
} else if max_width <= 1 {
547-
".".to_string()
548-
} else {
549-
// Build string char by char until we hit width limit
550-
let mut result = String::new();
551-
let mut current_width = 0;
552-
let target_width = max_width - 1; // Leave room for ellipsis
553-
554-
for c in content.chars() {
555-
let char_width = c.to_string().width();
556-
if current_width + char_width > target_width {
557-
break;
558-
}
559-
result.push(c);
560-
current_width += char_width;
561-
}
562-
result.push('…');
563-
result
564-
}
565-
}
566-
567536
/// Render the diff view widget
568537
pub fn render_diff_view(
569538
frame: &mut Frame,
@@ -629,14 +598,14 @@ fn render_diff_line(line: &DiffLine, line_num_width: usize, width: usize) -> Lin
629598
match line.line_type {
630599
DiffLineType::FileHeader => {
631600
let content = format!("━━━ {} ", line.content);
632-
let truncated = truncate_line(&content, width);
601+
let truncated = truncate_width(&content, width);
633602
Line::from(vec![Span::styled(truncated, style)])
634603
}
635604
DiffLineType::HunkHeader => {
636605
// " " prefix takes line_num_width * 2 + 3
637606
let prefix_width = line_num_width * 2 + 4;
638607
let max_content = width.saturating_sub(prefix_width);
639-
let truncated = truncate_line(&line.content, max_content);
608+
let truncated = truncate_width(&line.content, max_content);
640609
Line::from(vec![
641610
Span::styled(
642611
format!("{:>width$} ", "", width = line_num_width * 2 + 3),
@@ -671,7 +640,7 @@ fn render_diff_line(line: &DiffLine, line_num_width: usize, width: usize) -> Lin
671640
// Format: "XXXX │ XXXX +content"
672641
let fixed_width = line_num_width * 2 + 6; // " │ " (3) + " " (1) + prefix (1) + padding (1)
673642
let max_content = width.saturating_sub(fixed_width);
674-
let truncated = truncate_line(&line.content, max_content);
643+
let truncated = truncate_width(&line.content, max_content);
675644

676645
Line::from(vec![
677646
Span::styled(old_num, theme::dimmed()),

src/studio/components/file_tree.rs

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::path::{Path, PathBuf};
1414
use unicode_width::UnicodeWidthStr;
1515

1616
use crate::studio::theme;
17+
use crate::studio::utils::truncate_width;
1718

1819
// ═══════════════════════════════════════════════════════════════════════════════
1920
// Git Status
@@ -661,7 +662,7 @@ fn render_entry(entry: &FlatEntry, is_selected: bool, width: usize) -> Line<'sta
661662
let max_name_width = width.saturating_sub(fixed_width);
662663

663664
// Truncate name if needed (using unicode width)
664-
let display_name = truncate_to_width(&entry.name, max_name_width);
665+
let display_name = truncate_width(&entry.name, max_name_width);
665666

666667
Line::from(vec![
667668
Span::styled(marker, marker_style),
@@ -681,39 +682,6 @@ fn render_entry(entry: &FlatEntry, is_selected: bool, width: usize) -> Line<'sta
681682
])
682683
}
683684

684-
/// Truncate a string to fit within the given display width
685-
fn truncate_to_width(s: &str, max_width: usize) -> String {
686-
if max_width == 0 {
687-
return String::new();
688-
}
689-
690-
let s_width = s.width();
691-
if s_width <= max_width {
692-
return s.to_string();
693-
}
694-
695-
if max_width <= 1 {
696-
return ".".to_string();
697-
}
698-
699-
// Find the longest prefix that fits
700-
let mut result = String::new();
701-
let mut current_width = 0;
702-
let target_width = max_width - 1; // Reserve 1 for ellipsis
703-
704-
for c in s.chars() {
705-
let char_width = c.to_string().width();
706-
if current_width + char_width > target_width {
707-
break;
708-
}
709-
result.push(c);
710-
current_width += char_width;
711-
}
712-
713-
result.push('…');
714-
result
715-
}
716-
717685
/// Get icon for file based on extension (Unicode symbols, no emoji)
718686
fn get_file_icon(name: &str) -> &'static str {
719687
// Check for special filenames first

src/studio/components/message_editor.rs

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22
//!
33
//! Text editor for commit messages using tui-textarea.
44
5+
use crate::studio::theme;
6+
use crate::studio::utils::truncate_width;
7+
use crate::types::GeneratedMessage;
58
use crossterm::event::{KeyCode, KeyEvent};
69
use ratatui::Frame;
710
use ratatui::layout::Rect;
811
use ratatui::style::{Modifier, Style};
912
use ratatui::text::{Line, Span};
1013
use ratatui::widgets::{Block, Borders, Paragraph};
1114
use tui_textarea::TextArea;
12-
use unicode_width::UnicodeWidthStr;
13-
14-
use crate::studio::theme;
15-
use crate::types::GeneratedMessage;
1615

1716
// ═══════════════════════════════════════════════════════════════════════════════
1817
// Message Editor State
@@ -300,35 +299,6 @@ pub fn render_message_editor(
300299
}
301300
}
302301

303-
/// Truncate a string to fit within the given display width (accounting for unicode)
304-
fn truncate_str(s: &str, max_width: usize) -> String {
305-
if max_width == 0 {
306-
return String::new();
307-
}
308-
let s_width = s.width();
309-
if s_width <= max_width {
310-
s.to_string()
311-
} else if max_width <= 1 {
312-
".".to_string()
313-
} else {
314-
// Build string char by char until we hit width limit
315-
let mut result = String::new();
316-
let mut current_width = 0;
317-
let target_width = max_width - 1; // Leave room for ellipsis
318-
319-
for c in s.chars() {
320-
let char_width = c.to_string().width();
321-
if current_width + char_width > target_width {
322-
break;
323-
}
324-
result.push(c);
325-
current_width += char_width;
326-
}
327-
result.push('…');
328-
result
329-
}
330-
}
331-
332302
/// Render the message in view mode (non-editing)
333303
fn render_message_view(frame: &mut Frame, area: Rect, state: &MessageEditorState) {
334304
let Some(msg) = state.current_generated() else {
@@ -345,7 +315,7 @@ fn render_message_view(frame: &mut Frame, area: Rect, state: &MessageEditorState
345315
} else {
346316
width.saturating_sub(emoji.chars().count() + 1)
347317
};
348-
let title = truncate_str(&msg.title, title_width);
318+
let title = truncate_width(&msg.title, title_width);
349319

350320
if emoji.is_empty() {
351321
lines.push(Line::from(Span::styled(
@@ -372,7 +342,7 @@ fn render_message_view(frame: &mut Frame, area: Rect, state: &MessageEditorState
372342

373343
// Body (truncated lines)
374344
for body_line in msg.message.lines() {
375-
let truncated = truncate_str(body_line, width);
345+
let truncated = truncate_width(body_line, width);
376346
lines.push(Line::from(Span::styled(
377347
truncated,
378348
Style::default().fg(theme::TEXT_PRIMARY),
@@ -402,7 +372,7 @@ pub fn render_message_preview(msg: &GeneratedMessage, width: usize) -> Line<'sta
402372
} else {
403373
width.saturating_sub(emoji.chars().count() + 1)
404374
};
405-
let title = truncate_str(&msg.title, title_width);
375+
let title = truncate_width(&msg.title, title_width);
406376

407377
if emoji.is_empty() {
408378
Line::from(Span::styled(title, theme::dimmed()))

src/studio/history.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,8 @@ const MAX_CHAT_MESSAGES: usize = 500;
2929
/// Max content versions per (mode, `content_type`) key
3030
const MAX_CONTENT_VERSIONS: usize = 50;
3131

32-
/// UTF-8 safe string truncation (no panic on multi-byte boundaries)
33-
fn truncate_preview(s: &str, max_chars: usize) -> String {
34-
if s.chars().count() <= max_chars {
35-
s.to_string()
36-
} else {
37-
format!("{}...", s.chars().take(max_chars).collect::<String>())
38-
}
39-
}
40-
4132
use super::state::Mode;
33+
use super::utils::truncate_chars;
4234

4335
// ═══════════════════════════════════════════════════════════════════════════════
4436
// Session Metadata (for persistence)
@@ -281,7 +273,7 @@ impl History {
281273
},
282274
change: HistoryChange::ChatMessage {
283275
role,
284-
preview: truncate_preview(content, 100),
276+
preview: truncate_chars(content, 100),
285277
},
286278
};
287279

@@ -321,7 +313,7 @@ impl History {
321313
},
322314
change: HistoryChange::ChatMessage {
323315
role,
324-
preview: truncate_preview(content, 100),
316+
preview: truncate_chars(content, 100),
325317
},
326318
};
327319

src/studio/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod reducer;
2424
mod render;
2525
mod state;
2626
mod theme;
27+
pub mod utils;
2728

2829
// Submodules
2930
pub mod components;

0 commit comments

Comments
 (0)