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
12 changes: 8 additions & 4 deletions benches/benchmark.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::hint::black_box;

use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use mdref::{find_links, find_references};
use mdref::{NoopProgress, find_links, find_references};

mod support;

Expand Down Expand Up @@ -43,9 +43,12 @@ fn benchmark_find_operations(c: &mut Criterion) {
&fixture,
|b, fixture| {
b.iter(|| {
let result =
find_references(black_box(&fixture.hot_file), black_box(&fixture.root))
.expect("find_references benchmark should succeed");
let result = find_references(
black_box(&fixture.hot_file),
black_box(&fixture.root),
&NoopProgress,
)
.expect("find_references benchmark should succeed");
black_box(result);
});
},
Expand All @@ -60,6 +63,7 @@ fn benchmark_find_operations(c: &mut Criterion) {
let result = find_references(
black_box(&fixture.hot_directory),
black_box(&fixture.root),
&NoopProgress,
)
.expect("find_references directory benchmark should succeed");
black_box(result);
Expand Down
3 changes: 2 additions & 1 deletion benches/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
path::{Path, PathBuf},
};

use mdref::{Result, diff_paths, mv};
use mdref::{NoopProgress, Result, diff_paths, mv};
use tempfile::TempDir;

const FIXED_MARKDOWN_FILES: usize = 4;
Expand Down Expand Up @@ -121,6 +121,7 @@ pub fn run_move_operation(operation: MoveOperation<'_>) -> Result<()> {
black_box(operation.destination),
black_box(operation.root),
false,
&NoopProgress,
)
}

Expand Down
10 changes: 5 additions & 5 deletions src/commands/find.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::io::Write;

use mdref::{MdrefError, Reference, Result, find_links, find_references_with_progress};
use mdref::{MdrefError, Reference, Result, find_links, find_references};
use serde::Serialize;

use super::{OutputFormat, progress};
use super::{OutputFormat, progress::Spinner};

pub fn run(
path: String,
Expand All @@ -25,11 +25,11 @@ fn run_with_writer<W: Write>(
let root_path = root_dir.unwrap_or_else(|| ".".to_string());

// Find references to the specified file.
let progress = progress::create_spinner(show_progress);
let spinner = Spinner::new(show_progress);

let references = find_references_with_progress(&path, &root_path, progress.as_ref())?;
let references = find_references(&path, &root_path, spinner.as_reporter())?;

progress::finish(&progress);
spinner.finish();

// Find all links within the specified file.
let links = find_links(&path)?;
Expand Down
18 changes: 9 additions & 9 deletions src/commands/mv.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::io::Write;

use mdref::{Result, core::mv::preview_move_with_progress, mv_with_progress};
use mdref::{NoopProgress, Result, mv, preview_move};
use serde::Serialize;

use crate::commands::{
OutputFormat, json_move_changes, progress, write_json_output, write_move_preview_human,
OutputFormat, json_move_changes, progress::Spinner, write_json_output, write_move_preview_human,
};

pub fn run(
Expand Down Expand Up @@ -38,30 +38,30 @@ fn run_with_writer<W: Write>(
) -> Result<()> {
let root = root.unwrap_or_else(|| ".".to_string());

let progress = progress::create_spinner(show_progress && !dry_run);
let spinner = Spinner::new(show_progress && !dry_run);

match format {
OutputFormat::Human => {
if dry_run {
let preview = preview_move_with_progress(&source, &dest, &root, None)?;
let preview = preview_move(&source, &dest, &root, &NoopProgress)?;
return write_move_preview_human(&preview, writer);
}

writeln!(writer, "Move {source} -> {dest} in {root}")?;
let result = mv_with_progress(&source, &dest, &root, false, progress.as_ref());
let result = mv(&source, &dest, &root, false, spinner.as_reporter());

progress::finish(&progress);
spinner.finish();

result
}
OutputFormat::Json => {
let preview = preview_move_with_progress(&source, &dest, &root, None)?;
let preview = preview_move(&source, &dest, &root, &NoopProgress)?;

if !dry_run {
mv_with_progress(&source, &dest, &root, false, progress.as_ref())?;
mv(&source, &dest, &root, false, spinner.as_reporter())?;
}

progress::finish(&progress);
spinner.finish();

let payload = MoveCommandOutput {
operation: "mv",
Expand Down
74 changes: 60 additions & 14 deletions src/commands/progress.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,71 @@
use indicatif::{ProgressBar, ProgressStyle};
use mdref::ProgressReporter;

const SPINNER_TEMPLATE: &str = "{spinner:.green} [{pos}/{len}] {msg}";

/// Create an optional spinner progress bar.
/// A CLI-side progress handle that renders an `indicatif` spinner and exposes
/// itself as a generic [`ProgressReporter`] to the `core` layer.
///
/// Returns `Some(ProgressBar)` when `enabled` is `true`, `None` otherwise.
/// All command modules share the same spinner style, so this centralises the
/// template in one place.
pub fn create_spinner(enabled: bool) -> Option<ProgressBar> {
if !enabled {
return None;
/// The type doubles as a newtype adapter: `core` never sees `indicatif` at
/// all, and Rust's orphan rule is satisfied because `Spinner` is defined in
/// this crate. When `enabled` is `false`, the spinner is absent and all
/// reporter methods become no-ops.
///
/// Use [`Spinner::as_reporter`] when calling into `core`, and drop the
/// [`Spinner`] (or call [`Spinner::finish`]) to clear the terminal line when
/// done.
pub struct Spinner {
bar: Option<ProgressBar>,
}

impl Spinner {
/// Create a spinner that renders when `enabled` is `true`, or a silent
/// no-op reporter otherwise. Both variants share the same `ProgressReporter`
/// contract, so the caller code stays branch-free.
pub fn new(enabled: bool) -> Self {
if !enabled {
return Self { bar: None };
}

let progress_bar = ProgressBar::new_spinner();
progress_bar
.set_style(ProgressStyle::with_template(SPINNER_TEMPLATE).expect("valid template"));
Self {
bar: Some(progress_bar),
}
}

/// Borrow this spinner as a `&dyn ProgressReporter` for `core` APIs.
pub fn as_reporter(&self) -> &dyn ProgressReporter {
self
}

let progress_bar = ProgressBar::new_spinner();
progress_bar.set_style(ProgressStyle::with_template(SPINNER_TEMPLATE).expect("valid template"));
Some(progress_bar)
/// Finish and clear the underlying spinner, if any.
pub fn finish(&self) {
if let Some(bar) = &self.bar {
bar.finish_and_clear();
}
}
}

/// Finish and clear the progress bar, if present.
pub fn finish(progress: &Option<ProgressBar>) {
if let Some(progress_bar) = progress {
progress_bar.finish_and_clear();
impl ProgressReporter for Spinner {
fn set_message(&self, message: &str) {
if let Some(bar) = &self.bar {
// indicatif requires an owned `Cow<'static, str>` for messages, so
// we allocate here. This only runs a handful of times per command.
bar.set_message(message.to_owned());
}
}

fn set_total(&self, total: u64) {
if let Some(bar) = &self.bar {
bar.set_length(total);
}
}

fn inc(&self, delta: u64) {
if let Some(bar) = &self.bar {
bar.inc(delta);
}
}
}
18 changes: 9 additions & 9 deletions src/commands/rename.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::io::Write;

use mdref::{Result, core::mv::preview_move_with_progress, rename_with_progress};
use mdref::{NoopProgress, Result, preview_move, rename};
use serde::Serialize;

use crate::commands::{
OutputFormat, json_move_changes, progress, write_json_output, write_move_preview_human,
OutputFormat, json_move_changes, progress::Spinner, write_json_output, write_move_preview_human,
};

pub fn run(
Expand All @@ -31,30 +31,30 @@ fn run_with_writer<W: Write>(
let root_path = root.unwrap_or_else(|| ".".to_string());
let destination = std::path::Path::new(&old).with_file_name(&new);

let progress = progress::create_spinner(show_progress && !dry_run);
let spinner = Spinner::new(show_progress && !dry_run);

match format {
OutputFormat::Human => {
if dry_run {
let preview = preview_move_with_progress(&old, &destination, &root_path, None)?;
let preview = preview_move(&old, &destination, &root_path, &NoopProgress)?;
return write_move_preview_human(&preview, writer);
}

writeln!(writer, "Rename {old} -> {new} in {root_path}")?;
let result = rename_with_progress(&old, &new, &root_path, false, progress.as_ref());
let result = rename(&old, &new, &root_path, false, spinner.as_reporter());

progress::finish(&progress);
spinner.finish();

result
}
OutputFormat::Json => {
let preview = preview_move_with_progress(&old, &destination, &root_path, None)?;
let preview = preview_move(&old, &destination, &root_path, &NoopProgress)?;

if !dry_run {
rename_with_progress(&old, &new, &root_path, false, progress.as_ref())?;
rename(&old, &new, &root_path, false, spinner.as_reporter())?;
}

progress::finish(&progress);
spinner.finish();

let payload = RenameCommandOutput {
operation: "rename",
Expand Down
42 changes: 19 additions & 23 deletions src/core/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,32 @@ use comrak::{
nodes::{AstNode, NodeValue},
parse_document,
};
use indicatif::ProgressBar;
use rayon::prelude::*;

use super::util::{
collect_markdown_files, is_external_url, strip_anchor, strip_utf8_bom_prefix, url_decode_link,
use super::{
progress::ProgressReporter,
util::{
collect_markdown_files, is_external_url, strip_anchor, strip_utf8_bom_prefix,
url_decode_link,
},
};
use crate::{Reference, Result};

/// Find all references to a given file within Markdown files in the specified root directory.
/// Returns a vector of References containing the referencing file path, line number, column number, and the link text.
pub fn find_references<P, B>(path: P, root_dir: B) -> Result<Vec<Reference>>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
find_references_with_progress(path, root_dir, None)
}

/// Find all references with an optional progress bar.
///
/// When a `ProgressBar` is provided, it is incremented once for each Markdown file scanned.
/// The caller is responsible for creating and finishing the progress bar.
pub fn find_references_with_progress<P, B>(
/// Returns a vector of [`Reference`]s containing the referencing file path, line number,
/// column number, and the link text.
///
/// # Progress
///
/// Callers report progress through a [`ProgressReporter`] trait object. Pass
/// [`crate::NoopProgress`] (as `&NoopProgress`) when progress updates are not needed.
/// The reporter is called with [`ProgressReporter::set_total`] once before scanning,
/// and with [`ProgressReporter::inc`] once per Markdown file as it is processed.
pub fn find_references<P, B>(
path: P,
root_dir: B,
progress: Option<&ProgressBar>,
progress: &dyn ProgressReporter,
) -> Result<Vec<Reference>>
where
P: AsRef<Path>,
Expand All @@ -49,9 +49,7 @@ where
})?;
let markdown_files = collect_markdown_files(root_dir.as_ref());

if let Some(progress_bar) = progress {
progress_bar.set_length(markdown_files.len() as u64);
}
progress.set_total(markdown_files.len() as u64);

let results: Vec<Result<Vec<Reference>>> = markdown_files
.par_iter()
Expand All @@ -61,9 +59,7 @@ where
source: e,
})?;
let refs = process_md_file(&content, path, Some(&canonical_path));
if let Some(progress_bar) = progress {
progress_bar.inc(1);
}
progress.inc(1);
Ok(refs)
})
.collect();
Expand Down
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ pub mod find;
pub mod model;
pub mod mv;
pub mod pathdiff;
pub mod progress;
pub mod rename;
pub mod util;
Loading