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