duat_core/file/
mod.rs

1//! The primary widget of Duat, used to display files.
2//!
3//! Most extensible features of Duat have the primary purpose of
4//! serving the [`File`], such as multiple [`Cursor`]s, a
5//! `History` system, [`RawArea::PrintInfo`], etc.
6//!
7//! The [`File`] also provides a list of printed lines through the
8//! [`File::printed_lines`] method. This method is notably used by the
9//! [`LineNumbers`] widget, that shows the numbers of the currently
10//! printed lines.
11//!
12//! [`LineNumbers`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.LineNumbers.html
13//! [`Cursor`]: crate::mode::Cursor
14use std::{fs, marker::PhantomData, path::PathBuf};
15
16use self::reader::Readers;
17pub use self::reader::{RangeList, Reader, ReaderCfg};
18use crate::{
19    cfg::PrintCfg,
20    context::{self, FileHandle, Handle, load_cache},
21    data::{Pass, RwData},
22    form::Painter,
23    hook::{self, FileWritten},
24    mode::Selections,
25    text::{Bytes, Text, txt},
26    ui::{PushSpecs, RawArea, Ui, Widget, WidgetCfg},
27};
28
29mod reader;
30
31/// The configuration for a new [`File`]
32#[derive(Default, Clone)]
33#[doc(hidden)]
34pub struct FileCfg {
35    text_op: TextOp,
36    cfg: PrintCfg,
37}
38
39impl FileCfg {
40    /// Returns a new instance of [`FileCfg`], opening a new buffer
41    pub(crate) fn new() -> Self {
42        FileCfg {
43            text_op: TextOp::NewBuffer,
44            cfg: PrintCfg::default_for_input(),
45        }
46    }
47
48    /// Changes the path of this cfg
49    pub(crate) fn open_path(self, path: PathBuf) -> Self {
50        Self { text_op: TextOp::OpenPath(path), ..self }
51    }
52
53    /// Takes a previous [`File`]
54    pub(crate) fn take_from_prev(
55        self,
56        bytes: Bytes,
57        pk: PathKind,
58        has_unsaved_changes: bool,
59    ) -> Self {
60        Self {
61            text_op: TextOp::TakeBuf(bytes, pk, has_unsaved_changes),
62            ..self
63        }
64    }
65
66    /// Sets the [`PrintCfg`]
67    pub(crate) fn set_print_cfg(&mut self, cfg: PrintCfg) {
68        self.cfg = cfg;
69    }
70}
71
72impl<U: Ui> WidgetCfg<U> for FileCfg {
73    type Widget = File<U>;
74
75    fn build(self, _: &mut Pass, _: Option<FileHandle<U>>) -> (Self::Widget, PushSpecs) {
76        let (text, path) = match self.text_op {
77            TextOp::NewBuffer => (Text::new_with_history(), PathKind::new_unset()),
78            TextOp::TakeBuf(bytes, pk, has_unsaved_changes) => match &pk {
79                PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
80                    let selections = {
81                        let cursor = load_cache(path).unwrap_or_default();
82                        Selections::new_with_main(cursor)
83                    };
84                    let text = Text::from_file(bytes, selections, path, has_unsaved_changes);
85                    (text, pk)
86                }
87                PathKind::NotSet(_) => (
88                    Text::from_bytes(bytes, Some(Selections::default()), true),
89                    pk,
90                ),
91            },
92            TextOp::OpenPath(path) => {
93                let canon_path = path.canonicalize();
94                if let Ok(path) = &canon_path
95                    && let Ok(file) = std::fs::read_to_string(path)
96                {
97                    let selections = {
98                        let cursor = load_cache(path).unwrap_or_default();
99                        Selections::new_with_main(cursor)
100                    };
101                    let text = Text::from_file(Bytes::new(&file), selections, path, false);
102                    (text, PathKind::SetExists(path.clone()))
103                } else if canon_path.is_err()
104                    && let Ok(mut canon_path) = path.with_file_name(".").canonicalize()
105                {
106                    canon_path.push(path.file_name().unwrap());
107                    (Text::new_with_history(), PathKind::SetAbsent(canon_path))
108                } else {
109                    (Text::new_with_history(), PathKind::new_unset())
110                }
111            }
112        };
113
114        let file = File {
115            path,
116            text,
117            cfg: self.cfg,
118            printed_lines: (0..40).map(|i| (i, i == 1)).collect(),
119            readers: Readers::default(),
120            layout_order: 0,
121            _ghost: PhantomData,
122        };
123
124        // The PushSpecs don't matter
125        (file, PushSpecs::above())
126    }
127}
128
129/// The widget that is used to print and edit files
130pub struct File<U: Ui> {
131    path: PathKind,
132    text: Text,
133    cfg: PrintCfg,
134    printed_lines: Vec<(usize, bool)>,
135    readers: Readers<U>,
136    pub(crate) layout_order: usize,
137    _ghost: PhantomData<U>,
138}
139
140impl<U: Ui> File<U> {
141    ////////// Writing the File
142
143    /// Writes the file to the current [`PathBuf`], if one was set
144    pub fn write(&mut self) -> Result<Option<usize>, Text> {
145        self.write_quit(false)
146    }
147
148    pub(crate) fn write_quit(&mut self, quit: bool) -> Result<Option<usize>, Text> {
149        if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
150            let path = path.clone();
151            if self.text.has_unsaved_changes() {
152                let bytes = self
153                    .text
154                    .write_to(std::io::BufWriter::new(fs::File::create(&path)?))
155                    .inspect(|_| self.path = PathKind::SetExists(path.clone()))?;
156
157                let path = path.to_string_lossy().to_string();
158                hook::queue(FileWritten((path, bytes, quit)));
159
160                Ok(Some(bytes))
161            } else {
162                Ok(None)
163            }
164        } else {
165            Err(txt!("No file was set").build())
166        }
167    }
168
169    /// Writes the file to the given [`Path`]
170    ///
171    /// [`Path`]: std::path::Path
172    pub fn write_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<usize>> {
173        self.write_quit_to(path, false)
174    }
175
176    /// Writes the file to the given [`Path`]
177    ///
178    /// [`Path`]: std::path::Path
179    pub(crate) fn write_quit_to(
180        &self,
181        path: impl AsRef<std::path::Path>,
182        quit: bool,
183    ) -> std::io::Result<Option<usize>> {
184        if self.text.has_unsaved_changes() {
185            let path = path.as_ref();
186            let res = self
187                .text
188                .write_to(std::io::BufWriter::new(fs::File::create(path)?))
189                .map(Some);
190
191            if let Ok(Some(bytes)) = res.as_ref() {
192                hook::queue(FileWritten((
193                    path.to_string_lossy().to_string(),
194                    *bytes,
195                    quit,
196                )));
197            }
198
199            res
200        } else {
201            Ok(None)
202        }
203    }
204
205    ////////// Path querying functions
206
207    /// The full path of the file.
208    ///
209    /// If there is no set path, returns `"*scratch file*#{id}"`.
210    pub fn path(&self) -> String {
211        self.path.path()
212    }
213
214    /// The full path of the file.
215    ///
216    /// Returns [`None`] if the path has not been set yet.
217    pub fn path_set(&self) -> Option<String> {
218        self.path.path_set()
219    }
220
221    /// The file's name.
222    ///
223    /// If there is no set path, returns `"*scratch file #{id}*"`.
224    pub fn name(&self) -> String {
225        self.path.name()
226    }
227
228    /// The file's name.
229    ///
230    /// Returns [`None`] if the path has not been set yet.
231    pub fn name_set(&self) -> Option<String> {
232        self.path.name_set()
233    }
234
235    /// The type of [`PathBuf`]
236    ///
237    /// This represents the three possible states for a [`File`]'s
238    /// [`PathBuf`], as it could either represent a real [`File`], not
239    /// exist, or not have been defined yet.
240    pub fn path_kind(&self) -> PathKind {
241        self.path.clone()
242    }
243
244    /// Returns the currently printed set of lines.
245    ///
246    /// These are returned as a `usize`, showing the index of the line
247    /// in the file, and a `bool`, which is `true` when the line is
248    /// wrapped.
249    pub fn printed_lines(&self) -> &[(usize, bool)] {
250        &self.printed_lines
251    }
252
253    ////////// General querying functions
254
255    /// The number of bytes in the file.
256    pub fn len_bytes(&self) -> usize {
257        self.text.len().byte()
258    }
259
260    /// The number of [`char`]s in the file.
261    pub fn len_chars(&self) -> usize {
262        self.text.len().char()
263    }
264
265    /// The number of lines in the file.
266    pub fn len_lines(&self) -> usize {
267        self.text.len().line()
268    }
269
270    /// The [`Selections`] that are used on the [`Text`], if they
271    /// exist
272    pub fn selections(&self) -> &Selections {
273        self.text.selections().unwrap()
274    }
275
276    /// A mutable reference to the [`Selections`], if they exist
277    pub fn selections_mut(&mut self) -> Option<&mut Selections> {
278        self.text.selections_mut()
279    }
280
281    /// Whether o not the [`File`] exists or not
282    pub fn exists(&self) -> bool {
283        self.path_set()
284            .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
285    }
286
287    /// Adds a [`Reader`] to react to [`Text`] [`Change`]s
288    ///
289    /// [`Change`]: crate::text::Change
290    pub fn add_reader(&mut self, pa: &mut Pass, cfg: impl ReaderCfg<U>) {
291        if let Err(err) = self.readers.add(pa, self.text.bytes_mut(), cfg) {
292            context::error!("{err}");
293        }
294    }
295
296    /// Gets a [`Reader`]
297    pub fn get_reader<R: Reader<U>>(&self) -> Option<RwData<R>> {
298        self.readers.get()
299    }
300}
301
302impl<U: Ui> Widget<U> for File<U> {
303    type Cfg = FileCfg;
304
305    fn cfg() -> Self::Cfg {
306        FileCfg::new()
307    }
308
309    fn update(pa: &mut Pass, handle: Handle<Self, U>) {
310        let (widget, area) = (handle.widget(), handle.area());
311        let (map, readers) = widget.read(pa, |file| {
312            (BytesDataMap(widget.clone()), file.readers.clone())
313        });
314
315        let moments = widget.acquire_mut(pa).text.last_unprocessed_moment();
316        if let Some(moments) = moments {
317            for moment in moments {
318                readers.process_moment(map.clone(), moment);
319            }
320        }
321
322        let mut file = widget.acquire_mut(pa);
323
324        if let Some(main) = file.text().selections().and_then(Selections::get_main) {
325            area.scroll_around_point(file.text(), main.caret(), file.print_cfg());
326        }
327
328        if file.readers.needs_update() {
329            let (start, _) = area.start_points(&file.text, file.cfg);
330            let (end, _) = area.end_points(&file.text, file.cfg);
331
332            // SAFETY: I'm not passing the Pass to inner structures, so this
333            // should be fine.
334            let mut pa = unsafe { Pass::new() };
335            readers.update_range(&mut pa, &mut file.text, start.byte()..end.byte());
336        }
337
338        file.text.update_bounds();
339    }
340
341    fn needs_update(&self) -> bool {
342        false
343    }
344
345    fn text(&self) -> &Text {
346        &self.text
347    }
348
349    fn text_mut(&mut self) -> &mut Text {
350        &mut self.text
351    }
352
353    fn print_cfg(&self) -> PrintCfg {
354        self.cfg
355    }
356
357    fn print(&mut self, painter: Painter, area: &<U as Ui>::Area) {
358        let (start, _) = area.start_points(&self.text, self.cfg);
359
360        let mut last_line = area
361            .rev_print_iter(self.text.iter_rev(start), self.cfg)
362            .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
363
364        self.printed_lines.clear();
365        let printed_lines = &mut self.printed_lines;
366
367        let mut has_wrapped = false;
368
369        area.print_with(&mut self.text, self.cfg, painter, move |caret, item| {
370            has_wrapped |= caret.wrap;
371            if has_wrapped && item.part.is_char() {
372                has_wrapped = false;
373                let line = item.line();
374                let wrapped = last_line.is_some_and(|ll| ll == line);
375                last_line = Some(line);
376                printed_lines.push((line, wrapped));
377            }
378        })
379    }
380
381    fn once() -> Result<(), Text> {
382        Ok(())
383    }
384}
385
386/// Represents the presence or absence of a path
387#[derive(Debug, Clone, PartialEq, Eq)]
388pub enum PathKind {
389    /// A [`PathBuf`] that has been defined and points to a real file
390    SetExists(PathBuf),
391    /// A [`PathBuf`] that has been defined but isn't a real file
392    SetAbsent(PathBuf),
393    /// A [`PathBuf`] that has not been defined
394    ///
395    /// The number within represents a specific [`File`], and when
396    /// printed to, for example, the [`StatusLine`], would show up as
397    /// `txt!("[file]*scratch file*#{id}")`
398    ///
399    /// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
400    NotSet(usize),
401}
402
403impl PathKind {
404    /// Returns a new unset [`PathBuf`]
405    fn new_unset() -> PathKind {
406        use std::sync::atomic::{AtomicUsize, Ordering};
407        static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
408
409        PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
410    }
411
412    /// The full path of the file.
413    ///
414    /// If there is no set path, returns `"*scratch file*#{id}"`.
415    pub fn path(&self) -> String {
416        match self {
417            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
418                path.to_string_lossy().to_string()
419            }
420            PathKind::NotSet(id) => {
421                format!("*scratch file*#{id}")
422            }
423        }
424    }
425
426    /// The full path of the file.
427    ///
428    /// Returns [`None`] if the path has not been set yet.
429    pub fn path_set(&self) -> Option<String> {
430        match self {
431            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
432                Some(path.to_string_lossy().to_string())
433            }
434            PathKind::NotSet(_) => None,
435        }
436    }
437
438    /// The file's name.
439    ///
440    /// If there is no set path, returns `"*scratch file #{id}*"`.
441    pub fn name(&self) -> String {
442        match self {
443            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
444                let cur_dir = context::cur_dir();
445                if let Ok(path) = path.strip_prefix(cur_dir) {
446                    path.to_string_lossy().to_string()
447                } else {
448                    path.to_string_lossy().to_string()
449                }
450            }
451            PathKind::NotSet(id) => format!("*scratch file #{id}*"),
452        }
453    }
454
455    /// The file's name.
456    ///
457    /// Returns [`None`] if the path has not been set yet.
458    pub fn name_set(&self) -> Option<String> {
459        match self {
460            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
461                let cur_dir = context::cur_dir();
462                Some(if let Ok(path) = path.strip_prefix(cur_dir) {
463                    path.to_string_lossy().to_string()
464                } else {
465                    path.to_string_lossy().to_string()
466                })
467            }
468            PathKind::NotSet(_) => None,
469        }
470    }
471}
472
473/// What to do when opening the [`File`]
474#[derive(Default, Clone)]
475enum TextOp {
476    #[default]
477    NewBuffer,
478    TakeBuf(Bytes, PathKind, bool),
479    OpenPath(PathBuf),
480}
481
482/// An [`RwData`] wrapper which only gives access to the [`Bytes`] of
483/// a [`File`]
484#[derive(Clone)]
485pub struct BytesDataMap<U: Ui>(RwData<File<U>>);
486
487impl<U: Ui> BytesDataMap<U> {
488    /// Reads the [`Bytes`] of the [`File`]'s [`Text`]
489    ///
490    /// If you are looking at this method from the context of
491    /// [`Reader::apply_changes`], you probably actually want to use
492    /// [`BytesDataMap::write_with_reader`], since it is far more
493    /// compatible with that usecase.
494    ///
495    /// # Panics
496    ///
497    /// Panics if there is a mutable borrow of this struct somewhere,
498    /// which could happen if you use [`RwData::write_unsafe`] or
499    /// [`RwData::write_unsafe_as`]
500    pub fn read<Ret>(&self, pa: &Pass, f: impl FnOnce(&Bytes) -> Ret) -> Ret {
501        self.0.read(pa, |file| f(file.text.bytes()))
502    }
503
504    /// Reads the [`Bytes`] of a [`File`], alongside a [`Reader`]
505    ///
506    /// This can be very convenient when you want access to these two
507    /// things at once, and is completely safe, since [`File`] doesn't
508    /// implement [`Reader`], the other [`RwData`] will never be
509    /// [`RwData<File>`], so a double borrow could never happen.
510    ///
511    /// # Panics
512    ///
513    /// Panics if there is are any borrows of either struct elsewhere,
514    /// which could happen if you use [`RwData::write_unsafe`] or
515    /// [`RwData::write_unsafe_as`]
516    pub fn write_with_reader<Ret, Rd: Reader<U>>(
517        &self,
518        pa: &mut Pass,
519        rd: &RwData<Rd>,
520        f: impl FnOnce(&mut Bytes, &mut Rd) -> Ret,
521    ) -> Ret {
522        // SAFETY: Since the other type is not a File, we can safely borrow
523        // both.
524        unsafe {
525            self.0
526                .write_unsafe(|file| rd.write(pa, |rd| f(file.text.bytes_mut(), rd)))
527        }
528    }
529
530    /// Wether someone else called [`write`] or [`write_as`] since the
531    /// last [`read`] or [`write`]
532    ///
533    /// Do note that this *DOES NOT* mean that the value inside has
534    /// actually been changed, it just means a mutable reference was
535    /// acquired after the last call to [`has_changed`].
536    ///
537    /// Generally though, you can use this method to gauge that.
538    ///
539    /// [`write`]: RwData::write
540    /// [`write_as`]: RwData::write_as
541    /// [`read`]: RwData::read
542    /// [`has_changed`]: RwData::has_changed
543    /// [`Text`]: crate::text::Text
544    /// [`Widget`]: crate::ui::Widget
545    pub fn has_changed(&self) -> bool {
546        self.0.has_changed()
547    }
548}