mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Diff wrapping: width-aware patch summary rendering with hanging indent\n\n- Add PatchSummaryCell with custom render to compute wrapping using the actual area width\n- Refactor diff_render: expose create_diff_summary_with_width + render_patch_details_with_width\n- Use conservative width fallback when exact width unavailable\n- Keep existing DiffCell; only patch summary/details affected\n- Derive Clone+Copy for PatchEventType
This commit is contained in:
@@ -30,6 +30,18 @@ pub(super) fn create_diff_summary(
|
||||
title: &str,
|
||||
changes: &HashMap<PathBuf, FileChange>,
|
||||
event_type: PatchEventType,
|
||||
) -> Vec<RtLine<'static>> {
|
||||
create_diff_summary_with_width(title, changes, event_type, None)
|
||||
}
|
||||
|
||||
/// Same as `create_diff_summary` but allows specifying a target content width in columns.
|
||||
/// When `width_cols` is provided, wrapping for detailed diff lines uses that width to
|
||||
/// ensure hanging indents align within the caller’s render area.
|
||||
pub(super) fn create_diff_summary_with_width(
|
||||
title: &str,
|
||||
changes: &HashMap<PathBuf, FileChange>,
|
||||
event_type: PatchEventType,
|
||||
width_cols: Option<usize>,
|
||||
) -> Vec<RtLine<'static>> {
|
||||
struct FileSummary {
|
||||
display_path: String,
|
||||
@@ -172,17 +184,32 @@ pub(super) fn create_diff_summary(
|
||||
);
|
||||
|
||||
if show_details {
|
||||
out.extend(render_patch_details(changes));
|
||||
out.extend(render_patch_details_with_width(changes, width_cols));
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
|
||||
pub(super) fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
|
||||
render_patch_details_with_width(changes, None)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn render_patch_details_with_width(
|
||||
changes: &HashMap<PathBuf, FileChange>,
|
||||
width_cols: Option<usize>,
|
||||
) -> Vec<RtLine<'static>> {
|
||||
let mut out: Vec<RtLine<'static>> = Vec::new();
|
||||
// Use terminal width or a reasonable fallback
|
||||
let term_cols: usize = terminal::size().map(|(w, _)| w as usize).unwrap_or(120);
|
||||
// Use caller-provided width or fall back to a conservative estimate based on terminal width.
|
||||
// Subtract a gutter safety margin so our pre-wrapping rarely exceeds the
|
||||
// actual chat content width (prevents secondary wrapping that breaks hanging indents).
|
||||
let term_cols: usize = if let Some(w) = width_cols {
|
||||
w as usize
|
||||
} else {
|
||||
let full = terminal::size().map(|(w, _)| w as usize).unwrap_or(120);
|
||||
full.saturating_sub(20).max(40)
|
||||
};
|
||||
|
||||
for (index, (path, change)) in changes.iter().enumerate() {
|
||||
let is_first_file = index == 0;
|
||||
@@ -197,7 +224,7 @@ fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'s
|
||||
FileChange::Add { content } => {
|
||||
for (i, raw) in content.lines().enumerate() {
|
||||
let ln = i + 1;
|
||||
out.extend(push_wrapped_diff_line(
|
||||
out.extend(push_wrapped_diff_line_with_width(
|
||||
ln,
|
||||
DiffLineType::Insert,
|
||||
raw,
|
||||
@@ -209,7 +236,7 @@ fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'s
|
||||
let original = std::fs::read_to_string(path).unwrap_or_default();
|
||||
for (i, raw) in original.lines().enumerate() {
|
||||
let ln = i + 1;
|
||||
out.extend(push_wrapped_diff_line(
|
||||
out.extend(push_wrapped_diff_line_with_width(
|
||||
ln,
|
||||
DiffLineType::Delete,
|
||||
raw,
|
||||
@@ -240,32 +267,32 @@ fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'s
|
||||
match l {
|
||||
diffy::Line::Insert(text) => {
|
||||
let s = text.trim_end_matches('\n');
|
||||
out.extend(push_wrapped_diff_line(
|
||||
new_ln,
|
||||
DiffLineType::Insert,
|
||||
s,
|
||||
term_cols,
|
||||
));
|
||||
out.extend(push_wrapped_diff_line_with_width(
|
||||
new_ln,
|
||||
DiffLineType::Insert,
|
||||
s,
|
||||
term_cols,
|
||||
));
|
||||
new_ln += 1;
|
||||
}
|
||||
diffy::Line::Delete(text) => {
|
||||
let s = text.trim_end_matches('\n');
|
||||
out.extend(push_wrapped_diff_line(
|
||||
old_ln,
|
||||
DiffLineType::Delete,
|
||||
s,
|
||||
term_cols,
|
||||
));
|
||||
out.extend(push_wrapped_diff_line_with_width(
|
||||
old_ln,
|
||||
DiffLineType::Delete,
|
||||
s,
|
||||
term_cols,
|
||||
));
|
||||
old_ln += 1;
|
||||
}
|
||||
diffy::Line::Context(text) => {
|
||||
let s = text.trim_end_matches('\n');
|
||||
out.extend(push_wrapped_diff_line(
|
||||
new_ln,
|
||||
DiffLineType::Context,
|
||||
s,
|
||||
term_cols,
|
||||
));
|
||||
out.extend(push_wrapped_diff_line_with_width(
|
||||
new_ln,
|
||||
DiffLineType::Context,
|
||||
s,
|
||||
term_cols,
|
||||
));
|
||||
old_ln += 1;
|
||||
new_ln += 1;
|
||||
}
|
||||
@@ -292,7 +319,7 @@ pub(super) fn create_diff_details_only(
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn push_wrapped_diff_line(
|
||||
fn push_wrapped_diff_line_with_width(
|
||||
line_number: usize,
|
||||
kind: DiffLineType,
|
||||
text: &str,
|
||||
@@ -545,7 +572,7 @@ mod tests {
|
||||
// Call the wrapping function directly so we can precisely control the width
|
||||
// Use a fixed width for testing wrapping behavior
|
||||
const TEST_WRAP_WIDTH: usize = 80;
|
||||
let lines = push_wrapped_diff_line(1, DiffLineType::Insert, long_line, TEST_WRAP_WIDTH);
|
||||
let lines = push_wrapped_diff_line_with_width(1, DiffLineType::Insert, long_line, TEST_WRAP_WIDTH);
|
||||
|
||||
// Render into a small terminal to capture the visual layout
|
||||
snapshot_lines(
|
||||
|
||||
@@ -12,7 +12,7 @@ use codex_core::plan_tool::PlanItemArg;
|
||||
use codex_core::plan_tool::StepStatus;
|
||||
use codex_core::plan_tool::UpdatePlanArgs;
|
||||
use codex_core::protocol::FileChange;
|
||||
use crate::diff_render::create_diff_summary;
|
||||
use crate::diff_render::create_diff_summary_with_width;
|
||||
use codex_core::protocol::McpInvocation;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
@@ -45,6 +45,7 @@ pub(crate) struct CommandOutput {
|
||||
pub(crate) stderr: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum PatchEventType {
|
||||
ApprovalRequest,
|
||||
ApplyBegin { auto_approved: bool },
|
||||
@@ -2801,21 +2802,16 @@ pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlainHistoryCell {
|
||||
pub(crate) fn new_patch_event(
|
||||
event_type: PatchEventType,
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
) -> PlainHistoryCell {
|
||||
) -> PatchSummaryCell {
|
||||
let title = match event_type {
|
||||
PatchEventType::ApprovalRequest => "proposed patch",
|
||||
PatchEventType::ApplyBegin { auto_approved: true } => "Updating...",
|
||||
PatchEventType::ApplyBegin { auto_approved: false } => "Updating...",
|
||||
PatchEventType::ApprovalRequest => "proposed patch".to_string(),
|
||||
PatchEventType::ApplyBegin { .. } => "Updating...".to_string(),
|
||||
};
|
||||
|
||||
let lines: Vec<Line<'static>> = create_diff_summary(title, &changes, event_type)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let kind = match title {
|
||||
"proposed patch" => HistoryCellType::Patch { kind: PatchKind::Proposed },
|
||||
_ => HistoryCellType::Patch { kind: PatchKind::ApplyBegin },
|
||||
let kind = match event_type {
|
||||
PatchEventType::ApprovalRequest => PatchKind::Proposed,
|
||||
PatchEventType::ApplyBegin { .. } => PatchKind::ApplyBegin,
|
||||
};
|
||||
PlainHistoryCell { lines, kind }
|
||||
PatchSummaryCell { title, changes, event_type, kind }
|
||||
}
|
||||
|
||||
pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
|
||||
@@ -2832,6 +2828,69 @@ pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
|
||||
PlainHistoryCell { lines, kind: HistoryCellType::Patch { kind: PatchKind::ApplyFailure } }
|
||||
}
|
||||
|
||||
// ==================== PatchSummaryCell ====================
|
||||
// Renders patch summary + details with width-aware hanging indents so wrapped
|
||||
// diff lines align under their code indentation.
|
||||
|
||||
pub(crate) struct PatchSummaryCell {
|
||||
pub(crate) title: String,
|
||||
pub(crate) changes: HashMap<PathBuf, FileChange>,
|
||||
pub(crate) event_type: PatchEventType,
|
||||
pub(crate) kind: PatchKind,
|
||||
}
|
||||
|
||||
impl HistoryCell for PatchSummaryCell {
|
||||
fn as_any(&self) -> &dyn std::any::Any { self }
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
|
||||
fn kind(&self) -> HistoryCellType { HistoryCellType::Patch { kind: self.kind } }
|
||||
|
||||
// We compute lines based on width at render time; provide a conservative
|
||||
// default for non-width callers (not normally used in our pipeline).
|
||||
fn display_lines(&self) -> Vec<Line<'static>> {
|
||||
create_diff_summary_with_width(&self.title, &self.changes, self.event_type, Some(80))
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn has_custom_render(&self) -> bool { true }
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let lines: Vec<Line<'static>> = create_diff_summary_with_width(
|
||||
&self.title,
|
||||
&self.changes,
|
||||
self.event_type,
|
||||
Some(width as usize),
|
||||
)
|
||||
.into_iter()
|
||||
.collect();
|
||||
Paragraph::new(Text::from(lines))
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(width)
|
||||
.try_into()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn custom_render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_rows: u16) {
|
||||
let lines: Vec<Line<'static>> = create_diff_summary_with_width(
|
||||
&self.title,
|
||||
&self.changes,
|
||||
self.event_type,
|
||||
Some(area.width as usize),
|
||||
)
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let text = Text::from(lines);
|
||||
let bg_block = Block::default().style(Style::default().bg(crate::colors::background()));
|
||||
Paragraph::new(text)
|
||||
.block(bg_block)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((skip_rows, 0))
|
||||
.style(Style::default().bg(crate::colors::background()))
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// new_patch_apply_success was removed in favor of in-place header mutation and type update in chatwidget
|
||||
|
||||
// ==================== Spacing Helper ====================
|
||||
|
||||
Reference in New Issue
Block a user