1use std::collections::HashMap;
7use std::path::Path;
8
9use crate::key::{KeyCode, KeyEvent, KeyModifiers};
10
11pub fn byte_at(buf: &str, cursor: usize) -> usize {
13 buf.char_indices()
14 .nth(cursor)
15 .map(|(i, _)| i)
16 .unwrap_or(buf.len())
17}
18
19fn word_start(buf: &str, cursor: usize) -> usize {
21 let chars: Vec<char> = buf.chars().collect();
22 let mut c = cursor.min(chars.len());
23 while c > 0 && chars[c - 1].is_whitespace() {
24 c -= 1;
25 }
26 while c > 0 && !chars[c - 1].is_whitespace() {
27 c -= 1;
28 }
29 c
30}
31
32fn word_end(buf: &str, cursor: usize) -> usize {
34 let chars: Vec<char> = buf.chars().collect();
35 let mut c = cursor.min(chars.len());
36 while c < chars.len() && chars[c].is_whitespace() {
37 c += 1;
38 }
39 while c < chars.len() && !chars[c].is_whitespace() {
40 c += 1;
41 }
42 c
43}
44
45#[derive(Clone, Copy, PartialEq, Eq, Debug)]
47pub enum Edit {
48 Edited,
50 Cancel,
52 Submit,
54 Complete,
56 History(bool),
58 Ignored,
60}
61
62#[derive(Clone, Debug, Default)]
64pub struct LineEdit {
65 pub buf: String,
66 pub cursor: usize,
67 hist_pos: Option<usize>,
69 stash: String,
72}
73
74impl LineEdit {
75 pub fn new(prefill: String) -> LineEdit {
77 let cursor = prefill.chars().count();
78 LineEdit {
79 buf: prefill,
80 cursor,
81 hist_pos: None,
82 stash: String::new(),
83 }
84 }
85
86 pub fn set(&mut self, text: &str) {
88 self.buf = text.to_string();
89 self.cursor = self.buf.chars().count();
90 }
91
92 pub fn key(&mut self, key: KeyEvent) -> Edit {
94 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
95 let alt = key.modifiers.contains(KeyModifiers::ALT);
96 let buf = &mut self.buf;
97 let cursor = &mut self.cursor;
98 match key.code {
99 KeyCode::Esc => return Edit::Cancel,
100 KeyCode::Char('g') if ctrl => return Edit::Cancel,
101 KeyCode::Char('b') if alt => *cursor = word_start(buf, *cursor),
104 KeyCode::Char('f') if alt => *cursor = word_end(buf, *cursor),
105 KeyCode::Char('d') if alt => {
106 let end = word_end(buf, *cursor);
107 let (from, to) = (byte_at(buf, *cursor), byte_at(buf, end));
108 buf.replace_range(from..to, "");
109 }
110 KeyCode::Enter => return Edit::Submit,
111 KeyCode::Tab => return Edit::Complete,
112 KeyCode::Up => return Edit::History(true),
113 KeyCode::Down => return Edit::History(false),
114 KeyCode::Left => *cursor = cursor.saturating_sub(1),
115 KeyCode::Right => *cursor = (*cursor + 1).min(buf.chars().count()),
116 KeyCode::Home => *cursor = 0,
117 KeyCode::End => *cursor = buf.chars().count(),
118 KeyCode::Char('a') if ctrl => *cursor = 0,
119 KeyCode::Char('e') if ctrl => *cursor = buf.chars().count(),
120 KeyCode::Backspace => {
121 if *cursor > 0 {
122 buf.remove(byte_at(buf, *cursor - 1));
123 *cursor -= 1;
124 }
125 }
126 KeyCode::Delete => {
127 if *cursor < buf.chars().count() {
128 buf.remove(byte_at(buf, *cursor));
129 }
130 }
131 KeyCode::Char('d') if ctrl => {
132 if *cursor < buf.chars().count() {
133 buf.remove(byte_at(buf, *cursor));
134 }
135 }
136 KeyCode::Char('u') if ctrl => {
137 let i = byte_at(buf, *cursor);
139 buf.replace_range(..i, "");
140 *cursor = 0;
141 }
142 KeyCode::Char('k') if ctrl => {
143 let i = byte_at(buf, *cursor);
144 buf.truncate(i);
145 }
146 KeyCode::Char('w') if ctrl => {
147 let c = word_start(buf, *cursor);
149 let (start, end) = (byte_at(buf, c), byte_at(buf, *cursor));
150 buf.replace_range(start..end, "");
151 *cursor = c;
152 }
153 KeyCode::Char(c) if !ctrl && !alt => {
155 buf.insert(byte_at(buf, *cursor), c);
156 *cursor += 1;
157 }
158 _ => return Edit::Ignored,
159 }
160 Edit::Edited
161 }
162
163 pub fn history_step(&mut self, bucket: &[String], older: bool) {
167 if bucket.is_empty() {
168 return;
169 }
170 let next = match (self.hist_pos, older) {
171 (None, true) => Some(0),
172 (None, false) => return,
173 (Some(p), true) => Some((p + 1).min(bucket.len() - 1)),
174 (Some(0), false) => None,
175 (Some(p), false) => Some(p - 1),
176 };
177 match next {
178 Some(p) => {
179 if self.hist_pos.is_none() {
180 self.stash = self.buf.clone();
181 }
182 self.buf = bucket[p].clone();
183 }
184 None => self.buf = self.stash.clone(),
185 }
186 self.hist_pos = next;
187 self.cursor = self.buf.chars().count();
188 }
189}
190
191pub const KNOWN_BUCKETS: &[&str] = &[
194 "mailbox", "pattern", "address", "command", "other", "file", "notmuch",
195];
196
197#[derive(Default, Debug)]
199pub struct History {
200 buckets: HashMap<&'static str, Vec<String>>,
201}
202
203impl History {
204 pub fn get(&self, bucket: &str) -> &[String] {
205 self.buckets.get(bucket).map(Vec::as_slice).unwrap_or(&[])
206 }
207
208 pub fn push(&mut self, bucket: &'static str, entry: &str) {
210 let entry = entry.trim();
211 if entry.is_empty() {
212 return;
213 }
214 let list = self.buckets.entry(bucket).or_default();
215 list.retain(|e| e != entry);
216 list.insert(0, entry.to_string());
217 list.truncate(100);
218 }
219
220 pub fn load(&mut self, path: &Path) {
224 let Ok(text) = std::fs::read_to_string(path) else {
225 return;
226 };
227 for line in text.lines() {
228 if let Some((bucket, entry)) = line.split_once('\t')
229 && !entry.is_empty()
230 && let Some(known) = KNOWN_BUCKETS.iter().find(|b| **b == bucket)
231 {
232 let list = self.buckets.entry(known).or_default();
233 if list.len() < 100 && !list.iter().any(|e| e == entry) {
236 list.push(entry.to_string());
237 }
238 }
239 }
240 }
241
242 pub fn save(&self, path: &Path, cap: usize) {
246 let mut out = String::new();
247 for bucket in KNOWN_BUCKETS {
248 let Some(entries) = self.buckets.get(bucket) else {
249 continue;
250 };
251 for entry in entries.iter().take(cap) {
252 if !entry.contains(['\t', '\n']) {
255 out += &format!("{bucket}\t{entry}\n");
256 }
257 }
258 }
259 let _ = rmut_core::scratch::save_private(path, out.as_bytes());
260 }
261}
262
263#[derive(Clone, Debug)]
268pub struct Complete {
269 start: usize,
270 tail: String,
271 candidates: Vec<String>,
272 index: usize,
273 expect: String,
274}
275
276impl Complete {
277 pub fn token(buf: &str, cursor: usize, address_list: bool) -> (usize, String) {
282 let head = &buf[..byte_at(buf, cursor)];
283 let after_comma = if address_list {
284 head.rfind(',').map(|i| i + 1).unwrap_or(0)
285 } else {
286 0
287 };
288 let start =
289 after_comma + head[after_comma..].len() - head[after_comma..].trim_start().len();
290 (start, head[start..].trim().to_string())
291 }
292
293 fn fill(&self, buf: &str) -> (String, usize) {
295 let head = format!("{}{}", &buf[..self.start], self.candidates[self.index]);
296 let cursor = head.chars().count();
297 (head + &self.tail, cursor)
298 }
299
300 pub fn cycle(&mut self, buf: &str) -> Option<((String, usize), String)> {
304 if self.expect != buf || self.candidates.len() < 2 {
305 return None;
306 }
307 self.index = (self.index + 1) % self.candidates.len();
308 let next = self.fill(buf);
309 self.expect = next.0.clone();
310 let note = format!("match {}/{}", self.index + 1, self.candidates.len());
311 Some((next, note))
312 }
313
314 pub fn first(
320 buf: &str,
321 cursor: usize,
322 start: usize,
323 candidates: Vec<String>,
324 ) -> (Complete, (String, usize), Option<String>) {
325 let mut state = Complete {
326 start,
327 tail: buf[byte_at(buf, cursor)..].to_string(),
328 candidates,
329 index: 0,
330 expect: String::new(),
331 };
332 let next = state.fill(buf);
333 state.expect = next.0.clone();
334 let note = (state.candidates.len() > 1)
335 .then(|| format!("match 1/{} (Tab cycles)", state.candidates.len()));
336 (state, next, note)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 fn press(edit: &mut LineEdit, code: KeyCode) -> Edit {
345 edit.key(KeyEvent::new(code, KeyModifiers::NONE))
346 }
347
348 fn ctrl(edit: &mut LineEdit, c: char) -> Edit {
349 edit.key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL))
350 }
351
352 #[test]
353 fn ctrl_g_cancels_and_alt_moves_by_words() {
354 let alt =
355 |e: &mut LineEdit, c: char| e.key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::ALT));
356 let mut e = LineEdit::new("one two three".into());
357 assert_eq!(ctrl(&mut e, 'g'), Edit::Cancel);
358 alt(&mut e, 'b');
359 assert_eq!(e.cursor, 9);
360 alt(&mut e, 'b');
361 assert_eq!(e.cursor, 4);
362 alt(&mut e, 'f');
363 assert_eq!(e.cursor, 7);
364 alt(&mut e, 'd');
365 assert_eq!(e.buf, "one two");
366 e.cursor = 0;
367 alt(&mut e, 'd');
368 assert_eq!(e.buf, " two");
369 assert_eq!(alt(&mut e, 'x'), Edit::Ignored);
371 assert_eq!(e.buf, " two");
372 }
373
374 #[test]
375 fn editing_keys_move_insert_and_kill() {
376 let mut e = LineEdit::new("ab".into());
377 assert_eq!(press(&mut e, KeyCode::Char('c')), Edit::Edited);
378 assert_eq!(e.buf, "abc");
379 press(&mut e, KeyCode::Left);
380 press(&mut e, KeyCode::Char('x'));
381 assert_eq!(e.buf, "abxc");
382 press(&mut e, KeyCode::Backspace);
383 assert_eq!(e.buf, "abc");
384 ctrl(&mut e, 'a');
385 press(&mut e, KeyCode::Delete);
386 assert_eq!(e.buf, "bc");
387 ctrl(&mut e, 'e');
388 ctrl(&mut e, 'u');
389 assert_eq!(e.buf, "");
390 e.set("two words here");
391 ctrl(&mut e, 'w');
392 assert_eq!(e.buf, "two words ");
393 press(&mut e, KeyCode::Home);
394 ctrl(&mut e, 'k');
395 assert_eq!(e.buf, "");
396 assert_eq!(press(&mut e, KeyCode::Enter), Edit::Submit);
397 assert_eq!(press(&mut e, KeyCode::Esc), Edit::Cancel);
398 assert_eq!(press(&mut e, KeyCode::Tab), Edit::Complete);
399 assert_eq!(press(&mut e, KeyCode::Up), Edit::History(true));
400 assert_eq!(press(&mut e, KeyCode::PageUp), Edit::Ignored);
401 }
402
403 #[test]
404 fn cursor_is_in_chars_not_bytes() {
405 let mut e = LineEdit::new("héllo".into());
406 press(&mut e, KeyCode::Left);
407 press(&mut e, KeyCode::Left);
408 press(&mut e, KeyCode::Backspace);
409 assert_eq!(e.buf, "hélo");
410 assert_eq!(byte_at("héllo", 2), 3);
411 }
412
413 #[test]
414 fn history_steps_and_restores_the_stash() {
415 let bucket = vec!["newest".to_string(), "older".to_string()];
416 let mut e = LineEdit::new("typing".into());
417 e.history_step(&bucket, true);
418 assert_eq!(e.buf, "newest");
419 e.history_step(&bucket, true);
420 assert_eq!(e.buf, "older");
421 e.history_step(&bucket, true);
422 assert_eq!(e.buf, "older", "stays at the oldest");
423 e.history_step(&bucket, false);
424 e.history_step(&bucket, false);
425 assert_eq!(e.buf, "typing", "back past the newest restores the line");
426 e.history_step(&[], true);
427 assert_eq!(e.buf, "typing");
428 }
429
430 #[test]
431 fn history_dedupes_and_round_trips_through_a_file() {
432 let mut h = History::default();
433 h.push("pattern", "~f jane");
434 h.push("pattern", "~N");
435 h.push("pattern", "~f jane");
436 h.push("pattern", " ");
437 assert_eq!(h.get("pattern"), ["~f jane", "~N"]);
438 let dir = tempfile::tempdir().unwrap();
439 let path = dir.path().join("sub").join("history");
440 h.push("command", "set beep");
441 h.save(&path, 1);
442 let mut back = History::default();
443 back.load(&path);
444 assert_eq!(back.get("pattern"), ["~f jane"], "capped at 1");
445 assert_eq!(back.get("command"), ["set beep"]);
446 assert!(back.get("bogus").is_empty());
447 }
448
449 #[test]
450 fn completion_tokens_and_cycling() {
451 assert_eq!(Complete::token("jane, bo", 8, true), (6, "bo".into()));
452 assert_eq!(Complete::token(" =arch", 7, false), (2, "=arch".into()));
453 let (mut c, line, note) = Complete::first(
454 "jane, bo",
455 8,
456 6,
457 vec!["bob@example.com".into(), "bonnie@example.com".into()],
458 );
459 assert_eq!(line, ("jane, bob@example.com".to_string(), 21));
460 assert_eq!(note.as_deref(), Some("match 1/2 (Tab cycles)"));
461 let (line, note) = c.cycle(&line.0).unwrap();
462 assert_eq!(line.0, "jane, bonnie@example.com");
463 assert_eq!(note, "match 2/2");
464 assert!(c.cycle("edited since").is_none());
465 let (mut one, line, note) = Complete::first("x", 1, 0, vec!["xy".into()]);
466 assert_eq!((line.0.as_str(), note), ("xy", None));
467 assert!(one.cycle("xy").is_none());
468 }
469
470 #[test]
471 fn completion_works_on_the_token_before_the_cursor() {
472 let buf = "ja, carol@example.com";
475 let (start, word) = Complete::token(buf, 2, true);
476 assert_eq!((start, word.as_str()), (0, "ja"));
477 let (_, (line, cursor), _) =
478 Complete::first(buf, 2, start, vec!["jane@example.com".into()]);
479 assert_eq!(line, "jane@example.com, carol@example.com");
480 assert_eq!(cursor, 16);
481 }
482}