input.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. use std::io::{self, IsTerminal, Write};
  2. use crossterm::cursor::{MoveDown, MoveToColumn, MoveUp};
  3. use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
  4. use crossterm::queue;
  5. use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
  6. #[derive(Debug, Clone, PartialEq, Eq)]
  7. pub struct InputBuffer {
  8. buffer: String,
  9. cursor: usize,
  10. }
  11. impl InputBuffer {
  12. #[must_use]
  13. pub fn new() -> Self {
  14. Self {
  15. buffer: String::new(),
  16. cursor: 0,
  17. }
  18. }
  19. pub fn insert(&mut self, ch: char) {
  20. self.buffer.insert(self.cursor, ch);
  21. self.cursor += ch.len_utf8();
  22. }
  23. pub fn insert_newline(&mut self) {
  24. self.insert('\n');
  25. }
  26. pub fn backspace(&mut self) {
  27. if self.cursor == 0 {
  28. return;
  29. }
  30. let previous = self.buffer[..self.cursor]
  31. .char_indices()
  32. .last()
  33. .map_or(0, |(idx, _)| idx);
  34. self.buffer.drain(previous..self.cursor);
  35. self.cursor = previous;
  36. }
  37. pub fn move_left(&mut self) {
  38. if self.cursor == 0 {
  39. return;
  40. }
  41. self.cursor = self.buffer[..self.cursor]
  42. .char_indices()
  43. .last()
  44. .map_or(0, |(idx, _)| idx);
  45. }
  46. pub fn move_right(&mut self) {
  47. if self.cursor >= self.buffer.len() {
  48. return;
  49. }
  50. if let Some(next) = self.buffer[self.cursor..].chars().next() {
  51. self.cursor += next.len_utf8();
  52. }
  53. }
  54. pub fn move_home(&mut self) {
  55. self.cursor = 0;
  56. }
  57. pub fn move_end(&mut self) {
  58. self.cursor = self.buffer.len();
  59. }
  60. #[must_use]
  61. pub fn as_str(&self) -> &str {
  62. &self.buffer
  63. }
  64. #[cfg(test)]
  65. #[must_use]
  66. pub fn cursor(&self) -> usize {
  67. self.cursor
  68. }
  69. pub fn clear(&mut self) {
  70. self.buffer.clear();
  71. self.cursor = 0;
  72. }
  73. pub fn replace(&mut self, value: impl Into<String>) {
  74. self.buffer = value.into();
  75. self.cursor = self.buffer.len();
  76. }
  77. #[must_use]
  78. fn current_command_prefix(&self) -> Option<&str> {
  79. if self.cursor != self.buffer.len() {
  80. return None;
  81. }
  82. let prefix = &self.buffer[..self.cursor];
  83. if prefix.contains(char::is_whitespace) || !prefix.starts_with('/') {
  84. return None;
  85. }
  86. Some(prefix)
  87. }
  88. pub fn complete_slash_command(&mut self, candidates: &[String]) -> bool {
  89. let Some(prefix) = self.current_command_prefix() else {
  90. return false;
  91. };
  92. let matches = candidates
  93. .iter()
  94. .filter(|candidate| candidate.starts_with(prefix))
  95. .map(String::as_str)
  96. .collect::<Vec<_>>();
  97. if matches.is_empty() {
  98. return false;
  99. }
  100. let replacement = longest_common_prefix(&matches);
  101. if replacement == prefix {
  102. return false;
  103. }
  104. self.replace(replacement);
  105. true
  106. }
  107. }
  108. #[derive(Debug, Clone, PartialEq, Eq)]
  109. pub struct RenderedBuffer {
  110. lines: Vec<String>,
  111. cursor_row: u16,
  112. cursor_col: u16,
  113. }
  114. impl RenderedBuffer {
  115. #[must_use]
  116. pub fn line_count(&self) -> usize {
  117. self.lines.len()
  118. }
  119. fn write(&self, out: &mut impl Write) -> io::Result<()> {
  120. for (index, line) in self.lines.iter().enumerate() {
  121. if index > 0 {
  122. writeln!(out)?;
  123. }
  124. write!(out, "{line}")?;
  125. }
  126. Ok(())
  127. }
  128. #[cfg(test)]
  129. #[must_use]
  130. pub fn lines(&self) -> &[String] {
  131. &self.lines
  132. }
  133. #[cfg(test)]
  134. #[must_use]
  135. pub fn cursor_position(&self) -> (u16, u16) {
  136. (self.cursor_row, self.cursor_col)
  137. }
  138. }
  139. #[derive(Debug, Clone, PartialEq, Eq)]
  140. pub enum ReadOutcome {
  141. Submit(String),
  142. Cancel,
  143. Exit,
  144. }
  145. pub struct LineEditor {
  146. prompt: String,
  147. continuation_prompt: String,
  148. history: Vec<String>,
  149. history_index: Option<usize>,
  150. draft: Option<String>,
  151. completions: Vec<String>,
  152. }
  153. impl LineEditor {
  154. #[must_use]
  155. pub fn new(prompt: impl Into<String>, completions: Vec<String>) -> Self {
  156. Self {
  157. prompt: prompt.into(),
  158. continuation_prompt: String::from("> "),
  159. history: Vec::new(),
  160. history_index: None,
  161. draft: None,
  162. completions,
  163. }
  164. }
  165. pub fn push_history(&mut self, entry: impl Into<String>) {
  166. let entry = entry.into();
  167. if entry.trim().is_empty() {
  168. return;
  169. }
  170. self.history.push(entry);
  171. self.history_index = None;
  172. self.draft = None;
  173. }
  174. pub fn read_line(&mut self) -> io::Result<ReadOutcome> {
  175. if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
  176. return self.read_line_fallback();
  177. }
  178. enable_raw_mode()?;
  179. let mut stdout = io::stdout();
  180. let mut input = InputBuffer::new();
  181. let mut rendered_lines = 1usize;
  182. self.redraw(&mut stdout, &input, rendered_lines)?;
  183. loop {
  184. let event = event::read()?;
  185. if let Event::Key(key) = event {
  186. match self.handle_key(key, &mut input) {
  187. EditorAction::Continue => {
  188. rendered_lines = self.redraw(&mut stdout, &input, rendered_lines)?;
  189. }
  190. EditorAction::Submit => {
  191. disable_raw_mode()?;
  192. writeln!(stdout)?;
  193. self.history_index = None;
  194. self.draft = None;
  195. return Ok(ReadOutcome::Submit(input.as_str().to_owned()));
  196. }
  197. EditorAction::Cancel => {
  198. disable_raw_mode()?;
  199. writeln!(stdout)?;
  200. self.history_index = None;
  201. self.draft = None;
  202. return Ok(ReadOutcome::Cancel);
  203. }
  204. EditorAction::Exit => {
  205. disable_raw_mode()?;
  206. writeln!(stdout)?;
  207. self.history_index = None;
  208. self.draft = None;
  209. return Ok(ReadOutcome::Exit);
  210. }
  211. }
  212. }
  213. }
  214. }
  215. fn read_line_fallback(&self) -> io::Result<ReadOutcome> {
  216. let mut stdout = io::stdout();
  217. write!(stdout, "{}", self.prompt)?;
  218. stdout.flush()?;
  219. let mut buffer = String::new();
  220. let bytes_read = io::stdin().read_line(&mut buffer)?;
  221. if bytes_read == 0 {
  222. return Ok(ReadOutcome::Exit);
  223. }
  224. while matches!(buffer.chars().last(), Some('\n' | '\r')) {
  225. buffer.pop();
  226. }
  227. Ok(ReadOutcome::Submit(buffer))
  228. }
  229. #[allow(clippy::too_many_lines)]
  230. fn handle_key(&mut self, key: KeyEvent, input: &mut InputBuffer) -> EditorAction {
  231. match key {
  232. KeyEvent {
  233. code: KeyCode::Char('c'),
  234. modifiers,
  235. ..
  236. } if modifiers.contains(KeyModifiers::CONTROL) => {
  237. if input.as_str().is_empty() {
  238. EditorAction::Exit
  239. } else {
  240. input.clear();
  241. self.history_index = None;
  242. self.draft = None;
  243. EditorAction::Cancel
  244. }
  245. }
  246. KeyEvent {
  247. code: KeyCode::Char('j'),
  248. modifiers,
  249. ..
  250. } if modifiers.contains(KeyModifiers::CONTROL) => {
  251. input.insert_newline();
  252. EditorAction::Continue
  253. }
  254. KeyEvent {
  255. code: KeyCode::Enter,
  256. modifiers,
  257. ..
  258. } if modifiers.contains(KeyModifiers::SHIFT) => {
  259. input.insert_newline();
  260. EditorAction::Continue
  261. }
  262. KeyEvent {
  263. code: KeyCode::Enter,
  264. ..
  265. } => EditorAction::Submit,
  266. KeyEvent {
  267. code: KeyCode::Backspace,
  268. ..
  269. } => {
  270. input.backspace();
  271. EditorAction::Continue
  272. }
  273. KeyEvent {
  274. code: KeyCode::Left,
  275. ..
  276. } => {
  277. input.move_left();
  278. EditorAction::Continue
  279. }
  280. KeyEvent {
  281. code: KeyCode::Right,
  282. ..
  283. } => {
  284. input.move_right();
  285. EditorAction::Continue
  286. }
  287. KeyEvent {
  288. code: KeyCode::Up, ..
  289. } => {
  290. self.navigate_history_up(input);
  291. EditorAction::Continue
  292. }
  293. KeyEvent {
  294. code: KeyCode::Down,
  295. ..
  296. } => {
  297. self.navigate_history_down(input);
  298. EditorAction::Continue
  299. }
  300. KeyEvent {
  301. code: KeyCode::Tab, ..
  302. } => {
  303. input.complete_slash_command(&self.completions);
  304. EditorAction::Continue
  305. }
  306. KeyEvent {
  307. code: KeyCode::Home,
  308. ..
  309. } => {
  310. input.move_home();
  311. EditorAction::Continue
  312. }
  313. KeyEvent {
  314. code: KeyCode::End, ..
  315. } => {
  316. input.move_end();
  317. EditorAction::Continue
  318. }
  319. KeyEvent {
  320. code: KeyCode::Esc, ..
  321. } => {
  322. input.clear();
  323. self.history_index = None;
  324. self.draft = None;
  325. EditorAction::Cancel
  326. }
  327. KeyEvent {
  328. code: KeyCode::Char(ch),
  329. modifiers,
  330. ..
  331. } if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT => {
  332. input.insert(ch);
  333. self.history_index = None;
  334. self.draft = None;
  335. EditorAction::Continue
  336. }
  337. _ => EditorAction::Continue,
  338. }
  339. }
  340. fn navigate_history_up(&mut self, input: &mut InputBuffer) {
  341. if self.history.is_empty() {
  342. return;
  343. }
  344. match self.history_index {
  345. Some(0) => {}
  346. Some(index) => {
  347. let next_index = index - 1;
  348. input.replace(self.history[next_index].clone());
  349. self.history_index = Some(next_index);
  350. }
  351. None => {
  352. self.draft = Some(input.as_str().to_owned());
  353. let next_index = self.history.len() - 1;
  354. input.replace(self.history[next_index].clone());
  355. self.history_index = Some(next_index);
  356. }
  357. }
  358. }
  359. fn navigate_history_down(&mut self, input: &mut InputBuffer) {
  360. let Some(index) = self.history_index else {
  361. return;
  362. };
  363. if index + 1 < self.history.len() {
  364. let next_index = index + 1;
  365. input.replace(self.history[next_index].clone());
  366. self.history_index = Some(next_index);
  367. return;
  368. }
  369. input.replace(self.draft.take().unwrap_or_default());
  370. self.history_index = None;
  371. }
  372. fn redraw(
  373. &self,
  374. out: &mut impl Write,
  375. input: &InputBuffer,
  376. previous_line_count: usize,
  377. ) -> io::Result<usize> {
  378. let rendered = render_buffer(&self.prompt, &self.continuation_prompt, input);
  379. if previous_line_count > 1 {
  380. queue!(out, MoveUp(saturating_u16(previous_line_count - 1)))?;
  381. }
  382. queue!(out, MoveToColumn(0), Clear(ClearType::FromCursorDown),)?;
  383. rendered.write(out)?;
  384. queue!(
  385. out,
  386. MoveUp(saturating_u16(rendered.line_count().saturating_sub(1))),
  387. MoveToColumn(0),
  388. )?;
  389. if rendered.cursor_row > 0 {
  390. queue!(out, MoveDown(rendered.cursor_row))?;
  391. }
  392. queue!(out, MoveToColumn(rendered.cursor_col))?;
  393. out.flush()?;
  394. Ok(rendered.line_count())
  395. }
  396. }
  397. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  398. enum EditorAction {
  399. Continue,
  400. Submit,
  401. Cancel,
  402. Exit,
  403. }
  404. #[must_use]
  405. pub fn render_buffer(
  406. prompt: &str,
  407. continuation_prompt: &str,
  408. input: &InputBuffer,
  409. ) -> RenderedBuffer {
  410. let before_cursor = &input.as_str()[..input.cursor];
  411. let cursor_row = saturating_u16(before_cursor.chars().filter(|ch| *ch == '\n').count());
  412. let cursor_line = before_cursor.rsplit('\n').next().unwrap_or_default();
  413. let cursor_prompt = if cursor_row == 0 {
  414. prompt
  415. } else {
  416. continuation_prompt
  417. };
  418. let cursor_col = saturating_u16(cursor_prompt.chars().count() + cursor_line.chars().count());
  419. let mut lines = Vec::new();
  420. for (index, line) in input.as_str().split('\n').enumerate() {
  421. let prefix = if index == 0 {
  422. prompt
  423. } else {
  424. continuation_prompt
  425. };
  426. lines.push(format!("{prefix}{line}"));
  427. }
  428. if lines.is_empty() {
  429. lines.push(prompt.to_string());
  430. }
  431. RenderedBuffer {
  432. lines,
  433. cursor_row,
  434. cursor_col,
  435. }
  436. }
  437. #[must_use]
  438. fn longest_common_prefix(values: &[&str]) -> String {
  439. let Some(first) = values.first() else {
  440. return String::new();
  441. };
  442. let mut prefix = (*first).to_string();
  443. for value in values.iter().skip(1) {
  444. while !value.starts_with(&prefix) {
  445. prefix.pop();
  446. if prefix.is_empty() {
  447. break;
  448. }
  449. }
  450. }
  451. prefix
  452. }
  453. #[must_use]
  454. fn saturating_u16(value: usize) -> u16 {
  455. u16::try_from(value).unwrap_or(u16::MAX)
  456. }
  457. #[cfg(test)]
  458. mod tests {
  459. use super::{render_buffer, InputBuffer, LineEditor};
  460. use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
  461. fn key(code: KeyCode) -> KeyEvent {
  462. KeyEvent::new(code, KeyModifiers::NONE)
  463. }
  464. #[test]
  465. fn supports_basic_line_editing() {
  466. let mut input = InputBuffer::new();
  467. input.insert('h');
  468. input.insert('i');
  469. input.move_end();
  470. input.insert_newline();
  471. input.insert('x');
  472. assert_eq!(input.as_str(), "hi\nx");
  473. assert_eq!(input.cursor(), 4);
  474. input.move_left();
  475. input.backspace();
  476. assert_eq!(input.as_str(), "hix");
  477. assert_eq!(input.cursor(), 2);
  478. }
  479. #[test]
  480. fn completes_unique_slash_command() {
  481. let mut input = InputBuffer::new();
  482. for ch in "/he".chars() {
  483. input.insert(ch);
  484. }
  485. assert!(input.complete_slash_command(&[
  486. "/help".to_string(),
  487. "/hello".to_string(),
  488. "/status".to_string(),
  489. ]));
  490. assert_eq!(input.as_str(), "/hel");
  491. assert!(input.complete_slash_command(&["/help".to_string(), "/status".to_string()]));
  492. assert_eq!(input.as_str(), "/help");
  493. }
  494. #[test]
  495. fn ignores_completion_when_prefix_is_not_a_slash_command() {
  496. let mut input = InputBuffer::new();
  497. for ch in "hello".chars() {
  498. input.insert(ch);
  499. }
  500. assert!(!input.complete_slash_command(&["/help".to_string()]));
  501. assert_eq!(input.as_str(), "hello");
  502. }
  503. #[test]
  504. fn history_navigation_restores_current_draft() {
  505. let mut editor = LineEditor::new("› ", vec![]);
  506. editor.push_history("/help");
  507. editor.push_history("status report");
  508. let mut input = InputBuffer::new();
  509. for ch in "draft".chars() {
  510. input.insert(ch);
  511. }
  512. let _ = editor.handle_key(key(KeyCode::Up), &mut input);
  513. assert_eq!(input.as_str(), "status report");
  514. let _ = editor.handle_key(key(KeyCode::Up), &mut input);
  515. assert_eq!(input.as_str(), "/help");
  516. let _ = editor.handle_key(key(KeyCode::Down), &mut input);
  517. assert_eq!(input.as_str(), "status report");
  518. let _ = editor.handle_key(key(KeyCode::Down), &mut input);
  519. assert_eq!(input.as_str(), "draft");
  520. }
  521. #[test]
  522. fn tab_key_completes_from_editor_candidates() {
  523. let mut editor = LineEditor::new(
  524. "› ",
  525. vec![
  526. "/help".to_string(),
  527. "/status".to_string(),
  528. "/session".to_string(),
  529. ],
  530. );
  531. let mut input = InputBuffer::new();
  532. for ch in "/st".chars() {
  533. input.insert(ch);
  534. }
  535. let _ = editor.handle_key(key(KeyCode::Tab), &mut input);
  536. assert_eq!(input.as_str(), "/status");
  537. }
  538. #[test]
  539. fn renders_multiline_buffers_with_continuation_prompt() {
  540. let mut input = InputBuffer::new();
  541. for ch in "hello\nworld".chars() {
  542. if ch == '\n' {
  543. input.insert_newline();
  544. } else {
  545. input.insert(ch);
  546. }
  547. }
  548. let rendered = render_buffer("› ", "> ", &input);
  549. assert_eq!(
  550. rendered.lines(),
  551. &["› hello".to_string(), "> world".to_string()]
  552. );
  553. assert_eq!(rendered.cursor_position(), (1, 7));
  554. }
  555. #[test]
  556. fn ctrl_c_exits_only_when_buffer_is_empty() {
  557. let mut editor = LineEditor::new("› ", vec![]);
  558. let mut empty = InputBuffer::new();
  559. assert!(matches!(
  560. editor.handle_key(
  561. KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
  562. &mut empty,
  563. ),
  564. super::EditorAction::Exit
  565. ));
  566. let mut filled = InputBuffer::new();
  567. filled.insert('x');
  568. assert!(matches!(
  569. editor.handle_key(
  570. KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
  571. &mut filled,
  572. ),
  573. super::EditorAction::Cancel
  574. ));
  575. assert!(filled.as_str().is_empty());
  576. }
  577. }