Skip to content

Commit 9cb1128

Browse files
authored
Reduce typing latency caused by POLL_WAIT (#846)
* Rework `read_line_helper()` * Add a timeout when external printers are enabled * Tiny rework for `ReedlineRawEvent` Technically not part of the PR, but let me smuggle this in real quick. * Fix format
1 parent 5dd7e0e commit 9cb1128

File tree

4 files changed

+108
-109
lines changed

4 files changed

+108
-109
lines changed

src/edit_mode/emacs.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ mod test {
194194
#[test]
195195
fn ctrl_l_leads_to_clear_screen_event() {
196196
let mut emacs = Emacs::default();
197-
let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
197+
let ctrl_l = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
198198
KeyCode::Char('l'),
199199
KeyModifiers::CONTROL,
200200
)))
@@ -214,7 +214,7 @@ mod test {
214214
);
215215

216216
let mut emacs = Emacs::new(keybindings);
217-
let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
217+
let ctrl_l = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
218218
KeyCode::Char('l'),
219219
KeyModifiers::CONTROL,
220220
)))
@@ -227,7 +227,7 @@ mod test {
227227
#[test]
228228
fn inserting_character_works() {
229229
let mut emacs = Emacs::default();
230-
let l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
230+
let l = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
231231
KeyCode::Char('l'),
232232
KeyModifiers::NONE,
233233
)))
@@ -244,7 +244,7 @@ mod test {
244244
fn inserting_capital_character_works() {
245245
let mut emacs = Emacs::default();
246246

247-
let uppercase_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
247+
let uppercase_l = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
248248
KeyCode::Char('l'),
249249
KeyModifiers::SHIFT,
250250
)))
@@ -262,7 +262,7 @@ mod test {
262262
let keybindings = Keybindings::default();
263263

264264
let mut emacs = Emacs::new(keybindings);
265-
let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
265+
let ctrl_l = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
266266
KeyCode::Char('l'),
267267
KeyModifiers::CONTROL,
268268
)))
@@ -276,7 +276,7 @@ mod test {
276276
fn inserting_capital_character_for_non_ascii_remains_as_is() {
277277
let mut emacs = Emacs::default();
278278

279-
let uppercase_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
279+
let uppercase_l = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
280280
KeyCode::Char('😀'),
281281
KeyModifiers::SHIFT,
282282
)))

src/edit_mode/vi/mod.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,9 @@ mod test {
185185
#[test]
186186
fn esc_leads_to_normal_mode_test() {
187187
let mut vi = Vi::default();
188-
let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
189-
KeyCode::Esc,
190-
KeyModifiers::NONE,
191-
)))
192-
.unwrap();
188+
let esc =
189+
ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)))
190+
.unwrap();
193191
let result = vi.parse_event(esc);
194192

195193
assert_eq!(
@@ -215,7 +213,7 @@ mod test {
215213
..Default::default()
216214
};
217215

218-
let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
216+
let esc = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
219217
KeyCode::Char('e'),
220218
KeyModifiers::NONE,
221219
)))
@@ -241,7 +239,7 @@ mod test {
241239
..Default::default()
242240
};
243241

244-
let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
242+
let esc = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
245243
KeyCode::Char('$'),
246244
KeyModifiers::SHIFT,
247245
)))
@@ -267,7 +265,7 @@ mod test {
267265
..Default::default()
268266
};
269267

270-
let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
268+
let esc = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
271269
KeyCode::Char('$'),
272270
KeyModifiers::SUPER,
273271
)))
@@ -287,7 +285,7 @@ mod test {
287285
..Default::default()
288286
};
289287

290-
let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
288+
let esc = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(
291289
KeyCode::Char('q'),
292290
KeyModifiers::NONE,
293291
)))

src/engine.rs

Lines changed: 73 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,17 @@ use {
5151
// a POLL_WAIT of zero means that every single event is treated as soon as it
5252
// arrives. This doesn't allow for the possibility of more than 1 event
5353
// happening at the same time.
54-
const POLL_WAIT: u64 = 10;
55-
// Since a paste event is multiple Event::Key events happening at the same time, we specify
56-
// how many events should be in the crossterm_events vector before it is considered
57-
// a paste. 10 events in 10 milliseconds is conservative enough (unlikely somebody
58-
// will type more than 10 characters in 10 milliseconds)
54+
const POLL_WAIT: Duration = Duration::from_millis(100);
55+
// Since a paste event is multiple `Event::Key` events happening at the same
56+
// time, we specify how many events should be in the `crossterm_events` vector
57+
// before it is considered a paste. 10 events is conservative enough.
5958
const EVENTS_THRESHOLD: usize = 10;
6059

60+
/// Maximum time Reedline will block on input before yielding control to
61+
/// external printers.
62+
#[cfg(feature = "external_printer")]
63+
const EXTERNAL_PRINTER_WAIT: Duration = Duration::from_millis(100);
64+
6165
/// Determines if inputs should be used to extend the regular line buffer,
6266
/// traverse the history in the standard prompt or edit the search string in the
6367
/// reverse search
@@ -695,12 +699,7 @@ impl Reedline {
695699

696700
self.repaint(prompt)?;
697701

698-
let mut crossterm_events: Vec<ReedlineRawEvent> = vec![];
699-
let mut reedline_events: Vec<ReedlineEvent> = vec![];
700-
701702
loop {
702-
let mut paste_enter_state = false;
703-
704703
#[cfg(feature = "external_printer")]
705704
if let Some(ref external_printer) = self.external_printer {
706705
// get messages from printer as crlf separated "lines"
@@ -716,76 +715,81 @@ impl Reedline {
716715
}
717716
}
718717

719-
let mut latest_resize = None;
720-
loop {
721-
// There could be multiple events queued up!
722-
// pasting text, resizes, blocking this thread (e.g. during debugging)
723-
// We should be able to handle all of them as quickly as possible without causing unnecessary output steps.
724-
if !event::poll(Duration::from_millis(POLL_WAIT))? {
725-
break;
718+
// Helper function that returns true if the input is complete and
719+
// can be sent to the hosting application.
720+
fn completed(events: &[Event]) -> bool {
721+
if let Some(event) = events.last() {
722+
matches!(
723+
event,
724+
Event::Key(KeyEvent {
725+
code: KeyCode::Enter,
726+
modifiers: KeyModifiers::NONE,
727+
..
728+
})
729+
)
730+
} else {
731+
false
726732
}
733+
}
727734

728-
match event::read()? {
729-
Event::Resize(x, y) => {
730-
latest_resize = Some((x, y));
731-
}
732-
enter @ Event::Key(KeyEvent {
733-
code: KeyCode::Enter,
734-
modifiers: KeyModifiers::NONE,
735-
..
736-
}) => {
737-
let enter = ReedlineRawEvent::convert_from(enter);
738-
if let Some(enter) = enter {
739-
crossterm_events.push(enter);
740-
// Break early to check if the input is complete and
741-
// can be send to the hosting application. If
742-
// multiple complete entries are submitted, events
743-
// are still in the crossterm queue for us to
744-
// process.
745-
paste_enter_state = crossterm_events.len() > EVENTS_THRESHOLD;
746-
break;
747-
}
748-
}
749-
x => {
750-
let raw_event = ReedlineRawEvent::convert_from(x);
751-
if let Some(evt) = raw_event {
752-
crossterm_events.push(evt);
753-
}
754-
}
755-
}
735+
let mut events: Vec<Event> = vec![];
736+
737+
// If the `external_printer` feature is enabled, we need to
738+
// periodically yield so that external printers get a chance to
739+
// print. Otherwise, we can just block until we receive an event.
740+
#[cfg(feature = "external_printer")]
741+
if event::poll(EXTERNAL_PRINTER_WAIT)? {
742+
events.push(crossterm::event::read()?);
756743
}
744+
#[cfg(not(feature = "external_printer"))]
745+
events.push(crossterm::event::read()?);
757746

758-
if let Some((x, y)) = latest_resize {
759-
reedline_events.push(ReedlineEvent::Resize(x, y));
747+
// Receive all events in the queue without blocking. Will stop when
748+
// a line of input is completed.
749+
while !completed(&events) && event::poll(Duration::from_millis(0))? {
750+
events.push(crossterm::event::read()?);
760751
}
761752

762-
// Accelerate pasted text by fusing `EditCommand`s
763-
//
764-
// (Text should only be `EditCommand::InsertChar`s)
765-
let mut last_edit_commands = None;
766-
for event in crossterm_events.drain(..) {
767-
match (&mut last_edit_commands, self.edit_mode.parse_event(event)) {
768-
(None, ReedlineEvent::Edit(ec)) => {
769-
last_edit_commands = Some(ec);
770-
}
771-
(None, other_event) => {
772-
reedline_events.push(other_event);
773-
}
774-
(Some(ref mut last_ecs), ReedlineEvent::Edit(ec)) => {
775-
last_ecs.extend(ec);
776-
}
777-
(ref mut a @ Some(_), other_event) => {
778-
reedline_events.push(ReedlineEvent::Edit(a.take().unwrap()));
753+
// If we believe there's text pasting or resizing going on, batch
754+
// more events at the cost of a slight delay.
755+
if events.len() > EVENTS_THRESHOLD
756+
|| events.iter().any(|e| matches!(e, Event::Resize(_, _)))
757+
{
758+
while !completed(&events) && event::poll(POLL_WAIT)? {
759+
events.push(crossterm::event::read()?);
760+
}
761+
}
779762

780-
reedline_events.push(other_event);
763+
// Convert `Event` into `ReedlineEvent`. Also, fuse consecutive
764+
// `ReedlineEvent::EditCommand` into one. Also, if there're multiple
765+
// `ReedlineEvent::Resize`, only keep the last one.
766+
let mut reedline_events: Vec<ReedlineEvent> = vec![];
767+
let mut edits = vec![];
768+
let mut resize = None;
769+
for event in events {
770+
if let Ok(event) = ReedlineRawEvent::try_from(event) {
771+
match self.edit_mode.parse_event(event) {
772+
ReedlineEvent::Edit(edit) => edits.extend(edit),
773+
ReedlineEvent::Resize(x, y) => resize = Some((x, y)),
774+
event => {
775+
if !edits.is_empty() {
776+
reedline_events
777+
.push(ReedlineEvent::Edit(std::mem::take(&mut edits)));
778+
}
779+
reedline_events.push(event);
780+
}
781781
}
782782
}
783783
}
784-
if let Some(ec) = last_edit_commands {
785-
reedline_events.push(ReedlineEvent::Edit(ec));
784+
if !edits.is_empty() {
785+
reedline_events.push(ReedlineEvent::Edit(edits));
786+
}
787+
if let Some((x, y)) = resize {
788+
reedline_events.push(ReedlineEvent::Resize(x, y));
786789
}
787790

788-
for event in reedline_events.drain(..) {
791+
// Handle reedline events.
792+
for event in reedline_events {
789793
match self.handle_event(prompt, event)? {
790794
EventStatus::Exits(signal) => {
791795
// Check if we are merely suspended (to process an ExecuteHostCommand event)
@@ -798,9 +802,7 @@ impl Reedline {
798802
return Ok(signal);
799803
}
800804
EventStatus::Handled => {
801-
if !paste_enter_state {
802-
self.repaint(prompt)?;
803-
}
805+
self.repaint(prompt)?;
804806
}
805807
EventStatus::Inapplicable => {
806808
// Nothing changed, no need to repaint

src/enums.rs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -697,41 +697,40 @@ pub(crate) enum EventStatus {
697697
Exits(Signal),
698698
}
699699

700-
/// A simple wrapper for [crossterm::event::Event]
700+
/// A wrapper for [crossterm::event::Event].
701701
///
702-
/// Which will make sure that the given event doesn't contain [KeyEventKind::Release]
703-
/// and convert from [KeyEventKind::Repeat] to [KeyEventKind::Press]
704-
pub struct ReedlineRawEvent {
705-
inner: Event,
706-
}
702+
/// It ensures that the given event doesn't contain [KeyEventKind::Release]
703+
/// (which is rejected) or [KeyEventKind::Repeat] (which is converted to
704+
/// [KeyEventKind::Press]).
705+
pub struct ReedlineRawEvent(Event);
706+
707+
impl TryFrom<Event> for ReedlineRawEvent {
708+
type Error = ();
707709

708-
impl ReedlineRawEvent {
709-
/// It will return None if `evt` is released Key.
710-
pub fn convert_from(evt: Event) -> Option<Self> {
711-
match evt {
710+
fn try_from(event: Event) -> Result<Self, Self::Error> {
711+
match event {
712712
Event::Key(KeyEvent {
713713
kind: KeyEventKind::Release,
714714
..
715-
}) => None,
715+
}) => Err(()),
716716
Event::Key(KeyEvent {
717717
code,
718718
modifiers,
719719
kind: KeyEventKind::Repeat,
720720
state,
721-
}) => Some(Self {
722-
inner: Event::Key(KeyEvent {
723-
code,
724-
modifiers,
725-
kind: KeyEventKind::Press,
726-
state,
727-
}),
728-
}),
729-
other => Some(Self { inner: other }),
721+
}) => Ok(Self(Event::Key(KeyEvent {
722+
code,
723+
modifiers,
724+
kind: KeyEventKind::Press,
725+
state,
726+
}))),
727+
other => Ok(Self(other)),
730728
}
731729
}
730+
}
732731

733-
/// Consume and get crossterm event object.
734-
pub fn into(self) -> Event {
735-
self.inner
732+
impl From<ReedlineRawEvent> for Event {
733+
fn from(event: ReedlineRawEvent) -> Self {
734+
event.0
736735
}
737736
}

0 commit comments

Comments
 (0)