From faddde853ff2b10cb5b6891283b3c4d350b5f978 Mon Sep 17 00:00:00 2001 From: YizhePKU Date: Thu, 17 Oct 2024 21:17:25 +0800 Subject: [PATCH] Rework `read_line_helper()` --- src/engine.rs | 134 +++++++++++++++++++++++--------------------------- 1 file changed, 62 insertions(+), 72 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 21595214..3fd58581 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -51,11 +51,10 @@ use { // a POLL_WAIT of zero means that every single event is treated as soon as it // arrives. This doesn't allow for the possibility of more than 1 event // happening at the same time. -const POLL_WAIT: u64 = 10; -// Since a paste event is multiple Event::Key events happening at the same time, we specify -// how many events should be in the crossterm_events vector before it is considered -// a paste. 10 events in 10 milliseconds is conservative enough (unlikely somebody -// will type more than 10 characters in 10 milliseconds) +const POLL_WAIT: Duration = Duration::from_millis(100); +// Since a paste event is multiple `Event::Key` events happening at the same +// time, we specify how many events should be in the `crossterm_events` vector +// before it is considered a paste. 10 events is conservative enough. const EVENTS_THRESHOLD: usize = 10; /// Determines if inputs should be used to extend the regular line buffer, @@ -695,12 +694,7 @@ impl Reedline { self.repaint(prompt)?; - let mut crossterm_events: Vec = vec![]; - let mut reedline_events: Vec = vec![]; - loop { - let mut paste_enter_state = false; - #[cfg(feature = "external_printer")] if let Some(ref external_printer) = self.external_printer { // get messages from printer as crlf separated "lines" @@ -716,76 +710,74 @@ impl Reedline { } } - let mut latest_resize = None; - loop { - // There could be multiple events queued up! - // pasting text, resizes, blocking this thread (e.g. during debugging) - // We should be able to handle all of them as quickly as possible without causing unnecessary output steps. - if !event::poll(Duration::from_millis(POLL_WAIT))? { - break; - } - - match event::read()? { - Event::Resize(x, y) => { - latest_resize = Some((x, y)); - } - enter @ Event::Key(KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::NONE, - .. - }) => { - let enter = ReedlineRawEvent::convert_from(enter); - if let Some(enter) = enter { - crossterm_events.push(enter); - // Break early to check if the input is complete and - // can be send to the hosting application. If - // multiple complete entries are submitted, events - // are still in the crossterm queue for us to - // process. - paste_enter_state = crossterm_events.len() > EVENTS_THRESHOLD; - break; - } - } - x => { - let raw_event = ReedlineRawEvent::convert_from(x); - if let Some(evt) = raw_event { - crossterm_events.push(evt); - } - } + // Helper function that returns true if the input is complete and + // can be sent to the hosting application. + fn completed(events: &[Event]) -> bool { + if let Some(event) = events.last() { + matches!( + event, + Event::Key(KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + .. + }) + ) + } else { + false } } - if let Some((x, y)) = latest_resize { - reedline_events.push(ReedlineEvent::Resize(x, y)); + let mut events: Vec = vec![]; + + // Block until we receive an event. + events.push(crossterm::event::read()?); + + // Receive all events in the queue without blocking. Will stop when + // a line of input is completed. + while !completed(&events) && event::poll(Duration::from_millis(0))? { + events.push(crossterm::event::read()?); } - // Accelerate pasted text by fusing `EditCommand`s - // - // (Text should only be `EditCommand::InsertChar`s) - let mut last_edit_commands = None; - for event in crossterm_events.drain(..) { - match (&mut last_edit_commands, self.edit_mode.parse_event(event)) { - (None, ReedlineEvent::Edit(ec)) => { - last_edit_commands = Some(ec); - } - (None, other_event) => { - reedline_events.push(other_event); - } - (Some(ref mut last_ecs), ReedlineEvent::Edit(ec)) => { - last_ecs.extend(ec); - } - (ref mut a @ Some(_), other_event) => { - reedline_events.push(ReedlineEvent::Edit(a.take().unwrap())); + // If we believe there's text pasting or resizing going on, batch + // more events at the cost of a slight delay. + if events.len() > EVENTS_THRESHOLD + || events.iter().any(|e| matches!(e, Event::Resize(_, _))) + { + while !completed(&events) && event::poll(POLL_WAIT)? { + events.push(crossterm::event::read()?); + } + } - reedline_events.push(other_event); + // Convert `Event` into `ReedlineEvent`. Also, fuse consecutive + // `ReedlineEvent::EditCommand` into one. Also, if there're multiple + // `ReedlineEvent::Resize`, only keep the last one. + let mut reedline_events: Vec = vec![]; + let mut edits = vec![]; + let mut resize = None; + for event in events { + if let Some(event) = ReedlineRawEvent::convert_from(event) { + match self.edit_mode.parse_event(event) { + ReedlineEvent::Edit(edit) => edits.extend(edit), + ReedlineEvent::Resize(x, y) => resize = Some((x, y)), + event => { + if !edits.is_empty() { + reedline_events + .push(ReedlineEvent::Edit(std::mem::take(&mut edits))); + } + reedline_events.push(event); + } } } } - if let Some(ec) = last_edit_commands { - reedline_events.push(ReedlineEvent::Edit(ec)); + if !edits.is_empty() { + reedline_events.push(ReedlineEvent::Edit(edits)); + } + if let Some((x, y)) = resize { + reedline_events.push(ReedlineEvent::Resize(x, y)); } - for event in reedline_events.drain(..) { + // Handle reedline events. + for event in reedline_events { match self.handle_event(prompt, event)? { EventStatus::Exits(signal) => { // Check if we are merely suspended (to process an ExecuteHostCommand event) @@ -798,9 +790,7 @@ impl Reedline { return Ok(signal); } EventStatus::Handled => { - if !paste_enter_state { - self.repaint(prompt)?; - } + self.repaint(prompt)?; } EventStatus::Inapplicable => { // Nothing changed, no need to repaint