use crate::prelude::*;
use crate::{ItemType, RenameItem};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Change {
Content {
path: String,
replacements: usize,
},
RenameFile { path: String, to: String },
RenameDirectory { path: String, to: String },
}
impl Change {
pub fn path(&self) -> &str {
match self {
Change::Content { path, .. }
| Change::RenameFile { path, .. }
| Change::RenameDirectory { path, .. } => path,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Barrier {
PermissionDenied,
BrokenLink,
LinkLoop,
Vanished,
NotText,
Other,
}
impl Barrier {
pub fn is_permission(self) -> bool {
matches!(self, Barrier::PermissionDenied)
}
pub fn describe(self) -> &'static str {
match self {
Barrier::PermissionDenied => "not permitted to read",
Barrier::BrokenLink => "a link pointing at nothing",
Barrier::LinkLoop => "a link that points into itself",
Barrier::Vanished => "went away while the tree was being read",
Barrier::NotText => "not text in any encoding, so it was not searched",
Barrier::Other => "could not be read",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Unreadable {
pub path: String,
pub barrier: Barrier,
pub reason: String,
}
pub fn barrier_of(error: &walkdir::Error) -> Barrier {
if error.loop_ancestor().is_some() {
return Barrier::LinkLoop;
}
match error.io_error().map(std::io::Error::kind) {
Some(std::io::ErrorKind::PermissionDenied) => Barrier::PermissionDenied,
Some(std::io::ErrorKind::NotFound) => Barrier::BrokenLink,
None => Barrier::Other,
Some(_) => Barrier::Other,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan {
pub root: PathBuf,
pub changes: Vec<Change>,
pub files_renamed: usize,
pub directories_renamed: usize,
pub files_edited: usize,
pub replacements: usize,
pub unreadable: Vec<Unreadable>,
}
impl Plan {
pub fn total(&self) -> usize {
self.changes.len()
}
pub fn page(&self, offset: usize, limit: usize) -> &[Change] {
if offset >= self.changes.len() {
return &[];
}
let end = offset.saturating_add(limit).min(self.changes.len());
&self.changes[offset..end]
}
}
pub fn changes_of(
root: &Path,
content_files: &[PathBuf],
counts: &[usize],
rename_items: &[RenameItem],
) -> Vec<Change> {
let mut changes = Vec::with_capacity(content_files.len() + rename_items.len());
for (file, replacements) in content_files.iter().zip(counts.iter()) {
changes.push(Change::Content {
path: relative(root, file),
replacements: *replacements,
});
}
for item in rename_items {
let path = relative(root, &item.original_path);
let to = relative(root, &item.new_path);
changes.push(match item.item_type {
ItemType::Directory => Change::RenameDirectory { path, to },
ItemType::File => Change::RenameFile { path, to },
});
}
changes
}
pub const CONTEXT: usize = 2;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DiffLine {
Context { number: usize, text: String },
Removed { number: usize, text: String },
Added { number: usize, text: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hunk {
pub start: usize,
pub lines: Vec<DiffLine>,
}
pub fn hunks_of(original: &str, pairs: &[(String, String)]) -> Vec<Hunk> {
let mut changed: Vec<(usize, String, String)> = Vec::new();
for (index, line) in original.lines().enumerate() {
let mut replaced = line.to_string();
for (pattern, substitute) in pairs {
if !pattern.is_empty() && replaced.contains(pattern.as_str()) {
replaced = replaced.replace(pattern.as_str(), substitute);
}
}
if replaced != line {
changed.push((index + 1, line.to_string(), replaced));
}
}
if changed.is_empty() {
return Vec::new();
}
let lines: Vec<&str> = original.lines().collect();
let mut hunks: Vec<Hunk> = Vec::new();
for (number, before, after) in changed {
let first = number.saturating_sub(CONTEXT).max(1);
let extend = hunks
.last()
.is_some_and(|last| last_line(last) + 1 >= first);
if !extend {
hunks.push(Hunk {
start: first,
lines: Vec::new(),
});
}
let hunk = hunks.last_mut().expect("just pushed or extended");
let from = if hunk.lines.is_empty() {
first
} else {
last_line(hunk) + 1
};
for n in from..number {
hunk.lines.push(DiffLine::Context {
number: n,
text: lines[n - 1].to_string(),
});
}
hunk.lines.push(DiffLine::Removed {
number,
text: before,
});
hunk.lines.push(DiffLine::Added {
number,
text: after,
});
let trailing = (number + 1..=(number + CONTEXT).min(lines.len())).collect::<Vec<_>>();
for n in trailing {
hunk.lines.push(DiffLine::Context {
number: n,
text: lines[n - 1].to_string(),
});
}
}
hunks
}
fn last_line(hunk: &Hunk) -> usize {
hunk.lines
.last()
.map(|line| match line {
DiffLine::Context { number, .. }
| DiffLine::Removed { number, .. }
| DiffLine::Added { number, .. } => *number,
})
.unwrap_or(hunk.start)
}
fn relative(root: &Path, path: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.into_owned()
}
#[cfg(test)]
#[path = "preview_tests.rs"]
mod preview_tests;