1use std::cell::Cell;
13
14use rich::Segment;
15
16use crate::component::{Component, Context, Flow, View};
17use crate::components::{Theme, fit, plain, question, text};
18use crate::edit::{Buffer, Pos, TAB, cells, clean, graphemes, length};
19use crate::event::{Event, Key, KeyCode};
20use crate::keymap::{Keymap, keys};
21use crate::policy::{LineIo, NotInteractive};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25struct Row {
26 line: usize,
27 start: usize,
29 end: usize,
30 first: bool,
32}
33
34#[must_use = "a component does nothing until it is run or mounted"]
36pub struct TextArea {
37 prompt: String,
38 buffer: Buffer,
41 placeholder: Option<String>,
42 submit: Key,
43 height: usize,
44 line_numbers: bool,
45 theme: Theme,
46 top: Cell<usize>,
49 answer: Option<Option<String>>,
50 rebound: Keymap,
52}
53
54impl TextArea {
55 pub fn new(prompt: impl Into<String>) -> TextArea {
56 TextArea {
57 prompt: prompt.into(),
58 buffer: Buffer::new(""),
59 placeholder: None,
60 submit: Key::ctrl('d'),
61 rebound: Keymap::new("textarea"),
62 height: 5,
63 line_numbers: false,
64 theme: Theme::default(),
65 top: Cell::new(0),
66 answer: None,
67 }
68 }
69
70 pub fn value(mut self, value: impl AsRef<str>) -> Self {
72 self.buffer.set_text(value.as_ref());
73 self.buffer.bottom(false);
74 self
75 }
76
77 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
79 self.placeholder = Some(placeholder.into());
80 self
81 }
82
83 pub fn char_limit(mut self, chars: usize) -> Self {
87 self.buffer.set_limit(Some(chars));
88 self.buffer.bottom(false);
89 self
90 }
91
92 pub fn truncate(text: &str, chars: usize) -> &str {
96 crate::edit::truncate(text, chars)
97 }
98
99 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
103 self.rebound.rebind(action, keys);
104 self
105 }
106
107 pub fn emacs_keys(mut self) -> Self {
124 for (action, names) in [
125 ("left", "left ctrl+b"),
126 ("right", "right ctrl+f"),
127 ("up", "up ctrl+p"),
128 ("down", "down ctrl+n"),
129 ("word-left", "ctrl+left alt+b"),
130 ("word-right", "ctrl+right alt+f"),
131 ("kill-to-end", "ctrl+k"),
132 ("kill-to-start", "ctrl+u"),
133 ("kill-word", "ctrl+w"),
134 ("kill-next-word", "alt+d"),
135 ("kill-previous-word", "alt+backspace"),
136 ("yank", "ctrl+y"),
137 ("yank-pop", "alt+y"),
138 ("paragraph-up", "alt+{"),
139 ("paragraph-down", "alt+}"),
140 ("delete-to-start", ""),
141 ("delete-to-end", ""),
142 ("delete-word", ""),
143 ] {
144 self.rebound.rebind(action, keys(names));
145 }
146 self
147 }
148
149 pub fn submit_key(mut self, key: Key) -> Self {
151 self.submit = key;
152 self
153 }
154
155 pub fn height(mut self, rows: usize) -> Self {
157 self.height = rows.max(1);
158 self
159 }
160
161 pub fn line_numbers(mut self, on: bool) -> Self {
163 self.line_numbers = on;
164 self
165 }
166
167 pub fn theme(mut self, theme: Theme) -> Self {
168 self.theme = theme;
169 self
170 }
171
172 pub fn text(&self) -> String {
174 self.buffer.text()
175 }
176
177 pub fn caret(&self) -> (usize, usize) {
179 self.buffer.caret_column()
180 }
181
182 fn count(&self) -> usize {
183 self.buffer.count()
184 }
185
186 fn shown_rows(&self, rows: usize) -> usize {
189 self.height.min(rows.saturating_sub(2)).max(1)
190 }
191
192 fn gutter(&self) -> usize {
193 if self.line_numbers {
194 self.buffer.line_count().to_string().len() + 3
195 } else {
196 2
197 }
198 }
199
200 fn rows(&self, width: usize) -> Vec<Row> {
203 let room = width.saturating_sub(self.gutter() + 1).max(1);
204 let mut rows = Vec::new();
205 for (index, line) in self.buffer.lines().iter().enumerate() {
206 let spans = graphemes(line);
207 let mut start = 0;
208 let mut cells = 0;
209 let mut first = true;
210 for (at, _, width) in spans {
211 if cells + width > room && at > start {
212 rows.push(Row {
213 line: index,
214 start,
215 end: at,
216 first,
217 });
218 first = false;
219 start = at;
220 cells = 0;
221 }
222 cells += width;
223 }
224 rows.push(Row {
225 line: index,
226 start,
227 end: line.len(),
228 first,
229 });
230 }
231 rows
232 }
233
234 fn caret_at(&self, rows: &[Row]) -> (usize, usize) {
236 for (index, row) in rows.iter().enumerate() {
237 let next_same_line = rows
238 .get(index + 1)
239 .is_some_and(|next| next.line == row.line);
240 let Pos { line, at } = self.buffer.caret();
241 let within = at >= row.start && (at < row.end || !next_same_line);
242 if row.line == line && within {
243 let text = self.buffer.line(row.line);
244 return (index, cells(&text[row.start..at]));
245 }
246 }
247 (0, 0)
248 }
249
250 fn follow(&self, width: usize, height: usize) {
252 let rows = self.rows(width);
253 let (row, _) = self.caret_at(&rows);
254 let top = self.top.get();
255 if row < top {
256 self.top.set(row);
257 } else if row >= top + height {
258 self.top.set(row + 1 - height);
259 }
260 }
261}
262
263impl Component for TextArea {
264 type Output = String;
265
266 fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<String> {
267 let height = self.shown_rows(context.height);
268 if let Event::Paste(pasted) = event {
269 self.buffer.insert(pasted);
270 self.follow(context.width, height);
271 return Flow::Continue;
272 }
273 let Some(key) = event.key() else {
274 if let Event::Resize { .. } = event {
275 self.follow(context.width, height);
276 }
277 return Flow::Continue;
278 };
279 let keymap = self.keymap();
280 let action = keymap.action(key).or_else(|| {
283 let typing = matches!(key.code, KeyCode::Char(_));
284 (!typing && key.modifiers != crate::event::Modifiers::NONE)
285 .then(|| keymap.action(Key::new(key.code)))
286 .flatten()
287 });
288 match action.map(str::to_string).as_deref() {
289 Some("submit") => {
290 let text = self.text();
291 self.answer = Some(Some(text.clone()));
292 return Flow::Done(text);
293 }
294 Some("cancel") => {
295 self.answer = Some(None);
296 return Flow::Cancel;
297 }
298 Some("newline") => self.buffer.insert("\n"),
299 Some("indent") => self.buffer.insert(" "),
300 Some("backspace") => self.buffer.backspace(),
301 Some("delete") => self.buffer.delete(),
302 Some("left") => self.buffer.left(false),
303 Some("right") => self.buffer.right(false),
304 Some("up") => self.buffer.vertical(-1, false),
305 Some("down") => self.buffer.vertical(1, false),
306 Some("page-up") => self.buffer.vertical(-(height as isize), false),
307 Some("page-down") => self.buffer.vertical(height as isize, false),
308 Some("top") => self.buffer.top(false),
309 Some("bottom") => self.buffer.bottom(false),
310 Some("home") => self.buffer.home(false),
311 Some("end") => self.buffer.end(false),
312 Some("delete-to-start") => self.buffer.delete_to_start(),
313 Some("delete-to-end") => self.buffer.delete_to_end(),
314 Some("delete-word") => self.buffer.delete_word(),
315 Some(action)
318 if crate::edit_keys::perform(
319 &mut self.buffer,
320 action,
321 crate::edit_keys::Act {
322 page: height,
323 ..crate::edit_keys::Act::default()
324 },
325 ) == crate::edit_keys::Outcome::Used => {}
326 _ => match key.code {
327 KeyCode::Char(c) if !key.modifiers.ctrl && !key.modifiers.alt => {
328 let mut buffer = [0; 4];
329 self.buffer.insert(c.encode_utf8(&mut buffer));
330 }
331 _ => return Flow::Ignored,
332 },
333 }
334 self.follow(context.width, height);
335 Flow::Continue
336 }
337
338 fn keymap(&self) -> Keymap {
339 let mut keymap = Keymap::new("textarea")
340 .bind("submit", [self.submit], "submit")
341 .bind("cancel", keys("escape"), "cancel")
342 .bind("newline", keys("enter"), "new line")
343 .bind("indent", keys("tab"), "indent")
344 .bind("up", keys("up"), "line up")
345 .bind("down", keys("down"), "line down")
346 .bind("left", keys("left"), "move left")
347 .bind("right", keys("right"), "move right")
348 .bind("page-up", keys("pageup"), "page up")
349 .bind("page-down", keys("pagedown"), "page down")
350 .bind("home", keys("home ctrl+a"), "start of the line")
351 .bind("end", keys("end ctrl+e"), "end of the line")
352 .bind("top", keys("ctrl+home"), "start of the text")
353 .bind("bottom", keys("ctrl+end"), "end of the text")
354 .bind(
355 "delete-to-start",
356 keys("ctrl+u"),
357 "delete to the line's start",
358 )
359 .bind("delete-to-end", keys("ctrl+k"), "delete to the line's end")
360 .bind("delete-word", keys("ctrl+w"), "delete a word")
361 .bind("backspace", keys("backspace"), "delete back")
362 .bind("delete", keys("delete"), "delete forward");
363 for (action, description) in [
365 ("word-left", "word left"),
366 ("word-right", "word right"),
367 ("paragraph-up", "paragraph up"),
368 ("paragraph-down", "paragraph down"),
369 ("undo", "undo"),
370 ("redo", "redo"),
371 ("delete-next-word", "delete a word forward"),
372 ("kill-to-end", "kill to the line's end"),
373 ("kill-to-start", "kill to the line's start"),
374 ("kill-word", "kill a word back"),
375 ("kill-next-word", "kill a word forward"),
376 ("kill-previous-word", "kill to the word's start"),
377 ("yank", "yank the last kill"),
378 ("yank-pop", "swap the yank for an older kill"),
379 ] {
380 keymap = keymap.bind(action, [], description);
381 }
382 keymap.extend(self.rebound.clone());
383 keymap
384 }
385
386 fn render(&self, context: &Context<'_>) -> View {
387 let width = context.width;
388 let theme = &self.theme;
389 let mut header = question(theme, &self.prompt);
390 if let Some(answer) = &self.answer {
391 header.push(match answer {
392 Some(answer) => {
393 let mut lines = answer.lines();
394 let first = lines.next().unwrap_or("").to_string();
395 let more = lines.count();
396 if more > 0 {
397 text(format!("{first} โฆ (+{more} lines)"), &theme.answer)
398 } else {
399 text(first, &theme.answer)
400 }
401 }
402 None => text("cancelled", &theme.hint),
403 });
404 return View::new(vec![fit(header, width)]);
405 }
406 let mut lines = vec![fit(header, width)];
407 let rows = self.rows(width);
408 let (caret_row, caret_column) = self.caret_at(&rows);
409 let height = self.shown_rows(context.height);
410 let top = self.top.get().min(rows.len().saturating_sub(1));
411 let gutter = self.gutter();
412 let empty = self.buffer.is_empty();
413 for index in top..top + height {
414 let mut line: Vec<Segment> = Vec::new();
415 match rows.get(index) {
416 Some(row) => {
417 let number = if self.line_numbers && row.first {
418 format!("{:>w$} โ ", row.line + 1, w = gutter - 3)
419 } else if self.line_numbers {
420 format!("{} โ ", " ".repeat(gutter - 3))
421 } else {
422 "โ ".to_string()
423 };
424 line.push(text(number, &theme.border));
425 if empty {
426 if let Some(placeholder) = &self.placeholder {
427 line.push(text(placeholder.clone(), &theme.hint));
428 }
429 } else {
430 let piece = &self.buffer.line(row.line)[row.start..row.end];
431 line.push(plain(piece.replace('\t', &" ".repeat(TAB))));
432 }
433 }
434 None => line.push(text(format!("{}~", " ".repeat(gutter - 2)), &theme.border)),
435 }
436 lines.push(fit(line, width));
437 }
438 let mut hint = String::from(" ");
439 if let Some(limit) = self.buffer.limit() {
440 hint.push_str(&format!("{}/{limit} ยท ", self.count()));
441 }
442 hint.push_str(&format!(
443 "enter new line ยท {} submit ยท esc cancel",
444 self.keymap()
445 .key("submit")
446 .map_or_else(|| "-".to_string(), |key| key.to_string())
447 ));
448 lines.push(fit(vec![text(hint, &theme.hint)], width));
449 let view = View::new(lines);
450 if caret_row >= top && caret_row < top + height {
451 view.with_cursor(
452 1 + caret_row - top,
453 (gutter + caret_column).min(width.saturating_sub(1)),
454 )
455 } else {
456 view
457 }
458 }
459
460 fn default_value(&self) -> Option<String> {
461 Some(self.text()).filter(|text| !text.is_empty())
462 }
463
464 fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<String>, NotInteractive> {
468 io.write(&format!("{} (end with Ctrl+D):\n", self.prompt));
469 let mut read: Vec<String> = Vec::new();
470 let mut total = 0usize;
471 while let Some(line) = io.read_line() {
472 total += length(&line) + 1;
473 read.push(clean(&line));
474 if self.buffer.limit().is_some_and(|limit| total > limit) {
475 break;
476 }
477 }
478 if read.is_empty() {
479 return Ok(Some(self.text()));
480 }
481 let text = read.join("\n");
482 Ok(Some(match self.buffer.limit() {
483 Some(limit) => TextArea::truncate(&text, limit).to_string(),
484 None => text,
485 }))
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn wraps_long_lines_one_cell_short() {
495 let area = TextArea::new("Notes").value("abcdefghij\nxy");
496 let rows = area.rows(12);
498 let pieces: Vec<&str> = rows
499 .iter()
500 .map(|row| &area.buffer.line(row.line)[row.start..row.end])
501 .collect();
502 assert_eq!(pieces, ["abcdefghi", "j", "xy"]);
503 assert!(rows[0].first && !rows[1].first && rows[2].first);
504 assert_eq!(area.caret_at(&rows), (2, 2));
505 }
506}