Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions apps/oxfmt/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
command::{FormatCommand, OutputOptions},
reporter::DefaultReporter,
result::CliRunResult,
service::FormatService,
service::{FormatService, SuccessResult},
walk::Walk,
};

Expand Down Expand Up @@ -97,10 +97,8 @@ impl FormatRunner {

// Get the receiver for streaming entries
let rx_entry = walker.stream_entries();
// Count all files for stats
let (tx_count, rx_count) = mpsc::channel::<()>();
// Collect file paths that were updated
let (tx_path, rx_path) = mpsc::channel();
// Collect format results (changed paths or unchanged count)
let (tx_success, rx_success) = mpsc::channel();
// Diagnostic from formatting service
let (mut diagnostic_service, tx_error) =
DiagnosticService::new(Box::new(DefaultReporter::default()));
Expand All @@ -125,11 +123,20 @@ impl FormatRunner {
#[cfg(feature = "napi")]
let format_service = format_service.with_external_formatter(external_formatter_clone);

format_service.run_streaming(rx_entry, &tx_error, &tx_path, tx_count);
format_service.run_streaming(rx_entry, &tx_error, &tx_success);
});

// First, collect and print sorted file paths to stdout
let mut changed_paths: Vec<String> = rx_path.iter().collect();
// Collect results and separate changed paths from unchanged count
let mut changed_paths: Vec<String> = vec![];
let mut unchanged_count: usize = 0;
for result in rx_success {
match result {
SuccessResult::Changed(path) => changed_paths.push(path),
SuccessResult::Unchanged => unchanged_count += 1,
}
}

// Print sorted changed file paths to stdout
if !changed_paths.is_empty() {
changed_paths.sort_unstable();
print_and_flush(stdout, &changed_paths.join("\n"));
Expand All @@ -138,9 +145,11 @@ impl FormatRunner {
// Then, output diagnostics errors to stderr
// NOTE: This is blocking and print errors
let diagnostics = diagnostic_service.run(stderr);
// NOTE: We are not using `DiagnosticService` for warnings
let error_count = diagnostics.errors_count();

// Count the processed files
let total_target_files_count = rx_count.iter().count();
let total_target_files_count = changed_paths.len() + unchanged_count + error_count;
let print_stats = |stdout| {
let elapsed_ms = start_time.elapsed().as_millis();
print_and_flush(
Expand All @@ -163,7 +172,7 @@ impl FormatRunner {
return CliRunResult::NoFilesFound;
}

if 0 < diagnostics.errors_count() {
if 0 < error_count {
// Each error is already printed in reporter
print_and_flush(
stderr,
Expand Down
92 changes: 42 additions & 50 deletions apps/oxfmt/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use oxc_span::SourceType;

use crate::{command::OutputOptions, walk::WalkEntry};

type PathSender = mpsc::Sender<String>;
pub enum SuccessResult {
Changed(String),
Unchanged,
}

pub struct FormatService {
allocator_pool: AllocatorPool,
Expand Down Expand Up @@ -55,67 +58,56 @@ impl FormatService {
}

/// Process entries as they are received from the channel
#[expect(clippy::needless_pass_by_value)]
pub fn run_streaming(
&self,
rx_entry: mpsc::Receiver<WalkEntry>,
tx_error: &DiagnosticSender,
tx_path: &PathSender,
// Take ownership to close the channel when done
tx_count: mpsc::Sender<()>,
tx_success: &mpsc::Sender<SuccessResult>,
) {
rx_entry.into_iter().par_bridge().for_each(|entry| {
self.process_entry(&entry, tx_error, tx_path);
// Signal that we processed one file (ignore send errors if receiver dropped)
let _ = tx_count.send(());
});
}

/// Process a single entry
fn process_entry(&self, entry: &WalkEntry, tx_error: &DiagnosticSender, tx_path: &PathSender) {
let start_time = Instant::now();
let start_time = Instant::now();

let path = &entry.path;
let (code, is_changed) = match self.format_file(entry) {
Ok(res) => res,
Err(diagnostics) => {
tx_error.send(diagnostics).unwrap();
return;
}
};
let path = &entry.path;
let (code, is_changed) = match self.format_file(&entry) {
Ok(res) => res,
Err(diagnostics) => {
tx_error.send(diagnostics).unwrap();
return;
}
};

let elapsed = start_time.elapsed();
let elapsed = start_time.elapsed();

// Write back if needed
if matches!(self.output_options, OutputOptions::Write) && is_changed {
fs::write(path, code)
.map_err(|_| format!("Failed to write to '{}'", path.to_string_lossy()))
.unwrap();
}
// Write back if needed
if matches!(self.output_options, OutputOptions::Write) && is_changed {
fs::write(path, code)
.map_err(|_| format!("Failed to write to '{}'", path.to_string_lossy()))
.unwrap();
}

// Notify if needed
if let Some(output) = match (&self.output_options, is_changed) {
(OutputOptions::Check | OutputOptions::ListDifferent, true) => {
let display_path = path
// Show path relative to `cwd` for cleaner output
.strip_prefix(&self.cwd)
.unwrap_or(path)
.to_string_lossy()
// Normalize path separators for consistent output across platforms
.cow_replace('\\', "/")
.to_string();
let elapsed = elapsed.as_millis();
// Report result
let result = match (&self.output_options, is_changed) {
(OutputOptions::Check | OutputOptions::ListDifferent, true) => {
let display_path = path
// Show path relative to `cwd` for cleaner output
.strip_prefix(&self.cwd)
.unwrap_or(path)
.to_string_lossy()
// Normalize path separators for consistent output across platforms
.cow_replace('\\', "/")
.to_string();
let elapsed = elapsed.as_millis();

if matches!(self.output_options, OutputOptions::Check) {
Some(format!("{display_path} ({elapsed}ms)"))
} else {
Some(display_path)
if matches!(self.output_options, OutputOptions::Check) {
SuccessResult::Changed(format!("{display_path} ({elapsed}ms)"))
} else {
SuccessResult::Changed(display_path)
}
}
}
_ => None,
} {
tx_path.send(output).unwrap();
}
_ => SuccessResult::Unchanged,
};
tx_success.send(result).unwrap();
});
}

fn format_file(&self, entry: &WalkEntry) -> Result<(String, bool), Vec<Error>> {
Expand Down
Loading