use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use super::{Action, Outcome, Preview, edit, list, read, run, search};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Workspace {
root: PathBuf,
}
impl Workspace {
pub fn new(root: &Path) -> io::Result<Self> {
let root = fs::canonicalize(root)?;
if !root.is_dir() {
return Err(io::Error::new(io::ErrorKind::NotADirectory, "the workspace must be a folder"));
}
Ok(Self { root })
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
pub fn preview(&self, action: &Action) -> Result<Preview, String> {
match action {
Action::Read { path, .. } | Action::List { path, .. } | Action::Search { path, .. } => {
self.resolve(path).map(|_| Preview::Plain)
}
Action::Edit { path, old_text, new_text } => {
edit::plan(self, path, old_text, new_text).map(|plan| plan.preview())
}
Action::Run { command } => Ok(Preview::Command { command: command.clone() }),
}
}
pub fn perform(&self, action: &Action, cancelled: &dyn Fn() -> bool) -> Outcome {
let result = match action {
Action::Read { path, lines } => read::read(self, path, *lines),
Action::List { path, depth } => list::list(self, path, *depth),
Action::Search { pattern, path, glob, ignore_case } => {
search::search(self, pattern, path, glob.as_deref(), *ignore_case, cancelled, search::LIMIT)
}
Action::Edit { path, old_text, new_text } => edit::edit(self, path, old_text, new_text),
Action::Run { command } => return run::run(self, command, cancelled, run::LIMIT),
};
match result {
Ok(text) => Outcome::done(text),
Err(text) => Outcome::failed(text),
}
}
pub fn perform_as_shown(&self, action: &Action, shown: Option<&Preview>, cancelled: &dyn Fn() -> bool) -> Outcome {
self.perform_watching(action, shown, cancelled, &mut |_| {})
}
pub fn perform_watching(
&self,
action: &Action,
shown: Option<&Preview>,
cancelled: &dyn Fn() -> bool,
on_tail: &mut dyn FnMut(&str),
) -> Outcome {
if let Action::Run { command } = action {
return run::run_watching(self, command, cancelled, run::LIMIT, on_tail);
}
if let (Action::Edit { path, old_text, new_text }, Some(shown)) = (action, shown) {
let now = edit::plan(self, path, old_text, new_text).map(|plan| plan.preview());
if now.as_ref().is_ok_and(|now| now != shown) {
return Outcome::failed(format!(
"`{path}` changed after the person was shown this edit, so nothing was written; read it again and ask anew."
));
}
}
self.perform(action, cancelled)
}
pub(super) fn resolve(&self, path: &str) -> Result<PathBuf, String> {
let given = Path::new(path);
let joined = if given.is_absolute() { given.to_path_buf() } else { self.root.join(given) };
let mut existing = joined.as_path();
let mut missing = Vec::new();
while fs::symlink_metadata(existing).is_err() {
match (existing.file_name(), existing.parent()) {
(Some(name), Some(parent)) => {
missing.push(name);
existing = parent;
}
_ => {
return Err(format!("`{path}` cannot be resolved; use a plain path inside the working folder."));
}
}
}
let real = fs::canonicalize(existing).map_err(|_| {
format!("`{path}` goes through a link that leads nowhere; use a path inside the working folder.")
})?;
let real = missing.iter().rev().fold(real, |acc, name| acc.join(name));
if real.starts_with(&self.root) {
Ok(real)
} else {
Err(format!("`{path}` is outside the working folder; only paths inside it can be used."))
}
}
pub(super) fn relative(&self, real: &Path) -> String {
let relative = real.strip_prefix(&self.root).unwrap_or(real);
if relative.as_os_str().is_empty() { ".".to_owned() } else { relative.to_string_lossy().into_owned() }
}
}
pub(super) const READ_CAP: u64 = 16 * 1024 * 1024;
pub(super) struct Contents {
pub(super) bytes: Vec<u8>,
pub(super) size: u64,
}
impl Contents {
pub(super) fn whole(&self) -> bool {
self.size <= READ_CAP
}
}
pub(super) fn read_capped(real: &Path) -> io::Result<Option<Contents>> {
use std::io::Read as _;
let meta = fs::metadata(real)?;
if !meta.is_file() {
return Ok(None);
}
let mut bytes = Vec::new();
fs::File::open(real)?.take(READ_CAP).read_to_end(&mut bytes)?;
let size = meta.len().max(u64::try_from(bytes.len()).unwrap_or(u64::MAX));
Ok(Some(Contents { bytes, size }))
}
pub(super) fn not_regular(shown: &str) -> String {
format!("`{shown}` is not a regular file (a pipe, a device or a socket); it cannot be read or edited.")
}
pub(super) fn io_problem(shown: &str, error: &io::Error) -> String {
match error.kind() {
io::ErrorKind::NotFound => format!("`{shown}` does not exist."),
io::ErrorKind::PermissionDenied => format!("`{shown}` cannot be used: permission denied."),
_ => format!("`{shown}` cannot be used: {error}."),
}
}