duat_core/buffer/
mod.rs

1//! The primary [`Widget`] of Duat, used to display buffers.
2//!
3//! Most extensible features of Duat have the primary purpose of
4//! serving the [`Buffer`], such as multiple [`Cursor`]s, a
5//! `History` system, [`RawArea::PrintInfo`], etc.
6//!
7//! The [`Buffer`] also provides a list of printed lines through the
8//! [`Buffer::printed_lines`] method. This method is notably used by
9//! the [`LineNumbers`] widget, that shows the numbers of the
10//! currently printed lines.
11//!
12//! [`LineNumbers`]: https://docs.rs/duat/latest/duat/widgets/struct.LineNumbers.html
13//! [`Cursor`]: crate::mode::Cursor
14//! [`RawArea::PrintInfo`]: crate::ui::traits::RawArea::PrintInfo
15use std::{
16    fs,
17    path::{Path, PathBuf},
18    sync::Arc,
19};
20
21use crossterm::event::{MouseButton, MouseEventKind};
22use parking_lot::{Mutex, MutexGuard};
23
24use self::parser::Parsers;
25pub use self::parser::{BufferTracker, Parser};
26use crate::{
27    context::{self, Cache, Handle},
28    data::Pass,
29    form::Painter,
30    hook::{self, BufferWritten},
31    mode::{MouseEvent, Selections},
32    opts::PrintOpts,
33    session::TwoPointsPlace,
34    text::{BuilderPart, Bytes, Text, txt},
35    ui::{RwArea, Widget},
36};
37
38mod parser;
39
40/// The widget that is used to print and edit buffers
41pub struct Buffer {
42    path: PathKind,
43    text: Text,
44    printed_lines: Mutex<Vec<(usize, bool)>>,
45    parsers: Parsers,
46    sync_opts: Arc<Mutex<PrintOpts>>,
47    pub(crate) layout_order: usize,
48    /// The [`PrintOpts`] of this [`Buffer`]
49    ///
50    /// You can use this member to change the way this `Buffer` will
51    /// be printed specifically.
52    pub opts: PrintOpts,
53    prev_opts: PrintOpts,
54}
55
56impl Buffer {
57    /// Returns a new [`Buffer`], private for now
58    pub(crate) fn new(path: Option<PathBuf>, opts: PrintOpts) -> Self {
59        let (text, path) = match path {
60            Some(path) => {
61                let canon_path = path.canonicalize();
62                if let Ok(path) = &canon_path
63                    && let Ok(buffer) = std::fs::read_to_string(path)
64                {
65                    let selections = {
66                        let selection = Cache::new().load(path).unwrap_or_default();
67                        Selections::new(selection)
68                    };
69                    let text = Text::from_parts(Bytes::new(&buffer), selections, true);
70                    (text, PathKind::SetExists(path.clone()))
71                } else if canon_path.is_err()
72                    && let Ok(mut canon_path) = path.with_file_name(".").canonicalize()
73                {
74                    canon_path.push(path.file_name().unwrap());
75                    (Text::new_with_history(), PathKind::SetAbsent(canon_path))
76                } else {
77                    (Text::new_with_history(), PathKind::new_unset())
78                }
79            }
80            None => (Text::new_with_history(), PathKind::new_unset()),
81        };
82
83        Self {
84            path,
85            text,
86            sync_opts: Arc::new(Mutex::new(opts)),
87            printed_lines: Mutex::new(Vec::new()),
88            parsers: Parsers::default(),
89            layout_order: 0,
90            opts,
91            prev_opts: opts,
92        }
93    }
94
95    ////////// Saving the Buffer
96
97    /// Writes the buffer to the current [`PathBuf`], if one was set
98    pub fn save(&mut self) -> Result<Option<usize>, Text> {
99        self.save_quit(false)
100    }
101
102    /// Saves and quits, resulting in no config reload
103    pub(crate) fn save_quit(&mut self, quit: bool) -> Result<Option<usize>, Text> {
104        if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
105            let path = path.clone();
106            if self.text.has_unsaved_changes() {
107                let bytes = self
108                    .text
109                    .save_on(std::io::BufWriter::new(fs::File::create(&path)?))
110                    .inspect(|_| self.path = PathKind::SetExists(path.clone()))?;
111
112                let path = path.to_string_lossy().to_string();
113                hook::queue(BufferWritten((path, bytes, quit)));
114
115                Ok(Some(bytes))
116            } else {
117                Ok(None)
118            }
119        } else {
120            Err(txt!("No buffer was set"))
121        }
122    }
123
124    /// Writes the buffer to the given [`Path`]
125    ///
126    /// [`Path`]: std::path::Path
127    pub fn save_to(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<usize>> {
128        self.save_quit_to(path, false)
129    }
130
131    /// Writes the buffer to the given [`Path`]
132    ///
133    /// [`Path`]: std::path::Path
134    pub(crate) fn save_quit_to(
135        &mut self,
136        path: impl AsRef<std::path::Path>,
137        quit: bool,
138    ) -> std::io::Result<Option<usize>> {
139        if self.text.has_unsaved_changes() {
140            let path = path.as_ref();
141            let res = self
142                .text
143                .save_on(std::io::BufWriter::new(fs::File::create(path)?))
144                .map(Some);
145
146            if let Ok(Some(bytes)) = res.as_ref() {
147                hook::queue(BufferWritten((
148                    path.to_string_lossy().to_string(),
149                    *bytes,
150                    quit,
151                )));
152            }
153
154            res
155        } else {
156            Ok(None)
157        }
158    }
159
160    ////////// Path querying functions
161
162    /// The full path of the buffer.
163    ///
164    /// If there is no set path, returns `"*scratch buffer*#{id}"`.
165    pub fn path(&self) -> String {
166        self.path.path()
167    }
168
169    /// The full path of the buffer.
170    ///
171    /// Returns [`None`] if the path has not been set yet, i.e., if
172    /// the buffer is a scratch buffer.
173    pub fn path_set(&self) -> Option<String> {
174        self.path.path_set()
175    }
176
177    /// A [`Text`] from the full path of this [`PathKind`]
178    ///
179    /// # Formatting
180    ///
181    /// If the buffer's `path` was set:
182    ///
183    /// ```text
184    /// [buffer]{path}
185    /// ```
186    ///
187    /// If the buffer's `path` was not set:
188    ///
189    /// ```text
190    /// [buffer.new.scratch]*scratch buffer #{id}*
191    /// ```
192    pub fn path_txt(&self) -> Text {
193        self.path_kind().path_txt()
194    }
195
196    /// The buffer's name.
197    ///
198    /// If there is no set path, returns `"*scratch buffer #{id}*"`.
199    pub fn name(&self) -> String {
200        self.path.name()
201    }
202
203    /// The buffer's name.
204    ///
205    /// Returns [`None`] if the path has not been set yet, i.e., if
206    /// the buffer is a scratch buffer.
207    pub fn name_set(&self) -> Option<String> {
208        self.path.name_set()
209    }
210
211    /// A [`Text`] from the name of this [`PathKind`]
212    ///
213    /// The name of a [`Buffer`] widget is the same as the path, but
214    /// it strips away the current directory. If it can't, it will
215    /// try to strip away the home directory, replacing it with
216    /// `"~"`. If that also fails, it will just show the full
217    /// path.
218    ///
219    /// # Formatting
220    ///
221    /// If the buffer's `name` was set:
222    ///
223    /// ```text
224    /// [buffer]{name}
225    /// ```
226    ///
227    /// If the buffer's `name` was not set:
228    ///
229    /// ```text
230    /// [buffer.new.scratch]*scratch buffer #{id}*
231    /// ```
232    pub fn name_txt(&self) -> Text {
233        self.path.name_txt()
234    }
235
236    /// The type of [`PathBuf`]
237    ///
238    /// This represents the three possible states for a [`Buffer`]'s
239    /// [`PathBuf`], as it could either represent a real [`Buffer`],
240    /// not exist, or not have been defined yet.
241    pub fn path_kind(&self) -> PathKind {
242        self.path.clone()
243    }
244
245    /// Returns the currently printed set of lines.
246    ///
247    /// These are returned as a `usize`, showing the index of the line
248    /// in the buffer, and a `bool`, which is `true` when the line is
249    /// wrapped.
250    pub fn printed_lines(&self) -> MutexGuard<'_, Vec<(usize, bool)>> {
251        self.printed_lines.lock()
252    }
253
254    ////////// General querying functions
255
256    /// The [`Bytes`] of the [`Buffer`]'s [`Text`]
257    pub fn bytes(&self) -> &Bytes {
258        self.text.bytes()
259    }
260
261    /// The number of bytes in the buffer.
262    pub fn len_bytes(&self) -> usize {
263        self.text.len().byte()
264    }
265
266    /// The number of [`char`]s in the buffer.
267    pub fn len_chars(&self) -> usize {
268        self.text.len().char()
269    }
270
271    /// The number of lines in the buffer.
272    pub fn len_lines(&self) -> usize {
273        self.text.len().line()
274    }
275
276    /// The [`Selections`] that are used on the [`Text`]
277    pub fn selections(&self) -> &Selections {
278        self.text.selections()
279    }
280
281    /// A mutable reference to the [`Selections`]
282    pub fn selections_mut(&mut self) -> &mut Selections {
283        self.text.selections_mut()
284    }
285
286    /// Whether o not the [`Buffer`] exists or not
287    pub fn exists(&self) -> bool {
288        self.path_set()
289            .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
290    }
291
292    ////////// Parser functions
293
294    /// Adds a [`Parser`] to this `Buffer`
295    ///
296    /// The `Parser` will be able to keep track of every single
297    /// [`Change`] that takes place in the `Buffer`'s [`Text`], and
298    /// can act on the `Buffer` accordingly.
299    ///
300    /// This function will fail if a [`Parser`] of the same type was
301    /// already added to this [`Buffer`]
302    ///
303    /// [`Change`]: crate::text::Change
304    pub fn add_parser<P: Parser>(
305        &mut self,
306        f: impl FnOnce(BufferTracker) -> P,
307    ) -> Result<(), Text> {
308        self.parsers.add(self, f)
309    }
310
311    /// Reads from a specific [`Parser`], if it was [added]
312    ///
313    /// This function will block until the `Parser` is ready to be
314    /// read. This usually implies that the `Parser` is done
315    /// processing all the [`Change`]s up to this point. But it could
316    /// also be the case that the `Parser` doesn't really care about
317    /// `Change`s.
318    ///
319    /// While this function is being called, trying to read or write
320    /// to the same [`Parser`] will always return [`None`].
321    ///
322    /// If you want to read in a non blocking way, see
323    /// [`Buffer::try_read_parser`].
324    ///
325    /// [added]: Handle::add_parser
326    /// [`Change`]: crate::text::Change
327    pub fn read_parser<P: Parser, Ret>(&self, read: impl FnOnce(&P) -> Ret) -> Option<Ret> {
328        self.parsers.read_parser(read)
329    }
330
331    /// Tries tor read from a specific [`Parser`], if it was [added]
332    ///
333    /// Unlike [`Buffer::read_parser`], this function will only be
334    /// called when the `Parser` is ready to be read. This may not
335    /// be the case if, for example, it is still processing
336    /// [`Change`]s to the [`Text`]. In that case, the function will
337    /// not be called and [`try_read_parser`] will return [`None`].
338    ///
339    /// While this function is being called, trying to read or write
340    /// to the same [`Parser`] will always return [`None`].
341    ///
342    /// [added]: Handle::add_parser
343    /// [`Change`]: crate::text::Change
344    /// [`try_read_parser`]: Self::try_read_parser
345    pub fn try_read_parser<P: Parser, Ret>(&self, read: impl FnOnce(&P) -> Ret) -> Option<Ret> {
346        self.parsers.try_read_parser(read)
347    }
348
349    /// Writes to a specific [`Parser`], if it was [added]
350    ///
351    /// This function will block until the `Parser` is ready to be
352    /// written to. This usually implies that the `Parser` is done
353    /// processing all the [`Change`]s up to this point. But it could
354    /// also be the case that the `Parser` doesn't really care
355    /// about `Change`s.
356    ///
357    /// While this function is being called, trying to read or write
358    /// to the same `Parser` will always return `None`.
359    ///
360    /// If you want to write in a non blocking way, see
361    /// [`Buffer::try_write_parser`].
362    ///
363    /// [added]: Handle::add_parser
364    /// [`Change`]: crate::text::Change
365    pub fn write_parser<P: Parser, Ret>(&self, write: impl FnOnce(&mut P) -> Ret) -> Option<Ret> {
366        self.parsers.write_parser(write)
367    }
368
369    /// Tries tor read a specific [`Parser`], if it was [added]
370    ///
371    /// Unlike [`Buffer::write_parser`], this function will only be
372    /// called when the `Parser` is ready to be written to. This may
373    /// not be the case if, for example, it is still processing
374    /// [`Change`]s to the [`Text`]. In that case, the function will
375    /// not be called and [`try_write_parser`] will return [`None`].
376    ///
377    /// While this function is being called, trying to read or write
378    /// to the same `Parser` will always return `None`.
379    ///
380    /// [added]: Handle::add_parser
381    /// [`Change`]: crate::text::Change
382    /// [`try_write_parser`]: Self::try_write_parser
383    pub fn try_write_parser<P: Parser, Ret>(
384        &self,
385        write: impl FnOnce(&mut P) -> Ret,
386    ) -> Option<Ret> {
387        self.parsers.try_write_parser(write)
388    }
389
390    /// Prepare this `Buffer` for reloading
391    ///
392    /// This works by creating a new [`Buffer`], which will take
393    /// ownership of a stripped down version of this one's [`Text`]
394    pub(crate) fn prepare_for_reloading(&mut self) -> Self {
395        self.text.prepare_for_reloading();
396        Self {
397            path: self.path.clone(),
398            text: std::mem::take(&mut self.text),
399            printed_lines: Mutex::new(Vec::new()),
400            parsers: Parsers::default(),
401            sync_opts: Arc::default(),
402            layout_order: self.layout_order,
403            opts: PrintOpts::default(),
404            prev_opts: PrintOpts::default(),
405        }
406    }
407}
408
409impl Widget for Buffer {
410    fn update(pa: &mut Pass, handle: &Handle<Self>) {
411        let parsers = std::mem::take(&mut handle.write(pa).parsers);
412
413        let opts = handle.read(pa).opts;
414
415        let (buffer, area) = handle.write_with_area(pa);
416        if buffer.prev_opts != opts {
417            *buffer.sync_opts.lock() = opts;
418            buffer.prev_opts = opts;
419        }
420
421        if let Some(main) = buffer.text().selections().get_main() {
422            area.scroll_around_points(
423                buffer.text(),
424                main.caret().to_two_points_after(),
425                buffer.get_print_opts(),
426            );
427        }
428
429        let start = area.start_points(&buffer.text, opts);
430        let end = area.end_points(&buffer.text, opts);
431
432        parsers.update(pa, handle, start.real.byte()..end.real.byte());
433
434        let buffer = handle.write(pa);
435        buffer.parsers = parsers;
436
437        buffer.text.update_bounds();
438    }
439
440    fn needs_update(&self, _: &Pass) -> bool {
441        self.parsers.needs_update()
442    }
443
444    fn text(&self) -> &Text {
445        &self.text
446    }
447
448    fn text_mut(&mut self) -> &mut Text {
449        &mut self.text
450    }
451
452    fn get_print_opts(&self) -> PrintOpts {
453        self.opts
454    }
455
456    fn on_mouse_event(pa: &mut Pass, handle: &Handle<Self>, event: MouseEvent) {
457        match event.kind {
458            MouseEventKind::Down(MouseButton::Left) => {
459                let point = match event.points {
460                    Some(TwoPointsPlace::Within(points) | TwoPointsPlace::AheadOf(points)) => {
461                        points.real
462                    }
463                    _ => handle.text(pa).last_point(),
464                };
465
466                handle.selections_mut(pa).remove_extras();
467                handle.edit_main(pa, |mut c| {
468                    c.unset_anchor();
469                    c.move_to(point)
470                })
471            }
472            MouseEventKind::Down(_) => {}
473            MouseEventKind::Up(_) => {}
474            MouseEventKind::Drag(MouseButton::Left) => {
475                let point = match event.points {
476                    Some(TwoPointsPlace::Within(points) | TwoPointsPlace::AheadOf(points)) => {
477                        points.real
478                    }
479                    _ => handle.text(pa).last_point(),
480                };
481
482                handle.selections_mut(pa).remove_extras();
483                handle.edit_main(pa, |mut c| {
484                    c.set_anchor_if_needed();
485                    c.move_to(point);
486                })
487            }
488            MouseEventKind::Drag(_) => {}
489            MouseEventKind::Moved => {}
490            MouseEventKind::ScrollDown => {
491                let opts = handle.opts(pa);
492                let (widget, area) = handle.write_with_area(pa);
493                area.scroll_ver(widget.text(), 3, opts);
494            }
495            MouseEventKind::ScrollUp => {
496                let opts = handle.opts(pa);
497                let (widget, area) = handle.write_with_area(pa);
498                area.scroll_ver(widget.text(), -3, opts);
499            }
500            MouseEventKind::ScrollLeft => {}
501            MouseEventKind::ScrollRight => {}
502        }
503    }
504
505    fn print(&self, pa: &Pass, painter: Painter, area: &RwArea) {
506        let opts = self.opts;
507        let start_points = area.start_points(pa, &self.text, opts);
508
509        let mut last_line = area
510            .rev_print_iter(pa, &self.text, start_points, opts)
511            .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
512
513        let mut printed_lines = self.printed_lines.lock();
514        printed_lines.clear();
515
516        let mut has_wrapped = false;
517
518        area.print_with(pa, &self.text, opts, painter, move |caret, item| {
519            has_wrapped |= caret.wrap;
520            if has_wrapped && item.part.is_char() {
521                has_wrapped = false;
522                let line = item.line();
523                let wrapped = last_line.is_some_and(|ll| ll == line);
524                last_line = Some(line);
525                printed_lines.push((line, wrapped));
526            }
527        })
528    }
529}
530
531impl Handle {
532    /// Adds a [`Parser`] to react to [`Text`] [`Change`]s
533    ///
534    /// [`Change`]: crate::text::Change
535    pub fn add_parser<P: Parser>(
536        &self,
537        pa: &mut Pass,
538        f: impl FnOnce(BufferTracker) -> P,
539    ) -> Result<(), Text> {
540        let buffer = self.widget().read(pa);
541        buffer.parsers.add(buffer, f)
542    }
543}
544
545/// Represents the presence or absence of a path
546#[derive(Debug, Clone)]
547pub enum PathKind {
548    /// A [`PathBuf`] that has been defined and points to a real
549    /// buffer
550    SetExists(PathBuf),
551    /// A [`PathBuf`] that has been defined but isn't a real buffer
552    SetAbsent(PathBuf),
553    /// A [`PathBuf`] that has not been defined
554    ///
555    /// The number within represents a specific [`Buffer`], and when
556    /// printed to, for example, the [`StatusLine`], would show up as
557    /// `txt!("[buffer]*scratch buffer*#{id}")`
558    ///
559    /// [`StatusLine`]: https://docs.rs/duat/latest/duat/widgets/struct.StatusLine.html
560    NotSet(usize),
561}
562
563impl PathKind {
564    /// Returns a new unset [`PathBuf`]
565    pub(crate) fn new_unset() -> PathKind {
566        use std::sync::atomic::{AtomicUsize, Ordering};
567        static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
568
569        PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
570    }
571
572    /// Returns a [`PathBuf`] if `self` is [`SetExists`] or
573    /// [`SetAbsent`]
574    ///
575    /// [`SetExists`]: PathKind::SetExists
576    /// [`SetAbsent`]: PathKind::SetAbsent
577    pub fn as_path(&self) -> Option<PathBuf> {
578        match self {
579            PathKind::SetExists(path) | PathKind::SetAbsent(path) => Some(path.clone()),
580            PathKind::NotSet(_) => None,
581        }
582    }
583
584    /// The full path of the buffer.
585    ///
586    /// If there is no set path, returns `"*scratch buffer*#{id}"`.
587    pub fn path(&self) -> String {
588        match self {
589            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
590                path.to_string_lossy().to_string()
591            }
592            PathKind::NotSet(id) => {
593                format!("*scratch buffer*#{id}")
594            }
595        }
596    }
597
598    /// The full path of the buffer.
599    ///
600    /// Returns [`None`] if the path has not been set yet.
601    pub fn path_set(&self) -> Option<String> {
602        match self {
603            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
604                Some(path.to_string_lossy().to_string())
605            }
606            PathKind::NotSet(_) => None,
607        }
608    }
609
610    /// The buffer's name.
611    ///
612    /// If there is no set path, returns `"*scratch buffer #{id}*"`.
613    pub fn name(&self) -> String {
614        match self {
615            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
616                let cur_dir = context::current_dir();
617                if let Ok(path) = path.strip_prefix(cur_dir) {
618                    path.to_string_lossy().to_string()
619                } else if let Some(home_dir) = dirs_next::home_dir()
620                    && let Ok(path) = path.strip_prefix(home_dir)
621                {
622                    Path::new("~").join(path).to_string_lossy().to_string()
623                } else {
624                    path.to_string_lossy().to_string()
625                }
626            }
627            PathKind::NotSet(id) => format!("*scratch buffer #{id}*"),
628        }
629    }
630
631    /// The buffer's name.
632    ///
633    /// Returns [`None`] if the path has not been set yet.
634    pub fn name_set(&self) -> Option<String> {
635        match self {
636            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
637                let cur_dir = context::current_dir();
638                Some(if let Ok(path) = path.strip_prefix(cur_dir) {
639                    path.to_string_lossy().to_string()
640                } else if let Some(home_dir) = dirs_next::home_dir()
641                    && let Ok(path) = path.strip_prefix(home_dir)
642                {
643                    Path::new("~").join(path).to_string_lossy().to_string()
644                } else {
645                    path.to_string_lossy().to_string()
646                })
647            }
648            PathKind::NotSet(_) => None,
649        }
650    }
651
652    /// A [`Text`] from the full path of this [`PathKind`]
653    ///
654    /// # Formatting
655    ///
656    /// If the buffer's `path` was set:
657    ///
658    /// ```text
659    /// [buffer]{path}
660    /// ```
661    ///
662    /// If the buffer's `path` was not set:
663    ///
664    /// ```text
665    /// [buffer.new.scratch]*scratch buffer #{id}*
666    /// ```
667    pub fn path_txt(&self) -> Text {
668        match self {
669            PathKind::SetExists(path) | PathKind::SetAbsent(path) => txt!("[buffer]{path}"),
670            PathKind::NotSet(id) => txt!("[buffer.new.scratch]*scratch buffer #{id}*"),
671        }
672    }
673
674    /// A [`Text`] from the name of this `PathKind`
675    ///
676    /// The name of a [`Buffer`] widget is the same as the path, but
677    /// it strips away the current directory. If it can't, it will
678    /// try to strip away the home directory, replacing it with
679    /// `"~"`. If that also fails, it will just show the full
680    /// path.
681    ///
682    /// # Formatting
683    ///
684    /// If the buffer's `name` was set:
685    ///
686    /// ```text
687    /// [buffer]{name}
688    /// ```
689    ///
690    /// If the buffer's `name` was not set:
691    ///
692    /// ```text
693    /// [buffer.new.scratch]*scratch buffer #{id}*
694    /// ```
695    pub fn name_txt(&self) -> Text {
696        match self {
697            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
698                let cur_dir = context::current_dir();
699                if let Ok(path) = path.strip_prefix(cur_dir) {
700                    txt!("[buffer]{path}")
701                } else if let Some(home_dir) = dirs_next::home_dir()
702                    && let Ok(path) = path.strip_prefix(home_dir)
703                {
704                    txt!("[buffer]{}", Path::new("~").join(path))
705                } else {
706                    txt!("[buffer]{path}")
707                }
708            }
709            PathKind::NotSet(id) => txt!("[buffer.new.scratch]*scratch buffer #{id}*"),
710        }
711    }
712}
713
714impl<P: AsRef<Path>> From<P> for PathKind {
715    fn from(value: P) -> Self {
716        let path = value.as_ref();
717        if let Ok(true) = path.try_exists() {
718            PathKind::SetExists(path.into())
719        } else {
720            PathKind::SetAbsent(path.into())
721        }
722    }
723}
724
725impl PartialEq for PathKind {
726    fn eq(&self, other: &Self) -> bool {
727        match (self, other) {
728            (
729                Self::SetExists(l0) | Self::SetAbsent(l0),
730                Self::SetExists(r0) | Self::SetAbsent(r0),
731            ) => l0 == r0,
732            (Self::NotSet(l0), Self::NotSet(r0)) => l0 == r0,
733            _ => false,
734        }
735    }
736}
737
738impl Eq for PathKind {}
739
740impl From<PathKind> for BuilderPart {
741    fn from(value: PathKind) -> Self {
742        BuilderPart::Text(value.name_txt())
743    }
744}