1use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use super::{Action, Outcome, Preview, edit, list, read, run, search};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Workspace {
12 root: PathBuf,
13}
14
15impl Workspace {
16 pub fn new(root: &Path) -> io::Result<Self> {
22 let root = fs::canonicalize(root)?;
23 if !root.is_dir() {
24 return Err(io::Error::new(io::ErrorKind::NotADirectory, "the workspace must be a folder"));
25 }
26 Ok(Self { root })
27 }
28
29 #[must_use]
31 pub fn root(&self) -> &Path {
32 &self.root
33 }
34
35 pub fn preview(&self, action: &Action) -> Result<Preview, String> {
42 match action {
43 Action::Read { path, .. } | Action::List { path, .. } | Action::Search { path, .. } => {
44 self.resolve(path).map(|_| Preview::Plain)
45 }
46 Action::Edit { path, old_text, new_text } => {
47 edit::plan(self, path, old_text, new_text).map(|plan| plan.preview())
48 }
49 Action::Run { command } => Ok(Preview::Command { command: command.clone() }),
50 }
51 }
52
53 pub fn perform(&self, action: &Action, cancelled: &dyn Fn() -> bool) -> Outcome {
58 let result = match action {
59 Action::Read { path, lines } => read::read(self, path, *lines),
60 Action::List { path, depth } => list::list(self, path, *depth),
61 Action::Search { pattern, path, glob, ignore_case } => {
62 search::search(self, pattern, path, glob.as_deref(), *ignore_case, cancelled, search::LIMIT)
63 }
64 Action::Edit { path, old_text, new_text } => edit::edit(self, path, old_text, new_text),
65 Action::Run { command } => return run::run(self, command, cancelled, run::LIMIT),
66 };
67 match result {
68 Ok(text) => Outcome::done(text),
69 Err(text) => Outcome::failed(text),
70 }
71 }
72
73 pub fn perform_as_shown(&self, action: &Action, shown: Option<&Preview>, cancelled: &dyn Fn() -> bool) -> Outcome {
77 self.perform_watching(action, shown, cancelled, &mut |_| {})
78 }
79
80 pub fn perform_watching(
83 &self,
84 action: &Action,
85 shown: Option<&Preview>,
86 cancelled: &dyn Fn() -> bool,
87 on_tail: &mut dyn FnMut(&str),
88 ) -> Outcome {
89 if let Action::Run { command } = action {
90 return run::run_watching(self, command, cancelled, run::LIMIT, on_tail);
91 }
92 if let (Action::Edit { path, old_text, new_text }, Some(shown)) = (action, shown) {
93 let now = edit::plan(self, path, old_text, new_text).map(|plan| plan.preview());
94 if now.as_ref().is_ok_and(|now| now != shown) {
95 return Outcome::failed(format!(
96 "`{path}` changed after the person was shown this edit, so nothing was written; read it again and ask anew."
97 ));
98 }
99 }
100 self.perform(action, cancelled)
101 }
102
103 pub(super) fn resolve(&self, path: &str) -> Result<PathBuf, String> {
110 let given = Path::new(path);
111 let joined = if given.is_absolute() { given.to_path_buf() } else { self.root.join(given) };
112 let mut existing = joined.as_path();
113 let mut missing = Vec::new();
114 while fs::symlink_metadata(existing).is_err() {
115 match (existing.file_name(), existing.parent()) {
118 (Some(name), Some(parent)) => {
119 missing.push(name);
120 existing = parent;
121 }
122 _ => {
123 return Err(format!("`{path}` cannot be resolved; use a plain path inside the working folder."));
124 }
125 }
126 }
127 let real = fs::canonicalize(existing).map_err(|_| {
128 format!("`{path}` goes through a link that leads nowhere; use a path inside the working folder.")
129 })?;
130 let real = missing.iter().rev().fold(real, |acc, name| acc.join(name));
131 if real.starts_with(&self.root) {
132 Ok(real)
133 } else {
134 Err(format!("`{path}` is outside the working folder; only paths inside it can be used."))
135 }
136 }
137
138 pub(super) fn relative(&self, real: &Path) -> String {
140 let relative = real.strip_prefix(&self.root).unwrap_or(real);
141 if relative.as_os_str().is_empty() { ".".to_owned() } else { relative.to_string_lossy().into_owned() }
142 }
143}
144
145pub(super) const READ_CAP: u64 = 16 * 1024 * 1024;
148
149pub(super) struct Contents {
151 pub(super) bytes: Vec<u8>,
153 pub(super) size: u64,
155}
156
157impl Contents {
158 pub(super) fn whole(&self) -> bool {
160 self.size <= READ_CAP
161 }
162}
163
164pub(super) fn read_capped(real: &Path) -> io::Result<Option<Contents>> {
168 use std::io::Read as _;
169 let meta = fs::metadata(real)?;
170 if !meta.is_file() {
171 return Ok(None);
172 }
173 let mut bytes = Vec::new();
174 fs::File::open(real)?.take(READ_CAP).read_to_end(&mut bytes)?;
175 let size = meta.len().max(u64::try_from(bytes.len()).unwrap_or(u64::MAX));
177 Ok(Some(Contents { bytes, size }))
178}
179
180pub(super) fn not_regular(shown: &str) -> String {
182 format!("`{shown}` is not a regular file (a pipe, a device or a socket); it cannot be read or edited.")
183}
184
185pub(super) fn io_problem(shown: &str, error: &io::Error) -> String {
187 match error.kind() {
188 io::ErrorKind::NotFound => format!("`{shown}` does not exist."),
189 io::ErrorKind::PermissionDenied => format!("`{shown}` cannot be used: permission denied."),
190 _ => format!("`{shown}` cannot be used: {error}."),
191 }
192}