1use std::collections::VecDeque;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::thread::JoinHandle;
8use std::time::Duration;
9
10use bevy_ecs::prelude::*;
11use bevy_ecs::schedule::ScheduleLabel;
12use bevy_ecs::system::SystemParam;
13use bevy_log::error;
14use crossbeam_channel::Receiver;
15use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
16use ratatui::layout::Rect;
17
18use super::clipboard;
19use super::complete::{self, Completion, CompletionKind, CompletionSource};
20use super::editor::{Editor, MainEditor};
21use super::history::{PromptHistory, Sent};
22use super::keys::{self, Found, InputAction, KeyBinding, KeyFocus, KeyPress, KeyPressed, KeyScope};
23use super::pane::{MainPane, PaneState};
24use super::panel::{TerminalSize, TuiPanel};
25use super::pick::{self, Choices, PickerInput, Showing};
26use super::render::selectable;
27use super::terminal::TerminalFocus;
28use super::view::{ItemRow, TranscriptItem, TranscriptView, TuiView};
29use rig_ecs::agent::{ActiveTurn, Notice, StopTurn};
30use rig_ecs::inbox::{Deliver, DeliveryMode};
31use rig_ecs::tasks::Wake;
32use rig_harness::main_agent::ShownAgent;
33use rig_harness::pick::{Chosen, Pick};
34use rig_harness::prelude::{Processes, Scratch};
35
36const PAGE: usize = 10;
38
39const POLL: Duration = Duration::from_millis(100);
42
43const KEYS_PER_FRAME: usize = 1024;
47
48#[derive(Resource, Clone, Debug, Default, PartialEq, Eq)]
83pub struct PendingInput(pub Vec<KeyInput>);
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub enum KeyInput {
88 Event(Event),
91 Action(InputAction),
93}
94
95#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
102pub struct TuiKeys;
103
104#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub enum InputSystems {
108 Filter,
111 Dispatch,
113}
114
115#[derive(Resource, Clone, Debug, Default, PartialEq, Eq)]
120pub struct InputContext {
121 scope: KeyScope,
122 editor: Option<Entity>,
123 completion: Option<Completion>,
124 typed: Option<(CompletionKind, String)>,
125 answering: Option<Entity>,
126 picking: Option<Entity>,
127 selection: Option<usize>,
128}
129
130impl InputContext {
131 pub fn scope(&self) -> KeyScope {
136 self.scope
137 }
138
139 pub fn editor(&self) -> Option<Entity> {
142 self.editor
143 }
144
145 pub fn completion(&self) -> Option<&Completion> {
147 self.completion.as_ref()
148 }
149
150 pub fn typed(&self) -> Option<(CompletionKind, &str)> {
155 let (kind, text) = self.typed.as_ref()?;
156 Some((*kind, text))
157 }
158
159 pub fn answering(&self) -> Option<Entity> {
162 self.answering
163 }
164
165 pub fn picking(&self) -> Option<Entity> {
168 self.picking
169 }
170
171 pub fn selection(&self) -> Option<usize> {
176 self.selection
177 }
178}
179
180#[derive(Resource)]
182pub(crate) struct TerminalInput {
183 events: Receiver<Event>,
184 stop: Arc<AtomicBool>,
186 thread: Option<JoinHandle<()>>,
187}
188
189impl TerminalInput {
190 pub(crate) fn start(wake: Wake) -> std::io::Result<Self> {
192 let (sender, events) = crossbeam_channel::unbounded();
193 let stop = Arc::new(AtomicBool::new(false));
194 let shared = Arc::clone(&stop);
195 let thread = std::thread::Builder::new()
196 .name("rig-harness-input".to_owned())
197 .spawn(move || {
198 while !shared.load(Ordering::Relaxed) {
199 let event = match event::poll(POLL) {
200 Ok(false) => continue,
201 Ok(true) => event::read(),
202 Err(failure) => Err(failure),
203 };
204 match event {
205 Ok(event) => {
206 if sender.send(event).is_err() {
207 return;
208 }
209 wake.wake();
210 }
211 Err(failure) => {
212 error!("could not read the terminal: {failure}");
213 return;
214 }
215 }
216 }
217 })?;
218 Ok(Self {
219 events,
220 stop,
221 thread: Some(thread),
222 })
223 }
224
225 pub(crate) fn stop(mut self) {
228 self.stop.store(true, Ordering::Relaxed);
229 if let Some(thread) = self.thread.take() {
230 thread.join().ok();
231 }
232 }
233}
234
235impl Drop for TerminalInput {
236 fn drop(&mut self) {
237 self.stop.store(true, Ordering::Relaxed);
238 }
239}
240
241pub(crate) fn receive_events(input: Res<TerminalInput>, mut events: ResMut<PendingInput>) {
243 let mut read = input.events.try_iter().peekable();
244 if read.peek().is_some() {
245 events.0.extend(read.map(KeyInput::Event));
246 }
247}
248
249pub(crate) fn clear_events(mut events: ResMut<PendingInput>) {
252 if !events.0.is_empty() {
253 events.0.clear();
254 }
255}
256
257pub(crate) fn run_keys(world: &mut World) {
263 if world
264 .get_resource::<PendingInput>()
265 .is_none_or(|events| events.0.is_empty())
266 {
267 return;
268 }
269 let mut events = world.resource_mut::<PendingInput>();
270 let mut queue: VecDeque<KeyInput> = std::mem::take(&mut events.0).into();
271 let mut done = 0;
272 while let Some(item) = queue.pop_front() {
273 if done == KEYS_PER_FRAME {
274 queue.push_front(item);
275 world.resource_mut::<PendingInput>().0 = queue.into();
276 if let Some(wake) = world.get_resource::<Wake>() {
277 wake.wake();
278 }
279 return;
280 }
281 done += 1;
282 world.resource_mut::<PendingInput>().0.push(item);
283 let text = main_text(world);
284 world.run_schedule(TuiKeys);
285 let typed = main_text(world) != text;
286 if typed {
287 let mut main = world.query_filtered::<&mut TranscriptView, With<MainPane>>();
288 for mut transcript in main.iter_mut(world) {
289 if transcript.selected.is_some() {
290 transcript.selected = None;
291 }
292 }
293 }
294 let left = std::mem::take(&mut world.resource_mut::<PendingInput>().0);
295 for item in left.into_iter().rev() {
296 queue.push_front(item);
297 }
298 }
299}
300
301fn main_text(world: &mut World) -> Option<String> {
303 let mut main = world.query_filtered::<&Editor, With<MainEditor>>();
304 let text = main.single(world).ok().map(Editor::text);
305 text.map(str::to_owned)
306}
307
308pub(crate) fn note_input_context(
311 (view, main): (Res<TuiView>, Query<&TranscriptView, With<MainPane>>),
312 showing: Query<(Entity, &PickerInput)>,
313 focus: Res<KeyFocus>,
314 panels: Query<(), With<TuiPanel>>,
315 choices: Choices,
316 (editors, sources): (
317 Query<(Entity, &Editor, Has<MainEditor>)>,
318 Query<&CompletionSource>,
319 ),
320 mut state: ResMut<InputContext>,
321) {
322 let main_editor = editors.iter().find(|(.., main)| *main);
323 let typed = main_editor.and_then(|(_, input, _)| complete::typed(input, &sources));
324 let focused = focus.0.filter(|panel| editors.contains(*panel));
325 let editor = focused.or(main_editor.map(|(entity, ..)| entity));
326 let scope = match focus.0 {
327 Some(panel) if panels.contains(panel) => KeyScope::Panel(panel),
328 _ if reading(&main) => KeyScope::Transcript,
329 _ => KeyScope::Input,
330 };
331 let choosing = showing.iter().next();
332 let answering = choosing.filter(|(_, c)| c.draft.is_some()).map(|(e, _)| e);
333 let picker = choosing.filter(|(_, c)| c.draft.is_none());
334 let picking = picker.map(|(e, _)| e);
335 let selection = picker.and_then(|(entity, choosing)| {
336 let shown = choices.shown(entity)?;
337 match shown.rows(&choosing.filter).get(choosing.selected) {
338 Some(&pick::Row::Item(index)) => Some(index),
339 _ => None,
340 }
341 });
342 state.set_if_neq(InputContext {
343 scope,
344 editor,
345 completion: view.completion.clone(),
346 typed,
347 answering,
348 picking,
349 selection,
350 });
351}
352
353fn reading(main: &Query<&TranscriptView, With<MainPane>>) -> bool {
356 main.iter().any(|view| view.selected.is_some())
357}
358
359#[derive(SystemParam)]
361pub(crate) struct Keymap<'w, 's> {
362 bindings: Query<'w, 's, (Entity, &'static KeyBinding, Option<&'static InputAction>)>,
363 focus: ResMut<'w, KeyFocus>,
364 panels: Query<'w, 's, (), With<TuiPanel>>,
365}
366
367impl Keymap<'_, '_> {
368 fn scope(&mut self, reading: bool) -> KeyScope {
372 let unfocused = if reading {
373 KeyScope::Transcript
374 } else {
375 KeyScope::Input
376 };
377 match self.focus.0 {
378 Some(panel) if self.panels.contains(panel) => KeyScope::Panel(panel),
379 Some(_) => {
380 self.focus.0 = None;
381 unfocused
382 }
383 None => unfocused,
384 }
385 }
386}
387
388pub(crate) struct Context<'a, 'w, 's> {
390 agent: Option<Entity>,
392 busy: bool,
393 scratch: Option<&'a Scratch>,
394 processes: Option<&'a Processes>,
395 wake: &'a Wake,
396 history: &'a [String],
398 choices: &'a Choices<'w, 's>,
400}
401
402pub(crate) struct Target<'a> {
407 view: &'a mut TuiView,
408 input: &'a mut Editor,
409 pane: Option<(Mut<'a, TranscriptView>, Mut<'a, PaneState>)>,
410}
411
412type Panes<'w, 's> = Query<
414 'w,
415 's,
416 (
417 Entity,
418 &'static mut TranscriptView,
419 &'static mut PaneState,
420 Has<MainPane>,
421 ),
422>;
423
424type Editors<'w, 's> = Query<'w, 's, (Entity, &'static mut Editor, Has<MainEditor>)>;
426
427fn keyed_editor(editors: &Editors, focus: Option<Entity>) -> Option<Entity> {
430 let focused = focus.filter(|entity| editors.contains(*entity));
431 let main = || editors.iter().find(|(.., main)| *main);
432 focused.or_else(|| main().map(|(entity, ..)| entity))
433}
434
435fn keyed_pane(panes: &Panes, focus: Option<Entity>) -> Option<Entity> {
437 let focused = focus.filter(|entity| panes.contains(*entity));
438 let main = || {
439 panes
440 .iter()
441 .find(|(.., main)| *main)
442 .map(|(entity, ..)| entity)
443 };
444 focused.or_else(main)
445}
446
447pub(crate) fn read_input(
449 mut events: ResMut<PendingInput>,
450 (mut view, mut panes, shown): (ResMut<TuiView>, Panes, Res<ShownAgent>),
451 (mut editors, mut showing): (Editors, Showing),
452 mut keymap: Keymap,
453 choices: Choices,
454 agents: Query<Has<ActiveTurn>>,
455 (scratch, processes, wake, history): (
456 Option<Res<Scratch>>,
457 Option<Res<Processes>>,
458 Res<Wake>,
459 Res<PromptHistory>,
460 ),
461 (mut screen, mut focus): (ResMut<TerminalSize>, ResMut<TerminalFocus>),
462 mut commands: Commands,
463) {
464 if events.0.is_empty() {
467 return;
468 }
469 let busy = shown
470 .0
471 .and_then(|agent| agents.get(agent).ok())
472 .unwrap_or(false);
473 let context = Context {
474 agent: shown.0,
475 busy,
476 scratch: scratch.as_deref(),
477 processes: processes.as_deref(),
478 wake: &wake,
479 history: &history.entries,
480 choices: &choices,
481 };
482 let mut none = Editor::default();
484 for item in std::mem::take(&mut events.0) {
485 let editor = keyed_editor(&editors, keymap.focus.0);
486 let in_panel = editor.is_some() && editor == keymap.focus.0;
487 let mut found = editor.and_then(|editor| editors.get_mut(editor).ok());
488 let input: &mut Editor = match &mut found {
489 Some((_, editor, _)) => editor,
490 None => &mut none,
491 };
492 let pane = keyed_pane(&panes, keymap.focus.0);
493 let reading = panes
494 .iter()
495 .any(|(_, view, _, main)| main && view.selected.is_some());
496 let event = match item {
497 KeyInput::Event(event) => event,
498 KeyInput::Action(action) => {
499 let pane = pane.and_then(|pane| panes.get_mut(pane).ok());
500 let mut target = Target {
501 view: &mut view,
502 input,
503 pane: pane.map(|(_, view, state, _)| (view, state)),
504 };
505 act(action, &mut target, (&context, &showing), &mut commands);
506 continue;
507 }
508 };
509 match event {
510 Event::Key(key) if key.kind == KeyEventKind::Release => {
511 released(key, (shown.0, reading), &mut keymap, &mut commands);
512 }
513 Event::Key(key) => {
514 let picking = showing.iter_mut().find(|(_, c)| c.draft.is_none());
516 if let Some((entity, mut choosing)) = picking {
517 let shown = choices.shown(entity);
518 let chosen =
519 shown.and_then(|shown| pick::choice_key(key, &mut choosing, &shown));
520 if let Some(chosen) = chosen {
521 commands.trigger(Pick::person(entity, chosen));
522 view.queue.retain(|&queued| queued != entity);
524 pick::show_first(&view, &choices, (&showing, input), &mut commands);
525 }
526 continue;
527 }
528 let pane = pane.and_then(|pane| panes.get_mut(pane).ok());
529 let mut target = Target {
530 view: &mut view,
531 input,
532 pane: pane.map(|(_, view, state, _)| (view, state)),
533 };
534 pressed(
535 key,
536 (reading, in_panel),
537 &mut target,
538 &mut keymap,
539 (&context, &showing),
540 &mut commands,
541 );
542 }
543 Event::Paste(text) => match showing.iter_mut().find(|(_, c)| c.draft.is_none()) {
546 Some((_, mut choosing)) => {
547 choosing
548 .filter
549 .push_str(text.lines().next().unwrap_or_default());
550 choosing.selected = 0;
551 }
552 None if !in_panel && matches!(keymap.scope(reading), KeyScope::Panel(_)) => {}
553 None => match clipboard::dropped_image(&text) {
555 Some(path) => clipboard::type_path(input, &path),
556 None => input.insert(&text),
557 },
558 },
559 Event::Resize(width, height) => {
560 screen.set_if_neq(TerminalSize(Rect::new(0, 0, width, height)));
561 }
562 Event::FocusGained => {
563 focus.set_if_neq(TerminalFocus(Some(true)));
564 }
565 Event::FocusLost => {
566 focus.set_if_neq(TerminalFocus(Some(false)));
567 }
568 Event::Mouse(_) => {}
569 }
570 }
571}
572
573fn pressed(
579 key: KeyEvent,
580 (reading, in_panel): (bool, bool),
581 target: &mut Target,
582 keymap: &mut Keymap,
583 (context, showing): (&Context, &Showing),
584 commands: &mut Commands,
585) {
586 let Target { view, input, .. } = target;
587 let press = KeyPress::of(&key);
588 let scope = keymap.scope(reading);
589 let bindings: Vec<(Entity, &KeyBinding)> = keymap
590 .bindings
591 .iter()
592 .map(|(entity, binding, _)| (entity, binding))
593 .collect();
594 view.pending.push(press);
595 let found = keys::find(&bindings, &view.pending, scope);
596 if found == Found::Partial {
597 return;
598 }
599 let sequence = std::mem::take(&mut view.pending).len() > 1;
600 let Found::Binding(entity) = found else {
601 if sequence {
602 return;
603 }
604 match scope {
605 KeyScope::Panel(_) if press.code == KeyCode::Esc => keymap.focus.0 = None,
606 KeyScope::Panel(_) if !in_panel => {}
607 _ => {
608 if let Some(character) = press.typed() {
609 input.insert_char(character);
610 }
611 }
612 }
613 return;
614 };
615 let Ok((_, binding, action)) = keymap.bindings.get(entity) else {
616 return;
617 };
618 if binding.releases && key.kind == KeyEventKind::Repeat {
620 return;
621 }
622 match action {
623 Some(action) => act(*action, target, (context, showing), commands),
624 None => commands.trigger(KeyPressed {
625 entity,
626 agent: context.agent,
627 released: false,
628 }),
629 }
630}
631
632fn released(
635 key: KeyEvent,
636 (agent, reading): (Option<Entity>, bool),
637 keymap: &mut Keymap,
638 commands: &mut Commands,
639) {
640 let press = KeyPress::of(&key);
641 let scope = keymap.scope(reading);
642 let released = keymap.bindings.iter().find(|(_, binding, _)| {
643 binding.releases
644 && binding.keys.presses() == [press]
645 && scope.applying().any(|applying| applying == binding.scope)
646 });
647 if let Some((entity, ..)) = released {
648 commands.trigger(KeyPressed {
649 entity,
650 agent,
651 released: true,
652 });
653 }
654}
655
656fn act(
660 action: InputAction,
661 target: &mut Target,
662 (context, showing): (&Context, &Showing),
663 commands: &mut Commands,
664) {
665 use InputAction as Do;
666 let Target { view, input, pane } = target;
667 let editing = showing
669 .iter()
670 .find(|(_, choosing)| choosing.draft.is_some());
671 let editing = editing.map(|(entity, _)| entity);
672 match (action, editing) {
673 (Do::Send, Some(ask)) if view.completion.as_ref().is_none_or(|c| c.is_exact()) => {
674 let answer = Chosen::text(input.take());
675 return close(ask, Some(answer), target, (context, showing), commands);
676 }
677 (Do::Stop, Some(ask)) if view.completion.is_none() => {
678 return close(ask, None, target, (context, showing), commands);
679 }
680 _ => {}
681 }
682 match action {
683 Do::Clear => input.clear(),
684 Do::NewLine => input.insert_char('\n'),
685 Do::LineStart => input.home(),
686 Do::LineEnd => input.end(),
687 Do::Left => input.left(),
688 Do::Right => input.right(),
689 Do::WordLeft => input.word_left(),
690 Do::WordRight => input.word_right(),
691 Do::Backspace => input.backspace(),
692 Do::Delete => input.delete(),
693 Do::DeleteWordBefore => input.delete_word_before(),
694 Do::DeleteWordAfter => input.delete_word_after(),
695 Do::DeleteToLineStart => input.delete_to_line_start(),
696 Do::DeleteToLineEnd => input.delete_to_line_end(),
697 Do::LineUp => input.up(context.history),
698 Do::LineDown => input.down(context.history),
699 Do::Up => match &mut view.completion {
700 Some(completion) => completion.select(-1),
701 None => input.up(context.history),
702 },
703 Do::Down => match &mut view.completion {
704 Some(completion) => completion.select(1),
705 None => input.down(context.history),
706 },
707 Do::ScrollUp | Do::ScrollDown | Do::PageUp | Do::PageDown => {
708 if let Some((_, state)) = pane {
709 let page = PAGE.min(state.page());
710 match action {
711 Do::ScrollUp => state.scroll.up(1),
712 Do::ScrollDown => state.scroll.down(1),
713 Do::PageUp => state.scroll.up(page),
714 _ => state.scroll.down(page),
715 }
716 }
717 }
718 Do::Tab if view.completion.is_none() && context.busy && editing.is_none() => {
719 let state = pane.as_mut().map(|(_, state)| &mut **state);
720 send(view, input, state, context.agent, commands, true);
721 }
722 Do::Tab => accept_completion(view, input),
723 Do::Send => {
724 if view
726 .completion
727 .as_ref()
728 .is_some_and(|completion| !completion.is_exact())
729 {
730 accept_completion(view, input);
731 return;
732 }
733 if input.continue_line() {
734 return;
735 }
736 let state = pane.as_mut().map(|(_, state)| &mut **state);
737 send(view, input, state, context.agent, commands, false);
738 }
739 Do::Stop => match view.completion.take() {
740 Some(completion) => view.dismissed = Some(completion.start),
741 None => {
742 if let Some(entity) = context.agent {
743 commands.trigger(StopTurn::new(entity));
744 }
745 }
746 },
747 Do::PasteImage => {
748 let Some(images) = context.scratch.and_then(Scratch::images) else {
749 let why = "This session keeps no files to paste an image into.";
750 commands.write_message(Notice::info(context.agent, why));
751 return;
752 };
753 let launcher = context
754 .processes
755 .map(Processes::launcher)
756 .unwrap_or_default();
757 clipboard::paste_image(images, launcher, context.wake, commands);
758 }
759 Do::SelectUp | Do::SelectDown => {
760 if let Some((transcript, state)) = pane {
761 select(action == Do::SelectUp, transcript, state);
762 }
763 }
764 Do::Deselect => {
765 if let Some((transcript, _)) = pane
766 && transcript.selected.is_some()
767 {
768 transcript.selected = None;
769 }
770 }
771 Do::AnswerInline(number) => {
772 answer_inline(usize::from(number), target, (context, showing), commands);
773 }
774 Do::DismissInline => {
775 if let Some(&ask) = view.inline.first() {
776 view.inline.remove(0);
777 commands.trigger(Pick::person(ask, None));
778 }
779 }
780 }
781}
782
783fn close(
786 ask: Entity,
787 chosen: Option<Chosen>,
788 target: &mut Target,
789 (context, showing): (&Context, &Showing),
790 commands: &mut Commands,
791) {
792 commands.trigger(Pick::person(ask, chosen));
793 target.view.queue.retain(|&queued| queued != ask);
794 pick::show_first(
795 target.view,
796 context.choices,
797 (showing, target.input),
798 commands,
799 );
800}
801
802fn select(up: bool, transcript: &mut Mut<TranscriptView>, state: &mut Mut<PaneState>) {
806 let items = state
807 .transcript
808 .marks_in(&state.parts)
809 .map(|(item, _)| item);
810 let items: Vec<&TranscriptItem> = items.filter(|item| selectable(item)).collect();
811 let at = (transcript.selected.as_ref())
812 .and_then(|selected| items.iter().position(|item| *item == selected));
813 let next = match (at, up) {
814 (None, true) => items.len().checked_sub(1),
815 (None, false) => None,
816 (Some(at), true) => Some(at.saturating_sub(1)),
817 (Some(at), false) => Some(at + 1),
818 };
819 let selected = next
820 .and_then(|next| items.get(next))
821 .map(|item| (*item).clone());
822 if let Some(item) = &selected {
823 state.scroll.to(item.clone(), ItemRow::Reveal);
824 }
825 if transcript.selected != selected {
826 transcript.selected = selected;
827 }
828}
829
830fn answer_inline(
834 number: usize,
835 target: &mut Target,
836 (context, showing): (&Context, &Showing),
837 commands: &mut Commands,
838) {
839 let Some(&ask) = target.view.inline.first() else {
840 return;
841 };
842 let Some(shown) = context.choices.shown(ask) else {
843 return;
844 };
845 let Some(index) = number.checked_sub(1) else {
846 return;
847 };
848 if index < shown.items.len() {
849 target.view.inline.remove(0);
850 commands.trigger(Pick::person(ask, Some(Chosen::item(index))));
851 } else if index == shown.items.len() && shown.free_text && showing.is_empty() {
852 target.view.inline.remove(0);
853 target.view.queue.insert(0, ask);
854 pick::show_first(
855 target.view,
856 context.choices,
857 (showing, target.input),
858 commands,
859 );
860 }
861}
862
863pub(crate) fn send(
869 view: &mut TuiView,
870 input: &mut Editor,
871 pane: Option<&mut PaneState>,
872 agent: Option<Entity>,
873 commands: &mut Commands,
874 queue: bool,
875) {
876 let Some(entity) = agent else {
877 return;
878 };
879 if input.text().trim().is_empty() {
880 return;
881 }
882 let text = input.take();
883 commands.write_message(Sent(text.clone()));
884 if let Some(pane) = pane {
885 pane.scroll.follow();
886 }
887 let mode = if queue {
888 DeliveryMode::Queue
889 } else {
890 DeliveryMode::Steer
891 };
892 let refused = view.refused.take().as_ref() == Some(&text);
893 let typed = Deliver::new(entity, text, mode);
894 commands.trigger(if refused { typed.literal() } else { typed });
895}
896
897fn accept_completion(view: &mut TuiView, input: &mut Editor) {
899 let Some(completion) = view.completion.take() else {
900 return;
901 };
902 if let Some(text) = completion.chosen() {
903 input.replace_before_cursor(completion.start, &text);
904 }
905}
906
907#[cfg(test)]
908mod tests;