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