mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
90f262e9a4
Introduce a full codex-tui source snapshot under the new codex-tui2 crate so viewport work can be replayed in isolation. This change copies the entire codex-rs/tui/src tree into codex-rs/tui2/src in one atomic step, rather than piecemeal, to keep future diffs vs the original viewport bookmark easy to reason about. The goal is for codex-tui2 to render identically to the existing TUI behind the `features.tui2` flag while we gradually port the viewport/history commits from the joshka/viewport bookmark onto this forked tree. While on this baseline change, we also ran the codex-tui2 snapshot test suite and accepted all insta snapshots for the new crate, so the snapshot files now use the codex-tui2 naming scheme and encode the unmodified legacy TUI behavior. This keeps later viewport commits focused on intentional behavior changes (and their snapshots) rather than on mechanical snapshot renames.
674 lines
23 KiB
Rust
674 lines
23 KiB
Rust
use diffy::Hunk;
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::style::Color;
|
|
use ratatui::style::Modifier;
|
|
use ratatui::style::Style;
|
|
use ratatui::style::Stylize;
|
|
use ratatui::text::Line as RtLine;
|
|
use ratatui::text::Span as RtSpan;
|
|
use ratatui::widgets::Paragraph;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::exec_command::relativize_to_home;
|
|
use crate::render::Insets;
|
|
use crate::render::line_utils::prefix_lines;
|
|
use crate::render::renderable::ColumnRenderable;
|
|
use crate::render::renderable::InsetRenderable;
|
|
use crate::render::renderable::Renderable;
|
|
use codex_core::git_info::get_git_repo_root;
|
|
use codex_core::protocol::FileChange;
|
|
|
|
// Internal representation for diff line rendering
|
|
enum DiffLineType {
|
|
Insert,
|
|
Delete,
|
|
Context,
|
|
}
|
|
|
|
pub struct DiffSummary {
|
|
changes: HashMap<PathBuf, FileChange>,
|
|
cwd: PathBuf,
|
|
}
|
|
|
|
impl DiffSummary {
|
|
pub fn new(changes: HashMap<PathBuf, FileChange>, cwd: PathBuf) -> Self {
|
|
Self { changes, cwd }
|
|
}
|
|
}
|
|
|
|
impl Renderable for FileChange {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
let mut lines = vec![];
|
|
render_change(self, &mut lines, area.width as usize);
|
|
Paragraph::new(lines).render(area, buf);
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
let mut lines = vec![];
|
|
render_change(self, &mut lines, width as usize);
|
|
lines.len() as u16
|
|
}
|
|
}
|
|
|
|
impl From<DiffSummary> for Box<dyn Renderable> {
|
|
fn from(val: DiffSummary) -> Self {
|
|
let mut rows: Vec<Box<dyn Renderable>> = vec![];
|
|
|
|
for (i, row) in collect_rows(&val.changes).into_iter().enumerate() {
|
|
if i > 0 {
|
|
rows.push(Box::new(RtLine::from("")));
|
|
}
|
|
let mut path = RtLine::from(display_path_for(&row.path, &val.cwd));
|
|
path.push_span(" ");
|
|
path.extend(render_line_count_summary(row.added, row.removed));
|
|
rows.push(Box::new(path));
|
|
rows.push(Box::new(RtLine::from("")));
|
|
rows.push(Box::new(InsetRenderable::new(
|
|
Box::new(row.change) as Box<dyn Renderable>,
|
|
Insets::tlbr(0, 2, 0, 0),
|
|
)));
|
|
}
|
|
|
|
Box::new(ColumnRenderable::with(rows))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn create_diff_summary(
|
|
changes: &HashMap<PathBuf, FileChange>,
|
|
cwd: &Path,
|
|
wrap_cols: usize,
|
|
) -> Vec<RtLine<'static>> {
|
|
let rows = collect_rows(changes);
|
|
render_changes_block(rows, wrap_cols, cwd)
|
|
}
|
|
|
|
// Shared row for per-file presentation
|
|
#[derive(Clone)]
|
|
struct Row {
|
|
#[allow(dead_code)]
|
|
path: PathBuf,
|
|
move_path: Option<PathBuf>,
|
|
added: usize,
|
|
removed: usize,
|
|
change: FileChange,
|
|
}
|
|
|
|
fn collect_rows(changes: &HashMap<PathBuf, FileChange>) -> Vec<Row> {
|
|
let mut rows: Vec<Row> = Vec::new();
|
|
for (path, change) in changes.iter() {
|
|
let (added, removed) = match change {
|
|
FileChange::Add { content } => (content.lines().count(), 0),
|
|
FileChange::Delete { content } => (0, content.lines().count()),
|
|
FileChange::Update { unified_diff, .. } => calculate_add_remove_from_diff(unified_diff),
|
|
};
|
|
let move_path = match change {
|
|
FileChange::Update {
|
|
move_path: Some(new),
|
|
..
|
|
} => Some(new.clone()),
|
|
_ => None,
|
|
};
|
|
rows.push(Row {
|
|
path: path.clone(),
|
|
move_path,
|
|
added,
|
|
removed,
|
|
change: change.clone(),
|
|
});
|
|
}
|
|
rows.sort_by_key(|r| r.path.clone());
|
|
rows
|
|
}
|
|
|
|
fn render_line_count_summary(added: usize, removed: usize) -> Vec<RtSpan<'static>> {
|
|
let mut spans = Vec::new();
|
|
spans.push("(".into());
|
|
spans.push(format!("+{added}").green());
|
|
spans.push(" ".into());
|
|
spans.push(format!("-{removed}").red());
|
|
spans.push(")".into());
|
|
spans
|
|
}
|
|
|
|
fn render_changes_block(rows: Vec<Row>, wrap_cols: usize, cwd: &Path) -> Vec<RtLine<'static>> {
|
|
let mut out: Vec<RtLine<'static>> = Vec::new();
|
|
|
|
let render_path = |row: &Row| -> Vec<RtSpan<'static>> {
|
|
let mut spans = Vec::new();
|
|
spans.push(display_path_for(&row.path, cwd).into());
|
|
if let Some(move_path) = &row.move_path {
|
|
spans.push(format!(" → {}", display_path_for(move_path, cwd)).into());
|
|
}
|
|
spans
|
|
};
|
|
|
|
// Header
|
|
let total_added: usize = rows.iter().map(|r| r.added).sum();
|
|
let total_removed: usize = rows.iter().map(|r| r.removed).sum();
|
|
let file_count = rows.len();
|
|
let noun = if file_count == 1 { "file" } else { "files" };
|
|
let mut header_spans: Vec<RtSpan<'static>> = vec!["• ".dim()];
|
|
if let [row] = &rows[..] {
|
|
let verb = match &row.change {
|
|
FileChange::Add { .. } => "Added",
|
|
FileChange::Delete { .. } => "Deleted",
|
|
_ => "Edited",
|
|
};
|
|
header_spans.push(verb.bold());
|
|
header_spans.push(" ".into());
|
|
header_spans.extend(render_path(row));
|
|
header_spans.push(" ".into());
|
|
header_spans.extend(render_line_count_summary(row.added, row.removed));
|
|
} else {
|
|
header_spans.push("Edited".bold());
|
|
header_spans.push(format!(" {file_count} {noun} ").into());
|
|
header_spans.extend(render_line_count_summary(total_added, total_removed));
|
|
}
|
|
out.push(RtLine::from(header_spans));
|
|
|
|
for (idx, r) in rows.into_iter().enumerate() {
|
|
// Insert a blank separator between file chunks (except before the first)
|
|
if idx > 0 {
|
|
out.push("".into());
|
|
}
|
|
// File header line (skip when single-file header already shows the name)
|
|
let skip_file_header = file_count == 1;
|
|
if !skip_file_header {
|
|
let mut header: Vec<RtSpan<'static>> = Vec::new();
|
|
header.push(" â”” ".dim());
|
|
header.extend(render_path(&r));
|
|
header.push(" ".into());
|
|
header.extend(render_line_count_summary(r.added, r.removed));
|
|
out.push(RtLine::from(header));
|
|
}
|
|
|
|
let mut lines = vec![];
|
|
render_change(&r.change, &mut lines, wrap_cols - 4);
|
|
out.extend(prefix_lines(lines, " ".into(), " ".into()));
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
fn render_change(change: &FileChange, out: &mut Vec<RtLine<'static>>, width: usize) {
|
|
match change {
|
|
FileChange::Add { content } => {
|
|
let line_number_width = line_number_width(content.lines().count());
|
|
for (i, raw) in content.lines().enumerate() {
|
|
out.extend(push_wrapped_diff_line(
|
|
i + 1,
|
|
DiffLineType::Insert,
|
|
raw,
|
|
width,
|
|
line_number_width,
|
|
));
|
|
}
|
|
}
|
|
FileChange::Delete { content } => {
|
|
let line_number_width = line_number_width(content.lines().count());
|
|
for (i, raw) in content.lines().enumerate() {
|
|
out.extend(push_wrapped_diff_line(
|
|
i + 1,
|
|
DiffLineType::Delete,
|
|
raw,
|
|
width,
|
|
line_number_width,
|
|
));
|
|
}
|
|
}
|
|
FileChange::Update { unified_diff, .. } => {
|
|
if let Ok(patch) = diffy::Patch::from_str(unified_diff) {
|
|
let mut max_line_number = 0;
|
|
for h in patch.hunks() {
|
|
let mut old_ln = h.old_range().start();
|
|
let mut new_ln = h.new_range().start();
|
|
for l in h.lines() {
|
|
match l {
|
|
diffy::Line::Insert(_) => {
|
|
max_line_number = max_line_number.max(new_ln);
|
|
new_ln += 1;
|
|
}
|
|
diffy::Line::Delete(_) => {
|
|
max_line_number = max_line_number.max(old_ln);
|
|
old_ln += 1;
|
|
}
|
|
diffy::Line::Context(_) => {
|
|
max_line_number = max_line_number.max(new_ln);
|
|
old_ln += 1;
|
|
new_ln += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let line_number_width = line_number_width(max_line_number);
|
|
let mut is_first_hunk = true;
|
|
for h in patch.hunks() {
|
|
if !is_first_hunk {
|
|
let spacer = format!("{:width$} ", "", width = line_number_width.max(1));
|
|
let spacer_span = RtSpan::styled(spacer, style_gutter());
|
|
out.push(RtLine::from(vec![spacer_span, "â‹®".dim()]));
|
|
}
|
|
is_first_hunk = false;
|
|
|
|
let mut old_ln = h.old_range().start();
|
|
let mut new_ln = h.new_range().start();
|
|
for l in h.lines() {
|
|
match l {
|
|
diffy::Line::Insert(text) => {
|
|
let s = text.trim_end_matches('\n');
|
|
out.extend(push_wrapped_diff_line(
|
|
new_ln,
|
|
DiffLineType::Insert,
|
|
s,
|
|
width,
|
|
line_number_width,
|
|
));
|
|
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,
|
|
width,
|
|
line_number_width,
|
|
));
|
|
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,
|
|
width,
|
|
line_number_width,
|
|
));
|
|
old_ln += 1;
|
|
new_ln += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn display_path_for(path: &Path, cwd: &Path) -> String {
|
|
let path_in_same_repo = match (get_git_repo_root(cwd), get_git_repo_root(path)) {
|
|
(Some(cwd_repo), Some(path_repo)) => cwd_repo == path_repo,
|
|
_ => false,
|
|
};
|
|
let chosen = if path_in_same_repo {
|
|
pathdiff::diff_paths(path, cwd).unwrap_or_else(|| path.to_path_buf())
|
|
} else {
|
|
relativize_to_home(path)
|
|
.map(|p| PathBuf::from_iter([Path::new("~"), p.as_path()]))
|
|
.unwrap_or_else(|| path.to_path_buf())
|
|
};
|
|
chosen.display().to_string()
|
|
}
|
|
|
|
fn calculate_add_remove_from_diff(diff: &str) -> (usize, usize) {
|
|
if let Ok(patch) = diffy::Patch::from_str(diff) {
|
|
patch
|
|
.hunks()
|
|
.iter()
|
|
.flat_map(Hunk::lines)
|
|
.fold((0, 0), |(a, d), l| match l {
|
|
diffy::Line::Insert(_) => (a + 1, d),
|
|
diffy::Line::Delete(_) => (a, d + 1),
|
|
diffy::Line::Context(_) => (a, d),
|
|
})
|
|
} else {
|
|
// For unparsable diffs, return 0 for both counts.
|
|
(0, 0)
|
|
}
|
|
}
|
|
|
|
fn push_wrapped_diff_line(
|
|
line_number: usize,
|
|
kind: DiffLineType,
|
|
text: &str,
|
|
width: usize,
|
|
line_number_width: usize,
|
|
) -> Vec<RtLine<'static>> {
|
|
let ln_str = line_number.to_string();
|
|
let mut remaining_text: &str = text;
|
|
|
|
// Reserve a fixed number of spaces (equal to the widest line number plus a
|
|
// trailing spacer) so the sign column stays aligned across the diff block.
|
|
let gutter_width = line_number_width.max(1);
|
|
let prefix_cols = gutter_width + 1;
|
|
|
|
let mut first = true;
|
|
let (sign_char, line_style) = match kind {
|
|
DiffLineType::Insert => ('+', style_add()),
|
|
DiffLineType::Delete => ('-', style_del()),
|
|
DiffLineType::Context => (' ', style_context()),
|
|
};
|
|
let mut lines: Vec<RtLine<'static>> = Vec::new();
|
|
|
|
loop {
|
|
// Fit the content for the current terminal row:
|
|
// compute how many columns are available after the prefix, then split
|
|
// at a UTF-8 character boundary so this row's chunk fits exactly.
|
|
let available_content_cols = width.saturating_sub(prefix_cols + 1).max(1);
|
|
let split_at_byte_index = remaining_text
|
|
.char_indices()
|
|
.nth(available_content_cols)
|
|
.map(|(i, _)| i)
|
|
.unwrap_or_else(|| remaining_text.len());
|
|
let (chunk, rest) = remaining_text.split_at(split_at_byte_index);
|
|
remaining_text = rest;
|
|
|
|
if first {
|
|
// Build gutter (right-aligned line number plus spacer) as a dimmed span
|
|
let gutter = format!("{ln_str:>gutter_width$} ");
|
|
// Content with a sign ('+'/'-'/' ') styled per diff kind
|
|
let content = format!("{sign_char}{chunk}");
|
|
lines.push(RtLine::from(vec![
|
|
RtSpan::styled(gutter, style_gutter()),
|
|
RtSpan::styled(content, line_style),
|
|
]));
|
|
first = false;
|
|
} else {
|
|
// Continuation lines keep a space for the sign column so content aligns
|
|
let gutter = format!("{:gutter_width$} ", "");
|
|
lines.push(RtLine::from(vec![
|
|
RtSpan::styled(gutter, style_gutter()),
|
|
RtSpan::styled(chunk.to_string(), line_style),
|
|
]));
|
|
}
|
|
if remaining_text.is_empty() {
|
|
break;
|
|
}
|
|
}
|
|
lines
|
|
}
|
|
|
|
fn line_number_width(max_line_number: usize) -> usize {
|
|
if max_line_number == 0 {
|
|
1
|
|
} else {
|
|
max_line_number.to_string().len()
|
|
}
|
|
}
|
|
|
|
fn style_gutter() -> Style {
|
|
Style::default().add_modifier(Modifier::DIM)
|
|
}
|
|
|
|
fn style_context() -> Style {
|
|
Style::default()
|
|
}
|
|
|
|
fn style_add() -> Style {
|
|
Style::default().fg(Color::Green)
|
|
}
|
|
|
|
fn style_del() -> Style {
|
|
Style::default().fg(Color::Red)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use insta::assert_snapshot;
|
|
use ratatui::Terminal;
|
|
use ratatui::backend::TestBackend;
|
|
use ratatui::text::Text;
|
|
use ratatui::widgets::Paragraph;
|
|
use ratatui::widgets::WidgetRef;
|
|
use ratatui::widgets::Wrap;
|
|
fn diff_summary_for_tests(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
|
|
create_diff_summary(changes, &PathBuf::from("/"), 80)
|
|
}
|
|
|
|
fn snapshot_lines(name: &str, lines: Vec<RtLine<'static>>, width: u16, height: u16) {
|
|
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("terminal");
|
|
terminal
|
|
.draw(|f| {
|
|
Paragraph::new(Text::from(lines))
|
|
.wrap(Wrap { trim: false })
|
|
.render_ref(f.area(), f.buffer_mut())
|
|
})
|
|
.expect("draw");
|
|
assert_snapshot!(name, terminal.backend());
|
|
}
|
|
|
|
fn snapshot_lines_text(name: &str, lines: &[RtLine<'static>]) {
|
|
// Convert Lines to plain text rows and trim trailing spaces so it's
|
|
// easier to validate indentation visually in snapshots.
|
|
let text = lines
|
|
.iter()
|
|
.map(|l| {
|
|
l.spans
|
|
.iter()
|
|
.map(|s| s.content.as_ref())
|
|
.collect::<String>()
|
|
})
|
|
.map(|s| s.trim_end().to_string())
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
assert_snapshot!(name, text);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_wrap_behavior_insert() {
|
|
// Narrow width to force wrapping within our diff line rendering
|
|
let long_line = "this is a very long line that should wrap across multiple terminal columns and continue";
|
|
|
|
// Call the wrapping function directly so we can precisely control the width
|
|
let lines =
|
|
push_wrapped_diff_line(1, DiffLineType::Insert, long_line, 80, line_number_width(1));
|
|
|
|
// Render into a small terminal to capture the visual layout
|
|
snapshot_lines("wrap_behavior_insert", lines, 90, 8);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_update_block() {
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
let original = "line one\nline two\nline three\n";
|
|
let modified = "line one\nline two changed\nline three\n";
|
|
let patch = diffy::create_patch(original, modified).to_string();
|
|
|
|
changes.insert(
|
|
PathBuf::from("example.txt"),
|
|
FileChange::Update {
|
|
unified_diff: patch,
|
|
move_path: None,
|
|
},
|
|
);
|
|
|
|
let lines = diff_summary_for_tests(&changes);
|
|
|
|
snapshot_lines("apply_update_block", lines, 80, 12);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_update_with_rename_block() {
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
let original = "A\nB\nC\n";
|
|
let modified = "A\nB changed\nC\n";
|
|
let patch = diffy::create_patch(original, modified).to_string();
|
|
|
|
changes.insert(
|
|
PathBuf::from("old_name.rs"),
|
|
FileChange::Update {
|
|
unified_diff: patch,
|
|
move_path: Some(PathBuf::from("new_name.rs")),
|
|
},
|
|
);
|
|
|
|
let lines = diff_summary_for_tests(&changes);
|
|
|
|
snapshot_lines("apply_update_with_rename_block", lines, 80, 12);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_multiple_files_block() {
|
|
// Two files: one update and one add, to exercise combined header and per-file rows
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
|
|
// File a.txt: single-line replacement (one delete, one insert)
|
|
let patch_a = diffy::create_patch("one\n", "one changed\n").to_string();
|
|
changes.insert(
|
|
PathBuf::from("a.txt"),
|
|
FileChange::Update {
|
|
unified_diff: patch_a,
|
|
move_path: None,
|
|
},
|
|
);
|
|
|
|
// File b.txt: newly added with one line
|
|
changes.insert(
|
|
PathBuf::from("b.txt"),
|
|
FileChange::Add {
|
|
content: "new\n".to_string(),
|
|
},
|
|
);
|
|
|
|
let lines = diff_summary_for_tests(&changes);
|
|
|
|
snapshot_lines("apply_multiple_files_block", lines, 80, 14);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_add_block() {
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
changes.insert(
|
|
PathBuf::from("new_file.txt"),
|
|
FileChange::Add {
|
|
content: "alpha\nbeta\n".to_string(),
|
|
},
|
|
);
|
|
|
|
let lines = diff_summary_for_tests(&changes);
|
|
|
|
snapshot_lines("apply_add_block", lines, 80, 10);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_delete_block() {
|
|
// Write a temporary file so the delete renderer can read original content
|
|
let tmp_path = PathBuf::from("tmp_delete_example.txt");
|
|
std::fs::write(&tmp_path, "first\nsecond\nthird\n").expect("write tmp file");
|
|
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
changes.insert(
|
|
tmp_path.clone(),
|
|
FileChange::Delete {
|
|
content: "first\nsecond\nthird\n".to_string(),
|
|
},
|
|
);
|
|
|
|
let lines = diff_summary_for_tests(&changes);
|
|
|
|
// Cleanup best-effort; rendering has already read the file
|
|
let _ = std::fs::remove_file(&tmp_path);
|
|
|
|
snapshot_lines("apply_delete_block", lines, 80, 12);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_update_block_wraps_long_lines() {
|
|
// Create a patch with a long modified line to force wrapping
|
|
let original = "line 1\nshort\nline 3\n";
|
|
let modified = "line 1\nshort this_is_a_very_long_modified_line_that_should_wrap_across_multiple_terminal_columns_and_continue_even_further_beyond_eighty_columns_to_force_multiple_wraps\nline 3\n";
|
|
let patch = diffy::create_patch(original, modified).to_string();
|
|
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
changes.insert(
|
|
PathBuf::from("long_example.txt"),
|
|
FileChange::Update {
|
|
unified_diff: patch,
|
|
move_path: None,
|
|
},
|
|
);
|
|
|
|
let lines = create_diff_summary(&changes, &PathBuf::from("/"), 72);
|
|
|
|
// Render with backend width wider than wrap width to avoid Paragraph auto-wrap.
|
|
snapshot_lines("apply_update_block_wraps_long_lines", lines, 80, 12);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_update_block_wraps_long_lines_text() {
|
|
// This mirrors the desired layout example: sign only on first inserted line,
|
|
// subsequent wrapped pieces start aligned under the line number gutter.
|
|
let original = "1\n2\n3\n4\n";
|
|
let modified = "1\nadded long line which wraps and_if_there_is_a_long_token_it_will_be_broken\n3\n4 context line which also wraps across\n";
|
|
let patch = diffy::create_patch(original, modified).to_string();
|
|
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
changes.insert(
|
|
PathBuf::from("wrap_demo.txt"),
|
|
FileChange::Update {
|
|
unified_diff: patch,
|
|
move_path: None,
|
|
},
|
|
);
|
|
|
|
let lines = create_diff_summary(&changes, &PathBuf::from("/"), 28);
|
|
snapshot_lines_text("apply_update_block_wraps_long_lines_text", &lines);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_update_block_line_numbers_three_digits_text() {
|
|
let original = (1..=110).map(|i| format!("line {i}\n")).collect::<String>();
|
|
let modified = (1..=110)
|
|
.map(|i| {
|
|
if i == 100 {
|
|
format!("line {i} changed\n")
|
|
} else {
|
|
format!("line {i}\n")
|
|
}
|
|
})
|
|
.collect::<String>();
|
|
let patch = diffy::create_patch(&original, &modified).to_string();
|
|
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
changes.insert(
|
|
PathBuf::from("hundreds.txt"),
|
|
FileChange::Update {
|
|
unified_diff: patch,
|
|
move_path: None,
|
|
},
|
|
);
|
|
|
|
let lines = create_diff_summary(&changes, &PathBuf::from("/"), 80);
|
|
snapshot_lines_text("apply_update_block_line_numbers_three_digits_text", &lines);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_snapshot_apply_update_block_relativizes_path() {
|
|
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
|
|
let abs_old = cwd.join("abs_old.rs");
|
|
let abs_new = cwd.join("abs_new.rs");
|
|
|
|
let original = "X\nY\n";
|
|
let modified = "X changed\nY\n";
|
|
let patch = diffy::create_patch(original, modified).to_string();
|
|
|
|
let mut changes: HashMap<PathBuf, FileChange> = HashMap::new();
|
|
changes.insert(
|
|
abs_old,
|
|
FileChange::Update {
|
|
unified_diff: patch,
|
|
move_path: Some(abs_new),
|
|
},
|
|
);
|
|
|
|
let lines = create_diff_summary(&changes, &cwd, 80);
|
|
|
|
snapshot_lines("apply_update_block_relativizes_path", lines, 80, 10);
|
|
}
|
|
}
|