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});