Skip to main content

duat_core/
utils.rs

1//! General utility functions for Duat.
2//!
3//! This module contains a bunch of functions which are mostly useful
4//! within Duat, but I have found use for them in some of my plugins,
5//! so I'm making them public for other to use as well.
6//!
7//! An example of these functions are those that deal with ranges of
8//! "shifted parts".
9//!
10//! Internally (and externally), these functions work with structs
11//! that have a "shift" component. The shifting component indicates at
12//! which point in the list there is a shift. Elements before that
13//! point are considered to be correctly shifted, while elements
14//! after that point are considered to be incorrectly shifted by the
15//! exact size of the shift.
16//!
17//! This essentially lets me have a sorted list (for binary search and
18//! fast insertion via [`GapBuffer`]s) while still letting me shift
19//! elements (usually by a [`Change`] in the [`Text`]) without
20//! updating every element after the `Change`. I just have to shift
21//! elements between where the `Change` was inserted and where the
22//! shift starts.
23//!
24//! This mirrors how text editing works in the real world. A user
25//! doesn't just type randomly on the text buffer, the changes are
26//! usually localized. Given that, this model of keeping a shifting
27//! point results in a very low number of updates that need to be
28//! made.
29//!
30//! One other (more common) way to prevent a bazillion changes to the
31//! text is to divide said text in lines. So a change in some place
32//! will only affect other elements on the same line. However, the
33//! biggest problem with this approach is that you are purposefully
34//! dividing your text in lines, which makes a lot of other things
35//! more complicated than simply having a buffer of bytes. For
36//! example, a change spanning multiple lines is much more complex to
37//! implement in this kind of system. And this complexity trickles
38//! down through your whole text editor, even down to printing text,
39//! which I also find really simple with my system.
40//!
41//! [`GapBuffer`]: gap_buf::GapBuffer
42//! [`Change`]: crate::buffer::Change
43use std::{
44    any::TypeId,
45    collections::HashMap,
46    ops::Range,
47    path::{Path, PathBuf},
48    sync::{LazyLock, Mutex, OnceLock, RwLock},
49};
50
51pub use shellexpand::full as expand_path;
52
53use crate::text::{Text, txt};
54
55/// A struct for lazy memoization, meant to be kept as a `static`.
56pub struct Memoized<K: std::hash::Hash + std::cmp::Eq, V>(LazyLock<Mutex<HashMap<K, V>>>);
57
58impl<K: std::hash::Hash + std::cmp::Eq, V: Clone + 'static> Memoized<K, V> {
59    /// Returns a new `Memoized`, for quick memoization
60    pub const fn new() -> Self {
61        Self(LazyLock::new(Mutex::default))
62    }
63
64    /// Gets a key, or inserts a new one with a given function
65    pub fn get_or_insert_with<Q>(&self, key: &Q, f: impl FnOnce() -> V) -> V
66    where
67        K: std::borrow::Borrow<Q>,
68        Q: std::hash::Hash + Eq + Clone + Into<K>,
69    {
70        let mut map = self.0.lock().unwrap_or_else(|err| err.into_inner());
71        match map.get(key) {
72            Some(value) => value.clone(),
73            None => map.entry(key.clone().into()).or_insert(f()).clone(),
74        }
75    }
76}
77
78impl<K: std::hash::Hash + std::cmp::Eq, V: Clone + 'static> Default for Memoized<K, V> {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84/// Takes a type and generates an appropriate name for it.
85///
86/// Use this function if you need a name of a type to be
87/// referrable by string, such as by commands or by the
88/// user.
89pub fn duat_name<T: ?Sized + 'static>() -> &'static str {
90    fn duat_name_inner(type_id: TypeId, type_name: &str) -> &'static str {
91        static NAMES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
92            LazyLock::new(RwLock::default);
93        let mut names = NAMES.write().unwrap();
94
95        if let Some(name) = names.get(&type_id) {
96            name
97        } else {
98            let mut name = String::new();
99
100            for path in type_name.split_inclusive(['<', '>', ',', ' ']) {
101                for segment in path.split("::") {
102                    let is_type = segment.chars().any(|c| c.is_uppercase());
103                    let is_punct = segment.chars().all(|c| !c.is_alphanumeric());
104                    let is_dyn = segment.starts_with("dyn");
105                    if is_type || is_punct || is_dyn {
106                        name.push_str(segment);
107                    }
108                }
109            }
110
111            names.insert(type_id, name.leak());
112            names.get(&type_id).unwrap()
113        }
114    }
115
116    duat_name_inner(TypeId::of::<T>(), std::any::type_name::<T>())
117}
118
119/// Returns the source crate of a given type.
120pub fn src_crate<T: ?Sized + 'static>() -> &'static str {
121    fn src_crate_inner(type_id: TypeId, type_name: &'static str) -> &'static str {
122        static CRATES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
123            LazyLock::new(|| RwLock::new(HashMap::new()));
124        let mut crates = CRATES.write().unwrap();
125
126        if let Some(src_crate) = crates.get(&type_id) {
127            src_crate
128        } else {
129            let src_crate = type_name.split([' ', ':']).find(|w| *w != "dyn").unwrap();
130
131            crates.insert(type_id, src_crate);
132            crates.get(&type_id).unwrap()
133        }
134    }
135
136    src_crate_inner(TypeId::of::<T>(), std::any::type_name::<T>())
137}
138
139/// The path for the crate that was loaded.
140static CRATE_DIR: OnceLock<Option<&Path>> = OnceLock::new();
141/// The profile that was loaded.
142static PROFILE: OnceLock<&str> = OnceLock::new();
143
144/// Sets the crate directory.
145///
146/// **FOR USE BY THE DUAT EXECUTABLE ONLY**
147#[doc(hidden)]
148#[track_caller]
149pub(crate) fn set_crate_profile_and_dir(profile: String, dir: Option<String>) {
150    CRATE_DIR
151        .set(dir.map(|dir| Path::new(dir.leak())))
152        .expect("Crate directory set multiple times.");
153    PROFILE
154        .set(profile.leak())
155        .expect("Profile set multiple times.");
156}
157
158/// The path for the config crate of Duat.
159#[track_caller]
160pub fn crate_dir() -> Result<&'static Path, Text> {
161    CRATE_DIR
162        .get()
163        .expect("Config not set yet")
164        .ok_or_else(|| txt!("Config directory is [a]undefined"))
165}
166
167/// The Profile that is currently loaded.
168///
169/// This is only valid inside of the loaded configuration's `run`
170/// function, not in the metastatic Ui.
171pub fn profile() -> &'static str {
172    PROFILE.get().expect("Profile not set yet")
173}
174
175/// The path for a plugin's auxiliary buffers.
176///
177/// If you want to store something in a more permanent basis, and also
178/// possibly allow for the user to modify some buffers (e.g. a TOML
179/// buffer with definitions for various LSPs), you should place it in
180/// here.
181///
182/// This function will also create said directory, if it doesn't
183/// already exist, only returning [`Some`], if it managed to verify
184/// its existance.
185pub fn plugin_dir(plugin: &str) -> Result<PathBuf, Text> {
186    assert_ne!(plugin, "", "Can't have an empty plugin name");
187
188    static PLUGIN_DIR: LazyLock<Option<&Path>> = LazyLock::new(|| {
189        dirs_next::data_local_dir().map(|local_dir| {
190            let path: &'static str = local_dir
191                .join("duat")
192                .join("plugins")
193                .to_string_lossy()
194                .to_string()
195                .leak();
196
197            Path::new(path)
198        })
199    });
200
201    let plugin_dir = (*PLUGIN_DIR)
202        .ok_or_else(|| txt!("Local directory is [a]undefined"))?
203        .join(plugin);
204    std::fs::create_dir_all(&plugin_dir)?;
205
206    Ok(plugin_dir)
207}
208
209/// Convenience function for the bounds of a range.
210///
211/// Non panicking version of [`get_range`].
212pub fn try_get_range(range: impl std::ops::RangeBounds<usize>, max: usize) -> Option<Range<usize>> {
213    let start = match range.start_bound() {
214        std::ops::Bound::Included(start) => *start,
215        std::ops::Bound::Excluded(start) => *start + 1,
216        std::ops::Bound::Unbounded => 0,
217    };
218    let end = match range.end_bound() {
219        std::ops::Bound::Included(end) => *end + 1,
220        std::ops::Bound::Excluded(end) => *end,
221        std::ops::Bound::Unbounded => max,
222    };
223
224    if start > max || end > max || start > end {
225        None
226    } else {
227        Some(start..end)
228    }
229}
230
231/// Convenience function for the bounds of a range.
232///
233/// Panicking version of [`try_get_range`].
234#[track_caller]
235pub fn get_range(range: impl std::ops::RangeBounds<usize>, max: usize) -> Range<usize> {
236    let start = match range.start_bound() {
237        std::ops::Bound::Included(start) => *start,
238        std::ops::Bound::Excluded(start) => *start + 1,
239        std::ops::Bound::Unbounded => 0,
240    };
241    let end = match range.end_bound() {
242        std::ops::Bound::Included(end) => *end + 1,
243        std::ops::Bound::Excluded(end) => *end,
244        std::ops::Bound::Unbounded => max,
245    };
246
247    crate::ranges::assert_range(&(start..end));
248
249    assert!(
250        start <= max,
251        "start out of bounds: the len is {max}, but the index is {start}",
252    );
253    assert!(
254        end <= max,
255        "end out of bounds: the len is {max}, but the index is {end}",
256    );
257
258    start..end
259}
260
261/// Adds two shifts together.
262pub fn add_shifts(lhs: [i32; 3], rhs: [i32; 3]) -> [i32; 3] {
263    let b = lhs[0] + rhs[0];
264    let c = lhs[1] + rhs[1];
265    let l = lhs[2] + rhs[2];
266    [b, c, l]
267}
268
269/// Allows binary searching with an initial guess and displaced
270/// entries.
271///
272/// This function essentially looks at a list of entries and with a
273/// starting shift position, shifts them by an amount, before
274/// comparing inside of the binary search.
275#[track_caller]
276pub fn merging_range_by_guess_and_lazy_shift<T, U: Copy + Ord + std::fmt::Debug, V: Copy>(
277    (container, len): (&impl std::ops::Index<usize, Output = T>, usize),
278    (guess_i, [start, end]): (usize, [U; 2]),
279    (shift_from, shift, zero_shift, shift_fn): (usize, V, V, fn(U, V) -> U),
280    (start_fn, end_fn): (fn(&T) -> U, fn(&T) -> U),
281) -> Range<usize> {
282    let sh = |n: usize| if n >= shift_from { shift } else { zero_shift };
283    let start_of = |i: usize| shift_fn(start_fn(&container[i]), sh(i));
284    let end_of = |i: usize| shift_fn(end_fn(&container[i]), sh(i));
285    let search = |n: usize, t: &T| shift_fn(start_fn(t), sh(n));
286
287    let mut m_range = if let Some(prev_i) = guess_i.checked_sub(1)
288        && (prev_i < len && start_of(prev_i) <= start && start <= end_of(prev_i))
289    {
290        prev_i..guess_i
291    } else {
292        match binary_search_by_key_and_index(container, len, start, search) {
293            Ok(i) => i..i + 1,
294            Err(i) => {
295                if let Some(prev_i) = i.checked_sub(1)
296                    && start <= end_of(prev_i)
297                {
298                    prev_i..i
299                } else {
300                    i..i
301                }
302            }
303        }
304    };
305
306    // On Cursors, the Cursors can intersect, so we need to check
307    while m_range.start > 0 && start <= end_of(m_range.start - 1) {
308        m_range.start -= 1;
309    }
310
311    // This block determines how far ahead this cursor will merge
312    if m_range.end < len && end >= start_of(m_range.end) {
313        m_range.end = match binary_search_by_key_and_index(container, len, end, search) {
314            Ok(i) => i + 1,
315            Err(i) => i,
316        }
317    }
318
319    while m_range.end + 1 < len && end >= start_of(m_range.end + 1) {
320        m_range.end += 1;
321    }
322
323    m_range
324}
325
326/// A binary search that takes into account the index of the element
327/// in order to extract the key.
328///
329/// In Duat, this is used for searching in ordered lists where the
330/// elements after a certain index are shifted by some amount, while
331/// those behind that point aren't shifted at all.
332#[inline(always)]
333pub fn binary_search_by_key_and_index<T, K>(
334    container: &(impl std::ops::Index<usize, Output = T> + ?Sized),
335    len: usize,
336    key: K,
337    f: impl Fn(usize, &T) -> K,
338) -> std::result::Result<usize, usize>
339where
340    K: PartialEq + Eq + PartialOrd + Ord,
341{
342    let mut size = len;
343    let mut left = 0;
344    let mut right = size;
345
346    while left < right {
347        let mid = left + size / 2;
348
349        let k = f(mid, &container[mid]);
350
351        match k.cmp(&key) {
352            std::cmp::Ordering::Less => left = mid + 1,
353            std::cmp::Ordering::Equal => return Ok(mid),
354            std::cmp::Ordering::Greater => right = mid,
355        }
356
357        size = right - left;
358    }
359
360    Err(left)
361}
362
363/// A function to catch panics.
364///
365/// Used in duat-core in order to prevent sudden panics from just
366/// crashing the program, which would be bad for the end user I think.
367///
368/// You shouldn't use this function unless you are doing a trait based
369/// API, where the implementation of traits by users might cause
370/// panics.
371pub fn catch_panic<R>(f: impl FnOnce() -> R) -> Option<R> {
372    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).ok()
373}
374
375/// Log something to a log file, when things are panicking in a way
376/// that exits Duat.
377#[macro_export]
378macro_rules! log_to_file {
379    ($($tokens:tt)*) => {{
380        use std::io::Write;
381        let mut log = std::fs::OpenOptions::new()
382            .append(true)
383            .create(true)
384            .open("log")
385            .unwrap();
386
387        writeln!(log, $($tokens)*).unwrap();
388    }};
389}
390
391/// Macro used internally for doc tests in duat-core.
392#[doc(hidden)]
393#[rustfmt::skip]
394#[macro_export]
395macro_rules! doc_duat {
396    ($duat:ident) => {
397        #[allow(unused, missing_docs)]
398        mod $duat {
399            pub use $crate::{clipboard, notify, process};
400            
401            pub struct Opts {
402                pub wrap_lines: bool,
403                pub wrap_on_word: bool,
404                pub wrapping_cap: Option<u32>,
405                pub indent_wraps: bool,
406                pub tabstop: u8,
407                pub print_new_line: bool,
408                pub scrolloff: $crate::opts::ScrollOff,
409                pub force_scrolloff: bool,
410                pub extra_word_chars: &'static [char],
411                pub show_ghosts: bool,
412                pub allow_overscroll: bool,
413                pub one_line_footer: bool,
414                pub footer_on_top: bool,
415            }
416
417            pub mod hook {
418                pub use $crate::hook::*;
419            }
420            
421            pub mod text {
422                pub use $crate::text::*;
423            }
424            
425            pub mod cursor {
426                pub use $crate::form::{
427                    extra_cursor as get_extra, id_of, main_cursor as get_main,
428                    set_extra_cursor as set_extra, set_main_cursor as set_main,
429                    unset_cursors as unset, unset_extra_cursor as unset_extra,
430                    unset_main_cursor as unset_main,
431                };
432            }
433
434            pub mod opts {
435                use super::prelude::*;
436                pub use $crate::opts::{self, PrintOpts};
437                pub fn set(set: impl FnOnce(&mut super::Opts)) {}
438                pub fn fmt_status<T>(set: impl FnMut(&mut Pass) -> T) {}
439            }
440
441            pub mod data {
442                pub use $crate::data::*;
443            }
444
445            pub mod state {
446                use super::prelude::*;
447                pub fn name_txt(buffer: &Buffer) -> Text { Text::default() }
448                pub fn path_txt(buffer: &Buffer) -> Text { Text::default() }
449                pub fn mode_name() -> data::DataMap<&'static str, &'static str> { todo!() }
450                pub fn mode_txt() -> data::DataMap<&'static str, Text> { todo!() }
451                pub fn main_byte(buffer: &Buffer) -> usize { 0 }
452                pub fn main_char(buffer: &Buffer) -> usize { 0 }
453                pub fn main_line(buffer: &Buffer) -> usize { 0 }
454                pub fn main_col(buffer: &Buffer, area: &ui::Area) -> usize { 0 }
455                pub fn main_txt(buffer: &Buffer, area: &ui::Area) -> Text { Text::default() }
456                pub fn selections(buffer: &Buffer) -> usize { 0 }
457                pub fn sels_txt(buffer: &Buffer) -> Text { Text::default() }
458                pub fn cur_map_txt() -> data::DataMap<(Vec<KeyEvent>, bool), Text> { todo!() }
459                pub fn last_key() -> data::RwData<String> { todo!() }
460            }
461
462            pub mod mode {
463                pub use $crate::mode::*;
464                pub use super::modes::*;
465            }
466                
467            pub mod prelude {
468                pub use std::ops::Range;
469                pub use $crate::try_or_log_err;
470                
471                pub use $crate::{
472                    Ns, buffer::Buffer, cmd,
473                    context::{self, Handle},
474                    data::{self, Pass},
475                    form::{self, CursorShape, Form},
476                    hook::{
477                        self, BufferOpened, BufferSaved, BufferUpdated, ColorschemeSet,
478                        ConfigLoaded, ConfigUnloaded, FocusChanged, FocusedOn, FocusedOnDuat,
479                        FormSet, Hookable, KeySent, ModeSwitched, UnfocusedFrom, UnfocusedFromDuat,
480                        WidgetOpened, WindowOpened,
481                    },
482                    text::{
483                        self, Builder, Conceal, Inlay, Spacer, Spawn, Strs, Text, txt, Point, Mask,
484                        TextMut
485                    },
486                    ui::{self, Area, Widget},
487                };
488                
489                pub use super::{
490                    cursor::*,
491                    mode::{
492                        self, KeyCode, KeyEvent, Mode, Prompt, Pager, User, alias, alt, ctrl, event,
493                        map, shift,
494                    },
495                    state::*, widgets::*, PassFileType, FileType, opts, Opts
496                };
497
498                #[macro_export]
499                macro_rules! setup_duat{ ($setup:ident) => {} }
500            }
501
502            pub mod widgets {
503                use std::fmt::Alignment;
504                
505                pub struct LineNumbers {
506                    buffer: Handle,
507                    text: Text,
508                    pub relative: bool,
509                    pub align: Alignment,
510                    pub main_align: Alignment,
511                    pub show_wraps: bool,
512                }
513                impl LineNumbers {
514                    pub fn builder() -> LineNumbersOpts { LineNumbersOpts {
515                        relative: false,
516                        align: Alignment::Right,
517                        main_align: Alignment::Right,
518                        show_wraps: false,
519                        on_the_right: false
520                    }}
521                }
522                impl Widget for LineNumbers {
523                    fn text(&self) -> &Text { &self.text }
524                    fn text_mut(&mut self) -> TextMut<'_> { self.text.as_mut() }
525                }
526                #[derive(Clone, Copy, Debug)]
527                pub struct LineNumbersOpts {
528                    pub relative: bool,
529                    pub align: Alignment,
530                    pub main_align: Alignment,
531                    pub show_wraps: bool,
532                    pub on_the_right: bool,
533                }
534                impl LineNumbersOpts {
535                    pub fn push_on(self, _: &mut Pass, _: &Handle) {}
536                }
537                
538                #[macro_export]
539                macro_rules! status{ ($str: literal) => { $duat::widgets::StatusLine } }
540                pub struct StatusLine;
541                impl StatusLine {
542                    pub fn above(self) -> Self { Self }
543                    pub fn push_on(self, _: &mut Pass, _: &impl $crate::ui::PushTarget) {}
544                }
545
546                pub struct LogBook;
547                impl LogBook {
548                    pub fn builder() -> LogBookOpts { LogBookOpts::default() }
549                    pub fn push_on(self, _: &mut Pass, _: &impl $crate::ui::PushTarget) {}
550                }
551                #[derive(Default, Clone, Copy, Debug)]
552                pub struct LogBookOpts {
553                    pub close_on_unfocus: bool,
554                    pub hidden: bool,
555                    pub side: $crate::ui::Side,
556                    pub height: f32,
557                    pub width: f32,
558                }
559                impl LogBookOpts {
560                    pub fn push_on(self, _: &mut Pass, _: &impl $crate::ui::PushTarget) {}
561                }
562                
563                use super::prelude::*;
564                pub struct VertRule;
565                impl VertRule {
566                    pub fn builder() -> VertRuleBuilder { VertRuleBuilder }
567                }
568                pub struct VertRuleBuilder;
569                impl VertRuleBuilder {
570                    pub fn push_on(self, _: &mut Pass, _: &impl $crate::ui::PushTarget) {}
571                    pub fn on_the_right(self) -> Self { self }
572                }
573            }
574
575            pub mod modes {
576                use super::prelude::*;
577                #[derive(Clone)]
578                pub struct Pager;
579                impl $crate::mode::Mode for Pager {
580                    type Widget = $crate::buffer::Buffer;
581                    fn send_key(
582                        &mut self,
583                        _: &mut Pass,
584                        _: $crate::mode::KeyEvent,
585                        _: Handle<Self::Widget>,
586                    ) {
587                    }
588                }
589                
590                #[derive(Clone)]
591                pub struct Prompt;
592                impl $crate::mode::Mode for Prompt {
593                    type Widget = $crate::buffer::Buffer;
594                    fn send_key(
595                        &mut self,
596                        _: &mut Pass,
597                        _: $crate::mode::KeyEvent,
598                        _: Handle<Self::Widget>,
599                    ) {
600                    }
601                }
602
603                #[derive(Clone)]
604                pub struct RunCommands;
605                impl RunCommands {
606                    pub fn new() -> Prompt {
607                        Prompt
608                    }
609                    pub fn new_with(initial: &str) -> Prompt {
610                        Prompt
611                    }
612                }
613            }
614
615            pub trait FileType {
616                fn filetype(&self) -> Option<&'static str> { None }
617            }
618
619            impl FileType for prelude::Buffer {}
620            impl FileType for String {}
621            impl FileType for &'_ str {}
622            impl FileType for std::path::PathBuf {}
623            impl FileType for &'_ std::path::Path {}
624
625            pub trait PassFileType {
626                fn filetype(&self, _: &prelude::Pass) -> Option<&'static str> { None }
627            }
628            impl PassFileType for prelude::data::RwData<prelude::Buffer> {}
629            impl PassFileType for prelude::Handle {}
630
631            
632        }
633    }
634}