duat_core/
opts.rs

1//! General printing options for printing [`Buffer`]s
2//!
3//! This is essentially the things that you'd expect to change in a
4//! text editor such as Neovim or Kakoune. They are contained in a
5//! [`PrintOpts`] struct, which is very light and cheap to copy
6//! around, and is used not only by the [`Buffer`], but by every other
7//! [`Widget`] as well.
8//!
9//! [`Buffer`]: crate::buffer::Buffer
10//! [`Widget`]: crate::ui::Widget
11use std::{collections::HashMap, sync::LazyLock};
12
13use parking_lot::Mutex;
14
15use crate::text::Matcheable;
16
17/// The distance to keep between the [`Cursor`] and the edges of the
18/// screen when scrolling
19///
20/// [`Cursor`]: crate::mode::Cursor
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct ScrollOff {
23    /// The horizontal scrolloff
24    pub x: u8,
25    /// The vertical scrolloff
26    pub y: u8,
27}
28
29/// Configuration options for printing.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct PrintOpts {
32    /// Enables wrapping of lines
33    ///
34    /// The default is `true`
35    pub wrap_lines: bool,
36    /// Wrap on word boundaries, rather than on any character
37    ///
38    /// The default is `false`.
39    pub wrap_on_word: bool,
40    /// Where to start wrapping
41    ///
42    /// The default is `None`
43    ///
44    /// If this value is `None` and `opts.wrap_lines == false`, then
45    /// wrapping will take place at the right edge of the screen.
46    ///
47    /// Otherwise, if it is `Some({cap})`, then wrapping will take
48    /// place `{cap}` cells from the left edge. This value may or may
49    /// not be greater than the width of the area. If it is greater
50    /// than it, then wrapping will take place slightly outside the
51    /// screen as a concequence.
52    pub wrapping_cap: Option<u32>,
53    /// Whether to indent wrapped lines or not
54    ///
55    /// In [`Buffer`]s, the default is `true`.
56    ///
57    /// This turns this:
58    ///
59    /// ```text
60    ///     This is a very long line of text, so long that it
61    /// wraps around
62    /// ```
63    ///
64    /// Into this:
65    ///
66    /// ```text
67    ///     This is a very long line of text, so long that it
68    ///     wraps around
69    /// ```
70    ///
71    /// [`Buffer`]: crate::buffer::Buffer
72    pub indent_wraps: bool,
73    /// How long tabs should be on screen
74    ///
75    /// In [`Buffer`]s, the default is `4`
76    ///
77    /// This also affect other things, like if your tabs are converted
78    /// into spaces, this will also set how many spaces should be
79    /// added.
80    ///
81    /// [`Buffer`]: crate::buffer::Buffer
82    pub tabstop: u8,
83    /// Wether to print the `'\n'` character as an empty space (`' '`)
84    ///
85    /// In [`Buffer`]s, the default is `true`
86    ///
87    /// [`Buffer`]: crate::buffer::Buffer
88    pub print_new_line: bool,
89    /// How much space to keep between the cursor and edges
90    ///
91    /// In [`Buffer`]s, the default is `ScrollOff { x: 3, y: 3 }`
92    ///
93    /// [`Buffer`]: crate::buffer::Buffer
94    pub scrolloff: ScrollOff,
95    /// Whether to limit scrolloff at the end of lines
96    ///
97    /// In [`Buffer`]s, the default is `false`
98    ///
99    /// This makes it so, as you reach the end of a long line of text,
100    /// the cursor line will continue scrolling to the left,
101    /// maintaining the `scrolloff.x`'s gap.
102    ///
103    /// [`Buffer`]: crate::buffer::Buffer
104    pub force_scrolloff: bool,
105    /// Extra characters to be considered part of a word
106    ///
107    /// The default is `&[]`.
108    ///
109    /// Normally, word characters include all of those in the [`\w`]
110    /// character set, which most importantly includes `[0-9A-Za-z_]`.
111    ///
112    /// You can use this setting to add more characters to that list,
113    /// usually something like `-`, `$` or `@`, which are useful to
114    /// consider as word characters in some circumstances.
115    ///
116    /// [`\w`]: https://www.unicode.org/reports/tr18/#word
117    pub extra_word_chars: &'static [char],
118    /// Whether to show [ghoxt text]
119    ///
120    /// In [`Buffer`]s, the default is `true`
121    ///
122    /// This is just a switch to decide if you want ghosts or not.
123    ///
124    /// [`Buffer`]: crate::buffer::Buffer
125    /// [ghoxt text]: crate::text::Ghost
126    pub show_ghosts: bool,
127    /// Wether to allow the [`Text`] to scroll until only
128    /// `scrolloff.y` line are on screen
129    ///
130    /// In [`Buffer`]s, the default is `true`
131    ///
132    /// If you disable this, when your cursor reaches the end of the
133    /// text, if try to scroll the text down, nothing will happen.
134    /// Otherwise, the text will continue scrolling down until there
135    /// are only `scrolloff.y` lines visible on screen.
136    ///
137    /// [`Buffer`]: crate::buffer::Buffer
138    /// [`Text`]: crate::text::Text
139    pub allow_overscroll: bool,
140}
141
142impl PrintOpts {
143    /// The default [`PrintOpts`]
144    ///
145    /// There is, essentially, almost no reason to deviate from this
146    /// in any [`Widget`] other than a [`Buffer`], since those most
147    /// likely will only be printed with the [default `PrintInfo`]
148    /// from a [`RawArea`], i.e., no scrolling is involved, and you
149    /// should usually strive to control the other elements of
150    /// [`Text`]s that the options of a [`PrintOpts`] will want to
151    /// change.
152    ///
153    /// The lack of need to customize this is reflected in
154    /// [`Widget::get_print_opts`], which calls this function by
155    /// default. However, in a [`Buffer`], you'll probably want to
156    /// look at the options below.
157    ///
158    /// The default value is:
159    ///
160    /// ```rust
161    /// use duat_core::opts::*;
162    /// PrintOpts {
163    ///     wrap_lines: false,
164    ///     wrap_on_word: false,
165    ///     wrapping_cap: None,
166    ///     indent_wraps: true,
167    ///     tabstop: 4,
168    ///     print_new_line: false,
169    ///     scrolloff: ScrollOff { x: 3, y: 3 },
170    ///     extra_word_chars: &[],
171    ///     force_scrolloff: false,
172    ///     show_ghosts: true,
173    ///     allow_overscroll: false,
174    /// };
175    /// ```
176    ///
177    /// [`Widget`]: crate::ui::Widget
178    /// [`Buffer`]: crate::buffer::Buffer
179    /// [default `PrintInfo`]: crate::ui::traits::RawArea::PrintInfo
180    /// [`RawArea`]: crate::ui::traits::RawArea
181    /// [`Text`]: crate::text::Text
182    /// [`Widget::get_print_opts`]: crate::ui::Widget::get_print_opts
183    pub const fn new() -> Self {
184        Self {
185            wrap_lines: false,
186            wrap_on_word: false,
187            wrapping_cap: None,
188            indent_wraps: true,
189            tabstop: 4,
190            print_new_line: false,
191            scrolloff: ScrollOff { x: 3, y: 3 },
192            extra_word_chars: &[],
193            force_scrolloff: false,
194            show_ghosts: true,
195            allow_overscroll: false,
196        }
197    }
198
199    /// The default used in buffers and other such inputs
200    ///
201    /// This different default exists because on [`Widget`]s with
202    /// [`Selection`]s, some extra considerations need to be taken
203    /// into account, like new lines needing to be printed in order
204    /// for the `Selection` to visually occupy a `\n` character.
205    ///
206    /// The default value is:
207    ///
208    /// ```rust
209    /// use duat_core::opts::*;
210    /// PrintOpts {
211    ///     wrap_lines: false,
212    ///     wrap_on_word: false,
213    ///     wrapping_cap: None,
214    ///     indent_wraps: true,
215    ///     tabstop: 4,
216    ///     print_new_line: true,
217    ///     scrolloff: ScrollOff { x: 3, y: 3 },
218    ///     extra_word_chars: &[],
219    ///     force_scrolloff: false,
220    ///     show_ghosts: true,
221    ///     allow_overscroll: false,
222    /// };
223    /// ```
224    ///
225    /// [`Widget`]: crate::ui::Widget
226    /// [`Selection`]: crate::mode::Selection
227    pub const fn default_for_input() -> Self {
228        Self {
229            wrap_lines: false,
230            wrap_on_word: false,
231            wrapping_cap: None,
232            indent_wraps: true,
233            tabstop: 4,
234            print_new_line: true,
235            scrolloff: ScrollOff { x: 3, y: 3 },
236            extra_word_chars: &[],
237            force_scrolloff: false,
238            show_ghosts: true,
239            allow_overscroll: true,
240        }
241    }
242
243    ////////// Queries
244
245    /// What the wrap width should be, given an area of a certain
246    /// width
247    #[inline]
248    pub const fn wrap_width(&self, width: u32) -> Option<u32> {
249        if !self.wrap_lines {
250            None
251        } else if let Some(cap) = self.wrapping_cap {
252            Some(cap)
253        } else {
254            Some(width)
255        }
256    }
257
258    /// How many spaces should come at a given x position
259    #[inline]
260    pub fn tabstop_spaces_at(&self, x: u32) -> u32 {
261        self.tabstop as u32 - (x % self.tabstop as u32)
262    }
263
264    /// Gets a `&str` that matches every character that should be part
265    /// of a word
266    ///
267    /// This will come in the form of the regex `[\w{other chars}]`.
268    pub fn word_chars_regex(&self) -> &'static str {
269        PATTERNS
270            .lock()
271            .entry(self.extra_word_chars)
272            .or_insert_with(|| {
273                format!(
274                    r"[\w{}]",
275                    escape_special(self.extra_word_chars.iter().collect()),
276                )
277                .leak()
278            })
279    }
280
281    /// Wether a given `char` is considered a word character
282    pub fn is_word_char(&self, char: char) -> bool {
283        let pat = *PATTERNS
284            .lock()
285            .entry(self.extra_word_chars)
286            .or_insert_with(|| {
287                format!(
288                    r"[\w{}]",
289                    escape_special(self.extra_word_chars.iter().collect()),
290                )
291                .leak()
292            });
293
294        let mut bytes = [b'\0'; 4];
295        let str = char.encode_utf8(&mut bytes);
296        str.reg_matches(pat, ..).unwrap()
297    }
298}
299
300impl Default for PrintOpts {
301    fn default() -> Self {
302        Self::new()
303    }
304}
305
306/// Escapes regex characters
307#[doc(hidden)]
308pub fn escape_special(mut regex: String) -> String {
309    for (i, char) in regex.char_indices().collect::<Vec<_>>() {
310        if let '(' | ')' | '{' | '}' | '[' | ']' | '$' | '^' | '.' | '*' | '+' | '?' | '|' = char {
311            regex.insert(i, '\\');
312        }
313    }
314    regex
315}
316
317static PATTERNS: LazyLock<Mutex<HashMap<&[char], &'static str>>> = LazyLock::new(|| {
318    let mut map: HashMap<&[char], _> = HashMap::new();
319    map.insert(&[], r"[\w]");
320    Mutex::new(map)
321});