duat_core/
lib.rs

1//! The core of Duat, this crate is meant to be used only for the
2//! creation of plugins for Duat.
3//!
4//! # Quick Start
5//!
6//! The capabilities of `duat-core` are largely the same as the those
7//! of Duat, however, the main difference is the multi [`Ui`] APIs of
8//! this crate. In it, the public functions and types are defined in
9//! terms of `U: Ui`,  which means that they can work on various
10//! different interfaces:
11//!
12//! ```rust
13//! # use duat_core::{
14//! #     mode::{self, Cursors, EditHelper, KeyCode, KeyEvent, Mode, key}, ui::Ui, widgets::File,
15//! # };
16//! #[derive(Default, Clone)]
17//! struct FindSeq(Option<char>);
18//!
19//! impl<U: Ui> Mode<U> for FindSeq {
20//!     type Widget = File;
21//!
22//!     fn send_key(&mut self, key: KeyEvent, file: &mut File, area: &U::Area) {
23//!         use KeyCode::*;
24//!         let mut helper = EditHelper::new(file, area);
25//!
26//!         // Make sure that the typed key is a character.
27//!         let key!(Char(c)) = key else {
28//!             mode::reset();
29//!             return;
30//!         };
31//!         // Checking if a character was already sent.
32//!         let Some(first) = self.0 else {
33//!             self.0 = Some(c);
34//!             return;
35//!         };
36//!
37//!         helper.move_many(.., |mut m| {
38//!             let pat: String = [first, c].iter().collect();
39//!             let matched = m.search_fwd(pat, None).next();
40//!             if let Some([p0, p1]) = matched {
41//!                 m.move_to(p0);
42//!                 m.set_anchor();
43//!                 m.move_to(p1);
44//!                 m.move_hor(-1)
45//!             }
46//!         });
47//!
48//!         mode::reset();
49//!     }
50//! }
51//! ```
52//!
53//! In this example, I have created a [`Mode`] for [`File`]s. This
54//! mode is (I think) popular within Vim circles. It's like the `f`
55//! key in Vim, but it lets you look for a sequence of 2 characters,
56//! instead of just one.
57//!
58//! What's great about it is that it will work no matter what editing
59//! model the user is using. It could be Vim inspired, Kakoune
60//! inspired, Emacs inspired, doesn't matter. All the user has to do
61//! to use this mode is this:
62//!
63//! ```rust
64//! # struct Normal;
65//! # #[derive(Default, Clone)]
66//! # struct FindSeq;
67//! # fn map<M>(take: &str, give: &impl std::any::Any) {}
68//! # // I fake it here because this function is from duat, not duat-core
69//! map::<Normal>("<C-s>", &FindSeq::default());
70//! ```
71//!
72//! And now, whenever the usert types `Control S` in `Normal` mode,
73//! the mode will switch to `FindSeq`. You could replace `Normal` with
74//! any other mode, from any other editing model, and this would still
75//! work.
76//!
77//! Of course, this is most useful for plugins, for your own
78//! configuration, you should probably just rely on [`map`] to
79//! accomplish the same thing.
80//!
81//! Okay, but that was a relatively simple example, here's a more
82//! advanced example, which makes use of more of Duat's features.
83//!
84//! This is a copy of [EasyMotion], a plugin for
85//! Vim/Neovim/Kakoune/Emacs that lets you skip around the screen with
86//! at most 2 keypresses.
87//!
88//! In order to emulate it, we use [ghost text] and [concealment]:
89//!
90//! ```rust
91//! # use duat_core::{
92//! #     mode::{self, Cursors, EditHelper, KeyCode, KeyEvent, Mode, key},
93//! #     text::{Key, Point, Tag, text}, ui::{Area, Ui}, widgets::File,
94//! # };
95//! #[derive(Clone)]
96//! pub struct EasyMotion {
97//!     is_line: bool,
98//!     key: Key,
99//!     points: Vec<[Point; 2]>,
100//!     seq: String,
101//! }
102//!
103//! (NOTE: something)
104//!
105//! impl EasyMotion {
106//!     pub fn word() -> Self {
107//!         Self {
108//!             is_line: false,
109//!             key: Key::new(),
110//!             points: Vec::new(),
111//!             seq: String::new(),
112//!         }
113//!     }
114//!
115//!     pub fn line() -> Self {
116//!         Self {
117//!             is_line: true,
118//!             key: Key::new(),
119//!             points: Vec::new(),
120//!             seq: String::new(),
121//!         }
122//!     }
123//! }
124//!
125//! impl<U: Ui> Mode<U> for EasyMotion {
126//!     type Widget = File;
127//!
128//!     fn on_switch(&mut self, file: &mut File, area: &<U as Ui>::Area) {
129//!         let cfg = file.print_cfg();
130//!         let text = file.text_mut();
131//!
132//!         let regex = match self.is_line {
133//!             true => "[^\n\\s][^\n]+",
134//!             false => "[^\n\\s]+",
135//!         };
136//!         let (start, _) = area.first_points(text, cfg);
137//!         let (end, _) = area.last_points(text, cfg);
138//!         self.points = text.search_fwd(regex, (start, end)).unwrap().collect();
139//!
140//!         let seqs = key_seqs(self.points.len());
141//!
142//!         for (seq, [p0, _]) in seqs.iter().zip(&self.points) {
143//!             let ghost = text!([EasyMotionWord] seq);
144//!
145//!             text.insert_tag(p0.byte(), Tag::GhostText(ghost), self.key);
146//!             text.insert_tag(p0.byte(), Tag::StartConceal, self.key);
147//!             let seq_end = p0.byte() + seq.chars().count() ;
148//!             text.insert_tag(seq_end, Tag::EndConceal, self.key);
149//!         }
150//!     }
151//!
152//!     fn send_key(&mut self, key: KeyEvent, file: &mut File, area: &U::Area) {
153//!         let char = match key {
154//!             key!(KeyCode::Char(c)) => c,
155//!             // Return a char that will never match.
156//!             _ => '❌'
157//!         };
158//!         self.seq.push(char);
159//!
160//!         let mut helper = EditHelper::new(file, area);
161//!         helper.cursors_mut().remove_extras();
162//!
163//!         let seqs = key_seqs(self.points.len());
164//!         for (seq, &[p0, p1]) in seqs.iter().zip(&self.points) {
165//!             if *seq == self.seq {
166//!                 helper.move_main(|mut m| {
167//!                     m.move_to(p0);
168//!                     m.set_anchor();
169//!                     m.move_to(p1);
170//!                 });
171//!                 mode::reset();
172//!             } else if seq.starts_with(&self.seq) {
173//!                 continue;
174//!             }
175//!
176//!             helper.text_mut().remove_tags(p1.byte(), self.key);
177//!             helper.text_mut().remove_tags(p1.byte() + seq.len(), self.key);
178//!         }
179//!
180//!         if self.seq.chars().count() == 2 || !LETTERS.contains(char) {
181//!             mode::reset();
182//!         }
183//!     }
184//! }
185//!
186//! fn key_seqs(len: usize) -> Vec<String> {
187//!     let double = len / LETTERS.len();
188//!
189//!     let mut seqs = Vec::new();
190//!     seqs.extend(LETTERS.chars().skip(double).map(char::into));
191//!
192//!     let chars = LETTERS.chars().take(double);
193//!     seqs.extend(chars.flat_map(|c1| LETTERS.chars().map(move |c2| format!("{c1}{c2}"))));
194//!
195//!     seqs
196//! }
197//!
198//! static LETTERS: &str = "abcdefghijklmnopqrstuvwxyz";
199//! ```
200//! All that this plugin is doing is:
201//!
202//! - Search on the screen for words/lines;
203//! - In the beginning of said words/lines, add a [`Tag::GhostText`];
204//! - Also add a [`Tag::StartConceal`] and a [`Tag::EndConceal`];
205//! - Then, just match the typed keys and [remove] tags accordingly;
206//! - [Move] to the matched sequence, if it exists;
207//!
208//! Now, in order to use this mode, it's the exact same thing as
209//! `FindSeq`:
210//!
211//! ```rust
212//! # struct Normal;
213//! #[derive(Clone)]
214//! # pub struct EasyMotion;
215//! # impl EasyMotion {
216//! #     pub fn word() -> Self {
217//! #         Self
218//! #     }
219//! #     pub fn line() -> Self {
220//! #         Self
221//! #     }
222//! # }
223//! # fn map<M>(take: &str, give: &impl std::any::Any) {}
224//! # // I fake it here because this function is from duat, not duat-core
225//! map::<Normal>("<CA-w>", &EasyMotion::word());
226//! map::<Normal>("<CA-l>", &EasyMotion::line());
227//! ```
228//!
229//! [`Mode`]: crate::mode::Mode
230//! [`File`]: crate::widgets::File
231//! [`map`]: https://docs.rs/duat/0.2.0/duat/prelude/fn.map.html
232//! [EasyMotion]: https://github.com/easymotion/vim-easymotion
233//! [ghost text]: crate::text::Tag::GhostText
234//! [concealment]: crate::text::Tag::StartConceal
235//! [`Tag::GhostText`]: crate::text::Tag::GhostText
236//! [`Tag::StartConceal`]: crate::text::Tag::StartConceal
237//! [`Tag::EndConceal`]: crate::text::Tag::EndConceal
238//! [remove]: crate::text::Text::remove_tags
239//! [Move]: crate::mode::Mover::move_to
240#![feature(
241    let_chains,
242    decl_macro,
243    step_trait,
244    type_alias_impl_trait,
245    trait_alias,
246    debug_closure_helpers,
247    box_as_ptr,
248    unboxed_closures,
249    fn_traits,
250    associated_type_defaults,
251    dropck_eyepatch,
252)]
253#![allow(clippy::single_range_in_vec_init)]
254
255use std::{
256    any::{TypeId, type_name},
257    collections::HashMap,
258    ops::Range,
259    path::Path,
260    sync::{
261        Arc, LazyLock, Once,
262        atomic::{AtomicBool, Ordering},
263    },
264    time::Duration,
265};
266
267#[allow(unused_imports)]
268use dirs_next::cache_dir;
269pub use lender::{Lender, Lending};
270pub use parking_lot::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
271use ui::Window;
272use widgets::{File, Node, Widget};
273
274use self::{
275    text::{Text, err},
276    ui::Ui,
277};
278
279pub mod cache;
280pub mod cfg;
281pub mod cmd;
282pub mod context;
283pub mod data;
284pub mod form;
285pub mod hooks;
286pub mod mode;
287pub mod session;
288pub mod status;
289pub mod text;
290pub mod ui;
291pub mod widgets;
292
293pub mod prelude {
294    //! The prelude of Duat
295    pub use crate::{
296        cmd,
297        data::{self, RwData},
298        form,
299        text::{Builder, Text, err, hint, ok, text},
300        ui, widgets,
301    };
302}
303
304/// A plugin for Duat
305///
306/// Plugins must follow the builder pattern, and can be specific to
307/// certain [`Ui`]s. Generally, plugins should do all the setup
308/// necessary for their function when [`Plugin::plug`] is called.
309///
310/// [`Plugin`] will usually be [plugged] by a `macro` in the Duat
311/// config crate. This macro requires that the [`Plugin`] be
312/// compatible with the [`Ui`]. And this can cause some inconvenience
313/// for the end user. For example, say we have a plugin like this:
314///
315/// ```rust
316/// # use duat_core::{Plugin, ui::Ui};
317/// struct MyPlugin;
318///
319/// impl<U: Ui> Plugin<U> for MyPlugin {
320///     fn new() -> Self {
321///         MyPlugin
322///     }
323///
324///     fn plug(self) {
325///         //..
326///     }
327/// }
328///
329/// impl MyPlugin {
330///     pub fn modify(self) -> Self {
331///         //..
332/// #       self
333///     }
334/// }
335/// ```
336///
337/// In the config crate, the user would have to add the plugin in a
338/// really awkward way:
339///
340/// ```rust
341/// # use duat_core::Plugin;
342/// # macro_rules! plug {
343/// #     ($($plug:expr),+) => {};
344/// # }
345/// # struct MyPlugin;
346/// # impl<U: duat_core::ui::Ui> duat_core::Plugin<U> for MyPlugin {
347/// #     fn new() -> Self {
348/// #         MyPlugin
349/// #     }
350/// #     fn plug(self) {}
351/// # }
352/// # fn test<Ui: duat_core::ui::Ui>() {
353/// plug!(<MyPlugin as Plugin<Ui>>::new().modify());
354/// # }
355/// ```
356///
357/// To prevent that, just add a [`Ui`] [`PhantomData`] parameter:
358///
359/// ```rust
360/// # use std::marker::PhantomData;
361/// # use duat_core::{Plugin, ui::Ui};
362/// struct MyPlugin<U>(PhantomData<U>);
363///
364/// impl<U: Ui> Plugin<U> for MyPlugin<U> {
365///     fn new() -> Self {
366///         MyPlugin(PhantomData)
367///     }
368///
369///     fn plug(self) {
370///         //..
371///     }
372/// }
373///
374/// impl<U> MyPlugin<U> {
375///     pub fn modify(self) -> Self {
376///         //..
377/// #       self
378///     }
379/// }
380/// ```
381/// And now the plugin can be plugged much more normally:
382///
383///
384/// ```rust
385/// # use std::marker::PhantomData;
386/// # use duat_core::Plugin;
387/// # macro_rules! plug {
388/// #     ($($plug:expr),+) => {};
389/// # }
390/// # struct MyPlugin<U>(PhantomData<U>);
391/// # impl<U: duat_core::ui::Ui> duat_core::Plugin<U> for MyPlugin<U> {
392/// #     fn new() -> Self {
393/// #         MyPlugin(PhantomData)
394/// #     }
395/// #     fn plug(self) {}
396/// # }
397/// # impl<U> MyPlugin<U> {
398/// #     pub fn modify(self) -> Self {
399/// #         self
400/// #     }
401/// # }
402/// # fn test<Ui: duat_core::ui::Ui>() {
403/// plug!(MyPlugin::new().modify());
404/// # }
405/// ```
406/// [plugged]: Plugin::plug
407/// [`PhantomData`]: std::marker::PhantomData
408pub trait Plugin<U: Ui>: Sized {
409    /// Returns a builder pattern instance of this [`Plugin`]
410    fn new() -> Self;
411
412    /// Sets up the [`Plugin`]
413    fn plug(self);
414}
415
416pub mod thread {
417    //! Multithreading for Duat
418    //!
419    //! The main rationale behind multithreading in Duat is not so
420    //! much the performance gains, but more to allow for multi
421    //! tasking, as some plugins (like an LSP) may block for a while,
422    //! which would be frustrating for end users.
423    //!
424    //! The functions in this module differ from [`std::thread`] in
425    //! that they synchronize with Duat, telling the application when
426    //! there are no more threads running, so Duat can safely quit or
427    //! reload.
428    use std::{
429        sync::atomic::{AtomicUsize, Ordering},
430        thread::JoinHandle,
431    };
432
433    /// Duat's [`JoinHandle`]s
434    pub static HANDLES: AtomicUsize = AtomicUsize::new(0);
435    /// Spawns a new thread, returning a [`JoinHandle`] for it.
436    ///
437    /// Use this function instead of [`std::thread::spawn`].
438    ///
439    /// The threads from this function work in the same way that
440    /// threads from [`std::thread::spawn`] work, but it has
441    /// synchronicity with Duat, and makes sure that the
442    /// application won't exit or reload the configuration before
443    /// all spawned threads have stopped.
444    pub fn spawn<R: Send + 'static>(f: impl FnOnce() -> R + Send + 'static) -> JoinHandle<R> {
445        HANDLES.fetch_add(1, Ordering::Relaxed);
446        std::thread::spawn(|| {
447            let ret = f();
448            HANDLES.fetch_sub(1, Ordering::Relaxed);
449            ret
450        })
451    }
452
453    /// Returns true if there are any threads still running
454    pub(crate) fn still_running() -> bool {
455        HANDLES.load(Ordering::Relaxed) > 0
456    }
457}
458
459pub mod clipboard {
460    //! Clipboard interaction for Duat
461    //!
462    //! Just a regular clipboard, no image functionality.
463    use std::sync::OnceLock;
464
465    pub use arboard::Clipboard;
466    use parking_lot::Mutex;
467
468    static CLIPB: OnceLock<&'static Mutex<Clipboard>> = OnceLock::new();
469
470    /// Gets a [`String`] from the clipboard
471    ///
472    /// This can fail if the clipboard does not contain UTF-8 encoded
473    /// text.
474    ///
475    /// Or if there is no clipboard i guess
476    pub fn get_text() -> Option<String> {
477        CLIPB.get().unwrap().lock().get_text().ok()
478    }
479
480    /// Sets a [`String`] to the clipboard
481    pub fn set_text(text: impl std::fmt::Display) {
482        let clipb = CLIPB.get().unwrap();
483        clipb.lock().set_text(text.to_string()).unwrap();
484    }
485
486    pub(crate) fn set_clipboard(clipb: &'static Mutex<Clipboard>) {
487        CLIPB.set(clipb).map_err(|_| {}).expect("Setup ran twice");
488    }
489}
490
491////////// General utility functions
492
493/// A checker that returns `true` every `duration`
494///
495/// This is primarily used within [`WidgetCfg::build`], where a
496/// `checker` must be returned in order to update the widget.
497///
498/// [`WidgetCfg::build`]: crate::widgets::WidgetCfg::build
499pub fn periodic_checker(duration: Duration) -> impl Fn() -> bool {
500    let check = Arc::new(AtomicBool::new(false));
501    crate::thread::spawn({
502        let check = check.clone();
503        move || {
504            while !crate::context::will_reload_or_quit() {
505                std::thread::sleep(duration);
506                check.store(true, Ordering::Release);
507            }
508        }
509    });
510
511    move || check.fetch_and(false, Ordering::Acquire)
512}
513
514/// Takes a type and generates an appropriate name for it
515///
516/// Use this function if you need a name of a type to be
517/// referrable by string, such as by commands or by the
518/// user.
519///
520/// # NOTE
521///
522/// Any `<Ui>` or `Ui, ` type arguments will be removed from the final
523/// result, since Duat is supposed to have only one [`Ui`] in use.
524pub fn duat_name<T: ?Sized + 'static>() -> &'static str {
525    fn duat_name_inner(type_id: TypeId, type_name: &str) -> &'static str {
526        static NAMES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
527            LazyLock::new(RwLock::default);
528        let mut names = NAMES.write();
529
530        if let Some(name) = names.get(&type_id) {
531            name
532        } else {
533            let mut name = String::new();
534
535            for path in type_name.split_inclusive(['<', '>', ',', ' ']) {
536                for segment in path.split("::") {
537                    let is_type = segment.chars().any(|c| c.is_uppercase());
538                    let is_punct = segment.chars().all(|c| !c.is_alphanumeric());
539                    let is_dyn = segment.starts_with("dyn");
540                    if is_type || is_punct || is_dyn {
541                        name.push_str(segment);
542                    }
543                }
544            }
545
546            while let Some((i, len)) = None
547                .or_else(|| name.find("<Ui>").map(|i| (i, "<Ui>".len())))
548                .or_else(|| name.find("Ui, ").map(|i| (i, "Ui, ".len())))
549                .or_else(|| name.find("::<Ui>").map(|i| (i, "::<Ui>".len())))
550            {
551                unsafe {
552                    name.as_mut_vec().splice(i..(i + len), []);
553                }
554            }
555
556            names.insert(type_id, name.leak());
557            names.get(&type_id).unwrap()
558        }
559    }
560
561    duat_name_inner(TypeId::of::<T>(), std::any::type_name::<T>())
562}
563
564/// Returns the source crate of a given type
565///
566/// This is primarily used on the [`cache`] module.
567pub fn src_crate<T: ?Sized + 'static>() -> &'static str {
568    fn src_crate_inner(type_id: TypeId, type_name: &'static str) -> &'static str {
569        static CRATES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
570            LazyLock::new(|| RwLock::new(HashMap::new()));
571        let mut crates = CRATES.write();
572
573        if let Some(src_crate) = crates.get(&type_id) {
574            src_crate
575        } else {
576            let src_crate = type_name.split([' ', ':']).find(|w| *w != "dyn").unwrap();
577
578            crates.insert(type_id, src_crate);
579            crates.get(&type_id).unwrap()
580        }
581    }
582
583    src_crate_inner(TypeId::of::<T>(), std::any::type_name::<T>())
584}
585
586/// The path for the config crate of Duat
587pub fn crate_dir() -> Option<&'static Path> {
588    static CONFIG_PATH: LazyLock<Option<&'static Path>> = LazyLock::new(|| {
589        dirs_next::config_dir().map(|config_dir| {
590            let path: &'static str = config_dir.join("duat").to_string_lossy().to_string().leak();
591            Path::new(path)
592        })
593    });
594    *CONFIG_PATH
595}
596
597/// Convenience function for the bounds of a range
598#[track_caller]
599fn get_ends(range: impl std::ops::RangeBounds<usize>, max: usize) -> (usize, usize) {
600    let start = match range.start_bound() {
601        std::ops::Bound::Included(start) => *start,
602        std::ops::Bound::Excluded(start) => *start + 1,
603        std::ops::Bound::Unbounded => 0,
604    };
605    let end = match range.end_bound() {
606        std::ops::Bound::Included(end) => *end + 1,
607        std::ops::Bound::Excluded(end) => *end,
608        std::ops::Bound::Unbounded => max,
609    };
610    assert!(
611        start <= max,
612        "index out of bounds: the len is {max}, but the index is {start}, coming from {}",
613        std::panic::Location::caller()
614    );
615    assert!(
616        end <= max,
617        "index out of bounds: the len is {max}, but the index is {end}, coming from {}",
618        std::panic::Location::caller()
619    );
620
621    (start, end)
622}
623
624/// Adds two shifts together
625fn add_shifts(lhs: [i32; 3], rhs: [i32; 3]) -> [i32; 3] {
626    let b = lhs[0] + rhs[0];
627    let c = lhs[1] + rhs[1];
628    let l = lhs[2] + rhs[2];
629    [b, c, l]
630}
631
632/// Allows binary searching with an initial guess and displaced
633/// entries
634///
635/// This function essentially looks at a list of entries and with a
636/// starting shift position, shifts them by an amount, before
637/// comparing inside of the binary search.
638///
639/// By using this function, it is very possible to
640/// It is currently used in 2 places, in the `History` of [`Text`]s,
641/// and in the `Cursors` list.
642fn merging_range_by_guess_and_lazy_shift<T, U: Copy + Ord, V: Copy>(
643    (container, len): (&impl std::ops::Index<usize, Output = T>, usize),
644    (guess_i, [start, end]): (usize, [U; 2]),
645    (sh_from, shift, zero_shift, shift_fn): (usize, V, V, fn(U, V) -> U),
646    (start_fn, end_fn): (fn(&T) -> U, fn(&T) -> U),
647) -> Range<usize> {
648    fn binary_search_by_key_and_index<T, K>(
649        container: &(impl std::ops::Index<usize, Output = T> + ?Sized),
650        len: usize,
651        key: K,
652        f: impl Fn(usize, &T) -> K,
653    ) -> std::result::Result<usize, usize>
654    where
655        K: PartialEq + Eq + PartialOrd + Ord,
656    {
657        let mut size = len;
658        let mut left = 0;
659        let mut right = size;
660
661        while left < right {
662            let mid = left + size / 2;
663
664            let k = f(mid, &container[mid]);
665
666            match k.cmp(&key) {
667                std::cmp::Ordering::Less => left = mid + 1,
668                std::cmp::Ordering::Equal => return Ok(mid),
669                std::cmp::Ordering::Greater => right = mid,
670            }
671
672            size = right - left;
673        }
674
675        Err(left)
676    }
677
678    let sh = |n: usize| if sh_from <= n { shift } else { zero_shift };
679    let start_of = |i: usize| shift_fn(start_fn(&container[i]), sh(i));
680    let end_of = |i: usize| shift_fn(end_fn(&container[i]), sh(i));
681    let search = |n: usize, t: &T| shift_fn(start_fn(t), sh(n));
682
683    let mut c_range = if let Some(prev_i) = guess_i.checked_sub(1)
684        && (prev_i < len && start_of(prev_i) <= start && start <= end_of(prev_i))
685    {
686        prev_i..guess_i
687    } else {
688        match binary_search_by_key_and_index(container, len, start, search) {
689            Ok(i) => i..i + 1,
690            Err(i) => {
691                if let Some(prev_i) = i.checked_sub(1)
692                    && start <= end_of(prev_i)
693                {
694                    prev_i..i
695                } else {
696                    i..i
697                }
698            }
699        }
700    };
701
702    // On Cursors, the Cursors can intersect, so we need to check
703    while c_range.start > 0 && start <= end_of(c_range.start - 1) {
704        c_range.start -= 1;
705    }
706
707    // This block determines how far ahead this cursor will merge
708    if c_range.end < len && end >= start_of(c_range.end) {
709        c_range.end = match binary_search_by_key_and_index(container, len, end, search) {
710            Ok(i) => i + 1,
711            Err(i) => i,
712        }
713    };
714
715    while c_range.end + 1 < len && end >= start_of(c_range.end + 1) {
716        c_range.end += 1;
717    }
718
719    c_range
720}
721
722/// An entry for a file with the given name
723#[allow(clippy::result_large_err)]
724fn file_entry<'a, U: Ui>(
725    windows: &'a [Window<U>],
726    name: &str,
727) -> std::result::Result<(usize, usize, &'a Node<U>), Text> {
728    windows
729        .iter()
730        .enumerate()
731        .flat_map(window_index_widget)
732        .find(|(.., node)| node.read_as::<File>().is_some_and(|f| f.name() == name))
733        .ok_or_else(|| err!("File with name " [*a] name [] " not found."))
734}
735
736/// An entry for a widget of a specific type
737#[allow(clippy::result_large_err)]
738fn widget_entry<W: Widget<U>, U: Ui>(
739    windows: &[Window<U>],
740    w: usize,
741) -> std::result::Result<(usize, usize, &Node<U>), Text> {
742    let mut ff = context::fixed_file::<U>().unwrap();
743
744    if let Some((widget, _)) = ff.get_related_widget::<W>() {
745        windows
746            .iter()
747            .enumerate()
748            .flat_map(window_index_widget)
749            .find(|(.., n)| n.ptr_eq(&widget))
750    } else {
751        iter_around(windows, w, 0).find(|(.., node)| node.data_is::<W>())
752    }
753    .ok_or(err!("No widget of type " [*a] { type_name::<W>() } [] " found."))
754}
755
756/// Iterator over a group of windows, that returns the window's index
757fn window_index_widget<U: Ui>(
758    (index, window): (usize, &Window<U>),
759) -> impl ExactSizeIterator<Item = (usize, usize, &Node<U>)> + DoubleEndedIterator {
760    window
761        .nodes()
762        .enumerate()
763        .map(move |(i, entry)| (index, i, entry))
764}
765
766/// Iterates around a specific widget, going forwards
767fn iter_around<U: Ui>(
768    windows: &[Window<U>],
769    window: usize,
770    widget: usize,
771) -> impl Iterator<Item = (usize, usize, &Node<U>)> + '_ {
772    let prev_len: usize = windows.iter().take(window).map(Window::len_widgets).sum();
773
774    windows
775        .iter()
776        .enumerate()
777        .skip(window)
778        .flat_map(window_index_widget)
779        .skip(widget + 1)
780        .chain(
781            windows
782                .iter()
783                .enumerate()
784                .take(window + 1)
785                .flat_map(window_index_widget)
786                .take(prev_len + widget),
787        )
788}
789
790/// Iterates around a specific widget, going backwards
791fn iter_around_rev<U: Ui>(
792    windows: &[Window<U>],
793    window: usize,
794    widget: usize,
795) -> impl Iterator<Item = (usize, usize, &Node<U>)> {
796    let next_len: usize = windows.iter().skip(window).map(Window::len_widgets).sum();
797
798    windows
799        .iter()
800        .enumerate()
801        .rev()
802        .skip(windows.len() - window)
803        .flat_map(move |(i, win)| {
804            window_index_widget((i, win))
805                .rev()
806                .skip(win.len_widgets() - widget)
807        })
808        .chain(
809            windows
810                .iter()
811                .enumerate()
812                .rev()
813                .take(windows.len() - window)
814                .flat_map(move |(i, win)| window_index_widget((i, win)).rev())
815                .take(next_len - (widget + 1)),
816        )
817}
818
819// Debugging objects.
820#[doc(hidden)]
821pub static DEBUG_TIME_START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
822#[doc(hidden)]
823pub static HOOK: Once = Once::new();
824#[doc(hidden)]
825pub static LOG: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new(String::new()));
826
827/// Log information to a log file
828#[doc(hidden)]
829pub macro log($($text:tt)*) {{
830    if let Some(cache) = cache_dir()
831        && let Ok(file) = std::fs::OpenOptions::new()
832            .create(true)
833            .append(true)
834            .open(cache.join("duat/log"))
835    {
836        use std::{io::Write, time::Instant};
837
838        let mut file = std::io::BufWriter::new(file);
839        let mut text = format!($($text)*);
840
841        if let Some(start) = $crate::DEBUG_TIME_START.get()
842            && text != "" {
843            if text.lines().count() > 1 {
844                let chars = text
845                    .char_indices()
846                    .filter_map(|(pos, char)| (char == '\n').then_some(pos));
847                let nl_indices: Vec<usize> = chars.collect();
848                for index in nl_indices.iter().rev() {
849                    text.insert_str(index + 1, "  ");
850                }
851
852                let duration = Instant::now().duration_since(*start);
853                write!(file, "\nat {:.4?}:\n  {text}", duration).unwrap();
854            } else {
855                let duration = Instant::now().duration_since(*start);
856                write!(file, "\nat {:.4?}: {text}", duration).unwrap();
857            }
858        } else {
859            write!(file, "\n{text}").unwrap();
860        }
861    }
862}}