// Copyright (c) 2021 Soni L.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// Documentation and comments licensed under CC BY-SA 4.0.
//! Suggestion machinery.
use ::std::future::Future;
use ::std::ops::Range;
use ::std::pin::Pin;
/// A suggested editing operation.
///
/// # Examples
///
/// ```rust
/// use ::iosonism::suggestion::Suggestion;
///
/// let s = Suggestion::new(3..3, "home".into());
/// assert_eq!(s.apply("Go ".into()), "Go home");
/// ```
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Suggestion {
range: Range<usize>,
text: String,
}
impl Suggestion {
/// Creates a new `Suggestion` for `text` for the given `range`.
pub fn new(range: Range<usize>, text: String) -> Self {
Self {
range: range,
text: text,
}
}
/// Returns the range associated with this suggestion.
pub fn get_range(&self) -> Range<usize> {
self.range.clone()
}
/// Returns the replacement text associated with this suggestion.
pub fn get_text(&self) -> &str {
&self.text
}
/// Applies this suggestion on the `input`.
///
/// # Panics
///
/// Panics if the range is outside the `input`'s bounds, or not on an UTF-8
/// boundary.
pub fn apply(&self, mut input: String) -> String {
// the use of String here actually has a performance reason:
// either you already have a String, in which case this is fast,
// or you need a String anyway, in which case it's your responsibility
// to make your &str into one.
input.replace_range(self.range.clone(), &self.text);
input
}
/// Expands this suggestion to cover the whole given range.
///
/// The text needed to cover the range will be taken from the given input.
///
/// # Panics
///
/// Panics if any range is not on an UTF-8 boundary. Panics if the given
/// range doesn't cover this suggestion's range, or is outside the input's
/// bounds.
///
/// # Examples
///
/// Basic usage:
///
/// ```rust
/// use ::iosonism::suggestion::Suggestion;
///
/// let mut s = Suggestion::new(7..7, "orld!".into());
/// s.expand("hello w", 6..7);
/// assert_eq!(s, Suggestion::new(6..7, "world!".into()));
/// ```
pub fn expand(&mut self, input: &str, range: Range<usize>) {
let input = &input[range.clone()];
self.text.insert_str(0, &input[..(self.range.start-range.start)]);
self.text.push_str(&input[(self.range.end-range.start)..]);
self.range = range;
}
}
/// Editing suggestions.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Suggestions {
// brigadier also supports "tooltips". FIXME revisit this.
range: Range<usize>,
suggestions: Vec<Suggestion>,
}
/// [private] An empty `Suggestions`.
const EMPTY: Suggestions = Suggestions { range: 0..0, suggestions: Vec::new() };
impl Suggestions {
/// Returns the range these suggestions apply to.
pub fn get_range(&self) -> Range<usize> {
self.range.clone()
}
/// Returns the suggestions.
pub fn get_list(&self) -> &[Suggestion] {
&self.suggestions
}
/// Returns the suggestions.
pub fn take_list(self) -> Vec<Suggestion> {
self.suggestions
}
/// Returns `true` if there are no suggestions.
pub fn is_empty(&self) -> bool {
self.suggestions.is_empty()
}
/// Merges together multiple `Suggestions` for the command.
pub fn merge<I: IntoIterator<Item=Suggestions>>(
command: &str,
input: I,
) -> Self {
// check if empty or single-element
let mut it = input.into_iter();
let first = it.next();
if first.is_none() {
return EMPTY
}
let first = first.unwrap();
let second = it.next();
if second.is_none() {
return first
}
let second = second.unwrap();
// combine everything back together and hope for the best
Self::create(command, first.take_list().into_iter().chain({
second.take_list()
}).chain(it.flat_map(|e| e.take_list())))
}
/// Creates a `Suggestions` from the individual suggestions for the command.
pub fn create<I: IntoIterator<Item=Suggestion>>(
command: &str,
suggestions: I,
) -> Self {
let mut suggestions: Vec<_> = suggestions.into_iter().collect();
if suggestions.is_empty() {
return EMPTY
}
// find the largest range that spans all the suggestions.
let mut start = usize::MAX;
let mut end = 0;
for suggestion in suggestions.iter() {
start = ::std::cmp::min(suggestion.get_range().start, start);
end = ::std::cmp::max(suggestion.get_range().end, end);
}
let range = start..end;
// expand all suggestions.
for suggestion in suggestions.iter_mut() {
// cloning a range is basically free.
suggestion.expand(command, range.clone());
}
// brigadier uses a HashSet first for deduplication. we currently use
// a case-sensitive sort and then do the deduplication. we can probably
// do something better here, and maybe even be language aware?
// FIXME investigate
// note that by the time we're here, all suggestions have been expanded,
// so the ranges are all the same (and no longer relevant).
suggestions.sort_unstable_by(|a, b| {
::std::cmp::Ord::cmp(a.get_text(), b.get_text())
});
suggestions.dedup();
Self {
range: range,
suggestions: suggestions,
}
}
}
/// Helper for building suggestions.
pub struct SuggestionsBuilder<'a> {
// brigadier throws an inputLowerCase/remainingLowerCase in here.
// given we're using 'a, it feels *weird* to use String with it.
// so for now we don't bother. FIXME revisit this?
// also note that brigadier exists for minecraft, whereas we're porting it
// for use with a debugger. we can assume our users are familiar with and/or
// prefer case-sensitivity.
input: &'a str,
start: usize,
remaining: &'a str,
result: Vec<Suggestion>,
}
impl<'a> SuggestionsBuilder<'a> {
/// Creates a new `SuggestionsBuilder`.
pub fn new(input: &'a str, start: usize) -> Self {
Self {
input: input,
start: start,
remaining: &input[start..],
result: Vec::new(),
}
}
/// Returns the full input.
pub fn get_input(&self) -> &'a str {
self.input
}
/// Returns the start position.
pub fn get_start(&self) -> usize {
self.start
}
/// Returns the remaining input.
pub fn get_remaining(&self) -> &'a str {
self.remaining
}
/// Builds the final `Suggestions`, draining this builder.
pub fn drain_build(&mut self) -> Suggestions {
Suggestions::create(self.input, ::std::mem::take(&mut self.result))
}
/// Builds the final `Suggestions`, as a `Future`, draining this builder.
pub fn drain_build_future(
&mut self,
) -> Pin<Box<dyn Future<Output=Suggestions> + Send + 'a>> {
let input = self.input;
let result = ::std::mem::take(&mut self.result);
Box::pin(async move {
Suggestions::create(input, result)
})
}
/// Adds a new suggestion.
pub fn suggest(&mut self, text: String) -> &mut Self {
if text != self.remaining {
self.result.push({
Suggestion::new(self.start..self.input.len(), text)
});
}
self
}
/// Adds all the suggestions from the given SuggestionBuilder, draining it.
pub fn add_drain(&mut self, other: &mut SuggestionsBuilder) -> &mut Self {
self.result.extend(std::mem::take(&mut other.result));
self
}
/// Creates a *new* `SuggestionsBuilder`, with no suggestions, starting at
/// the given position.
pub fn starting_from(&self, start: usize) -> Self {
Self::new(self.input, start)
}
/// Creates a *new* `SuggestionsBuilder`, with no suggestions, starting from
/// the same position as this one.
pub fn restart(&self) -> Self {
self.starting_from(self.start)
}
}