From 513280ee39c4ae6cf997c1e722370f2aaf3c0292 Mon Sep 17 00:00:00 2001 From: StudentWeis Date: Sat, 25 Apr 2026 17:28:35 +0800 Subject: [PATCH 1/2] refactor(cli): extract shared progress-bar helpers into progress module Extract duplicated ProgressBar creation (new_spinner + set_style) and teardown (finish_and_clear) logic from find.rs, mv.rs, and rename.rs into a new commands/progress.rs module with create_spinner() and finish() helpers. Refs #2 --- src/commands/find.rs | 18 +++--------------- src/commands/mod.rs | 1 + src/commands/mv.rs | 22 ++++------------------ src/commands/progress.rs | 25 +++++++++++++++++++++++++ src/commands/rename.rs | 22 ++++------------------ 5 files changed, 37 insertions(+), 51 deletions(-) create mode 100644 src/commands/progress.rs diff --git a/src/commands/find.rs b/src/commands/find.rs index eef06ab..5b5d805 100644 --- a/src/commands/find.rs +++ b/src/commands/find.rs @@ -1,10 +1,9 @@ use std::io::Write; -use indicatif::{ProgressBar, ProgressStyle}; use mdref::{MdrefError, Reference, Result, find_links, find_references_with_progress}; use serde::Serialize; -use super::OutputFormat; +use super::{OutputFormat, progress}; pub fn run( path: String, @@ -26,22 +25,11 @@ fn run_with_writer( let root_path = root_dir.unwrap_or_else(|| ".".to_string()); // Find references to the specified file. - let progress = if show_progress { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_style( - ProgressStyle::with_template("{spinner:.green} [{pos}/{len}] {msg}") - .expect("valid template"), - ); - Some(progress_bar) - } else { - None - }; + let progress = progress::create_spinner(show_progress); let references = find_references_with_progress(&path, &root_path, progress.as_ref())?; - if let Some(progress_bar) = &progress { - progress_bar.finish_and_clear(); - } + progress::finish(&progress); // Find all links within the specified file. let links = find_links(&path)?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f95d55f..3cf7024 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,6 +9,7 @@ use serde::Serialize; mod find; mod mv; +pub(crate) mod progress; mod rename; #[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] diff --git a/src/commands/mv.rs b/src/commands/mv.rs index 45b398b..bfe3c4b 100644 --- a/src/commands/mv.rs +++ b/src/commands/mv.rs @@ -1,11 +1,10 @@ use std::io::Write; -use indicatif::{ProgressBar, ProgressStyle}; use mdref::{Result, core::mv::preview_move_with_progress, mv_with_progress}; use serde::Serialize; use crate::commands::{ - OutputFormat, json_move_changes, write_json_output, write_move_preview_human, + OutputFormat, json_move_changes, progress, write_json_output, write_move_preview_human, }; pub fn run( @@ -39,16 +38,7 @@ fn run_with_writer( ) -> Result<()> { let root = root.unwrap_or_else(|| ".".to_string()); - let progress = if show_progress && !dry_run { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_style( - ProgressStyle::with_template("{spinner:.green} [{pos}/{len}] {msg}") - .expect("valid template"), - ); - Some(progress_bar) - } else { - None - }; + let progress = progress::create_spinner(show_progress && !dry_run); match format { OutputFormat::Human => { @@ -60,9 +50,7 @@ fn run_with_writer( writeln!(writer, "Move {source} -> {dest} in {root}")?; let result = mv_with_progress(&source, &dest, &root, false, progress.as_ref()); - if let Some(progress_bar) = &progress { - progress_bar.finish_and_clear(); - } + progress::finish(&progress); result } @@ -73,9 +61,7 @@ fn run_with_writer( mv_with_progress(&source, &dest, &root, false, progress.as_ref())?; } - if let Some(progress_bar) = &progress { - progress_bar.finish_and_clear(); - } + progress::finish(&progress); let payload = MoveCommandOutput { operation: "mv", diff --git a/src/commands/progress.rs b/src/commands/progress.rs new file mode 100644 index 0000000..0f69d5d --- /dev/null +++ b/src/commands/progress.rs @@ -0,0 +1,25 @@ +use indicatif::{ProgressBar, ProgressStyle}; + +const SPINNER_TEMPLATE: &str = "{spinner:.green} [{pos}/{len}] {msg}"; + +/// Create an optional spinner progress bar. +/// +/// 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 { + if !enabled { + return None; + } + + 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 progress bar, if present. +pub fn finish(progress: &Option) { + if let Some(progress_bar) = progress { + progress_bar.finish_and_clear(); + } +} diff --git a/src/commands/rename.rs b/src/commands/rename.rs index 2956a96..46e047d 100644 --- a/src/commands/rename.rs +++ b/src/commands/rename.rs @@ -1,11 +1,10 @@ use std::io::Write; -use indicatif::{ProgressBar, ProgressStyle}; use mdref::{Result, core::mv::preview_move_with_progress, rename_with_progress}; use serde::Serialize; use crate::commands::{ - OutputFormat, json_move_changes, write_json_output, write_move_preview_human, + OutputFormat, json_move_changes, progress, write_json_output, write_move_preview_human, }; pub fn run( @@ -32,16 +31,7 @@ fn run_with_writer( let root_path = root.unwrap_or_else(|| ".".to_string()); let destination = std::path::Path::new(&old).with_file_name(&new); - let progress = if show_progress && !dry_run { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_style( - ProgressStyle::with_template("{spinner:.green} [{pos}/{len}] {msg}") - .expect("valid template"), - ); - Some(progress_bar) - } else { - None - }; + let progress = progress::create_spinner(show_progress && !dry_run); match format { OutputFormat::Human => { @@ -53,9 +43,7 @@ fn run_with_writer( writeln!(writer, "Rename {old} -> {new} in {root_path}")?; let result = rename_with_progress(&old, &new, &root_path, false, progress.as_ref()); - if let Some(progress_bar) = &progress { - progress_bar.finish_and_clear(); - } + progress::finish(&progress); result } @@ -66,9 +54,7 @@ fn run_with_writer( rename_with_progress(&old, &new, &root_path, false, progress.as_ref())?; } - if let Some(progress_bar) = &progress { - progress_bar.finish_and_clear(); - } + progress::finish(&progress); let payload = RenameCommandOutput { operation: "rename", From f9058cb98ce12f8dc8d78a174cb2bbdb0b6d95b5 Mon Sep 17 00:00:00 2001 From: StudentWeis Date: Sat, 25 Apr 2026 17:30:15 +0800 Subject: [PATCH 2/2] docs: update doc --- doc/DESIGN.md | 80 ++++++++++++++++++++++++++++----- doc/ProjectReview-2026-04-25.md | 31 ------------- 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/doc/DESIGN.md b/doc/DESIGN.md index e6bd7f8..ef71744 100644 --- a/doc/DESIGN.md +++ b/doc/DESIGN.md @@ -1,22 +1,78 @@ # mdref design document -mdref is a command-line tool for managing references of Markdown files. +mdref is a command-line tool and library for discovering, moving, and renaming Markdown references. -This document outlines the design and architecture of mdref. +This document describes the system as it exists today, the boundaries that are intentionally in scope, and future candidates that are not implemented yet. Read it together with [README.md](../README.md) for user-facing usage and [DirectoryMove.md](./DirectoryMove.md) for the directory-move algorithm. -## Functionality Design +## Current implementation -Basic operations include: +### Primary workflows -- find: Find all references to a Markdown file. -- move: Move a Markdown file and update all references. +- `find`: find inbound references to a Markdown file and list outbound links inside that file. +- `mv`: move a Markdown file or directory and rewrite affected local Markdown links. +- `rename`: rename a file in place by delegating to `mv` with a new filename in the same directory. -Other potential features: +### Layering -- validate: Validate that all references in Markdown files are valid. -- report: Generate a report of all Markdown files and their references. +- CLI entrypoints in `src/main.rs` and `src/commands/*` own argument parsing, progress display, and human or JSON rendering. +- The public library surface in `src/lib.rs` exposes `find_references`, `find_references_with_progress`, `mv`, `mv_with_progress`, `rename`, and `rename_with_progress`. +- Core behavior lives under `src/core`: + - `find.rs` parses Markdown and locates references. + - `mv.rs` validates paths, plans rewrites, executes moves, and coordinates rollback. + - `rename.rs` is a semantic wrapper around `mv`. + - `model/*` contains shared data structures such as move previews, replacements, and transactions. -Other features: +### Reference discovery model -- gitignore support: Ignore files/directories based on .gitignore patterns. -- atomic operations: Ensure move operations are atomic to prevent broken references. +- Discovery is limited to Markdown files with the `.md` extension. +- Directory scans use standard ignore handling through `.gitignore` and related ignore files, and this still applies when the root is not itself a Git repository. +- `find` returns two views of the same target: + - inbound references from other Markdown files under the chosen root + - outbound links found inside the target file +- Supported local reference forms include inline links and link reference definitions. +- External URLs such as `https://`, `mailto:`, and similar schemes are treated as non-local and are never rewritten. +- Pure fragment links such as `#section` are not rewritten. File links with fragments keep the fragment. + +### Move and rename model + +- A file move updates: + - other Markdown files that point to the moved file + - links inside the moved file whose relative target changes after the move +- A directory move is planned before any mutation: + - external files pointing into the moved directory are rewritten + - moved Markdown files pointing outside the directory are rewritten + - links between files that move together are usually left unchanged because their relative positions do not change +- `rename` is implemented as a same-directory move and therefore shares validation, rewrite planning, dry-run behavior, and rollback semantics with `mv`. +- `--dry-run` computes the full move preview without modifying files. +- Execution uses a transaction-like flow: plan first, then mutate, then attempt rollback if a later step fails. + +### Output contracts + +- Human output is intended for interactive use: + - `find` prints separate sections for references and links. + - `mv` and `rename` print a summary for real runs. + - dry-run mode prints a preview of the move and each planned replacement. +- JSON output is available for `find`, `mv`, and `rename` and is intended for automation. +- Successful `find` output includes `operation`, `target`, `references`, and `links`. +- Successful `mv` output includes `operation`, `source`, `destination`, `root`, `dry_run`, and `changes`. +- Successful `rename` output includes `operation`, `source`, `new_name`, `destination`, `root`, `dry_run`, and `changes`. +- Each change entry includes the affected `path`, a `kind` (`reference_update` or `moved_file_update`), and line or column-based replacements. +- When JSON output is requested, command failures are also emitted as JSON on stderr with command context and an `error` message. + +## Known boundaries + +- The project is focused on local Markdown references. It does not try to validate or rewrite arbitrary text formats or non-Markdown documents. +- Only `.md` files discovered by the scan participate in reference discovery and rewrite planning. +- Ignored files and directories are intentionally skipped during scanning, so references inside ignored Markdown files are not updated. +- Path resolution prefers canonicalized real paths when possible. This helps with symlink-aware comparisons and paths that do not exist yet, but the exact filesystem behavior still depends on the host platform. +- Rollback is best-effort rather than a hard atomicity guarantee. The code attempts to restore moved paths and rewritten file contents, but filesystem boundaries, permissions, and platform-specific rename semantics can still limit recovery. +- Directory move behavior is described in more detail in [DirectoryMove.md](./DirectoryMove.md). This document stays at the architectural level. + +## Future candidates + +The following items are not part of the current implementation and should be treated as roadmap ideas, not existing capabilities: + +- `validate`: verify that local Markdown references resolve successfully. +- `report`: generate a broader project-level summary of Markdown files and relationships. +- Additional reporting or machine-readable formats beyond today's human and JSON outputs. +- Broader documentation around path resolution, symlink handling, and rollback guarantees as dedicated reference material. diff --git a/doc/ProjectReview-2026-04-25.md b/doc/ProjectReview-2026-04-25.md index 7380e9a..10371fa 100644 --- a/doc/ProjectReview-2026-04-25.md +++ b/doc/ProjectReview-2026-04-25.md @@ -34,22 +34,6 @@ ## 优先级最高的改进点 -### 1. 同步设计文档与当前实现,避免文档成为误导源 - -优先级:高 - -观察依据: - -- [doc/DESIGN.md](./DESIGN.md) 目前只列出 `find`、`move`,并把 `validate`、`report` 写成潜在特性。 -- 但当前实现已经明确支持 `rename`、`dry-run`、JSON 输出、目录移动、事务式回滚、`.gitignore` 感知扫描等能力。 -- [README.md](../README.md) 已描述部分能力,但设计文档没有同步到位,容易让贡献者对系统边界产生错误判断。 - -建议方向: - -- 重写 [doc/DESIGN.md](./DESIGN.md),改成“当前实现 + 已知边界 + 后续路线图”的结构。 -- 把“已实现能力”和“未来候选能力”明确分开,不要混写在同一层级。 -- 补充 `rename`、目录移动、回滚模型、JSON 输出契约、忽略规则等内容。 - ### 2. 拆分 [src/core/mv.rs](../src/core/mv.rs),降低核心变更风险 优先级:高 @@ -66,21 +50,6 @@ - 比较自然的边界是:`validate`、`plan`、`apply`、`case_only`、`preview`、`transaction`。 - 第一阶段只做模块搬迁和命名收敛,不改行为;第二阶段再考虑局部抽象优化。 -### 3. 收敛 CLI 层重复逻辑,减少命令实现的平行分叉 - -优先级:中高 - -观察依据: - -- [src/commands/find.rs](../src/commands/find.rs)、[src/commands/mv.rs](../src/commands/mv.rs)、[src/commands/rename.rs](../src/commands/rename.rs) 都在重复创建 `ProgressBar`、设置样式、结束进度条、在 human/json 输出之间分支。 -- JSON 序列化的公共部分已经在 [src/commands/mod.rs](../src/commands/mod.rs) 里抽了一部分,但运行流程仍然分散。 -- `rename` 与 `mv` 的命令层尤其相似,只是输入语义不同。 - -建议方向: - -- 在 `commands` 下新增轻量辅助模块,例如 `progress.rs` 或 `ui.rs`。 -- 抽出统一的进度条创建逻辑与“完成后清理”逻辑。 -- 对 `mv` / `rename` 的 dry-run 与 json 输出路径做更强的共享封装,减少双份维护。 ### 4. 收敛公共 API 表面积,重新评估 `*_with_progress` 这一层包装