From 7ba9fa19f891e10fa5a950cfa0921d0378506eee Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Fri, 23 Jan 2026 08:53:25 +0000 Subject: [PATCH] refactor(tasks): improve terminal diff output with context lines (#18437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Improves the `print_diff_in_terminal` function with better readability and a simpler API: **API Changes:** - `print_diff_in_terminal(expected, result)` - Simple API that takes two strings - `print_text_diff(diff)` - For when you already have a `TextDiff` (e.g., to use `diff.ratio()`) **Features:** - Show 3 lines of context above and below each change - Add line numbers with gutter separator (`│`) - Add blank line between adjacent hunks for visual separation - Add `...` separator when lines are skipped between hunks - Use single-pass streaming algorithm with small fixed-size buffer - Colors: red (`-`) for deletions, green (`+`) for insertions, dim for context **Formatter Example:** - Added `--diff` flag to show diff between original and formatted code Example output: ``` 1 │class A { 2 │ #x; 3 │} - 4 │var _x = /* @__PURE__ */ new WeakMap(); - 5 │var _B_brand = /* @__PURE__ */ new WeakSet(); 4 │class B { - 7 │ constructor() { - 8 │ babelHelpers.classPrivateMethodInitSpec(this, _B_brand); - 9 │ babelHelpers.classPrivateFieldInitSpec(this, _x, void 0); + 5 │ #x; + 6 │ #y() { + 7 │ this.#x; 8 │ } - 11 │} - 12 │function _y() { - 13 │ babelHelpers.classPrivateFieldGet2(_x, this); 9 │} ``` ## Test plan - [x] `cargo check -p oxc_tasks_common` passes - [x] Clippy passes - [x] Verified output with `just test-transform --filter "plugins-integration/class-features-node-12/input.js"` 🤖 Generated with [Claude Code](https://claude.ai/code) --- Cargo.lock | 1 + crates/oxc_formatter/Cargo.toml | 1 + crates/oxc_formatter/examples/formatter.rs | 17 ++- .../src/module_runner_transform.rs | 8 +- tasks/common/src/diff.rs | 105 +++++++++++++++--- tasks/common/src/lib.rs | 2 +- tasks/prettier_conformance/src/lib.rs | 2 +- tasks/transform_conformance/src/test_case.rs | 6 +- 8 files changed, 117 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 071c8343f2126..0063412933175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1931,6 +1931,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", + "oxc_tasks_common", "phf", "pico-args", "rustc-hash", diff --git a/crates/oxc_formatter/Cargo.toml b/crates/oxc_formatter/Cargo.toml index 90310227e3f48..441ba5895b3c1 100644 --- a/crates/oxc_formatter/Cargo.toml +++ b/crates/oxc_formatter/Cargo.toml @@ -37,6 +37,7 @@ unicode-width = { workspace = true } [dev-dependencies] insta = { workspace = true } +oxc_tasks_common = { path = "../../tasks/common" } pico-args = { workspace = true } serde_json = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/oxc_formatter/examples/formatter.rs b/crates/oxc_formatter/examples/formatter.rs index 341fd7e760f10..c7bce33670b37 100644 --- a/crates/oxc_formatter/examples/formatter.rs +++ b/crates/oxc_formatter/examples/formatter.rs @@ -9,6 +9,7 @@ //! ```bash //! cargo run -p oxc_formatter --example formatter [filename] //! cargo run -p oxc_formatter --example formatter -- --no-semi [filename] +//! cargo run -p oxc_formatter --example formatter -- --diff [filename] //! ``` use std::{fs, path::Path}; @@ -19,6 +20,7 @@ use oxc_formatter::{ }; use oxc_parser::Parser; use oxc_span::SourceType; +use oxc_tasks_common::print_diff_in_terminal; use pico_args::Arguments; /// Format a JavaScript or TypeScript file @@ -26,6 +28,7 @@ fn main() -> Result<(), String> { let mut args = Arguments::from_env(); let no_semi = args.contains("--no-semi"); let show_ir = args.contains("--ir"); + let show_diff = args.contains("--diff"); let print_width = args.opt_value_from_str::<&'static str, u16>("--print-width").unwrap_or(None); let name = args.free_from_str().unwrap_or_else(|_| "test.js".to_string()); @@ -68,9 +71,17 @@ fn main() -> Result<(), String> { println!("--- End IR ---\n"); } - println!("--- Formatted Code ---"); let code = formatted.print().map_err(|e| e.to_string())?.into_code(); - println!("{code}"); - println!("--- End Formatted Code ---"); + + if show_diff { + println!("--- Diff ---"); + print_diff_in_terminal(&source_text, &code); + println!("--- End Diff ---"); + } else { + println!("--- Formatted Code ---"); + println!("{code}"); + println!("--- End Formatted Code ---"); + } + Ok(()) } diff --git a/crates/oxc_transformer_plugins/src/module_runner_transform.rs b/crates/oxc_transformer_plugins/src/module_runner_transform.rs index 611fa510d1d4d..d7080e670549a 100644 --- a/crates/oxc_transformer_plugins/src/module_runner_transform.rs +++ b/crates/oxc_transformer_plugins/src/module_runner_transform.rs @@ -857,7 +857,7 @@ mod test { use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; - use oxc_tasks_common::print_diff_in_terminal; + use oxc_tasks_common::print_text_diff; use oxc_transformer::{JsxRuntime, TransformOptions, Transformer}; use super::ModuleRunnerTransform; @@ -921,7 +921,7 @@ mod test { let result = transform(source_text, false).unwrap().code; if result != expected { let diff = TextDiff::from_lines(&expected, &result); - print_diff_in_terminal(&diff); + print_text_diff(&diff); panic!("Expected code does not match the result"); } } @@ -932,7 +932,7 @@ mod test { let result = transform(source_text, true).unwrap().code; if result != expected { let diff = TextDiff::from_lines(&expected, &result); - print_diff_in_terminal(&diff); + print_text_diff(&diff); panic!("Expected code does not match the result"); } } @@ -944,7 +944,7 @@ mod test { transform(source_text, false).unwrap(); if code != expected { let diff = TextDiff::from_lines(&expected, &code); - print_diff_in_terminal(&diff); + print_text_diff(&diff); panic!("Expected code does not match the result"); } for dep in deps { diff --git a/tasks/common/src/diff.rs b/tasks/common/src/diff.rs index bc326f3e716b7..7e698a6116c58 100644 --- a/tasks/common/src/diff.rs +++ b/tasks/common/src/diff.rs @@ -1,19 +1,98 @@ +use std::collections::VecDeque; + use console::Style; use similar::{ChangeTag, DiffableStr, TextDiff}; -pub fn print_diff_in_terminal(diff: &TextDiff) -where - T: DiffableStr + ?Sized, -{ - for op in diff.ops() { - for change in diff.iter_changes(op) { - let (sign, style) = match change.tag() { - ChangeTag::Delete => ("-", Style::new().red()), - ChangeTag::Insert => ("+", Style::new().green()), - // ChangeTag::Equal => (" ", Style::new()), - ChangeTag::Equal => continue, - }; - print!("{}{}", style.apply_to(sign).bold(), style.apply_to(change)); +const CONTEXT_LINES: usize = 3; + +/// Prints a colored diff to the terminal with context lines around changes. +/// +/// Features: +/// - 3 lines of context above and below each change +/// - Line numbers in a gutter with `│` separator +/// - Blank line between adjacent hunks +/// - `...` separator when lines are skipped between hunks +/// - Colors: red (`-`) for deletions, green (`+`) for insertions, dim for context +/// +/// Example output: +/// ```text +/// 1 │fn main() { +/// 2 │ let x = 1; +/// - 3 │ let y = 2; +/// + 3 │ let y = 3; +/// 4 │ println!("{}", x + y); +/// 5 │} +/// ... +/// 42 │fn other() { +/// - 43 │ old_code(); +/// + 43 │ new_code(); +/// 44 │} +/// ``` +pub fn print_diff_in_terminal(expected: &str, result: &str) { + let diff = TextDiff::from_lines(expected, result); + print_text_diff(&diff); +} + +/// Prints an existing `TextDiff` to the terminal. +/// Use this when you need access to the `TextDiff` for other operations (e.g., `diff.ratio()`). +pub fn print_text_diff(diff: &TextDiff) { + let mut context_buffer: VecDeque<_> = VecDeque::with_capacity(CONTEXT_LINES); + let mut trailing_remaining = 0; + let mut has_printed = false; + let mut had_gap = false; + let mut needs_separator = false; + + for change in diff.ops().iter().flat_map(|op| diff.iter_changes(op)) { + let tag = change.tag(); + + if tag == ChangeTag::Equal { + if trailing_remaining > 0 { + print_line(tag, change.new_index(), &change.to_string()); + trailing_remaining -= 1; + needs_separator = true; + } else { + if context_buffer.len() == CONTEXT_LINES { + context_buffer.pop_front(); + had_gap = true; + } + context_buffer.push_back((tag, change.new_index(), change.to_string())); + } + continue; + } + + if has_printed && (needs_separator || !context_buffer.is_empty()) { + if had_gap { + println!("{}", Style::new().cyan().apply_to("...")); + } else { + println!(); + } + } + + while let Some((tag, line_num, content)) = context_buffer.pop_front() { + print_line(tag, line_num, &content); } + + let line_num = + if tag == ChangeTag::Delete { change.old_index() } else { change.new_index() }; + print_line(tag, line_num, &change.to_string()); + + has_printed = true; + had_gap = false; + needs_separator = false; + trailing_remaining = CONTEXT_LINES; } } + +fn print_line(tag: ChangeTag, line_num: Option, content: &str) { + let (sign, style) = match tag { + ChangeTag::Delete => ("-", Style::new().red()), + ChangeTag::Insert => ("+", Style::new().green()), + ChangeTag::Equal => (" ", Style::new().dim()), + }; + print!( + "{}{} │{}", + style.apply_to(sign).bold(), + Style::new().dim().apply_to(format!("{:>4}", line_num.map_or(0, |n| n + 1))), + style.apply_to(content) + ); +} diff --git a/tasks/common/src/lib.rs b/tasks/common/src/lib.rs index a87ddd3080212..5fa85a0cddc9a 100644 --- a/tasks/common/src/lib.rs +++ b/tasks/common/src/lib.rs @@ -6,7 +6,7 @@ mod request; mod snapshot; mod test_file; -pub use diff::print_diff_in_terminal; +pub use diff::{print_diff_in_terminal, print_text_diff}; pub use crate::{request::agent, snapshot::Snapshot, test_file::*}; diff --git a/tasks/prettier_conformance/src/lib.rs b/tasks/prettier_conformance/src/lib.rs index cfad1aad3f932..11391b4a0f962 100644 --- a/tasks/prettier_conformance/src/lib.rs +++ b/tasks/prettier_conformance/src/lib.rs @@ -301,7 +301,7 @@ impl TestRunner { "{}", path.strip_prefix(fixtures_root()).unwrap().to_string_lossy() ); - oxc_tasks_common::print_diff_in_terminal(&diff); + oxc_tasks_common::print_text_diff(&diff); } println!(); } diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index 69e2985829766..1a4f91ecb7930 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -14,7 +14,7 @@ use oxc::{ span::{SourceType, VALID_EXTENSIONS}, transformer::{BabelOptions, HelperLoaderMode, TransformOptions}, }; -use oxc_tasks_common::{normalize_path, print_diff_in_terminal, project_root}; +use oxc_tasks_common::{normalize_path, print_text_diff, project_root}; use crate::{ TestRunnerOptions, @@ -363,7 +363,7 @@ impl TestCase { if !passed { let diff = TextDiff::from_lines(&output, actual_errors); println!("Diff:\n"); - print_diff_in_terminal(&diff); + print_text_diff(&diff); } } } else { @@ -378,7 +378,7 @@ impl TestCase { if !passed { let diff = TextDiff::from_lines(&output, &self.transformed_code); println!("Diff:\n"); - print_diff_in_terminal(&diff); + print_text_diff(&diff); } }