use std::path::{Path, PathBuf};
use crate::cli::ExamplesArgs;
pub fn parse_context(s: &str) -> (usize, usize) {
if let Some((b, a)) = s.split_once(':') {
let before = b.parse().unwrap_or(2);
let after = a.parse().unwrap_or(2);
(before, after)
} else {
let n = s.parse().unwrap_or(2);
(n, n)
}
}
fn collect_rs_files(dir: &Path, max_depth: u32) -> Vec<PathBuf> {
let mut result = Vec::new();
collect_rs_files_inner(dir, max_depth, &mut result);
result.sort();
result
}
fn collect_rs_files_inner(dir: &Path, depth_remaining: u32, out: &mut Vec<PathBuf>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if depth_remaining > 0 {
collect_rs_files_inner(&path, depth_remaining - 1, out);
}
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
fn collect_all_files(source_root: &Path, args: &ExamplesArgs) -> Vec<PathBuf> {
let mut files = Vec::new();
let examples_dir = source_root.join("examples");
if examples_dir.is_dir() {
files.extend(collect_rs_files(&examples_dir, 999));
}
if let Some(depth) = args.tests {
let tests_dir = source_root.join("tests");
if tests_dir.is_dir() {
files.extend(collect_rs_files(&tests_dir, depth.saturating_sub(1)));
}
}
if let Some(depth) = args.benches {
let benches_dir = source_root.join("benches");
if benches_dir.is_dir() {
files.extend(collect_rs_files(&benches_dir, depth.saturating_sub(1)));
}
}
files.sort();
files
}
fn render_list(source_root: &Path, files: &[PathBuf]) -> String {
let mut out = String::new();
for file in files {
let relative = file
.strip_prefix(source_root)
.unwrap_or(file)
.to_string_lossy()
.replace('\\', "/");
out.push_str(&format!("@{relative}\n"));
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(_) => {
out.push_str(" (read error)\n\n");
continue;
}
};
let mut has_doc = false;
for line in content.lines() {
let trimmed = line.trim();
if let Some(stripped) = trimmed.strip_prefix("//!") {
has_doc = true;
let doc = stripped.strip_prefix(' ').unwrap_or(stripped);
out.push_str(&format!(" {doc}\n"));
} else if trimmed.is_empty() {
continue;
} else {
break;
}
}
if !has_doc {
out.push_str(" (no module doc)\n");
}
out.push('\n');
}
out
}
fn digit_count(mut n: usize) -> usize {
if n == 0 {
return 1;
}
let mut count = 0;
while n > 0 {
n /= 10;
count += 1;
}
count
}
fn render_grep(source_root: &Path, files: &[PathBuf], pattern: &str, context: &str) -> String {
let (ctx_before, ctx_after) = parse_context(context);
let case_sensitive = pattern.chars().any(|c| c.is_uppercase());
let pattern_lower = pattern.to_lowercase();
let mut out = String::new();
for file in files {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(_) => continue,
};
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let matches: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, line)| {
if case_sensitive {
line.contains(pattern)
} else {
line.to_lowercase().contains(&pattern_lower)
}
})
.map(|(i, _)| i)
.collect();
if matches.is_empty() {
continue;
}
let mut ranges: Vec<(usize, usize)> = Vec::new();
for &m in &matches {
let start = m.saturating_sub(ctx_before);
let end = (m + ctx_after).min(total - 1);
if let Some(last) = ranges.last_mut()
&& start <= last.1 + 1
{
last.1 = last.1.max(end);
continue;
}
ranges.push((start, end));
}
let max_line_no = ranges.last().map_or(1, |r| r.1 + 1);
let width = digit_count(max_line_no).max(4);
let relative = file
.strip_prefix(source_root)
.unwrap_or(file)
.to_string_lossy()
.replace('\\', "/");
out.push_str(&format!("@{relative}\n"));
let match_set: std::collections::HashSet<usize> = matches.iter().copied().collect();
for (range_idx, &(start, end)) in ranges.iter().enumerate() {
if range_idx > 0 {
out.push_str(" ...\n");
}
for (i, line) in lines.iter().enumerate().take(end + 1).skip(start) {
let line_no = i + 1; let marker = if match_set.contains(&i) { '*' } else { ' ' };
out.push_str(&format!(
"{marker}{line_no:>width$}: {line}\n",
width = width,
));
}
}
out.push('\n');
}
out
}
pub fn render_examples(source_root: &Path, crate_display: &str, args: &ExamplesArgs) -> String {
let files = collect_all_files(source_root, args);
if files.is_empty() {
return format!("// no examples found for {crate_display}\n");
}
let mut out = format!("// examples for {crate_display}\n");
out.push_str(&format!("// root: {}/\n\n", source_root.display()));
if let Some(ref pattern) = args.pattern() {
let grep_output = render_grep(source_root, &files, pattern, &args.context);
if grep_output.is_empty() {
out.push_str(&format!("// no matches for \"{pattern}\"\n"));
} else {
out.push_str(&grep_output);
}
} else {
out.push_str(&render_list(source_root, &files));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_context_single() {
assert_eq!(parse_context("3"), (3, 3));
assert_eq!(parse_context("0"), (0, 0));
}
#[test]
fn test_parse_context_pair() {
assert_eq!(parse_context("1:3"), (1, 3));
assert_eq!(parse_context("0:5"), (0, 5));
}
#[test]
fn test_parse_context_invalid() {
assert_eq!(parse_context("abc"), (2, 2)); }
#[test]
fn test_digit_count() {
assert_eq!(digit_count(0), 1);
assert_eq!(digit_count(1), 1);
assert_eq!(digit_count(9), 1);
assert_eq!(digit_count(10), 2);
assert_eq!(digit_count(99), 2);
assert_eq!(digit_count(100), 3);
assert_eq!(digit_count(9999), 4);
}
}