duat_core/widgets/
file.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, [`Area::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`]: crate::widgets::LineNumbers
13//! [`Cursor`]: crate::mode::Cursor
14use std::{fs, path::PathBuf};
15
16use gapbuf::GapBuffer;
17
18use crate::{
19    cache::load_cache,
20    cfg::{IterCfg, PrintCfg},
21    context, form,
22    mode::Cursors,
23    text::{Text, err},
24    ui::{Area, PushSpecs, Ui},
25    widgets::{Widget, WidgetCfg},
26};
27
28/// The configuration for a new [`File`]
29#[derive(Default, Clone)]
30pub struct FileCfg {
31    text_op: TextOp,
32    cfg: PrintCfg,
33}
34
35impl FileCfg {
36    /// Returns a new instance of [`FileCfg`], opening a new buffer
37    pub(crate) fn new() -> Self {
38        FileCfg {
39            text_op: TextOp::NewBuffer,
40            cfg: PrintCfg::default_for_input(),
41        }
42    }
43
44    /// Changes the path of this cfg
45    pub(crate) fn open_path(self, path: PathBuf) -> Self {
46        Self { text_op: TextOp::OpenPath(path), ..self }
47    }
48
49    /// Takes a previous [`File`]
50    pub(crate) fn take_from_prev(
51        self,
52        buf: GapBuffer<u8>,
53        path_kind: PathKind,
54        has_unsaved_changes: bool,
55    ) -> Self {
56        Self {
57            text_op: TextOp::TakeBuf(buf, path_kind, has_unsaved_changes),
58            ..self
59        }
60    }
61
62    /// Sets the [`PrintCfg`]
63    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(buf, path, has_unsaved_changes) => match &path {
75                PathKind::SetExists(p) | PathKind::SetAbsent(p) => {
76                    let cursors = load_cache(p).unwrap_or_default();
77                    (Text::from_file(buf, cursors, p, has_unsaved_changes), path)
78                }
79                PathKind::NotSet(_) => (Text::from_buf(buf, Some(Cursors::default()), true), path),
80            },
81            TextOp::OpenPath(path) => {
82                let canon_path = path.canonicalize();
83                if let Ok(path) = &canon_path
84                    && let Ok(file) = std::fs::read_to_string(path)
85                {
86                    let cursors = load_cache(path).unwrap_or_default();
87                    let buf = GapBuffer::from_iter(file.bytes());
88                    (
89                        Text::from_file(buf, cursors, path, false),
90                        PathKind::SetExists(path.clone()),
91                    )
92                } else if canon_path.is_err()
93                    && let Ok(mut canon_path) = path.with_file_name(".").canonicalize()
94                {
95                    canon_path.push(path.file_name().unwrap());
96                    (Text::new_with_history(), PathKind::SetAbsent(canon_path))
97                } else {
98                    (Text::new_with_history(), PathKind::new_unset())
99                }
100            }
101        };
102
103        #[cfg(feature = "wack")]
104        let text = {
105            let mut text = text;
106            use crate::{
107                form::{self, Form},
108                text::{Key, Tag, text},
109            };
110
111            let key = Key::new();
112            let key2 = Key::new();
113            let form1 = form::set("form1lmao", Form::red().bold());
114            text.insert_tag(3, Tag::PushForm(form1), key);
115            text.insert_tag(2, Tag::PushForm(form1), key);
116            text.insert_tag(2, Tag::PushForm(form1), key2);
117
118            text.insert_tag(7, Tag::PopForm(form1), key);
119            text.insert_tag(8, Tag::PopForm(form1), key);
120            text.insert_tag(8, Tag::PopForm(form1), key2);
121
122            text
123        };
124
125        let file = File {
126            path,
127            text,
128            cfg: self.cfg,
129            printed_lines: Vec::new(),
130            layout_ordering: 0,
131        };
132
133        // The PushSpecs don't matter
134        (file, Box::new(|| false), PushSpecs::above())
135    }
136}
137
138/// The widget that is used to print and edit files
139pub struct File {
140    path: PathKind,
141    text: Text,
142    cfg: PrintCfg,
143    printed_lines: Vec<(usize, bool)>,
144    pub(crate) layout_ordering: usize,
145}
146
147impl File {
148    ////////// Writing the File
149
150    /// Writes the file to the current [`Path`], if one was set
151    ///
152    /// [`Path`]: std::path::Path
153    #[allow(clippy::result_large_err)]
154    pub fn write(&mut self) -> Result<usize, Text> {
155        if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
156            let path = path.clone();
157            self.text
158                .write_to(std::io::BufWriter::new(fs::File::create(&path)?))
159                .inspect(|_| self.path = PathKind::SetExists(path))
160                .map_err(Text::from)
161        } else {
162            Err(err!("No file was set"))
163        }
164    }
165
166    /// Writes the file to the given [`Path`]
167    ///
168    /// [`Path`]: std::path::Path
169    pub fn write_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<usize> {
170        self.text
171            .write_to(std::io::BufWriter::new(fs::File::create(path)?))
172    }
173
174    ////////// Path querying functions
175
176    /// The full path of the file.
177    ///
178    /// If there is no set path, returns `"*scratch file*#{id}"`.
179    pub fn path(&self) -> String {
180        self.path.path()
181    }
182
183    /// The full path of the file.
184    ///
185    /// Returns [`None`] if the path has not been set yet.
186    pub fn path_set(&self) -> Option<String> {
187        self.path.path_set()
188    }
189
190    /// The file's name.
191    ///
192    /// If there is no set path, returns `"*scratch file #{id}*"`.
193    pub fn name(&self) -> String {
194        self.path.name()
195    }
196
197    /// The file's name.
198    ///
199    /// Returns [`None`] if the path has not been set yet.
200    pub fn name_set(&self) -> Option<String> {
201        self.path.name_set()
202    }
203
204    pub fn path_kind(&self) -> PathKind {
205        self.path.clone()
206    }
207
208    /// Returns the currently printed set of lines.
209    ///
210    /// These are returned as a `usize`, showing the index of the line
211    /// in the file, and a `bool`, which is `true` when the line is
212    /// wrapped.
213    pub fn printed_lines(&self) -> &[(usize, bool)] {
214        &self.printed_lines
215    }
216
217    ////////// General querying functions
218
219    /// The number of bytes in the file.
220    pub fn len_bytes(&self) -> usize {
221        self.text.len().byte()
222    }
223
224    /// The number of [`char`]s in the file.
225    pub fn len_chars(&self) -> usize {
226        self.text.len().char()
227    }
228
229    /// The number of lines in the file.
230    pub fn len_lines(&self) -> usize {
231        self.text.len().line()
232    }
233
234    /// The [`Text`] of the [`File`]
235    pub fn text(&self) -> &Text {
236        &self.text
237    }
238
239    pub fn text_mut(&mut self) -> &mut Text {
240        &mut self.text
241    }
242
243    /// The mutable [`Text`] of the [`File`]
244    pub fn print_cfg(&self) -> PrintCfg {
245        self.cfg
246    }
247
248    /// The [`Cursors`] that are used on the [`Text`], if they exist
249    pub fn cursors(&self) -> Option<&Cursors> {
250        self.text.cursors()
251    }
252
253    /// A mutable reference to the [`Cursors`], if they exist
254    pub fn cursors_mut(&mut self) -> Option<&mut Cursors> {
255        self.text.cursors_mut()
256    }
257
258    /// Whether o not the [`File`] exists or not
259    pub fn exists(&self) -> bool {
260        self.path_set()
261            .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
262    }
263}
264
265impl<U: Ui> Widget<U> for File {
266    type Cfg = FileCfg;
267
268    fn cfg() -> Self::Cfg {
269        FileCfg::new()
270    }
271
272    fn update(&mut self, _area: &U::Area) {}
273
274    fn text(&self) -> &Text {
275        &self.text
276    }
277
278    fn text_mut(&mut self) -> &mut Text {
279        self.text_mut()
280    }
281
282    fn print_cfg(&self) -> PrintCfg {
283        self.cfg
284    }
285
286    fn print(&mut self, area: &<U as Ui>::Area) {
287        let (start, _) = area.first_points(&self.text, self.cfg);
288
289        let mut last_line = area
290            .rev_print_iter(self.text.iter_rev(start), IterCfg::new(self.cfg))
291            .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
292
293        self.printed_lines.clear();
294        let printed_lines = &mut self.printed_lines;
295
296        let mut has_wrapped = false;
297
298        area.print_with(
299            &mut self.text,
300            self.cfg,
301            form::painter(),
302            move |caret, item| {
303                has_wrapped |= caret.wrap;
304                if has_wrapped && item.part.is_char() {
305                    has_wrapped = false;
306                    let line = item.line();
307                    let wrapped = last_line.is_some_and(|ll| ll == line);
308                    last_line = Some(line);
309                    printed_lines.push((line, wrapped));
310                }
311            },
312        )
313    }
314
315    fn once() -> crate::Result<(), ()> {
316        Ok(())
317    }
318}
319
320/// Represents the presence or absence of a path
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub enum PathKind {
323    SetExists(PathBuf),
324    SetAbsent(PathBuf),
325    NotSet(usize),
326}
327
328impl PathKind {
329    /// Returns a new unset [`Path`]
330    fn new_unset() -> PathKind {
331        use std::sync::atomic::{AtomicUsize, Ordering};
332        static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
333
334        PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
335    }
336
337    pub fn path(&self) -> String {
338        match self {
339            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
340                path.to_string_lossy().to_string()
341            }
342            PathKind::NotSet(id) => {
343                let path = std::env::current_dir()
344                    .unwrap()
345                    .to_string_lossy()
346                    .to_string();
347
348                format!("{path}/*scratch file*#{id}")
349            }
350        }
351    }
352
353    pub fn path_set(&self) -> Option<String> {
354        match self {
355            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
356                Some(path.to_string_lossy().to_string())
357            }
358            PathKind::NotSet(_) => None,
359        }
360    }
361
362    pub fn name(&self) -> String {
363        match self {
364            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
365                let cur_dir = context::cur_dir();
366                if let Ok(path) = path.strip_prefix(cur_dir) {
367                    path.to_string_lossy().to_string()
368                } else {
369                    path.to_string_lossy().to_string()
370                }
371            }
372            PathKind::NotSet(id) => format!("*scratch file #{id}*"),
373        }
374    }
375
376    pub fn name_set(&self) -> Option<String> {
377        match self {
378            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
379                let cur_dir = context::cur_dir();
380                Some(if let Ok(path) = path.strip_prefix(cur_dir) {
381                    path.to_string_lossy().to_string()
382                } else {
383                    path.to_string_lossy().to_string()
384                })
385            }
386            PathKind::NotSet(_) => None,
387        }
388    }
389}
390
391/// What to do when opening the [`File`]
392#[derive(Default, Clone)]
393enum TextOp {
394    #[default]
395    NewBuffer,
396    TakeBuf(GapBuffer<u8>, PathKind, bool),
397    OpenPath(PathBuf),
398}