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, [`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`]: 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::parser::InnerParsers;
17pub use self::parser::{FileParts, FileSnapshot, Parser, ParserBox, ParserCfg, Parsers};
18use crate::{
19    cfg::{NewLine, PrintCfg, ScrollOff, TabStops, WordChars, WrapMethod},
20    context::{self, Cache, Handle},
21    data::Pass,
22    form::Painter,
23    hook::{self, FileWritten},
24    mode::{Selection, Selections},
25    text::{Bytes, Text, txt},
26    ui::{Area, BuildInfo, PushSpecs, Ui, Widget, WidgetCfg},
27};
28
29mod parser;
30
31/// The configuration for a new [`File`]
32#[derive(Default)]
33#[doc(hidden)]
34pub struct FileCfg<U: Ui> {
35    text_op: TextOp,
36    print_cfg: PrintCfg,
37    add_parsers: Option<Box<dyn FnOnce(&mut File<U>)>>,
38}
39
40impl<U: Ui> FileCfg<U> {
41    /// Returns a new instance of [`FileCfg`], opening a new buffer
42    pub(crate) fn new() -> Self {
43        FileCfg {
44            text_op: TextOp::NewBuffer,
45            print_cfg: PrintCfg::default_for_input(),
46            add_parsers: None,
47        }
48    }
49
50    /// Adds a [`Parser`] to the [`File`]
51    pub fn with_parser(mut self, parser_cfg: impl ParserCfg<U> + 'static) -> Self {
52        let add_parsers = std::mem::take(&mut self.add_parsers);
53        self.add_parsers = Some(Box::new(move |file| {
54            if let Some(prev_add_parsers) = add_parsers {
55                prev_add_parsers(file)
56            }
57
58            if let Err(err) = file.parsers.add(file, parser_cfg) {
59                context::error!("{err}");
60            }
61        }));
62
63        self
64    }
65
66    ////////// PrintCfg functions
67
68    /// A mutable reference to the [`PrintCfg`]
69    ///
70    /// You mostly won't need this, as you can just use the other
71    /// [`PrintCfg`] derived methods for the [`FileCfg`].
72    pub fn print_cfg(&mut self) -> &mut PrintCfg {
73        &mut self.print_cfg
74    }
75
76    /// Don't wrap when reaching the end of the area
77    pub const fn dont_wrap(mut self) -> Self {
78        self.print_cfg.dont_wrap();
79        self
80    }
81
82    /// Wrap on the right edge of the area
83    pub const fn wrap_on_edge(mut self) -> Self {
84        self.print_cfg.wrap_method = WrapMethod::Edge;
85        self
86    }
87
88    /// Wrap on [word] terminations
89    ///
90    /// [word]: word_chars
91    pub const fn wrap_on_word(mut self) -> Self {
92        self.print_cfg.wrap_method = WrapMethod::Edge;
93        self
94    }
95
96    /// Wrap on a given distance from the left edge
97    ///
98    /// This can wrap beyond the screen, being a mix of [`unwrapped`]
99    /// and [`edge_wrapped`].
100    ///
101    /// [`unwrapped`]: Self::unwrapped
102    /// [`edge_wrapped`]: Self::edge_wrapped
103    pub const fn wrap_at(mut self, cap: u8) -> Self {
104        self.print_cfg.wrap_method = WrapMethod::Capped(cap);
105        self
106    }
107
108    /// Reindent wrapped lines to the same level of indentation
109    pub const fn indent_wraps(mut self, value: bool) -> Self {
110        self.print_cfg.indent_wrapped = value;
111        self
112    }
113
114    /// Sets the size of tabs
115    pub const fn tabstop(mut self, tabstop: u8) -> Self {
116        self.print_cfg.tab_stops = TabStops(tabstop);
117        self
118    }
119
120    /// Sets a character to replace `'\n'`s with
121    pub const fn new_line_as(mut self, char: char) -> Self {
122        self.print_cfg.new_line = NewLine::AlwaysAs(char);
123        self
124    }
125
126    /// Sets a character to replace `'\n'` only with trailing white
127    /// space
128    pub const fn trailing_new_line_as(mut self, char: char) -> Self {
129        self.print_cfg.new_line = NewLine::AfterSpaceAs(char);
130        self
131    }
132
133    /// Sets the horizontal and vertical scrolloff, respectively
134    pub const fn scrolloff(mut self, x: u8, y: u8) -> Self {
135        self.print_cfg.scrolloff = ScrollOff { x, y };
136        self
137    }
138
139    /// Sets the horizontal scrolloff
140    pub const fn x_scrolloff(mut self, x: u8) -> Self {
141        self.print_cfg.scrolloff = ScrollOff { y: self.print_cfg.scrolloff.y, x };
142        self
143    }
144
145    /// Sets the vertical scrolloff
146    pub const fn y_scrolloff(mut self, y: u8) -> Self {
147        self.print_cfg.scrolloff = ScrollOff { y, x: self.print_cfg.scrolloff.x };
148        self
149    }
150
151    /// Sets the [`WordChars`]
152    pub const fn word_chars(mut self, word_chars: WordChars) -> Self {
153        self.print_cfg.word_chars = word_chars;
154        self
155    }
156
157    /// Sets a forced horizontal scrolloff
158    ///
159    /// Without forced horizontal scrolloff, when you reach the end of
160    /// a long line of text, the cursor will also reach the edge of
161    /// the screen. With this enabled, Duat will keep a distance
162    /// between the cursor and the edge of the screen.
163    ///
164    /// This is particularly useful in a situation like the
165    /// [`PromptLine`] widget, in order to keep good visibility of the
166    /// command.
167    ///
168    /// [`PromptLine`]: docs.rs/duat-utils/latest/duat_utils/widgets/struct.PromptLine.html
169    pub const fn forced_horizontal_scrolloff(mut self, value: bool) -> Self {
170        self.print_cfg.force_scrolloff = value;
171        self
172    }
173
174    ////////// Path functions
175
176    /// The path that the [`File`] will open with, if it was set
177    pub fn path_set(&self) -> Option<String> {
178        match &self.text_op {
179            TextOp::TakeBuf(_, PathKind::NotSet(_), _) | TextOp::NewBuffer => None,
180            TextOp::TakeBuf(_, PathKind::SetExists(path) | PathKind::SetAbsent(path), _)
181            | TextOp::OpenPath(path) => Some(path.to_str()?.to_string()),
182        }
183    }
184
185    /// Changes the path of this cfg
186    pub(crate) fn open_path(self, path: PathBuf) -> Self {
187        Self { text_op: TextOp::OpenPath(path), ..self }
188    }
189
190    /// Takes a previous [`File`]
191    pub(crate) fn take_from_prev(
192        self,
193        bytes: Bytes,
194        pk: PathKind,
195        has_unsaved_changes: bool,
196    ) -> Self {
197        Self {
198            text_op: TextOp::TakeBuf(bytes, pk, has_unsaved_changes),
199            ..self
200        }
201    }
202}
203
204impl<U: Ui> WidgetCfg<U> for FileCfg<U> {
205    type Widget = File<U>;
206
207    fn build(self, _: &mut Pass, _: BuildInfo<U>) -> (Self::Widget, PushSpecs) {
208        let (text, path) = match self.text_op {
209            TextOp::NewBuffer => (Text::new_with_history(), PathKind::new_unset()),
210            TextOp::TakeBuf(bytes, pk, has_unsaved_changes) => match &pk {
211                PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
212                    let selections = {
213                        let cursor = Cache::new().load(path).unwrap_or_default();
214                        Selections::new(cursor)
215                    };
216                    let text = Text::from_file(bytes, selections, path, has_unsaved_changes);
217                    (text, pk)
218                }
219                PathKind::NotSet(_) => (
220                    Text::from_bytes(bytes, Selections::new(Selection::default()), true),
221                    pk,
222                ),
223            },
224            TextOp::OpenPath(path) => {
225                let canon_path = path.canonicalize();
226                if let Ok(path) = &canon_path
227                    && let Ok(file) = std::fs::read_to_string(path)
228                {
229                    let selections = {
230                        let cursor = Cache::new().load(path).unwrap_or_default();
231                        Selections::new(cursor)
232                    };
233                    let text = Text::from_file(Bytes::new(&file), selections, path, false);
234                    (text, PathKind::SetExists(path.clone()))
235                } else if canon_path.is_err()
236                    && let Ok(mut canon_path) = path.with_file_name(".").canonicalize()
237                {
238                    canon_path.push(path.file_name().unwrap());
239                    (Text::new_with_history(), PathKind::SetAbsent(canon_path))
240                } else {
241                    (Text::new_with_history(), PathKind::new_unset())
242                }
243            }
244        };
245
246        let mut file = File {
247            path,
248            text,
249            cfg: self.print_cfg,
250            printed_lines: (0..40).map(|i| (i, i == 1)).collect(),
251            parsers: InnerParsers::default(),
252            layout_order: 0,
253            _ghost: PhantomData,
254        };
255
256        if let Some(add_parsers) = self.add_parsers {
257            add_parsers(&mut file);
258        }
259
260        // The PushSpecs don't matter
261        (file, PushSpecs::above())
262    }
263}
264
265impl<U: Ui> Clone for FileCfg<U> {
266    fn clone(&self) -> Self {
267        Self {
268            text_op: self.text_op.clone(),
269            print_cfg: self.print_cfg,
270            add_parsers: None,
271        }
272    }
273}
274
275/// The widget that is used to print and edit files
276pub struct File<U: Ui> {
277    path: PathKind,
278    text: Text,
279    printed_lines: Vec<(usize, bool)>,
280    parsers: InnerParsers<U>,
281    /// The [`PrintCfg`] of this [`File`]
282    pub cfg: PrintCfg,
283    pub(crate) layout_order: usize,
284    _ghost: PhantomData<U>,
285}
286
287impl<U: Ui> File<U> {
288    ////////// Writing the File
289
290    /// Writes the file to the current [`PathBuf`], if one was set
291    pub fn save(&mut self) -> Result<Option<usize>, Text> {
292        self.save_quit(false)
293    }
294
295    pub(crate) fn save_quit(&mut self, quit: bool) -> Result<Option<usize>, Text> {
296        if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
297            let path = path.clone();
298            if self.text.has_unsaved_changes() {
299                let bytes = self
300                    .text
301                    .write_to(std::io::BufWriter::new(fs::File::create(&path)?))
302                    .inspect(|_| self.path = PathKind::SetExists(path.clone()))?;
303
304                let path = path.to_string_lossy().to_string();
305                hook::queue(FileWritten((path, bytes, quit)));
306
307                Ok(Some(bytes))
308            } else {
309                Ok(None)
310            }
311        } else {
312            Err(txt!("No file was set").build())
313        }
314    }
315
316    /// Writes the file to the given [`Path`]
317    ///
318    /// [`Path`]: std::path::Path
319    pub fn save_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<usize>> {
320        self.save_quit_to(path, false)
321    }
322
323    /// Writes the file to the given [`Path`]
324    ///
325    /// [`Path`]: std::path::Path
326    pub(crate) fn save_quit_to(
327        &self,
328        path: impl AsRef<std::path::Path>,
329        quit: bool,
330    ) -> std::io::Result<Option<usize>> {
331        if self.text.has_unsaved_changes() {
332            let path = path.as_ref();
333            let res = self
334                .text
335                .write_to(std::io::BufWriter::new(fs::File::create(path)?))
336                .map(Some);
337
338            if let Ok(Some(bytes)) = res.as_ref() {
339                hook::queue(FileWritten((
340                    path.to_string_lossy().to_string(),
341                    *bytes,
342                    quit,
343                )));
344            }
345
346            res
347        } else {
348            Ok(None)
349        }
350    }
351
352    ////////// Path querying functions
353
354    /// The full path of the file.
355    ///
356    /// If there is no set path, returns `"*scratch file*#{id}"`.
357    pub fn path(&self) -> String {
358        self.path.path()
359    }
360
361    /// The full path of the file.
362    ///
363    /// Returns [`None`] if the path has not been set yet.
364    pub fn path_set(&self) -> Option<String> {
365        self.path.path_set()
366    }
367
368    /// The file's name.
369    ///
370    /// If there is no set path, returns `"*scratch file #{id}*"`.
371    pub fn name(&self) -> String {
372        self.path.name()
373    }
374
375    /// The file's name.
376    ///
377    /// Returns [`None`] if the path has not been set yet.
378    pub fn name_set(&self) -> Option<String> {
379        self.path.name_set()
380    }
381
382    /// The type of [`PathBuf`]
383    ///
384    /// This represents the three possible states for a [`File`]'s
385    /// [`PathBuf`], as it could either represent a real [`File`], not
386    /// exist, or not have been defined yet.
387    pub fn path_kind(&self) -> PathKind {
388        self.path.clone()
389    }
390
391    /// Returns the currently printed set of lines.
392    ///
393    /// These are returned as a `usize`, showing the index of the line
394    /// in the file, and a `bool`, which is `true` when the line is
395    /// wrapped.
396    pub fn printed_lines(&self) -> &[(usize, bool)] {
397        &self.printed_lines
398    }
399
400    ////////// General querying functions
401
402    /// The [`Bytes`] of the [`File`]'s [`Text`]
403    pub fn bytes(&self) -> &Bytes {
404        self.text.bytes()
405    }
406
407    /// The number of bytes in the file.
408    pub fn len_bytes(&self) -> usize {
409        self.text.len().byte()
410    }
411
412    /// The number of [`char`]s in the file.
413    pub fn len_chars(&self) -> usize {
414        self.text.len().char()
415    }
416
417    /// The number of lines in the file.
418    pub fn len_lines(&self) -> usize {
419        self.text.len().line()
420    }
421
422    /// The [`Selections`] that are used on the [`Text`], if they
423    /// exist
424    pub fn selections(&self) -> &Selections {
425        self.text.selections()
426    }
427
428    /// A mutable reference to the [`Selections`], if they exist
429    pub fn selections_mut(&mut self) -> &mut Selections {
430        self.text.selections_mut()
431    }
432
433    /// Whether o not the [`File`] exists or not
434    pub fn exists(&self) -> bool {
435        self.path_set()
436            .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
437    }
438
439    /// Reads a specific [`Parser`], if it was [added]
440    ///
441    /// If the [`Parser`] was sent to another thread, this function
442    /// will block until it returns to this thread. If you don't wish
443    /// for this behaviour, see [`File::try_read_parser`].
444    ///
445    /// This function will also update the [`Parser`]s with the latest
446    /// changes that happened in the [`File`], keeping state
447    /// consistent even as you are actively updating it within the
448    /// same scope. Do note that a [`Parser`] that was in this thread,
449    /// could be sent to another thread because of this.
450    ///
451    /// [added]: Handle::add_parser
452    pub fn read_parser<Rd: Parser<U>, Ret>(&self, read: impl FnOnce(&Rd) -> Ret) -> Option<Ret> {
453        // SAFETY: The Pass is never borrowed at the same time that the `read`
454        // function is called
455        let pa = &mut unsafe { Pass::new() };
456
457        // In theory, it's possible to call try_read_parser or read_parser
458        // from within this function, which would call
459        // self.text.unprocessed_moments.
460        // Because of the Option::take of the Rd within,
461        // self.parsers.process_moment would panic.
462        // However, self.text.unprocessed_moments should only return Some on
463        // the first call, i.e., before any Option::take calls, so it
464        // shouldn't be a problem.
465        if let Some(moments) = self.text.unprocessed_moments() {
466            let cfg = self.print_cfg();
467            for moment in moments {
468                self.parsers.process_moment(pa, moment, cfg);
469            }
470        }
471
472        self.parsers.read_parser(read)
473    }
474
475    /// Tries tor read a specific [`Parser`], if it was [added]
476    ///
477    /// Not only does it not trigger if the [`Parser`] doesn't exist,
478    /// also will not trigger if it was sent to another thread, and
479    /// isn't ready to be brought back. If you wish to wait for the
480    ///
481    /// This function will also update the [`Parser`]s with the latest
482    /// changes that happened in the [`File`], keeping state
483    /// consistent even as you are actively updating it within the
484    /// same scope. Do note that a [`Parser`] that was in this thread,
485    /// could be sent to another thread because of this.
486    ///
487    /// [added]: Handle::add_parser
488    pub fn try_read_parser<Rd: Parser<U>, Ret>(
489        &self,
490        read: impl FnOnce(&Rd) -> Ret,
491    ) -> Option<Ret> {
492        // SAFETY: The Pass is never borrowed at the same time that the `read`
493        // function is called
494        let pa = &mut unsafe { Pass::new() };
495
496        if let Some(moments) = self.text.unprocessed_moments() {
497            let cfg = self.print_cfg();
498            for moment in moments {
499                self.parsers.process_moment(pa, moment, cfg);
500            }
501        }
502
503        self.parsers.try_read_parser(read)
504    }
505}
506
507impl<U: Ui> Handle<File<U>, U> {
508    /// Adds a [`Parser`] to react to [`Text`] [`Change`]s
509    ///
510    /// [`Change`]: crate::text::Change
511    pub fn add_parser(&mut self, pa: &mut Pass, cfg: impl ParserCfg<U>) {
512        let file = self.widget().read(pa);
513
514        if let Err(err) = file.parsers.add(file, cfg) {
515            context::error!("{err}");
516        }
517    }
518}
519
520impl<U: Ui> Widget<U> for File<U> {
521    type Cfg = FileCfg<U>;
522
523    fn cfg() -> Self::Cfg {
524        FileCfg::new()
525    }
526
527    fn update(pa: &mut Pass, handle: &Handle<Self, U>) {
528        let parsers = std::mem::take(&mut handle.write(pa).parsers);
529
530        let file = handle.read(pa);
531        let cfg = file.cfg;
532
533        for moment in file.text.unprocessed_moments().into_iter().flatten() {
534            parsers.process_moment(pa, moment, cfg);
535        }
536
537        let (file, area) = handle.write_with_area(pa);
538
539        if let Some(main) = file.text().selections().get_main() {
540            area.scroll_around_point(file.text(), main.caret(), file.print_cfg());
541        }
542
543        let (start, _) = area.start_points(&file.text, file.cfg);
544        let (end, _) = area.end_points(&file.text, file.cfg);
545
546        parsers.update_range(&mut file.text, start..end);
547        file.parsers = parsers;
548
549        file.text.update_bounds();
550    }
551
552    fn needs_update(&self, _: &Pass) -> bool {
553        self.parsers.needs_update()
554    }
555
556    fn text(&self) -> &Text {
557        &self.text
558    }
559
560    fn text_mut(&mut self) -> &mut Text {
561        &mut self.text
562    }
563
564    fn print_cfg(&self) -> PrintCfg {
565        self.cfg
566    }
567
568    fn print(&mut self, painter: Painter, area: &<U as Ui>::Area) {
569        let (start, _) = area.start_points(&self.text, self.cfg);
570
571        let mut last_line = area
572            .rev_print_iter(self.text.iter_rev(start), self.cfg)
573            .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
574
575        self.printed_lines.clear();
576        let printed_lines = &mut self.printed_lines;
577
578        let mut has_wrapped = false;
579
580        area.print_with(&mut self.text, self.cfg, painter, move |caret, item| {
581            has_wrapped |= caret.wrap;
582            if has_wrapped && item.part.is_char() {
583                has_wrapped = false;
584                let line = item.line();
585                let wrapped = last_line.is_some_and(|ll| ll == line);
586                last_line = Some(line);
587                printed_lines.push((line, wrapped));
588            }
589        })
590    }
591
592    fn once() -> Result<(), Text> {
593        Ok(())
594    }
595}
596
597/// Represents the presence or absence of a path
598#[derive(Debug, Clone, PartialEq, Eq)]
599pub enum PathKind {
600    /// A [`PathBuf`] that has been defined and points to a real file
601    SetExists(PathBuf),
602    /// A [`PathBuf`] that has been defined but isn't a real file
603    SetAbsent(PathBuf),
604    /// A [`PathBuf`] that has not been defined
605    ///
606    /// The number within represents a specific [`File`], and when
607    /// printed to, for example, the [`StatusLine`], would show up as
608    /// `txt!("[file]*scratch file*#{id}")`
609    ///
610    /// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
611    NotSet(usize),
612}
613
614impl PathKind {
615    /// Returns a new unset [`PathBuf`]
616    fn new_unset() -> PathKind {
617        use std::sync::atomic::{AtomicUsize, Ordering};
618        static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
619
620        PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
621    }
622
623    /// The full path of the file.
624    ///
625    /// If there is no set path, returns `"*scratch file*#{id}"`.
626    pub fn path(&self) -> String {
627        match self {
628            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
629                path.to_string_lossy().to_string()
630            }
631            PathKind::NotSet(id) => {
632                format!("*scratch file*#{id}")
633            }
634        }
635    }
636
637    /// The full path of the file.
638    ///
639    /// Returns [`None`] if the path has not been set yet.
640    pub fn path_set(&self) -> Option<String> {
641        match self {
642            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
643                Some(path.to_string_lossy().to_string())
644            }
645            PathKind::NotSet(_) => None,
646        }
647    }
648
649    /// The file's name.
650    ///
651    /// If there is no set path, returns `"*scratch file #{id}*"`.
652    pub fn name(&self) -> String {
653        match self {
654            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
655                let cur_dir = context::cur_dir();
656                if let Ok(path) = path.strip_prefix(cur_dir) {
657                    path.to_string_lossy().to_string()
658                } else {
659                    path.to_string_lossy().to_string()
660                }
661            }
662            PathKind::NotSet(id) => format!("*scratch file #{id}*"),
663        }
664    }
665
666    /// The file's name.
667    ///
668    /// Returns [`None`] if the path has not been set yet.
669    pub fn name_set(&self) -> Option<String> {
670        match self {
671            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
672                let cur_dir = context::cur_dir();
673                Some(if let Ok(path) = path.strip_prefix(cur_dir) {
674                    path.to_string_lossy().to_string()
675                } else {
676                    path.to_string_lossy().to_string()
677                })
678            }
679            PathKind::NotSet(_) => None,
680        }
681    }
682
683    /// A [`Text`] from the full path of this [`PathKind`]
684    ///
685    /// # Formatting
686    ///
687    /// If the file's `path` was set:
688    ///
689    /// ```text
690    /// [file]{path}
691    /// ```
692    ///
693    /// If the file's `path` was not set:
694    ///
695    /// ```text
696    /// [file.new.scratch]*scratch file #{id}*
697    /// ```
698    pub fn path_txt(&self) -> Text {
699        match self {
700            PathKind::SetExists(path) | PathKind::SetAbsent(path) => txt!("[file]{path}").build(),
701            PathKind::NotSet(id) => txt!("[file.new.scratch]*scratch file #{id}*").build(),
702        }
703    }
704
705    /// A [`Text`] from the name of this [`PathKind`]
706    ///
707    /// # Formatting
708    ///
709    /// If the file's `name` was set:
710    ///
711    /// ```text
712    /// [file]{name}
713    /// ```
714    ///
715    /// If the file's `name` was not set:
716    ///
717    /// ```text
718    /// [file.new.scratch]*scratch file #{id}*
719    /// ```
720    pub fn name_txt(&self) -> Text {
721        match self {
722            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
723                let cur_dir = context::cur_dir();
724                if let Ok(path) = path.strip_prefix(cur_dir) {
725                    path.to_string_lossy().to_string()
726                } else {
727                    path.to_string_lossy().to_string()
728                };
729                txt!("[file]{path}").build()
730            }
731            PathKind::NotSet(id) => txt!("[file.new.scratch]*scratch file #{id}*").build(),
732        }
733    }
734}
735
736/// What to do when opening the [`File`]
737#[derive(Default, Clone)]
738enum TextOp {
739    #[default]
740    NewBuffer,
741    TakeBuf(Bytes, PathKind, bool),
742    OpenPath(PathBuf),
743}