1use std::{fs, path::PathBuf};
15
16use crate::{
17 cache::load_cache,
18 cfg::PrintCfg,
19 context, form,
20 hooks::{self, FileWritten},
21 mode::Cursors,
22 text::{Bytes, Text, err},
23 ui::{Area, PushSpecs, Ui},
24 widgets::{Widget, WidgetCfg},
25};
26
27#[derive(Default, Clone)]
29#[doc(hidden)]
30pub struct FileCfg {
31 text_op: TextOp,
32 cfg: PrintCfg,
33}
34
35impl FileCfg {
36 pub(crate) fn new() -> Self {
38 FileCfg {
39 text_op: TextOp::NewBuffer,
40 cfg: PrintCfg::default_for_input(),
41 }
42 }
43
44 pub(crate) fn open_path(self, path: PathBuf) -> Self {
46 Self { text_op: TextOp::OpenPath(path), ..self }
47 }
48
49 pub(crate) fn take_from_prev(
51 self,
52 bytes: Bytes,
53 pk: PathKind,
54 has_unsaved_changes: bool,
55 ) -> Self {
56 Self {
57 text_op: TextOp::TakeBuf(bytes, pk, has_unsaved_changes),
58 ..self
59 }
60 }
61
62 pub(crate) fn set_print_cfg(&mut self, cfg: PrintCfg) {
64 self.cfg = cfg;
65 }
66}
67
68impl<U: Ui> WidgetCfg<U> for FileCfg {
69 type Widget = File;
70
71 fn build(self, _: bool) -> (Self::Widget, impl Fn() -> bool, PushSpecs) {
72 let (text, path) = match self.text_op {
73 TextOp::NewBuffer => (Text::new_with_history(), PathKind::new_unset()),
74 TextOp::TakeBuf(bytes, pk, has_unsaved_changes) => match &pk {
75 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
76 let cursors = {
77 let cursor = load_cache(path).unwrap_or_default();
78 Cursors::new_with_main(cursor)
79 };
80 let text = Text::from_file(bytes, cursors, path, has_unsaved_changes);
81 (text, pk)
82 }
83 PathKind::NotSet(_) => {
84 (Text::from_bytes(bytes, Some(Cursors::default()), true), pk)
85 }
86 },
87 TextOp::OpenPath(path) => {
88 let canon_path = path.canonicalize();
89 if let Ok(path) = &canon_path
90 && let Ok(file) = std::fs::read_to_string(path)
91 {
92 let cursors = {
93 let cursor = load_cache(path).unwrap_or_default();
94 Cursors::new_with_main(cursor)
95 };
96 let text = Text::from_file(Bytes::new(&file), cursors, path, false);
97 (text, PathKind::SetExists(path.clone()))
98 } else if canon_path.is_err()
99 && let Ok(mut canon_path) = path.with_file_name(".").canonicalize()
100 {
101 canon_path.push(path.file_name().unwrap());
102 (Text::new_with_history(), PathKind::SetAbsent(canon_path))
103 } else {
104 (Text::new_with_history(), PathKind::new_unset())
105 }
106 }
107 };
108
109 let file = File {
110 path,
111 text,
112 cfg: self.cfg,
113 printed_lines: (0..40).map(|i| (i, i == 1)).collect(),
114 layout_ordering: 0,
115 };
116
117 (file, Box::new(|| false), PushSpecs::above())
119 }
120}
121
122pub struct File {
124 path: PathKind,
125 text: Text,
126 cfg: PrintCfg,
127 printed_lines: Vec<(usize, bool)>,
128 pub(crate) layout_ordering: usize,
129}
130
131impl File {
132 #[allow(clippy::result_large_err)]
138 pub fn write(&mut self) -> Result<Option<usize>, Text> {
139 if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
140 let path = path.clone();
141 if self.text.has_unsaved_changes() {
142 let res = self
143 .text
144 .write_to(std::io::BufWriter::new(fs::File::create(&path)?))
145 .inspect(|_| self.path = PathKind::SetExists(path.clone()))
146 .map(Some)
147 .map_err(Text::from);
148
149 if let Ok(Some(bytes)) = res.as_ref() {
150 hooks::trigger::<FileWritten>((path.to_string_lossy().to_string(), *bytes));
151 }
152
153 res
154 } else {
155 Ok(None)
156 }
157 } else {
158 Err(err!("No file was set"))
159 }
160 }
161
162 pub fn write_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<usize>> {
166 if self.text.has_unsaved_changes() {
167 let path = path.as_ref();
168 let res = self
169 .text
170 .write_to(std::io::BufWriter::new(fs::File::create(path)?))
171 .map(Some);
172
173 if let Ok(Some(bytes)) = res.as_ref() {
174 hooks::trigger::<FileWritten>((path.to_string_lossy().to_string(), *bytes));
175 }
176
177 res
178 } else {
179 Ok(None)
180 }
181 }
182
183 pub fn path(&self) -> String {
189 self.path.path()
190 }
191
192 pub fn path_set(&self) -> Option<String> {
196 self.path.path_set()
197 }
198
199 pub fn name(&self) -> String {
203 self.path.name()
204 }
205
206 pub fn name_set(&self) -> Option<String> {
210 self.path.name_set()
211 }
212
213 pub fn path_kind(&self) -> PathKind {
214 self.path.clone()
215 }
216
217 pub fn printed_lines(&self) -> &[(usize, bool)] {
223 &self.printed_lines
224 }
225
226 pub fn len_bytes(&self) -> usize {
230 self.text.len().byte()
231 }
232
233 pub fn len_chars(&self) -> usize {
235 self.text.len().char()
236 }
237
238 pub fn len_lines(&self) -> usize {
240 self.text.len().line()
241 }
242
243 pub fn text(&self) -> &Text {
245 &self.text
246 }
247
248 pub fn text_mut(&mut self) -> &mut Text {
249 &mut self.text
250 }
251
252 pub fn print_cfg(&self) -> PrintCfg {
254 self.cfg
255 }
256
257 pub fn cursors(&self) -> &Cursors {
259 self.text.cursors().unwrap()
260 }
261
262 pub fn cursors_mut(&mut self) -> Option<&mut Cursors> {
264 self.text.cursors_mut()
265 }
266
267 pub fn exists(&self) -> bool {
269 self.path_set()
270 .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
271 }
272}
273
274impl<U: Ui> Widget<U> for File {
275 type Cfg = FileCfg;
276
277 fn cfg() -> Self::Cfg {
278 FileCfg::new()
279 }
280
281 fn update(&mut self, _area: &U::Area) {}
282
283 fn text(&self) -> &Text {
284 &self.text
285 }
286
287 fn text_mut(&mut self) -> &mut Text {
288 self.text_mut()
289 }
290
291 fn print_cfg(&self) -> PrintCfg {
292 self.cfg
293 }
294
295 fn print(&mut self, area: &<U as Ui>::Area) {
296 let (start, _) = area.first_points(&self.text, self.cfg);
297
298 let mut last_line = area
299 .rev_print_iter(self.text.iter_rev(start), self.cfg)
300 .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
301
302 self.printed_lines.clear();
303 let printed_lines = &mut self.printed_lines;
304
305 let mut has_wrapped = false;
306
307 area.print_with(
308 &mut self.text,
309 self.cfg,
310 form::painter::<Self>(),
311 move |caret, item| {
312 has_wrapped |= caret.wrap;
313 if has_wrapped && item.part.is_char() {
314 has_wrapped = false;
315 let line = item.line();
316 let wrapped = last_line.is_some_and(|ll| ll == line);
317 last_line = Some(line);
318 printed_lines.push((line, wrapped));
319 }
320 },
321 )
322 }
323
324 fn once() -> Result<(), Text> {
325 Ok(())
326 }
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331pub enum PathKind {
332 SetExists(PathBuf),
333 SetAbsent(PathBuf),
334 NotSet(usize),
335}
336
337impl PathKind {
338 fn new_unset() -> PathKind {
340 use std::sync::atomic::{AtomicUsize, Ordering};
341 static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
342
343 PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
344 }
345
346 pub fn path(&self) -> String {
347 match self {
348 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
349 path.to_string_lossy().to_string()
350 }
351 PathKind::NotSet(id) => {
352 let path = std::env::current_dir()
353 .unwrap()
354 .to_string_lossy()
355 .to_string();
356
357 format!("{path}/*scratch file*#{id}")
358 }
359 }
360 }
361
362 pub fn path_set(&self) -> Option<String> {
363 match self {
364 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
365 Some(path.to_string_lossy().to_string())
366 }
367 PathKind::NotSet(_) => None,
368 }
369 }
370
371 pub fn name(&self) -> String {
372 match self {
373 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
374 let cur_dir = context::cur_dir();
375 if let Ok(path) = path.strip_prefix(cur_dir) {
376 path.to_string_lossy().to_string()
377 } else {
378 path.to_string_lossy().to_string()
379 }
380 }
381 PathKind::NotSet(id) => format!("*scratch file #{id}*"),
382 }
383 }
384
385 pub fn name_set(&self) -> Option<String> {
386 match self {
387 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
388 let cur_dir = context::cur_dir();
389 Some(if let Ok(path) = path.strip_prefix(cur_dir) {
390 path.to_string_lossy().to_string()
391 } else {
392 path.to_string_lossy().to_string()
393 })
394 }
395 PathKind::NotSet(_) => None,
396 }
397 }
398}
399
400#[derive(Default, Clone)]
402enum TextOp {
403 #[default]
404 NewBuffer,
405 TakeBuf(Bytes, PathKind, bool),
406 OpenPath(PathBuf),
407}