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        if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
146            let path = path.clone();
147            if self.text.has_unsaved_changes() {
148                let bytes = self
149                    .text
150                    .write_to(std::io::BufWriter::new(fs::File::create(&path)?))
151                    .inspect(|_| self.path = PathKind::SetExists(path.clone()))?;
152
153                let path = path.to_string_lossy().to_string();
154                hook::queue(FileWritten((path, bytes)));
155
156                Ok(Some(bytes))
157            } else {
158                Ok(None)
159            }
160        } else {
161            Err(txt!("No file was set").build())
162        }
163    }
164
165    /// Writes the file to the given [`Path`]
166    ///
167    /// [`Path`]: std::path::Path
168    pub fn write_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<usize>> {
169        if self.text.has_unsaved_changes() {
170            let path = path.as_ref();
171            let res = self
172                .text
173                .write_to(std::io::BufWriter::new(fs::File::create(path)?))
174                .map(Some);
175
176            if let Ok(Some(bytes)) = res.as_ref() {
177                hook::queue(FileWritten((path.to_string_lossy().to_string(), *bytes)));
178            }
179
180            res
181        } else {
182            Ok(None)
183        }
184    }
185
186    ////////// Path querying functions
187
188    /// The full path of the file.
189    ///
190    /// If there is no set path, returns `"*scratch file*#{id}"`.
191    pub fn path(&self) -> String {
192        self.path.path()
193    }
194
195    /// The full path of the file.
196    ///
197    /// Returns [`None`] if the path has not been set yet.
198    pub fn path_set(&self) -> Option<String> {
199        self.path.path_set()
200    }
201
202    /// The file's name.
203    ///
204    /// If there is no set path, returns `"*scratch file #{id}*"`.
205    pub fn name(&self) -> String {
206        self.path.name()
207    }
208
209    /// The file's name.
210    ///
211    /// Returns [`None`] if the path has not been set yet.
212    pub fn name_set(&self) -> Option<String> {
213        self.path.name_set()
214    }
215
216    /// The type of [`PathBuf`]
217    ///
218    /// This represents the three possible states for a [`File`]'s
219    /// [`PathBuf`], as it could either represent a real [`File`], not
220    /// exist, or not have been defined yet.
221    pub fn path_kind(&self) -> PathKind {
222        self.path.clone()
223    }
224
225    /// Returns the currently printed set of lines.
226    ///
227    /// These are returned as a `usize`, showing the index of the line
228    /// in the file, and a `bool`, which is `true` when the line is
229    /// wrapped.
230    pub fn printed_lines(&self) -> &[(usize, bool)] {
231        &self.printed_lines
232    }
233
234    ////////// General querying functions
235
236    /// The number of bytes in the file.
237    pub fn len_bytes(&self) -> usize {
238        self.text.len().byte()
239    }
240
241    /// The number of [`char`]s in the file.
242    pub fn len_chars(&self) -> usize {
243        self.text.len().char()
244    }
245
246    /// The number of lines in the file.
247    pub fn len_lines(&self) -> usize {
248        self.text.len().line()
249    }
250
251    /// The [`Selections`] that are used on the [`Text`], if they
252    /// exist
253    pub fn selections(&self) -> &Selections {
254        self.text.selections().unwrap()
255    }
256
257    /// A mutable reference to the [`Selections`], if they exist
258    pub fn selections_mut(&mut self) -> Option<&mut Selections> {
259        self.text.selections_mut()
260    }
261
262    /// Whether o not the [`File`] exists or not
263    pub fn exists(&self) -> bool {
264        self.path_set()
265            .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
266    }
267
268    /// Adds a [`Reader`] to react to [`Text`] [`Change`]s
269    ///
270    /// [`Change`]: crate::text::Change
271    pub fn add_reader(&mut self, pa: &mut Pass, cfg: impl ReaderCfg<U>) {
272        if let Err(err) = self.readers.add(pa, self.text.bytes_mut(), cfg) {
273            context::error!("{err}");
274        }
275    }
276
277    /// Gets a [`Reader`]
278    pub fn get_reader<R: Reader<U>>(&self) -> Option<RwData<R>> {
279        self.readers.get()
280    }
281}
282
283impl<U: Ui> Widget<U> for File<U> {
284    type Cfg = FileCfg;
285
286    fn cfg() -> Self::Cfg {
287        FileCfg::new()
288    }
289
290    fn update(pa: &mut Pass, handle: Handle<Self, U>) {
291        let (widget, area) = (handle.widget(), handle.area());
292        let (map, readers) = widget.read(pa, |file| {
293            (BytesDataMap(widget.clone()), file.readers.clone())
294        });
295
296        let moments = widget.acquire_mut(pa).text.last_unprocessed_moment();
297        if let Some(moments) = moments {
298            for moment in moments {
299                readers.process_moment(map.clone(), moment);
300            }
301        }
302
303        let mut file = widget.acquire_mut(pa);
304
305        if let Some(main) = file.text().selections().and_then(Selections::get_main) {
306            area.scroll_around_point(file.text(), main.caret(), file.print_cfg());
307        }
308
309        if file.readers.needs_update() {
310            let (start, _) = area.first_points(&file.text, file.cfg);
311            let (end, _) = area.last_points(&file.text, file.cfg);
312
313            // SAFETY: I'm not passing the Pass to inner structures, so this
314            // should be fine.
315            let mut pa = unsafe { Pass::new() };
316            readers.update_range(&mut pa, &mut file.text, start.byte()..end.byte());
317        }
318
319        file.text.update_bounds();
320    }
321
322    fn text(&self) -> &Text {
323        &self.text
324    }
325
326    fn text_mut(&mut self) -> &mut Text {
327        &mut self.text
328    }
329
330    fn needs_update(&self) -> bool {
331        false
332    }
333
334    fn print_cfg(&self) -> PrintCfg {
335        self.cfg
336    }
337
338    fn print(&mut self, painter: Painter, area: &<U as Ui>::Area) {
339        let (start, _) = area.first_points(&self.text, self.cfg);
340
341        let mut last_line = area
342            .rev_print_iter(self.text.iter_rev(start), self.cfg)
343            .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
344
345        self.printed_lines.clear();
346        let printed_lines = &mut self.printed_lines;
347
348        let mut has_wrapped = false;
349
350        area.print_with(&mut self.text, self.cfg, painter, move |caret, item| {
351            has_wrapped |= caret.wrap;
352            if has_wrapped && item.part.is_char() {
353                has_wrapped = false;
354                let line = item.line();
355                let wrapped = last_line.is_some_and(|ll| ll == line);
356                last_line = Some(line);
357                printed_lines.push((line, wrapped));
358            }
359        })
360    }
361
362    fn once() -> Result<(), Text> {
363        Ok(())
364    }
365}
366
367/// Represents the presence or absence of a path
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub enum PathKind {
370    /// A [`PathBuf`] that has been defined and points to a real file
371    SetExists(PathBuf),
372    /// A [`PathBuf`] that has been defined but isn't a real file
373    SetAbsent(PathBuf),
374    /// A [`PathBuf`] that has not been defined
375    ///
376    /// The number within represents a specific [`File`], and when
377    /// printed to, for example, the [`StatusLine`], would show up as
378    /// `txt!("[file]*scratch file*#{id}")`
379    ///
380    /// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
381    NotSet(usize),
382}
383
384impl PathKind {
385    /// Returns a new unset [`PathBuf`]
386    fn new_unset() -> PathKind {
387        use std::sync::atomic::{AtomicUsize, Ordering};
388        static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
389
390        PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
391    }
392
393    /// The full path of the file.
394    ///
395    /// If there is no set path, returns `"*scratch file*#{id}"`.
396    pub fn path(&self) -> String {
397        match self {
398            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
399                path.to_string_lossy().to_string()
400            }
401            PathKind::NotSet(id) => {
402                format!("*scratch file*#{id}")
403            }
404        }
405    }
406
407    /// The full path of the file.
408    ///
409    /// Returns [`None`] if the path has not been set yet.
410    pub fn path_set(&self) -> Option<String> {
411        match self {
412            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
413                Some(path.to_string_lossy().to_string())
414            }
415            PathKind::NotSet(_) => None,
416        }
417    }
418
419    /// The file's name.
420    ///
421    /// If there is no set path, returns `"*scratch file #{id}*"`.
422    pub fn name(&self) -> String {
423        match self {
424            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
425                let cur_dir = context::cur_dir();
426                if let Ok(path) = path.strip_prefix(cur_dir) {
427                    path.to_string_lossy().to_string()
428                } else {
429                    path.to_string_lossy().to_string()
430                }
431            }
432            PathKind::NotSet(id) => format!("*scratch file #{id}*"),
433        }
434    }
435
436    /// The file's name.
437    ///
438    /// Returns [`None`] if the path has not been set yet.
439    pub fn name_set(&self) -> Option<String> {
440        match self {
441            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
442                let cur_dir = context::cur_dir();
443                Some(if let Ok(path) = path.strip_prefix(cur_dir) {
444                    path.to_string_lossy().to_string()
445                } else {
446                    path.to_string_lossy().to_string()
447                })
448            }
449            PathKind::NotSet(_) => None,
450        }
451    }
452}
453
454/// What to do when opening the [`File`]
455#[derive(Default, Clone)]
456enum TextOp {
457    #[default]
458    NewBuffer,
459    TakeBuf(Bytes, PathKind, bool),
460    OpenPath(PathBuf),
461}
462
463/// An [`RwData`] wrapper which only gives access to the [`Bytes`] of
464/// a [`File`]
465#[derive(Clone)]
466pub struct BytesDataMap<U: Ui>(RwData<File<U>>);
467
468impl<U: Ui> BytesDataMap<U> {
469    /// Reads the [`Bytes`] of the [`File`]'s [`Text`]
470    ///
471    /// If you are looking at this method from the context of
472    /// [`Reader::apply_changes`], you probably actually want to use
473    /// [`BytesDataMap::read_and_write_reader`], since it is far more
474    /// compatible with that usecase.
475    ///
476    /// # Panics
477    ///
478    /// Panics if there is a mutable borrow of this struct somewhere,
479    /// which could happen if you use [`RwData::write_unsafe`] or
480    /// [`RwData::write_unsafe_as`]
481    pub fn read<Ret>(&self, pa: &Pass, f: impl FnOnce(&Bytes) -> Ret) -> Ret {
482        self.0.read(pa, |file| f(file.text.bytes()))
483    }
484
485    /// Reads the [`Bytes`] of a [`File`], alongside a [`Reader`]
486    ///
487    /// This can be very convenient when you want access to these two
488    /// things at once, and is completely safe, since [`File`] doesn't
489    /// implement [`Reader`], the other [`RwData`] will never be
490    /// [`RwData<File>`], so a double borrow could never happen.
491    ///
492    /// # Panics
493    ///
494    /// Panics if there is are any borrows of either struct elsewhere,
495    /// which could happen if you use [`RwData::write_unsafe`] or
496    /// [`RwData::write_unsafe_as`]
497    pub fn read_and_write_reader<Ret, Rd: Reader<U>>(
498        &self,
499        pa: &mut Pass,
500        rd: &RwData<Rd>,
501        f: impl FnOnce(&Bytes, &mut Rd) -> Ret,
502    ) -> Ret {
503        // SAFETY: Since the other type is not a File, we can safely borrow
504        // both.
505        unsafe {
506            self.0
507                .read_unsafe(|file| rd.write(pa, |rd| f(file.text.bytes(), rd)))
508        }
509    }
510
511    /// Wether someone else called [`write`] or [`write_as`] since the
512    /// last [`read`] or [`write`]
513    ///
514    /// Do note that this *DOES NOT* mean that the value inside has
515    /// actually been changed, it just means a mutable reference was
516    /// acquired after the last call to [`has_changed`].
517    ///
518    /// Generally though, you can use this method to gauge that.
519    ///
520    /// [`write`]: RwData::write
521    /// [`write_as`]: RwData::write_as
522    /// [`read`]: RwData::read
523    /// [`has_changed`]: RwData::has_changed
524    /// [`Text`]: crate::text::Text
525    /// [`Widget`]: crate::ui::Widget
526    pub fn has_changed(&self) -> bool {
527        self.0.has_changed()
528    }
529}