Skip to content
Merged
28 changes: 15 additions & 13 deletions examples/persistent_menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
// menu; without quick completions, the menu closes once the line is empty.

use reedline::{
default_emacs_keybindings, ColumnarMenu, Completer, DefaultPrompt, Emacs, KeyCode,
KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span,
Suggestion,
default_emacs_keybindings, ColumnarMenu, Completer, CompletionResult, DefaultPrompt, Emacs,
KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal,
Span, Suggestion,
};
use std::io;

Expand All @@ -24,17 +24,19 @@ struct PrefixCompleter {
}

impl Completer for PrefixCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
fn complete(&mut self, line: &str, pos: usize) -> CompletionResult {
let filter = &line[..pos];
self.commands
.iter()
.filter(|command| command.starts_with(filter))
.map(|command| Suggestion {
value: command.clone(),
span: Span::new(0, pos),
..Suggestion::default()
})
.collect()
CompletionResult::fresh(
self.commands
.iter()
.filter(|command| command.starts_with(filter))
.map(|command| Suggestion {
value: command.clone(),
span: Span::new(0, pos),
..Suggestion::default()
})
.collect::<Vec<_>>(),
)
}
}

Expand Down
126 changes: 98 additions & 28 deletions src/completion/base.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use nu_ansi_term::Style;
use std::ops::Range;
use std::sync::Arc;

/// A span of source code, with positions in bytes
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
Expand All @@ -11,6 +12,12 @@ pub struct Span {
pub end: usize,
}

/// A shared, immutable list of completion suggestions.
///
/// Held behind an [`Arc`] so a completer that caches results can hand the same
/// list to reedline on every keystroke, without massive penalty
pub type Suggestions = Arc<[Suggestion]>;

impl Span {
/// Creates a new `Span` from start and end inputs.
/// The end parameter must be greater than or equal to the start parameter.
Expand All @@ -27,27 +34,100 @@ impl Span {
}
}

/// The outcome of a [`Completer::complete`] request.
///
/// A synchronous completer only ever produces [`Fresh`](Self::Fresh). An
/// asynchronous completer that computes in the background reports its progress
/// through this type.
#[derive(Debug, Clone)]
pub enum CompletionResult {
/// Final, authoritative results. No further computation is in flight.
Fresh(Suggestions),
/// Best-effort results to show in the moment; a fresh computation is still running and
/// will replace these once it finishes.
Stale(Suggestions),
/// No results are available yet; a computation is spinning in the background.
Pending,
}

impl CompletionResult {
/// Wrap authoritative results.
pub fn fresh(suggestions: impl Into<Suggestions>) -> Self {
CompletionResult::Fresh(suggestions.into())
}

/// Best-effort fallback while an authoritative result is still computing:
/// [`Stale`](Self::Stale) when there is something to show now, else
/// [`Pending`](Self::Pending).
pub fn stale_or_pending(fallback: Suggestions) -> Self {
if fallback.is_empty() {
CompletionResult::Pending
} else {
CompletionResult::Stale(fallback)
}
}

/// Borrow the suggestions this result carries (empty for [`Pending`](Self::Pending)).
pub fn suggestions(&self) -> &[Suggestion] {
match self {
CompletionResult::Fresh(values) | CompletionResult::Stale(values) => values,
CompletionResult::Pending => &[],
}
}

/// Move the shared suggestion list out of the result without copying.
///
/// Returns `None` for [`Pending`](Self::Pending) nothing is settled yet, so
/// callers should keep whatever they are already displaying.
pub fn into_shared(self) -> Option<Suggestions> {
match self {
CompletionResult::Fresh(values) | CompletionResult::Stale(values) => Some(values),
CompletionResult::Pending => None,
}
}

/// Whether there is nothing to show yet because a computation is in flight.
/// When `true`, callers should preserve any results already displayed.
pub fn is_pending(&self) -> bool {
matches!(self, CompletionResult::Pending)
}
}

/// Vitality of a completer's background work, grabbed by the engine once per
/// event-loop iteration. It tells the engine whether to keep polling for
/// input (rather than blocking) and when a finished result is ready to display.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionStatus {
/// No background completion is in flight.... the engine may block on input.
Idle,
/// A background completion is still running... the engine should keep polling.
Pending,
/// The latest background completion just finished! Its results are now
/// available and any active menu should be refreshed.
Ready,
}

/// A trait that defines how to convert some text and a position to a list of potential completions in that position.
/// The text could be a part of the whole line, and the position is the index of the end of the text in the original line.
pub trait Completer {
/// the action that will take the line and position and convert it to a vector of completions, which include the
/// span to replace and the contents of that replacement
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion>;
fn complete(&mut self, line: &str, pos: usize) -> CompletionResult;

/// same as [`Completer::complete`] but it will return a vector of ranges of the strings
/// the suggestions are based on
/// same as [`Completer::complete`] but it will also return a vector of ranges
/// of the strings the suggestions are based on
fn complete_with_base_ranges(
&mut self,
line: &str,
pos: usize,
) -> (Vec<Suggestion>, Vec<Range<usize>>) {
) -> (CompletionResult, Vec<Range<usize>>) {
let result = self.complete(line, pos);
let mut ranges = vec![];
let suggestions = self.complete(line, pos);
for suggestion in &suggestions {
for suggestion in result.suggestions() {
ranges.push(suggestion.span.start..suggestion.span.end);
}
ranges.dedup();
(suggestions, ranges)
(result, ranges)
}

/// action that will return a partial section of available completions
Expand All @@ -59,38 +139,28 @@ pub trait Completer {
pos: usize,
start: usize,
offset: usize,
) -> Vec<Suggestion> {
) -> Suggestions {
self.complete(line, pos)
.into_iter()
.suggestions()
.iter()
.skip(start)
.take(offset)
.cloned()
.collect()
}

/// number of available completions
fn total_completions(&mut self, line: &str, pos: usize) -> usize {
self.complete(line, pos).len()
}

/// Returns `true` while completions are being computed in the background.
///
/// When this returns `true` the engine switches to polling mode so it can
/// call [`check_pending`](Self::check_pending) periodically without
/// stopping on keyboard input. The default implementation always returns
/// `false` (sync completer).
fn has_pending(&mut self) -> bool {
false
self.complete(line, pos).suggestions().len()
}

/// Checks whether a background completion has finished.
/// Poll the completer's background work.
///
/// Returns `true` once the results have been stored in the completer's
/// internal cache so that the next call to [`complete`](Self::complete)
/// will return them immediately. The engine calls this while a menu is
/// active and [`has_pending`](Self::has_pending) has returned `true`.
/// The default implementation always returns `false`.
fn check_pending(&mut self) -> bool {
false
/// Called once per event-loop iteration by the engine. Synchronous
/// completers use the default, which always reports
/// [`CompletionStatus::Idle`].
fn poll_completion(&mut self) -> CompletionStatus {
CompletionStatus::Idle
}
}

Expand Down
20 changes: 10 additions & 10 deletions src/completion/default.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Completer, Span, Suggestion};
use crate::{Completer, CompletionResult, Span, Suggestion};
use std::{
collections::{BTreeMap, BTreeSet},
str::Chars,
Expand Down Expand Up @@ -53,22 +53,22 @@ impl Completer for DefaultCompleter {
/// let mut completions = DefaultCompleter::default();
/// completions.insert(vec!["batman","robin","batmobile","batcave","robber"].iter().map(|s| s.to_string()).collect());
/// assert_eq!(
/// completions.complete("bat",3),
/// completions.complete("bat",3).suggestions(),
/// vec![
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
/// ]);
///
/// assert_eq!(
/// completions.complete("to the\r\nbat",11),
/// completions.complete("to the\r\nbat",11).suggestions(),
/// vec![
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
/// ]);
/// ```
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
fn complete(&mut self, line: &str, pos: usize) -> CompletionResult {
let mut span_line_whitespaces = 0;
let mut completions = vec![];
// Trimming in case someone passes in text containing stuff after the cursor, if
Expand Down Expand Up @@ -121,7 +121,7 @@ impl Completer for DefaultCompleter {
}
}
completions.dedup();
completions
CompletionResult::fresh(completions)
}
}

Expand Down Expand Up @@ -182,13 +182,13 @@ impl DefaultCompleter {
/// let mut completions = DefaultCompleter::default();
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
/// assert_eq!(
/// completions.complete("te",2),
/// completions.complete("te",2).suggestions(),
/// vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()}]);
///
/// let mut completions = DefaultCompleter::with_inclusions(&['-', '_']);
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
/// assert_eq!(
/// completions.complete("te",2),
/// completions.complete("te",2).suggestions(),
/// vec![
/// Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()},
/// Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()},
Expand Down Expand Up @@ -376,7 +376,7 @@ mod tests {
);

assert_eq!(
completions.complete("n", 3),
completions.complete("n", 3).suggestions(),
[
Suggestion {
value: "null".into(),
Expand Down Expand Up @@ -421,9 +421,9 @@ mod tests {

let buffer = "this is t";

let (suggestions, ranges) = completions.complete_with_base_ranges(buffer, 9);
let (result, ranges) = completions.complete_with_base_ranges(buffer, 9);
assert_eq!(
suggestions,
result.suggestions(),
[
Suggestion {
value: "test".into(),
Expand Down
19 changes: 11 additions & 8 deletions src/completion/history.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::{collections::HashSet, ops::Deref};

use crate::{
history::SearchQuery, menu_functions::parse_selection_char, Completer, History, HistoryItem,
Result, Span, Suggestion,
history::SearchQuery, menu_functions::parse_selection_char, Completer, CompletionResult,
History, HistoryItem, Result, Span, Suggestion,
};

const SELECTION_CHAR: char = '!';
Expand All @@ -27,13 +27,14 @@ fn search_unique(
}

impl Completer for HistoryCompleter<'_> {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
match search_unique(self, line) {
fn complete(&mut self, line: &str, pos: usize) -> CompletionResult {
let suggestions = match search_unique(self, line) {
Err(_) => vec![],
Ok(search_results) => search_results
.map(|value| self.create_suggestion(line, pos, value.command_line.deref()))
.collect(),
}
};
CompletionResult::fresh(suggestions)
}

// TODO: Implement `fn partial_complete()`
Expand Down Expand Up @@ -110,7 +111,8 @@ mod tests {
let input = "git s";
let mut sut = HistoryCompleter::new(&history);

let actual = sut.complete(input, input.len());
let completion = sut.complete(input, input.len());
let actual = completion.suggestions();
let num_completions = sut.total_completions(input, input.len());

assert_eq!(actual[0].value, "git status", "it was the last command");
Expand Down Expand Up @@ -149,8 +151,9 @@ mod tests {
let mut sut = HistoryCompleter::new(&history);
let actual: Vec<String> = sut
.complete(line, line.len())
.into_iter()
.map(|suggestion| suggestion.value)
.suggestions()
.iter()
.map(|suggestion| suggestion.value.clone())
.collect();
assert_eq!(actual, expected);
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion src/completion/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ mod base;
mod default;
pub(crate) mod history;

pub use base::{Completer, Span, Suggestion};
pub use base::{Completer, CompletionResult, CompletionStatus, Span, Suggestion, Suggestions};
pub use default::DefaultCompleter;
Loading
Loading