1use crate::ui::text_box::{
2 helper_structs::{CursorPos, TextBoxViewport},
3 utils::{find_word_start_backward, find_word_start_forward},
4};
5use ratatui::style::Style;
6use std::{
7 cmp::{self, Ordering},
8 fmt,
9};
10
11#[derive(Clone, Debug)]
12pub enum TextBoxEditKind {
13 InsertChar(char),
14 DeleteChar(char),
15 InsertNewline,
16 DeleteNewline,
17 InsertStr(String),
18 DeleteStr(String),
19 InsertChunk(Vec<String>),
20 DeleteChunk(Vec<String>),
21}
22
23impl TextBoxEditKind {
24 pub fn apply(&self, lines: &mut Vec<String>, before: &CursorPos, after: &CursorPos) {
25 match self {
26 TextBoxEditKind::InsertChar(c) => {
27 lines[before.row].insert(before.offset, *c);
28 }
29 TextBoxEditKind::DeleteChar(_) => {
30 lines[before.row].remove(after.offset);
31 }
32 TextBoxEditKind::InsertNewline => {
33 let line = &mut lines[before.row];
34 let next_line = line[before.offset..].to_string();
35 line.truncate(before.offset);
36 lines.insert(before.row + 1, next_line);
37 }
38 TextBoxEditKind::DeleteNewline => {
39 debug_assert!(before.row > 0, "invalid pos: {:?}", before);
40 let line = lines.remove(before.row);
41 lines[before.row - 1].push_str(&line);
42 }
43 TextBoxEditKind::InsertStr(s) => {
44 lines[before.row].insert_str(before.offset, s.as_str());
45 }
46 TextBoxEditKind::DeleteStr(s) => {
47 lines[after.row].drain(after.offset..after.offset + s.len());
48 }
49 TextBoxEditKind::InsertChunk(c) => {
50 debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
51
52 let first_line = &mut lines[before.row];
54 let mut last_line = first_line.drain(before.offset..).as_str().to_string();
55 first_line.push_str(&c[0]);
56
57 let next_row = before.row + 1;
59 last_line.insert_str(0, c.last().unwrap());
60 lines.insert(next_row, last_line);
61
62 lines.splice(next_row..next_row, c[1..c.len() - 1].iter().cloned());
64 }
65 TextBoxEditKind::DeleteChunk(c) => {
66 debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
67
68 let mut last_line = lines
70 .drain(after.row + 1..after.row + c.len())
71 .last()
72 .unwrap();
73 last_line.drain(..c[c.len() - 1].len());
75
76 let first_line = &mut lines[after.row];
78 first_line.truncate(after.offset);
79 first_line.push_str(&last_line);
80 }
81 }
82 }
83
84 pub fn invert(&self) -> Self {
85 use TextBoxEditKind::*;
86 match self.clone() {
87 InsertChar(c) => DeleteChar(c),
88 DeleteChar(c) => InsertChar(c),
89 InsertNewline => DeleteNewline,
90 DeleteNewline => InsertNewline,
91 InsertStr(s) => DeleteStr(s),
92 DeleteStr(s) => InsertStr(s),
93 InsertChunk(c) => DeleteChunk(c),
94 DeleteChunk(c) => InsertChunk(c),
95 }
96 }
97}
98
99#[derive(PartialEq, Eq, Clone, Copy)]
100pub enum CharKind {
101 Space,
102 Punctuation,
103 Other,
104}
105
106impl CharKind {
107 pub fn new(c: char) -> Self {
108 if c.is_whitespace() {
109 Self::Space
110 } else if c.is_ascii_punctuation() {
111 Self::Punctuation
112 } else {
113 Self::Other
114 }
115 }
116}
117
118pub enum TextBoxScroll {
119 Delta { rows: i16, cols: i16 },
120 PageDown,
121 PageUp,
122}
123
124impl TextBoxScroll {
125 pub(crate) fn scroll(self, viewport: &mut TextBoxViewport) {
126 let (rows, cols) = match self {
127 Self::Delta { rows, cols } => (rows, cols),
128 Self::PageDown => {
129 let (_, _, _, height) = viewport.rect();
130 (height as i16, 0)
131 }
132 Self::PageUp => {
133 let (_, _, _, height) = viewport.rect();
134 (-(height as i16), 0)
135 }
136 };
137 viewport.scroll(rows, cols);
138 }
139}
140
141impl From<(i16, i16)> for TextBoxScroll {
142 fn from((rows, cols): (i16, i16)) -> Self {
143 Self::Delta { rows, cols }
144 }
145}
146
147#[derive(Debug, Clone)]
148pub enum YankText {
149 Piece(String),
150 Chunk(Vec<String>),
151}
152
153impl Default for YankText {
154 fn default() -> Self {
155 Self::Piece(String::new())
156 }
157}
158
159impl From<String> for YankText {
160 fn from(s: String) -> Self {
161 Self::Piece(s)
162 }
163}
164impl From<Vec<String>> for YankText {
165 fn from(mut c: Vec<String>) -> Self {
166 match c.len() {
167 0 => Self::default(),
168 1 => Self::Piece(c.remove(0)),
169 _ => Self::Chunk(c),
170 }
171 }
172}
173
174impl fmt::Display for YankText {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self {
177 Self::Piece(s) => write!(f, "{}", s),
178 Self::Chunk(ss) => write!(f, "{}", ss.join("\n")),
179 }
180 }
181}
182
183#[derive(Eq, PartialEq)]
184pub enum Boundary {
185 Cursor(Style),
186 Select(Style),
187 End,
188}
189
190impl PartialOrd for Boundary {
191 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
192 Some(self.cmp(other))
193 }
194}
195
196impl Ord for Boundary {
197 fn cmp(&self, other: &Self) -> Ordering {
198 fn rank(b: &Boundary) -> u8 {
199 match b {
200 Boundary::Cursor(_) => 2,
201 Boundary::Select(_) => 1,
202 Boundary::End => 0,
203 }
204 }
205 rank(self).cmp(&rank(other))
206 }
207}
208
209impl Boundary {
210 pub fn style(&self) -> Option<Style> {
211 match self {
212 Boundary::Cursor(s) => Some(*s),
213 Boundary::Select(s) => Some(*s),
214 Boundary::End => None,
215 }
216 }
217}
218
219#[derive(Clone, Copy, Debug)]
220pub enum CursorMove {
221 Forward,
222 Back,
223 Up,
224 Down,
225 Head,
226 End,
227 Top,
228 Bottom,
229 WordForward,
230 WordBack,
231 ParagraphForward,
232 ParagraphBack,
233 Jump(u16, u16),
234 InViewport,
235}
236
237impl CursorMove {
238 pub(crate) fn next_cursor(
239 &self,
240 (row, col): (usize, usize),
241 lines: &[String],
242 viewport: &TextBoxViewport,
243 ) -> Option<(usize, usize)> {
244 use CursorMove::*;
245
246 fn fit_col(col: usize, line: &str) -> usize {
247 cmp::min(col, line.chars().count())
248 }
249
250 match self {
251 Forward if col >= lines[row].chars().count() => {
252 (row + 1 < lines.len()).then_some((row + 1, 0))
253 }
254 Forward => Some((row, col + 1)),
255 Back if col == 0 => {
256 let row = row.checked_sub(1)?;
257 Some((row, lines[row].chars().count()))
258 }
259 Back => Some((row, col - 1)),
260 Up => {
261 let row = row.checked_sub(1)?;
262 Some((row, fit_col(col, &lines[row])))
263 }
264 Down => Some((row + 1, fit_col(col, lines.get(row + 1)?))),
265 Head => Some((row, 0)),
266 End => Some((row, lines[row].chars().count())),
267 Top => Some((0, fit_col(col, &lines[0]))),
268 Bottom => {
269 let row = lines.len() - 1;
270 Some((row, fit_col(col, &lines[row])))
271 }
272 WordForward => {
273 if let Some(col) = find_word_start_forward(&lines[row], col) {
274 Some((row, col))
275 } else if row + 1 < lines.len() {
276 Some((row + 1, 0))
277 } else {
278 Some((row, lines[row].chars().count()))
279 }
280 }
281 WordBack => {
282 if let Some(col) = find_word_start_backward(&lines[row], col) {
283 Some((row, col))
284 } else if row > 0 {
285 Some((row - 1, lines[row - 1].chars().count()))
286 } else {
287 Some((row, 0))
288 }
289 }
290 ParagraphForward => {
291 let mut prev_is_empty = lines[row].is_empty();
292 for (row, line) in lines.iter().enumerate().skip(row + 1) {
293 let is_empty = line.is_empty();
294 if !is_empty && prev_is_empty {
295 return Some((row, fit_col(col, line)));
296 }
297 prev_is_empty = is_empty;
298 }
299 let row = lines.len() - 1;
300 Some((row, fit_col(col, &lines[row])))
301 }
302 ParagraphBack => {
303 let row = row.checked_sub(1)?;
304 let mut prev_is_empty = lines[row].is_empty();
305 for row in (0..row).rev() {
306 let is_empty = lines[row].is_empty();
307 if is_empty && !prev_is_empty {
308 return Some((row + 1, fit_col(col, &lines[row + 1])));
309 }
310 prev_is_empty = is_empty;
311 }
312 Some((0, fit_col(col, &lines[0])))
313 }
314 Jump(row, col) => {
315 let row = cmp::min(*row as usize, lines.len() - 1);
316 let col = fit_col(*col as usize, &lines[row]);
317 Some((row, col))
318 }
319 InViewport => {
320 let (row_top, col_top, row_bottom, col_bottom) = viewport.position();
321
322 let row = row.clamp(row_top as usize, row_bottom as usize);
323 let row = cmp::min(row, lines.len() - 1);
324 let col = col.clamp(col_top as usize, col_bottom as usize);
325 let col = fit_col(col, &lines[row]);
326
327 Some((row, col))
328 }
329 }
330 }
331}