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//! Internally (and externally), these functions work with structs
8//! that have a "shift" component. Specifically, these structs are an
9//! ordered list with a shifting component. The shifting component
10//! indicates at which point in the list there is a shift. Elements
11//! before that point are considered to be correctly shifted, whilst
12//! elements after that point are considered to be incorrectly shifted
13//! by the exact size of the shift.
14//!
15//! This essentially lets me have a sorted list (for binary search and
16//! fast insertion via [`GapBuffer`]s) while still letting me shift
17//! elements (usually by a [`Change`] in the [`Text`]) without
18//! updating every element after the [`Change`]. I just have to shift
19//! elements between where the [`Change`] was inserted and where the
20//! shift starts.
21//!
22//! This mirrors how text editing works in the real world. A user
23//! doesn't just type randomly on the text buffer, the changes are
24//! usually localized. Given that, this model of keeping a shifting
25//! point results in a very low number of updates that need to be
26//! made.
27//!
28//! One other (more common) way to prevent a bazillion changes to the
29//! text is to divide said text in lines. So a change in some place
30//! will only affect other elements on the same line. However, the
31//! biggest problem with this approach is that you are purposefully
32//! dividing your text in lines, which makes a lot of other things
33//! more complicated than simply having a buffer of bytes. For
34//! example, a change spanning multiple lines is much more complex to
35//! implement in this kind of system. And this complexity trickles
36//! down through your whole text editor, even down to printing text,
37//! which I also find really simple with my system.
38//!
39//! [`GapBuffer`]: gapbuf::GapBuffer
40//! [`Change`]: crate::text::Change
41use std::{
42    any::TypeId,
43    collections::HashMap,
44    ops::Range,
45    path::{Path, PathBuf},
46    sync::{LazyLock, OnceLock},
47};
48
49use parking_lot::RwLock;
50
51use crate::text::{Text, txt};
52
53/// Takes a type and generates an appropriate name for it
54///
55/// Use this function if you need a name of a type to be
56/// referrable by string, such as by commands or by the
57/// user.
58///
59/// # NOTE
60///
61/// Any `<Ui>` or `Ui, ` type arguments will be removed from the final
62/// result, since Duat is supposed to have only one [`Ui`] in use.
63///
64/// [`Ui`]: crate::ui::Ui
65pub fn duat_name<T: ?Sized + 'static>() -> &'static str {
66    fn duat_name_inner(type_id: TypeId, type_name: &str) -> &'static str {
67        static NAMES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
68            LazyLock::new(RwLock::default);
69        let mut names = NAMES.write();
70
71        if let Some(name) = names.get(&type_id) {
72            name
73        } else {
74            let mut name = String::new();
75
76            for path in type_name.split_inclusive(['<', '>', ',', ' ']) {
77                for segment in path.split("::") {
78                    let is_type = segment.chars().any(|c| c.is_uppercase());
79                    let is_punct = segment.chars().all(|c| !c.is_alphanumeric());
80                    let is_dyn = segment.starts_with("dyn");
81                    if is_type || is_punct || is_dyn {
82                        name.push_str(segment);
83                    }
84                }
85            }
86
87            while let Some((i, len)) = None
88                .or_else(|| name.find("<Ui>").map(|i| (i, "<Ui>".len())))
89                .or_else(|| name.find("Ui, ").map(|i| (i, "Ui, ".len())))
90                .or_else(|| name.find("::<Ui>").map(|i| (i, "::<Ui>".len())))
91            {
92                unsafe {
93                    name.as_mut_vec().splice(i..(i + len), []);
94                }
95            }
96
97            names.insert(type_id, name.leak());
98            names.get(&type_id).unwrap()
99        }
100    }
101
102    duat_name_inner(TypeId::of::<T>(), std::any::type_name::<T>())
103}
104
105/// Returns the source crate of a given type
106pub fn src_crate<T: ?Sized + 'static>() -> &'static str {
107    fn src_crate_inner(type_id: TypeId, type_name: &'static str) -> &'static str {
108        static CRATES: LazyLock<RwLock<HashMap<TypeId, &'static str>>> =
109            LazyLock::new(|| RwLock::new(HashMap::new()));
110        let mut crates = CRATES.write();
111
112        if let Some(src_crate) = crates.get(&type_id) {
113            src_crate
114        } else {
115            let src_crate = type_name.split([' ', ':']).find(|w| *w != "dyn").unwrap();
116
117            crates.insert(type_id, src_crate);
118            crates.get(&type_id).unwrap()
119        }
120    }
121
122    src_crate_inner(TypeId::of::<T>(), std::any::type_name::<T>())
123}
124
125/// The path for the crate that was loaded
126static CRATE_DIR: OnceLock<Option<&Path>> = OnceLock::new();
127/// The profile that was loaded
128static PROFILE: OnceLock<&str> = OnceLock::new();
129
130/// Sets the crate directory
131///
132/// **FOR USE BY THE DUAT EXECUTABLE ONLY**
133#[doc(hidden)]
134pub fn set_crate_dir_and_profile(dir: Option<&'static Path>, profile: &'static str) {
135    CRATE_DIR
136        .set(dir)
137        .expect("Crate directory set multiple times.");
138    PROFILE.set(profile).expect("Profile set multiple times.");
139}
140
141/// The path for the config crate of Duat
142pub fn crate_dir() -> Result<&'static Path, Text> {
143    CRATE_DIR
144        .get()
145        .expect("Config not set yet")
146        .ok_or_else(|| txt!("Config directory is [a]undefined"))
147}
148
149/// The Profile that is currently loaded
150///
151/// This is only valid inside of the loaded configuration's `run`
152/// function, not in the metastatic Ui.
153pub fn profile() -> &'static str {
154    PROFILE.get().expect("Profile not set yet")
155}
156
157/// The path for a plugin's auxiliary buffers
158///
159/// If you want to store something in a more permanent basis, and also
160/// possibly allow for the user to modify some buffers (e.g. a TOML
161/// buffer with definitions for various LSPs), you should place it in
162/// here.
163///
164/// This function will also create said directory, if it doesn't
165/// already exist, only returning [`Some`], if it managed to verify
166/// its existance.
167pub fn plugin_dir(plugin: &str) -> Result<PathBuf, Text> {
168    assert_ne!(plugin, "", "Can't have an empty plugin name");
169
170    static PLUGIN_DIR: LazyLock<Option<&Path>> = LazyLock::new(|| {
171        dirs_next::data_local_dir().map(|local_dir| {
172            let path: &'static str = local_dir
173                .join("duat")
174                .join("plugins")
175                .to_string_lossy()
176                .to_string()
177                .leak();
178
179            Path::new(path)
180        })
181    });
182
183    let plugin_dir = (*PLUGIN_DIR)
184        .ok_or_else(|| txt!("Local directory is [a]undefined"))?
185        .join(plugin);
186    std::fs::create_dir_all(&plugin_dir)?;
187
188    Ok(plugin_dir)
189}
190
191/// Convenience function for the bounds of a range
192#[track_caller]
193pub fn get_ends(range: impl std::ops::RangeBounds<usize>, max: usize) -> (usize, usize) {
194    let start = match range.start_bound() {
195        std::ops::Bound::Included(start) => *start,
196        std::ops::Bound::Excluded(start) => *start + 1,
197        std::ops::Bound::Unbounded => 0,
198    };
199    let end = match range.end_bound() {
200        std::ops::Bound::Included(end) => *end + 1,
201        std::ops::Bound::Excluded(end) => *end,
202        std::ops::Bound::Unbounded => max,
203    };
204    assert!(
205        start <= max,
206        "start out of bounds: the len is {max}, but the index is {start}",
207    );
208    assert!(
209        end <= max,
210        "end out of bounds: the len is {max}, but the index is {end}",
211    );
212
213    (start, end)
214}
215
216/// Adds two shifts together
217pub fn add_shifts(lhs: [i32; 3], rhs: [i32; 3]) -> [i32; 3] {
218    let b = lhs[0] + rhs[0];
219    let c = lhs[1] + rhs[1];
220    let l = lhs[2] + rhs[2];
221    [b, c, l]
222}
223
224/// Allows binary searching with an initial guess and displaced
225/// entries
226///
227/// This function essentially looks at a list of entries and with a
228/// starting shift position, shifts them by an amount, before
229/// comparing inside of the binary search.
230pub fn merging_range_by_guess_and_lazy_shift<T, U: Copy + Ord + std::fmt::Debug, V: Copy>(
231    (container, len): (&impl std::ops::Index<usize, Output = T>, usize),
232    (guess_i, [start, end]): (usize, [U; 2]),
233    (shift_from, shift, zero_shift, shift_fn): (usize, V, V, fn(U, V) -> U),
234    (start_fn, end_fn): (fn(&T) -> U, fn(&T) -> U),
235) -> Range<usize> {
236    let sh = |n: usize| if n >= shift_from { shift } else { zero_shift };
237    let start_of = |i: usize| shift_fn(start_fn(&container[i]), sh(i));
238    let end_of = |i: usize| shift_fn(end_fn(&container[i]), sh(i));
239    let search = |n: usize, t: &T| shift_fn(start_fn(t), sh(n));
240
241    let mut m_range = if let Some(prev_i) = guess_i.checked_sub(1)
242        && (prev_i < len && start_of(prev_i) <= start && start <= end_of(prev_i))
243    {
244        prev_i..guess_i
245    } else {
246        match binary_search_by_key_and_index(container, len, start, search) {
247            Ok(i) => i..i + 1,
248            Err(i) => {
249                if let Some(prev_i) = i.checked_sub(1)
250                    && start <= end_of(prev_i)
251                {
252                    prev_i..i
253                } else {
254                    i..i
255                }
256            }
257        }
258    };
259
260    // On Cursors, the Cursors can intersect, so we need to check
261    while m_range.start > 0 && start <= end_of(m_range.start - 1) {
262        m_range.start -= 1;
263    }
264
265    // This block determines how far ahead this cursor will merge
266    if m_range.end < len && end >= start_of(m_range.end) {
267        m_range.end = match binary_search_by_key_and_index(container, len, end, search) {
268            Ok(i) => i + 1,
269            Err(i) => i,
270        }
271    }
272
273    while m_range.end + 1 < len && end >= start_of(m_range.end + 1) {
274        m_range.end += 1;
275    }
276
277    m_range
278}
279
280/// A binary search that takes into account the index of the element
281/// in order to extract the key
282///
283/// In Duat, this is used for searching in ordered lists where the
284/// elements after a certain index are shifted by some amount, while
285/// those behind that point aren't shifted at all.
286pub fn binary_search_by_key_and_index<T, K>(
287    container: &(impl std::ops::Index<usize, Output = T> + ?Sized),
288    len: usize,
289    key: K,
290    f: impl Fn(usize, &T) -> K,
291) -> std::result::Result<usize, usize>
292where
293    K: PartialEq + Eq + PartialOrd + Ord,
294{
295    let mut size = len;
296    let mut left = 0;
297    let mut right = size;
298
299    while left < right {
300        let mid = left + size / 2;
301
302        let k = f(mid, &container[mid]);
303
304        match k.cmp(&key) {
305            std::cmp::Ordering::Less => left = mid + 1,
306            std::cmp::Ordering::Equal => return Ok(mid),
307            std::cmp::Ordering::Greater => right = mid,
308        }
309
310        size = right - left;
311    }
312
313    Err(left)
314}
315
316/// Macro used internally for doc tests in duat-core
317#[doc(hidden)]
318#[rustfmt::skip]
319#[macro_export]
320macro_rules! doc_duat {
321    ($duat:ident) => {
322        #[allow(unused, missing_docs)]
323        mod $duat {
324            pub mod cursor {
325                pub use ::duat_core::form::{
326                    extra_cursor as get_extra, id_of, main_cursor as get_main,
327                    set_extra_cursor as set_extra, set_main_cursor as set_main,
328                    unset_cursors as unset, unset_extra_cursor as unset_extra,
329                    unset_main_cursor as unset_main,
330                };
331            }
332
333            pub mod opts {
334                use super::prelude::*;
335                pub use ::duat_core::opts::{self, PrintOpts};
336                pub fn set(set: impl FnOnce(PrintOpts) -> PrintOpts) {}
337                pub fn set_lines<T>(set: T) {}
338                pub fn set_status<T>(set: impl FnMut(&mut Pass) -> T) {}
339                pub fn set_notifs<T>(set: T) {}
340                pub fn set_logs<T>(set: T) {}
341                pub fn one_line_footer() {}
342            }
343
344            pub mod data {
345                pub use ::duat_core::data::*;
346            }
347
348            pub mod state {
349                use super::prelude::*;
350                pub fn name_txt(buffer: &Buffer) -> Text { Text::default() }
351                pub fn path_txt(buffer: &Buffer) -> Text { Text::default() }
352                pub fn mode_name() -> data::DataMap<&'static str, &'static str> { todo!() }
353                pub fn mode_txt() -> data::DataMap<&'static str, Text> { todo!() }
354                pub fn main_byte(buffer: &Buffer) -> usize { 0 }
355                pub fn main_char(buffer: &Buffer) -> usize { 0 }
356                pub fn main_line(buffer: &Buffer) -> usize { 0 }
357                pub fn main_col(buffer: &Buffer, area: &ui::Area) -> usize { 0 }
358                pub fn main_txt(buffer: &Buffer, area: &ui::Area) -> Text { Text::default() }
359                pub fn selections(buffer: &Buffer) -> usize { 0 }
360                pub fn sels_txt(buffer: &Buffer) -> Text { Text::default() }
361                pub fn cur_map_txt() -> data::DataMap<(Vec<KeyEvent>, bool), Text> { todo!() }
362                pub fn last_key() -> data::RwData<String> { todo!() }
363            }
364                
365            pub mod prelude {
366                pub use std::ops::Range;
367                
368                pub use ::duat_core::{
369                    Plugin, Plugins,
370                    buffer::{Buffer, BufferTracker, Parser},
371                    clipboard, cmd,
372                    context::{self, Handle},
373                    data::{self, Pass},
374                    form::{self, CursorShape, Form},
375                    hook::{
376                        self, BufferWritten, ColorSchemeSet, ConfigLoaded, ConfigUnloaded,
377                        ExitedDuat, FocusChanged, FocusedOnDuat, FormSet, Hookable, KeysSent,
378                        KeysSentTo, ModeSet, ModeSwitched, UnfocusedFrom, UnfocusedFromDuat,
379                        WidgetCreated, WindowCreated,
380                    },
381                    lender::{Lender, DoubleEndedLender, ExactSizeLender},
382                    mode::{
383                        self, KeyCode, KeyEvent, Mode, User, alias, alt, ctrl, event,
384                        map, shift,
385                    },
386                    text::{
387                        self, AlignCenter, AlignLeft, AlignRight, Builder, Conceal, Ghost, Spacer,
388                        SpawnTag, Tagger, Text, txt, Point, Searcher
389                    },
390                    ui::{self, Area, Widget},
391                };
392                
393                pub use super::{
394                    cursor::*, state::*, modes::*, widgets::*, PassFileType, FileType, opts, plug
395                };
396
397                #[macro_export]
398                macro_rules! setup_duat{ ($setup:ident) => {} }
399            }
400
401            pub mod widgets {
402                use std::fmt::Alignment;
403                
404                pub struct LineNumbers {
405                    buffer: Handle,
406                    text: Text,
407                    pub relative: bool,
408                    pub align: Alignment,
409                    pub main_align: Alignment,
410                    pub show_wraps: bool,
411                }
412                impl LineNumbers {
413                    pub fn builder() -> LineNumbersOpts { LineNumbersOpts {
414                        relative: false,
415                        align: Alignment::Right,
416                        main_align: Alignment::Right,
417                        show_wraps: false,
418                        on_the_right: false
419                    }}
420                }
421                impl Widget for LineNumbers {
422                    fn update(pa: &mut Pass, handle: &Handle<Self>) {}
423                    fn needs_update(&self, pa: &Pass) -> bool { false }
424                    fn text(&self) -> &Text { &self.text }
425                    fn text_mut(&mut self) -> &mut Text { &mut self.text }
426                }
427                #[derive(Clone, Copy, Debug)]
428                pub struct LineNumbersOpts {
429                    pub relative: bool,
430                    pub align: Alignment,
431                    pub main_align: Alignment,
432                    pub show_wraps: bool,
433                    pub on_the_right: bool,
434                }
435                impl LineNumbersOpts {
436                    pub fn push_on(self, _: &mut Pass, _: &Handle) {}
437                }
438                
439                #[macro_export]
440                macro_rules! status{ ($str: literal) => { $duat::widgets::StatusLine } }
441                pub struct StatusLine;
442                impl StatusLine {
443                    pub fn above(self) -> Self { Self }
444                    pub fn push_on(self, _: &mut Pass, _: &impl ::duat_core::ui::PushTarget) {}
445                }
446                
447                use super::prelude::*;
448                pub struct VertRule;
449                impl VertRule {
450                    pub fn builder() -> VertRuleBuilder { VertRuleBuilder }
451                }
452                pub struct VertRuleBuilder;
453                impl VertRuleBuilder {
454                    pub fn push_on(self, _: &mut Pass, _: &impl ::duat_core::ui::PushTarget) {}
455                    pub fn on_the_right(self) -> Self { self }
456                }
457            }
458
459            pub mod modes {
460                use super::prelude::*;
461                #[derive(Clone)]
462                pub struct Pager;
463                impl ::duat_core::mode::Mode for Pager {
464                    type Widget = ::duat_core::buffer::Buffer;
465                    fn send_key(
466                        &mut self,
467                        _: &mut Pass,
468                        _: ::duat_core::mode::KeyEvent,
469                        _: Handle<Self::Widget>,
470                    ) {
471                    }
472                }
473                
474                #[derive(Clone)]
475                pub struct Prompt;
476                impl ::duat_core::mode::Mode for Prompt {
477                    type Widget = ::duat_core::buffer::Buffer;
478                    fn send_key(
479                        &mut self,
480                        _: &mut Pass,
481                        _: ::duat_core::mode::KeyEvent,
482                        _: Handle<Self::Widget>,
483                    ) {
484                    }
485                }
486            }
487
488            pub trait FileType {
489                fn filetype(&self) -> Option<&'static str> { None }
490            }
491
492            impl FileType for prelude::Buffer {}
493            impl FileType for String {}
494            impl FileType for &'_ str {}
495            impl FileType for std::path::PathBuf {}
496            impl FileType for &'_ std::path::Path {}
497
498            pub trait PassFileType {
499                fn filetype(&self, _: &prelude::Pass) -> Option<&'static str> { None }
500            }
501            impl PassFileType for prelude::data::RwData<prelude::Buffer> {}
502            impl PassFileType for prelude::Handle {}
503
504            pub fn plug<P: $crate::Plugin>(plugin: P) {}
505        }
506    }
507}