duat_utils/
lib.rs

1//! Standard complementary additions to Duat
2//!
3//! This crate essentially consists of the standard bits that pretty
4//! much every `config` crate will want to use, but aren't strictly
5//! speaking necessary for Duat to function. This split is mostly to
6//! improve compile times, but semantically, if a crate doesn't need
7//! all of these extra things, it is nice to separate them out.
8//!
9//! The crate has the following elements:
10//!
11//! - 5 [`widgets`]:
12//!   - [`LineNumbers`] shows the numbers on a [`File`] (for now), and
13//!     you can configure their alignment, relativeness, etc.
14//!   - The [`PromptLine`] lets you run commands and do other things,
15//!     like incremental search
16//!   - The [`StatusLine`] lets you display information that gets
17//!     updated automatically, it can show information from
18//!     [`RwData`]s, mapping functions, static elements, and every bit
19//!     of Duat. It's syntax, in [`status!`] is the same as the
20//!     [`txt!`] macro.
21//!   - [`Notifications`] shows things that have been logged to the
22//!     [`Logs`] of Duat, through the [`error!`], [`warn!`] and
23//!     [`info!`] macros.
24//!   - [`LogBook`] is a log of everything that has been notified to
25//!     Duat. It is usually more admissive than [`Notifications`], and
26//!     is most commonly scrolled by the [`Pager`] [`Mode`].
27//!
28//! - 3 [`modes`]:
29//!   - [`Regular`] is essentially the standard [`Mode`] that text
30//!     editors use. Sort of like VSCode.
31//!   - [`Prompt`] is a multitool that can serve many purposes,
32//!     through the [`PromptMode`] trait, which allows one to act on
33//!     the [`PromptLine`] while abstracting over less important
34//!     elements of the [`Widget`].
35//!   - [`Pager`] is a simple, read only [`Mode`], designed for
36//!     scrolling and searching through [`Widget`]s, most commonly the
37//!     [`LogBook`].
38//!
39//! - For the [`PromptLine`], there are 4 [`PromptMode`]s:
40//!   - [`RunCommands`] will interpret and run Duat commands, with
41//!     syntax highlighting for correctness, defined by the
42//!     [`Parameter`] trait.
43//!   - [`PipeSelections`] will pipe each selection on the current
44//!     [`File`], replacing them with the return value from a shell
45//!     command.
46//!   - [`IncSearch`] is a specialized mode used for incremental
47//!     search, which can abstract over what the search actually does
48//!     with the [`IncSearcher`] trait.
49//!
50//! - For [`IncSearch`], there are 4 [`IncSearcher`]s:
51//!   - [`SearchFwd`] will move each [`Cursor`] to the next match.
52//!   - [`SearchRev`] will move each [`Cursor`] to the previous match.
53//!   - [`ExtendFwd`] will extend each [`Cursor`]'s selections to the
54//!     next match.
55//!   - [`ExtendRev`] will extend each [`Cursor`]'s selections to the
56//!     previous match.
57//!
58//! Note that the [`IncSearcher`] trait can be used for many more
59//! interesting things, like in [`duat-kak`] for example, where its
60//! implementors allow for splitting selections, selecting everything
61//! within a range, and many more such things in the future.
62//!
63//! - There are also two [`hooks`]:
64//!   - [`SearchUpdated`] for when an [`IncSearch`] is updated.
65//!   - [`SearchPerformed`] for when an [`IncSearch`] is finished.
66//!
67//! And finally, there is the [`state`] module, which contains a bunch
68//! of [`StatusLine`] parts for you to customize the [`StatusLine`]
69//! with.
70//!
71//! I would consider this crate essential for all `config`s of Duat
72//! out there, since it defines primitives that are not only hard to
73//! replace, but might also be very extensible by plugins in the
74//! ecosystem.
75//!
76//! [`LineNumbers`]: widgets::LineNumbers
77//! [`File`]: duat_core::file::File
78//! [`PromptLine`]: widgets::PromptLine
79//! [`StatusLine`]: widgets::StatusLine
80//! [`RwData`]: duat_core::data::RwData
81//! [`status!`]: widgets::status
82//! [`txt!`]: duat_core::text::txt
83//! [`Notifications`]: widgets::Notifications
84//! [`Logs`]: duat_core::context::Logs
85//! [`error!`]: duat_core::context::error
86//! [`warn!`]: duat_core::context::warn
87//! [`info!`]: duat_core::context::info
88//! [`Regular`]: modes::Regular
89//! [`Mode`]: duat_core::mode::Mode
90//! [`Prompt`]: modes::Prompt
91//! [`PromptMode`]: modes::PromptMode
92//! [`Widget`]: duat_core::ui::Widget
93//! [`RunCommands`]: modes::RunCommands
94//! [`Parameter`]: duat_core::cmd::Parameter
95//! [`PipeSelections`]: modes::PipeSelections
96//! [`IncSearch`]: modes::IncSearch
97//! [`IncSearcher`]: modes::IncSearcher
98//! [`SearchFwd`]: modes::SearchFwd
99//! [`Cursor`]: duat_core::mode::Cursor
100//! [`SearchRev`]: modes::SearchRev
101//! [`ExtendFwd`]: modes::ExtendFwd
102//! [`ExtendRev`]: modes::ExtendRev
103//! [`duat-kak`]: https://docs.rs/duat-kak/latest/duat_kak
104//! [`SearchUpdated`]: hooks::SearchUpdated
105//! [`SearchPerformed`]: hooks::SearchPerformed
106//! [`Pager`]: modes::Pager
107//! [`LogBook`]: widgets::LogBook
108#![feature(
109    decl_macro,
110    closure_lifetime_binder,
111    default_field_values,
112    associated_type_defaults
113)]
114
115use duat_core::prelude::*;
116use regex_syntax::ast::Ast;
117
118pub mod modes;
119pub mod state;
120pub mod widgets;
121
122pub mod hooks {
123    //! Additional hooks for `duat-utils` specific things
124    //!
125    //! Right now, these are the two [`hook`]s available for use:
126    //!
127    //!   - [`SearchUpdated`] for when an [`IncSearch`] is updated.
128    //!   - [`SearchPerformed`] for when an [`IncSearch`] is finished.
129    //!
130    //! More may come in the future
131    //!
132    //! [`hook`]: duat_core::hook
133    //! [`IncSearch`]: crate::modes::IncSearch
134    use duat_core::hook::Hookable;
135
136    /// [`Hookable`]: Triggers when a [search] is updated
137    ///
138    /// Will not be triggered if the previous and current patterns are
139    /// the same.
140    ///
141    /// # Arguments
142    ///
143    /// - The previous regex pattern
144    /// - The current regex pattern
145    ///
146    /// [search]: crate::modes::IncSearch
147    pub struct SearchUpdated(pub(crate) (String, String));
148
149    impl Hookable for SearchUpdated {
150        type Input<'h> = (&'h str, &'h str);
151
152        fn get_input(&mut self) -> Self::Input<'_> {
153            (&self.0.0, &self.0.1)
154        }
155    }
156
157    /// [`Hookable`]: Triggers when a [search] is performed
158    ///
159    /// Will not be triggered on empty searches.
160    ///
161    /// # Arguments
162    ///
163    /// - The searched regex pattern
164    ///
165    /// [search]: crate::modes::IncSearch
166    pub struct SearchPerformed(pub(crate) String);
167
168    impl Hookable for SearchPerformed {
169        type Input<'h> = &'h str;
170
171        fn get_input(&mut self) -> Self::Input<'_> {
172            &self.0
173        }
174    }
175}
176
177fn tag_from_ast(tagger: Tagger, text: &mut Text, ast: &Ast) {
178    use duat_core::form::FormId;
179    use regex_syntax::ast::{Ast::*, Span};
180
181    let mut insert_form = |id: FormId, span: Span| {
182        text.insert_tag(tagger, span.start.offset..span.end.offset, id.to_tag(0));
183    };
184
185    match ast {
186        Empty(_) => {}
187        Flags(set_flags) => {
188            let id = form::id_of!("regex.operator.flags");
189            insert_form(id, set_flags.span);
190        }
191        Literal(literal) => {
192            let id = form::id_of!("regex.literal");
193            insert_form(id, literal.span);
194        }
195        Dot(span) => {
196            let id = form::id_of!("regex.operator.dot");
197            insert_form(id, **span);
198        }
199        Assertion(assertion) => {
200            let id = form::id_of!("regex.operator.assertion");
201            insert_form(id, assertion.span);
202        }
203        ClassUnicode(class) => {
204            let id = form::id_of!("regex.class.unicode");
205            insert_form(id, class.span);
206        }
207        ClassPerl(class) => {
208            let id = form::id_of!("regex.class.perl");
209            insert_form(id, class.span);
210        }
211        ClassBracketed(class) => {
212            let class_id = form::id_of!("regex.class.bracketed");
213            let bracket_id = form::id_of!("regex.bracket.class");
214
215            insert_form(class_id, *class.kind.span());
216
217            let range = class.span.start.offset..class.span.start.offset + 1;
218            text.insert_tag(tagger, range, bracket_id.to_tag(0));
219            let range = class.span.end.offset - 1..class.span.end.offset;
220            text.insert_tag(tagger, range, bracket_id.to_tag(0));
221        }
222        Repetition(repetition) => {
223            let id = form::id_of!("regex.operator.repetition");
224            insert_form(id, repetition.op.span);
225        }
226        Group(group) => {
227            let group_id = form::id_of!("regex.group");
228            let bracket_id = form::id_of!("regex.bracket.group");
229
230            insert_form(group_id, *group.ast.span());
231
232            let range = group.span.start.offset..group.span.start.offset + 1;
233            text.insert_tag(tagger, range, bracket_id.to_tag(0));
234            let range = group.span.end.offset - 1..group.span.end.offset;
235            text.insert_tag(tagger, range, bracket_id.to_tag(0));
236
237            tag_from_ast(tagger, text, &group.ast);
238        }
239        Alternation(alternation) => {
240            let id = form::id_of!("regex.operator.alternation");
241
242            let mut prev_end = None;
243
244            for ast in alternation.asts.iter() {
245                tag_from_ast(tagger, text, ast);
246
247                if let Some(end) = prev_end {
248                    let range = end..ast.span().start.offset;
249                    text.insert_tag(tagger, range, id.to_tag(0));
250                }
251
252                prev_end = Some(ast.span().end.offset);
253            }
254        }
255        Concat(concat) => {
256            for ast in concat.asts.iter() {
257                tag_from_ast(tagger, text, ast);
258            }
259        }
260    }
261}
262
263mod private_exports {
264    pub use duat_core;
265    pub use format_like::format_like;
266
267    pub macro parse_str($appender_checker:expr, $str:literal) {{
268        use crate::{
269            private_exports::duat_core::{context::Handle, data::Pass, file::File, text::Builder},
270            widgets::State,
271        };
272
273        let (mut appender, checker) = $appender_checker;
274        let (mut ap, _) = State::from($str).fns();
275
276        let appender = move |pa: &Pass, builder: &mut Builder, reader: &Handle<File<_>, _>| {
277            appender(pa, builder, reader);
278            ap(pa, builder, reader);
279        };
280
281        (appender, checker)
282    }}
283
284    pub macro parse_status_part {
285        ($appender_checker:expr, "", $part:expr) => {{
286            use crate::{
287                private_exports::duat_core::{
288                    context::Handle, data::Pass, file::File, text::Builder
289                },
290                widgets::State,
291            };
292
293			#[allow(unused_mut)]
294            let (mut appender, checker) = $appender_checker;
295            let (ap, ch) = State::from($part).fns();
296
297            let checker = move |pa: &Pass| checker(pa) || ch(pa);
298
299            let appender = move |pa: &Pass, builder: &mut Builder, handle: &Handle<File<_>, _>| {
300                appender(pa, builder, handle);
301                ap(pa, builder, handle);
302            };
303
304            (appender, checker)
305        }},
306        ($appender_checker:expr, $modif:literal, $part:expr) => {{
307            use crate::{
308                private_exports::duat_core::{
309                    context::Handle, data::Pass, file::File, text::Builder
310                },
311                widgets::State,
312            };
313
314            let (mut appender, checker) = $appender_checker;
315            let (ap, ch) = State::from(format!(concat!("{:", $modif, "}"), $part)).fns();
316
317            let checker = move |pa: &Pass| checker(pa) || ch(pa);
318
319            let appender = move |pa: &Pass, builder: &mut Builder, handle: &Handle<File<_>, _>| {
320                appender(pa, builder, handle);
321                ap(pa, builder, handle);
322            };
323
324            (appender, checker)
325        }}
326    }
327
328    pub macro parse_form {
329        ($appender_checker:expr, "",) => {{
330            use crate::private_exports::duat_core::{
331                context::Handle, data::Pass, file::File, form, text::Builder
332            };
333
334            let (appender, checker) = $appender_checker;
335            let appender = move |pa: &Pass, builder: &mut Builder, handle: &Handle<File<_>, _>| {
336                appender(pa, builder, handle);
337                builder.push(form::DEFAULT_ID);
338            };
339
340            (appender, checker)
341        }},
342        ($appender_checker:expr, "", $($form:tt)*) => {{
343            use crate::private_exports::duat_core::{
344                context::Handle, data::Pass, file::File, form, text::Builder
345            };
346
347            let (appender, checker) = $appender_checker;
348            let id = form::id_of!(concat!($(stringify!($form)),*));
349
350            let appender = move |pa: &Pass, builder: &mut Builder, handle: &Handle<File<_>, _>| {
351                appender(pa, builder, handle);
352                builder.push(id);
353            };
354
355            (appender, checker)
356        }},
357        ($pre_fn_and_checker:expr, $modif:literal, $($form:ident).*) => {{
358            compile_error!(concat!(
359                "at the moment, Forms in StatusLines don't support modifiers like ",
360                $modif
361            ))
362        }}
363    }
364}